Rich output rendering: GFM markdown, tool-result blocks, card blocks

Three layers:

1. GFM markdown — add remark-gfm so tables, task lists, strikethrough,
   autolinks render properly. Style table elements (overflow-aware
   container, muted header, divider rows). Render `[ ]` task list items
   as visible checkboxes.

2. Structured tool-result rendering — new `tool-result-renderers.tsx`
   dispatches by tool name to render a small UI block beneath each
   ToolCallCard:
   - list_tenants → table with status pills + plan column
   - get_tenant → tenant detail card
   - get_platform_stats → KPI tiles (total + per-status)
   - list_audit_log → timeline rows with actor_type + action
   - list_users → user list with role chips
   - suspend_tenant / activate_tenant → tenant card with action confirm
   ToolCallCard collapses by default — operators expand for raw JSON.

3. Custom ```card``` blocks the LLM can emit inline:
   - {"kind":"pill","status":"…"} — status pill
   - {"kind":"stat","label":"…","value":…} — stat tile
   - {"kind":"callout","tone":"info|warning|danger|success",…} — callout
   Malformed blocks fall through to the prose unchanged. Client strips
   well-formed blocks from prose and renders them as components.

Domain primer updated to teach the model the card schemas and remind it
NOT to re-render tool-result data as markdown tables (that's done
automatically — it should add commentary only).

Layers are independent: 1 + 2 always work; 3 is purely additive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jules
2026-05-01 20:39:06 +10:00
parent b9a163c7cc
commit 45fa130951
6 changed files with 758 additions and 12 deletions

View File

@@ -78,6 +78,13 @@ import {
import { ARCADIA_KNOWLEDGE } from "~/lib/arcadia-knowledge"
import { formatAdminContextForPrompt } from "~/lib/admin-context"
import { ConfirmCard } from "~/components/assistant/confirm-card"
import { renderToolResult } from "~/components/assistant/tool-result-renderers"
function ToolResultBlock({ name, result }: { name: string; result: unknown }) {
const rich = renderToolResult(name, result)
if (!rich) return null
return <div className="px-1">{rich}</div>
}
const SNAPSHOT_KEY = "crema.ai.snapshot"
type StoredMessage = { role: "user" | "assistant"; content: string }
@@ -549,7 +556,12 @@ function ChatSurface({
{calls.length > 0 && (
<div className="self-start flex w-full max-w-[80ch] flex-col gap-2">
{calls.map((c) => (
<ToolCallCard key={c.id} call={c} defaultExpanded={c.status !== "success"} />
<div key={c.id} className="flex flex-col gap-2">
<ToolCallCard call={c} defaultExpanded={false} />
{c.status === "success" && (
<ToolResultBlock name={c.name} result={c.result} />
)}
</div>
))}
</div>
)}