// Lazy-fetched schemas for the typed fenced blocks the assistant can emit. // The system prompt only ships a thin index (kind → one-line purpose). Full // JSON schemas + examples live here and are pulled on demand via the // `get_block_schema` tool. Keeps the always-on prompt small and lets new // blocks be added by editing this file alone — no prompt edits required. // // Renderer is in app/components/assistant/message-body.tsx — keep these in // sync (kinds, field names) when adding or changing blocks. export type BlockKind = | "kpi" | "table" | "chart-bar" | "chart-line" | "chart-donut" | "chart-spark" | "code" | "diff" | "card" | "flowchart" | "orgchart" | "steps" | "checklist" | "welcome" | "hint" export const BLOCK_INDEX: Record = { kpi: "Headline numbers row (2–6 metrics).", table: "Tabular data (≥3 rows or ≥3 columns).", "chart-bar": "Compare ≤8 categories.", "chart-line": "Ordered series / trend over time.", "chart-donut": "Part-to-whole, ≤5 slices.", "chart-spark": "Inline trend, no axes.", code: "Syntax-highlighted snippet (SQL, JSON, YAML, etc).", diff: "Before/after comparison.", card: "Inline pill, stat chip, or callout banner.", flowchart: "Process / decision flow with shaped nodes (start/end/process/decision/io).", orgchart: "Tree of nested entities (org structure, dependency tree, taxonomy).", steps: "Multi-step plan with statuses (queued/running/done/error/skipped).", checklist: "Onboarding checklist with completable tasks (links/CTAs allowed).", welcome: "Hero welcome card with title, description, primary/secondary CTA.", hint: "Tip / lightbulb card with tone (info/success/warning/neutral/primary).", } const SCHEMAS: Record = { kpi: `\`\`\`kpi { "items": [ { "label": "Tenants", "value": 42 }, { "label": "Active users", "value": 318, "unit": "/day" } ] } \`\`\` Fields: items[]: { label: string, value: string|number, unit?: string }. Use 2–6 items. Don't repeat the numbers in prose.`, table: `\`\`\`table { "columns": [ { "id": "slug", "header": "Tenant" }, { "id": "users", "header": "Users", "align": "right" }, { "id": "status", "header": "Status" } ], "rows": [ { "slug": "acme", "users": 42, "status": "active" }, { "slug": "globex", "users": 18, "status": "suspended" } ], "idKey": "slug" } \`\`\` Fields: - columns[]: { id: string, header?: string, align?: "left"|"center"|"right", sortable?: boolean } - rows[]: object keyed by column id. - idKey?: string — column whose value is the row id (defaults to first column). Use for ≥3 rows OR ≥3 columns. Smaller lists → markdown table.`, "chart-bar": `\`\`\`chart-bar { "title": "Users by tenant", "data": [ { "label": "acme", "value": 42 }, { "label": "globex", "value": 18 } ] } \`\`\` Fields: title?: string, data[]: { label: string, value: number, color?: string }. ≤8 categories. For more, use a table.`, "chart-line": `\`\`\`chart-line { "title": "Signups over time", "series": [ { "x": 1, "y": 12 }, { "x": 2, "y": 19 }, { "x": 3, "y": 24 } ] } \`\`\` Fields: title?: string, series[]: { x: number, y: number }. Use for ordered numeric series ≥3 points. x is treated as a numeric axis.`, "chart-donut": `\`\`\`chart-donut { "title": "Status breakdown", "data": [ { "label": "active", "value": 38 }, { "label": "suspended", "value": 4 } ] } \`\`\` Fields: title?: string, data[]: { label: string, value: number, color?: string }. ≤5 slices. Skip if one slice would be >90%.`, "chart-spark": `\`\`\`chart-spark { "values": [3, 5, 4, 8, 12, 9, 14] } \`\`\` Fields: values: number[], width?, height?, stroke?, fill?. Use inline next to a single number to show its recent trend.`, code: `\`\`\`code { "code": "SELECT count(*) FROM tenants WHERE status='active';", "language": "sql", "title": "Active tenant count", "lineNumbers": false, "highlightLines": [] } \`\`\` Fields: code: string, language?: string, title?: string, lineNumbers?: boolean, highlightLines?: number[]. Languages with syntax: js/ts/tsx, python, rust, go, html, css, sql, json, yaml. Prefer this over plain markdown fences when the snippet matters (queries the user might copy, configs, etc.).`, diff: `\`\`\`diff { "oldCode": "max_users: 100\\n", "newCode": "max_users: 250\\n", "language": "yaml", "title": "Tenant quota change", "mode": "unified" } \`\`\` Fields: oldCode: string, newCode: string, language?: string, title?: string, mode?: "unified"|"split". Use for showing exactly what changed in a config, query, or file.`, card: `\`\`\`card { "kind": "callout", "tone": "warning", "title": "Heads up", "body": "This action is destructive." } \`\`\` Three sub-kinds: - pill: { "kind": "pill", "status": "active"|"suspended"|"deactivated"|other, "label"?: string } — small status badge. - stat: { "kind": "stat", "label": string, "value": string|number } — inline metric chip. - callout: { "kind": "callout", "tone": "info"|"warning"|"danger"|"success", "title"?: string, "body"?: string } — banner. Use sparingly. For multiple metrics use \`kpi\` instead of multiple \`stat\` cards.`, flowchart: `\`\`\`flowchart { "nodes": [ { "id": "a", "type": "start", "label": "Receive request", "x": 60, "y": 20 }, { "id": "b", "type": "process", "label": "Validate token", "x": 60, "y": 100 }, { "id": "c", "type": "decision", "label": "Token valid?", "x": 60, "y": 180 }, { "id": "d", "type": "process", "label": "Process", "x": 220, "y": 180 }, { "id": "e", "type": "end", "label": "Reject", "x": 60, "y": 280 } ], "edges": [ { "from": "a", "to": "b" }, { "from": "b", "to": "c" }, { "from": "c", "to": "d", "label": "yes" }, { "from": "c", "to": "e", "label": "no" } ] } \`\`\` Fields: - nodes[]: { id: string, type: "start"|"end"|"process"|"decision"|"io", label: string, x: number, y: number } — coordinates in pixels (canvas auto-sizes). - edges[]: { from: nodeId, to: nodeId, label?: string }. Use for control flow, workflows, request lifecycles. Keep ≤12 nodes; lay out top-to-bottom or left-to-right with ~80–120px spacing.`, orgchart: `\`\`\`orgchart { "data": { "id": "root", "name": "Platform", "title": "Tenant", "children": [ { "id": "a", "name": "Auth", "title": "Service", "children": [ { "id": "a1", "name": "Sessions", "title": "Module" }, { "id": "a2", "name": "MFA", "title": "Module" } ] }, { "id": "b", "name": "Billing", "title": "Service" } ] }, "horizontal": false } \`\`\` Fields: - data: OrgNode = { id: string, name: string, title?: string, avatar?: string (url), children?: OrgNode[] } - horizontal?: boolean — left-to-right vs top-to-bottom (default). Use for nested hierarchies (org charts, dependency trees, taxonomies). Skip for flat lists.`, steps: `\`\`\`steps { "steps": [ { "id": "1", "title": "List tenants", "status": "done", "detail": "Found 42 tenants" }, { "id": "2", "title": "Filter suspended", "status": "running" }, { "id": "3", "title": "Build report", "status": "queued" } ] } \`\`\` Fields: - steps[]: { id: string, title: string, status: "queued"|"planning"|"running"|"waiting"|"done"|"error"|"skipped", detail?: string, substeps?: same-shape[] } Use for: showing a multi-step plan you're about to execute, or a post-hoc trail of what you did. Skip for single-step actions.`, checklist: `\`\`\`checklist { "title": "Get started", "description": "Finish setting up your tenant.", "tasks": [ { "id": "1", "title": "Invite your team", "description": "Add at least one admin.", "completed": true, "estimate": "2 min" }, { "id": "2", "title": "Connect a storage bucket", "completed": false, "href": "/buckets", "estimate": "5 min" }, { "id": "3", "title": "Set up SSO", "completed": false, "optional": true, "href": "/sso", "estimate": "10 min" } ] } \`\`\` Fields: - title?: string, description?: string - tasks[]: { id: string, title: string, description?: string, completed?: boolean, optional?: boolean, estimate?: string, href?: string } Use for: actionable setup lists with progress. Each task with an href becomes a click-through link. Toggling is read-only in chat (can't persist completion across turns).`, welcome: `\`\`\`welcome { "title": "Welcome to Arcadia Admin", "description": "Manage tenants, users, and platform settings from one place.", "badge": "v2", "primaryAction": { "label": "Create your first tenant", "href": "/tenants" }, "secondaryAction": { "label": "Read the docs", "href": "/library" } } \`\`\` Fields: - title: string (required), description?: string, badge?: string - primaryAction?, secondaryAction?: { label: string, href?: string } Use sparingly — once at the top of a thread that's introducing a feature/product, never as a recurring response.`, hint: `\`\`\`hint { "title": "Tip", "tone": "info", "body": "Suspending a tenant blocks login but preserves data — use deactivate to permanently disable.", "action": { "label": "See suspension docs", "href": "/library?q=suspend" } } \`\`\` Fields: - title?: string, body: string - tone?: "info"|"success"|"warning"|"neutral"|"primary" (default "info") - action?: { label: string, href?: string } Use for: discoverability tips, gotchas, "did you know". One per reply.`, } const ALL_KINDS = Object.keys(SCHEMAS) as BlockKind[] export function isBlockKind(kind: string): kind is BlockKind { return (ALL_KINDS as string[]).includes(kind) } export function getBlockSchema(kind: string): string | null { if (!isBlockKind(kind)) return null return SCHEMAS[kind] } /** Thin index suitable for the always-on system prompt. */ export function blockIndexForPrompt(): string { const lines = ALL_KINDS.map((k) => ` ${k} — ${BLOCK_INDEX[k]}`) return [ "Rich output: when a UI primitive will communicate better than prose, emit a typed fenced ```\\n\\n``` block. The chat renderer turns it into a @crema/*-ui component inline at that position.", "", "Available kinds:", ...lines, "", "Before emitting a block for the FIRST time in a thread, call get_block_schema(kind) to fetch the exact JSON shape and field rules. Once you've seen a schema in this conversation, reuse it from memory.", "Always lead with one short sentence of prose, then the block. Don't repeat block data in prose.", "JSON must be valid (double quotes, no trailing commas). If unsure of the schema, fetch it.", ].join("\n") }