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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user