// Renders an assistant message: GFM markdown for prose, plus typed fenced // code blocks rendered as rich UI from `@crema/*-ui` libs (charts, tables, // KPIs, code, diffs, status pills, callouts), pills for command-bus action // blocks, and tool-result cards (role: "tool"). // // Typed blocks recognized (each is a fenced ```\n\n``` block): // action — command-bus DSL (handled by extractActionBlocks; replaced // with a "Ran N actions" pill) // card — { kind: "pill" | "stat" | "callout", ... } (legacy) // chart-spark — { values: number[], stroke?, fill? } // chart-bar — { data: [{ label, value, color? }] } // chart-line — { series: [{ x, y }] } // chart-donut — { data: [{ label, value, color? }] } // table — { columns: [{ id, header, accessor? }], rows: [...] } // kpi — { items: [{ label, value, unit? }] } // code — { code, language?, title?, lineNumbers? } // diff — { oldCode, newCode, language?, title? } import { type ReactNode, useMemo } from "react" import ReactMarkdown from "react-markdown" import remarkGfm from "remark-gfm" import { Sparkles, Wrench } from "lucide-react" import { extractActionBlocks } from "@crema/action-bus" import { stripToolCallTags, type ToolCall } from "@crema/llm-ui" import { Sparkline, BarChart, LineChart, Donut, type ChartDatum, type SeriesPoint, } from "@crema/chart-ui" import { DataTable, type Column } from "@crema/table-ui" import { KPIRow } from "@crema/data-ui" import { CodeBlock, DiffViewer } from "@crema/code-ui" import { FlowChart, OrgChart } from "@crema/diagram-ui" import { StepTrail, type AgentStep } from "@crema/agent-ui" import { OnboardingChecklist, WelcomeCard, HintCard, type ChecklistTask, type OnboardingTone, } from "@crema/onboarding-ui" const ACTION_BLOCK_RE = /```action\s*\n[\s\S]*?```/g // Captures the kind tag and the body of every fenced block we render as UI. // Kept alongside the markdown ones — react-markdown ignores anything we strip. const TYPED_BLOCK_RE = /```(card|chart-spark|chart-bar|chart-line|chart-donut|table|kpi|code|diff|flowchart|orgchart|steps|checklist|welcome|hint)\s*\n([\s\S]*?)```/g export type MessageBodyProps = { content: string isToolResult?: boolean toolCalls?: ToolCall[] } type Segment = | { type: "prose"; text: string } | { type: "block"; kind: string; spec: unknown; raw: string } function parseSegments(content: string): Segment[] { const segments: Segment[] = [] TYPED_BLOCK_RE.lastIndex = 0 let lastIndex = 0 let match: RegExpExecArray | null while ((match = TYPED_BLOCK_RE.exec(content)) !== null) { const [raw, kind, body] = match if (match.index > lastIndex) { segments.push({ type: "prose", text: content.slice(lastIndex, match.index) }) } let spec: unknown = null try { spec = JSON.parse(body.trim()) } catch { // malformed → emit the raw fence as prose so the user sees the model output segments.push({ type: "prose", text: raw }) lastIndex = match.index + raw.length continue } segments.push({ type: "block", kind, spec, raw }) lastIndex = match.index + raw.length } if (lastIndex < content.length) { segments.push({ type: "prose", text: content.slice(lastIndex) }) } return segments } function renderBlock(kind: string, spec: any, key: number): ReactNode { switch (kind) { case "card": return case "chart-spark": return (
) case "chart-bar": return (
) case "chart-line": return (
) case "chart-donut": return (
) case "table": return case "kpi": return (
) case "code": return (
) case "diff": return (
) case "flowchart": return (
) case "orgchart": return (
) case "steps": return (
) case "checklist": return (
) case "welcome": return (
) case "hint": return (
{spec.body ?? ""}
) default: return (
          {JSON.stringify(spec, null, 2)}
        
) } } function ChartFrame({ title, children }: { title?: string; children: ReactNode }) { return (
{title &&
{title}
} {children}
) } function ChartLegend({ data }: { data?: ChartDatum[] }) { if (!data || data.length === 0) return null return (
    {data.map((d) => (
  • {d.label} {d.value}
  • ))}
) } function TableBlock({ spec }: { spec: any }) { const rows: Record[] = Array.isArray(spec.rows) ? spec.rows : [] const columns: Column>[] = (spec.columns ?? []).map((c: any) => ({ id: c.id, header: c.header ?? c.id, accessor: c.accessor ?? c.id, sortable: c.sortable ?? true, align: c.align, })) const idKey = spec.idKey ?? columns[0]?.id ?? "id" return (
String(r[idKey] ?? Math.random())} density="compact" />
) } type CardSpec = | { kind: "pill"; status: string; label?: string } | { kind: "stat"; label: string; value: string | number; tone?: string } | { kind: "callout"; title?: string; tone?: "info" | "warning" | "danger" | "success"; body?: string } | { kind: string; [k: string]: unknown } function CardBlock({ spec }: { spec: CardSpec }) { switch (spec.kind) { case "pill": { const s = spec as { kind: "pill"; status: string; label?: string } const tone = s.status === "active" ? "border-emerald-500/40 bg-emerald-500/15 text-emerald-700 dark:text-emerald-300" : s.status === "suspended" ? "border-amber-500/40 bg-amber-500/15 text-amber-700 dark:text-amber-300" : s.status === "deactivated" ? "border-rose-500/40 bg-rose-500/15 text-rose-700 dark:text-rose-300" : "border-border bg-muted text-muted-foreground" return ( {s.label ?? s.status} ) } case "stat": { const s = spec as { kind: "stat"; label: string; value: string | number } return ( {s.label} {s.value} ) } case "callout": { const s = spec as { kind: "callout"; title?: string; tone?: "info" | "warning" | "danger" | "success"; body?: string } const tone = s.tone ?? "info" const palette: Record = { info: "border-sky-500/40 bg-sky-500/10", warning: "border-amber-500/40 bg-amber-500/10", danger: "border-rose-500/40 bg-rose-500/10", success: "border-emerald-500/40 bg-emerald-500/10", } return (
{s.title &&
{s.title}
} {s.body &&
{s.body}
}
) } default: return (
          {JSON.stringify(spec, null, 2)}
        
) } } const PROSE_COMPONENTS = { p: ({ children }: any) =>

{children}

, code: ({ children, className }: any) => { const isBlock = className?.startsWith("language-") if (isBlock) { return (
          {children}
        
) } return {children} }, ul: ({ children }: any) =>
    {children}
, ol: ({ children }: any) =>
    {children}
, li: ({ children }: any) =>
  • {children}
  • , a: ({ children, href }: any) => ( {children} ), table: ({ children }: any) => (
    {children}
    ), thead: ({ children }: any) => {children}, th: ({ children }: any) => {children}, td: ({ children }: any) => {children}, input: ({ checked, type, ...rest }: any) => type === "checkbox" ? ( ) : ( ), } function ProseChunk({ text }: { text: string }) { const trimmed = text.trim() if (!trimmed) return null return ( {trimmed} ) } export function MessageBody({ content, isToolResult, toolCalls }: MessageBodyProps) { const { segments, actionCount } = useMemo(() => { const blocks = extractActionBlocks(content) const cleaned = stripToolCallTags(content).replace(ACTION_BLOCK_RE, "") return { segments: parseSegments(cleaned), actionCount: blocks.length, } }, [content]) if (isToolResult) { return (
    Tool result
              {content}
            
    ) } return (
    {segments.map((seg, i) => seg.type === "prose" ? : renderBlock(seg.kind, seg.spec, i), )} {actionCount > 0 && ( Ran {actionCount} action{actionCount > 1 ? "s" : ""} )} {toolCalls && toolCalls.length > 0 && ( Called {toolCalls.map((c) => c.name).join(", ")} )}
    ) }