diff --git a/frontend/components/chat/add-patient-dialog.tsx b/frontend/components/chat/add-patient-dialog.tsx
new file mode 100644
index 0000000..fab5f2e
--- /dev/null
+++ b/frontend/components/chat/add-patient-dialog.tsx
@@ -0,0 +1,403 @@
+"use client";
+
+import { Plus, RefreshCw, X } from "lucide-react";
+import { type FormEvent, type ReactNode, useState } from "react";
+
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { cn } from "@/lib/utils";
+import {
+ addPatient,
+ type AllergySeverity,
+ generateFileNumber,
+ type Patient,
+} from "@/lib/patients";
+
+type AddPatientDialogProps = {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onCreated: (fileNumber: string) => void;
+};
+
+type AllergyDraft = { substance: string; reaction: string; severity: AllergySeverity };
+type MedicationDraft = { name: string; dose: string; frequency: string };
+
+const controlClass =
+ "h-9 w-full rounded-3xl border border-transparent bg-input/50 px-3 text-sm text-foreground outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30";
+
+function Field({ label, children }: { label: string; children: ReactNode }) {
+ return (
+
+ {label}
+ {children}
+
+ );
+}
+
+function SectionHeader({
+ label,
+ onAdd,
+}: {
+ label: string;
+ onAdd: () => void;
+}) {
+ return (
+
+
+ {label}
+
+
+
+ Add
+
+
+ );
+}
+
+function initialsFromName(name: string): string {
+ const parts = name.trim().split(/\s+/).filter(Boolean);
+ if (parts.length === 0) {
+ return "?";
+ }
+ return parts
+ .slice(0, 2)
+ .map((part) => part[0])
+ .join("")
+ .toUpperCase();
+}
+
+const today = () =>
+ new Date().toLocaleDateString("en-US", {
+ month: "short",
+ day: "2-digit",
+ year: "numeric",
+ });
+
+export function AddPatientDialog({
+ open,
+ onOpenChange,
+ onCreated,
+}: AddPatientDialogProps) {
+ // Lazily generate the file number on mount. The dialog is remounted (via a
+ // `key`) each time it opens, so this gives a fresh number + cleared form
+ // without a reset effect.
+ const [fileNumber, setFileNumber] = useState(generateFileNumber);
+ const [name, setName] = useState("");
+ const [age, setAge] = useState("");
+ const [sex, setSex] = useState("F");
+ const [status, setStatus] = useState("active");
+ const [pcp, setPcp] = useState("");
+ const [bp, setBp] = useState("");
+ const [hr, setHr] = useState("");
+ const [temp, setTemp] = useState("");
+ const [spo2, setSpo2] = useState("");
+ const [allergies, setAllergies] = useState([]);
+ const [medications, setMedications] = useState([]);
+
+ const handleSubmit = (event: FormEvent) => {
+ event.preventDefault();
+ if (!name.trim()) {
+ return;
+ }
+
+ const patient: Patient = {
+ fileNumber,
+ name: name.trim(),
+ age: Number(age) || 0,
+ sex,
+ pcp: pcp.trim() || "—",
+ status,
+ initials: initialsFromName(name),
+ allergies: allergies.filter((a) => a.substance.trim()),
+ alerts: [],
+ medications: medications.filter((m) => m.name.trim()),
+ problems: [],
+ vitals: {
+ bp: bp.trim() || "—",
+ hr: hr.trim() || "—",
+ temp: temp.trim() || "—",
+ spo2: spo2.trim() || "—",
+ takenAt: today(),
+ },
+ vitalsTrend: { label: "Heart rate", unit: "bpm", points: [] },
+ labs: [],
+ labTrend: { label: "—", unit: "", points: [] },
+ encounters: [],
+ };
+
+ addPatient(patient);
+ onCreated(fileNumber);
+ onOpenChange(false);
+ };
+
+ return (
+
+
+
+ Add patient
+
+ Create a new chart. A file number has been generated for you.
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/components/chat/chat-input.tsx b/frontend/components/chat/chat-input.tsx
index e0a43da..efdeb74 100644
--- a/frontend/components/chat/chat-input.tsx
+++ b/frontend/components/chat/chat-input.tsx
@@ -11,6 +11,7 @@ import {
Plus,
Square,
Stethoscope,
+ UserPlus,
X,
} from "lucide-react";
import {
@@ -22,6 +23,7 @@ import {
useState,
} from "react";
+import { AddPatientDialog } from "@/components/chat/add-patient-dialog";
import {
DropdownMenu,
DropdownMenuContent,
@@ -133,6 +135,9 @@ export function ChatInput({ onSubmit, status, onStop }: ChatInputProps) {
const [specialty, setSpecialty] = useState("internal-medicine");
const [facility, setFacility] = useState("main-hospital");
const [timeRange, setTimeRange] = useState("12m");
+ const [addOpen, setAddOpen] = useState(false);
+ // Bumped on each open so the dialog remounts with a fresh file number + form.
+ const [addKey, setAddKey] = useState(0);
const fileInputRef = useRef(null);
const isGenerating = status === "submitted" || status === "streaming";
@@ -187,6 +192,7 @@ export function ChatInput({ onSubmit, status, onStop }: ChatInputProps) {
}, []);
return (
+ <>
+
+ onSubmit(`/patient ${fileNumber}`)}
+ onOpenChange={setAddOpen}
+ open={addOpen}
+ />
+ >
);
}
diff --git a/frontend/components/chat/patient-cards.tsx b/frontend/components/chat/patient-cards.tsx
index cd2bf7d..f4ed9de 100644
--- a/frontend/components/chat/patient-cards.tsx
+++ b/frontend/components/chat/patient-cards.tsx
@@ -85,7 +85,14 @@ function Row({ label, value }: { label: ReactNode; value: ReactNode }) {
);
}
+function Empty({ children }: { children: ReactNode }) {
+ return {children}
;
+}
+
function TrendBlock({ trend }: { trend: Trend }) {
+ if (trend.points.length === 0) {
+ return No trend data yet. ;
+ }
return (
@@ -101,6 +108,9 @@ function TrendBlock({ trend }: { trend: Trend }) {
}
function TrendDetail({ trend }: { trend: Trend }) {
+ if (trend.points.length === 0) {
+ return
No trend data yet. ;
+ }
const min = Math.min(...trend.points);
const max = Math.max(...trend.points);
return (
@@ -285,26 +295,30 @@ function LabsCard({ patient }: { patient: Patient }) {
-
- {patient.labs.map((lab) => (
-
-
-
{lab.name}
-
- {lab.takenAt}
-
+ patient.labs.length === 0 ? (
+
No labs on file.
+ ) : (
+
+
+ {patient.labs.map((lab) => (
+
+
+ {lab.name}
+
+ {lab.takenAt}
+
+
+ {labValue(lab.value, lab.flag)}
- {labValue(lab.value, lab.flag)}
-
- ))}
+ ))}
+
+
+
-
-
-
+ )
}
title="Labs"
>
@@ -313,34 +327,43 @@ function LabsCard({ patient }: { patient: Patient }) {
As of {patient.labs[0]?.takenAt ?? "—"}
-
- {patient.labs.map((lab) => (
-
- ))}
-
-
-
+ {patient.labs.length === 0 ? (
+ No labs on file.
+ ) : (
+ <>
+
+ {patient.labs.map((lab) => (
+
+ ))}
+
+
+
+ >
+ )}
);
}
function MedicationsCard({ patient }: { patient: Patient }) {
- const list = (
-
- {patient.medications.map((med) => (
-
- ))}
-
- );
+ const list =
+ patient.medications.length === 0 ? (
+
No active medications.
+ ) : (
+
+ {patient.medications.map((med) => (
+
+ ))}
+
+ );
return (
- {patient.problems.map((problem) => (
-
- ))}
-
- );
+ const list =
+ patient.problems.length === 0 ? (
+ No active problems.
+ ) : (
+
+ {patient.problems.map((problem) => (
+
+ ))}
+
+ );
return (
No visits yet.;
+ }
return (
{patient.encounters.map((encounter) => (
diff --git a/frontend/lib/patients.ts b/frontend/lib/patients.ts
index dad7553..2ef0c46 100644
--- a/frontend/lib/patients.ts
+++ b/frontend/lib/patients.ts
@@ -266,3 +266,18 @@ export async function getPatient(fileNumber: string): Promise
{
await new Promise((resolve) => setTimeout(resolve, 700));
return PATIENTS[fileNumber.trim()] ?? null;
}
+
+// Generate a unique 5-digit file number not already in the store.
+export function generateFileNumber(): string {
+ let candidate: string;
+ do {
+ candidate = String(10000 + Math.floor(Math.random() * 89999));
+ } while (candidate in PATIENTS);
+ return candidate;
+}
+
+// Add (or replace) a patient in the in-memory store. Session-only — no backend
+// yet, so this resets on reload. Swap point for a real "create patient" API.
+export function addPatient(patient: Patient): void {
+ PATIENTS[patient.fileNumber] = patient;
+}