/** * JsonRenderer - 使用官方 @json-render/react API 的渲染器 * * 功能: * - 使用官方 Renderer 组件渲染 json-render spec * - 支持单个 spec 和 specs 数组 * - 支持流式渲染(SSE) * - 兼容旧格式的 ComponentSpec * * 使用方式: * ```tsx * // 渲染单个 spec(旧格式) * * * // 渲染多个 specs(旧格式) * * * // 渲染官方格式 spec * * ``` */ 'use client'; import { useMemo } from 'react'; import { Renderer, JSONUIProvider, type Spec, } from '@json-render/react'; import type { UIElement } from '@json-render/core'; import { registry } from '@/lib/registry'; // ============ Types ============ /** * 旧格式组件 spec 类型 * 旧格式: { type: 'card', title: '...', children: [...] } */ export interface LegacyComponentSpec { type: string; children?: LegacyComponentSpec[]; [key: string]: unknown; } /** * JsonRenderer Props */ export interface JsonRendererProps { /** 单个组件 spec(支持旧格式和官方格式) */ spec?: LegacyComponentSpec | Spec | null; /** 多个组件 spec 数组 */ specs?: LegacyComponentSpec[] | Spec[]; /** 可选样式类 */ className?: string; /** 事件处理函数 */ onAction?: (actionName: string, params?: Record) => void; /** 是否正在加载 */ loading?: boolean; } /** * StreamingJsonRenderer Props - 用于 SSE 流式渲染 */ export interface StreamingJsonRendererProps extends JsonRendererProps { /** 流式数据 */ specs?: LegacyComponentSpec[]; } // ============ Helper Functions ============ /** * 生成唯一 key */ let keyCounter = 0; function generateKey(): string { return `el_${++keyCounter}`; } /** * 将旧的嵌套格式 ComponentSpec 转换为扁平化的官方 Spec 格式 * * 旧格式: { type: 'card', title: '...', children: [...] } * 新格式: { root: 'card1', elements: { 'card1': { type: 'card', props: { title: '...' }, children: ['text1'] } } } */ function convertLegacyToSpec( legacySpec: LegacyComponentSpec, elements: Record = {}, parentKey?: string ): string { const { type, children, ...restProps } = legacySpec; const key = generateKey(); // 转换 children(递归) let childKeys: string[] | undefined; if (children && Array.isArray(children) && children.length > 0) { childKeys = children.map((child) => convertLegacyToSpec(child, elements, key) ); } // 创建 UIElement const element: UIElement = { type, props: restProps, }; if (childKeys && childKeys.length > 0) { element.children = childKeys; } elements[key] = element; return key; } /** * 检查是否是官方 Spec 格式 */ function isOfficialSpec(spec: unknown): spec is Spec { if (!spec || typeof spec !== 'object') return false; const s = spec as Record; return typeof s.root === 'string' && typeof s.elements === 'object'; } /** * 检查是否是旧格式 ComponentSpec */ function isLegacySpec(spec: unknown): spec is LegacyComponentSpec { if (!spec || typeof spec !== 'object') return false; const s = spec as Record; return typeof s.type === 'string' && !('root' in s) && !('elements' in s); } /** * 将任何格式的 spec 转换为官方 Spec 格式 */ function normalizeSpec(spec: LegacyComponentSpec | Spec | null | undefined): Spec | null { if (!spec) return null; // 已经是官方格式 if (isOfficialSpec(spec)) { return spec; } // 旧格式,需要转换 if (isLegacySpec(spec)) { const elements: Record = {}; const root = convertLegacyToSpec(spec, elements); if (Object.keys(elements).length === 0) { return null; } return { root, elements }; } return null; } /** * 将多个 spec 转换为容器 spec */ function wrapSpecsInContainer(specs: (LegacyComponentSpec | Spec)[]): Spec | null { if (!specs || specs.length === 0) return null; // 过滤有效的 specs const validSpecs = specs.filter(Boolean); if (validSpecs.length === 0) return null; // 如果只有一个 spec,直接转换 if (validSpecs.length === 1) { return normalizeSpec(validSpecs[0]); } // 多个 specs,包装在 stack 容器中 const elements: Record = {}; keyCounter = 0; // 重置计数器 const childKeys = validSpecs.map((spec) => { if (isOfficialSpec(spec)) { // 官方格式,合并 elements Object.assign(elements, spec.elements); return spec.root; } // 旧格式,转换 return convertLegacyToSpec(spec as LegacyComponentSpec, elements); }); // 创建 stack 容器 const stackKey = generateKey(); elements[stackKey] = { type: 'stack', props: { direction: 'column', spacing: 2, }, children: childKeys, }; return { root: stackKey, elements }; } // ============ Components ============ /** * 基础 JsonRenderer 组件 * * 使用官方 @json-render/react Renderer 渲染组件 */ export function JsonRenderer({ spec, specs, className, onAction, loading, }: JsonRendererProps) { // 转换 spec 格式 const finalSpec = useMemo(() => { if (specs && specs.length > 0) { return wrapSpecsInContainer(specs); } if (spec) { return normalizeSpec(spec); } return null; }, [spec, specs]); // 创建 action handlers const handlers = useMemo(() => { const baseHandlers: Record) => Promise | void> = { sendMessage: async (params?: Record) => { onAction?.('sendMessage', params); }, selectNovel: async (params?: Record) => { onAction?.('selectNovel', params); }, copy: async (params?: Record) => { onAction?.('copy', params); // 如果有 text 参数,直接复制到剪贴板 if (params?.text && typeof params.text === 'string') { try { await navigator.clipboard.writeText(params.text); } catch (e) { console.error('Failed to copy to clipboard:', e); } } }, }; return baseHandlers; }, [onAction]); if (!finalSpec) { return null; } return (
); } /** * 流式 JsonRenderer 组件 * * 用于 SSE 流式渲染,支持动态更新 specs */ export function StreamingJsonRenderer({ specs, className, onAction, loading, }: StreamingJsonRendererProps) { // 转换 specs 格式 const finalSpec = useMemo(() => { if (!specs || specs.length === 0) return null; return wrapSpecsInContainer(specs); }, [specs]); // 创建 action handlers const handlers = useMemo(() => { const baseHandlers: Record) => Promise | void> = { sendMessage: async (params?: Record) => { onAction?.('sendMessage', params); }, selectNovel: async (params?: Record) => { onAction?.('selectNovel', params); }, copy: async (params?: Record) => { onAction?.('copy', params); if (params?.text && typeof params.text === 'string') { try { await navigator.clipboard.writeText(params.text); } catch (e) { console.error('Failed to copy to clipboard:', e); } } }, }; return baseHandlers; }, [onAction]); if (!finalSpec) { return null; } return (
); } // ============ Default Export ============ /** * 默认导出 - 基础 JsonRenderer * * 支持单个 spec 或 specs 数组 */ export default JsonRenderer; // ============ Additional Exports ============ // 导出类型 export type { Spec, StateModel } from '@json-render/react'; export type { UIElement } from '@json-render/core'; // 兼容旧类型 export type ComponentSpec = LegacyComponentSpec;