From d30c310336f6c36d3cf156bd76073bbf0e89c40e Mon Sep 17 00:00:00 2001 From: Khalid Abdi Date: Wed, 17 Jun 2026 19:30:12 +0300 Subject: [PATCH] feat(chat): replace model picker with situation modes (Chat/Analysis/Graph) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The composer's SELECT is now a clinician-facing mode picker instead of a list of LLM vendors (the model still comes from Settings → AI and continues to drive the Veil consent gate). The mode travels with each send: - backend appends an Analysis/Graph directive to the agent's system prompt - Graph mode renders an Obsidian-style RecordGraph for the /patient fast-path (new data-recordGraph message part reusing the shared graph component) Co-Authored-By: Claude Opus 4.8 --- backend/src/routes/chat.ts | 24 +++++- frontend/components/chat/chat-input.tsx | 24 +++--- frontend/components/chat/chat-panel.tsx | 42 ++++++++-- frontend/components/chat/mode-picker.tsx | 77 +++++++++++++++++++ frontend/lib/ai-chat.ts | 3 + frontend/lib/chat-modes.ts | 44 +++++++++++ frontend/lib/i18n/locales/en/translation.json | 18 +++++ 7 files changed, 207 insertions(+), 25 deletions(-) create mode 100644 frontend/components/chat/mode-picker.tsx create mode 100644 frontend/lib/chat-modes.ts diff --git a/backend/src/routes/chat.ts b/backend/src/routes/chat.ts index cb47fe7..c172db5 100644 --- a/backend/src/routes/chat.ts +++ b/backend/src/routes/chat.ts @@ -78,7 +78,23 @@ function inlineTextFiles(messages: UIMessage[]): UIMessage[] { }); } -function systemPrompt(veilActive: boolean, providerLabel: string): string { +// A short directive for the clinician's chosen "situation" mode, appended to the +// base prompt. Chat mode adds nothing. +function modeDirective(mode: string | undefined): string { + if (mode === "analysis") { + return "Mode — Analysis: the clinician wants interpretation, not just retrieval. After fetching a patient's data, surface patterns and correlations across their problems, labs and visits (e.g. recurring complaints, trends, likely links) and call out anything notable. Stay grounded in the tool results."; + } + if (mode === "graph") { + return "Mode — Graph: the clinician wants to see how a patient's problems and visits connect. When they reference a patient, call getPatient (its record graph renders automatically) and briefly describe the key relationships between illnesses and encounters."; + } + return ""; +} + +function systemPrompt( + veilActive: boolean, + providerLabel: string, + mode?: string, +): string { return [ "You are temetro, a clinical assistant that helps clinicians retrieve,", "organize, and add patient information. You operate over a real patient", @@ -130,6 +146,7 @@ function systemPrompt(veilActive: boolean, providerLabel: string): string { 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.` : "", + modeDirective(mode), ] .filter(Boolean) .join("\n"); @@ -137,10 +154,11 @@ function systemPrompt(veilActive: boolean, providerLabel: string): string { chatRouter.post("/", async (req, res, next) => { try { - const { messages, model: requestedModel } = req.body as { + const { messages, model: requestedModel, mode } = req.body as { messages: UIMessage[]; model?: string; effort?: string; + mode?: string; }; if (!Array.isArray(messages)) { res.status(400).json({ error: "messages must be an array." }); @@ -171,7 +189,7 @@ chatRouter.post("/", async (req, res, next) => { }; const modelMessages = await convertToModelMessages(inlineTextFiles(messages)); - const system = systemPrompt(veil.active, resolved.providerLabel); + const system = systemPrompt(veil.active, resolved.providerLabel, mode); const stream = createUIMessageStream({ execute: async ({ writer }) => { diff --git a/frontend/components/chat/chat-input.tsx b/frontend/components/chat/chat-input.tsx index 1fcc16f..fa5071a 100644 --- a/frontend/components/chat/chat-input.tsx +++ b/frontend/components/chat/chat-input.tsx @@ -11,19 +11,17 @@ import { } from "react"; import { useTranslation } from "react-i18next"; -import { ModelPicker } from "@/components/chat/model-picker"; +import { ModePicker } from "@/components/chat/mode-picker"; import { PatientFormDialog } from "@/components/chat/patient-form-dialog"; -import type { Effort } from "@/lib/ai-models"; +import type { ChatMode } from "@/lib/chat-modes"; import { cn } from "@/lib/utils"; type ChatInputProps = { onSubmit: (text: string, files: File[]) => void; status: ChatStatus; onStop?: () => void; - model: string; - effort: Effort; - onModelChange: (model: string) => void; - onEffortChange: (effort: Effort) => void; + mode: ChatMode; + onModeChange: (mode: ChatMode) => void; }; const iconButton = @@ -37,10 +35,8 @@ export function ChatInput({ onSubmit, status, onStop, - model, - effort, - onModelChange, - onEffortChange, + mode, + onModeChange, }: ChatInputProps) { const { t } = useTranslation(); @@ -181,11 +177,9 @@ export function ChatInput({
-