(
+ () => ({
+ a({ href, children, ...props }) {
+ if (typeof href === "string" && href.startsWith(CITE_HREF_PREFIX)) {
+ const id = href.slice(CITE_HREF_PREFIX.length);
+ const num = numberById.get(id) ?? 0;
+ return ;
+ }
+ return (
+
+ {children}
+
+ );
+ },
+ }),
+ [numberById, sourceById],
+ );
+
+ return {processed};
+}
+
+// Provenance footer shown beneath a message that has sources — primarily a
+// fallback when the model didn't place inline markers, so retrieved records are
+// always attributed.
+export function SourcesFooter({ sources }: { sources: SourceData[] }) {
+ const { t } = useTranslation();
+ if (sources.length === 0) return null;
+ return (
+
+
+ {t("chat.sources.title")}
+
+ {sources.map((s, i) => (
+
+ {i + 1}
+ {s.title}
+
+ ))}
+
+ );
+}
diff --git a/frontend/lib/ai-chat.ts b/frontend/lib/ai-chat.ts
index ae8d4a7..f3a471d 100644
--- a/frontend/lib/ai-chat.ts
+++ b/frontend/lib/ai-chat.ts
@@ -43,6 +43,16 @@ export type StepData = {
status: "active" | "complete";
};
+// A citable source the agent retrieved (one per display-tool call). The model
+// references it inline via a [[src:id]] marker; the client renders that marker
+// as an inline citation chip and a sources footer. `title` is the real,
+// clinician-facing label; `id` is PHI-free (s1, s2, …).
+export type SourceData = {
+ id: string;
+ title: string;
+ kind: string;
+};
+
// Read-only list cards (display actions).
export type AppointmentListData = { appointments: Appointment[] };
export type TaskListData = { tasks: Task[] };
@@ -85,6 +95,7 @@ export type TemetroDataParts = {
importPreview: ImportPreviewData;
veilNotice: VeilNoticeData;
step: StepData;
+ source: SourceData;
appointmentList: AppointmentListData;
taskList: TaskListData;
prescriptionList: PrescriptionListData;
diff --git a/frontend/lib/i18n/locales/en/translation.json b/frontend/lib/i18n/locales/en/translation.json
index a9f2d18..f2e0660 100644
--- a/frontend/lib/i18n/locales/en/translation.json
+++ b/frontend/lib/i18n/locales/en/translation.json
@@ -947,6 +947,19 @@
"thinking": "Thinking…",
"steps": "Steps",
"reasoning": "Reasoning",
+ "sources": {
+ "title": "Sources",
+ "kind": {
+ "patient": "Patient record",
+ "lab": "Lab results",
+ "appointments": "Schedule",
+ "tasks": "Tasks",
+ "prescriptions": "Prescriptions",
+ "clinic": "Clinic",
+ "analytics": "Analytics",
+ "inventory": "Inventory"
+ }
+ },
"graphCard": {
"label": "Record graph"
},