mirror of
https://github.com/temetro/temetro.git
synced 2026-08-28 19:37:33 +00:00
frontend: wire chat to the real agent + lab charts + import approval
Replace the mock chat with the live backend agent: - chat-panel uses @ai-sdk/react useChat against /api/chat (credentials included), passing the selected model + effort per send. The `/patient <file#>` fast-path is kept as an instant client-side shortcut. - Renders the agent's streamed data parts: patientCard → record cards (PatientResult), labCard → new LabChartCard (visx area chart with a high/low flag badge in the top-right corner + recent values), importPreview → ImportPreviewCard (the human approval gate: review counts/issues, then commit via POST /api/ai/import — nothing is written until approved). - Veil consent: a one-time dialog before the first send to a cloud model, explaining that identifiers are de-identified before leaving the clinic. - Attached text files (csv/json/txt…) now include their content in the message so the agent can parse an export for import. Shared chat message/data-part types in lib/ai-chat.ts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import type { UIMessage } from "ai";
|
||||
|
||||
import type { Lab, Patient, Trend } from "@/lib/patients";
|
||||
|
||||
// Custom data parts the backend agent streams alongside its text. They carry
|
||||
// REAL (un-redacted) record data straight to the clinician's screen for rich
|
||||
// rendering — the model itself only ever sees Veil-redacted tool results.
|
||||
|
||||
export type LabCardData = {
|
||||
fileNumber: string;
|
||||
name: string;
|
||||
labs: Lab[];
|
||||
labTrend: Trend;
|
||||
};
|
||||
|
||||
export type ImportPreviewData = {
|
||||
// Validated, ready-to-commit records (server re-validates on commit).
|
||||
valid: unknown[];
|
||||
invalid: { index: number; errors: string[] }[];
|
||||
total: number;
|
||||
};
|
||||
|
||||
export type VeilNoticeData = {
|
||||
provider: string;
|
||||
level: string;
|
||||
};
|
||||
|
||||
// Maps each data part name → its payload. Part `type` strings are the key
|
||||
// prefixed with `data-` (e.g. `data-patientCard`), per the AI SDK convention.
|
||||
export type TemetroDataParts = {
|
||||
patientCard: Patient;
|
||||
labCard: LabCardData;
|
||||
importPreview: ImportPreviewData;
|
||||
veilNotice: VeilNoticeData;
|
||||
};
|
||||
|
||||
export type TemetroUIMessage = UIMessage<never, TemetroDataParts>;
|
||||
@@ -50,3 +50,14 @@ export async function testAiConnection(input: {
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
// Commit records the clinician approved in a chat import preview. The backend
|
||||
// re-validates and writes via the audited patient service.
|
||||
export async function commitImport(
|
||||
records: unknown[],
|
||||
): Promise<{ created: string[]; failed: { fileNumber?: string; error: string }[] }> {
|
||||
return apiFetch("/api/ai/import", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ records }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -640,6 +640,39 @@
|
||||
"gemini15Pro": "Google long-context model",
|
||||
"ollama": "Runs locally on your infrastructure"
|
||||
}
|
||||
},
|
||||
"patientNotFound": "I couldn't find a patient with file number {{fileNumber}}.",
|
||||
"consent": {
|
||||
"title": "Send to external AI provider?",
|
||||
"body": "This message will be processed by {{provider}}, an external provider.",
|
||||
"veilNote": "Veil de-identifies patient information (names, MRNs, providers) before it leaves your infrastructure, and restores it in the answer. Local Ollama models avoid this entirely.",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "De-identify & send"
|
||||
},
|
||||
"labCard": {
|
||||
"flags": {
|
||||
"low": "Low",
|
||||
"high": "High",
|
||||
"critical": "Critical",
|
||||
"normal": "Normal"
|
||||
}
|
||||
},
|
||||
"importCard": {
|
||||
"title": "Import patient records",
|
||||
"ready": "Ready to import",
|
||||
"skipped": "Skipped",
|
||||
"total": "Parsed",
|
||||
"row": "Row {{index}}",
|
||||
"approve": "Import {{count}} record(s)",
|
||||
"approve_one": "Import 1 record",
|
||||
"reject": "Discard",
|
||||
"importing": "Importing…",
|
||||
"rejectedNote": "Import discarded. Nothing was written.",
|
||||
"importedTitle": "Records imported",
|
||||
"importedBody": "Imported {{count}} patient record(s).",
|
||||
"failedCount": "{{count}} failed",
|
||||
"failedTitle": "Import failed",
|
||||
"failedBody": "The records could not be imported. Please try again."
|
||||
}
|
||||
},
|
||||
"patientCard": {
|
||||
|
||||
Reference in New Issue
Block a user