const VERDICT_SCORE = { best: 1, ok: 0.55, harm: 0 };
const VERDICT_LABEL = { best: "OPTIMAL", ok: "ACCEPTABLE", harm: "SUBOPTIMAL" };
const METRICS = [
{ k: "lives", t: "Lives Saved", d: "Outcome-critical calls" },
{ k: "timeToDx", t: "Time to Diagnosis", d: "Speed & accuracy of recognition" },
{ k: "resource", t: "Resource Utilization", d: "Scarce-asset stewardship" },
{ k: "guideline", t: "Guideline Adherence", d: "JTS / TCCC / CPG alignment" },
{ k: "evac", t: "Evacuation Appropriateness", d: "Right precedence, right destination" },
{ k: "mission", t: "Mission Impact", d: "Force-health & operational effect" },
];
function seededOrder(n, seed) {
const arr = Array.from({ length: n }, (_, i) => i);
let s = seed;
for (let i = n - 1; i > 0; i--) {
s = (s * 9301 + 49297) % 233280;
const j = Math.floor((s / 233280) * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
function CaseView({ id, onBack }) {
const idx = window.GUIDON_INDEX.find(s => s.id === id);
const data = window.GUIDON_CASES[id];
if (!idx) return <div className="wrap" style={{ padding: 40 }}>Unknown scenario.</div>;
if (!data) return <StubCase idx={idx} onBack={onBack} />;
const storeKey = "guidon.progress." + id;
const [answers, setAnswers] = useState(() => {
try { return JSON.parse(localStorage.getItem(storeKey)) || {}; } catch (e) { return {}; }
});
useEffect(() => { try { localStorage.setItem(storeKey, JSON.stringify(answers)); } catch (e) {} }, [answers]);
const [tab, setTab] = useState("hpi");
const total = data.decisions.length;
const answeredCount = Object.keys(answers).length;
const complete = answeredCount >= total;
const choose = (di, oi) => {
if (answers[di] != null) return;
setAnswers(a => ({ ...a, [di]: oi }));
};
const restart = () => { setAnswers({}); window.scrollTo({ top: 0 }); };
const scores = useMemo(() => computeScores(data, answers), [data, answers]);
const patientTabs = buildPatientTabs(data.patient);
return (
<div className="case">
{/* case header */}
<div className="case-head ticks">
<span className="tick-tr" /><span className="tick-br" />
<div className="wrap case-head-inner">
<div className="case-head-l">
<div className="case-head-meta">
<span className="case-code mono">{idx.code}</span>
<AcuityChip acuity={idx.acuity} />
<span className="case-sys mono">{idx.system.toUpperCase()}</span>
</div>
<h1 className="case-title">{idx.title}</h1>
<p className="case-sub">{data.subtitle}</p>
</div>
<div className="case-head-prog">
<div className="prog-ring mono">
<span className="prog-num">{answeredCount}<span className="prog-den">/{total}</span></span>
<span className="prog-lbl kicker">Decisions</span>
</div>
</div>
</div>
</div>
<div className="wrap case-body">
{/* LEFT — dossier */}
<div className="dossier">
{/* mission */}
<section className="panel">
<PanelTag n="A">Mission Context</PanelTag>
<div className="kv">
<Row k="Theater" v={data.mission.cocom} mono />
<Row k="Setting" v={data.mission.setting} />
<Row k="Role / provider" v={data.mission.role} />
<Row k="Evac picture" v={data.mission.evac} warn />
</div>
</section>
{/* capability */}
<section className="panel">
<PanelTag n="B">Resources Available</PanelTag>
<div className="cap-level">
<LevelBars level={data.capability.level} withLabel />
<span className="cap-level-txt mono">{window.GUIDON_TAX.LEVELS[data.capability.level].label.toUpperCase()}</span>
</div>
<div className="cap-cols">
<div className="cap-col">
<div className="cap-h cap-h-have mono">ON HAND</div>
<ul className="cap-list">{data.capability.have.map((x, i) => <li key={i} className="cap-have">{x}</li>)}</ul>
</div>
<div className="cap-col">
<div className="cap-h cap-h-lack mono">NOT AVAILABLE</div>
<ul className="cap-list">{data.capability.lack.map((x, i) => <li key={i} className="cap-lack">{x}</li>)}</ul>
</div>
</div>
</section>
{/* patient */}
<section className="panel panel-patient">
<PanelTag n="C">Dynamic Patient Information</PanelTag>
<div className="pt-id mono">{data.patient.pt}</div>
<div className="pt-tabs">
{patientTabs.map(t => (
<button key={t.key} className={"pt-tab" + (tab === t.key ? " on" : "")} onClick={() => setTab(t.key)}>{t.label}</button>
))}
</div>
<div className="pt-panel">
{patientTabs.find(t => t.key === tab).render()}
</div>
</section>
</div>
{/* RIGHT — decisions + after action */}
<div className="flow">
<div className="flow-head">
<PanelTag>Decision Walkthrough</PanelTag>
{answeredCount > 0 && <button className="btn btn-ghost btn-sm" onClick={restart}>Restart case</button>}
</div>
{data.decisions.map((d, di) => {
const locked = di > 0 && answers[di - 1] == null;
const answered = answers[di] != null;
const visible = !locked || answered;
if (!visible) return <LockedDecision key={di} n={di + 1} phase={d.phase} />;
return <Decision key={di} d={d} di={di} caseId={id} chosen={answers[di]} onChoose={choose} />;
})}
{data.soap && <SoapNote soap={data.soap} caseId={id} />}
{data.osce && <OsceEval osce={data.osce} caseId={id} />}
{complete && <AfterAction scores={scores} data={data} onRestart={restart} />}
</div>
</div>
<div className="wrap case-foot mono">
TRAINING SCENARIO · {idx.code} · DECISIONS AND FEEDBACK ARE INSTRUCTIONAL — VERIFY AGAINST CURRENT JTS/TCCC CPGs
</div>
</div>
);
}
function Decision({ d, di, caseId, chosen, onChoose }) {
const order = useMemo(() => seededOrder(d.options.length, caseId * 97 + di * 13 + 7), [caseId, di, d.options.length]);
const answered = chosen != null;
const chosenOpt = answered ? d.options[chosen] : null;
return (
<section className={"decision" + (answered ? " done" : " active")}>
<div className="decision-head">
<span className="decision-n mono">{String(di + 1).padStart(2, "0")}</span>
<span className="decision-phase kicker">{d.phase}</span>
{answered && <span className={"verdict-pill mono v-" + chosenOpt.verdict}>{VERDICT_LABEL[chosenOpt.verdict]}</span>}
</div>
<p className="decision-stem">{d.stem}</p>
<div className="opts">
{order.map(oi => {
const o = d.options[oi];
const isChosen = chosen === oi;
const showVerdict = answered;
let cls = "opt";
if (answered) {
if (isChosen) cls += " opt-chosen v-" + o.verdict;
else if (o.verdict === "best") cls += " opt-reveal-best";
else cls += " opt-dim";
}
return (
<button key={oi} className={cls} disabled={answered} onClick={() => onChoose(di, oi)}>
<span className="opt-mark mono">{showVerdict ? (o.verdict === "best" ? "✓" : isChosen ? "•" : "") : "○"}</span>
<span className="opt-body">
<span className="opt-text">{o.t}</span>
{answered && (isChosen || o.verdict === "best") && (
<span className={"opt-fb" + (o.verdict === "harm" ? " fb-harm" : o.verdict === "ok" ? " fb-ok" : " fb-best")}>{o.fb}</span>
)}
</span>
</button>
);
})}
</div>
{answered && <div className="decision-metrics mono">{d.metrics.map(m => <span key={m} className="dm-chip">{METRICS.find(x => x.k === m).t}</span>)}</div>}
</section>
);
}
function LockedDecision({ n, phase }) {
return (
<section className="decision locked">
<div className="decision-head">
<span className="decision-n mono">{String(n).padStart(2, "0")}</span>
<span className="decision-phase kicker">{phase}</span>
<span className="lock-tag mono">— answer above to unlock —</span>
</div>
</section>
);
}
function soapLines(v) {
return Array.isArray(v) ? v : [v];
}
function SoapNote({ soap, caseId }) {
const [open, setOpen] = useState(false);
const [revealed, setRevealed] = useState(false);
const rows = [
{ k: "S", label: "Subjective", v: soap.s },
{ k: "O", label: "Objective", v: soap.o },
{ k: "A", label: "Assessment", v: soap.a },
{ k: "P", label: "Plan", v: soap.p },
];
return (
<section className={"soap" + (open ? " open" : "")}>
<button className="soap-toggle" onClick={() => setOpen(o => !o)} aria-expanded={open}>
<span className="soap-toggle-l">
<span className="soap-chevron mono" aria-hidden="true">{open ? "\u25BE" : "\u25B8"}</span>
<span className="panel-tag-n mono">S</span>
<span className="panel-tag-label kicker">Model SOAP Note</span>
</span>
<span className="soap-toggle-r mono">{open ? "HIDE" : "OPEN"} · INSTRUCTOR KEY</span>
</button>
{open && (
<div className="soap-body">
<div className={"soap-doc" + (revealed ? " revealed" : " blurred")} aria-hidden={!revealed}>
{rows.map(r => (
<div key={r.k} className="soap-row">
<span className="soap-k mono">{r.k}</span>
<div className="soap-c">
<div className="soap-c-label kicker">{r.label}</div>
{soapLines(r.v).length > 1
? <ul className="soap-c-list">{soapLines(r.v).map((l, i) => <li key={i}>{l}</li>)}</ul>
: <p className="soap-c-text">{soapLines(r.v)[0]}</p>}
</div>
</div>
))}
</div>
{!revealed && (
<div className="soap-veil">
<div className="soap-veil-inner">
<span className="soap-veil-tag mono">REDACTED — INSTRUCTOR KEY</span>
<p className="soap-veil-note">Document your own SOAP note first, then reveal the model and compare.</p>
<button className="btn btn-primary" onClick={() => setRevealed(true)}>Reveal model note</button>
</div>
</div>
)}
{revealed && (
<button className="soap-rehide mono" onClick={() => setRevealed(false)}>↺ Re-blur</button>
)}
</div>
)}
</section>
);
}
const OSCE_RATINGS = [
{ n: 1, t: "Unsafe", c: "critical" },
{ n: 2, t: "Borderline", c: "critical" },
{ n: 3, t: "Competent", c: "urgent" },
{ n: 4, t: "Proficient", c: "routine" },
{ n: 5, t: "Exemplary", c: "routine" },
];
function osceItem(it) { return typeof it === "string" ? { t: it, crit: false } : { t: it.t, crit: !!it.crit }; }
function OsceEval({ osce, caseId }) {
const key = "guidon.osce." + caseId;
const [state, setState] = useState(() => { try { return JSON.parse(localStorage.getItem(key)) || {}; } catch (e) { return {}; } });
useEffect(() => { try { localStorage.setItem(key, JSON.stringify(state)); } catch (e) {} }, [state, key]);
const [open, setOpen] = useState(false);
const items = [];
osce.domains.forEach((d, di) => d.items.forEach((raw, ii) => items.push({ id: di + "-" + ii, ...osceItem(raw) })));
const tickedCount = items.filter(it => state[it.id]).length;
const pct = items.length ? Math.round((tickedCount / items.length) * 100) : 0;
const critItems = items.filter(it => it.crit);
const critMissed = critItems.filter(it => !state[it.id]).length;
const rating = state.__rating || null;
const toggle = (id) => setState(s => ({ ...s, [id]: !s[id] }));
const setRating = (n) => setState(s => ({ ...s, __rating: s.__rating === n ? null : n }));
const reset = () => setState({});
return (
<section className={"osce" + (open ? " open" : "")}>
<button className="osce-toggle" onClick={() => setOpen(o => !o)} aria-expanded={open}>
<span className="soap-toggle-l">
<span className="soap-chevron mono" aria-hidden="true">{open ? "\u25BE" : "\u25B8"}</span>
<span className="panel-tag-n mono">E</span>
<span className="panel-tag-label kicker">Objective Structured Clinical Exam</span>
</span>
<span className="osce-toggle-r mono">{tickedCount}/{items.length} · {pct}%{critMissed > 0 ? " · " + critMissed + " CRIT MISS" : ""}</span>
</button>
{open && (
<div className="osce-body">
<p className="osce-intro mono">OSCE — examiner ticks each clinical action the candidate performs. ⚑ marks a critical (must-do) action.</p>
<div className="osce-bar"><span className={"osce-bar-fill fill-" + (pct >= 85 ? "routine" : pct >= 60 ? "urgent" : "critical")} style={{ width: pct + "%" }} /></div>
{osce.domains.map((d, di) => (
<div key={di} className="osce-domain">
<div className="osce-domain-h">
<span className="osce-domain-n mono">{String(di + 1).padStart(2, "0")}</span>
<span className="osce-domain-t">{d.name}</span>
</div>
<ul className="osce-list">
{d.items.map((raw, ii) => {
const it = osceItem(raw);
const id = di + "-" + ii;
const on = !!state[id];
return (
<li key={ii} className={"osce-item" + (on ? " on" : "") + (it.crit ? " crit" : "")}>
<button className="osce-check" onClick={() => toggle(id)} aria-pressed={on}>
<span className="osce-box mono" aria-hidden="true">{on ? "\u2713" : ""}</span>
<span className="osce-item-t">{it.t}</span>
{it.crit && <span className="osce-crit mono" title="Critical action">⚑</span>}
</button>
</li>
);
})}
</ul>
</div>
))}
<div className="osce-global">
<div className="osce-domain-h">
<span className="osce-domain-n mono">GR</span>
<span className="osce-domain-t">Global Rating</span>
</div>
<div className="osce-rating">
{OSCE_RATINGS.map(r => (
<button key={r.n} className={"osce-rate band-" + r.c + (rating === r.n ? " on" : "")} onClick={() => setRating(r.n)}>
<span className="osce-rate-n mono">{r.n}</span>
<span className="osce-rate-t">{r.t}</span>
</button>
))}
</div>
</div>
<div className="osce-summary">
<div className="osce-summary-l">
<span className="osce-summary-pct mono">{pct}%</span>
<span className="osce-summary-lbl mono">{tickedCount} of {items.length} actions · {critMissed === 0 ? "all critical actions met" : critMissed + " critical action" + (critMissed > 1 ? "s" : "") + " missed"}</span>
</div>
{(tickedCount > 0 || rating) && <button className="btn btn-ghost btn-sm" onClick={reset}>Reset checklist</button>}
</div>
</div>
)}
</section>
);
}
function AfterAction({ scores, data, onRestart }) {
const overallPct = Math.round(scores.overall * 100);
const band = overallPct >= 85 ? { t: "MISSION-READY", c: "routine" } : overallPct >= 65 ? { t: "DEVELOPING", c: "urgent" } : { t: "RETRAIN", c: "critical" };
return (
<section className="aar">
<div className="aar-head">
<PanelTag>After-Action Review</PanelTag>
<span className={"aar-band mono band-" + band.c}>{band.t} · {overallPct}%</span>
</div>
<div className="aar-metrics">
{METRICS.map(m => {
const v = scores.byMetric[m.k];
const pct = v == null ? null : Math.round(v * 100);
const c = pct == null ? "" : pct >= 85 ? "routine" : pct >= 60 ? "urgent" : "critical";
return (
<div key={m.k} className="metric">
<div className="metric-top">
<span className="metric-t">{m.t}</span>
<span className="metric-pct mono">{pct == null ? "—" : pct + "%"}</span>
</div>
<div className="metric-bar"><span className={"metric-fill fill-" + c} style={{ width: (pct || 0) + "%" }} /></div>
<div className="metric-d mono">{m.d}</div>
</div>
);
})}
</div>
<div className="aar-cols">
<div className="aar-block">
<div className="aar-h kicker">Key Teaching Points</div>
<ul className="aar-list teach">{data.teaching.map((x, i) => <li key={i}>{x}</li>)}</ul>
</div>
<div className="aar-block">
<div className="aar-h kicker">PCC Considerations</div>
<ul className="aar-list">{data.pcc.map((x, i) => <li key={i}>{x}</li>)}</ul>
</div>
<div className="aar-block">
<div className="aar-h kicker">Follow-up</div>
<ul className="aar-list">{data.followup.map((x, i) => <li key={i}>{x}</li>)}</ul>
</div>
</div>
<div className="aar-refs">
<div className="aar-h kicker">References</div>
<ol className="ref-list">
{data.refs.map((r, i) => (
<li key={i} className="ref">
<span className="ref-n mono">{String(i + 1).padStart(2, "0")}</span>
<span className="ref-body">
<span className="ref-t">{r.t}</span>
<span className="ref-src mono">{r.src}{r.date ? " · " + r.date : ""}</span>
</span>
</li>
))}
</ol>
</div>
<div className="aar-actions">
<button className="btn btn-primary" onClick={onRestart}>Run case again</button>
</div>
</section>
);
}
function StubCase({ idx, onBack }) {
return (
<div className="case">
<div className="case-head ticks">
<span className="tick-tr" /><span className="tick-br" />
<div className="wrap case-head-inner">
<div className="case-head-l">
<div className="case-head-meta">
<span className="case-code mono">{idx.code}</span>
<AcuityChip acuity={idx.acuity} />
<span className="case-sys mono">{idx.system.toUpperCase()}</span>
</div>
<h1 className="case-title">{idx.title}</h1>
<p className="case-sub">{idx.oneLiner}</p>
</div>
</div>
</div>
<div className="wrap stub-body">
<div className="stub-flag mono">CASE INDEXED · FULL BUILD IN DEVELOPMENT</div>
<div className="stub-grid">
<div className="panel">
<PanelTag n="A">Brief</PanelTag>
<div className="kv">
<Row k="Theater(s)" v={idx.cocom.join(" · ")} mono />
<Row k="Acuity" v={window.ACUITY_LABEL[idx.acuity]} mono />
<Row k="Starting resource" v={window.GUIDON_TAX.LEVELS[idx.level].label} />
<Row k="System" v={idx.system} />
</div>
</div>
<div className="panel">
<PanelTag n="B">Decision Framework (applies to every case)</PanelTag>
<ol className="framework">
{["Initial assessment", "Differential diagnosis", "Immediate interventions", "Diagnostics", "Definitive treatment", "Evacuation decision", "PCC considerations", "Follow-up"].map((x, i) => <li key={i}>{x}</li>)}
</ol>
</div>
</div>
<p className="stub-note">
The <b>Cardiovascular</b> module (CV-01 → CV-10) is fully built end-to-end with dynamic patient data,
scored decision points, after-action review, and JTS/TCCC references. This scenario is indexed and
slated for the same treatment in a later batch.
</p>
<button className="btn btn-primary" onClick={onBack}>← Back to library</button>
</div>
</div>
);
}
function Row({ k, v, mono, warn }) {
return (
<div className="kv-row">
<span className="kv-k kicker">{k}</span>
<span className={"kv-v" + (mono ? " mono" : "") + (warn ? " kv-warn" : "")}>{v}</span>
</div>
);
}
function computeScores(data, answers) {
const acc = {}; const cnt = {}; let sum = 0, sn = 0;
data.decisions.forEach((d, di) => {
const oi = answers[di];
if (oi == null) return;
const sc = VERDICT_SCORE[d.options[oi].verdict];
sum += sc; sn++;
d.metrics.forEach(m => { acc[m] = (acc[m] || 0) + sc; cnt[m] = (cnt[m] || 0) + 1; });
});
const byMetric = {};
METRICS.forEach(m => { byMetric[m.k] = cnt[m.k] ? acc[m.k] / cnt[m.k] : null; });
return { byMetric, overall: sn ? sum / sn : 0 };
}
function buildPatientTabs(p) {
const tabs = [];
tabs.push({ key: "hpi", label: "History", render: () => <p className="pt-text">{p.hpi}</p> });
tabs.push({
key: "vitals", label: "Vitals", render: () => (
<div className="vitals">
{Object.entries(p.vitals).map(([k, v]) => (
<div key={k} className="vital"><span className="vital-k mono">{k}</span><span className="vital-v mono">{v}</span></div>
))}
</div>
)
});
tabs.push({ key: "exam", label: "Exam", render: () => <p className="pt-text">{p.exam}</p> });
tabs.push({
key: "labs", label: "Labs", render: () => (
Array.isArray(p.labs) ? (
<table className="labs"><tbody>
{p.labs.map((l, i) => (
<tr key={i} className={l.flag === "high" ? "lab-flag" : ""}>
<td className="lab-t">{l.t}</td>
<td className="lab-v mono">{l.v}</td>
<td className="lab-f mono">{l.flag === "high" ? "⚑" : ""}</td>
</tr>
))}
</tbody></table>
) : <p className="pt-text mono">{p.labs}</p>
)
});
tabs.push({ key: "us", label: "Ultrasound", render: () => <p className="pt-text">{p.us}</p> });
tabs.push({ key: "ecg", label: "ECG", render: () => <p className="pt-text pt-ecg">{p.ecg}</p> });
tabs.push({ key: "imaging", label: "Imaging", render: () => <p className="pt-text">{p.imaging}</p> });
return tabs;
}
window.CaseView = CaseView;