mirror of
https://github.com/temetro/temetro.git
synced 2026-08-08 09:43:13 +00:00
31d86bb5dd
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>
124 lines
4.2 KiB
TypeScript
124 lines
4.2 KiB
TypeScript
"use client";
|
|
|
|
import { ArrowDown, ArrowUp } from "lucide-react";
|
|
import { useMemo } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import { Area, AreaChart } from "@/components/charts/area-chart";
|
|
import { Grid } from "@/components/charts/grid";
|
|
import { ChartTooltip } from "@/components/charts/tooltip";
|
|
import { XAxis } from "@/components/charts/x-axis";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Card } from "@/components/ui/card";
|
|
import type { LabCardData } from "@/lib/ai-chat";
|
|
import type { LabFlag } from "@/lib/patients";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
type BadgeVariant = "default" | "secondary" | "destructive" | "outline";
|
|
|
|
const labFlagVariant: Record<LabFlag, BadgeVariant> = {
|
|
normal: "outline",
|
|
low: "secondary",
|
|
high: "secondary",
|
|
critical: "destructive",
|
|
};
|
|
|
|
// Plot the headline lab series. labTrend.points are most-recent-last numbers; we
|
|
// synthesise evenly spaced dates ending today purely so the date x-axis spaces
|
|
// the readings — the values are what matter.
|
|
function toChartData(points: number[]): { date: Date; value: number }[] {
|
|
const today = new Date();
|
|
return points.map((value, i) => {
|
|
const date = new Date(today);
|
|
date.setDate(today.getDate() - (points.length - 1 - i));
|
|
return { date, value };
|
|
});
|
|
}
|
|
|
|
// The flag of the most recent reading of the headline lab, for the corner badge.
|
|
function headlineFlag(data: LabCardData): LabFlag | null {
|
|
const matching = data.labs.filter((l) => l.name === data.labTrend.label);
|
|
const last = matching[matching.length - 1] ?? data.labs[data.labs.length - 1];
|
|
return last?.flag ?? null;
|
|
}
|
|
|
|
export function LabChartCard({ data }: { data: LabCardData }) {
|
|
const { t } = useTranslation();
|
|
const chartData = useMemo(
|
|
() => toChartData(data.labTrend.points),
|
|
[data.labTrend.points],
|
|
);
|
|
const flag = headlineFlag(data);
|
|
const latest = data.labTrend.points[data.labTrend.points.length - 1];
|
|
|
|
return (
|
|
<Card className="w-full gap-3 p-4">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<div className="space-y-0.5">
|
|
<span className="text-sm font-medium text-foreground">
|
|
{data.name}
|
|
</span>
|
|
<p className="text-xs text-muted-foreground">
|
|
{data.labTrend.label} · {data.labTrend.unit}
|
|
</p>
|
|
</div>
|
|
{/* High/low indicator, top-right corner. */}
|
|
{flag && flag !== "normal" ? (
|
|
<Badge className="gap-1" variant={labFlagVariant[flag]}>
|
|
{flag === "low" ? (
|
|
<ArrowDown className="size-3" />
|
|
) : (
|
|
<ArrowUp className="size-3" />
|
|
)}
|
|
{t(`chat.labCard.flags.${flag}`)}
|
|
</Badge>
|
|
) : (
|
|
<span className="font-semibold text-foreground tabular-nums">
|
|
{latest}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<AreaChart aspectRatio="2 / 1" data={chartData}>
|
|
<Grid horizontal />
|
|
<Area dataKey="value" fill="var(--chart-line-primary)" />
|
|
<XAxis tickMode="data" />
|
|
<ChartTooltip showDatePill={false} />
|
|
</AreaChart>
|
|
|
|
{data.labs.length > 0 ? (
|
|
<ul className="space-y-1.5">
|
|
{data.labs.slice(-6).map((lab, i) => (
|
|
<li
|
|
className="flex items-center justify-between gap-2 text-sm"
|
|
key={`${lab.name}-${i}`}
|
|
>
|
|
<span className="text-muted-foreground">{lab.name}</span>
|
|
<span className="flex items-center gap-2">
|
|
<span
|
|
className={cn(
|
|
"tabular-nums",
|
|
lab.flag === "critical"
|
|
? "text-destructive-foreground"
|
|
: "text-foreground",
|
|
)}
|
|
>
|
|
{lab.value}
|
|
</span>
|
|
{lab.flag !== "normal" ? (
|
|
<Badge
|
|
className="px-1.5 py-0 text-[11px]"
|
|
variant={labFlagVariant[lab.flag]}
|
|
>
|
|
{t(`chat.labCard.flags.${lab.flag}`)}
|
|
</Badge>
|
|
) : null}
|
|
</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
) : null}
|
|
</Card>
|
|
);
|
|
}
|