// ============================================================ // PHYSICAL EXAM GUIDE — full React port. // // Renders: // • Age-group + system pills (6 × 4 = 24 combinations) // • System overview banner // • CV system extras: APTM legend, cardiac sounds, innocent murmurs // • Resp system extras: respiratory sounds library // • Collapsible grading-scales reference (system-scoped) // • Component checklist with per-step normal / abnormal / (unset) // toggle, abnormal-hints hint list, pearl + significance callouts // • Patient age / gender + model inputs // • Generate Exam Report → POST /api/generate-pe-narrative // // PE_DATA is the full hierarchy ported verbatim from vanilla // peGuide.js (see client/src/data/pe-data.ts). Clinical reference // libraries (scales, APTM, sound files) live in pe-guide.ts. // ============================================================ import { useMemo, useState } from 'react'; import { useMutation } from '@tanstack/react-query'; import { api } from '@/lib/api'; import type { PeNarrativeOk } from '@/shared/types'; import { PE_DATA, AGE_GROUP_ORDER, SYSTEM_ORDER, SYSTEM_LABELS, type PeComponent, type PeStep, } from '@/data/pe-data'; import { SCALES, SYSTEM_SCALES, APTM_LEGEND, INNOCENT_MURMURS, RESP_SOUNDS, CARDIAC_SOUNDS, type ScaleDef, type SoundEntry, } from '@/data/pe-guide'; const card = 'rounded-lg border border-border bg-card p-5 space-y-3'; const pill = 'px-3 py-1.5 rounded-full text-xs font-medium border transition-colors cursor-pointer'; const btnPrimary = 'rounded-md bg-primary text-primary-foreground px-4 py-2 text-sm font-medium disabled:opacity-50'; const btnGhost = 'rounded-md border border-border bg-background px-3 py-2 text-sm font-medium hover:bg-muted disabled:opacity-50'; const input = 'w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-ring'; type StepStatus = 'normal' | 'abnormal' | null; // Key used to identify a step in the status map across age-group / system. function stepKey(age: string, sys: string, componentIdx: number, stepIdx: number) { return `${age}/${sys}/${componentIdx}/${stepIdx}`; } function ScaleCard({ id, scale }: { id: string; scale: ScaleDef }) { return (

{scale.title}

