diff --git a/frontend/components/chat/add-patient-dialog.tsx b/frontend/components/chat/add-patient-dialog.tsx
deleted file mode 100644
index fab5f2e..0000000
--- a/frontend/components/chat/add-patient-dialog.tsx
+++ /dev/null
@@ -1,403 +0,0 @@
-"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 (
-
- );
-}
-
-function SectionHeader({
- label,
- onAdd,
-}: {
- label: string;
- onAdd: () => void;
-}) {
- return (
-
-
- {label}
-
-
-
- );
-}
-
-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 (
-
- );
-}
diff --git a/frontend/components/chat/chat-input.tsx b/frontend/components/chat/chat-input.tsx
index efdeb74..327520a 100644
--- a/frontend/components/chat/chat-input.tsx
+++ b/frontend/components/chat/chat-input.tsx
@@ -23,7 +23,7 @@ import {
useState,
} from "react";
-import { AddPatientDialog } from "@/components/chat/add-patient-dialog";
+import { PatientFormDialog } from "@/components/chat/patient-form-dialog";
import {
DropdownMenu,
DropdownMenuContent,
@@ -343,8 +343,9 @@ export function ChatInput({ onSubmit, status, onStop }: ChatInputProps) {
- onSubmit(`/patient ${fileNumber}`)}
onOpenChange={setAddOpen}
open={addOpen}
diff --git a/frontend/components/chat/chat-panel.tsx b/frontend/components/chat/chat-panel.tsx
index 8e13bb8..bae5674 100644
--- a/frontend/components/chat/chat-panel.tsx
+++ b/frontend/components/chat/chat-panel.tsx
@@ -134,6 +134,17 @@ export function ChatPanel() {
+ setMessages((prev) =>
+ prev.map((m) =>
+ m.id === message.id &&
+ m.role === "assistant" &&
+ m.kind === "patient"
+ ? { ...m, patient: updated, status: "ready" }
+ : m
+ )
+ )
+ }
patient={message.patient}
status={message.status}
/>
diff --git a/frontend/components/chat/patient-cards.tsx b/frontend/components/chat/patient-cards.tsx
index f4ed9de..1270669 100644
--- a/frontend/components/chat/patient-cards.tsx
+++ b/frontend/components/chat/patient-cards.tsx
@@ -1,6 +1,7 @@
"use client";
-import type { ReactNode } from "react";
+import { Pencil } from "lucide-react";
+import { type ReactNode, useState } from "react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
@@ -21,6 +22,7 @@ import {
} from "@/components/ui/dialog";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
+import { PatientFormDialog } from "@/components/chat/patient-form-dialog";
import { Sparkline } from "@/components/chat/sparkline";
import { cn } from "@/lib/utils";
import type { AllergySeverity, LabFlag, Patient, Trend } from "@/lib/patients";
@@ -31,6 +33,7 @@ type PatientResultProps = {
status: "loading" | "ready" | "not-found";
fileNumber: string;
patient?: Patient;
+ onPatientUpdated?: (patient: Patient) => void;
};
const severityVariant: Record = {
@@ -183,7 +186,13 @@ function ExpandableCard({
);
}
-function SummaryCard({ patient }: { patient: Patient }) {
+function SummaryCard({
+ patient,
+ onEdit,
+}: {
+ patient: Patient;
+ onEdit?: () => void;
+}) {
const idLine = `${patient.age} · ${sexLabel[patient.sex]} · MRN ${patient.fileNumber}`;
return (
+
);
@@ -542,7 +562,16 @@ function LoadingCards() {
);
}
-export function PatientResult({ status, fileNumber, patient }: PatientResultProps) {
+export function PatientResult({
+ status,
+ fileNumber,
+ patient,
+ onPatientUpdated,
+}: PatientResultProps) {
+ const [editOpen, setEditOpen] = useState(false);
+ // Bumped on open so the editor remounts with the latest patient data.
+ const [editKey, setEditKey] = useState(0);
+
if (status === "not-found") {
return (
@@ -561,13 +590,27 @@ export function PatientResult({ status, fileNumber, patient }: PatientResultProp
) : (
<>
-
+ {
+ setEditKey((k) => k + 1);
+ setEditOpen(true);
+ }}
+ patient={patient}
+ />
+ onPatientUpdated?.(updated)}
+ open={editOpen}
+ patient={patient}
+ />
>
)}
diff --git a/frontend/components/chat/patient-form-dialog.tsx b/frontend/components/chat/patient-form-dialog.tsx
new file mode 100644
index 0000000..656f3c5
--- /dev/null
+++ b/frontend/components/chat/patient-form-dialog.tsx
@@ -0,0 +1,512 @@
+"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 LabFlag,
+ type Patient,
+} from "@/lib/patients";
+
+type PatientFormDialogProps = {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ mode: "create" | "edit";
+ patient?: Patient;
+ onCreated?: (fileNumber: string) => void;
+ onSaved?: (patient: Patient) => void;
+};
+
+type AllergyDraft = { substance: string; reaction: string; severity: AllergySeverity };
+type MedicationDraft = { name: string; dose: string; frequency: string };
+type ProblemDraft = { label: string; since: string };
+type LabDraft = { name: string; value: string; flag: LabFlag; takenAt: string };
+type VisitDraft = { type: string; date: string; provider: string; summary: 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 (
+
+ );
+}
+
+function SectionList({
+ label,
+ rows,
+ blank,
+ onChange,
+ render,
+}: {
+ label: string;
+ rows: T[];
+ blank: T;
+ onChange: (rows: T[]) => void;
+ render: (row: T, set: (patch: Partial) => void) => ReactNode;
+}) {
+ return (
+
+
+
+ {label}
+
+
+
+ {rows.map((row, index) => (
+
+ {render(row, (patch) =>
+ onChange(rows.map((r, i) => (i === index ? { ...r, ...patch } : r)))
+ )}
+
+
+ ))}
+
+ );
+}
+
+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 PatientFormDialog({
+ open,
+ onOpenChange,
+ mode,
+ patient,
+ onCreated,
+ onSaved,
+}: PatientFormDialogProps) {
+ const isEdit = mode === "edit";
+
+ const [fileNumber, setFileNumber] = useState(() =>
+ isEdit && patient ? patient.fileNumber : generateFileNumber()
+ );
+ const [name, setName] = useState(patient?.name ?? "");
+ const [age, setAge] = useState(patient ? String(patient.age) : "");
+ const [sex, setSex] = useState(patient?.sex ?? "F");
+ const [status, setStatus] = useState(
+ patient?.status ?? "active"
+ );
+ const [pcp, setPcp] = useState(patient?.pcp ?? "");
+ const [bp, setBp] = useState(patient?.vitals.bp ?? "");
+ const [hr, setHr] = useState(patient?.vitals.hr ?? "");
+ const [temp, setTemp] = useState(patient?.vitals.temp ?? "");
+ const [spo2, setSpo2] = useState(patient?.vitals.spo2 ?? "");
+ const [allergies, setAllergies] = useState(
+ () => patient?.allergies.map((a) => ({ ...a })) ?? []
+ );
+ const [medications, setMedications] = useState(
+ () => patient?.medications.map((m) => ({ ...m })) ?? []
+ );
+ const [problems, setProblems] = useState(
+ () => patient?.problems.map((p) => ({ ...p })) ?? []
+ );
+ const [labs, setLabs] = useState(
+ () => patient?.labs.map((l) => ({ ...l })) ?? []
+ );
+ const [visits, setVisits] = useState(
+ () =>
+ patient?.encounters.map((e) => ({
+ type: e.type,
+ date: e.date,
+ provider: e.provider,
+ summary: e.summary,
+ })) ?? []
+ );
+
+ const handleSubmit = (event: FormEvent) => {
+ event.preventDefault();
+ if (!name.trim()) {
+ return;
+ }
+
+ const built: 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: patient?.alerts ?? [],
+ medications: medications.filter((m) => m.name.trim()),
+ problems: problems.filter((p) => p.label.trim()),
+ vitals: {
+ bp: bp.trim() || "—",
+ hr: hr.trim() || "—",
+ temp: temp.trim() || "—",
+ spo2: spo2.trim() || "—",
+ takenAt: isEdit ? (patient?.vitals.takenAt ?? today()) : today(),
+ },
+ vitalsTrend: patient?.vitalsTrend ?? {
+ label: "Heart rate",
+ unit: "bpm",
+ points: [],
+ },
+ labs: labs
+ .filter((l) => l.name.trim())
+ .map((l) => ({ ...l, takenAt: l.takenAt.trim() || today() })),
+ labTrend: patient?.labTrend ?? { label: "—", unit: "", points: [] },
+ encounters: visits
+ .filter((v) => v.type.trim() || v.summary.trim())
+ .map((v) => ({
+ type: v.type.trim() || "Visit",
+ date: v.date.trim() || today(),
+ provider: v.provider,
+ summary: v.summary,
+ })),
+ };
+
+ addPatient(built);
+ if (isEdit) {
+ onSaved?.(built);
+ } else {
+ onCreated?.(fileNumber);
+ }
+ onOpenChange(false);
+ };
+
+ return (
+
+ );
+}