Structured output A comparison answered as a grid. Real table markup so a screen reader can associate a value with its row and column, a source chip on any cell that carries a fact, a scrolling container past a few columns, and a control to ask for a different shape without re-asking. npx shadcn@latest add table button Tokens this needs: --card, --card-foreground, --muted, --muted-foreground, --border, --accent ──────────────────────────────────────────────────────────────────────── // AnswerTable.tsx ──────────────────────────────────────────────────────────────────────── import { Button } from "@/components/ui/button"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; /** * A comparison answered as a grid. * * Real table markup, because a grid drawn with spacing is a wall of numbers * to a screen reader; header cells are headers. Each cell can carry its own * source, because a number in a cell is believed in a way the same number in * a sentence is not, and one chip on the whole answer does not say which * cells it covers. The table scrolls inside its own box past a few columns * rather than pushing the page sideways. */ export type Source = { id: number; label: string; href?: string }; export type Cell = { value: string; source?: Source }; export type Shape = "table" | "list"; type Props = { columns: string[]; /** One entry per column, in column order. */ rows: Cell[][]; onSource: (source: Source) => void; /** The model routed this answer to a table. The viewer may disagree without * asking the question again. */ onReshape: (shape: Shape) => void; onExport: (csv: string) => void; }; /** RFC 4180: quote a field that holds a comma, a quote or a newline. */ const field = (s: string) => (/[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s); export const toCsv = (columns: string[], rows: Cell[][]) => [columns, ...rows.map((r) => r.map((c) => c.value))].map((r) => r.map(field).join(",")).join("\n"); export function AnswerTable({ columns, rows, onSource, onReshape, onExport }: Props) { return (