/**
* 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;