const ACUTE_FINDINGS = [
{ id: "impalement", label: "Impalement" },
{ id: "evisceration", label: "Evisceration" },
{ id: "amputation", label: "Amputation" },
{ id: "open_chest", label: "Open Chest" },
{ id: "sucking_chest", label: "Sucking Chest" },
{ id: "flail_chest", label: "Flail Chest" },
{ id: "open_abdomen", label: "Open Abdomen" },
{ id: "crush", label: "Crush" },
{ id: "avulsion", label: "Avulsion" },
{ id: "degloving", label: "Degloving" },
{ id: "penetrating_head", label: "Penetrating Head" },
{ id: "spinal_injury", label: "Spinal Injury" }];
function BodyFront() {return null;}
function BodyBack() {return null;}
function Marker({ inj }) {
const cx = inj.x * 100;
const cy = inj.y * 220;
const t = inj.type;
if (t === "entry" || t === "gsw") {
const c = "var(--gsw)";
return (
<g className="injury-mark entry">
<circle cx={cx} cy={cy} r="5" fill="none" stroke={c} strokeWidth="0.35" opacity="0.45" className="bleed-pulse" />
<circle cx={cx} cy={cy} r="2.4" fill={c} opacity="0.35" />
<circle cx={cx} cy={cy} r="1.3" fill="#0a0c0d" stroke={c} strokeWidth="0.5" />
{[[1.6,-2.6],[2.4,-1.0],[-2.2,-1.6],[-2.6,0.8],[1.4,2.8],[-0.6,3.0]].map((p,i) =>
<circle key={i} cx={cx + p[0]} cy={cy + p[1]} r={0.4 + (i % 2) * 0.2} fill={c} opacity="0.75" />
)}
</g>);
}
if (t === "exit") {
const pts = [];
for (let i = 0; i < 14; i++) {
const a = i / 14 * Math.PI * 2;
const r = 2.6 + (i % 2 ? 1.8 : 0.4) + Math.sin(i * 2.1) * 0.4;
pts.push(`${(cx + Math.cos(a) * r).toFixed(2)},${(cy + Math.sin(a) * r).toFixed(2)}`);
}
return (
<g className="injury-mark exit">
<polygon points={pts.join(" ")} fill="var(--gsw-exit)" opacity="0.7" stroke="var(--gsw-exit)" strokeWidth="0.3" />
<polygon points={pts.join(" ")} fill="none" stroke="#0a0c0d" strokeWidth="0.4" opacity="0.4" />
<circle cx={cx} cy={cy} r="0.9" fill="#0a0c0d" />
</g>);
}
if (t === "frag") {
const c = "var(--gsw)";
const dots = [[0,0,1.2],[1.5,1,0.7],[-1.5,1,0.6],[0.5,-1.5,0.55],[-1.1,-1.1,0.6],[2.4,-0.5,0.5],[1.1,2.4,0.45],[-2.0,0.6,0.4]];
return (
<g className="injury-mark frag">
<circle cx={cx} cy={cy} r="4.4" fill={c} opacity="0.14" />
<circle cx={cx} cy={cy} r="5" fill="none" stroke={c} strokeWidth="0.25" opacity="0.4" className="bleed-pulse" />
{dots.map((p, i) =>
<circle key={i} cx={cx + p[0]} cy={cy + p[1]} r={p[2]} fill={c} />
)}
</g>);
}
if (t === "lac") {
const path = `M ${cx - 3.4} ${cy - 1.4} L ${cx - 1.6} ${cy + 0.6} L ${cx + 0.4} ${cy - 0.9} L ${cx + 2.4} ${cy + 1.1} L ${cx + 3.5} ${cy - 0.4}`;
return (
<g className="injury-mark lac">
<path d={path} stroke="var(--lac)" strokeWidth="1.6" fill="none" strokeLinecap="round" />
<path d={path} stroke="#0a0c0d" strokeWidth="0.55" fill="none" strokeLinecap="round" />
</g>);
}
if (t === "burn" || t === "burn_partial" || t === "burn_full") {
const full = t === "burn_full";
const core = full ? "var(--burn-full)" : "var(--burn)";
return (
<g className="injury-mark burn">
<circle cx={cx} cy={cy} r="5.2" fill={core} opacity="0.18" />
<circle cx={cx} cy={cy} r="3.6" fill={core} opacity="0.36" />
<circle cx={cx} cy={cy} r="2.0" fill={core} opacity={full ? 0.95 : 0.65} />
{full && [0, 1.05, 2.1, 3.15, 4.2, 5.25].map((a, i) => {
const sx = cx + Math.cos(a) * 2.4;
const sy = cy + Math.sin(a) * 2.4;
const ex = cx + Math.cos(a) * 4.0;
const ey = cy + Math.sin(a) * 4.0;
return <line key={i} x1={sx} y1={sy} x2={ex} y2={ey} stroke={core} strokeWidth="0.55" strokeLinecap="round" />;
})}
</g>);
}
if (t === "crush") {
const c = "var(--crush)";
return (
<g className="injury-mark crush">
<ellipse cx={cx} cy={cy} rx="6.2" ry="4.2" fill={c} opacity="0.18" />
<ellipse cx={cx} cy={cy} rx="4.2" ry="2.6" fill={c} opacity="0.42" />
{[-4, -2, 0, 2, 4].map((d, i) =>
<line key={i} x1={cx + d - 2} y1={cy - 1.6} x2={cx + d + 2} y2={cy + 1.6} stroke={c} strokeWidth="0.3" opacity="0.55" />
)}
</g>);
}
if (t === "frac") {
const c = "var(--frac)";
return (
<g className="injury-mark frac">
<path d={`M ${cx - 2.4} ${cy - 3.2} L ${cx - 0.4} ${cy - 0.6} L ${cx + 1.2} ${cy - 1.6} L ${cx - 0.9} ${cy + 1.3} L ${cx + 0.9} ${cy + 1.0} L ${cx - 0.9} ${cy + 3.4}`}
stroke={c} strokeWidth="1.3" fill="none" strokeLinecap="round" strokeLinejoin="round" />
</g>);
}
if (t === "amputation") {
const c = "var(--amp)";
return (
<g className="injury-mark amputation">
<line x1={cx - 5.5} y1={cy} x2={cx + 5.5} y2={cy} stroke={c} strokeWidth="0.7" strokeDasharray="1.2 1" />
<path d={`M ${cx - 3.2} ${cy + 0.4} Q ${cx} ${cy + 4.4} ${cx + 3.2} ${cy + 0.4} L ${cx - 3.2} ${cy + 0.4} Z`}
fill={c} opacity="0.55" stroke={c} strokeWidth="0.4" />
<path d={`M ${cx - 2} ${cy + 1.4} Q ${cx} ${cy + 3} ${cx + 2} ${cy + 1.4}`} stroke="#0a0c0d" strokeWidth="0.4" fill="none" />
</g>);
}
if (t === "evisceration") {
const c = "var(--evisc)";
return (
<g className="injury-mark evisc">
<path d={`M ${cx - 3.2} ${cy} C ${cx - 3.2} ${cy - 2.4} ${cx} ${cy - 3.6} ${cx + 2.2} ${cy - 2.6}
C ${cx + 4.2} ${cy - 1.6} ${cx + 3.6} ${cy + 1.8} ${cx + 1.6} ${cy + 2.8}
C ${cx - 0.6} ${cy + 3.6} ${cx - 3.6} ${cy + 1.6} ${cx - 3.2} ${cy} Z`}
fill={c} opacity="0.6" stroke={c} strokeWidth="0.4" />
<path d={`M ${cx - 1.6} ${cy - 0.2}
C ${cx - 1.6} ${cy - 1.2} ${cx - 0.4} ${cy - 1.2} ${cx - 0.4} ${cy - 0.2}
C ${cx - 0.4} ${cy + 0.8} ${cx + 0.8} ${cy + 0.8} ${cx + 0.8} ${cy - 0.2}
C ${cx + 0.8} ${cy - 1.2} ${cx + 1.8} ${cy - 1.2} ${cx + 1.8} ${cy - 0.2}`}
stroke="#0a0c0d" strokeWidth="0.45" fill="none" />
</g>);
}
return null;
}
function BodyView({ side, injuries, taken, tqStates, onClick }) {
const filtered = injuries.filter((i) => i.view === side);
const resKey = side === "front" ? "bodyFront" : "bodyBack";
const fallback = side === "front" ? "assets/body-front.png" : "assets/body-back.png";
const src = (window.__resources && window.__resources[resKey]) || fallback;
return (
<div className="body-view">
<div className={`view-label-bar ${side === "front" ? "right" : "left"}`}>
<span className="view-label-text">
{side === "front" ? "ANT · FRONT" : "POST · BACK"}
</span>
</div>
<div className="body-image-wrap">
<img src={src} alt={side} className="body-image" draggable="false" />
<svg viewBox="0 0 100 220" className="body-overlay" preserveAspectRatio="xMidYMid meet">
<g>
{filtered.map((inj, i) => <Marker key={i} inj={inj} />)}
</g>
<g>
{window.AppliedTreatments && <window.AppliedTreatments side={side} taken={taken} tqStates={tqStates} injuries={injuries} />}
</g>
</svg>
</div>
</div>);
}
function TqCell({ tq, tqState, elapsedSec, onApply, onConvert }) {
const [modal, setModal] = React.useState(false);
const applied = !!tqState;
const timeSince = applied && tqState.appliedAt != null ? Math.max(0, elapsedSec - tqState.appliedAt) : 0;
const minutes = Math.floor(timeSince / 60);
const longApplied = minutes >= 6;
return (
<>
<div className={`tq-cell ${applied ? "applied" : ""} ${longApplied ? "long" : ""}`}
onClick={() => setModal(true)}>
<div className="tq-head">
<span>{tq.label}</span>
<span className="tq-status">{applied ? "ON" : "OFF"}</span>
</div>
<div className="tq-row"><span>TYPE</span><span>{applied ? tqState.type : "—"}</span></div>
<div className="tq-row" data-comment-anchor="87c4e8aabb-div-87-13"><span>SITE</span><span>{applied ? tqState.site : "—"}</span></div>
<div className="tq-row"><span>TIME</span><span>{applied ? "T+" + window.formatClock(tqState.appliedAt) : "—"}</span></div>
{applied &&
<div className="tq-time-since">{minutes > 0 ? `↑ ${minutes}m applied` : "just applied"}</div>
}
</div>
{modal &&
<TqModal
tq={tq}
tqState={tqState}
onClose={() => setModal(false)}
onApply={(type, site) => {onApply(tq.key, type, site);setModal(false);}}
onConvert={() => {onConvert && onConvert(tq.key);setModal(false);}} />
}
</>);
}
function TqModal({ tq, tqState, onClose, onApply, onConvert }) {
const [type, setType] = React.useState(tqState?.type || "CAT Gen-7");
const [site, setSite] = React.useState(tqState?.site || "HIGH & TIGHT");
const types = ["CAT Gen-7", "SOFTT-W", "SAM-XT", "TMT", "Improvised"];
const sites = ["HIGH & TIGHT", "2\" PROX WOUND", "JOINT-LINE"];
return (
<div className="tq-modal-backdrop" onClick={onClose}>
<div className="tq-modal" onClick={(e) => e.stopPropagation()}>
<div className="tq-title">Tourniquet · {tq.label.replace("TQ · ", "")}</div>
<div className="tq-sub">Status: {tqState ? "APPLIED at T+" + window.formatClock(tqState.appliedAt) : "Not applied"}</div>
<div className="tq-section">
<div className="tq-section-label">Type</div>
<div className="tq-options">
{types.map((t) =>
<button key={t} className={`tq-opt ${type === t ? "active" : ""}`} onClick={() => setType(t)}>{t}</button>
)}
</div>
</div>
<div className="tq-section">
<div className="tq-section-label">Site</div>
<div className="tq-options">
{sites.map((s) =>
<button key={s} className={`tq-opt ${site === s ? "active" : ""}`} onClick={() => setSite(s)}>{s}</button>
)}
</div>
</div>
<div className="tq-section" style={{ fontSize: 10, color: "var(--ink-mute)", lineHeight: 1.5 }}>
<b style={{ color: "var(--warn)" }}>PEARL:</b> CAT Gen-7 is service-issue.
High & tight for life-threatening extremity hemorrhage; convert to 2" proximal wound dressing in PFC ≥2 hrs only after definitive control.
</div>
<div className="tq-actions">
<button className="tq-action" onClick={onClose}>CANCEL</button>
{tqState && <button className="tq-action" onClick={onConvert}>CONVERT</button>}
<button className="tq-action primary" onClick={() => onApply(type, site)}>
{tqState ? "UPDATE" : "APPLY TQ"}
</button>
</div>
</div>
</div>);
}
window.TqCell = TqCell;
window.TqModal = TqModal;
function PatientBrief({ scenario, text, label = "BRIEF", variant = "primary", rate = 1.0, onDone }) {
const supported = typeof window !== "undefined" && "speechSynthesis" in window;
const [speaking, setSpeaking] = React.useState(false);
const [paused, setPaused] = React.useState(false);
React.useEffect(() => {
if (!supported) return;
window.speechSynthesis.cancel();
setSpeaking(false);
setPaused(false);
}, [scenario.id]);
const speakable = (s) => String(s || "")
.replace(/·|•/g, ", ")
.replace(/\bICS\b/g, "I C S")
.replace(/\bIED\b/g, "I E D")
.replace(/\bBK\b/g, "below knee")
.replace(/\bAK\b/g, "above knee")
.replace(/\bSpO₂\b/gi, "S P O two")
.replace(/\bGCS\b/g, "G C S")
.replace(/\bTBSA\b/g, "T B S A")
.replace(/\bIOTV\b/g, "I O T V")
.replace(/\bLOC\b/g, "loss of consciousness")
.replace(/\bMAP\b/g, "M A P")
.replace(/\bHR\b/g, "heart rate")
.replace(/\bBP\b/g, "blood pressure")
.replace(/\bSBP\b/g, "systolic")
.replace(/\s+/g, " ")
.trim();
const buildBrief = () => {
const m = scenario.moi || {};
const sex = scenario.sex === "M" ? "male" : scenario.sex === "F" ? "female" : "";
const lines = [
`Patient presentation. Patient last four ${scenario.last4.split("").join(" ")}, callsign ${scenario.callsign}.`,
sex ? `${scenario.age}-year-old ${sex}.` : "",
scenario.mechanism ? `Scene narrative. ${scenario.mechanism}` : "",
m.event ? `Event: ${m.event}.` : "",
m.forces ? `Forces: ${m.forces}.` : "",
m.range ? `Range: ${m.range}.` : "",
m.protection ? `Protection: ${m.protection}.` : "",
m.timeline ? `Timeline: ${m.timeline}.` : "",
m.findings ? `Findings on arrival: ${m.findings}.` : "",
`Working diagnosis: ${scenario.diagnosis}.`
];
return speakable(lines.filter(Boolean).join(" "));
};
const start = () => {
if (!supported) return;
window.speechSynthesis.cancel();
const say = text ? speakable(text) : buildBrief();
const u = new SpeechSynthesisUtterance(say);
u.rate = rate;
u.pitch = 1.0;
u.volume = 1.0;
const voices = window.speechSynthesis.getVoices();
const en = voices.find(v => /en[-_]US/i.test(v.lang) && /female|samantha|google|natural/i.test(v.name)) ||
voices.find(v => /en[-_]US/i.test(v.lang)) ||
voices.find(v => /^en/i.test(v.lang));
if (en) u.voice = en;
u.onend = () => { setSpeaking(false); setPaused(false); if (onDone) onDone(); };
u.onerror = () => { setSpeaking(false); setPaused(false); };
window.speechSynthesis.speak(u);
setSpeaking(true);
setPaused(false);
};
const togglePause = () => {
if (!supported) return;
if (paused) { window.speechSynthesis.resume(); setPaused(false); }
else        { window.speechSynthesis.pause();  setPaused(true);  }
};
const stop = () => {
if (!supported) return;
window.speechSynthesis.cancel();
setSpeaking(false);
setPaused(false);
if (onDone) onDone();
};
if (!supported) {
return (
<div className="brief-bar disabled" title="Web Speech API not available in this browser">
🔇 {label} · unavailable
</div>);
}
return (
<div className="brief-bar">
{!speaking ?
<button className={`brief-btn ${variant}`} onClick={start} title={`Narrate ${label.toLowerCase()}`}>
▶ {label}
</button> :
<>
<button className="brief-btn" onClick={togglePause} title={paused ? "Resume" : "Pause"}>
{paused ? "▶ RESUME" : "❚❚ PAUSE"}
</button>
<button className="brief-btn danger" onClick={stop} title="Stop narration">
■ STOP
</button>
<span className="brief-status">{paused ? "PAUSED" : "NARRATING…"}</span>
</>
}
</div>);
}
window.PatientBrief = PatientBrief;
function CasualtyInfoPanel({ scenario, briefRate, onBriefDone }) {
const [tool, setTool] = useState("gsw");
const [nameOverrides, setNameOverrides] = useState({});
const [last4Overrides, setLast4Overrides] = useState({});
const [findingsOpen, setFindingsOpen] = useState(false);
React.useEffect(() => { setFindingsOpen(false); }, [scenario.id]);
const displayName = nameOverrides[scenario.id] ?? scenario.name;
const displayLast4 = last4Overrides[scenario.id] ?? scenario.last4;
return (
<div className="panel casualty info-panel">
<div className="panel-header">
<span className="marker">▮</span>
<span>Casualty Brief · DD-1380</span>
<span className="right">{scenario.callsign}</span>
</div>
<div className="panel-body" data-comment-anchor="3fb42e3471-div-177-7">
{/* Name & ID */}
<div className="name-row">
<div>
<div className="label-mini">Name · Last4</div>
<input
type="text"
className="name-field name-input"
value={displayName}
spellCheck={false}
onChange={(e) => setNameOverrides((m) => ({ ...m, [scenario.id]: e.target.value }))}
placeholder="LAST, FIRST" />
</div>
<div>
<div className="label-mini">&nbsp;</div>
<input
type="text"
className="id-field id-input"
value={displayLast4}
spellCheck={false}
maxLength={4}
inputMode="numeric"
onChange={(e) => setLast4Overrides((m) => ({ ...m, [scenario.id]: e.target.value.replace(/[^0-9]/g, "").slice(0, 4) }))}
placeholder="0000" />
</div>
</div>
{/* Narrative brief */}
<div className="mech-card">
<div className="mech-head">
<div className="mech-lbl">Mechanism · {scenario.diagnosis}</div>
<PatientBrief scenario={scenario} text={scenario.narrative} label="BRIEF"
rate={briefRate || 1.0} onDone={onBriefDone} />
</div>
<div className="mech-narrative">{scenario.narrative || scenario.mechanism}</div>
<div className={`size-up ${findingsOpen ? "open" : ""}`}>
<button className="size-up-q" onClick={() => {
setFindingsOpen((o) => !o);
if (window.Sound) { window.Sound.init(); window.Sound.play(findingsOpen ? "pageClick" : "confirm"); }
}}>
<span className="su-icon">{findingsOpen ? "▾" : "▸"}</span>
<span className="su-text">What is the picture you see before reaching your patient?</span>
</button>
{findingsOpen &&
<div className="findings-reveal">
<div className="findings-reveal-head">
<span className="fr-lbl">Findings on Arrival</span>
<PatientBrief scenario={scenario} text={`My findings. ${scenario.findingsRead || ""} ${scenario.mission || ""}`}
label="FINDINGS" variant="findings" rate={briefRate || 1.0} />
</div>
<div className="findings-reveal-body">{scenario.findingsRead}</div>
{scenario.mission &&
<div className="findings-mission"><span className="fm-tag">PRIMARY MISSION</span>{scenario.mission}</div>}
</div>}
</div>
{scenario.moi &&
<div className="moi-grid">
<div className="moi-row"><span className="moi-k">Event</span><span className="moi-v">{scenario.moi.event}</span></div>
<div className="moi-row"><span className="moi-k">Forces</span><span className="moi-v">{scenario.moi.forces}</span></div>
<div className="moi-row"><span className="moi-k">Range</span><span className="moi-v">{scenario.moi.range}</span></div>
<div className="moi-row"><span className="moi-k">PPE</span><span className="moi-v">{scenario.moi.protection}</span></div>
<div className="moi-row"><span className="moi-k">Timeline</span><span className="moi-v">{scenario.moi.timeline}</span></div>
</div>
}
</div>
{/* Acute findings */}
<div className="findings-block">
<div className="findings-head">
<span className="lbl">Acute Findings</span>
<span className="hint">Pre-flagged from scene</span>
</div>
<div className="findings-grid">
{ACUTE_FINDINGS.map((f) =>
<div key={f.id}
className={`finding-chip ${scenario.acuteFindings.includes(f.id) ? "active" : ""}`}>
{f.label}
</div>
)}
</div>
</div>
{/* Injury legend — shows what each glyph means on the body diagram */}
</div>
</div>);
}
function BodyPanel({ scenario, taken, tqStates, elapsedSec, onTqApply, onTqConvert }) {
const [glow, setGlow] = React.useState(true);
return (
<div className={`panel casualty body-panel ${glow ? "glow-on" : ""}`}>
<div className="panel-header">
<span className="marker">▮</span>
<span>TCCC · 360 Casualty View</span>
<button className={`glow-toggle ${glow ? "on" : ""}`}
onClick={() => { setGlow((g) => !g); if (window.Sound) window.Sound.play("pageClick"); }}
title="Toggle contour light effect">
✦ GLOW {glow ? "ON" : "OFF"}
</button>
<span className="right body-right">{scenario.name} · {scenario.last4}</span>
</div>
<div className="panel-body">
{/* Injury legend */}
<div className="injury-tools" aria-label="Injury legend">
<span className="injury-tool gsw"><span className="swatch" /> PENETRATING</span>
<span className="injury-tool burn"><span className="swatch" /> BURN</span>
<span className="injury-tool crush"><span className="swatch crush-swatch" /> CRUSH</span>
<span className="injury-tool frac"><span className="swatch" /> FRAC</span>
<span className="injury-tool amp"><span className="swatch amp-swatch" /> AMP / EVISC</span>
</div>
{/* Body diagrams */}
<div className="body-grid">
<BodyView side="front" injuries={scenario.injuries} taken={taken} tqStates={tqStates} />
<BodyView side="back" injuries={scenario.injuries} taken={taken} tqStates={tqStates} />
</div>
{/* Tourniquets */}
<div className="tq-grid">
{[
{ key: "tq_right_arm", label: "TQ · R ARM" },
{ key: "tq_left_arm", label: "TQ · L ARM" },
{ key: "tq_right_leg", label: "TQ · R LEG" },
{ key: "tq_left_leg", label: "TQ · L LEG" }].
map((tq) =>
<TqCell
key={tq.key}
tq={tq}
tqState={tqStates && tqStates[tq.key]}
elapsedSec={elapsedSec}
onApply={onTqApply}
onConvert={onTqConvert} />
)}
</div>
</div>
</div>);
}
window.CasualtyInfoPanel = CasualtyInfoPanel;
window.BodyPanel = BodyPanel;