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>
135 lines
4.4 KiB
TypeScript
135 lines
4.4 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
|
|
import { Badge } from "../components/ui/badge";
|
|
import type { CtePlanCte, CtePlanFilter, CtePlanV2 } from "./artifactV2";
|
|
|
|
const CHIP = "rounded-md bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground/90";
|
|
|
|
function Chip({ children }: { children: ReactNode }) {
|
|
return <span className={CHIP}>{children}</span>;
|
|
}
|
|
|
|
function FilterChip({ filter }: { filter: CtePlanFilter }) {
|
|
return (
|
|
<div className="flex flex-col gap-0.5">
|
|
<span className={CHIP}>
|
|
{filter.column} {filter.op} {filter.value}
|
|
</span>
|
|
{filter.description && <p className="text-xs text-muted-foreground">{filter.description}</p>}
|
|
{filter.rationale && <p className="text-xs text-muted-foreground">{filter.rationale}</p>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function CteCard({ cte, total }: { cte: CtePlanCte; total: number }) {
|
|
const dependsOn = cte.depends_on ?? [];
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant="outline" className="font-mono">
|
|
CTE {cte.index}/{total}
|
|
</Badge>
|
|
<CardTitle className="font-mono text-sm">{cte.name}</CardTitle>
|
|
</div>
|
|
{cte.purpose && <p className="text-sm text-muted-foreground">{cte.purpose}</p>}
|
|
</CardHeader>
|
|
<CardContent className="flex flex-col gap-3">
|
|
<div>
|
|
<h4 className="thot-label mb-1">Depends on</h4>
|
|
{dependsOn.length ? (
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{dependsOn.map((d) => (
|
|
<Chip key={d}>{d}</Chip>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-xs text-muted-foreground">no dependencies</p>
|
|
)}
|
|
</div>
|
|
|
|
{cte.tables && cte.tables.length > 0 && (
|
|
<div>
|
|
<h4 className="thot-label mb-1">Tables</h4>
|
|
<div className="flex flex-col gap-1">
|
|
{cte.tables.map((t) => (
|
|
<div key={t.name}>
|
|
<span className="font-mono text-sm">{t.name}</span>
|
|
{t.description && (
|
|
<p className="text-xs text-muted-foreground">{t.description}</p>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{cte.keys && cte.keys.length > 0 && (
|
|
<div>
|
|
<h4 className="thot-label mb-1">Keys</h4>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{cte.keys.map((k) => (
|
|
<Chip key={k}>{k}</Chip>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{cte.filters && cte.filters.length > 0 && (
|
|
<div>
|
|
<h4 className="thot-label mb-1">Filters</h4>
|
|
<div className="flex flex-col gap-2">
|
|
{cte.filters.map((f, i) => (
|
|
<FilterChip key={i} filter={f} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{cte.output_columns && cte.output_columns.length > 0 && (
|
|
<div>
|
|
<h4 className="thot-label mb-1">Output columns</h4>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{cte.output_columns.map((c) => (
|
|
<Chip key={c}>{c}</Chip>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{cte.rationale && (
|
|
<div>
|
|
<h4 className="thot-label mb-1">Rationale</h4>
|
|
<p className="text-sm text-foreground/90">{cte.rationale}</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export function CtePlanViewer({ plan }: { plan: CtePlanV2 }) {
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
{plan.question && <p className="text-sm text-muted-foreground">{plan.question}</p>}
|
|
{plan.strategy && <p className="thot-prose">{plan.strategy}</p>}
|
|
|
|
<div className="flex flex-wrap items-center gap-1.5 font-mono text-xs text-muted-foreground">
|
|
{plan.ctes.map((c, i) => (
|
|
<span key={c.name} className="flex items-center gap-1.5">
|
|
{i > 0 && <span aria-hidden="true">→</span>}
|
|
<span className="text-foreground/90">{c.name}</span>
|
|
</span>
|
|
))}
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-3">
|
|
{plan.ctes.map((cte) => (
|
|
<CteCard key={cte.name} cte={cte} total={plan.ctes.length} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|