/**
* 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);
}