// Renders an assistant message: GFM markdown for prose, custom ```card``` // blocks rendered as rich UI (status pills, tenant cards, KPIs), pills for // command-bus action blocks, and tool-result cards (role: "tool"). 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" const ACTION_BLOCK_RE = /```action\s*\n[\s\S]*?```/g const CARD_BLOCK_RE = /```card\s*\n([\s\S]*?)```/g export type MessageBodyProps = { content: string isToolResult?: boolean toolCalls?: ToolCall[] } 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 parseCardBlocks(content: string): { blocks: CardSpec[]; stripped: string } { const blocks: CardSpec[] = [] CARD_BLOCK_RE.lastIndex = 0 const stripped = content.replace(CARD_BLOCK_RE, (_, body: string) => { try { const parsed = JSON.parse(body.trim()) as CardSpec if (parsed && typeof parsed === "object" && typeof parsed.kind === "string") { blocks.push(parsed) return "" // strip from prose } } catch { // malformed — leave the original block in the prose so the user can see // what the model tried to emit. return _ } return _ }) return { blocks, stripped } } function renderCard(spec: CardSpec): ReactNode { 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)}
        
) } } export function MessageBody({ content, isToolResult, toolCalls }: MessageBodyProps) { const { prose, actionCount, cardBlocks } = useMemo(() => { const blocks = extractActionBlocks(content) const cleaned = stripToolCallTags(content) .replace(ACTION_BLOCK_RE, "") .trim() const { blocks: cardBlocks, stripped } = parseCardBlocks(cleaned) return { prose: stripped.trim(), actionCount: blocks.length, cardBlocks, } }, [content]) if (isToolResult) { return (
Tool result
          {content}
        
) } return (
{prose && (

{children}

, code: ({ children, className }) => { const isBlock = className?.startsWith("language-") if (isBlock) { return (
                    {children}
                  
) } return ( {children} ) }, ul: ({ children }) =>
    {children}
, ol: ({ children }) =>
    {children}
, li: ({ children }) =>
  • {children}
  • , a: ({ children, href }) => ( {children} ), table: ({ children }) => (
    {children}
    ), thead: ({ children }) => ( {children} ), th: ({ children }) => ( {children} ), td: ({ children }) => {children}, input: ({ checked, type, ...rest }) => type === "checkbox" ? ( ) : ( ), }} > {prose}
    )} {cardBlocks.length > 0 && (
    {cardBlocks.map((spec, i) => ( {renderCard(spec)} ))}
    )} {actionCount > 0 && ( Ran {actionCount} action{actionCount > 1 ? "s" : ""} )} {toolCalls && toolCalls.length > 0 && ( Called {toolCalls.map((c) => c.name).join(", ")} )}
    ) }