mirror of
https://github.com/temetro/temetro.git
synced 2026-07-27 20:28:57 +00:00
Add /patient lookup with patient-info cards, clinical chat input
Type `/patient <fileNumber>` to pull up a patient: a skeleton card set appears, then fills in with summary, allergies & alerts, medications & problems, and vitals/labs/visits — composed from existing Card/Badge/ Avatar/Separator/Skeleton primitives (no new design). - lib/patients.ts: typed mock fixture + async getPatient() (swap point for a real API later). - components/chat/patient-cards.tsx: PatientResult (loading/ready/ not-found). - chat-panel.tsx: discriminated-union message model, strict /patient parse, async lookup with update-by-id, clinical heading. - chat-input.tsx: relabel leftover coding-assistant controls to clinical copy + clinical icons; layout/styling unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between gap-2 px-3 pb-3">
|
||||
<div className="flex min-w-0 items-center gap-1">
|
||||
<button aria-label="Add context" className={iconButton} type="button">
|
||||
<button aria-label="Attach file" className={iconButton} type="button">
|
||||
<Plus className="size-[18px]" />
|
||||
</button>
|
||||
<button className={pillButton} type="button">
|
||||
<Hand className="size-4" />
|
||||
<span className="truncate">Default permissions</span>
|
||||
<span className="truncate">Standard access</span>
|
||||
<ChevronDown className="size-4 opacity-70" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<button className={cn(pillButton, "mr-1")} type="button">
|
||||
<span className="font-medium text-foreground">Custom</span>
|
||||
<span>High</span>
|
||||
<span className="font-medium text-foreground">Clinical</span>
|
||||
<span>Detailed</span>
|
||||
<ChevronDown className="size-4 opacity-70" />
|
||||
</button>
|
||||
<button aria-label="Voice input" className={iconButton} type="button">
|
||||
<button aria-label="Dictate" className={iconButton} type="button">
|
||||
<Mic className="size-[18px]" />
|
||||
</button>
|
||||
<button
|
||||
@@ -123,18 +123,18 @@ export function ChatInput({ onSubmit, status, onStop }: ChatInputProps) {
|
||||
{/* Bottom (darker) card peeking out below, more rounded: context selectors */}
|
||||
<div className="flex flex-wrap items-center gap-1 px-3 pt-2.5 pb-3">
|
||||
<button className={contextPill} type="button">
|
||||
<FolderGit2 className="size-4" />
|
||||
<span>design-ai</span>
|
||||
<Stethoscope className="size-4" />
|
||||
<span>Internal Medicine</span>
|
||||
<ChevronDown className="size-3.5 opacity-70" />
|
||||
</button>
|
||||
<button className={contextPill} type="button">
|
||||
<Laptop className="size-4" />
|
||||
<span>Work locally</span>
|
||||
<Building2 className="size-4" />
|
||||
<span>Main Hospital</span>
|
||||
<ChevronDown className="size-3.5 opacity-70" />
|
||||
</button>
|
||||
<button className={contextPill} type="button">
|
||||
<GitBranch className="size-4" />
|
||||
<span>main</span>
|
||||
<CalendarRange className="size-4" />
|
||||
<span>Last 12 months</span>
|
||||
<ChevronDown className="size-3.5 opacity-70" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -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 <fileNumber>` pulls up records.
|
||||
const PATIENT_COMMAND = /^\/patient\s+(\S+)$/i;
|
||||
|
||||
export function ChatPanel() {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [status, setStatus] = useState<ChatStatus>("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() {
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<Conversation>
|
||||
<ConversationContent className="mx-auto w-full max-w-3xl">
|
||||
{messages.map((message) => (
|
||||
<Message from={message.role} key={message.id}>
|
||||
<MessageContent>
|
||||
{message.role === "assistant" ? (
|
||||
<MessageResponse>{message.text}</MessageResponse>
|
||||
) : (
|
||||
<span className="whitespace-pre-wrap">{message.text}</span>
|
||||
)}
|
||||
</MessageContent>
|
||||
</Message>
|
||||
))}
|
||||
{messages.map((message) => {
|
||||
if (message.role === "assistant" && message.kind === "patient") {
|
||||
return (
|
||||
<Message from="assistant" key={message.id}>
|
||||
<MessageContent className="w-full">
|
||||
<PatientResult
|
||||
fileNumber={message.fileNumber}
|
||||
patient={message.patient}
|
||||
status={message.status}
|
||||
/>
|
||||
</MessageContent>
|
||||
</Message>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Message from={message.role} key={message.id}>
|
||||
<MessageContent>
|
||||
{message.role === "user" ? (
|
||||
<span className="whitespace-pre-wrap">{message.text}</span>
|
||||
) : (
|
||||
<MessageResponse>{message.text}</MessageResponse>
|
||||
)}
|
||||
</MessageContent>
|
||||
</Message>
|
||||
);
|
||||
})}
|
||||
</ConversationContent>
|
||||
<ConversationScrollButton />
|
||||
</Conversation>
|
||||
|
||||
@@ -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<AllergySeverity, BadgeVariant> = {
|
||||
mild: "outline",
|
||||
moderate: "secondary",
|
||||
severe: "destructive",
|
||||
};
|
||||
|
||||
const labFlagVariant: Record<LabFlag, BadgeVariant> = {
|
||||
normal: "outline",
|
||||
low: "secondary",
|
||||
high: "secondary",
|
||||
critical: "destructive",
|
||||
};
|
||||
|
||||
const statusVariant: Record<Patient["status"], BadgeVariant> = {
|
||||
active: "secondary",
|
||||
inpatient: "destructive",
|
||||
discharged: "outline",
|
||||
};
|
||||
|
||||
const sexLabel: Record<Patient["sex"], string> = { F: "Female", M: "Male" };
|
||||
|
||||
function SectionLabel({ children }: { children: string }) {
|
||||
return (
|
||||
<p className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function SummaryCard({ patient }: { patient: Patient }) {
|
||||
return (
|
||||
<Card size="sm">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar className="size-10">
|
||||
<AvatarFallback>{patient.initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 gap-0.5">
|
||||
<CardTitle>{patient.name}</CardTitle>
|
||||
<CardDescription>
|
||||
{patient.age} · {sexLabel[patient.sex]} · MRN {patient.fileNumber}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge className="capitalize" variant={statusVariant[patient.status]}>
|
||||
{patient.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<span className="text-muted-foreground">Primary care: </span>
|
||||
<span className="text-foreground">{patient.pcp}</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function AllergiesCard({ patient }: { patient: Patient }) {
|
||||
return (
|
||||
<Card size="sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Allergies & alerts</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{patient.alerts.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{patient.alerts.map((alert) => (
|
||||
<Badge key={alert} variant="outline">
|
||||
{alert}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
<SectionLabel>Allergies</SectionLabel>
|
||||
{patient.allergies.length === 0 ? (
|
||||
<p className="text-muted-foreground">No known allergies.</p>
|
||||
) : (
|
||||
patient.allergies.map((allergy) => (
|
||||
<div
|
||||
className="flex items-center justify-between gap-3"
|
||||
key={allergy.substance}
|
||||
>
|
||||
<span className="text-foreground">
|
||||
{allergy.substance}
|
||||
<span className="text-muted-foreground">
|
||||
{" "}
|
||||
— {allergy.reaction}
|
||||
</span>
|
||||
</span>
|
||||
<Badge
|
||||
className="capitalize"
|
||||
variant={severityVariant[allergy.severity]}
|
||||
>
|
||||
{allergy.severity}
|
||||
</Badge>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function MedicationsCard({ patient }: { patient: Patient }) {
|
||||
return (
|
||||
<Card size="sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Medications & problems</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<SectionLabel>Active medications</SectionLabel>
|
||||
{patient.medications.map((med) => (
|
||||
<div className="flex items-baseline justify-between gap-3" key={med.name}>
|
||||
<span className="text-foreground">{med.name}</span>
|
||||
<span className="text-muted-foreground">
|
||||
{med.dose} · {med.frequency}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex flex-col gap-2">
|
||||
<SectionLabel>Problem list</SectionLabel>
|
||||
{patient.problems.map((problem) => (
|
||||
<div
|
||||
className="flex items-baseline justify-between gap-3"
|
||||
key={problem.label}
|
||||
>
|
||||
<span className="text-foreground">{problem.label}</span>
|
||||
<span className="text-muted-foreground">since {problem.since}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Card size="sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Vitals, labs & visits</CardTitle>
|
||||
<CardDescription>Vitals taken {vitals.takenAt}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||
{vitalItems.map((item) => (
|
||||
<div className="flex flex-col gap-0.5" key={item.label}>
|
||||
<span className="text-xs text-muted-foreground">{item.label}</span>
|
||||
<span className="text-foreground">{item.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex flex-col gap-2">
|
||||
<SectionLabel>Recent labs</SectionLabel>
|
||||
{patient.labs.map((lab) => (
|
||||
<div className="flex items-center justify-between gap-3" key={lab.name}>
|
||||
<span className="text-foreground">{lab.name}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground">{lab.value}</span>
|
||||
<Badge className="capitalize" variant={labFlagVariant[lab.flag]}>
|
||||
{lab.flag}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex flex-col gap-3">
|
||||
<SectionLabel>Recent visits</SectionLabel>
|
||||
{patient.encounters.map((encounter) => (
|
||||
<div className="flex flex-col gap-0.5" key={encounter.date + encounter.type}>
|
||||
<div className="flex items-baseline justify-between gap-3">
|
||||
<span className="text-foreground">{encounter.type}</span>
|
||||
<span className="text-xs text-muted-foreground">{encounter.date}</span>
|
||||
</div>
|
||||
<span className="text-muted-foreground">{encounter.summary}</span>
|
||||
<span className="text-xs text-muted-foreground">{encounter.provider}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function LoadingCards() {
|
||||
return (
|
||||
<>
|
||||
<Card size="sm">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="size-10 rounded-full" />
|
||||
<div className="grid flex-1 gap-1.5">
|
||||
<Skeleton className="h-4 w-40" />
|
||||
<Skeleton className="h-3 w-56" />
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
{[0, 1, 2].map((card) => (
|
||||
<Card key={card} size="sm">
|
||||
<CardHeader>
|
||||
<Skeleton className="h-4 w-44" />
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-2.5">
|
||||
{[0, 1, 2].map((row) => (
|
||||
<Skeleton className="h-3.5 w-full" key={row} />
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function PatientResult({ status, fileNumber, patient }: PatientResultProps) {
|
||||
if (status === "not-found") {
|
||||
return (
|
||||
<Card size="sm">
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground">
|
||||
No patient found for file #{fileNumber}.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
{status === "loading" || !patient ? (
|
||||
<LoadingCards />
|
||||
) : (
|
||||
<>
|
||||
<SummaryCard patient={patient} />
|
||||
<AllergiesCard patient={patient} />
|
||||
<MedicationsCard patient={patient} />
|
||||
<VitalsCard patient={patient} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<string, Patient> = {
|
||||
"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<Patient | null> {
|
||||
// Simulate retrieval latency so the loading (skeleton) state is visible.
|
||||
await new Promise((resolve) => setTimeout(resolve, 700));
|
||||
return PATIENTS[fileNumber.trim()] ?? null;
|
||||
}
|
||||
Reference in New Issue
Block a user