| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347 |
- /**
- * JsonRenderer - 使用官方 @json-render/react API 的渲染器
- *
- * 功能:
- * - 使用官方 Renderer 组件渲染 json-render spec
- * - 支持单个 spec 和 specs 数组
- * - 支持流式渲染(SSE)
- * - 兼容旧格式的 ComponentSpec
- *
- * 使用方式:
- * ```tsx
- * // 渲染单个 spec(旧格式)
- * <JsonRenderer spec={{ type: 'card', title: 'Hello' }} />
- *
- * // 渲染多个 specs(旧格式)
- * <JsonRenderer specs={[spec1, spec2]} />
- *
- * // 渲染官方格式 spec
- * <JsonRenderer spec={officialSpec} />
- * ```
- */
- '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<string, unknown>) => 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<string, UIElement> = {},
- 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<string, unknown>;
- 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<string, unknown>;
- 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<string, UIElement> = {};
- 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<string, UIElement> = {};
- 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<string, (params?: Record<string, unknown>) => Promise<void> | void> = {
- sendMessage: async (params?: Record<string, unknown>) => {
- onAction?.('sendMessage', params);
- },
- selectNovel: async (params?: Record<string, unknown>) => {
- onAction?.('selectNovel', params);
- },
- copy: async (params?: Record<string, unknown>) => {
- 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 (
- <div className={className}>
- <JSONUIProvider
- registry={registry}
- handlers={handlers}
- >
- <Renderer
- spec={finalSpec}
- registry={registry}
- loading={loading}
- />
- </JSONUIProvider>
- </div>
- );
- }
- /**
- * 流式 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<string, (params?: Record<string, unknown>) => Promise<void> | void> = {
- sendMessage: async (params?: Record<string, unknown>) => {
- onAction?.('sendMessage', params);
- },
- selectNovel: async (params?: Record<string, unknown>) => {
- onAction?.('selectNovel', params);
- },
- copy: async (params?: Record<string, unknown>) => {
- 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 (
- <div className={className}>
- <JSONUIProvider
- registry={registry}
- handlers={handlers}
- >
- <Renderer
- spec={finalSpec}
- registry={registry}
- loading={loading}
- />
- </JSONUIProvider>
- </div>
- );
- }
- // ============ 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;
|