/**
* 聊天消息组件 - 支持 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 (
{children}
{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 (