| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376 |
- /**
- * json-render 组件目录
- *
- * 定义 MCP 工具专用组件和基础 shadcn 组件
- * 用于生成式 UI (Generative UI) 渲染
- *
- * ## 交互功能
- *
- * 组件支持通过 `emit` 函数触发交互事件:
- *
- * ```typescript
- * // 在组件中接收 emit 函数
- * const MyComponent = ({ emit }: any) => (
- * <button onClick={() => emit('eventName', payload)}>
- * Click Me
- * </button>
- * );
- * ```
- *
- * ### 支持的事件类型
- *
- * | 事件名 | 负载 | 说明 |
- * |--------|------|------|
- * | `sendMessage` | `{ message: string }` | 发送消息到聊天 |
- * | `selectNovel` | `{ id, title, ... }` | 选择小说 |
- * | `copy` | `{ text: string }` | 复制文本到剪贴板 |
- *
- * ### 组件交互示例
- *
- * ```typescript
- * // NovelList 组件 - 点击小说卡片自动发送消息
- * <div onClick={() => emit('selectNovel', { id: novel.id, title: novel.title })}>
- * {novel.title}
- * </div>
- * ```
- */
- '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<typeof ComponentSchema>;
- // ============ 工具函数 ============
- /**
- * 验证 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<string, unknown>;
- 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<string, unknown>;
- 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<any>) || [],
- };
- 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<any>) || [],
- };
- // 组件类型直接返回(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);
- }
|