diff --git a/backend/src/routes/chat.ts b/backend/src/routes/chat.ts index f49f19e..ceaf1bb 100644 --- a/backend/src/routes/chat.ts +++ b/backend/src/routes/chat.ts @@ -165,11 +165,12 @@ function systemPrompt( "tool, so keep your prose a brief summary rather than re-listing every field.", "", "Citations: every retrieval tool result includes a `sourceId` (e.g. \"s1\").", - "When you state a fact drawn from a tool result, append an inline citation", - "marker immediately after that statement, in the exact form [[src:ID]] using", - "the matching sourceId — e.g. \"BP is well controlled [[src:s1]].\" Cite only", - "facts grounded in tool results, place the marker right after the relevant", - "sentence, and never invent or guess a sourceId.", + "Cite **sparingly** — add at most ONE marker per paragraph, on the single most", + "important record-derived claim, in the exact form [[src:ID]] using the matching", + "sourceId (e.g. \"BP is well controlled this quarter [[src:s1]].\"). Do NOT cite", + "every sentence, do NOT cite individual list items (e.g. each allergy or", + "medication), and never repeat the same source more than once. Cite only facts", + "grounded in tool results, and never invent or guess a sourceId.", veilActive ? `Privacy: this conversation runs on an external provider (${providerLabel}). Patient identifiers are de-identified as tokens like [PATIENT_1] / [MRN_1]; refer to patients generically ("this patient") rather than repeating tokens.` : "", diff --git a/frontend/components/analysis/analysis-view.tsx b/frontend/components/analysis/analysis-view.tsx index 9906517..86205a4 100644 --- a/frontend/components/analysis/analysis-view.tsx +++ b/frontend/components/analysis/analysis-view.tsx @@ -28,18 +28,21 @@ import { cn } from "@/lib/utils"; // Time-range filter for the Overview header. Month-based series are sliced to // the trailing window; sub-month ranges (30d/today) fall back to the latest // point since the backend has no finer-grained series yet. -const RANGES = ["all", "12m", "3m", "30d", "today"] as const; +const RANGES = ["all", "3m", "30d", "today"] as const; type Range = (typeof RANGES)[number]; const RANGE_MONTHS: Record = { all: null, - "12m": 12, "3m": 3, "30d": 1, today: 1, }; +// Charts use monthly aggregates, so a sub-month window would collapse to a +// single point (which an area chart can't draw). Keep at least two trailing +// points so every range renders a valid mini-trend. function sliceMonths(arr: T[], range: Range): T[] { const months = RANGE_MONTHS[range]; - return months == null ? arr : arr.slice(-months); + if (months == null) return arr; + return arr.slice(-Math.max(months, 2)); } // The Overview sections the Customize popover can show/hide. @@ -128,7 +131,7 @@ export function AnalysisView() { const { t } = useTranslation(); const [data, setData] = useState(null); const [appointments, setAppointments] = useState([]); - const [range, setRange] = useState("30d"); + const [range, setRange] = useState("all"); const [visible, setVisible] = useState>({ visits: true, patients: true, diff --git a/frontend/components/chat/message-citations.tsx b/frontend/components/chat/message-citations.tsx index 1e62b38..ae1dccd 100644 --- a/frontend/components/chat/message-citations.tsx +++ b/frontend/components/chat/message-citations.tsx @@ -26,13 +26,18 @@ export function hasCitationMarkers(text: string): boolean { } // Rewrites [[src:id]] → [n](#cite-id) for known sources; strips unknown markers. +// Each source is cited at most once (its first occurrence) so an over-eager model +// that tags every word with the same source collapses to a single chip. function withCitationLinks( text: string, numberById: Map, ): string { + const seen = new Set(); return text.replace(CITATION_RE, (_match, id: string) => { const num = numberById.get(id); - return num ? `[${num}](${CITE_HREF_PREFIX}${id})` : ""; + if (!num || seen.has(id)) return ""; + seen.add(id); + return `[${num}](${CITE_HREF_PREFIX}${id})`; }); } diff --git a/frontend/components/chat/patient-cards.tsx b/frontend/components/chat/patient-cards.tsx index acfcfb6..6586e41 100644 --- a/frontend/components/chat/patient-cards.tsx +++ b/frontend/components/chat/patient-cards.tsx @@ -1,6 +1,6 @@ "use client"; -import { Pencil } from "lucide-react"; +import { ArrowRight, Pencil } from "lucide-react"; import { type ReactNode, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -61,9 +61,10 @@ const statusVariant: Record = { }; // Fixed width so the cards sit in a horizontal scroll row instead of squashing, -// plus a subtle clickable affordance (they open a detail dialog). +// plus a subtle clickable affordance (they open a detail dialog). Compact cards +// size to their own (short) content — see `items-start` in PatientResult. const rowCard = - "w-80 shrink-0 cursor-pointer text-left outline-none transition hover:ring-foreground/20 focus-visible:ring-2 focus-visible:ring-ring"; + "w-72 shrink-0 cursor-pointer gap-0 text-left outline-none transition hover:bg-accent/30 hover:ring-foreground/20 focus-visible:ring-2 focus-visible:ring-ring"; // COSS Card has no `size` variant; recreate the old compact ("sm") density by // tightening the inner section padding from p-6 → p-4 via data-slot selectors. @@ -100,30 +101,6 @@ function Empty({ children }: { children: ReactNode }) { return

{children}

; } -function TrendBlock({ trend }: { trend: Trend }) { - const { t } = useTranslation(); - if (trend.points.length === 0) { - return {t("patientCard.trend.empty")}; - } - return ( -
-
- - {t("patientCard.trend.last", { - label: trend.label, - count: trend.points.length, - })} - - - {trend.points.at(-1)} - {trend.unit} - -
- -
- ); -} - function TrendDetail({ trend }: { trend: Trend }) { const { t } = useTranslation(); if (trend.points.length === 0) { @@ -177,7 +154,8 @@ function AlertBadges({ alerts }: { alerts: string[] }) { ); } -// A card that previews `children` and opens a roomier dialog of `detail` on click. +// A compact card that previews `children` and opens a roomier dialog of `detail` +// on click. A muted "Click for more" footer signals the card is expandable. function ExpandableCard({ title, description, @@ -189,6 +167,7 @@ function ExpandableCard({ detail: ReactNode; children: ReactNode; }) { + const { t } = useTranslation(); return ( } > {children} +
+ {t("patientCard.clickForMore")} + +
@@ -243,6 +226,16 @@ function SummaryCard({ /> + {onEdit ? ( + + ) : null} } title={patient.name} @@ -259,34 +252,15 @@ function SummaryCard({ {statusLabel} - -
- - - - -
- - + + + ); @@ -327,10 +301,9 @@ function VitalsCard({ patient }: { patient: Patient }) { {t("patientCard.vitals.taken", { at: vitals.takenAt })} - - {vitalsGrid("gap-y-3")} - - + + + ); @@ -387,25 +360,6 @@ function LabsCard({ patient }: { patient: Patient }) { {t("patientCard.labs.asOf", { at: patient.labs[0]?.takenAt ?? "—" })} - - {patient.labs.length === 0 ? ( - {t("patientCard.labs.empty")} - ) : ( - <> -
- {patient.labs.map((lab) => ( - } - /> - ))} -
- - - - )} -
); } @@ -442,7 +396,6 @@ function MedicationsCard({ patient }: { patient: Patient }) { })} - {list} ); } @@ -477,7 +430,6 @@ function ProblemsCard({ patient }: { patient: Patient }) { {t("patientCard.problems.active", { count: patient.problems.length })} - {list} ); } @@ -528,10 +480,19 @@ function AllergiesCard({ patient }: { patient: Patient }) { > {t("patientCard.allergies.title")} + + {patient.allergies.length === 0 + ? t("patientCard.allergies.none") + : t("patientCard.allergies.count", { + count: patient.allergies.length, + })} + - - - + {patient.alerts.length > 0 ? ( + + + + ) : null} ); } @@ -576,10 +537,10 @@ function VisitsCard({ patient }: { patient: Patient }) { > {t("patientCard.visits.title")} + + {t("patientCard.visits.recent", { count: patient.encounters.length })} + - - - ); } diff --git a/frontend/components/sidebar-02/nav-main.tsx b/frontend/components/sidebar-02/nav-main.tsx index c683793..07f2383 100644 --- a/frontend/components/sidebar-02/nav-main.tsx +++ b/frontend/components/sidebar-02/nav-main.tsx @@ -31,10 +31,14 @@ export type Route = { // True when `link` matches the current path. "/" only matches exactly; any other // link matches itself and its nested routes (so a parent stays lit on subpages). +// Pass `exact` for sub-items so a parent link (e.g. /messages) doesn't also light +// the Inbox sub when a sibling sub (/messages/meetings) is the active page. function useIsActive() { const pathname = usePathname(); - return (link: string) => - link === "/" ? pathname === "/" : pathname === link || pathname.startsWith(`${link}/`); + return (link: string, exact = false) => { + if (link === "/" || exact) return pathname === link; + return pathname === link || pathname.startsWith(`${link}/`); + }; } export default function DashboardNavigation({ routes }: { routes: Route[] }) { @@ -90,7 +94,7 @@ export default function DashboardNavigation({ routes }: { routes: Route[] }) { } > {subRoute.title} diff --git a/frontend/lib/i18n/locales/en/translation.json b/frontend/lib/i18n/locales/en/translation.json index 1ecfc3d..96d0cb1 100644 --- a/frontend/lib/i18n/locales/en/translation.json +++ b/frontend/lib/i18n/locales/en/translation.json @@ -1206,6 +1206,7 @@ "notFound": "No patient found for file #{{number}}.", "overview": "Overview", "edit": "Edit", + "clickForMore": "Click for more", "sex": { "F": "Female", "M": "Male" @@ -1251,7 +1252,8 @@ "allergies": { "title": "Allergies & alerts", "sectionLabel": "Allergies", - "none": "No known allergies." + "none": "No known allergies.", + "count": "{{count}} recorded" }, "visits": { "title": "Recent visits",