From a719ad599043a11a21227f4a629bacf2c47800b4 Mon Sep 17 00:00:00 2001 From: mptyl Date: Fri, 3 Jul 2026 18:15:48 +0200 Subject: [PATCH] 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 --- frontend/src/viewers/ArtifactView.test.tsx | 20 +++++++-- frontend/src/viewers/ArtifactView.tsx | 52 ++++++++++++++++++++-- frontend/src/viewers/MarkdownView.tsx | 2 +- 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/frontend/src/viewers/ArtifactView.test.tsx b/frontend/src/viewers/ArtifactView.test.tsx index ccf02393..2fe33ffc 100644 --- a/frontend/src/viewers/ArtifactView.test.tsx +++ b/frontend/src/viewers/ArtifactView.test.tsx @@ -26,9 +26,23 @@ test("question renders markdown headings", () => { expect(screen.getByText("Domanda")).toBeInTheDocument(); }); -test("unknown kind falls back to formatted JSON", () => { - render(); - expect(screen.getByText(/"a": 1/)).toBeInTheDocument(); +test("unknown object kind renders as formatted sections, never raw JSON", () => { + render( + , + ); + // Humanized section labels, values as text/bullets — and NO raw JSON braces. + expect(screen.getByText("Recap")).toBeInTheDocument(); + expect(screen.getByText("Chiarimenti risolti")).toBeInTheDocument(); + expect(screen.getByText("finestra_temporale — 5 anni")).toBeInTheDocument(); + expect(screen.queryByText(/"recap"/)).not.toBeInTheDocument(); }); test("schema_linking renders the schema viewer", async () => { diff --git a/frontend/src/viewers/ArtifactView.tsx b/frontend/src/viewers/ArtifactView.tsx index 14903191..c5bc5ba6 100644 --- a/frontend/src/viewers/ArtifactView.tsx +++ b/frontend/src/viewers/ArtifactView.tsx @@ -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

—

; + } + if (typeof value === "string") return ; + if (typeof value === "number" || typeof value === "boolean") { + return

{String(value)}

; + } + if (Array.isArray(value)) { + const allPrimitive = value.every((v) => v === null || typeof v !== "object"); + if (allPrimitive) { + return ( +
    + {value.map((item, i) =>
  • {String(item)}
  • )} +
+ ); + } + return ( +
+ {value.map((item, i) => ( +
+ +
+ ))} +
+ ); + } return ( -
{text}
+
+ {Object.entries(value as Record).map(([k, v]) => ( +
+

+ {humanizeKey(k)} +

+ +
+ ))} +
); } @@ -104,5 +147,6 @@ export function ArtifactView({ artifact }: { artifact: ArtifactData }): ReactEle const md = toMarkdown(data); if (md !== null) return ; } - return ; + // Fallback for any other kind / shape: formatted text, never raw JSON. + return ; } diff --git a/frontend/src/viewers/MarkdownView.tsx b/frontend/src/viewers/MarkdownView.tsx index aa5d0032..93fd5e33 100644 --- a/frontend/src/viewers/MarkdownView.tsx +++ b/frontend/src/viewers/MarkdownView.tsx @@ -42,7 +42,7 @@ const components: Components = { export function MarkdownView({ source }: { source: string }) { return ( -
+
{source}