"use client"; import { AlertTriangle, CalendarPlus, Check, ClipboardList, Pill, Receipt, X, } from "lucide-react"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import type { ActionPreviewData } from "@/lib/ai-chat"; import { type AppointmentInput, createAppointment } from "@/lib/appointments"; import { createInvoice, formatMoney, type InvoiceInput, type InvoiceLineItem, } from "@/lib/invoices"; import { type PrescriptionInput, createPrescription } from "@/lib/prescriptions"; import { type TaskInput, createTask } from "@/lib/tasks"; import { notify } from "@/lib/toast"; type Status = "pending" | "committing" | "done" | "rejected"; export const ACTION_ICONS = { appointment: CalendarPlus, task: ClipboardList, prescription: Pill, invoice: Receipt, } as const; const ICONS = ACTION_ICONS; // Summarise the proposed record into a couple of readable lines per kind. export function summarize(data: ActionPreviewData): string[] { const r = data.record as Record; if (data.kind === "appointment") { return [ String(r.name ?? ""), [r.date, r.time].filter(Boolean).join(" · "), [r.type, r.provider].filter(Boolean).join(" · "), ].filter(Boolean); } if (data.kind === "task") { return [ String(r.title ?? ""), [r.assignee, r.due, r.priority].filter(Boolean).join(" · "), ].filter(Boolean); } if (data.kind === "invoice") { const items = (r.lineItems as InvoiceLineItem[] | undefined) ?? []; const total = items.reduce((s, li) => s + li.quantity * li.unitPrice, 0); return [ String(r.name ?? ""), `${items.length} item${items.length === 1 ? "" : "s"} · ${formatMoney(total)}`, ].filter(Boolean); } // prescription return [ [r.medication, r.dose].filter(Boolean).join(" "), [r.frequency, r.duration].filter(Boolean).join(" · "), String(r.name ?? ""), ].filter(Boolean); } export async function commitAction(data: ActionPreviewData): Promise { // Stamp provenance so the committed record is flagged "Added by AI" and shows // up for review/editing on the relevant page. if (data.kind === "appointment") { await createAppointment({ ...(data.record as AppointmentInput), source: "ai", }); } else if (data.kind === "task") { await createTask(data.record as TaskInput); } else if (data.kind === "invoice") { await createInvoice({ ...(data.record as InvoiceInput), source: "ai" }); } else { await createPrescription({ ...(data.record as PrescriptionInput), source: "ai", }); } } // The human approval gate for an agent-proposed add. The agent drafts the record // (dry run, nothing written); the clinician reviews it here and must approve // before it is committed via the matching RBAC-gated create endpoint. export function ActionPreviewCard({ data }: { data: ActionPreviewData }) { const { t } = useTranslation(); const [status, setStatus] = useState("pending"); const Icon = ICONS[data.kind]; const hasIssues = (data.issues?.length ?? 0) > 0; const lines = summarize(data); const approve = async () => { setStatus("committing"); try { await commitAction(data); setStatus("done"); notify.success( t("chat.actionCard.addedTitle"), t(`chat.actionCard.kind.${data.kind}`), ); } catch { setStatus("pending"); notify.error( t("chat.actionCard.failedTitle"), t("chat.actionCard.failedBody"), ); } }; return (
{t(`chat.actionCard.title.${data.kind}`)}
{lines.map((line, i) => (

{line}

))}
{hasIssues ? (
    {data.issues!.slice(0, 5).map((issue) => (
  • {issue}
  • ))}
) : null} {status === "done" ? (

{t("chat.actionCard.added")}

) : status === "rejected" ? (

{t("chat.actionCard.discarded")}

) : (
)}
); }