|
|
@@ -0,0 +1,392 @@
|
|
|
+/**
|
|
|
+ * json-render 注册表
|
|
|
+ *
|
|
|
+ * 创建组件注册表,将 Schema 定义与 React 组件绑定
|
|
|
+ * 用于 Renderer 的自动解析和渲染
|
|
|
+ */
|
|
|
+
|
|
|
+'use client';
|
|
|
+
|
|
|
+import { useState } from 'react';
|
|
|
+import type { ComponentSpec } from './json-render-catalog';
|
|
|
+
|
|
|
+// ============ React 组件实现 ============
|
|
|
+
|
|
|
+// Card 组件
|
|
|
+const Card = ({ title, children, className }: 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>}
|
|
|
+ {children}
|
|
|
+ </div>
|
|
|
+);
|
|
|
+
|
|
|
+// Stack 组件
|
|
|
+const Stack = ({ direction = 'column', spacing = 2, align = 'start', children, className }: 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 || ''}`}>
|
|
|
+ {children}
|
|
|
+ </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, disabled = false, className }: 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 (onClick && !disabled) {
|
|
|
+ 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 }: 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="text"
|
|
|
+ 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 组件 - 翻译结果展示
|
|
|
+const TranslationResult = ({ translated, termsUsed, className }: any) => {
|
|
|
+ const [copied, setCopied] = useState(false);
|
|
|
+
|
|
|
+ const handleCopy = () => {
|
|
|
+ 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 }: any) => (
|
|
|
+ <div className={`novel-list space-y-3 ${className || ''}`}>
|
|
|
+ {novels.map((novel: any) => (
|
|
|
+ <div key={novel.id} className="bg-white dark:bg-gray-800 rounded-lg border dark:border-gray-700 p-4 hover:shadow-md transition-shadow 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>
|
|
|
+);
|
|
|
+
|
|
|
+// ============ 创建注册表 ============
|
|
|
+
|
|
|
+// 简化的组件注册表 - 直接映射组件名称到 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,
|
|
|
+ 'chapter-reader': ChapterReader,
|
|
|
+ 'mcp-tool-call': McpToolCall,
|
|
|
+ 'login-panel': LoginPanel,
|
|
|
+ 'code-block': CodeBlock,
|
|
|
+ 'data-table': DataTable,
|
|
|
+};
|
|
|
+
|
|
|
+// 导出类型
|
|
|
+export type { ComponentSpec };
|