/**
* json-render 注册表
*
* 创建组件注册表,将 Schema 定义与 React 组件绑定
* 用于 Renderer 的自动解析和渲染
*/
'use client';
import { useState } from 'react';
// ============ React 组件实现 ============
// Card 组件
const Card = ({ title, children, className, renderChildren, emit }: any) => (
{title &&
{title}
}
{renderChildren ? renderChildren(children, emit) : null}
);
// Stack 组件
const Stack = ({ direction = 'column', spacing = 2, align = 'start', children, className, renderChildren, emit }: any) => {
const directionClass = direction === 'row' ? 'flex-row' : 'flex-col';
const spacingClass = spacing > 0 ? `gap-${spacing}` : '';
const alignClass = align === 'center' ? 'items-center' : align === 'end' ? 'items-end' : align === 'stretch' ? 'items-stretch' : 'items-start';
return (
{renderChildren ? renderChildren(children, emit) : null}
);
};
// Heading 组件
const Heading = ({ level = 'h2', text, className }: any) => {
const Tag = level;
const sizeClasses: Record = {
h1: 'text-3xl font-bold',
h2: 'text-2xl font-semibold',
h3: 'text-xl font-semibold',
h4: 'text-lg font-medium',
h5: 'text-base font-medium',
h6: 'text-sm font-medium',
};
const sizeClass = sizeClasses[level] || sizeClasses.h2;
return {text};
};
// Text 组件
const Text = ({ content, variant = 'body', className }: any) => {
const variantClasses: Record = {
body: 'text-gray-700 dark:text-gray-300',
muted: 'text-gray-500 dark:text-gray-400',
code: 'font-mono text-sm bg-gray-100 dark:bg-gray-900 px-1 py-0.5 rounded',
};
const variantClass = variantClasses[variant] || variantClasses.body;
return {content}
;
};
// Button 组件 - 支持交互事件
const Button = ({ label, variant = 'default', onClick, action, actionPayload, disabled = false, className, emit }: any) => {
const variantClasses: Record = {
default: 'bg-gray-200 hover:bg-gray-300 text-gray-800 dark:bg-gray-700 dark:hover:bg-gray-600 dark:text-gray-200',
primary: 'bg-blue-600 hover:bg-blue-700 text-white',
secondary: 'bg-purple-600 hover:bg-purple-700 text-white',
ghost: 'hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300',
danger: 'bg-red-600 hover:bg-red-700 text-white',
};
const variantClass = variantClasses[variant] || variantClasses.default;
const handleClick = () => {
if (disabled) return;
// 优先使用 action 事件(emit)
if (action && emit) {
emit(action, actionPayload);
return;
}
// 回退到 onClick 代码执行
if (onClick) {
try {
// 安全执行 onClick 代码
const fn = new Function('event', onClick);
fn(new Event('click'));
} catch (e) {
console.error('Error executing onClick:', e);
}
}
};
return (
);
};
// Input 组件
const Input = ({ placeholder, value, onChange, disabled = false, className, type = 'text' }: any) => {
const handleChange = (e: React.ChangeEvent) => {
if (onChange) {
try {
const fn = new Function('event', onChange);
fn(e);
} catch (err) {
console.error('Error executing onChange:', err);
}
}
};
return (
);
};
// Badge 组件
const Badge = ({ text, variant = 'default', className }: any) => {
const variantClasses: Record = {
default: 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200',
success: 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200',
warning: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200',
error: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-200',
info: 'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200',
};
const variantClass = variantClasses[variant] || variantClasses.default;
return (
{text}
);
};
// Separator 组件
const Separator = ({ orientation = 'horizontal', className }: any) => {
const orientationClass = orientation === 'horizontal' ? 'h-px w-full' : 'w-px h-full';
return ;
};
// ============ MCP 专用组件实现 ============
// TranslationResult 组件 - 翻译结果展示(支持 emit 事件)
const TranslationResult = ({ translated, termsUsed, className, emit }: any) => {
const [copied, setCopied] = useState(false);
const handleCopy = () => {
// 使用 emit 触发复制事件(如果可用),否则直接调用 clipboard API
if (emit) {
emit('copy', { text: translated });
} else {
navigator.clipboard.writeText(translated);
}
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
{/* 头部 */}
🌐
翻译结果
{/* 内容区域 */}
{/* 译文 */}
{/* 使用的术语 */}
{termsUsed && termsUsed.length > 0 && (
使用术语
{termsUsed.map((term: string, idx: number) => (
{term}
))}
)}
{/* 底部操作栏 */}
);
};
// NovelList 组件 - 支持点击交互
const NovelList = ({ novels, className, emit }: any) => (
{novels.map((novel: any) => (
emit && emit('selectNovel', { id: novel.id, title: novel.title, ...novel })}
className="bg-white dark:bg-gray-800 rounded-lg border dark:border-gray-700 p-4 hover:shadow-md hover:border-blue-300 dark:hover:border-blue-600 transition-all cursor-pointer"
>
{novel.title}
{novel.author &&
By {novel.author}
}
{novel.description &&
{novel.description}
}
{novel.chapterCount && }
{novel.tags?.map((tag: string) => (
))}
))}
);
// ChapterReader 组件
const ChapterReader = ({ novelTitle, chapterTitle, content, chapterNumber, totalChapters, className }: any) => (
{novelTitle}
{chapterTitle}
{chapterNumber && totalChapters && (
Chapter {chapterNumber} of {totalChapters}
)}
);
// McpToolCall 组件
const McpToolCall = ({ tool, status, args, result, error, className }: any) => {
const statusConfig: Record = {
pending: { icon: '⏳', color: 'text-yellow-600', bg: 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-300 dark:border-yellow-700' },
running: { icon: '🔄', color: 'text-blue-600', bg: 'bg-blue-50 dark:bg-blue-900/20 border-blue-300 dark:border-blue-700' },
success: { icon: '✅', color: 'text-green-600', bg: 'bg-green-50 dark:bg-green-900/20 border-green-300 dark:border-green-700' },
error: { icon: '❌', color: 'text-red-600', bg: 'bg-red-50 dark:bg-red-900/20 border-red-300 dark:border-red-700' },
};
const config = statusConfig[status] || statusConfig.success;
return (
{config.icon}
{tool}
{args && (
Arguments:
{JSON.stringify(args, null, 2)}
)}
{result && status === 'success' && (
Result:
{JSON.stringify(result, null, 2)}
)}
{error && status === 'error' && (
)}
);
};
// LoginPanel 组件
const LoginPanel = ({ server, email, className }: any) => (
🔐
{server}
Sign in to continue
);
// CodeBlock 组件
const CodeBlock = ({ code, language = 'text', inline = false, className }: any) => {
if (inline) {
return (
{code}
);
}
return (
{language}
{code}
);
};
// DataTable 组件
const DataTable = ({ columns, rows, className }: any) => (
{columns.map((col: any) => (
|
{col.label}
|
))}
{rows.map((row: any, idx: number) => (
{columns.map((col: any) => (
|
{String(row[col.key] ?? '')}
|
))}
))}
);
// NovelDetail 组件 - 小说详情卡片
const NovelDetail = ({ novel, className, emit }: any) => {
if (!novel) return null;
return (
{/* 头部 - 渐变背景 */}
{novel.title || '未知标题'}
{novel.isVip && (
👑 VIP
)}
{/* 内容区域 */}
{/* 基本信息 */}
📖 基本信息
作者:{novel.author || '-'}
类型:{novel.category || '-'}
简介:{novel.description || '暂无简介'}
{/* 统计数据 */}
📊 统计数据
章节数
{novel.chapterCount ?? '-'}章
总字数
{novel.wordCount ?? '-'}字
阅读量
{novel.viewCount ?? '-'}次
);
};
// SuggestionButtons 组件 - 建议操作按钮组
const SuggestionButtons = ({ suggestions, className, emit }: any) => {
if (!suggestions || !Array.isArray(suggestions) || suggestions.length === 0) return null;
return (
{suggestions.map((suggestion: any, index: number) => {
const handleClick = () => {
if (emit && suggestion.message) {
emit('sendMessage', { message: suggestion.message });
}
};
return (
);
})}
);
};
// ============ 创建注册表 ============
// 简化的组件注册表 - 直接映射组件名称到 React 组件
export const jsonRenderRegistry: Record> = {
// 基础组件
card: Card,
stack: Stack,
heading: Heading,
text: Text,
button: Button,
input: Input,
badge: Badge,
separator: Separator,
// MCP 专用组件
'translation-result': TranslationResult,
'novel-list': NovelList,
'novel-detail': NovelDetail,
'chapter-reader': ChapterReader,
'mcp-tool-call': McpToolCall,
'login-panel': LoginPanel,
'code-block': CodeBlock,
'data-table': DataTable,
'suggestion-buttons': SuggestionButtons,
};
// ============ 辅助函数 ============
/**
* 递归渲染子组件
* 用于 Card 和 Stack 组件中渲染 children 数组
* @param children - 子组件数组
* @param emit - ActionContext emit 函数(用于组件交互)
*/
export function renderChildren(children: any, emit?: (eventName: string, payload?: any) => void): React.ReactNode {
if (!children) return null;
if (!Array.isArray(children)) return null;
return children.map((child: any, idx: number) => {
if (!child || typeof child !== 'object') return null;
if (!child.type) return {String(child)};
const componentType = child.type;
const Component = (jsonRenderRegistry as any)[componentType];
if (Component) {
// 合并 props:原始 child props + emit 函数
const props: any = { ...child };
if (emit) {
props.emit = emit;
}
// 如果组件需要渲染子组件,传入 renderChildren 函数(并传递 emit)
if (componentType === 'card' || componentType === 'stack') {
props.renderChildren = (grandchildren: any) => renderChildren(grandchildren, emit);
}
return ;
}
return (
Unknown component: {componentType}
);
});
}
// 导出类型
export type { ComponentSpec } from './json-render-catalog';