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:
Khalid Abdi
2026-06-13 18:44:20 +03:00
parent ff37b555b0
commit 31d86bb5dd
9 changed files with 564 additions and 143 deletions
+19 -3
View File
@@ -55,7 +55,7 @@ export function ChatInput({
const canSend =
(value.trim().length > 0 || files.length > 0) && !isGenerating;
const submit = useCallback(() => {
const submit = useCallback(async () => {
const trimmed = value.trim();
if ((!trimmed && files.length === 0) || isGenerating) {
return;
@@ -64,8 +64,24 @@ export function ChatInput({
if (trimmed) {
parts.push(trimmed);
}
if (files.length > 0) {
parts.push(`[Attached: ${files.map((file) => file.name).join(", ")}]`);
// Include the text of attached files so the agent can parse them (e.g. a
// database export to import). Read text-like files; cap each so a huge file
// can't blow the context. Binary files are referenced by name only.
for (const file of files) {
const textLike =
/\.(csv|tsv|json|txt|md|xml|ndjson|tab)$/i.test(file.name) ||
file.type.startsWith("text/") ||
file.type === "application/json";
if (textLike) {
try {
const content = (await file.text()).slice(0, 200_000);
parts.push(`--- File: ${file.name} ---\n${content}`);
} catch {
parts.push(`[Attached: ${file.name}]`);
}
} else {
parts.push(`[Attached: ${file.name}]`);
}
}
onSubmit(parts.join("\n\n"));
setValue("");