| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540 |
- /**
- * json-render 注册表
- *
- * 创建组件注册表,将 Schema 定义与 React 组件绑定
- * 用于 Renderer 的自动解析和渲染
- */
- 'use client';
- import { useState } from 'react';
- // ============ React 组件实现 ============
- // Card 组件
- const Card = ({ title, children, className, renderChildren, emit }: any) => (
- <div className={`bg-white dark:bg-gray-800 rounded-lg border dark:border-gray-700 shadow-sm p-4 ${className || ''}`}>
- {title && <h3 className="text-lg font-semibold mb-3 text-gray-900 dark:text-white">{title}</h3>}
- {renderChildren ? renderChildren(children, emit) : null}
- </div>
- );
- // 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 (
- <div className={`flex ${directionClass} ${spacingClass} ${alignClass} ${className || ''}`}>
- {renderChildren ? renderChildren(children, emit) : null}
- </div>
- );
- };
- // Heading 组件
- const Heading = ({ level = 'h2', text, className }: any) => {
- const Tag = level;
- const sizeClasses: Record<string, string> = {
- 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 <Tag className={`${sizeClass} text-gray-900 dark:text-white ${className || ''}`}>{text}</Tag>;
- };
- // Text 组件
- const Text = ({ content, variant = 'body', className }: any) => {
- const variantClasses: Record<string, string> = {
- 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 <p className={`${variantClass} ${className || ''}`}>{content}</p>;
- };
- // Button 组件 - 支持交互事件
- const Button = ({ label, variant = 'default', onClick, action, actionPayload, disabled = false, className, emit }: any) => {
- const variantClasses: Record<string, string> = {
- 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 (
- <button
- onClick={handleClick}
- disabled={disabled}
- className={`${variantClass} px-4 py-2 rounded-md font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${className || ''}`}
- >
- {label}
- </button>
- );
- };
- // Input 组件
- const Input = ({ placeholder, value, onChange, disabled = false, className, type = 'text' }: any) => {
- const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- if (onChange) {
- try {
- const fn = new Function('event', onChange);
- fn(e);
- } catch (err) {
- console.error('Error executing onChange:', err);
- }
- }
- };
- return (
- <input
- type={type}
- placeholder={placeholder}
- defaultValue={value}
- onChange={handleChange}
- disabled={disabled}
- className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 ${className || ''}`}
- />
- );
- };
- // Badge 组件
- const Badge = ({ text, variant = 'default', className }: any) => {
- const variantClasses: Record<string, string> = {
- 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 (
- <span className={`inline-flex items-center px-2 py-1 rounded-md text-xs font-medium ${variantClass} ${className || ''}`}>
- {text}
- </span>
- );
- };
- // Separator 组件
- const Separator = ({ orientation = 'horizontal', className }: any) => {
- const orientationClass = orientation === 'horizontal' ? 'h-px w-full' : 'w-px h-full';
- return <div className={`bg-gray-200 dark:bg-gray-700 ${orientationClass} ${className || ''}`} />;
- };
- // ============ 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 (
- <div className={`translation-result bg-white dark:bg-gray-800 rounded-xl shadow-md border border-gray-200 dark:border-gray-700 overflow-hidden ${className || ''}`}>
- {/* 头部 */}
- <div className="bg-gradient-to-r from-blue-500 to-purple-500 px-4 py-3 flex items-center gap-2">
- <span className="text-xl">🌐</span>
- <h4 className="font-semibold text-white">翻译结果</h4>
- </div>
- {/* 内容区域 */}
- <div className="p-4">
- {/* 译文 */}
- <div className="bg-gradient-to-br from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 rounded-lg p-3 border border-blue-100 dark:border-blue-800/50 mb-3">
- <div className="flex items-center gap-2 mb-2">
- <span className="text-xs font-medium text-blue-600 dark:text-blue-400 uppercase tracking-wide">译文</span>
- </div>
- <p className="text-gray-900 dark:text-gray-100 font-medium leading-relaxed">{translated}</p>
- </div>
- {/* 使用的术语 */}
- {termsUsed && termsUsed.length > 0 && (
- <div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-3">
- <div className="flex items-center gap-2 mb-2">
- <span className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">使用术语</span>
- </div>
- <div className="flex flex-wrap gap-2">
- {termsUsed.map((term: string, idx: number) => (
- <span key={idx} className="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-200">
- {term}
- </span>
- ))}
- </div>
- </div>
- )}
- </div>
- {/* 底部操作栏 */}
- <div className="px-4 py-3 bg-gray-50 dark:bg-gray-900/30 border-t dark:border-gray-700 flex gap-2">
- <button
- onClick={handleCopy}
- className="flex-1 flex items-center justify-center gap-1 px-3 py-2 text-sm bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
- >
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
- </svg>
- <span>{copied ? '已复制!' : '复制译文'}</span>
- </button>
- </div>
- </div>
- );
- };
- // NovelList 组件 - 支持点击交互
- const NovelList = ({ novels, className, emit }: any) => (
- <div className={`novel-list space-y-3 ${className || ''}`}>
- {novels.map((novel: any) => (
- <div
- key={novel.id}
- onClick={() => 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"
- >
- <h4 className="font-semibold text-lg text-gray-900 dark:text-white mb-1">{novel.title}</h4>
- {novel.author && <p className="text-sm text-gray-500 dark:text-gray-400 mb-2">By {novel.author}</p>}
- {novel.description && <p className="text-sm text-gray-600 dark:text-gray-300 mb-2 line-clamp-2">{novel.description}</p>}
- <div className="flex items-center gap-2">
- {novel.chapterCount && <Badge text={`${novel.chapterCount} chapters`} variant="info" />}
- {novel.tags?.map((tag: string) => (
- <Badge key={tag} text={tag} variant="default" />
- ))}
- </div>
- </div>
- ))}
- </div>
- );
- // ChapterReader 组件
- const ChapterReader = ({ novelTitle, chapterTitle, content, chapterNumber, totalChapters, className }: any) => (
- <div className={`chapter-reader bg-white dark:bg-gray-800 rounded-lg border dark:border-gray-700 p-6 ${className || ''}`}>
- <div className="border-b dark:border-gray-700 pb-4 mb-4">
- <p className="text-sm text-gray-500 dark:text-gray-400 mb-1">{novelTitle}</p>
- <h2 className="text-2xl font-bold text-gray-900 dark:text-white">{chapterTitle}</h2>
- {chapterNumber && totalChapters && (
- <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
- Chapter {chapterNumber} of {totalChapters}
- </p>
- )}
- </div>
- <div className="prose dark:prose-invert max-w-none">
- <p className="text-gray-700 dark:text-gray-300 whitespace-pre-wrap leading-relaxed">{content}</p>
- </div>
- </div>
- );
- // McpToolCall 组件
- const McpToolCall = ({ tool, status, args, result, error, className }: any) => {
- const statusConfig: Record<string, { icon: string; color: string; bg: string }> = {
- 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 (
- <div className={`mcp-tool-call border-l-4 p-3 rounded-r ${config.bg} ${className || ''}`}>
- <div className="flex items-center gap-2 mb-2">
- <span className="text-lg">{config.icon}</span>
- <span className={`font-mono text-sm font-medium ${config.color}`}>{tool}</span>
- </div>
- {args && (
- <div className="mt-2">
- <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Arguments:</p>
- <pre className="text-xs bg-gray-100 dark:bg-gray-900 p-2 rounded overflow-x-auto">
- {JSON.stringify(args, null, 2)}
- </pre>
- </div>
- )}
- {result && status === 'success' && (
- <div className="mt-2">
- <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Result:</p>
- <pre className="text-xs bg-gray-100 dark:bg-gray-900 p-2 rounded overflow-x-auto">
- {JSON.stringify(result, null, 2)}
- </pre>
- </div>
- )}
- {error && status === 'error' && (
- <div className="mt-2">
- <p className="text-xs text-red-600 dark:text-red-400 mb-1">Error:</p>
- <p className="text-sm text-red-700 dark:text-red-300">{error}</p>
- </div>
- )}
- </div>
- );
- };
- // LoginPanel 组件
- const LoginPanel = ({ server, email, className }: any) => (
- <div className={`login-panel bg-white dark:bg-gray-800 rounded-lg border dark:border-gray-700 p-6 ${className || ''}`}>
- <div className="text-center mb-6">
- <div className="text-4xl mb-2">🔐</div>
- <h3 className="text-xl font-semibold text-gray-900 dark:text-white">{server}</h3>
- <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Sign in to continue</p>
- </div>
- <form className="space-y-4">
- <div>
- <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email</label>
- <Input type="email" placeholder="your@email.com" defaultValue={email} />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Password</label>
- <Input type="password" placeholder="••••••••" />
- </div>
- <Button label="Sign In" variant="primary" className="w-full" />
- </form>
- </div>
- );
- // CodeBlock 组件
- const CodeBlock = ({ code, language = 'text', inline = false, className }: any) => {
- if (inline) {
- return (
- <code className={`bg-gray-100 dark:bg-gray-900 px-1.5 py-0.5 rounded text-sm font-mono text-gray-800 dark:text-gray-200 ${className || ''}`}>
- {code}
- </code>
- );
- }
- return (
- <div className={`code-block bg-gray-900 dark:bg-gray-950 rounded-lg p-4 overflow-x-auto ${className || ''}`}>
- <div className="flex items-center justify-between mb-2">
- <span className="text-xs text-gray-400 uppercase">{language}</span>
- <button
- onClick={() => navigator.clipboard.writeText(code)}
- className="text-xs text-gray-400 hover:text-white transition-colors"
- >
- Copy
- </button>
- </div>
- <pre className="text-sm text-gray-100 font-mono">
- <code>{code}</code>
- </pre>
- </div>
- );
- };
- // DataTable 组件
- const DataTable = ({ columns, rows, className }: any) => (
- <div className={`data-table overflow-x-auto ${className || ''}`}>
- <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
- <thead className="bg-gray-50 dark:bg-gray-800">
- <tr>
- {columns.map((col: any) => (
- <th key={col.key} className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
- {col.label}
- </th>
- ))}
- </tr>
- </thead>
- <tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
- {rows.map((row: any, idx: number) => (
- <tr key={idx} className="hover:bg-gray-50 dark:hover:bg-gray-800">
- {columns.map((col: any) => (
- <td key={col.key} className="px-4 py-3 text-sm text-gray-700 dark:text-gray-300">
- {String(row[col.key] ?? '')}
- </td>
- ))}
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- );
- // NovelDetail 组件 - 小说详情卡片
- const NovelDetail = ({ novel, className, emit }: any) => {
- if (!novel) return null;
- return (
- <div className={`novel-detail bg-white dark:bg-gray-800 rounded-xl shadow-md border border-gray-200 dark:border-gray-700 overflow-hidden ${className || ''}`}>
- {/* 头部 - 渐变背景 */}
- <div className="bg-gradient-to-r from-purple-500 to-pink-500 px-4 py-4">
- <h3 className="font-bold text-white text-xl">{novel.title || '未知标题'}</h3>
- {novel.isVip && (
- <span className="inline-block mt-1 px-2 py-0.5 bg-yellow-400 text-yellow-900 text-xs rounded-full font-medium">
- 👑 VIP
- </span>
- )}
- </div>
- {/* 内容区域 */}
- <div className="p-4 space-y-4">
- {/* 基本信息 */}
- <div>
- <h4 className="font-semibold text-gray-900 dark:text-gray-100 mb-2 flex items-center gap-1">
- <span>📖</span> 基本信息
- </h4>
- <div className="space-y-1 text-sm text-gray-700 dark:text-gray-300">
- <p><span className="text-gray-500">作者:</span>{novel.author || '-'}</p>
- <p><span className="text-gray-500">类型:</span>{novel.category || '-'}</p>
- <p><span className="text-gray-500">简介:</span>{novel.description || '暂无简介'}</p>
- </div>
- </div>
- {/* 统计数据 */}
- <div>
- <h4 className="font-semibold text-gray-900 dark:text-gray-100 mb-2 flex items-center gap-1">
- <span>📊</span> 统计数据
- </h4>
- <div className="grid grid-cols-2 gap-2">
- <div className="bg-gray-50 dark:bg-gray-900/50 p-2 rounded-lg">
- <span className="text-xs text-gray-500">状态</span>
- <p className="font-medium text-sm">{novel.status || '-'}</p>
- </div>
- <div className="bg-gray-50 dark:bg-gray-900/50 p-2 rounded-lg">
- <span className="text-xs text-gray-500">章节数</span>
- <p className="font-medium text-sm">{novel.chapterCount ?? '-'}章</p>
- </div>
- <div className="bg-gray-50 dark:bg-gray-900/50 p-2 rounded-lg">
- <span className="text-xs text-gray-500">总字数</span>
- <p className="font-medium text-sm">{novel.wordCount ?? '-'}字</p>
- </div>
- <div className="bg-gray-50 dark:bg-gray-900/50 p-2 rounded-lg">
- <span className="text-xs text-gray-500">阅读量</span>
- <p className="font-medium text-sm">{novel.viewCount ?? '-'}次</p>
- </div>
- </div>
- </div>
- </div>
- </div>
- );
- };
- // SuggestionButtons 组件 - 建议操作按钮组
- const SuggestionButtons = ({ suggestions, className, emit }: any) => {
- if (!suggestions || !Array.isArray(suggestions) || suggestions.length === 0) return null;
- return (
- <div className={`suggestion-buttons flex flex-wrap gap-2 ${className || ''}`}>
- {suggestions.map((suggestion: any, index: number) => {
- const handleClick = () => {
- if (emit && suggestion.message) {
- emit('sendMessage', { message: suggestion.message });
- }
- };
- return (
- <button
- key={index}
- onClick={handleClick}
- className="px-4 py-2 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded-lg hover:bg-blue-200 dark:hover:bg-blue-900/50 transition-colors text-sm font-medium cursor-pointer"
- >
- {suggestion.icon && <span className="mr-1">{suggestion.icon}</span>}
- {suggestion.label || suggestion.message}
- </button>
- );
- })}
- </div>
- );
- };
- // ============ 创建注册表 ============
- // 简化的组件注册表 - 直接映射组件名称到 React 组件
- export const jsonRenderRegistry: Record<string, React.ComponentType<any>> = {
- // 基础组件
- 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 <span key={idx}>{String(child)}</span>;
- 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 <Component key={idx} {...props} />;
- }
- return (
- <div key={idx} className="p-2 bg-yellow-50 dark:bg-yellow-900/20 rounded border border-yellow-200 dark:border-yellow-800">
- <p className="text-sm text-yellow-700 dark:text-yellow-300">Unknown component: {componentType}</p>
- </div>
- );
- });
- }
- // 导出类型
- export type { ComponentSpec } from './json-render-catalog';
|