trace-field-notes / frontend /static /components.jsx
JacobLinCool's picture
feat: add privacy filtering and execution modes
8457788 verified
Raw
History Blame Contribute Delete
27 kB
/* ============================================================
atoms.jsx — shared primitives + topo background
============================================================ */
// ---- deterministic topo contour generator ----
function _noise(a, seed) {
return (
Math.sin(a * 3 + seed) * 0.45 +
Math.sin(a * 5 + seed * 1.7) * 0.28 +
Math.sin(a * 2 + seed * 0.6) * 0.5 +
Math.sin(a * 7 + seed * 2.3) * 0.16
);
}
function _blob(cx, cy, r, seed, amp) {
const N = 80;
let d = "";
for (let i = 0; i <= N; i++) {
const t = (i / N) * Math.PI * 2;
const rr = r * (1 + amp * _noise(t, seed));
const x = cx + rr * Math.cos(t);
const y = cy + rr * Math.sin(t) * 0.82;
d += (i === 0 ? "M" : "L") + x.toFixed(1) + " " + y.toFixed(1) + " ";
}
return d + "Z";
}
function TopoBackground() {
const peaks = [
{ cx: 250, cy: 230, seed: 1.2, count: 11, base: 26, step: 34, peakAt: 3 },
{ cx: 1160, cy: 640, seed: 4.7, count: 13, base: 24, step: 32, peakAt: 4 },
{ cx: 760, cy: 120, seed: 8.1, count: 7, base: 30, step: 40, peakAt: 1 },
];
return (
<svg viewBox="0 0 1440 900" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
{peaks.map((p, pi) =>
Array.from({ length: p.count }).map((_, i) => {
const r = p.base + i * p.step;
const amp = 0.05 + i * 0.012;
const strong = i === p.peakAt;
return (
<path
key={pi + "-" + i}
d={_blob(p.cx, p.cy, r, p.seed + i * 0.13, amp)}
fill="none"
stroke={strong ? "var(--topo-stroke-strong)" : "var(--topo-stroke)"}
strokeWidth={strong ? 1.4 : 0.9}
/>
);
})
)}
</svg>
);
}
// ---- tone helpers ----
function toneOf(recovery) {
return (window.TFN.TONE_OF[recovery]) || "unknown";
}
function toneColor(tone) {
return "var(--tone-" + tone + ")";
}
// ---- small atoms ----
function Kicker({ children }) {
return <div className="kicker">{children}</div>;
}
function Label({ children, accent, style }) {
return <div className={"label" + (accent ? " label--accent" : "")} style={style}>{children}</div>;
}
function ToneDot({ tone, size = 10 }) {
return (
<span
className="tone-dot"
style={{ background: toneColor(tone), color: toneColor(tone), width: size, height: size }}
/>
);
}
// codebook chip: pass field + code
function CodeChip({ field, code, withDotTone }) {
const label = (window.TFN.CODEBOOK[field] && window.TFN.CODEBOOK[field][code]) || code;
return (
<span className="chip" title={field.replace(/_/g, " ")}>
{withDotTone ? <span className="dot" style={{ background: toneColor(withDotTone) }} /> : null}
{label}
</span>
);
}
function Stamp({ tone, children }) {
return (
<span className="stamp" style={{ color: toneColor(tone) }}>
{children}
</span>
);
}
// section header used across the report
function SectionHead({ index, kicker, title, sub }) {
return (
<div className="sec-head">
<div className="sec-head__top">
{index ? <span className="sec-head__no mono">{index}</span> : null}
<Kicker>{kicker}</Kicker>
</div>
<h2 className="sec-head__title">{title}</h2>
{sub ? <p className="sec-head__sub">{sub}</p> : null}
</div>
);
}
Object.assign(window, {
TopoBackground, toneOf, toneColor,
Kicker, Label, ToneDot, CodeChip, Stamp, SectionHead,
});
/* ============================================================
trailmap.jsx — elevation-profile trail map + episode detail
x = progress through the session, y = risk / exposure.
The agent's journey climbs toward hazard.
============================================================ */
const ELEV = { stable: 0.12, detour: 0.44, iterative: 0.52, partial: 0.64, risk: 0.93, unknown: 0.30 };
const VBW = 1000, VBH = 360;
const PAD = { l: 116, r: 96, t: 48, b: 60 };
function _layout(episodes) {
const n = episodes.length;
const innerW = VBW - PAD.l - PAD.r;
const innerH = VBH - PAD.t - PAD.b;
const baseY = VBH - PAD.b;
return episodes.map((ep, i) => {
const tone = toneOf(ep.recovery_pattern);
const x = PAD.l + (n === 1 ? innerW / 2 : (i / (n - 1)) * innerW);
const jitter = ((i % 2) * 2 - 1) * 0.015;
const elev = Math.min(0.97, Math.max(0.06, ELEV[tone] + jitter));
const y = baseY - elev * innerH;
return { ep, tone, x, y, fx: (x / VBW) * 100, fy: (y / VBH) * 100, elev };
});
}
function _smoothPath(pts) {
if (pts.length === 1) return `M ${pts[0].x} ${pts[0].y}`;
let d = `M ${pts[0].x} ${pts[0].y}`;
for (let i = 0; i < pts.length - 1; i++) {
const p0 = pts[i - 1] || pts[i];
const p1 = pts[i];
const p2 = pts[i + 1];
const p3 = pts[i + 2] || p2;
const c1x = p1.x + (p2.x - p0.x) / 6;
const c1y = p1.y + (p2.y - p0.y) / 6;
const c2x = p2.x - (p3.x - p1.x) / 6;
const c2y = p2.y - (p3.y - p1.y) / 6;
d += ` C ${c1x.toFixed(1)} ${c1y.toFixed(1)}, ${c2x.toFixed(1)} ${c2y.toFixed(1)}, ${p2.x.toFixed(1)} ${p2.y.toFixed(1)}`;
}
return d;
}
function TrailMap({ episodes, selectedId, onSelect }) {
const pts = _layout(episodes);
const compact = episodes.length > 6;
const baseY = VBH - PAD.b;
const line = _smoothPath(pts);
const area = `${line} L ${pts[pts.length - 1].x} ${baseY} L ${pts[0].x} ${baseY} Z`;
const gridY = [0.25, 0.5, 0.75, 1].map((f) => baseY - f * (VBH - PAD.t - PAD.b));
return (
<div className="trail">
<div className="trail__chrome">
<div className="trail__axis-y">
<span>Hazard</span><span>Exposure</span><span>On-route</span>
</div>
<div className="trail__plot">
<svg viewBox={`0 0 ${VBW} ${VBH}`} preserveAspectRatio="xMidYMid meet" className="trail__svg">
<defs>
<linearGradient id="hypso" x1="0" y1={PAD.t} x2="0" y2={baseY} gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="var(--tone-risk)" stopOpacity="0.20" />
<stop offset="45%" stopColor="var(--tone-partial)" stopOpacity="0.12" />
<stop offset="100%" stopColor="var(--tone-stable)" stopOpacity="0.08" />
</linearGradient>
</defs>
{/* elevation grid */}
{gridY.map((y, i) => (
<line key={i} x1={PAD.l} y1={y} x2={VBW - PAD.r} y2={y}
stroke="var(--rule)" strokeWidth="1" strokeDasharray="2 6" />
))}
<line x1={PAD.l} y1={baseY} x2={VBW - PAD.r} y2={baseY} stroke="var(--rule-strong)" strokeWidth="1.2" />
{/* hypsometric fill + ridge line */}
<path d={area} fill="url(#hypso)" />
<path d={line} fill="none" stroke="var(--ink-3)" strokeWidth="2.4"
strokeLinecap="round" strokeLinejoin="round" />
{/* drop stems + waypoint nodes (selectable) */}
{pts.map((p) => {
const sel = p.ep.episode_id === selectedId;
return (
<g key={p.ep.episode_id} className="trail__node" onClick={() => onSelect(p.ep.episode_id)}>
<line x1={p.x} y1={p.y} x2={p.x} y2={baseY} stroke={toneColor(p.tone)} strokeWidth="1" strokeOpacity="0.4" />
<circle cx={p.x} cy={p.y} r={sel ? 13 : 9} fill="var(--paper-3)"
stroke={toneColor(p.tone)} strokeWidth={sel ? 4 : 3} />
<circle cx={p.x} cy={p.y} r="3" fill={toneColor(p.tone)} />
</g>
);
})}
</svg>
{/* HTML waypoint flags positioned over the SVG */}
{pts.map((p, i) => {
const sel = p.ep.episode_id === selectedId;
const above = p.fy > 46;
const edge = i === 0 ? " wp--first" : i === pts.length - 1 ? " wp--last" : "";
return (
<button
key={p.ep.episode_id}
className={
"wp"
+ (compact ? " wp--compact" : "")
+ (sel ? " wp--sel" : "")
+ (above ? " wp--above" : " wp--below")
+ edge
}
style={{ left: p.fx + "%", top: p.fy + "%", "--tone": toneColor(p.tone) }}
title={`${p.ep.episode_id}: ${p.ep.title}`}
aria-label={`Select ${p.ep.episode_id}: ${p.ep.title}`}
onClick={() => onSelect(p.ep.episode_id)}
>
<span className="wp__id mono">{p.ep.episode_id}</span>
{(!compact || sel) ? <span className="wp__title">{p.ep.title}</span> : null}
{(!compact || sel) ? <span className="wp__dur mono">{p.ep.message_span.duration_label}</span> : null}
</button>
);
})}
</div>
</div>
<div className="trail__xaxis">
<span className="mono">start · {episodes[0].message_span.start_time}</span>
<span className="label">progress through session →</span>
<span className="mono">end · {episodes[episodes.length - 1].message_span.end_time}</span>
</div>
</div>
);
}
function EpisodeRail({ episodes, selectedId, onSelect }) {
if (episodes.length <= 6) return null;
return (
<div className="episode-rail" aria-label="Episode navigation">
<span className="label episode-rail__label">Episodes</span>
<div className="episode-rail__items">
{episodes.map((ep) => {
const tone = toneOf(ep.recovery_pattern);
const selected = ep.episode_id === selectedId;
return (
<button
key={ep.episode_id}
className={"episode-rail__item" + (selected ? " episode-rail__item--sel" : "")}
style={{ "--tone": toneColor(tone) }}
onClick={() => onSelect(ep.episode_id)}
title={`${ep.episode_id}: ${ep.title}`}
>
<ToneDot tone={tone} size={9} />
<span className="episode-rail__id mono">{ep.episode_id}</span>
<span className="episode-rail__title">{ep.title}</span>
</button>
);
})}
</div>
</div>
);
}
// ---- Episode detail (used by both layouts) ----
function EpisodeDetail({ ep }) {
if (!ep) return null;
const tone = toneOf(ep.recovery_pattern);
const tm = window.TFN.TONE_META[tone];
return (
<div className="epd card card--raised" style={{ "--tone": toneColor(tone) }}>
<div className="epd__band" />
<div className="epd__head">
<div className="epd__id">
<span className="mono epd__no">{ep.episode_id}</span>
<ToneDot tone={tone} size={12} />
</div>
<div>
<h3 className="epd__title">{ep.title}</h3>
<div className="epd__meta mono">
{tm.label} · {ep.message_span.duration_label} · {ep.message_span.start_time}–{ep.message_span.end_time}
</div>
</div>
</div>
<div className="epd__flow">
{[
["Intention", ep.initial_intention],
["Difficulty", ep.reported_difficulty],
["Reroute", ep.strategy_after],
].map(([k, v]) => (
<div className="epd__step" key={k}>
<span className="label">{k}</span>
<p>{v}</p>
</div>
))}
</div>
<hr className="rule--dashed" />
<div className="epd__codes">
<CodeChip field="difficulty_type" code={ep.difficulty_type} />
<CodeChip field="appraisal" code={ep.appraisal} />
<CodeChip field="detour_type" code={ep.detour_type} />
<CodeChip field="resolution_mode" code={ep.resolution_mode} />
<CodeChip field="recovery_pattern" code={ep.recovery_pattern} withDotTone={tone} />
<CodeChip field="outcome_claim" code={ep.outcome_claim} />
</div>
{ep.evidence_quotes && ep.evidence_quotes.length ? (
<div className="epd__quotes">
<span className="label">Evidence — agent's own words</span>
{ep.evidence_quotes.map((q, i) => (
<blockquote key={i} className="quote">{q}</blockquote>
))}
</div>
) : null}
<div className="epd__memo">
<span className="label label--accent">Analyst memo</span>
<p>{ep.analyst_memo}</p>
</div>
</div>
);
}
// ---- Ledger (vertical) timeline variant ----
function LedgerTimeline({ episodes, selectedId, onSelect }) {
return (
<div className="ledger">
{episodes.map((ep) => {
const tone = toneOf(ep.recovery_pattern);
const sel = ep.episode_id === selectedId;
return (
<button key={ep.episode_id}
className={"ledger__row" + (sel ? " ledger__row--sel" : "")}
style={{ "--tone": toneColor(tone) }}
onClick={() => onSelect(ep.episode_id)}>
<span className="ledger__rail"><ToneDot tone={tone} size={13} /></span>
<span className="ledger__id mono">{ep.episode_id}</span>
<span className="ledger__main">
<span className="ledger__title">{ep.title}</span>
<span className="ledger__sub">{window.TFN.CODEBOOK.difficulty_type[ep.difficulty_type]} → {window.TFN.CODEBOOK.recovery_pattern[ep.recovery_pattern]}</span>
</span>
<span className="ledger__dur mono">{ep.message_span.duration_label}</span>
</button>
);
})}
</div>
);
}
Object.assign(window, { TrailMap, EpisodeRail, EpisodeDetail, LedgerTimeline });
/* ============================================================
report.jsx — the field report: verdict, trail, analysis sections
============================================================ */
const HONESTY = {
resolved_with_confidence: { tone: "stable", note: "Clear, committed claim." },
resolved_with_caveat: { tone: "stable", note: "States its own limits." },
partially_resolved: { tone: "partial", note: "Honest partial." },
not_resolved: { tone: "partial", note: "Admits it's unresolved." },
needs_verification: { tone: "partial", note: "Flags a verification gap." },
uncertain_but_proceeding: { tone: "partial", note: "Proceeds under stated uncertainty." },
premature_success_claim: { tone: "risk", note: "Claim outruns the evidence." },
unknown: { tone: "unknown", note: "—" },
};
// download helper for the export buttons (no-op if the backend didn't supply text)
function dl(text, filename, mime) {
if (!text) return;
const blob = new Blob([text], { type: mime || "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = filename; document.body.appendChild(a); a.click();
a.remove(); setTimeout(() => URL.revokeObjectURL(url), 1500);
}
function ReportHeader({ data }) {
return (
<div className="rhead">
<div className="rhead__tag mono">FIELD LOG № {data.agent_type_guess === "codex" ? "C-01" : "CC-04"}</div>
<div className="rhead__main">
<Label accent>Trace</Label>
<h1 className="rhead__file mono">{data.trace_title}</h1>
</div>
<dl className="rhead__grid">
{[
["Agent", data.agent_type_guess.replace("_", " ")],
["Captured", data.captured],
["Scope", "narrative msgs only"],
["Messages", String(data.narrative_message_count)],
["Engine", data.engine],
["Redactions", String(data.redaction_count)],
].map(([k, v]) => (
<div key={k} className="rhead__cell">
<dt className="label">{k}</dt>
<dd className="mono">{v}</dd>
</div>
))}
</dl>
</div>
);
}
function ModelStatus({ data }) {
const notes = (data.privacy_notes || []).filter((note) =>
/^(Analysis produced|Model analysis|Model assist|Unknown analysis engine)/.test(String(note))
);
if (!notes.length) return null;
const fellBack = notes.some((note) =>
/unavailable|rule-based analysis was returned|deterministic analysis was returned|unknown analysis engine/i.test(note)
);
return (
<div className="privacy model-status">
<span className="privacy__mark">{fellBack ? "!" : "✓"}</span>
<p>
<b>{fellBack
? "Model unavailable — showing the rule-based analysis instead."
: "This report was written by the model."}</b>{" "}
{notes.join(" ")}
</p>
</div>
);
}
function Verdict({ data }) {
const v = data.verdict;
const tm = window.TFN.TONE_META[v.tone];
const honestyWord = v.honesty === "overclaimed" ? "Overclaimed close-out"
: v.honesty === "candid" ? "Candid close-out" : "Mixed close-out";
return (
<div className="verdict card card--raised" style={{ "--tone": toneColor(v.tone) }}>
<div className="verdict__band" />
<div className="verdict__left">
<Kicker>Trail verdict</Kicker>
<h2 className="verdict__headline">{v.headline}</h2>
<p className="verdict__detail">{v.detail}</p>
<div className="verdict__stamps">
<Stamp tone={v.tone}>{honestyWord}</Stamp>
</div>
</div>
<div className="verdict__right">
<div className="verdict__gauge" style={{ "--tone": toneColor(v.tone) }}>
<span className="verdict__gauge-label label">Recovery read</span>
<span className="verdict__gauge-val">{tm.rating}</span>
<span className="verdict__gauge-blurb">{tm.blurb}</span>
</div>
<div className="verdict__stats">
<div><span className="verdict__num mono">{data.episodes.length}</span><span className="label">episodes</span></div>
<div><span className="verdict__num mono">{data.duration_total}</span><span className="label">on trail</span></div>
</div>
</div>
</div>
);
}
function Legend() {
const order = ["stable", "detour", "iterative", "partial", "risk", "unknown"];
const M = window.TFN.TONE_META;
return (
<div className="legend">
<span className="label">Waypoint key</span>
<div className="legend__items">
{order.map((t) => (
<span className="legend__item" key={t}>
<ToneDot tone={t} size={11} />
<span className="legend__txt"><b>{M[t].label}</b> · {M[t].rating}</span>
</span>
))}
</div>
</div>
);
}
function TrailSection({ data, variant, selectedId, setSelectedId }) {
const ep = data.episodes.find((e) => e.episode_id === selectedId) || data.episodes[0];
return (
<section className="sec">
<SectionHead index="01" kicker="Journey · elevation profile"
title="Where the route climbed into hazard"
sub="Each waypoint is a difficulty episode. The line rises with risk — open ground low, exposed claims high. Tap a waypoint to read it." />
<div className="card trail-card">
{variant === "ledger"
? <LedgerTimeline episodes={data.episodes} selectedId={ep.episode_id} onSelect={setSelectedId} />
: <TrailMap episodes={data.episodes} selectedId={ep.episode_id} onSelect={setSelectedId} />}
<hr className="rule" />
<Legend />
<EpisodeRail episodes={data.episodes} selectedId={ep.episode_id} onSelect={setSelectedId} />
</div>
<EpisodeDetail ep={ep} />
</section>
);
}
function DifficultyMap({ data }) {
const clusters = {};
data.episodes.forEach((e) => {
(clusters[e.difficulty_type] = clusters[e.difficulty_type] || []).push(e);
});
const CB = window.TFN.CODEBOOK.difficulty_type;
const entries = Object.entries(clusters).sort((a, b) => b[1].length - a[1].length);
return (
<section className="sec">
<SectionHead index="02" kicker="Terrain" title="What kind of ground it was"
sub="Difficulties grouped by type — the recurring terrain, not a leaderboard." />
<div className="dmap">
{entries.map(([type, eps]) => {
const quote = (eps.find((e) => e.evidence_quotes && e.evidence_quotes.length) || {}).evidence_quotes;
return (
<div className="dmap__cell card" key={type}>
<div className="dmap__hd">
<span className="dmap__type">{CB[type] || type}</span>
<span className="dmap__ids mono">{eps.map((e) => e.episode_id).join(" · ")}</span>
</div>
{quote ? <blockquote className="quote quote--sm">{quote[0]}</blockquote> : <p className="muted">No short evidence quote.</p>}
</div>
);
})}
</div>
</section>
);
}
function DetourAnalysis({ data }) {
const groups = { yes: [], mixed: [], no: [] };
data.episodes.forEach((e) => { if (groups[e.productive_detour]) groups[e.productive_detour].push(e); });
const defs = [
["yes", "Productive detours", "Off-route, but a better line emerged.", "detour"],
["mixed", "Mixed", "A reroute with real upside and a loose end.", "partial"],
["no", "Wandering / workaround", "Movement without a new line on the problem.", "risk"],
];
return (
<section className="sec">
<SectionHead index="03" kicker="Route choices" title="Detours — exploration or wandering?"
sub="The question that actually matters: when it left the planned path, did it find a better one?" />
<div className="detour">
{defs.map(([key, title, blurb, tone]) => (
<div className="detour__col card" key={key} style={{ "--tone": toneColor(tone) }}>
<div className="detour__hd">
<ToneDot tone={tone} size={11} />
<span className="detour__title">{title}</span>
<span className="detour__count mono">{groups[key].length}</span>
</div>
<p className="detour__blurb">{blurb}</p>
<div className="detour__list">
{groups[key].length ? groups[key].map((e) => (
<div className="detour__ep" key={e.episode_id}>
<span className="mono detour__epid">{e.episode_id}</span>
<CodeChip field="detour_type" code={e.detour_type} />
</div>
)) : <span className="muted detour__none">None observed.</span>}
</div>
</div>
))}
</div>
</section>
);
}
function RecoveryPattern({ data }) {
const p = data.overall_patterns;
const rows = [
["Difficulty style", p.difficulty_style],
["Detour style", p.detour_style],
["Recovery style", p.recovery_style],
["Standing caveat", p.risk_or_caveat],
];
return (
<section className="sec">
<SectionHead index="04" kicker="Field naturalist's read" title="How this agent travels"
sub="A behavioral read across the whole session — its habits under difficulty." />
<div className="recov card card--raised">
{rows.map(([k, v], i) => (
<div className="recov__row" key={k}>
<span className="recov__no mono">{String(i + 1).padStart(2, "0")}</span>
<span className="label recov__k">{k}</span>
<p className="recov__v">{v}</p>
</div>
))}
</div>
</section>
);
}
function OutcomeAudit({ data }) {
const CB = window.TFN.CODEBOOK.outcome_claim;
return (
<section className="sec">
<SectionHead index="05" kicker="Closeout audit" title="What it said when it called it done"
sub="Not whether the code is correct — whether the agent's claim matches its own evidence." />
<div className="audit card">
{data.episodes.map((e) => {
const h = HONESTY[e.outcome_claim] || HONESTY.unknown;
return (
<div className="audit__row" key={e.episode_id} style={{ "--tone": toneColor(h.tone) }}>
<div className="audit__rail"><span className="mono">{e.episode_id}</span><ToneDot tone={h.tone} size={11} /></div>
<div className="audit__body">
<div className="audit__claim">
<span className="audit__verb">{CB[e.outcome_claim] || e.outcome_claim}</span>
<span className="audit__note">{h.note}</span>
</div>
{e.evidence_quotes && e.evidence_quotes.length ? (
<blockquote className="quote quote--sm">{e.evidence_quotes[e.evidence_quotes.length - 1]}</blockquote>
) : null}
</div>
</div>
);
})}
</div>
</section>
);
}
function PrivacyExports({ data, onReset }) {
return (
<section className="sec">
<div className="px">
<div className="px__notes card">
<SectionHead kicker="Privacy ledger" title={`${data.redaction_count} item${data.redaction_count === 1 ? "" : "s"} redacted before analysis`} />
<ul className="px__list">
{data.privacy_notes.map((n, i) => <li key={i}>{n}</li>)}
</ul>
</div>
<div className="px__exports card card--raised">
<Label accent>Take it with you</Label>
<p className="px__blurb">Export the redacted narrative and the structured findings. The raw trace never leaves your machine.</p>
<div className="px__btns">
<button className="btn btn--sm" onClick={() => dl(data.exports && data.exports.narrative_md, (data.trace_title||"trace")+"-redacted.md", "text/markdown")}><span></span> Redacted narrative .md</button>
<button className="btn btn--sm" onClick={() => dl(data.exports && data.exports.report_md, (data.trace_title||"trace")+"-field-report.md", "text/markdown")}><span></span> Field report .md</button>
<button className="btn btn--sm" onClick={() => dl(data.exports && data.exports.episodes_json, (data.trace_title||"trace")+"-episodes.json", "application/json")}><span></span> Episodes .json</button>
</div>
<hr className="rule--dashed" />
<button className="btn btn--ghost btn--sm" onClick={onReset}>← Analyze another trace</button>
</div>
</div>
</section>
);
}
function ReportView({ data, variant, onReset }) {
const [selectedId, setSelectedId] = React.useState(
() => (data.verdict.tone === "risk"
? (data.episodes.find((e) => toneOf(e.recovery_pattern) === "risk") || data.episodes[0]).episode_id
: data.episodes[0].episode_id)
);
React.useEffect(() => {
setSelectedId(data.episodes[0].episode_id);
}, [data]);
return (
<div className="report">
<ReportHeader data={data} />
<ModelStatus data={data} />
<Verdict data={data} />
<TrailSection data={data} variant={variant} selectedId={selectedId} setSelectedId={setSelectedId} />
<DifficultyMap data={data} />
<DetourAnalysis data={data} />
<RecoveryPattern data={data} />
<OutcomeAudit data={data} />
<PrivacyExports data={data} onReset={onReset} />
</div>
);
}
Object.assign(window, { ReportView });