/** * json-render 组件目录 * * 定义 MCP 工具专用组件和基础 shadcn 组件 * 用于生成式 UI (Generative UI) 渲染 * * ## 交互功能 * * 组件支持通过 `emit` 函数触发交互事件: * * ```typescript * // 在组件中接收 emit 函数 * const MyComponent = ({ emit }: any) => ( * * ); * ``` * * ### 支持的事件类型 * * | 事件名 | 负载 | 说明 | * |--------|------|------| * | `sendMessage` | `{ message: string }` | 发送消息到聊天 | * | `selectNovel` | `{ id, title, ... }` | 选择小说 | * | `copy` | `{ text: string }` | 复制文本到剪贴板 | * * ### 组件交互示例 * * ```typescript * // NovelList 组件 - 点击小说卡片自动发送消息 *
emit('selectNovel', { id: novel.id, title: novel.title })}> * {novel.title} *
* ``` */ 'use client'; import { z } from 'zod'; // ============ 基础组件 Schema 定义 ============ // Card 组件 export const CardSchema = z.object({ type: z.literal('card'), title: z.string().optional(), children: z.array(z.any()).optional(), className: z.string().optional(), }); // Stack 组件 export const StackSchema = z.object({ type: z.literal('stack'), direction: z.enum(['row', 'column']).optional(), spacing: z.number().optional(), align: z.enum(['start', 'center', 'end', 'stretch']).optional(), children: z.array(z.any()).optional(), className: z.string().optional(), }); // Heading 组件 export const HeadingSchema = z.object({ type: z.literal('heading'), level: z.enum(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']).optional(), text: z.string(), className: z.string().optional(), }); // Text 组件 export const TextSchema = z.object({ type: z.literal('text'), content: z.string(), variant: z.enum(['body', 'muted', 'code']).optional(), className: z.string().optional(), }); // Button 组件 export const ButtonSchema = z.object({ type: z.literal('button'), label: z.string(), variant: z.enum(['default', 'primary', 'secondary', 'ghost', 'danger']).optional(), onClick: z.string().optional(), // JavaScript 代码字符串(保留兼容) action: z.string().optional(), // 交互事件名:使用 emit 触发 actionPayload: z.any().optional(), // 事件负载数据 disabled: z.boolean().optional(), className: z.string().optional(), }); // Input 组件 export const InputSchema = z.object({ type: z.literal('input'), placeholder: z.string().optional(), value: z.string().optional(), onChange: z.string().optional(), // JavaScript 代码字符串 disabled: z.boolean().optional(), className: z.string().optional(), }); // Badge 组件 export const BadgeSchema = z.object({ type: z.literal('badge'), text: z.string(), variant: z.enum(['default', 'success', 'warning', 'error', 'info']).optional(), className: z.string().optional(), }); // Separator 组件 export const SeparatorSchema = z.object({ type: z.literal('separator'), orientation: z.enum(['horizontal', 'vertical']).optional(), className: z.string().optional(), }); // ============ MCP 专用组件 Schema 定义 ============ // TranslationResult 组件 - 翻译结果展示 export const TranslationResultSchema = z.object({ type: z.literal('translation-result'), translated: z.string(), termsUsed: z.array(z.string()).optional(), className: z.string().optional(), }); // NovelList 组件 - 小说列表 export const NovelListSchema = z.object({ type: z.literal('novel-list'), novels: z.array(z.object({ id: z.string(), title: z.string(), author: z.string().optional(), description: z.string().optional(), chapterCount: z.number().optional(), tags: z.array(z.string()).optional(), })), onSelect: z.string().optional(), // 回调函数名(保留兼容) action: z.string().optional(), // 交互事件:点击卡片后触发的事件 className: z.string().optional(), }); // ChapterReader 组件 - 章节阅读器 export const ChapterReaderSchema = z.object({ type: z.literal('chapter-reader'), novelTitle: z.string(), chapterTitle: z.string(), content: z.string(), chapterNumber: z.number().optional(), totalChapters: z.number().optional(), onPrev: z.string().optional(), onNext: z.string().optional(), className: z.string().optional(), }); // McpToolCall 组件 - 工具调用过程展示 export const McpToolCallSchema = z.object({ type: z.literal('mcp-tool-call'), tool: z.string(), status: z.enum(['pending', 'running', 'success', 'error']), args: z.any().optional(), result: z.any().optional(), error: z.string().optional(), timestamp: z.string().optional(), className: z.string().optional(), }); // LoginPanel 组件 - Platform MCP 登录面板 export const LoginPanelSchema = z.object({ type: z.literal('login-panel'), server: z.string(), email: z.string().optional(), onLogin: z.string().optional(), loading: z.boolean().optional(), className: z.string().optional(), }); // CodeBlock 组件 - 代码块展示 export const CodeBlockSchema = z.object({ type: z.literal('code-block'), code: z.string(), language: z.string().optional(), inline: z.boolean().optional(), className: z.string().optional(), }); // DataTable 组件 - 数据表格 export const DataTableSchema = z.object({ type: z.literal('data-table'), columns: z.array(z.object({ key: z.string(), label: z.string(), sortable: z.boolean().optional(), })), rows: z.array(z.record(z.string(), z.any())), sortable: z.boolean().optional(), onSort: z.string().optional(), className: z.string().optional(), }); // NovelDetail 组件 - 小说详情卡片 export const NovelDetailSchema = z.object({ type: z.literal('novel-detail'), novel: z.object({ id: z.string().optional(), title: z.string().optional(), author: z.string().optional(), category: z.string().optional(), description: z.string().optional(), status: z.string().optional(), chapterCount: z.number().optional(), wordCount: z.number().optional(), viewCount: z.number().optional(), isVip: z.boolean().optional(), }).optional(), className: z.string().optional(), }); // SuggestionButtons 组件 - 建议操作按钮组 export const SuggestionButtonsSchema = z.object({ type: z.literal('suggestion-buttons'), suggestions: z.array(z.object({ label: z.string().optional(), message: z.string(), icon: z.string().optional(), })), className: z.string().optional(), }); // ============ 导出联合类型 ============ export const ComponentSchema = z.discriminatedUnion('type', [ CardSchema, StackSchema, HeadingSchema, TextSchema, ButtonSchema, InputSchema, BadgeSchema, SeparatorSchema, TranslationResultSchema, NovelListSchema, NovelDetailSchema, ChapterReaderSchema, McpToolCallSchema, LoginPanelSchema, CodeBlockSchema, DataTableSchema, SuggestionButtonsSchema, ]); export type ComponentSpec = z.infer; // ============ 工具函数 ============ /** * 验证 JSON spec 是否为有效的组件定义 */ export function validateComponentSpec(data: unknown): ComponentSpec | null { const result = ComponentSchema.safeParse(data); return result.success ? result.data : null; } /** * 从 MCP 工具调用结果生成组件 spec */ export function specFromToolCall(tool: string, data: unknown): ComponentSpec | null { console.log('[specFromToolCall] Input:', { tool, data }); const dataObj = data as Record; switch (tool) { case 'translate_text': // 后端返回: { success: true, translated: "...", terms_used: [...] } let translatedText = ''; let termsUsed: string[] = []; // data 可能是: // 1. 包装对象: { result: "{...}" } // 2. 直接结果: { success: true, translated: "...", terms_used: [...] } // 3. 字符串: "{success: true, ...}" // 先检查是否是包装对象(有 result 字段) let resultData = dataObj.result !== undefined ? dataObj.result : dataObj; if (typeof resultData === 'string') { try { const parsed = JSON.parse(resultData); translatedText = parsed.translated || ''; termsUsed = (parsed.terms_used as string[]) || []; } catch { translatedText = resultData; } } else if (typeof resultData === 'object' && resultData !== null) { const obj = resultData as Record; translatedText = (obj.translated as string) || ''; termsUsed = (obj.terms_used as string[]) || []; } return { type: 'translation-result', translated: translatedText, termsUsed: termsUsed, }; case 'get_novels': return { type: 'novel-list', novels: (dataObj.novels as Array) || [], }; case 'get_novel_detail': case 'novel_detail': return { type: 'novel-detail', novel: dataObj.novel || dataObj, }; case 'get_chapter': return { type: 'chapter-reader', novelTitle: (dataObj.novel_title as string) || 'Unknown Novel', chapterTitle: (dataObj.chapter_title as string) || `Chapter ${dataObj.chapter_number || 1}`, content: (dataObj.content as string) || '', chapterNumber: dataObj.chapter_number as number, totalChapters: dataObj.total_chapters as number, }; case 'mcp_tool_call': return { type: 'mcp-tool-call', tool: (dataObj.tool as string) || tool, status: (dataObj.status as any) || 'success', args: dataObj.args, result: dataObj.result, error: dataObj.error as string, timestamp: dataObj.timestamp as string, }; case 'login': return { type: 'login-panel', server: (dataObj.server as string) || 'Novel Platform', email: dataObj.email as string, }; case 'suggestions': case 'suggest_actions': return { type: 'suggestion-buttons', suggestions: (dataObj.suggestions as Array) || [], }; // 组件类型直接返回(AI 生成的组件 spec) case 'novel-detail': return dataObj as any; case 'suggestion-buttons': return dataObj as any; default: // 默认返回代码块组件显示原始数据 return { type: 'code-block', code: JSON.stringify(data, null, 2), language: 'json', }; } } /** * 从多个工具调用结果生成组件列表 */ export function specsFromToolCalls( toolCalls: Array<{ tool: string; result: unknown }> ): ComponentSpec[] { return toolCalls .map(({ tool, result }) => specFromToolCall(tool, result)) .filter((spec): spec is ComponentSpec => spec !== null); }