// 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 (
);
}
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.
setOpen(!open)} style={{
display: 'flex', alignItems: 'center', gap: 10,
padding: '6px 12px', border: `1px solid ${c.rule}`, borderRadius: 2,
background: c.paper, color: c.ink, cursor: 'pointer',
fontFamily: SANS, fontSize: 12,
}}>
CASE №
{patient.display_name}
▾
{open && (
{allPatients.map((p, i) => (
{ onPatientChange(p.id); setOpen(false); }}
style={{
width: '100%', textAlign: 'left', padding: '10px 12px',
borderRadius: 2, border: 'none', cursor: 'pointer',
background: p.id === patient.id ? c.accentSoft : 'transparent',
color: c.ink, fontFamily: 'inherit',
}}>
{String(i + 1).padStart(2, '0')}
{p.display_name}
{p.age != null && (
· {p.age}y
)}
{p.summary}
))}
)}
{dark ? '☀' : '☾'}
);
}
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 (
);
}
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 */}
)}
{e.category}
{e.flag === 'critical' && (
· Critical
)}
{(e.flag === 'high' || e.flag === 'low') && (
· {e.flag === 'high' ? 'High' : 'Low'}
)}
{e.title}
{e.body && e.body !== e.title && (active || e.flag === 'critical') && (
{e.body}
)}
{active && (
Source: {e.source}
{e.page && Page {e.page} }
{e.snippet && (
"{e.snippet}"
)}
)}
);
}
// ─────────────────────────────────────────────────────────────────────
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) => (
send(ex)} style={{
display: 'flex', gap: 12, alignItems: 'flex-start',
width: '100%', textAlign: 'left',
padding: '14px 0',
borderTop: i === 0 ? `1px solid ${c.rule}` : 'none',
borderBottom: `1px solid ${c.rule}`,
background: 'transparent', border: 'none', cursor: 'pointer',
borderRadius: 0, color: c.ink, fontFamily: 'inherit',
}}>
0{i + 1}
{ex}
→
))}
)}
{history.map((m, i) => (
{m.role === 'user' ? (
) : (
)}
))}
{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',
}} />
send()} style={{
padding: '6px 14px', borderRadius: 2,
background: c.accent, border: 'none', color: c.paper,
cursor: 'pointer', fontSize: 12, fontWeight: 500,
fontFamily: SANS, letterSpacing: '0.02em',
}}>Ask →
);
}
// 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( );