feat(frontend): v2 artifact viewers for cte_plan/cte_result/phase

Add CtePlanViewer, CteResultViewer, and PhaseSummaryViewer to render the
structured v2 payloads (schema_version: 2) the harness now emits for
artifact-gate widgets, per contracts.md. Extract PreviewGrid from
ResultsPanel as a reusable AG Grid component shared by CteResultViewer.
ArtifactView dispatches to the new viewers on a schema_version/shape
guard, falling back to the existing legacy renderers unchanged for
older sessions and replay fixtures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 00:57:20 +02:00
co-authored by Claude Fable 5
parent 3ad93cd02f
commit 1c97289ed8
12 changed files with 937 additions and 26 deletions
+121
View File
@@ -0,0 +1,121 @@
import { Badge } from "../components/ui/badge";
import { MarkdownView } from "./MarkdownView";
import type { PhaseCheck, PhaseSection, PhaseSectionItem, PhaseSummaryV2, PhaseTable } from "./artifactV2";
const CODE_CHIP = "rounded-md bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground/90";
function checkBadgeVariant(status: string): "default" | "destructive" | "outline" {
if (status === "ok") return "default";
if (status === "fail") return "destructive";
return "outline";
}
function Check({ check }: { check: PhaseCheck }) {
return (
<div className="flex items-start gap-2">
<Badge variant={checkBadgeVariant(check.status)}>{check.status}</Badge>
<div className="flex flex-col">
<span className="text-sm text-foreground/90">{check.label}</span>
{check.detail && <span className="text-xs text-muted-foreground">{check.detail}</span>}
</div>
</div>
);
}
function SectionItemRow({ item }: { item: PhaseSectionItem }) {
const tableColumn = item.table && item.column ? `${item.table}.${item.column}` : item.table ?? item.column;
return (
<div className="flex flex-col gap-0.5">
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm text-foreground/90">{item.label}</span>
{tableColumn && <span className={CODE_CHIP}>{tableColumn}</span>}
{item.value && <Badge variant="outline">{item.value}</Badge>}
</div>
{item.description && <p className="text-xs text-muted-foreground">{item.description}</p>}
{item.rationale && <p className="text-xs italic text-muted-foreground">{item.rationale}</p>}
</div>
);
}
function Section({ section }: { section: PhaseSection }) {
return (
<div>
<h4 className="thot-label mb-1.5">{section.title}</h4>
<div className="flex flex-col gap-2">
{section.items.map((item, i) => (
<SectionItemRow key={i} item={item} />
))}
</div>
</div>
);
}
function TableRecap({ table }: { table: PhaseTable }) {
return (
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span className="font-mono text-sm">{table.name}</span>
<Badge variant={table.role === "promoted" ? "default" : "outline"}>{table.role}</Badge>
</div>
{table.description && <p className="text-xs text-muted-foreground">{table.description}</p>}
{table.columns && table.columns.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{table.columns.map((c) => (
<div key={c.name} className="flex flex-col">
<span className={CODE_CHIP}>{c.name}</span>
{c.description && <span className="text-xs text-muted-foreground">{c.description}</span>}
{c.value_filter && <span className={CODE_CHIP}>{c.value_filter}</span>}
</div>
))}
</div>
)}
</div>
);
}
export function PhaseSummaryViewer({ phase }: { phase: PhaseSummaryV2 }) {
return (
<div className="flex flex-col gap-4">
<h3 className="thot-label">
{phase.phase.id} — {phase.phase.name}
</h3>
{phase.summary && <MarkdownView source={phase.summary} />}
{phase.checks && phase.checks.length > 0 && (
<div className="flex flex-col gap-2">
{phase.checks.map((c, i) => (
<Check key={i} check={c} />
))}
</div>
)}
{phase.sections && phase.sections.length > 0 && (
<div className="flex flex-col gap-4">
{phase.sections.map((s, i) => (
<Section key={i} section={s} />
))}
</div>
)}
{phase.tables && phase.tables.length > 0 && (
<div className="flex flex-col gap-3">
{phase.tables.map((t) => (
<TableRecap key={t.name} table={t} />
))}
</div>
)}
{phase.open_questions && phase.open_questions.length > 0 && (
<div>
<h4 className="thot-label mb-1">Open questions</h4>
<ul className="list-disc space-y-1 pl-5 text-sm text-foreground/90 marker:text-muted-foreground">
{phase.open_questions.map((q, i) => (
<li key={i}>{q}</li>
))}
</ul>
</div>
)}
</div>
);
}