feat(viewer): artifact modal renders structured text, never raw JSON

ArtifactView's fallback (StructuredValue) turns any object/array into labelled
sections, bullet lists and paragraphs (strings as markdown). Verified against the
real F1 gate (kind:"text", {recap, chiarimenti_risolti[], ...}). MarkdownView
uses the real .thot-prose class (Tailwind Typography is not installed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 18:15:48 +02:00
co-authored by Claude Opus 4.8
parent cec6620c00
commit a719ad5990
3 changed files with 66 additions and 8 deletions
+48 -4
View File
@@ -69,10 +69,53 @@ function cteNames(raw: unknown): string[] | null {
return null;
}
function JsonFallback({ value }: { value: unknown }): ReactElement {
const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
// "recap" -> "Recap", "chiarimenti_risolti" -> "Chiarimenti risolti"
function humanizeKey(k: string): string {
const spaced = k.replace(/[_-]+/g, " ").trim();
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
}
/** Renders any artifact payload as readable text — NEVER raw JSON. Objects become
* labelled sections, arrays become bullet lists, primitives become paragraphs;
* strings render as markdown. Nested structures recurse. */
function StructuredValue({ value }: { value: unknown }): ReactElement {
if (value === null || value === undefined || value === "") {
return <p className="text-sm text-muted-foreground">—</p>;
}
if (typeof value === "string") return <MarkdownView source={value} />;
if (typeof value === "number" || typeof value === "boolean") {
return <p className="text-sm leading-relaxed text-foreground/90">{String(value)}</p>;
}
if (Array.isArray(value)) {
const allPrimitive = value.every((v) => v === null || typeof v !== "object");
if (allPrimitive) {
return (
<ul className="list-disc space-y-1 pl-5 text-sm leading-relaxed text-foreground/90 marker:text-muted-foreground">
{value.map((item, i) => <li key={i}>{String(item)}</li>)}
</ul>
);
}
return (
<div className="flex flex-col gap-3">
{value.map((item, i) => (
<div key={i} className="rounded-lg border border-border/60 p-3">
<StructuredValue value={item} />
</div>
))}
</div>
);
}
return (
<pre className="overflow-auto whitespace-pre-wrap rounded bg-muted p-3 text-sm">{text}</pre>
<div className="flex flex-col gap-4">
{Object.entries(value as Record<string, unknown>).map(([k, v]) => (
<section key={k}>
<h4 className="mb-1.5 text-[0.72rem] font-semibold uppercase tracking-[0.1em] text-muted-foreground">
{humanizeKey(k)}
</h4>
<StructuredValue value={v} />
</section>
))}
</div>
);
}
@@ -104,5 +147,6 @@ export function ArtifactView({ artifact }: { artifact: ArtifactData }): ReactEle
const md = toMarkdown(data);
if (md !== null) return <MarkdownView source={md} />;
}
return <JsonFallback value={data} />;
// Fallback for any other kind / shape: formatted text, never raw JSON.
return <StructuredValue value={data} />;
}