{scale.rows.map(([labelText, desc], i) => ( ))}
{labelText} {desc}
); } function SoundCard({ entry }: { entry: SoundEntry }) { return (
{entry.title}
Where: {entry.where}
{entry.rate &&
Rate: {entry.rate}
}
Features: {entry.features}
Clinical: {entry.clinical}
); } function StepRow({ step, status, onStatus, }: { step: PeStep; status: StepStatus; onStatus: (next: StepStatus) => void; }) { const base = 'text-xs font-medium px-2 py-1 rounded border'; return (
{step.label}
Method: {step.method}
Normal: {step.normal}
); } function ComponentCard({ age, sys, idx, comp, getStatus, setStatus, }: { age: string; sys: string; idx: number; comp: PeComponent; getStatus: (k: string) => StepStatus; setStatus: (k: string, next: StepStatus) => void; }) { return (

{comp.name}

{comp.steps.map((step, si) => { const k = stepKey(age, sys, idx, si); return ( setStatus(k, next)} /> ); })}
{comp.abnormalHints.length > 0 && (
Watch for
)} {comp.pearl && (
Pearl: {comp.pearl}
)} {comp.significance && (
Significance: {comp.significance}
)}
); } export default function PeGuide() { const [age, setAge] = useState<(typeof AGE_GROUP_ORDER)[number]>('toddler'); const [sys, setSys] = useState<(typeof SYSTEM_ORDER)[number]>('msk'); const [patientAge, setPatientAge] = useState(''); const [patientGender, setPatientGender] = useState(''); const [format, setFormat] = useState<'narrative' | 'list'>('narrative'); const [statusMap, setStatusMap] = useState>({}); const [narrative, setNarrative] = useState(null); const group = PE_DATA[age]; const section = group[sys]; const generate = useMutation({ mutationFn: (body: unknown) => api.post('/api/generate-pe-narrative', body), onSuccess: (data) => setNarrative(data.narrative), onError: (e: Error) => setNarrative('Generation failed: ' + e.message), }); const summary = useMemo(() => { let normal = 0, abnormal = 0, notAssessed = 0; section.components.forEach((c, ci) => c.steps.forEach((_, si) => { const k = stepKey(age, sys, ci, si); const v = statusMap[k] ?? null; if (v === 'normal') normal++; else if (v === 'abnormal') abnormal++; else notAssessed++; }), ); return { normal, abnormal, notAssessed }; }, [age, sys, section, statusMap]); function reset() { // Only clear the current system's entries, not all state. setStatusMap((prev) => { const next = { ...prev }; section.components.forEach((c, ci) => c.steps.forEach((_, si) => { delete next[stepKey(age, sys, ci, si)]; }), ); return next; }); setNarrative(null); } function setAllNormal() { setStatusMap((prev) => { const next = { ...prev }; section.components.forEach((c, ci) => c.steps.forEach((_, si) => { next[stepKey(age, sys, ci, si)] = 'normal'; }), ); return next; }); } function onGenerate() { setNarrative(null); const steps: Array<{ component: string; label: string; method: string; normal: string; status: StepStatus; note?: string }> = []; section.components.forEach((c, ci) => c.steps.forEach((st, si) => { steps.push({ component: c.name, label: st.label, method: st.method, normal: st.normal, status: statusMap[stepKey(age, sys, ci, si)] ?? null, }); }), ); generate.mutate({ steps, ageGroup: age, system: sys, patientAge: patientAge || undefined, patientGender: patientGender || undefined, format, }); } const totalAssessed = summary.normal + summary.abnormal; return (

Physical Exam Guide

Age-group and system-specific exam checklist with abnormal-finding hints. Toggle normal / abnormal on each step, then generate a narrative for your note.

{/* Age-group pills */}
Age group
{AGE_GROUP_ORDER.map((g) => ( ))}
{/* System pills */}
System
{SYSTEM_ORDER.map((s) => ( ))}
{/* Overview */}

{group.label} — {SYSTEM_LABELS[sys]}

{section.overview}

{/* System-specific references */} {sys === 'cv' && (

Auscultation landmarks (APTM + Erb's)

{APTM_LEGEND.map((p) => (
{p.letter}
{p.title}
{p.location}
Listen for: {p.listen}
{p.innocent &&
Innocent: {p.innocent}
}
))}

Cardiac sounds library

{CARDIAC_SOUNDS.map((s) => )}

Classic innocent murmurs

{INNOCENT_MURMURS.map((m) => (
{m.name}
Age: {m.age} · Location: {m.location}
Sound: {m.character}
Confirm innocent: {m.confirm}
))}
)} {sys === 'resp' && (

Respiratory sounds library

{RESP_SOUNDS.map((s) => )}
)} {/* Grading scales (system-scoped, collapsible) */} {SYSTEM_SCALES[sys] && SYSTEM_SCALES[sys].length > 0 && (
Grading scales & reference
{SYSTEM_SCALES[sys].map((sk: string) => { const scale = SCALES[sk]; if (!scale) return null; return ; })}
)} {/* Checklist */}

Exam checklist

{summary.normal} normal {summary.abnormal} abnormal {summary.notAssessed} not assessed
{section.components.map((c, ci) => ( statusMap[k] ?? null} setStatus={(k, next) => setStatusMap((prev) => ({ ...prev, [k]: next }))} /> ))}
{/* Generate narrative */}

Generate Exam Report

Uses the statuses above + optional patient context.

setPatientAge(e.target.value)} /> setPatientGender(e.target.value)} />
{totalAssessed === 0 && ( Mark at least one step before generating. )}
{narrative && (
{narrative}
)}
); }