Summary: - Fix Prompt list API response schema missing activeVersion and draftVersion fields - Fastify was filtering out undefined schema fields, causing version columns to show empty - Add detailed diagnostic logging for Prompt debug mode troubleshooting - Verify debug mode works correctly (DRAFT version is used when debug enabled) Changes: - backend/src/common/prompt/prompt.routes.ts: Add activeVersion and draftVersion to response schema - backend/src/common/prompt/prompt.service.ts: Add diagnostic logs for setDebugMode and get methods - PKB module: Various authentication and document handling fixes from previous session Tested: Debug mode verified working - v2 DRAFT version correctly loaded when debug enabled
22 KiB
22 KiB
AIA V2.1 前端组件设计
版本:V2.1
创建日期:2026-01-11
技术栈:React 19 + TypeScript 5 + Ant Design 6 + Ant Design X 2.1
📁 模块结构
frontend-v2/src/modules/aia/
├── pages/
│ ├── Dashboard.tsx # 智能体大厅(首页)
│ └── Workspace.tsx # 对话工作台
├── components/
│ ├── AgentPipeline/
│ │ ├── index.tsx # 5阶段流水线容器
│ │ ├── StageColumn.tsx # 单阶段列
│ │ └── AgentCard.tsx # 智能体卡片
│ ├── IntentSearch/
│ │ ├── index.tsx # 意图搜索框
│ │ └── SuggestionDropdown.tsx # 建议下拉框
│ ├── ConversationList/
│ │ ├── index.tsx # 历史会话列表
│ │ └── ConversationItem.tsx # 会话项
│ ├── MessageList/
│ │ ├── index.tsx # 消息列表
│ │ ├── UserMessage.tsx # 用户消息
│ │ ├── AssistantMessage.tsx # AI回复
│ │ └── ThinkingBlock.tsx # 深度思考折叠块
│ ├── Attachment/
│ │ ├── AttachmentUpload.tsx # 附件上传
│ │ ├── AttachmentCard.tsx # 附件卡片
│ │ └── AttachmentPreview.tsx # 附件预览
│ ├── SlashCommands/
│ │ └── index.tsx # 快捷指令菜单
│ └── ActionBar/
│ └── index.tsx # 结果操作栏
├── hooks/
│ ├── useConversation.ts # 对话管理
│ ├── useAgents.ts # 智能体数据
│ ├── useIntentRouter.ts # 意图路由
│ ├── useStreamMessage.ts # 流式消息
│ └── useAttachment.ts # 附件上传
├── api/
│ └── index.ts # API 封装
├── types/
│ └── index.ts # TypeScript 类型
├── styles/
│ ├── dashboard.module.css # Dashboard 样式
│ └── workspace.module.css # Workspace 样式
└── index.tsx # 模块入口 + 路由
🎨 设计规范
色彩系统
:root {
/* 5阶段流水线主题色 */
--stage-design: #3B82F6; /* 蓝色 - 研究设计 */
--stage-data: #8B5CF6; /* 紫色 - 数据采集 */
--stage-analysis: #10B981; /* 绿色 - 统计分析 */
--stage-write: #F59E0B; /* 橙色 - 论文撰写 */
--stage-publish: #EF4444; /* 红色 - 成果发布 */
/* 功能色 */
--ai-assistant: #6366F1; /* AI助手主色 */
--thinking-bg: #F3F4F6; /* 思考块背景 */
--thinking-border: #E5E7EB; /* 思考块边框 */
/* Gemini 风格 */
--bg-primary: #FFFFFF;
--bg-secondary: #F9FAFB;
--text-primary: #111827;
--text-secondary: #6B7280;
--border-light: #E5E7EB;
}
间距系统
/* 遵循 8px 网格 */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
--spacing-2xl: 48px;
断点
/* 移动优先 */
--breakpoint-sm: 640px; /* 手机 */
--breakpoint-md: 768px; /* 平板 */
--breakpoint-lg: 1024px; /* 桌面 */
--breakpoint-xl: 1280px; /* 大屏 */
📄 页面设计
1. Dashboard(智能体大厅)
// pages/Dashboard.tsx
import { IntentSearch } from '../components/IntentSearch';
import { AgentPipeline } from '../components/AgentPipeline';
export const Dashboard: React.FC = () => {
return (
<div className={styles.dashboard}>
{/* 顶部意图搜索框 */}
<header className={styles.header}>
<h1>AI 智能助手</h1>
<p>有什么可以帮助您的?</p>
<IntentSearch />
</header>
{/* 5阶段智能体流水线 */}
<main className={styles.main}>
<AgentPipeline />
</main>
</div>
);
};
布局特点:
- 顶部居中大搜索框
- 5阶段流水线横向平铺(桌面)/ 纵向滚动(移动)
- Gemini 风格大留白
2. Workspace(对话工作台)
// pages/Workspace.tsx
import { ChatContainer } from '@/shared/components/Chat';
import { ConversationList } from '../components/ConversationList';
import { ThinkingBlock } from '../components/MessageList/ThinkingBlock';
import { AttachmentUpload } from '../components/Attachment/AttachmentUpload';
export const Workspace: React.FC = () => {
const { conversationId } = useParams();
const { conversation, messages, sendMessage } = useConversation(conversationId);
return (
<div className={styles.workspace}>
{/* 左侧边栏 - 历史会话 */}
<aside className={styles.sidebar}>
<ConversationList />
</aside>
{/* 主对话区 */}
<main className={styles.main}>
{/* 对话头部 */}
<header className={styles.header}>
<AgentAvatar agent={conversation?.agent} />
<h2>{conversation?.agent?.name}</h2>
</header>
{/* 消息列表 - 复用通用 Chat 组件 */}
<ChatContainer
messages={messages}
onSend={sendMessage}
renderMessage={(msg) => (
<div className={styles.message}>
{/* 深度思考块 */}
{msg.thinkingContent && (
<ThinkingBlock content={msg.thinkingContent} />
)}
{/* 消息内容 */}
<MarkdownRenderer content={msg.content} />
{/* 附件卡片 */}
{msg.attachments?.map(att => (
<AttachmentCard key={att.id} attachment={att} />
))}
</div>
)}
inputFooter={<AttachmentUpload />}
/>
</main>
</div>
);
};
🧩 组件详细设计
1. AgentPipeline(5阶段流水线)
// components/AgentPipeline/index.tsx
interface AgentPipelineProps {
onAgentClick: (agentId: string) => void;
}
export const AgentPipeline: React.FC<AgentPipelineProps> = ({ onAgentClick }) => {
const { agents } = useAgents();
const stages = [
{ key: 'design', title: '研究设计', color: 'var(--stage-design)' },
{ key: 'data', title: '数据采集', color: 'var(--stage-data)' },
{ key: 'analysis', title: '统计分析', color: 'var(--stage-analysis)' },
{ key: 'write', title: '论文撰写', color: 'var(--stage-write)' },
{ key: 'publish', title: '成果发布', color: 'var(--stage-publish)' },
];
return (
<div className={styles.pipeline}>
{stages.map((stage, index) => (
<StageColumn
key={stage.key}
title={stage.title}
color={stage.color}
agents={agents.filter(a => a.stage === stage.key)}
onAgentClick={onAgentClick}
showConnector={index < stages.length - 1}
/>
))}
</div>
);
};
样式特点:
.pipeline {
display: flex;
gap: var(--spacing-lg);
overflow-x: auto;
padding: var(--spacing-xl) 0;
}
/* 移动端纵向布局 */
@media (max-width: 768px) {
.pipeline {
flex-direction: column;
overflow-x: visible;
}
}
2. IntentSearch(意图搜索框)
// components/IntentSearch/index.tsx
export const IntentSearch: React.FC = () => {
const [query, setQuery] = useState('');
const [suggestions, setSuggestions] = useState<IntentSuggestion[]>([]);
const { routeIntent, isLoading } = useIntentRouter();
const navigate = useNavigate();
// 500ms 防抖
const debouncedQuery = useDebouncedValue(query, 500);
useEffect(() => {
if (debouncedQuery.length >= 2) {
routeIntent(debouncedQuery).then(setSuggestions);
}
}, [debouncedQuery]);
const handleSelect = (suggestion: IntentSuggestion) => {
// 跳转到对应智能体
navigate(`/aia/workspace?agent=${suggestion.agentId}&prompt=${encodeURIComponent(suggestion.prefillPrompt)}`);
};
return (
<div className={styles.searchContainer}>
<Input.Search
size="large"
placeholder="描述您的需求,AI 将为您推荐合适的助手..."
value={query}
onChange={(e) => setQuery(e.target.value)}
loading={isLoading}
className={styles.searchInput}
/>
{suggestions.length > 0 && (
<SuggestionDropdown
suggestions={suggestions}
onSelect={handleSelect}
/>
)}
</div>
);
};
3. ThinkingBlock(深度思考折叠块)
// components/MessageList/ThinkingBlock.tsx
interface ThinkingBlockProps {
content: string;
duration?: number; // 思考耗时(秒)
isStreaming?: boolean; // 是否正在生成
}
export const ThinkingBlock: React.FC<ThinkingBlockProps> = ({
content,
duration,
isStreaming = false,
}) => {
// 生成中展开,完成后自动收起
const [expanded, setExpanded] = useState(isStreaming);
useEffect(() => {
if (!isStreaming && expanded) {
// 完成后 1.5s 自动收起
const timer = setTimeout(() => setExpanded(false), 1500);
return () => clearTimeout(timer);
}
}, [isStreaming]);
return (
<div className={styles.thinkingBlock}>
<div
className={styles.header}
onClick={() => setExpanded(!expanded)}
>
<span className={styles.icon}>
{isStreaming ? <LoadingOutlined spin /> : <BulbOutlined />}
</span>
<span className={styles.title}>
{isStreaming ? '正在深度思考...' : `已深度思考 (耗时 ${duration?.toFixed(1)}s)`}
</span>
<span className={styles.expandIcon}>
{expanded ? <UpOutlined /> : <DownOutlined />}
</span>
</div>
{expanded && (
<div className={styles.content}>
<Typography.Text type="secondary">
{content}
</Typography.Text>
</div>
)}
</div>
);
};
样式:
.thinkingBlock {
background: var(--thinking-bg);
border: 1px solid var(--thinking-border);
border-radius: 8px;
margin-bottom: var(--spacing-md);
}
.header {
display: flex;
align-items: center;
padding: var(--spacing-sm) var(--spacing-md);
cursor: pointer;
user-select: none;
}
.content {
padding: var(--spacing-md);
padding-top: 0;
font-size: 13px;
line-height: 1.6;
color: var(--text-secondary);
}
4. AttachmentUpload(附件上传)
// components/Attachment/AttachmentUpload.tsx
interface AttachmentUploadProps {
conversationId: string;
onUploadComplete: (attachment: Attachment) => void;
maxCount?: number; // 默认 5
}
export const AttachmentUpload: React.FC<AttachmentUploadProps> = ({
conversationId,
onUploadComplete,
maxCount = 5,
}) => {
const { uploadFile, uploading, progress } = useAttachment();
const [attachments, setAttachments] = useState<Attachment[]>([]);
const handleUpload = async (file: File) => {
if (attachments.length >= maxCount) {
message.error(`最多上传 ${maxCount} 个附件`);
return false;
}
// 文件类型校验
const allowedTypes = ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'];
if (!allowedTypes.includes(file.type)) {
message.error('仅支持 PDF、Word、TXT、Excel 文件');
return false;
}
// 文件大小校验(20MB)
if (file.size > 20 * 1024 * 1024) {
message.error('文件大小不能超过 20MB');
return false;
}
const attachment = await uploadFile(conversationId, file);
setAttachments([...attachments, attachment]);
onUploadComplete(attachment);
return false; // 阻止默认上传
};
return (
<div className={styles.uploadContainer}>
<Upload
beforeUpload={handleUpload}
showUploadList={false}
accept=".pdf,.docx,.txt,.xlsx"
multiple
>
<Button icon={<PaperClipOutlined />} type="text">
添加附件
</Button>
</Upload>
{/* 已上传附件列表 */}
<div className={styles.attachmentList}>
{attachments.map(att => (
<AttachmentCard
key={att.id}
attachment={att}
onRemove={() => {
setAttachments(attachments.filter(a => a.id !== att.id));
}}
/>
))}
</div>
{/* 上传进度 */}
{uploading && (
<Progress percent={progress} size="small" />
)}
</div>
);
};
5. SlashCommands(快捷指令)
// components/SlashCommands/index.tsx
interface SlashCommandsProps {
onSelect: (command: SlashCommand) => void;
onClose: () => void;
}
const commands: SlashCommand[] = [
{ key: 'polish', icon: '✨', label: '润色', description: '优化文本表达' },
{ key: 'expand', icon: '📝', label: '扩写', description: '扩展内容细节' },
{ key: 'translate', icon: '🌐', label: '翻译', description: '中英互译' },
{ key: 'export', icon: '📄', label: '导出Word', description: '导出为 Word 文档' },
];
export const SlashCommands: React.FC<SlashCommandsProps> = ({
onSelect,
onClose,
}) => {
const [selectedIndex, setSelectedIndex] = useState(0);
// 键盘导航
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
switch (e.key) {
case 'ArrowUp':
e.preventDefault();
setSelectedIndex(i => Math.max(0, i - 1));
break;
case 'ArrowDown':
e.preventDefault();
setSelectedIndex(i => Math.min(commands.length - 1, i + 1));
break;
case 'Enter':
e.preventDefault();
onSelect(commands[selectedIndex]);
break;
case 'Escape':
onClose();
break;
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [selectedIndex]);
return (
<div className={styles.commandMenu}>
{commands.map((cmd, index) => (
<div
key={cmd.key}
className={`${styles.commandItem} ${index === selectedIndex ? styles.selected : ''}`}
onClick={() => onSelect(cmd)}
onMouseEnter={() => setSelectedIndex(index)}
>
<span className={styles.icon}>{cmd.icon}</span>
<div className={styles.content}>
<div className={styles.label}>{cmd.label}</div>
<div className={styles.description}>{cmd.description}</div>
</div>
</div>
))}
</div>
);
};
6. ActionBar(结果操作栏)
// components/ActionBar/index.tsx
interface ActionBarProps {
message: Message;
onCopy: () => void;
onRegenerate: () => void;
onExport: () => void;
}
export const ActionBar: React.FC<ActionBarProps> = ({
message,
onCopy,
onRegenerate,
onExport,
}) => {
return (
<div className={styles.actionBar}>
<Tooltip title="复制">
<Button
type="text"
icon={<CopyOutlined />}
onClick={onCopy}
/>
</Tooltip>
<Tooltip title="重新生成">
<Button
type="text"
icon={<ReloadOutlined />}
onClick={onRegenerate}
/>
</Tooltip>
<Tooltip title="导出 Word">
<Button
type="text"
icon={<FileWordOutlined />}
onClick={onExport}
/>
</Tooltip>
</div>
);
};
🎣 Hooks 设计
useConversation
// hooks/useConversation.ts
interface UseConversationReturn {
conversation: Conversation | null;
messages: Message[];
isLoading: boolean;
sendMessage: (content: string, attachmentIds?: string[]) => Promise<void>;
regenerate: (messageId: string) => Promise<void>;
deleteConversation: () => Promise<void>;
}
export function useConversation(conversationId?: string): UseConversationReturn {
const [conversation, setConversation] = useState<Conversation | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
const [isLoading, setIsLoading] = useState(false);
// 加载对话
useEffect(() => {
if (conversationId) {
api.getConversation(conversationId).then(data => {
setConversation(data);
setMessages(data.messages);
});
}
}, [conversationId]);
// 发送消息(流式)
const sendMessage = async (content: string, attachmentIds?: string[]) => {
setIsLoading(true);
// 添加用户消息
const userMessage: Message = {
id: `temp-${Date.now()}`,
role: 'user',
content,
attachments: [],
createdAt: new Date().toISOString(),
};
setMessages(prev => [...prev, userMessage]);
// 初始化 AI 消息
const aiMessage: Message = {
id: `temp-ai-${Date.now()}`,
role: 'assistant',
content: '',
thinkingContent: '',
createdAt: new Date().toISOString(),
};
setMessages(prev => [...prev, aiMessage]);
// 流式接收
await api.sendMessageStream(conversationId!, content, attachmentIds, {
onThinkingDelta: (delta) => {
setMessages(prev => {
const last = prev[prev.length - 1];
return [...prev.slice(0, -1), {
...last,
thinkingContent: (last.thinkingContent || '') + delta,
}];
});
},
onDelta: (delta) => {
setMessages(prev => {
const last = prev[prev.length - 1];
return [...prev.slice(0, -1), {
...last,
content: last.content + delta,
}];
});
},
onComplete: (finalMessage) => {
setMessages(prev => {
return [...prev.slice(0, -1), finalMessage];
});
setIsLoading(false);
},
onError: (error) => {
message.error(error.message);
setIsLoading(false);
},
});
};
return {
conversation,
messages,
isLoading,
sendMessage,
regenerate: async () => {},
deleteConversation: async () => {},
};
}
useStreamMessage
// hooks/useStreamMessage.ts
interface StreamCallbacks {
onThinkingStart?: () => void;
onThinkingDelta?: (content: string) => void;
onThinkingEnd?: (duration: number) => void;
onMessageStart?: (id: string) => void;
onDelta?: (content: string) => void;
onMessageEnd?: (message: Message) => void;
onComplete?: (message: Message) => void;
onError?: (error: Error) => void;
}
export function useStreamMessage() {
const streamMessage = async (
url: string,
body: object,
callbacks: StreamCallbacks
) => {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
},
body: JSON.stringify(body),
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader!.read();
if (done) break;
const chunk = decoder.decode(value);
const events = parseSSE(chunk);
for (const event of events) {
switch (event.type) {
case 'thinking_start':
callbacks.onThinkingStart?.();
break;
case 'thinking_delta':
callbacks.onThinkingDelta?.(event.data.content);
break;
case 'thinking_end':
callbacks.onThinkingEnd?.(event.data.duration);
break;
case 'message_start':
callbacks.onMessageStart?.(event.data.id);
break;
case 'delta':
callbacks.onDelta?.(event.data.content);
break;
case 'message_end':
callbacks.onMessageEnd?.(event.data);
break;
case 'done':
callbacks.onComplete?.(event.data);
break;
case 'error':
callbacks.onError?.(new Error(event.data.message));
break;
}
}
}
};
return { streamMessage };
}
📱 响应式设计
断点策略
// 断点定义
const breakpoints = {
sm: 640, // 手机
md: 768, // 平板(主要断点)
lg: 1024, // 桌面
xl: 1280, // 大屏
};
Dashboard 响应式
| 断点 | 布局 |
|---|---|
< 768px |
流水线纵向滚动,卡片单列 |
≥ 768px |
流水线横向 5 列 |
Workspace 响应式
| 断点 | 布局 |
|---|---|
< 768px |
侧边栏隐藏(抽屉滑出),输入框键盘适配 |
≥ 768px |
侧边栏固定显示 240px |
📝 类型定义
// types/index.ts
export interface Agent {
id: string;
name: string;
description: string;
icon: string;
stage: 'design' | 'data' | 'analysis' | 'write' | 'publish';
color: string;
knowledgeBaseId?: string;
isTool?: boolean;
targetModule?: string;
welcomeMessage?: string;
suggestedQuestions?: string[];
}
export interface Conversation {
id: string;
title: string;
agentId: string;
agent?: Agent;
projectId?: string;
messageCount: number;
lastMessage?: string;
createdAt: string;
updatedAt: string;
}
export interface Message {
id: string;
conversationId?: string;
role: 'user' | 'assistant';
content: string;
thinkingContent?: string;
attachments?: Attachment[];
model?: string;
tokens?: number;
isPinned?: boolean;
createdAt: string;
}
export interface Attachment {
id: string;
filename: string;
mimeType: string;
size: number;
ossUrl: string;
textExtracted: boolean;
tokenCount: number;
truncated: boolean;
createdAt: string;
}
export interface IntentSuggestion {
agentId: string;
agentName: string;
confidence: number;
prefillPrompt: string;
}
export interface SlashCommand {
key: string;
icon: string;
label: string;
description: string;
}
📝 更新日志
| 日期 | 版本 | 内容 |
|---|---|---|
| 2026-01-11 | V1.0 | 创建前端组件设计文档 |