// Recap — Bold Editorial UI // Ported from the design bundle (direction-editorial.jsx) and wired to the // real FastAPI backend at /api/patients and /api/answer. Single-instance // app (no canvas), full-window, light + dark. const { useState, useEffect, useMemo, useRef } = React; const PALETTE = { light: { bg: '#f4ede2', paper: '#fbf7ef', ink: '#1a1410', inkSoft: '#3a2e25', muted: '#6b5c4a', faint: '#a8967f', rule: '#d6c8b4', ruleSoft: '#e8ddc9', accent: '#b8412e', accentSoft: '#f3dcd0', mark: '#d4af37', }, dark: { bg: '#1a1410', paper: '#221a14', ink: '#f4ede2', inkSoft: '#d6c8b4', muted: '#a8967f', faint: '#6b5c4a', rule: '#3a2e25', ruleSoft: '#2a2017', accent: '#e8755e', accentSoft: '#2a1814', mark: '#e0c060', }, }; const CAT = { diagnosis: { label: 'Diagnosis', hint: 'Clinical condition' }, visit: { label: 'Visit', hint: 'Patient encounter' }, lab: { label: 'Lab', hint: 'Laboratory result' }, report: { label: 'Report', hint: 'Clinical report or summary' }, scan: { label: 'Scan', hint: 'Medical imaging' }, procedure: { label: 'Procedure', hint: 'Operation or intervention' }, med: { label: 'Medication', hint: 'Prescribed drug' }, note: { label: 'Note', hint: 'Free-text clinical note' }, photo: { label: 'Photo', hint: 'Patient-supplied image' }, other: { label: 'Other', hint: 'Uncategorized event' }, }; // Inline lucide-style icons (24x24 viewBox). stroke inherits from parent. // One glyph per event category, picked for instant recognition. const ICONS = { // alert-octagon — signals clinical importance for any diagnosis diagnosis: ( ), // stethoscope visit: ( ), // flask-conical lab: ( ), // file-text report: ( ), // image (frame + small sun + mountain) — universal "imaging" symbol scan: ( ), // scissors procedure: ( ), // pill med: ( ), // pen-line note: ( ), // camera photo: ( ), // dot fallback other: ( ), }; function EventIcon({ category, size = 12 }) { const paths = ICONS[category] || ICONS.other; return ( {paths} ); } const SERIF = '"Source Serif 4", "GT Sectra", "Tiempos Headline", Charter, Georgia, serif'; const SANS = '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif'; const MONO = '"JetBrains Mono", "SF Mono", ui-monospace, monospace'; function fmtDate(iso, opts = { y: true }) { const d = new Date(iso + (iso.length === 10 ? 'T00:00:00Z' : '')); if (opts.short) return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); return d.toLocaleDateString('en-US', { year: opts.y ? 'numeric' : undefined, month: 'short', day: 'numeric', }); } // ───────────────────────────────────────────────────────────────────── // Suggested questions per patient. Used as starter prompts only — // actual answers come from /api/answer (real LLM via inference gateway). const SUGGESTED = { sarah: [ 'When did her kidney function start declining?', 'What medications was she on when CKD was diagnosed?', 'Summarize her trajectory in 3 sentences.', ], marcus: [ 'How long from first symptom to diagnosis?', 'What was the response to R-CHOP?', 'Summarize this patient\'s journey.', ], aisha: [ 'What records does she have in foreign languages?', 'Is her current anemia recurrent or new?', 'What is her current pregnancy status?', ], demo: [ 'When did her kidney function start declining?', 'What was her first abnormal creatinine reading?', 'What medications was she on when CKD was diagnosed?', ], }; // ───────────────────────────────────────────────────────────────────── function App() { const [patients, setPatients] = useState([]); const [patientId, setPatientId] = useState(null); const [dark, setDark] = useState(false); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { fetch('/api/patients') .then((r) => r.json()) .then((data) => { setPatients(data); if (data.length > 0) setPatientId(data[0].id); setLoading(false); }) .catch((e) => { setError(String(e)); setLoading(false); }); }, []); const c = dark ? PALETTE.dark : PALETTE.light; const patient = useMemo( () => patients.find((p) => p.id === patientId), [patients, patientId], ); if (loading) { return ; } if (error) { return ; } if (!patient) { return ; } return (
setDark(!dark)} />
); } function Loading({ c }) { return (
Recap.
loading the chart…
); } function ErrorView({ c, message }) { return (
Something is off.
{message}
); } // ───────────────────────────────────────────────────────────────────── function Masthead({ c, dark, patient, allPatients, onPatientChange, onDarkToggle }) { const [open, setOpen] = useState(false); return (
Recap.
Reads the whole chart so you don't have to.
{open && (
{allPatients.map((p, i) => ( ))}
)}
); } function BackendBadge({ c }) { const [info, setInfo] = useState({ backend: '...' }); useEffect(() => { fetch('/api/health').then((r) => r.json()).then(setInfo).catch(() => {}); }, []); return (
AMD MI300X · 192 GB · {info.backend}
); } // ───────────────────────────────────────────────────────────────────── function Document({ c, patient }) { const events = patient.events; const groups = {}; events.forEach((e) => { const y = e.date.slice(0, 4); (groups[y] = groups[y] || []).push(e); }); const years = Object.keys(groups).sort(); return (
Patient Dossier · {events.length} events · {years.length} year{years.length === 1 ? '' : 's'} on record

