Files
HaHafeng 2481b786d8 deploy: Complete 0126-27 deployment - database upgrade, services update, code recovery
Major Changes:
- Database: Install pg_bigm/pgvector plugins, create test database
- Python service: v1.0 -> v1.1, add pymupdf4llm/openpyxl/pypandoc
- Node.js backend: v1.3 -> v1.7, fix pino-pretty and ES Module imports
- Frontend: v1.2 -> v1.3, skip TypeScript check for deployment
- Code recovery: Restore empty files from local backup

Technical Fixes:
- Fix pino-pretty error in production (conditional loading)
- Fix ES Module import paths (add .js extensions)
- Fix OSSAdapter TypeScript errors
- Update Prisma Schema (63 models, 16 schemas)
- Update environment variables (DATABASE_URL, EXTRACTION_SERVICE_URL, OSS)
- Remove deprecated variables (REDIS_URL, DIFY_API_URL, DIFY_API_KEY)

Documentation:
- Create 0126 deployment folder with 8 documents
- Update database development standards v2.0
- Update SAE deployment status records

Deployment Status:
- PostgreSQL: ai_clinical_research_test with plugins
- Python: v1.1 @ 172.17.173.84:8000
- Backend: v1.7 @ 172.17.173.89:3001
- Frontend: v1.3 @ 172.17.173.90:80

Tested: All services running successfully on SAE
2026-01-27 08:13:27 +08:00

904 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 # 模块入口 + 路由
```
---
## 🎨 设计规范
### 色彩系统
```css
: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;
}
```
### 间距系统
```css
/* 遵循 8px 网格 */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
--spacing-2xl: 48px;
```
### 断点
```css
/* 移动优先 */
--breakpoint-sm: 640px; /* 手机 */
--breakpoint-md: 768px; /* 平板 */
--breakpoint-lg: 1024px; /* 桌面 */
--breakpoint-xl: 1280px; /* 大屏 */
```
---
## 📄 页面设计
### 1. Dashboard智能体大厅
```tsx
// 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对话工作台
```tsx
// 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. AgentPipeline5阶段流水线
```tsx
// 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>
);
};
```
**样式特点**
```css
.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意图搜索框
```tsx
// 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深度思考折叠块
```tsx
// 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>
);
};
```
**样式**
```css
.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附件上传
```tsx
// 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快捷指令
```tsx
// 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结果操作栏
```tsx
// 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
```typescript
// 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
```typescript
// 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 };
}
```
---
## 📱 响应式设计
### 断点策略
```typescript
// 断点定义
const breakpoints = {
sm: 640, // 手机
md: 768, // 平板(主要断点)
lg: 1024, // 桌面
xl: 1280, // 大屏
};
```
### Dashboard 响应式
| 断点 | 布局 |
|------|------|
| `< 768px` | 流水线纵向滚动,卡片单列 |
| `≥ 768px` | 流水线横向 5 列 |
### Workspace 响应式
| 断点 | 布局 |
|------|------|
| `< 768px` | 侧边栏隐藏(抽屉滑出),输入框键盘适配 |
| `≥ 768px` | 侧边栏固定显示 240px |
---
## 📝 类型定义
```typescript
// 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 | 创建前端组件设计文档 |