)}
);
}
// Collapsible section wrapper used for Q&A / Verdicts / Plan
function CollapsibleSection({
title,
defaultOpen,
children,
}: {
title: string;
defaultOpen: boolean;
children: React.ReactNode;
}) {
return (
{title}
{children}
);
}
function asText(v: unknown): string {
return typeof v === "string" ? v : v == null ? "" : JSON.stringify(v, null, 2);
}
// Q&A: each entry is the answer text for a turn (a string), or an older
// {turn, question_id, answer} dict. Render each as a turn card with the answer
// as markdown so numbered sub-answers / bold render readably.
function QASection({ entries }: { entries: unknown[] }) {
return (
);
}
// Review verdicts: each {verdict, round_index, outcome, findings} → a card with a
// verdict badge + round + outcome header and the findings rendered as markdown.
function VerdictsSection({ entries }: { entries: unknown[] }) {
return (
{entries.map((entry, i) => {
const v =
entry && typeof entry === "object" ? (entry as Record) : {};
const verdict = asText(v.verdict) || "verdict";
const round = "round_index" in v ? Number(v.round_index) : i + 1;
const outcome = asText(v.outcome);
const findings = asText(v.findings);
return (
);
}
// Plan: {summary, phases:[{name, steps:[...]}]} → summary (markdown) + a phase
// list with nested steps. Any extra scalar keys render as a small label/value
// list; an unrecognized shape falls back to a collapsed JSON view.
function PlanSection({ plan }: { plan: Record }) {
const summary = asText(plan.summary);
const phases = Array.isArray(plan.phases) ? plan.phases : null;
const extra = Object.entries(plan).filter(
([k, val]) =>
k !== "summary" && k !== "phases" && (typeof val !== "object" || val === null),
);
if (!summary && !phases) {
return (