{patient.display_name}.

{patient.summary}
{patient.age != null && } {patient.gender && } {patient.mrn && } e.source)).size} label="source docs" />
{patient.tags && patient.tags.length > 0 && (
{patient.tags.map((t) => ( {t} ))}
)}
{years.map((y, yi) => ( ))}
); } function Stat({ c, value, label, mono }) { return (
{value}
{label}
); } function YearSection({ c, year, events, first }) { const [activeId, setActiveId] = useState(null); return (

{year}

{events.length} {events.length === 1 ? 'event' : 'events'}
{events.map((e, ei) => ( setActiveId(activeId === e.id ? null : e.id)} /> ))}
); } function DocEvent({ c, e, index, active, onClick }) { const cat = CAT[e.category] || CAT.other; const [iconHover, setIconHover] = useState(false); // Vertical center of the title line is roughly 26px from the top of the // content button (≈12px category label + 3px gap + half of 22px title line). // Center the icon there so it visually anchors to the title, not the date. const iconCenterY = 26; const iconSize = active ? 30 : 26; const iconPadTop = Math.max(iconCenterY - iconSize / 2, 0); return (
{fmtDate(e.date, { y: false, short: true })}
{String(index + 1).padStart(2, '0')}
{/* Hover wrapper sits exactly on the icon — tooltip uses bottom:100% relative to it */}
setIconHover(true)} onMouseLeave={() => setIconHover(false)}>
{iconHover && (
{cat.label} · {cat.hint} {/* Tooltip tail */}
)}
); } // ───────────────────────────────────────────────────────────────────── function ChatColumn({ c, patient }) { const [history, setHistory] = useState([]); const [input, setInput] = useState(''); const [thinking, setThinking] = useState(false); const scroller = useRef(null); useEffect(() => { setHistory([]); setInput(''); }, [patient.id]); useEffect(() => { if (scroller.current) scroller.current.scrollTop = scroller.current.scrollHeight; }, [history, thinking]); const send = async (text) => { const q = (text || input).trim(); if (!q) return; setInput(''); setHistory((h) => [...h, { role: 'user', text: q }]); setThinking(true); try { const r = await fetch('/api/answer', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ patient_id: patient.id, question: q }), }); const data = await r.json(); if (data.error) { setHistory((h) => [...h, { role: 'assistant', text: `Error: ${data.error}`, citations: [] }]); } else { setHistory((h) => [...h, { role: 'assistant', text: data.text, citations: data.citations || [], }]); } } catch (err) { setHistory((h) => [...h, { role: 'assistant', text: `Network error: ${String(err)}`, citations: [], }]); } finally { setThinking(false); } }; const examples = SUGGESTED[patient.id] || SUGGESTED.demo || []; return (
The Reading Room
Ask a question.
Get a cited answer.
{history.length === 0 && (
Suggested
{examples.map((ex, i) => ( ))}
)} {history.map((m, i) => (
{m.role === 'user' ? (
You asked
"{m.text}"
) : ( )}
))} {thinking && (
{' '}reading {patient.events.length} events…
)}
? setInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && send()} placeholder="Ask anything about this chart…" style={{ flex: 1, border: 'none', background: 'transparent', color: c.ink, fontSize: 14, outline: 'none', fontFamily: SERIF, padding: '2px 0', }} />
); } // Render markdown-ish bold + inline citation markers like [src:foo.pdf#p2]. function AssistantMessage({ c, m, patient }) { // Replace [src:foo.pdf#p2] with superscript clickable cite numbers. const citationsByKey = {}; let counter = 0; const text = (m.text || '').replace(/\[src:([^\]#]+)(?:#p(\d+))?\]/g, (_match, src, page) => { const key = `${src}|${page || ''}`; if (!(key in citationsByKey)) { counter += 1; citationsByKey[key] = { n: counter, src, page: page ? parseInt(page, 10) : null }; } return `‹CITE:${citationsByKey[key].n}›`; }); // Now split on the placeholders + bold markdown. const segments = text.split(/(‹CITE:\d+›|\*\*[^*]+\*\*)/g); return (
The chart says
{segments.map((seg, i) => { if (seg.startsWith('‹CITE:')) { const n = parseInt(seg.slice(6, -1), 10); return ( {n} ); } if (seg.startsWith('**') && seg.endsWith('**')) { return {seg.slice(2, -2)}; } return {seg}; })}
{(m.citations && m.citations.length > 0) && (
Drawn from
{m.citations.map((cit, i) => (
{i + 1}. {cit.snippet && ( {cit.snippet} )} {cit.snippet && · } {cit.source_id}{cit.page ? ` p.${cit.page}` : ''}
))}
)}
); } // Blinking cursor keyframes if (!document.getElementById('edit-keyframes')) { const s = document.createElement('style'); s.id = 'edit-keyframes'; s.textContent = `@keyframes edit-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }`; document.head.appendChild(s); } ReactDOM.createRoot(document.getElementById('root')).render();