mirror of
https://github.com/temetro/temetro.git
synced 2026-08-21 15:36:47 +00:00
fa2e499440
Make the chat feel alive and let the agent act on the clinic — safely. UX (frontend): - Stream `data-step` parts from each tool into an inline Chain-of-Thought trace, plus a "Thinking…" shimmer while a request is in flight (works even on the non-streamed external+Veil path). - Replace the modal Veil consent Dialog with an inline, once-per-session "Veil" confirmation above the input (with a "Use local model" option). - Render new list + action-preview cards; chat-input now uses COSS tokens. Agent (backend): - Add display tools (listAppointments / listTasks / listPrescriptions) and propose tools (proposeAppointment / proposeTask / proposePrescription) that validate as a dry run and stream an approval card — nothing is written until the clinician approves, via the existing RBAC-gated create endpoints. - previewImport now also covers single-patient add + migration. - System prompt: display + add only, never edit/delete or alter the schema; stronger migration guidance. ToolContext carries the viewer for task scoping. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
123 lines
3.4 KiB
TypeScript
123 lines
3.4 KiB
TypeScript
"use client";
|
|
|
|
import { CalendarClock, ClipboardList, Pill } from "lucide-react";
|
|
import type { LucideIcon } from "lucide-react";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Card } from "@/components/ui/card";
|
|
import type { Appointment } from "@/lib/appointments";
|
|
import { formatPrescribedAt, type Prescription } from "@/lib/prescriptions";
|
|
import type { Task } from "@/lib/tasks";
|
|
|
|
type Row = { primary: string; secondary?: string; badge?: string };
|
|
|
|
function Shell({
|
|
icon: Icon,
|
|
title,
|
|
rows,
|
|
emptyKey,
|
|
}: {
|
|
icon: LucideIcon;
|
|
title: string;
|
|
rows: Row[];
|
|
emptyKey: string;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
return (
|
|
<Card className="w-full gap-0 overflow-hidden p-0">
|
|
<div className="flex items-center gap-2 border-b px-4 py-3">
|
|
<Icon className="size-4 text-muted-foreground" />
|
|
<span className="font-medium text-sm">{title}</span>
|
|
<Badge className="ml-auto" variant="secondary">
|
|
{rows.length}
|
|
</Badge>
|
|
</div>
|
|
{rows.length === 0 ? (
|
|
<p className="px-4 py-6 text-center text-muted-foreground text-sm">
|
|
{t(emptyKey)}
|
|
</p>
|
|
) : (
|
|
<div className="divide-y divide-border">
|
|
{rows.map((row, i) => (
|
|
<div className="flex items-center gap-3 px-4 py-2.5" key={row.primary + i}>
|
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
<span className="truncate font-medium text-foreground text-sm">
|
|
{row.primary}
|
|
</span>
|
|
{row.secondary ? (
|
|
<span className="truncate text-muted-foreground text-xs">
|
|
{row.secondary}
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
{row.badge ? (
|
|
<Badge className="shrink-0" variant="outline">
|
|
{row.badge}
|
|
</Badge>
|
|
) : null}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export function AppointmentListCard({ appointments }: { appointments: Appointment[] }) {
|
|
const { t } = useTranslation();
|
|
const rows: Row[] = appointments.map((a) => ({
|
|
primary: a.name,
|
|
secondary: [a.date, a.time, a.type, a.provider].filter(Boolean).join(" · "),
|
|
badge: a.status,
|
|
}));
|
|
return (
|
|
<Shell
|
|
emptyKey="chat.lists.noAppointments"
|
|
icon={CalendarClock}
|
|
rows={rows}
|
|
title={t("chat.lists.appointments")}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function TaskListCard({ tasks }: { tasks: Task[] }) {
|
|
const { t } = useTranslation();
|
|
const rows: Row[] = tasks.map((tk) => ({
|
|
primary: tk.title,
|
|
secondary: [tk.assignee, tk.due].filter(Boolean).join(" · "),
|
|
badge: tk.done ? t("chat.lists.done") : tk.priority,
|
|
}));
|
|
return (
|
|
<Shell
|
|
emptyKey="chat.lists.noTasks"
|
|
icon={ClipboardList}
|
|
rows={rows}
|
|
title={t("chat.lists.tasks")}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function PrescriptionListCard({
|
|
prescriptions,
|
|
}: {
|
|
prescriptions: Prescription[];
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const rows: Row[] = prescriptions.map((rx) => ({
|
|
primary: [rx.medication, rx.dose].filter(Boolean).join(" "),
|
|
secondary: [rx.name, rx.frequency, formatPrescribedAt(rx.prescribedAt)]
|
|
.filter(Boolean)
|
|
.join(" · "),
|
|
badge: rx.status,
|
|
}));
|
|
return (
|
|
<Shell
|
|
emptyKey="chat.lists.noPrescriptions"
|
|
icon={Pill}
|
|
rows={rows}
|
|
title={t("chat.lists.prescriptions")}
|
|
/>
|
|
);
|
|
}
|