| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474 |
- /**
- * json-render 注册表
- *
- * 使用官方 defineRegistry API 创建组件注册表
- */
- 'use client';
- import { useState } from 'react';
- import { defineRegistry, type BaseComponentProps } from '@json-render/react';
- import { useActionEmit } from '@/lib/action-context';
- import { z } from 'zod';
- import catalog, {
- CardProps,
- StackProps,
- HeadingProps,
- TextProps,
- ButtonProps,
- BadgeProps,
- SeparatorProps,
- InputProps,
- TextAreaProps,
- DataTableProps,
- TranslationResultProps,
- NovelListProps,
- ChapterReaderProps,
- CodeBlockProps,
- ToolCallProps,
- LoginPanelProps,
- McpStatusProps,
- SuggestionButtonsProps,
- } from './catalog/catalog';
- // ============ 基础组件 ============
- const Card = ({ props, children }: BaseComponentProps<z.infer<typeof CardProps>>) => (
- <div className={`bg-white dark:bg-gray-800 rounded-lg border dark:border-gray-700 shadow-sm p-4 ${props.className || ''}`}>
- {props.title && <h3 className="text-lg font-semibold mb-3 text-gray-900 dark:text-white">{props.title}</h3>}
- {children}
- </div>
- );
- const Stack = ({ props, children }: BaseComponentProps<z.infer<typeof StackProps>>) => {
- const directionClass = props.direction === 'row' ? 'flex-row' : 'flex-col';
- const spacingClass = (props.spacing ?? 2) > 0 ? `gap-${props.spacing ?? 2}` : '';
- const alignClass = props.align === 'center' ? 'items-center'
- : props.align === 'end' ? 'items-end'
- : props.align === 'stretch' ? 'items-stretch'
- : 'items-start';
- return (
- <div className={`flex ${directionClass} ${spacingClass} ${alignClass} ${props.className || ''}`}>
- {children}
- </div>
- );
- };
- const Heading = ({ props }: BaseComponentProps<z.infer<typeof HeadingProps>>) => {
- const level = props.level || 'h2';
- const Tag = level as 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
- 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 ${props.className || ''}`}>{props.text}</Tag>;
- };
- const Text = ({ props }: BaseComponentProps<z.infer<typeof TextProps>>) => {
- const variantClasses: Record<string, string> = {
- default: 'text-gray-700 dark:text-gray-300',
- muted: 'text-gray-500 dark:text-gray-400',
- primary: 'text-blue-600 dark:text-blue-400',
- success: 'text-green-600 dark:text-green-400',
- warning: 'text-yellow-600 dark:text-yellow-400',
- destructive: 'text-red-600 dark:text-red-400',
- };
- const sizeClasses: Record<string, string> = {
- xs: 'text-xs',
- sm: 'text-sm',
- md: 'text-base',
- lg: 'text-lg',
- xl: 'text-xl',
- };
- const color = props.color || 'default';
- const size = props.size || 'md';
- return <p className={`${variantClasses[color]} ${sizeClasses[size]} ${props.className || ''}`}>{props.text}</p>;
- };
- const Button = ({ props, emit }: BaseComponentProps<z.infer<typeof ButtonProps>>) => {
- 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',
- outline: 'border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300',
- ghost: 'hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300',
- destructive: 'bg-red-600 hover:bg-red-700 text-white',
- };
- const variant = props.variant || 'default';
- const handleClick = () => {
- if (props.disabled) return;
- if (props.onClick) {
- emit(props.onClick);
- }
- };
- return (
- <button
- onClick={handleClick}
- disabled={props.disabled}
- className={`${variantClasses[variant]} px-4 py-2 rounded-md font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${props.className || ''}`}
- >
- {props.label}
- </button>
- );
- };
- const Badge = ({ props }: BaseComponentProps<z.infer<typeof BadgeProps>>) => {
- const variantClasses: Record<string, string> = {
- default: 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200',
- primary: 'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200',
- secondary: 'bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-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',
- destructive: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-200',
- };
- const variant = props.variant || 'default';
- return (
- <span className={`inline-flex items-center px-2 py-1 rounded-md text-xs font-medium ${variantClasses[variant]} ${props.className || ''}`}>
- {props.text}
- </span>
- );
- };
- const Separator = ({ props }: BaseComponentProps<z.infer<typeof SeparatorProps>>) => {
- const orientation = props.orientation || 'horizontal';
- const orientationClass = orientation === 'vertical' ? 'w-px h-full' : 'h-px w-full';
- return <div className={`bg-gray-200 dark:bg-gray-700 ${orientationClass} ${props.className || ''}`} />;
- };
- const Input = ({ props }: BaseComponentProps<z.infer<typeof InputProps>>) => (
- <input
- type={props.type || 'text'}
- placeholder={props.placeholder}
- disabled={props.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 ${props.className || ''}`}
- />
- );
- const TextArea = ({ props }: BaseComponentProps<z.infer<typeof TextAreaProps>>) => (
- <textarea
- placeholder={props.placeholder}
- rows={props.rows || 3}
- disabled={props.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 resize-none ${props.className || ''}`}
- />
- );
- const DataTable = ({ props }: BaseComponentProps<z.infer<typeof DataTableProps>>) => (
- <div className={`overflow-x-auto ${props.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>
- {props.columns?.map((col) => (
- <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">
- {props.data?.map((row, idx) => (
- <tr key={idx} className="hover:bg-gray-50 dark:hover:bg-gray-800">
- {props.columns?.map((col) => (
- <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>
- );
- // ============ MCP 专用组件 ============
- const TranslationResult = ({ props }: BaseComponentProps<z.infer<typeof TranslationResultProps>>) => {
- const [copied, setCopied] = useState(false);
- const handleCopy = () => {
- if (props.translated) {
- navigator.clipboard.writeText(props.translated);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- }
- };
- return (
- <div className={`bg-white dark:bg-gray-800 rounded-xl shadow-md border border-gray-200 dark:border-gray-700 overflow-hidden ${props.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">
- {props.original && (
- <div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-3 mb-3">
- <span className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">原文</span>
- <p className="text-gray-600 dark:text-gray-400 leading-relaxed mt-1">{props.original}</p>
- </div>
- )}
- <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">
- <span className="text-xs font-medium text-blue-600 dark:text-blue-400 uppercase tracking-wide">译文</span>
- <p className="text-gray-900 dark:text-gray-100 font-medium leading-relaxed mt-1">{props.translated}</p>
- </div>
- {props.termsUsed && props.termsUsed.length > 0 && (
- <div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-3">
- <span className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">使用术语</span>
- <div className="flex flex-wrap gap-2 mt-1">
- {props.termsUsed.map((term, idx) => (
- <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>
- );
- };
- const NovelList = ({ props, emit }: BaseComponentProps<z.infer<typeof NovelListProps>>) => (
- <div className={`space-y-3 ${props.className || ''}`}>
- {props.novels?.map((novel) => (
- <div
- key={String(novel.id)}
- onClick={() => emit('selectNovel')}
- 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 && (
- <span className="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200">
- {novel.chapterCount} chapters
- </span>
- )}
- {novel.tags?.map((tag) => (
- <span key={tag} className="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200">
- {tag}
- </span>
- ))}
- </div>
- </div>
- ))}
- </div>
- );
- const ChapterReader = ({ props }: BaseComponentProps<z.infer<typeof ChapterReaderProps>>) => (
- <div className={`bg-white dark:bg-gray-800 rounded-lg border dark:border-gray-700 p-6 ${props.className || ''}`}>
- <div className="border-b dark:border-gray-700 pb-4 mb-4">
- {props.novelTitle && <p className="text-sm text-gray-500 dark:text-gray-400 mb-1">{props.novelTitle}</p>}
- {props.chapterTitle && <h2 className="text-2xl font-bold text-gray-900 dark:text-white">{props.chapterTitle}</h2>}
- {props.chapterNumber && props.totalChapters && (
- <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
- Chapter {props.chapterNumber} of {props.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">{props.content}</p>
- </div>
- </div>
- );
- const CodeBlock = ({ props }: BaseComponentProps<z.infer<typeof CodeBlockProps>>) => {
- const [copied, setCopied] = useState(false);
- const handleCopy = () => {
- if (props.code) {
- navigator.clipboard.writeText(props.code);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- }
- };
- return (
- <div className={`bg-gray-900 dark:bg-gray-950 rounded-lg overflow-hidden ${props.className || ''}`}>
- <div className="flex items-center justify-between px-4 py-2 bg-gray-800 dark:bg-gray-900">
- <span className="text-xs text-gray-400 uppercase">{props.language || 'text'}</span>
- <button
- onClick={handleCopy}
- className="text-xs text-gray-400 hover:text-white transition-colors"
- >
- {copied ? '已复制!' : 'Copy'}
- </button>
- </div>
- <pre className="p-4 overflow-x-auto">
- <code className="text-sm text-gray-100 font-mono">{props.code}</code>
- </pre>
- </div>
- );
- };
- const ToolCall = ({ props }: BaseComponentProps<z.infer<typeof ToolCallProps>>) => {
- 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 status = props.status || 'success';
- const config = statusConfig[status];
- return (
- <div className={`border-l-4 p-3 rounded-r ${config.bg}`}>
- <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}`}>{props.toolName}</span>
- </div>
- {props.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">
- {typeof props.result === 'string' ? props.result : JSON.stringify(props.result, null, 2)}
- </pre>
- </div>
- )}
- {status === 'error' && (
- <div className="mt-2">
- <p className="text-xs text-red-600 dark:text-red-400">Error occurred</p>
- </div>
- )}
- </div>
- );
- };
- const LoginPanel = ({ props, emit }: BaseComponentProps<z.infer<typeof LoginPanelProps>>) => {
- const [email, setEmail] = useState('');
- const [password, setPassword] = useState('');
- const handleLogin = () => {
- if (props.onLogin) {
- emit(props.onLogin);
- }
- };
- return (
- <div className={`bg-white dark:bg-gray-800 rounded-lg border dark:border-gray-700 p-6 ${props.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">{props.serverName || 'MCP Server'}</h3>
- <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Sign in to continue</p>
- </div>
- <div 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"
- value={email}
- onChange={(e) => setEmail(e.target.value)}
- 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"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Password</label>
- <input
- type="password"
- placeholder="••••••••"
- value={password}
- onChange={(e) => setPassword(e.target.value)}
- 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"
- />
- </div>
- <button
- onClick={handleLogin}
- className="w-full bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md font-medium transition-colors"
- >
- Sign In
- </button>
- </div>
- </div>
- );
- };
- const McpStatus = ({ props }: BaseComponentProps<z.infer<typeof McpStatusProps>>) => (
- <div className={`flex items-center gap-2 px-3 py-2 rounded-lg ${props.connected ? 'bg-green-100 dark:bg-green-900/30' : 'bg-red-100 dark:bg-red-900/30'} ${props.className || ''}`}>
- <span className={`w-2 h-2 rounded-full ${props.connected ? 'bg-green-500' : 'bg-red-500'}`} />
- <span className="text-sm font-medium">
- {props.serverId}: {props.connected ? 'Connected' : 'Disconnected'}
- </span>
- </div>
- );
- const SuggestionButtons = ({ props }: BaseComponentProps<z.infer<typeof SuggestionButtonsProps>>) => {
- const { emit } = useActionEmit();
- const handleSelect = (suggestion: { label: string; message?: string }) => {
- const message = suggestion.message || suggestion.label;
- emit('sendMessage', { message });
- };
- return (
- <div className="flex flex-wrap gap-2">
- {props.suggestions?.map((s, i) => (
- <button
- key={i}
- onClick={() => handleSelect(s)}
- className="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-full text-sm transition-colors flex items-center gap-1.5"
- >
- {s.icon && <span>{s.icon}</span>}
- <span>{s.label}</span>
- </button>
- ))}
- </div>
- );
- };
- // ============ 创建并导出注册表 ============
- const { registry } = defineRegistry(catalog, {
- components: {
- // 基础组件
- card: Card,
- stack: Stack,
- heading: Heading,
- text: Text,
- button: Button,
- badge: Badge,
- separator: Separator,
- input: Input,
- 'text-area': TextArea,
- 'data-table': DataTable,
- // MCP 专用组件
- 'translation-result': TranslationResult,
- 'novel-list': NovelList,
- 'chapter-reader': ChapterReader,
- 'code-block': CodeBlock,
- 'tool-call': ToolCall,
- 'login-panel': LoginPanel,
- 'mcp-status': McpStatus,
- 'suggestion-buttons': SuggestionButtons,
- },
- });
- export { registry };
- export type { BaseComponentProps } from '@json-render/react';
|