|
@@ -0,0 +1,474 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * 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';
|