/** * 聊天消息组件 - 支持 Markdown 渲染和 JSON 组件混排 * * 更新:使用官方 @json-render/core API * - 支持从 props 直接接收编译后的 specs * - 使用官方 Renderer 渲染组件 */ 'use client'; import { useEffect, useMemo } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { Renderer, JSONUIProvider } from '@json-render/react'; import { compileSpecStream, parseSpecStreamLine } from '@json-render/core'; import { registry } from '@/lib/registry'; import { useActionEmit } from '@/lib/action-context'; import type { Spec } from '@json-render/core'; interface ChatMessageProps { role: 'user' | 'assistant'; content: string; isLoading?: boolean; specs?: Spec[]; // 新增:接收编译后的 specs } /** * 检查文本是否为 JSON Patch 格式 (SpecStream) */ function isSpecStreamFormat(text: string): boolean { const lines = text.split('\n').filter(l => l.trim()); if (lines.length === 0) return false; let patchCount = 0; for (const line of lines.slice(0, 5)) { const patch = parseSpecStreamLine(line); if (patch) patchCount++; } return patchCount >= 2; } /** * 从 SpecStream 格式中编译出完整的 spec */ function compileSpecFromStream(text: string): Spec | null { try { const spec = compileSpecStream(text as any); console.log('[compileSpecFromStream] Compiled spec:', spec); return spec; } catch (e) { console.error('[compileSpecFromStream] Failed to compile:', e); return null; } } /** * 从编译后的 spec 中提取可渲染的元素 * 对于 flat 格式 {root, elements},返回 root 指向的元素 */ function extractRenderableElements(spec: Spec): Spec[] { const elements: Spec[] = []; if (!spec || typeof spec !== 'object') return elements; // 如果 spec 有 root 和 elements 字段(flat 格式) // 需要渲染整个 spec 对象,让 Renderer 根据 root 找到对应元素 if (spec.root && spec.elements && typeof spec.elements === 'object') { // 返回完整的 flat spec,Renderer 会处理 elements.push(spec); return elements; } // 如果 spec 有 elements 字段但没有 root if (spec.elements && typeof spec.elements === 'object') { for (const [key, element] of Object.entries(spec.elements)) { if (element && typeof element === 'object' && 'type' in element) { elements.push(element as Spec); } } } // 如果 spec 本身就是一个组件 if ('type' in spec && spec.type) { elements.push(spec); } return elements; } function CodeBlock({ inline, className, children, ...props }: any) { const code = String(children).replace(/\n$/, ''); if (inline) { return {children}; } const match = /language-(\w+)/.exec(className || ''); const language = match ? match[1] : 'text'; return (
{language}
{children}
); } function Paragraph({ children }: any) { return

{children}

; } export default function ChatMessage({ role, content, isLoading, specs: propSpecs }: ChatMessageProps) { const isUser = role === 'user'; const { emit } = useActionEmit(); // 优先使用传入的 specs,否则从 content 编译 const compiledSpecs = useMemo(() => { // 如果 props 传入了 specs,直接使用 if (propSpecs && propSpecs.length > 0) { console.log('[ChatMessage] Using propSpecs:', propSpecs); return propSpecs; } if (isUser || !content) return []; // 回退:从 content 编译 if (isSpecStreamFormat(content)) { console.log('[ChatMessage] Detected SpecStream format, compiling...'); const spec = compileSpecFromStream(content); if (spec) { const elements = extractRenderableElements(spec); console.log('[ChatMessage] Extracted', elements.length, 'elements from content'); return elements; } } return []; }, [content, isUser, propSpecs]); useEffect(() => { if (!isUser) { console.log('[ChatMessage] Rendering:', { hasPropSpecs: !!(propSpecs && propSpecs.length > 0), isSpecStream: isSpecStreamFormat(content || ''), compiledSpecsCount: compiledSpecs.length, contentPreview: content?.substring(0, 100), }); } }, [content, isUser, compiledSpecs, propSpecs]); // 如果有编译出的 specs,渲染组件 if (compiledSpecs.length > 0) { return (
AI 助手
{compiledSpecs.map((spec, i) => { try { console.log('[ChatMessage] Rendering spec:', spec); return ; } catch (e) { console.error('[ChatMessage] Render error:', e); return
渲染错误: {String(e)}
; } })}
{isLoading && }
); } // 普通 Markdown 渲染 return (
{!isUser &&
AI 助手
} {!isUser ? (
{content || ''} {isLoading && }
) : (
{content || 暂无内容}
)}
); }