diff --git a/frontend/components/chat/chat-input.tsx b/frontend/components/chat/chat-input.tsx
index d1b9547..58d71b6 100644
--- a/frontend/components/chat/chat-input.tsx
+++ b/frontend/components/chat/chat-input.tsx
@@ -3,14 +3,14 @@
import type { ChatStatus } from "ai";
import {
ArrowUp,
+ Building2,
+ CalendarRange,
ChevronDown,
- FolderGit2,
- GitBranch,
Hand,
- Laptop,
Mic,
Plus,
Square,
+ Stethoscope,
} from "lucide-react";
import { type KeyboardEvent, useCallback, useState } from "react";
@@ -72,30 +72,30 @@ export function ChatInput({ onSubmit, status, onStop }: ChatInputProps) {
className="field-sizing-content block max-h-48 min-h-16 w-full resize-none bg-transparent px-5 pt-5 pb-2 text-base text-foreground outline-none placeholder:text-muted-foreground"
onChange={(event) => setValue(event.target.value)}
onKeyDown={handleKeyDown}
- placeholder="Do anything"
+ placeholder="Look up a patient — try /patient 10293"
rows={1}
value={value}
/>
-
- Custom
- High
+ Clinical
+ Detailed
-
+
-
- design-ai
+
+ Internal Medicine
-
- Work locally
+
+ Main Hospital
-
- main
+
+ Last 12 months
diff --git a/frontend/components/chat/chat-panel.tsx b/frontend/components/chat/chat-panel.tsx
index be9c225..e947f17 100644
--- a/frontend/components/chat/chat-panel.tsx
+++ b/frontend/components/chat/chat-panel.tsx
@@ -15,21 +15,31 @@ import {
MessageResponse,
} from "@/components/ai-elements/message";
import { ChatInput } from "@/components/chat/chat-input";
+import { PatientResult } from "@/components/chat/patient-cards";
+import { getPatient, type Patient } from "@/lib/patients";
-type ChatMessage = {
- id: string;
- role: "user" | "assistant";
- text: string;
-};
+type ChatMessage =
+ | { id: string; role: "user"; text: string }
+ | { id: string; role: "assistant"; kind: "text"; text: string }
+ | {
+ id: string;
+ role: "assistant";
+ kind: "patient";
+ fileNumber: string;
+ status: "loading" | "ready" | "not-found";
+ patient?: Patient;
+ };
-const HEADING = "What should we build in design-ai?";
+const HEADING = "Which patient would you like to look up?";
+
+// Strict trigger: only `/patient
` pulls up records.
+const PATIENT_COMMAND = /^\/patient\s+(\S+)$/i;
export function ChatPanel() {
const [messages, setMessages] = useState([]);
const [status, setStatus] = useState("ready");
- // UI-only: append the user's message and a mock assistant reply.
- const send = useCallback((text: string) => {
+ const send = useCallback(async (text: string) => {
const trimmed = text.trim();
if (!trimmed) {
return;
@@ -39,8 +49,46 @@ export function ChatPanel() {
...prev,
{ id: nanoid(), role: "user", text: trimmed },
]);
- setStatus("submitted");
+ const match = trimmed.match(PATIENT_COMMAND);
+
+ // Patient lookup: append a loading card set, then fill it in once the
+ // (mock) record comes back.
+ if (match) {
+ const fileNumber = match[1];
+ const resultId = nanoid();
+ setStatus("submitted");
+ setMessages((prev) => [
+ ...prev,
+ {
+ id: resultId,
+ role: "assistant",
+ kind: "patient",
+ fileNumber,
+ status: "loading",
+ },
+ ]);
+
+ const patient = await getPatient(fileNumber);
+ setMessages((prev) =>
+ prev.map((message) =>
+ message.id === resultId &&
+ message.role === "assistant" &&
+ message.kind === "patient"
+ ? {
+ ...message,
+ status: patient ? "ready" : "not-found",
+ patient: patient ?? undefined,
+ }
+ : message
+ )
+ );
+ setStatus("ready");
+ return;
+ }
+
+ // UI-only: mock assistant reply for anything that isn't a command.
+ setStatus("submitted");
window.setTimeout(() => {
setStatus("streaming");
setMessages((prev) => [
@@ -48,7 +96,8 @@ export function ChatPanel() {
{
id: nanoid(),
role: "assistant",
- text: `This is a **UI-only preview** of temetro. Connecting to your records and a model comes next.\n\nYou asked:\n\n> ${trimmed}`,
+ kind: "text",
+ text: `This is a **UI-only preview** of temetro. Try \`/patient 10293\` to pull up a patient's records.\n\nYou asked:\n\n> ${trimmed}`,
},
]);
window.setTimeout(() => setStatus("ready"), 250);
@@ -78,17 +127,33 @@ export function ChatPanel() {
- {messages.map((message) => (
-
-
- {message.role === "assistant" ? (
- {message.text}
- ) : (
- {message.text}
- )}
-
-
- ))}
+ {messages.map((message) => {
+ if (message.role === "assistant" && message.kind === "patient") {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ {message.role === "user" ? (
+ {message.text}
+ ) : (
+ {message.text}
+ )}
+
+
+ );
+ })}
diff --git a/frontend/components/chat/patient-cards.tsx b/frontend/components/chat/patient-cards.tsx
new file mode 100644
index 0000000..ae6ac98
--- /dev/null
+++ b/frontend/components/chat/patient-cards.tsx
@@ -0,0 +1,279 @@
+"use client";
+
+import { Avatar, AvatarFallback } from "@/components/ui/avatar";
+import { Badge } from "@/components/ui/badge";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Separator } from "@/components/ui/separator";
+import { Skeleton } from "@/components/ui/skeleton";
+import type { AllergySeverity, LabFlag, Patient } from "@/lib/patients";
+
+type BadgeVariant = "default" | "secondary" | "destructive" | "outline";
+
+type PatientResultProps = {
+ status: "loading" | "ready" | "not-found";
+ fileNumber: string;
+ patient?: Patient;
+};
+
+const severityVariant: Record
= {
+ mild: "outline",
+ moderate: "secondary",
+ severe: "destructive",
+};
+
+const labFlagVariant: Record = {
+ normal: "outline",
+ low: "secondary",
+ high: "secondary",
+ critical: "destructive",
+};
+
+const statusVariant: Record = {
+ active: "secondary",
+ inpatient: "destructive",
+ discharged: "outline",
+};
+
+const sexLabel: Record = { F: "Female", M: "Male" };
+
+function SectionLabel({ children }: { children: string }) {
+ return (
+
+ {children}
+
+ );
+}
+
+function SummaryCard({ patient }: { patient: Patient }) {
+ return (
+
+
+
+
+ {patient.initials}
+
+
+ {patient.name}
+
+ {patient.age} · {sexLabel[patient.sex]} · MRN {patient.fileNumber}
+
+
+
+ {patient.status}
+
+
+
+
+ Primary care:
+ {patient.pcp}
+
+
+ );
+}
+
+function AllergiesCard({ patient }: { patient: Patient }) {
+ return (
+
+
+ Allergies & alerts
+
+
+ {patient.alerts.length > 0 && (
+
+ {patient.alerts.map((alert) => (
+
+ {alert}
+
+ ))}
+
+ )}
+
+
Allergies
+ {patient.allergies.length === 0 ? (
+
No known allergies.
+ ) : (
+ patient.allergies.map((allergy) => (
+
+
+ {allergy.substance}
+
+ {" "}
+ — {allergy.reaction}
+
+
+
+ {allergy.severity}
+
+
+ ))
+ )}
+
+
+
+ );
+}
+
+function MedicationsCard({ patient }: { patient: Patient }) {
+ return (
+
+
+ Medications & problems
+
+
+
+
Active medications
+ {patient.medications.map((med) => (
+
+ {med.name}
+
+ {med.dose} · {med.frequency}
+
+
+ ))}
+
+
+
+
Problem list
+ {patient.problems.map((problem) => (
+
+ {problem.label}
+ since {problem.since}
+
+ ))}
+
+
+
+ );
+}
+
+function VitalsCard({ patient }: { patient: Patient }) {
+ const { vitals } = patient;
+ const vitalItems = [
+ { label: "BP", value: vitals.bp },
+ { label: "HR", value: vitals.hr },
+ { label: "Temp", value: vitals.temp },
+ { label: "SpO₂", value: vitals.spo2 },
+ ];
+
+ return (
+
+
+ Vitals, labs & visits
+ Vitals taken {vitals.takenAt}
+
+
+
+ {vitalItems.map((item) => (
+
+ {item.label}
+ {item.value}
+
+ ))}
+
+
+
+
Recent labs
+ {patient.labs.map((lab) => (
+
+
{lab.name}
+
+ {lab.value}
+
+ {lab.flag}
+
+
+
+ ))}
+
+
+
+
Recent visits
+ {patient.encounters.map((encounter) => (
+
+
+ {encounter.type}
+ {encounter.date}
+
+
{encounter.summary}
+
{encounter.provider}
+
+ ))}
+
+
+
+ );
+}
+
+function LoadingCards() {
+ return (
+ <>
+
+
+
+
+
+ {[0, 1, 2].map((card) => (
+
+
+
+
+
+ {[0, 1, 2].map((row) => (
+
+ ))}
+
+
+ ))}
+ >
+ );
+}
+
+export function PatientResult({ status, fileNumber, patient }: PatientResultProps) {
+ if (status === "not-found") {
+ return (
+
+
+
+ No patient found for file #{fileNumber}.
+
+
+
+ );
+ }
+
+ return (
+
+ {status === "loading" || !patient ? (
+
+ ) : (
+ <>
+
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/frontend/lib/patients.ts b/frontend/lib/patients.ts
new file mode 100644
index 0000000..a2fcc08
--- /dev/null
+++ b/frontend/lib/patients.ts
@@ -0,0 +1,228 @@
+// UI-only patient fixtures for the temetro chat demo.
+// There is no backend yet — `getPatient` is an async wrapper over a static
+// fixture so call sites already look like a real fetch and can be swapped for
+// an API call later. All data below is fictional sample data.
+
+export type AllergySeverity = "mild" | "moderate" | "severe";
+export type LabFlag = "normal" | "high" | "low" | "critical";
+
+export type Allergy = {
+ substance: string;
+ reaction: string;
+ severity: AllergySeverity;
+};
+
+export type Medication = {
+ name: string;
+ dose: string;
+ frequency: string;
+};
+
+export type Problem = {
+ label: string;
+ since: string;
+};
+
+export type Vitals = {
+ bp: string;
+ hr: string;
+ temp: string;
+ spo2: string;
+ takenAt: string;
+};
+
+export type Lab = {
+ name: string;
+ value: string;
+ flag: LabFlag;
+ takenAt: string;
+};
+
+export type Encounter = {
+ date: string;
+ type: string;
+ provider: string;
+ summary: string;
+};
+
+export type Patient = {
+ fileNumber: string; // MRN / file number, e.g. "10293"
+ name: string;
+ age: number;
+ sex: "M" | "F";
+ pcp: string; // primary care provider
+ status: "active" | "inpatient" | "discharged";
+ initials: string; // for AvatarFallback
+ allergies: Allergy[];
+ alerts: string[];
+ medications: Medication[];
+ problems: Problem[];
+ vitals: Vitals;
+ labs: Lab[];
+ encounters: Encounter[];
+};
+
+const PATIENTS: Record = {
+ "10293": {
+ fileNumber: "10293",
+ name: "Amina Hassan",
+ age: 64,
+ sex: "F",
+ pcp: "Dr. Lena Ortiz",
+ status: "active",
+ initials: "AH",
+ allergies: [
+ { substance: "Penicillin", reaction: "Anaphylaxis", severity: "severe" },
+ { substance: "Sulfa drugs", reaction: "Rash", severity: "moderate" },
+ ],
+ alerts: ["Anticoagulated", "Fall risk"],
+ medications: [
+ { name: "Metformin", dose: "1000 mg", frequency: "twice daily" },
+ { name: "Lisinopril", dose: "20 mg", frequency: "once daily" },
+ { name: "Apixaban", dose: "5 mg", frequency: "twice daily" },
+ { name: "Atorvastatin", dose: "40 mg", frequency: "at bedtime" },
+ ],
+ problems: [
+ { label: "Type 2 diabetes mellitus", since: "2014" },
+ { label: "Atrial fibrillation", since: "2019" },
+ { label: "Hypertension", since: "2011" },
+ { label: "Hyperlipidemia", since: "2015" },
+ ],
+ vitals: {
+ bp: "148/86 mmHg",
+ hr: "78 bpm",
+ temp: "37.0 °C",
+ spo2: "97%",
+ takenAt: "May 28, 2026",
+ },
+ labs: [
+ { name: "HbA1c", value: "8.2 %", flag: "high", takenAt: "May 20, 2026" },
+ { name: "eGFR", value: "52 mL/min", flag: "low", takenAt: "May 20, 2026" },
+ { name: "Potassium", value: "4.4 mmol/L", flag: "normal", takenAt: "May 20, 2026" },
+ { name: "INR", value: "2.6", flag: "normal", takenAt: "May 20, 2026" },
+ ],
+ encounters: [
+ {
+ date: "May 28, 2026",
+ type: "Clinic visit",
+ provider: "Dr. Lena Ortiz",
+ summary: "Diabetes follow-up; metformin dose increased.",
+ },
+ {
+ date: "Mar 12, 2026",
+ type: "Cardiology",
+ provider: "Dr. Raj Patel",
+ summary: "AF stable on apixaban; rate well controlled.",
+ },
+ {
+ date: "Jan 04, 2026",
+ type: "Emergency",
+ provider: "Dr. S. Cole",
+ summary: "Presented with palpitations; discharged same day.",
+ },
+ ],
+ },
+ "20481": {
+ fileNumber: "20481",
+ name: "Daniel Okoro",
+ age: 47,
+ sex: "M",
+ pcp: "Dr. Priya Nair",
+ status: "inpatient",
+ initials: "DO",
+ allergies: [
+ { substance: "Codeine", reaction: "Nausea", severity: "mild" },
+ ],
+ alerts: ["Isolation — MRSA"],
+ medications: [
+ { name: "Piperacillin-tazobactam", dose: "4.5 g", frequency: "every 8 hours" },
+ { name: "Enoxaparin", dose: "40 mg", frequency: "once daily" },
+ { name: "Pantoprazole", dose: "40 mg", frequency: "once daily" },
+ ],
+ problems: [
+ { label: "Community-acquired pneumonia", since: "2026" },
+ { label: "COPD", since: "2018" },
+ { label: "Former smoker", since: "2010" },
+ ],
+ vitals: {
+ bp: "112/70 mmHg",
+ hr: "94 bpm",
+ temp: "38.4 °C",
+ spo2: "92%",
+ takenAt: "May 31, 2026",
+ },
+ labs: [
+ { name: "WBC", value: "14.8 ×10⁹/L", flag: "high", takenAt: "May 31, 2026" },
+ { name: "CRP", value: "112 mg/L", flag: "critical", takenAt: "May 31, 2026" },
+ { name: "Lactate", value: "1.8 mmol/L", flag: "normal", takenAt: "May 31, 2026" },
+ { name: "Hemoglobin", value: "11.9 g/dL", flag: "low", takenAt: "May 31, 2026" },
+ ],
+ encounters: [
+ {
+ date: "May 30, 2026",
+ type: "Admission",
+ provider: "Dr. Priya Nair",
+ summary: "Admitted with febrile pneumonia; IV antibiotics started.",
+ },
+ {
+ date: "Feb 18, 2026",
+ type: "Pulmonology",
+ provider: "Dr. M. Adeyemi",
+ summary: "COPD review; inhaler technique reinforced.",
+ },
+ ],
+ },
+ "30574": {
+ fileNumber: "30574",
+ name: "Grace Liang",
+ age: 29,
+ sex: "F",
+ pcp: "Dr. Tom Becker",
+ status: "active",
+ initials: "GL",
+ allergies: [],
+ alerts: ["Pregnant — 2nd trimester"],
+ medications: [
+ { name: "Prenatal vitamins", dose: "1 tab", frequency: "once daily" },
+ { name: "Levothyroxine", dose: "75 mcg", frequency: "once daily" },
+ ],
+ problems: [
+ { label: "Pregnancy (G1P0, 22 weeks)", since: "2026" },
+ { label: "Hypothyroidism", since: "2021" },
+ ],
+ vitals: {
+ bp: "118/72 mmHg",
+ hr: "82 bpm",
+ temp: "36.8 °C",
+ spo2: "99%",
+ takenAt: "May 26, 2026",
+ },
+ labs: [
+ { name: "TSH", value: "2.1 mIU/L", flag: "normal", takenAt: "May 19, 2026" },
+ { name: "Hemoglobin", value: "11.2 g/dL", flag: "low", takenAt: "May 19, 2026" },
+ { name: "Fasting glucose", value: "84 mg/dL", flag: "normal", takenAt: "May 19, 2026" },
+ ],
+ encounters: [
+ {
+ date: "May 26, 2026",
+ type: "Antenatal visit",
+ provider: "Dr. Tom Becker",
+ summary: "Routine 22-week check; fetal growth on track.",
+ },
+ {
+ date: "Apr 14, 2026",
+ type: "Obstetrics",
+ provider: "Dr. Tom Becker",
+ summary: "Anatomy scan normal; thyroid dose unchanged.",
+ },
+ ],
+ },
+};
+
+export const SAMPLE_FILE_NUMBERS = Object.keys(PATIENTS);
+
+export async function getPatient(fileNumber: string): Promise {
+ // Simulate retrieval latency so the loading (skeleton) state is visible.
+ await new Promise((resolve) => setTimeout(resolve, 700));
+ return PATIENTS[fileNumber.trim()] ?? null;
+}