fix(pkb): fix create KB and upload issues - remove simulated upload, fix department mapping, add upload modal

Fixed issues:
- Remove simulateUpload function from DashboardPage Step 3
- Map department to description field when creating KB
- Add upload modal in WorkspacePage knowledge assets tab
- Fix DocumentUpload import path (../../stores to ../stores)

Known issue: Dify API validation error during document upload (file uploaded but DB record failed, needs investigation)

Testing: KB creation works, upload dialog opens correctly
This commit is contained in:
2026-01-13 13:17:20 +08:00
parent d595037316
commit 4088275290
280 changed files with 4344 additions and 150 deletions

View File

@@ -0,0 +1,135 @@
# **壹证循科技 \- AI智能问答 (AIA) V2.1 产品需求文档**
**文档状态:** Draft (V2.1)
**关联原型:** AI问答原型图V11.html
**架构标准:** Postgres-Only | Ant Design X | Schema Isolation
**最后更新:** 2026-01-11
**本次更新:** 新增移动端适配、深度思考模式、UI/UX 细节规范
## **1\. 概述与目标**
### **1.1 背景**
原 AIA 模块功能较为单一。V2.0+ 版本旨在构建一个**全流程驱动的科研智能体大厅 (Agent Hub)**,对标 ChatGPT/Gemini 的流畅体验,并深度结合医学科研场景。
### **1.2 核心价值**
1. **流程化引导**:打破“只有一个对话框”的迷茫感,通过 5 个阶段引导用户逐步推进研究。
2. **专业分工**:不同的智能体加载不同的 System Prompt 和知识库。
3. **全端体验**:提供桌面端到移动端的无缝衔接,随时随地记录科研灵感。
## **2\. 核心业务流程 (User Flow)**
1. **进入大厅 (Dashboard)**
* 顶部提供“全局意图搜索框”,用户输入模糊需求。
* 系统识别意图,自动跳转到对应的智能体。
2. **选择智能体**
* 用户也可以直接点击具体的卡片如“06 样本量计算”)。
3. **沉浸式工作台 (Workspace)**
* 进入具体的对话界面。
* **桌面端**:三栏布局(历史记录 \+ 对话区 \+ 如果有右侧引用栏)。
* **移动端**:单栏布局,历史记录折叠为抽屉。
4. **结果产出**
* AI 展示**思考过程 (Thinking Process)** 后输出最终答案。
* 用户可一键导出 Word 报告。
## **3\. 功能需求详情**
### **F1. 智能体大厅 (Dashboard) \- 基于 V11 原型**
#### **F1.1 全局意图路由 (Intent Router)**
* **UI**:顶部大搜索框,文案“输入研究想法...”。
* **逻辑**:后端调用 Router Agent 识别意图 \-\> 跳转目标 Agent \-\> 自动填入 Prompt。
#### **F1.2 智能体流水线展示 (Agent Pipeline)**
严格还原 V11 原型的 **5 阶段布局****3 色视觉体系**(详见原型图)。
* **跳转逻辑**
* **常规卡片**:进入 Chat Workspace。
* **工具卡片 (09, 10\)**:跳转 /dc/portal 或 /st/dashboard。
### **F2. 沉浸式对话工作台 (Chat Workspace)**
#### **F2.1 界面布局与风格 (UI/UX)**
* **设计理念**:参考 Gemini 的“大留白”风格,减少分割线,使用卡片投影区分层级。
* **左侧侧边栏 (Sidebar)**
* **桌面端**:固定显示,可折叠(图标化)。
* **移动端**默认隐藏通过左上角“汉堡菜单”滑出Drawer 组件)。
* **内容**按时间分组的历史会话今天、昨天、7天前
* **主对话区 (Main Area)**
* **Header**:显示当前智能体名称 \+ 状态("思考中..." / "输入中...")。
* **Input**:底部悬浮,支持多行自动增高。
#### **F2.2 核心对话能力增强**
* **深度思考模式 (Thinking Process)**
* **需求**针对“科学问题梳理”等复杂任务AI 需先输出推理过程。
* **交互**:输出 \<think\>...\</think\> 内容时,前端显示为**可折叠的灰色引用块**,默认展开,生成结束后自动收起,只显示“已深度思考 (耗时 12s)”。
* **Markdown 渲染增强**
* **公式**:使用 Katex 渲染 LaTeX 医学公式。
* **表格**:支持横向滚动,防止手机端撑破布局。
* **代码**:支持一键复制,语法高亮。
* **快捷指令 (Slash Commands)**
* 输入 / 弹出快捷菜单:/润色, /扩写, /翻译, /导出Word。
#### **F2.3 附件上传 (Attachment)**
* **格式**PDF, Word, TXT, Excel (限制 20MB)。
* **处理 (Postgres-Only)**
1. 上传 OSS \-\> common\_schema.files。
2. 后端提取文本 \-\> 截取注入 LLM Context。
3. **UI 显示**:在气泡下方显示附件小卡片(图标+文件名)。
#### **F2.4 结果操作栏 (Action Bar)**
每条 AI 回答下方提供微型工具栏Hover 显示):
* **复制**:复制 Markdown 源码。
* **重新生成**:不满意当前回答,触发重试。
* **导出 Word**:调用 RVW 模块的 docx 导出服务,将当前单轮对话导出为文档。
### **F3. 移动端适配 (Mobile Adaptation)**
#### **F3.1 响应式布局**
* **断点**md (768px)。
* **Dashboard**
* 隐藏顶部复杂导航,只保留 Logo 和 用户头像。
* 智能体卡片由 3 列变为 1 列流式布局。
* 左侧时间轴Timeline在移动端需调整样式避免占用过多水平空间可改为顶部横向步骤条或简化左侧线条
#### **F3.2 交互优化**
* **输入体验**
* 输入框聚焦时,确保不被 iOS/Android 软键盘遮挡scrollIntoView
* 发送按钮在移动端需始终可见。
* **触控反馈**:按钮和卡片增加 active 态背景色变化。
## **4\. 数据架构 (Postgres-Only)**
*(保持不变,复用 V2.0 定义)*
// schema.prisma (新增字段)
model AiaMessage {
// ... 现有字段
thinkingContent String? @db.Text // 存储思维链内容 (\<think\>...\</think\>)
// ...
}
## **5\. 配置化开发策略**
*(保持不变)*
## **6\. 非功能性需求**
* **流式体验**TTFB (首字响应时间) \< 1.5s。
* **移动端性能**:首屏加载 \< 1s (LCP)。
* **防抖**:意图搜索框需增加 500ms 防抖,避免频繁请求 Router Agent。

View File

@@ -0,0 +1,665 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI智能问答 V3.0 - 完整版</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Noto+Sans+SC:wght@400;500;700&display=swap');
body {
font-family: 'Noto Sans SC', 'Inter', sans-serif;
background-color: #F8FAFC;
}
/* --- 品牌色体系 (与V11保持一致) --- */
:root {
--brand-blue: #4F6EF2;
--brand-teal: #0D9488;
--brand-purple: #9333EA;
--brand-yellow: #CA8A04;
}
/* 自定义滚动条 */
.scrollbar-hide::-webkit-scrollbar { display: none; }
.custom-scroll::-webkit-scrollbar { width: 6px; }
.custom-scroll::-webkit-scrollbar-track { background: transparent; }
.custom-scroll::-webkit-scrollbar-thumb { background-color: #CBD5E1; border-radius: 3px; }
/* 思考过程折叠动画 */
.thinking-box {
transition: all 0.3s ease-in-out;
overflow: hidden;
}
.thinking-box.collapsed { max-height: 48px; }
.thinking-box.expanded { max-height: 800px; }
/* 打字机光标 */
.cursor-blink {
display: inline-block;
width: 2px;
height: 1.2em;
background-color: currentColor;
animation: blink 1s step-end infinite;
vertical-align: middle;
}
@keyframes blink { 50% { opacity: 0; } }
/* 卡片基础样式 */
.card-box {
transition: all 0.2s ease-in-out;
position: relative;
overflow: hidden;
}
.card-box:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
/* 水印序号 */
.num-watermark {
font-family: 'Inter', sans-serif;
font-weight: 800;
font-size: 1.5rem;
opacity: 0.3;
transition: all 0.3s;
}
.card-box:hover .num-watermark {
opacity: 0.8;
transform: scale(1.1);
}
/* --- 主题配色类 --- */
/* Blue (Phase 1 & 2) */
.theme-blue .agent-icon { background-color: #EFF6FF; color: #4F6EF2; border: 1px solid #DBEAFE; }
.theme-blue:hover { border-color: #4F6EF2; background-color: #EEF4FF; }
.theme-blue .num-watermark { color: #BFDBFE; }
/* Yellow (Phase 3) */
.theme-yellow .agent-icon { background-color: #FEFCE8; color: #CA8A04; border: 1px solid #FEF08A; }
.theme-yellow:hover { border-color: #CA8A04; background-color: #FEF9C3; }
.theme-yellow .num-watermark { color: #FDE047; }
/* Teal (Phase 4 - Tools) */
.theme-teal { background-color: #F0FDFA; }
.theme-teal .agent-icon { background-color: #FFFFFF; color: #0D9488; border: 1px solid #CCFBF1; }
.theme-teal:hover { border-color: #0D9488; background-color: #E0F2F1; }
.theme-teal .num-watermark { color: #99F6E4; }
/* Purple (Phase 5) */
.theme-purple .agent-icon { background-color: #FAF5FF; color: #9333EA; border: 1px solid #E9D5FF; }
.theme-purple:hover { border-color: #9333EA; background-color: #F3E8FF; }
.theme-purple .num-watermark { color: #D8B4FE; }
/* Markdown 样式微调 */
.prose p { margin-bottom: 0.5em; }
.prose ul { list-style-type: disc; padding-left: 1.2em; margin-bottom: 0.5em; }
/* 移动端侧边栏遮罩 */
.drawer-overlay { background-color: rgba(0,0,0,0.5); backdrop-filter: blur(2px); transition: opacity 0.3s; }
</style>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
brand: {
blue: '#4F6EF2',
teal: '#0D9488',
purple: '#9333EA',
yellow: '#CA8A04'
}
},
animation: {
'fade-in': 'fadeIn 0.3s ease-out',
'fade-in-up': 'fadeInUp 0.5s ease-out'
},
keyframes: {
fadeIn: { '0%': { opacity: '0' }, '100%': { opacity: '1' } },
fadeInUp: { '0%': { opacity: '0', transform: 'translateY(10px)' }, '100%': { opacity: '1', transform: 'translateY(0)' } }
}
}
}
}
</script>
</head>
<body class="text-slate-600 h-screen overflow-hidden flex flex-col">
<!-- ================= 应用容器 ================= -->
<div id="app" class="flex-1 flex flex-col relative overflow-hidden">
<!-- ================= 视图 1: 智能体大厅 (Dashboard) ================= -->
<div id="view-dashboard" class="flex-1 overflow-y-auto bg-white transition-opacity duration-300">
<!-- Navbar -->
<nav class="sticky top-0 z-40 bg-white/90 backdrop-blur border-b border-slate-100 h-16 flex items-center justify-between px-4 md:px-8">
<div class="flex items-center gap-2">
<div class="w-8 h-8 bg-brand-blue rounded-lg flex items-center justify-center text-white shadow-sm">
<i data-lucide="bot" class="w-5 h-5"></i>
</div>
<span class="font-bold text-lg text-slate-800 tracking-tight">AI 临床研究平台</span>
</div>
<div class="flex items-center gap-4">
<div class="w-8 h-8 rounded-full bg-slate-100 border border-slate-200 overflow-hidden cursor-pointer">
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Felix" alt="User">
</div>
</div>
</nav>
<!-- Hero Section -->
<div class="max-w-3xl mx-auto pt-10 pb-8 px-4 text-center">
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-blue-50 text-brand-blue text-xs font-semibold mb-4 border border-blue-100 animate-fade-in-up">
<i data-lucide="sparkles" class="w-3 h-3"></i> 已接入 DeepSeek-V3
</div>
<h1 class="text-3xl md:text-4xl font-bold text-slate-900 mb-6 tracking-tight">想研究什么?</h1>
<div class="relative group max-w-2xl mx-auto">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<i data-lucide="search" class="w-5 h-5 text-slate-400 group-focus-within:text-brand-blue transition-colors"></i>
</div>
<input type="text"
class="w-full pl-11 pr-14 py-4 rounded-xl border border-slate-200 shadow-sm focus:outline-none focus:ring-2 focus:ring-brand-blue/20 focus:border-brand-blue transition-all text-base bg-white"
placeholder="输入想法,自动匹配智能体(如:肺癌的回顾性研究..."
onkeydown="if(event.key === 'Enter') enterChat('TOPIC_01', this.value)"
>
<button class="absolute right-2 top-2 bottom-2 aspect-square bg-brand-blue text-white rounded-lg hover:bg-blue-600 transition-colors shadow-sm flex items-center justify-center" onclick="enterChat('TOPIC_01')">
<i data-lucide="arrow-right" class="w-5 h-5"></i>
</button>
</div>
</div>
<!-- Agent Pipeline (12 Modules) -->
<div class="max-w-5xl mx-auto px-4 pb-20 space-y-8">
<!-- Stage 1: 选题优化 (Blue) -->
<div class="relative pl-8 border-l-2 border-slate-100">
<div class="absolute -left-[9px] top-0 w-4 h-4 rounded-full bg-brand-blue border-2 border-white shadow-sm ring-2 ring-blue-50"></div>
<h2 class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-4">Phase 1: 选题优化</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- 01 -->
<div onclick="enterChat('TOPIC_01')" class="card-box bg-white border border-slate-200 rounded-xl p-4 cursor-pointer theme-blue group">
<span class="absolute top-3 right-3 num-watermark">01</span>
<div class="agent-icon w-10 h-10 rounded-lg flex items-center justify-center mb-3 transition-colors">
<i data-lucide="list-tree" class="w-5 h-5"></i>
</div>
<h3 class="font-bold text-slate-800 mb-1">科学问题梳理</h3>
<p class="text-xs text-slate-500 line-clamp-2">从科学问题的清晰度、系统性、可验证性等角度使用科学理论对您的科学问题进行全面的评价。</p>
</div>
<!-- 02 -->
<div onclick="enterChat('TOPIC_02')" class="card-box bg-white border border-slate-200 rounded-xl p-4 cursor-pointer theme-blue group">
<span class="absolute top-3 right-3 num-watermark">02</span>
<div class="agent-icon w-10 h-10 rounded-lg flex items-center justify-center mb-3 transition-colors">
<i data-lucide="target" class="w-5 h-5"></i>
</div>
<h3 class="font-bold text-slate-800 mb-1">PICO 梳理</h3>
<p class="text-xs text-slate-500 line-clamp-2">基于科学问题梳理研究对象、干预(暴露)、对照和结局指标,并评价并给出合理化的建议。</p>
</div>
<!-- 03 -->
<div onclick="enterChat('TOPIC_03')" class="card-box bg-white border border-slate-200 rounded-xl p-4 cursor-pointer theme-blue group">
<span class="absolute top-3 right-3 num-watermark">03</span>
<div class="agent-icon w-10 h-10 rounded-lg flex items-center justify-center mb-3 transition-colors">
<i data-lucide="clipboard-check" class="w-5 h-5"></i>
</div>
<h3 class="font-bold text-slate-800 mb-1">选题评价</h3>
<p class="text-xs text-slate-500 line-clamp-2">从创新性、临床价值、科学性和可行性等方面使用科学理论对您的临床问题进行全面的评价。</p>
</div>
</div>
</div>
<!-- Stage 2: 方案设计 (Blue) -->
<div class="relative pl-8 border-l-2 border-slate-100">
<div class="absolute -left-[9px] top-0 w-4 h-4 rounded-full bg-brand-blue border-2 border-white shadow-sm ring-2 ring-blue-50"></div>
<h2 class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-4">Phase 2: 方案设计</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<!-- 04 -->
<div onclick="enterChat('DESIGN_04')" class="card-box bg-white border border-slate-200 rounded-xl p-4 cursor-pointer theme-blue group">
<span class="absolute top-3 right-3 num-watermark">04</span>
<div class="agent-icon w-10 h-10 rounded-lg flex items-center justify-center mb-3 transition-colors">
<i data-lucide="eye" class="w-5 h-5"></i>
</div>
<h3 class="font-bold text-slate-800 mb-1">观察指标设计</h3>
<p class="text-xs text-slate-500 line-clamp-2">基于研究设计和因果关系提供可能的观察指标集。</p>
</div>
<!-- 05 -->
<div onclick="enterChat('DESIGN_05')" class="card-box bg-white border border-slate-200 rounded-xl p-4 cursor-pointer theme-blue group">
<span class="absolute top-3 right-3 num-watermark">05</span>
<div class="agent-icon w-10 h-10 rounded-lg flex items-center justify-center mb-3 transition-colors">
<i data-lucide="table" class="w-5 h-5"></i>
</div>
<h3 class="font-bold text-slate-800 mb-1">病例报告表设计</h3>
<p class="text-xs text-slate-500 line-clamp-2">基于研究方案设计梳理观察指标集并给出建议的病例报告表框架。</p>
</div>
<!-- 06 -->
<div onclick="enterChat('DESIGN_06')" class="card-box bg-white border border-slate-200 rounded-xl p-4 cursor-pointer theme-blue group">
<span class="absolute top-3 right-3 num-watermark">06</span>
<div class="agent-icon w-10 h-10 rounded-lg flex items-center justify-center mb-3 transition-colors">
<i data-lucide="calculator" class="w-5 h-5"></i>
</div>
<h3 class="font-bold text-slate-800 mb-1">样本量计算</h3>
<p class="text-xs text-slate-500 line-clamp-2">基于研究阶段和研究设计为研究提供科学合理的样本量估算结果。</p>
</div>
<!-- 07 -->
<div onclick="enterChat('DESIGN_07')" class="card-box bg-white border border-slate-200 rounded-xl p-4 cursor-pointer theme-blue group">
<span class="absolute top-3 right-3 num-watermark">07</span>
<div class="agent-icon w-10 h-10 rounded-lg flex items-center justify-center mb-3 transition-colors">
<i data-lucide="file-text" class="w-5 h-5"></i>
</div>
<h3 class="font-bold text-slate-800 mb-1">方案撰写 (Pro)</h3>
<p class="text-xs text-slate-500 line-clamp-2">基于科学问题、PICOS等信息给出一个初步的临床研究设计方案。</p>
</div>
</div>
</div>
<!-- Stage 3: 方案预评审 (Yellow) -->
<div class="relative pl-8 border-l-2 border-slate-100">
<div class="absolute -left-[9px] top-0 w-4 h-4 rounded-full bg-brand-yellow border-2 border-white shadow-sm ring-2 ring-yellow-50"></div>
<h2 class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-4">Phase 3: 方案预评审</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- 08 -->
<div onclick="enterChat('REVIEW_08')" class="card-box bg-white border border-slate-200 rounded-xl p-4 cursor-pointer theme-yellow group">
<span class="absolute top-3 right-3 num-watermark">08</span>
<div class="agent-icon w-10 h-10 rounded-lg flex items-center justify-center mb-3 transition-colors">
<i data-lucide="shield-check" class="w-5 h-5"></i>
</div>
<h3 class="font-bold text-slate-800 mb-1">方法学评审</h3>
<p class="text-xs text-slate-500 line-clamp-2">从研究问题、研究方案和临床意义方面,对研究进行临床研究方法学的全面评价。</p>
</div>
</div>
</div>
<!-- Stage 4: 数据与统计 (Teal - Tools) -->
<div class="relative pl-8 border-l-2 border-slate-100">
<div class="absolute -left-[9px] top-0 w-4 h-4 rounded-full bg-brand-teal border-2 border-white shadow-sm ring-2 ring-teal-50"></div>
<h2 class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-4">Phase 4: 数据与统计 (工具)</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- 09 -->
<div class="card-box border border-slate-200 rounded-xl p-4 cursor-pointer theme-teal group">
<i data-lucide="external-link" class="absolute top-3 right-3 w-4 h-4 text-slate-300 group-hover:text-brand-teal transition-colors"></i>
<span class="absolute bottom-3 right-3 num-watermark">09</span>
<div class="agent-icon w-10 h-10 rounded-lg flex items-center justify-center mb-3 transition-colors">
<i data-lucide="database" class="w-5 h-5"></i>
</div>
<h3 class="font-bold text-slate-800 mb-1">数据评价与预处理</h3>
<p class="text-xs text-slate-500 line-clamp-2">对现有数据的质量进行自动评价,并给出数据质量报告,包括缺失值、异常值、定量分析等。</p>
</div>
<!-- 10 -->
<div class="card-box border border-slate-200 rounded-xl p-4 cursor-pointer theme-teal group">
<i data-lucide="external-link" class="absolute top-3 right-3 w-4 h-4 text-slate-300 group-hover:text-brand-teal transition-colors"></i>
<span class="absolute bottom-3 right-3 num-watermark">10</span>
<div class="agent-icon w-10 h-10 rounded-lg flex items-center justify-center mb-3 transition-colors">
<i data-lucide="bar-chart-2" class="w-5 h-5"></i>
</div>
<h3 class="font-bold text-slate-800 mb-1">智能统计分析</h3>
<p class="text-xs text-slate-500 line-clamp-2">内置3条智能研究路径队列研究、预测模型、RCT研究以及近百种统计分析工具。</p>
</div>
</div>
</div>
<!-- Stage 5: 写作助手 (Purple) -->
<div class="relative pl-8 border-l-2 border-transparent">
<div class="absolute -left-[9px] top-0 w-4 h-4 rounded-full bg-brand-purple border-2 border-white shadow-sm ring-2 ring-purple-50"></div>
<h2 class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-4">Phase 5: 写作助手</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- 11 -->
<div onclick="enterChat('WRITING_11')" class="card-box bg-white border border-slate-200 rounded-xl p-4 cursor-pointer theme-purple group">
<span class="absolute top-3 right-3 num-watermark">11</span>
<div class="agent-icon w-10 h-10 rounded-lg flex items-center justify-center mb-3 transition-colors">
<i data-lucide="pen-tool" class="w-5 h-5"></i>
</div>
<h3 class="font-bold text-slate-800 mb-1">论文润色</h3>
<p class="text-xs text-slate-500 line-clamp-2">结合目标杂志,提供专业化的润色服务,给出合理化的修改建议。</p>
</div>
<!-- 12 -->
<div onclick="enterChat('WRITING_12')" class="card-box bg-white border border-slate-200 rounded-xl p-4 cursor-pointer theme-purple group">
<span class="absolute top-3 right-3 num-watermark">12</span>
<div class="agent-icon w-10 h-10 rounded-lg flex items-center justify-center mb-3 transition-colors">
<i data-lucide="languages" class="w-5 h-5"></i>
</div>
<h3 class="font-bold text-slate-800 mb-1">论文翻译</h3>
<p class="text-xs text-slate-500 line-clamp-2">结合目标杂志,提供专业化的翻译并进行润色,给出合理化的修改建议。</p>
</div>
</div>
</div>
</div>
</div>
<!-- ================= 视图 2: 沉浸式工作台 (Chat Workspace) ================= -->
<div id="view-chat" class="flex-1 flex bg-white hidden">
<!-- Mobile Drawer Overlay -->
<div id="mobile-overlay" class="absolute inset-0 z-40 bg-black/20 backdrop-blur-sm hidden lg:hidden" onclick="toggleSidebar()"></div>
<!-- Sidebar (Desktop & Mobile Drawer) -->
<aside id="chat-sidebar" class="absolute lg:relative z-50 w-64 h-full bg-slate-50 border-r border-slate-200 transform -translate-x-full lg:translate-x-0 transition-transform duration-300 flex flex-col">
<div class="p-4 border-b border-slate-100 flex items-center justify-between">
<button onclick="exitChat()" class="flex items-center gap-2 text-slate-500 hover:text-slate-800 transition-colors">
<i data-lucide="chevron-left" class="w-5 h-5"></i>
<span class="font-bold text-sm">返回大厅</span>
</button>
<button class="lg:hidden p-1 text-slate-400" onclick="toggleSidebar()">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<!-- New Chat Button -->
<div class="p-4">
<button onclick="clearChat()" class="w-full py-2.5 px-4 bg-white border border-slate-200 hover:border-brand-blue hover:text-brand-blue rounded-lg shadow-sm text-sm font-medium transition-all flex items-center justify-center gap-2 group">
<i data-lucide="plus" class="w-4 h-4 text-slate-400 group-hover:text-brand-blue"></i> 新建对话
</button>
</div>
<!-- History List -->
<div class="flex-1 overflow-y-auto px-2 pb-4 custom-scroll space-y-6">
<div>
<div class="px-2 text-[10px] font-bold text-slate-400 uppercase mb-2">Today</div>
<div class="space-y-1">
<button class="w-full text-left px-3 py-2 rounded-lg bg-slate-200/50 text-slate-800 text-xs font-medium truncate border-l-2 border-brand-blue">
关于肺癌的PICO梳理
</button>
<button class="w-full text-left px-3 py-2 rounded-lg hover:bg-slate-100 text-slate-600 text-xs truncate border-l-2 border-transparent">
样本量计算咨询
</button>
</div>
</div>
</div>
<!-- User Profile Bottom -->
<div class="p-4 border-t border-slate-200 bg-slate-100/50">
<div class="flex items-center gap-2">
<div class="w-7 h-7 rounded-full bg-brand-blue text-white flex items-center justify-center text-xs">U</div>
<div class="flex-1 min-w-0">
<div class="text-xs font-medium text-slate-700 truncate">Dr. Wang</div>
<div class="text-[10px] text-slate-400">专业版会员</div>
</div>
</div>
</div>
</aside>
<!-- Main Chat Area -->
<main class="flex-1 flex flex-col h-full relative w-full">
<!-- Chat Header -->
<header class="h-14 border-b border-slate-100 flex items-center justify-between px-4 bg-white/80 backdrop-blur z-10 sticky top-0">
<div class="flex items-center gap-3">
<button onclick="toggleSidebar()" class="lg:hidden p-2 -ml-2 text-slate-500 hover:bg-slate-100 rounded-md">
<i data-lucide="menu" class="w-5 h-5"></i>
</button>
<div id="current-agent-icon" class="w-8 h-8 rounded-lg bg-brand-blue text-white flex items-center justify-center">
<i data-lucide="bot" class="w-5 h-5"></i>
</div>
<div>
<h2 id="current-agent-name" class="text-sm font-bold text-slate-800">智能体名称</h2>
<div class="flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-green-500"></span>
<span class="text-[10px] text-slate-500">Online</span>
</div>
</div>
</div>
<div class="flex items-center gap-2">
<button class="p-2 text-slate-400 hover:text-brand-blue rounded-md transition-colors" title="导出对话">
<i data-lucide="download" class="w-4 h-4"></i>
</button>
</div>
</header>
<!-- Messages Container -->
<div id="messages-container" class="flex-1 overflow-y-auto p-4 md:p-6 space-y-6 custom-scroll bg-white">
<!-- 欢迎语 (动态生成) -->
</div>
<!-- Input Area -->
<div class="p-4 border-t border-slate-100 bg-white relative">
<!-- Attachments Preview (Hidden) -->
<div id="attachment-preview" class="hidden absolute -top-12 left-4 bg-white border border-slate-200 rounded-lg shadow-sm px-3 py-1.5 flex items-center gap-2 text-xs">
<i data-lucide="file-text" class="w-4 h-4 text-slate-400"></i>
<span class="text-slate-700 font-medium">protocol_draft_v1.pdf</span>
<button onclick="removeAttachment()" class="hover:text-red-500"><i data-lucide="x" class="w-3 h-3"></i></button>
</div>
<div class="max-w-4xl mx-auto relative bg-slate-50 border border-slate-200 rounded-2xl focus-within:ring-2 focus-within:ring-brand-blue/10 focus-within:border-brand-blue transition-all shadow-sm">
<textarea id="chat-input" rows="1"
class="w-full py-3.5 pl-4 pr-12 bg-transparent border-none focus:ring-0 text-sm resize-none max-h-32"
placeholder="输入问题,或使用 / 呼出快捷指令..."
oninput="this.style.height = ''; this.style.height = this.scrollHeight + 'px'"
onkeydown="if(event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage(); }"
></textarea>
<!-- Toolbar -->
<div class="absolute bottom-2 right-2 flex items-center gap-1">
<button class="p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-200 rounded-lg transition-colors" onclick="document.getElementById('file-upload').click()">
<i data-lucide="paperclip" class="w-4 h-4"></i>
</button>
<input type="file" id="file-upload" class="hidden" onchange="handleFileUpload(this)">
<button id="send-btn" class="p-2 bg-slate-900 text-white rounded-lg hover:bg-slate-700 transition-colors shadow-sm disabled:opacity-50 disabled:cursor-not-allowed" onclick="sendMessage()">
<i data-lucide="send-horizontal" class="w-4 h-4"></i>
</button>
</div>
</div>
<div class="text-center mt-2">
<p class="text-[10px] text-slate-400">内容由 AI 生成,需经过专业人员核实。支持 Markdown 公式与表格。</p>
</div>
</div>
</main>
</div>
</div>
<!-- ================= 逻辑脚本 ================= -->
<script>
// --- 1. 智能体配置 (12 Modules) ---
const AGENTS = {
'TOPIC_01': { name: '科学问题梳理', icon: 'list-tree', color: 'blue', prompt: '你好!我是**科学问题梳理助手**。请告诉我你感兴趣的研究方向,我将从科学性、创新性等维度帮你梳理。' },
'TOPIC_02': { name: 'PICO 梳理', icon: 'target', color: 'blue', prompt: '你好!我是**PICO梳理助手**。请描述你的研究想法,我来帮你拆解为 P (人群)、I (干预)、C (对照)、O (结局)。' },
'TOPIC_03': { name: '选题评价', icon: 'clipboard-check', color: 'blue', prompt: '你好!我是**选题评价助手**。请提供你的初步选题,我将从创新性、临床价值等方面进行评估。' },
'DESIGN_04': { name: '观察指标设计', icon: 'eye', color: 'blue', prompt: '你好!我是**观察指标设计助手**。请告诉我你的研究目的,我来帮你推荐主要和次要观察指标。' },
'DESIGN_05': { name: '病例报告表设计', icon: 'table', color: 'blue', prompt: '你好!我是**CRF设计助手**。我可以帮你构建病例报告表的框架。' },
'DESIGN_06': { name: '样本量计算', icon: 'calculator', color: 'blue', prompt: '你好需要计算样本量吗请告诉我研究类型如RCT、队列研究和主要指标的预期值。' },
'DESIGN_07': { name: '方案撰写 (Pro)', icon: 'file-text', color: 'blue', prompt: '你好!欢迎来到**方案设计工作台**。我们将基于科学问题和PICOS信息共同撰写一份完整的临床研究方案。' },
'REVIEW_08': { name: '方法学评审', icon: 'shield-check', color: 'yellow', prompt: '你好!我是**方法学评审助手**。请上传你的方案草稿,我将模拟审稿人视角进行方法学审查。' },
'WRITING_11': { name: '论文润色', icon: 'pen-tool', color: 'purple', prompt: 'Please paste the text you want to polish. I will check grammar, flow, and academic tone.' },
'WRITING_12': { name: '论文翻译', icon: 'languages', color: 'purple', prompt: '你好!请贴入需要翻译的段落,我将为你提供地道的学术英语翻译。' }
};
let currentAgentId = null;
// --- 2. 视图切换逻辑 ---
function enterChat(agentId, initialQuery = '') {
const agent = AGENTS[agentId];
if (!agent) {
// 处理工具类卡片 (09, 10) 的逻辑
// 在真实场景中跳转路由,这里模拟提示
alert(`即将跳转至工具模块:${agentId}`);
return;
}
currentAgentId = agentId;
// UI Update
document.getElementById('view-dashboard').style.display = 'none';
document.getElementById('view-chat').classList.remove('hidden');
// Set Header Info
document.getElementById('current-agent-name').innerText = agent.name;
const iconContainer = document.getElementById('current-agent-icon');
iconContainer.innerHTML = `<i data-lucide="${agent.icon}" class="w-5 h-5"></i>`;
// Set Color Theme (Update Header Icon Bg)
iconContainer.className = `w-8 h-8 rounded-lg flex items-center justify-center text-white`;
if (agent.color === 'purple') iconContainer.classList.add('bg-purple-600');
else if (agent.color === 'yellow') iconContainer.classList.add('bg-yellow-600');
else iconContainer.classList.add('bg-brand-blue');
// Init Chat
const container = document.getElementById('messages-container');
container.innerHTML = '';
// Add Welcome Message
addMessage('ai', agent.prompt);
// If there's an initial query from dashboard search
if (initialQuery) {
setTimeout(() => {
document.getElementById('chat-input').value = initialQuery;
sendMessage();
}, 500);
}
lucide.createIcons();
}
function exitChat() {
document.getElementById('view-chat').classList.add('hidden');
document.getElementById('view-dashboard').style.display = 'block';
}
function toggleSidebar() {
const sidebar = document.getElementById('chat-sidebar');
const overlay = document.getElementById('mobile-overlay');
if (sidebar.classList.contains('-translate-x-full')) {
sidebar.classList.remove('-translate-x-full'); // Open
overlay.classList.remove('hidden');
} else {
sidebar.classList.add('-translate-x-full'); // Close
overlay.classList.add('hidden');
}
}
function clearChat() {
document.getElementById('messages-container').innerHTML = '';
if (currentAgentId) addMessage('ai', AGENTS[currentAgentId].prompt);
}
// --- 3. 消息发送与深度思考模拟 ---
function sendMessage() {
const input = document.getElementById('chat-input');
const text = input.value.trim();
if (!text) return;
addMessage('user', text);
input.value = '';
input.style.height = 'auto';
// Simulate AI
const msgId = Date.now();
const container = document.getElementById('messages-container');
const aiWrapper = document.createElement('div');
aiWrapper.className = 'flex gap-3 animate-fade-in';
aiWrapper.innerHTML = `
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-indigo-500 to-brand-blue text-white flex-shrink-0 flex items-center justify-center shadow-sm">
<i data-lucide="bot" class="w-5 h-5"></i>
</div>
<div class="flex-1 space-y-2 max-w-[85%]">
<div id="think-${msgId}" class="thinking-box expanded bg-slate-50 border border-slate-200 rounded-lg text-xs text-slate-500 overflow-hidden">
<div class="px-3 py-2 border-b border-slate-100 flex items-center justify-between cursor-pointer hover:bg-slate-100" onclick="toggleThinking('${msgId}')">
<div class="flex items-center gap-2">
<i data-lucide="brain-circuit" class="w-3.5 h-3.5 animate-pulse text-brand-blue"></i>
<span class="font-medium" id="think-status-${msgId}">深度思考中...</span>
</div>
<i id="chevron-${msgId}" data-lucide="chevron-down" class="w-3 h-3 transition-transform"></i>
</div>
<div class="p-3 font-mono text-slate-400 bg-slate-50/50" id="think-content-${msgId}"></div>
</div>
<div id="answer-${msgId}" class="prose prose-sm prose-slate max-w-none bg-white p-3 rounded-xl border border-transparent"><span class="cursor-blink"></span></div>
</div>
`;
container.appendChild(aiWrapper);
container.scrollTop = container.scrollHeight;
lucide.createIcons();
simulateDeepSeekStream(msgId);
}
function addMessage(role, content) {
const container = document.getElementById('messages-container');
const isUser = role === 'user';
const div = document.createElement('div');
div.className = `flex gap-3 ${isUser ? 'flex-row-reverse' : ''} animate-fade-in`;
const avatar = isUser
? `<div class="w-8 h-8 rounded-lg bg-slate-200 overflow-hidden flex-shrink-0"><img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Felix"></div>`
: `<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-indigo-500 to-brand-blue text-white flex-shrink-0 flex items-center justify-center shadow-sm"><i data-lucide="bot" class="w-5 h-5"></i></div>`;
const bubble = isUser
? `<div class="bg-brand-blue text-white px-4 py-2.5 rounded-2xl rounded-tr-none shadow-md text-sm max-w-[85%]">${content}</div>`
: `<div class="prose prose-sm prose-slate max-w-[85%] bg-white border border-slate-100 px-4 py-3 rounded-2xl rounded-tl-none shadow-sm">${marked.parse(content)}</div>`;
div.innerHTML = avatar + bubble;
container.appendChild(div);
container.scrollTop = container.scrollHeight;
lucide.createIcons();
}
function simulateDeepSeekStream(msgId) {
const thoughts = ["识别上下文...", "检索知识库...", "组织回答..."];
const thinkContent = document.getElementById(`think-content-${msgId}`);
const thinkStatus = document.getElementById(`think-status-${msgId}`);
const answerBox = document.getElementById(`answer-${msgId}`);
let tIndex = 0;
const thinkInterval = setInterval(() => {
if (tIndex >= thoughts.length) {
clearInterval(thinkInterval);
thinkStatus.innerText = `已深度思考 (耗时 1.2s)`;
document.getElementById(`think-${msgId}`).classList.add('collapsed');
document.getElementById(`think-${msgId}`).classList.remove('expanded');
startAnswering(answerBox);
return;
}
const p = document.createElement('div');
p.innerText = `> ${thoughts[tIndex]}`;
thinkContent.appendChild(p);
tIndex++;
}, 500);
}
function startAnswering(element) {
const fullText = "收到,我明白了。这是一个很好的研究方向...";
let i = 0;
element.innerHTML = '<span class="cursor-blink"></span>';
const interval = setInterval(() => {
if (i >= fullText.length) {
clearInterval(interval);
element.innerHTML = marked.parse(fullText);
return;
}
element.innerHTML = marked.parse(fullText.substring(0, i+1)) + '<span class="cursor-blink"></span>';
i++;
document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight;
}, 30);
}
function toggleThinking(id) {
const box = document.getElementById(`think-${id}`);
if (box.classList.contains('collapsed')) {
box.classList.remove('collapsed');
box.classList.add('expanded');
document.getElementById(`chevron-${id}`).style.transform = 'rotate(180deg)';
} else {
box.classList.add('collapsed');
box.classList.remove('expanded');
document.getElementById(`chevron-${id}`).style.transform = 'rotate(0deg)';
}
}
function handleFileUpload(input) {
if (input.files && input.files[0]) {
const preview = document.getElementById('attachment-preview');
preview.querySelector('span').innerText = input.files[0].name;
preview.classList.remove('hidden');
}
}
function removeAttachment() {
document.getElementById('file-upload').value = '';
document.getElementById('attachment-preview').classList.add('hidden');
}
lucide.createIcons();
</script>
</body>
</html>

View File

@@ -0,0 +1,514 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>临床研究平台 - AI医学科研助手 V11</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@500;600;700&family=Noto+Sans+SC:wght@400;500;700&display=swap');
body {
font-family: 'Noto Sans SC', 'Inter', sans-serif;
background-color: #FFFFFF;
}
/* 核心品牌色 - 智能体 (Blue) */
:root {
--brand-blue: #4F6EF2;
--brand-hover: #3d5afe;
--card-bg: #F6F9FF;
--card-border: #E0E7FF;
--active-border: #4F6EF2;
--active-bg: #EEF4FF;
/* 新增:工具板块色 (Teal) */
--tool-teal: #0D9488;
--tool-bg: #F0FDFA; /* 极淡青色背景 */
--tool-border: #CCFBF1;
--tool-active-border: #0D9488;
--tool-active-bg: #E0F2F1;
}
.text-brand { color: var(--brand-blue); }
.bg-brand { background-color: var(--brand-blue); }
/* 通用卡片样式 (基于 V9改) */
.card-box {
background-color: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 10px;
padding: 14px;
transition: all 0.2s ease-in-out;
height: 100%;
min-height: 145px;
display: flex;
flex-direction: column;
position: relative;
box-shadow: 0 2px 5px rgba(0,0,0,0.02);
}
.card-box:hover {
transform: translateY(-3px);
border-color: var(--active-border);
border-width: 1.5px;
background-color: var(--active-bg);
box-shadow: 0 8px 16px rgba(79, 110, 242, 0.12);
margin: -0.5px;
}
/* 特殊样式:工具卡片 (Tool Card) */
.card-box.tool-card {
background-color: var(--tool-bg);
border-color: var(--tool-border);
}
.card-box.tool-card:hover {
border-color: var(--tool-active-border);
background-color: var(--tool-active-bg);
box-shadow: 0 8px 16px rgba(13, 148, 136, 0.15); /* 青色阴影 */
}
/* 图标容器 */
.icon-box {
background-color: #FFFFFF;
border: 1px solid #DCE6FF;
transition: all 0.2s;
padding: 5px;
}
.card-box:hover .icon-box {
background-color: #DBEafe;
border-color: #93C5FD;
transform: scale(1.05);
}
/* 工具卡片的图标容器 */
.card-box.tool-card .icon-box {
border-color: #CCFBF1;
color: var(--tool-teal);
}
.card-box.tool-card:hover .icon-box {
background-color: #CCFBF1;
border-color: #5EEAD4;
}
/* 左侧时间轴线 */
.timeline-line {
position: absolute;
left: 10px;
top: 28px;
bottom: -28px;
width: 2px;
background-color: #E2E8F0;
z-index: 0;
}
.last-item .timeline-line { display: none; }
/* 序号水印 */
.num-watermark {
color: #E0E7FF;
font-family: 'Inter', sans-serif;
font-weight: 700;
line-height: 1;
font-size: 1.25rem;
}
.card-box:hover .num-watermark {
color: #C7D2FE;
}
/* 工具卡片的水印颜色 */
.card-box.tool-card .num-watermark {
color: #CCFBF1;
}
.card-box.tool-card:hover .num-watermark {
color: #99F6E4;
}
/* 文本优化 */
.desc-text {
text-align: justify;
text-justify: inter-ideograph;
line-height: 1.5;
font-size: 13px;
color: #64748b;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
margin-top: 8px;
flex-grow: 1;
}
/* 标题样式 */
.card-title {
font-size: 15px;
font-weight: 700;
color: #0f172a;
}
/* 链接跳转图标 */
.link-icon {
position: absolute;
top: 12px;
right: 12px;
color: #94A3B8;
opacity: 0.6;
transition: all 0.2s;
}
.card-box:hover .link-icon {
opacity: 1;
color: var(--tool-teal);
transform: translate(2px, -2px);
}
/* 导航栏 */
.nav-item.active {
color: var(--brand-blue);
font-weight: 600;
position: relative;
}
.nav-item.active::after {
content: '';
position: absolute;
bottom: -19px;
left: 0;
width: 100%;
height: 3px;
background-color: var(--brand-blue);
border-radius: 2px 2px 0 0;
}
</style>
</head>
<body class="text-slate-600 bg-white">
<!-- 顶部导航 -->
<nav class="bg-white border-b border-slate-100 h-14 sticky top-0 z-50">
<div class="max-w-[760px] mx-auto px-4 h-full flex items-center justify-between">
<div class="flex items-center space-x-8">
<div class="flex items-center space-x-2">
<span class="text-brand font-bold text-base bg-indigo-50 px-2 py-1 rounded">临床研究平台</span>
</div>
<div class="hidden md:flex items-center space-x-6 text-sm font-medium text-slate-600">
<a href="#" class="hover:text-brand transition-colors">研究管理</a>
<a href="#" class="hover:text-brand transition-colors">统计分析工具</a>
<a href="#" class="nav-item active">AI问答</a>
</div>
</div>
<div class="flex items-center space-x-3 text-xs text-slate-400">
<span>186****8738</span>
<span class="text-slate-200">|</span>
<a href="#" class="hover:text-slate-600">退出账号</a>
</div>
</div>
</nav>
<!-- 主体内容 -->
<main class="max-w-[760px] mx-auto px-4 py-6">
<!-- 头部搜索区 -->
<div class="flex flex-col items-center justify-center mb-8 mt-8">
<div class="flex items-center space-x-2 mb-5">
<div class="bg-brand text-white p-1.5 rounded-lg">
<i data-lucide="brain-circuit" class="w-6 h-6"></i>
</div>
<h1 class="text-xl md:text-2xl font-bold text-slate-800">
医学研究专属大模型 <span class="text-[11px] font-normal text-brand bg-indigo-50 border border-indigo-100 px-2 py-0.5 rounded ml-2 align-middle">已接入DeepSeek</span>
</h1>
</div>
<div class="w-full max-w-2xl relative flex gap-2">
<input type="text" placeholder="输入研究想法例如SGLT2抑制剂对心衰患者预后的影响..."
class="flex-1 pl-4 pr-12 py-2.5 rounded-lg border border-slate-300 bg-white focus:outline-none focus:border-brand focus:ring-1 focus:ring-brand text-sm shadow-sm transition-all">
<button class="absolute right-1 top-1 bottom-1 bg-brand hover:bg-blue-600 text-white px-5 rounded-md transition-colors flex items-center justify-center">
<i data-lucide="search" class="w-5 h-5"></i>
</button>
</div>
</div>
<!-- 流水线模块 -->
<div class="pl-6 md:pl-8">
<!-- 1. 选题优化智能体 -->
<div class="relative mb-6">
<div class="absolute -left-7 md:-left-9 top-0 h-full flex flex-col items-center">
<div class="w-6 h-6 rounded-full bg-brand text-white flex items-center justify-center text-xs font-bold shadow-sm z-10 border border-white">1</div>
<div class="timeline-line"></div>
</div>
<h2 class="text-sm font-bold text-slate-800 mb-3 flex items-center">
选题优化智能体
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- 01 -->
<div class="card-box group cursor-pointer">
<div class="flex justify-between items-center mb-1">
<div class="flex items-center gap-2">
<div class="icon-box rounded text-brand">
<i data-lucide="list-tree" class="w-5 h-5"></i>
</div>
<h3 class="card-title">科学问题梳理</h3>
</div>
<span class="num-watermark">01</span>
</div>
<p class="desc-text">
从科学问题的清晰度、系统性、可验证性等角度使用科学理论对您的科学问题进行全面的评价。
</p>
</div>
<!-- 02 -->
<div class="card-box group cursor-pointer">
<div class="flex justify-between items-center mb-1">
<div class="flex items-center gap-2">
<div class="icon-box rounded text-brand">
<i data-lucide="target" class="w-5 h-5"></i>
</div>
<h3 class="card-title">PICO 梳理</h3>
</div>
<span class="num-watermark">02</span>
</div>
<p class="desc-text">
基于科学问题梳理研究对象、干预(暴露)、对照和结局指标,并评价并给出合理化的建议。
</p>
</div>
<!-- 03 -->
<div class="card-box group cursor-pointer">
<div class="flex justify-between items-center mb-1">
<div class="flex items-center gap-2">
<div class="icon-box rounded text-brand">
<i data-lucide="clipboard-check" class="w-5 h-5"></i>
</div>
<h3 class="card-title">选题评价</h3>
</div>
<span class="num-watermark">03</span>
</div>
<p class="desc-text">
从创新性、临床价值、科学性和可行性等方面使用科学理论对您的临床问题进行全面的评价。
</p>
</div>
</div>
</div>
<!-- 2. 方案设计智能体 -->
<div class="relative mb-6">
<div class="absolute -left-7 md:-left-9 top-0 h-full flex flex-col items-center">
<div class="w-6 h-6 rounded-full bg-brand text-white flex items-center justify-center text-xs font-bold shadow-sm z-10 border border-white">2</div>
<div class="timeline-line"></div>
</div>
<h2 class="text-sm font-bold text-slate-800 mb-3 flex items-center">
方案设计智能体
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- 04 -->
<div class="card-box group cursor-pointer">
<div class="flex justify-between items-center mb-1">
<div class="flex items-center gap-2">
<div class="icon-box rounded text-indigo-600 border-indigo-100">
<i data-lucide="eye" class="w-5 h-5"></i>
</div>
<h3 class="card-title">观察指标设计</h3>
</div>
<span class="num-watermark">04</span>
</div>
<p class="desc-text">
基于研究设计和因果关系提供可能的观察指标集。
</p>
</div>
<!-- 05 -->
<div class="card-box group cursor-pointer">
<div class="flex justify-between items-center mb-1">
<div class="flex items-center gap-2">
<div class="icon-box rounded text-indigo-600 border-indigo-100">
<i data-lucide="table" class="w-5 h-5"></i>
</div>
<h3 class="card-title">病例报告表设计</h3>
</div>
<span class="num-watermark">05</span>
</div>
<p class="desc-text">
基于研究方案设计梳理观察指标集并给出建议的病例报告表框架。
</p>
</div>
<!-- 06 -->
<div class="card-box group cursor-pointer">
<div class="flex justify-between items-center mb-1">
<div class="flex items-center gap-2">
<div class="icon-box rounded text-indigo-600 border-indigo-100">
<i data-lucide="calculator" class="w-5 h-5"></i>
</div>
<h3 class="card-title">样本量计算</h3>
</div>
<span class="num-watermark">06</span>
</div>
<p class="desc-text">
基于研究阶段和研究设计为研究提供科学合理的样本量估算结果。
</p>
</div>
<!-- 07 -->
<div class="card-box group cursor-pointer">
<div class="flex justify-between items-center mb-1">
<div class="flex items-center gap-2">
<div class="icon-box rounded text-indigo-600 border-indigo-100">
<i data-lucide="file-text" class="w-5 h-5"></i>
</div>
<h3 class="card-title">临床研究方案撰写</h3>
</div>
<span class="num-watermark">07</span>
</div>
<p class="desc-text">
基于科学问题、PICOS等信息给出一个初步的临床研究设计方案请尽量多给一些信息和需求。
</p>
</div>
</div>
</div>
<!-- 3. 方案预评审 -->
<div class="relative mb-6">
<div class="absolute -left-7 md:-left-9 top-0 h-full flex flex-col items-center">
<div class="w-6 h-6 rounded-full bg-brand text-white flex items-center justify-center text-xs font-bold shadow-sm z-10 border border-white">3</div>
<div class="timeline-line"></div>
</div>
<h2 class="text-sm font-bold text-slate-800 mb-3 flex items-center">
方案预评审
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- 08 -->
<div class="card-box group cursor-pointer">
<div class="flex justify-between items-center mb-1">
<div class="flex items-center gap-2">
<div class="icon-box rounded text-yellow-600 border-yellow-100">
<i data-lucide="shield-check" class="w-5 h-5"></i>
</div>
<h3 class="card-title">方法学评审智能体</h3>
</div>
<span class="num-watermark">08</span>
</div>
<p class="desc-text">
从研究问题、研究方案和临床意义方面,对研究进行临床研究方法学的全面评价。
</p>
</div>
</div>
</div>
<!-- 4. 数据处理与统计分析 (新增板块Plan A+B 混合区分) -->
<div class="relative mb-6">
<div class="absolute -left-7 md:-left-9 top-0 h-full flex flex-col items-center">
<!-- 序号 4, 颜色改为 Teal 以示区分 -->
<div class="w-6 h-6 rounded-full bg-teal-600 text-white flex items-center justify-center text-xs font-bold shadow-sm z-10 border border-white">4</div>
<div class="timeline-line"></div>
</div>
<div class="flex items-center gap-2 mb-3">
<h2 class="text-sm font-bold text-slate-800">
数据处理与统计分析
</h2>
<!-- UI标识工具标签 -->
<span class="px-2 py-0.5 rounded text-[10px] font-medium bg-teal-100 text-teal-700 border border-teal-200">工具</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- 09 (工具卡片) -->
<div class="card-box tool-card group cursor-pointer">
<!-- 右上角跳转图标 -->
<i data-lucide="external-link" class="w-3.5 h-3.5 link-icon"></i>
<div class="flex justify-between items-center mb-1">
<div class="flex items-center gap-2">
<div class="icon-box rounded">
<i data-lucide="database" class="w-5 h-5"></i>
</div>
<h3 class="card-title text-slate-800">数据评价与预处理</h3>
</div>
<span class="num-watermark">09</span>
</div>
<p class="desc-text">
对现有数据的质量进行自动评价,并给出数据质量报告,包括缺失值、异常值、定量分析等。
</p>
</div>
<!-- 10 (工具卡片) -->
<div class="card-box tool-card group cursor-pointer">
<i data-lucide="external-link" class="w-3.5 h-3.5 link-icon"></i>
<div class="flex justify-between items-center mb-1">
<div class="flex items-center gap-2">
<div class="icon-box rounded">
<i data-lucide="bar-chart-2" class="w-5 h-5"></i>
</div>
<h3 class="card-title text-slate-800">智能统计分析</h3>
</div>
<span class="num-watermark">10</span>
</div>
<p class="desc-text">
内置3条智能研究路径队列研究、预测模型、RCT研究以及近百种统计分析工具。
</p>
</div>
</div>
</div>
<!-- 5. 写作助手 (原4顺延为5) -->
<div class="relative last-item">
<div class="absolute -left-7 md:-left-9 top-0 h-full flex flex-col items-center">
<div class="w-6 h-6 rounded-full bg-brand text-white flex items-center justify-center text-xs font-bold shadow-sm z-10 border border-white">5</div>
</div>
<h2 class="text-sm font-bold text-slate-800 mb-3 flex items-center">
写作助手
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- 11 (原09) -->
<div class="card-box group cursor-pointer">
<div class="flex justify-between items-center mb-1">
<div class="flex items-center gap-2">
<div class="icon-box rounded text-purple-600 border-purple-100">
<i data-lucide="pen-tool" class="w-5 h-5"></i>
</div>
<h3 class="card-title">论文润色</h3>
</div>
<span class="num-watermark">11</span>
</div>
<p class="desc-text">
结合目标杂志,提供专业化的润色服务,给出合理化的修改建议。
</p>
</div>
<!-- 12 (原10) -->
<div class="card-box group cursor-pointer">
<div class="flex justify-between items-center mb-1">
<div class="flex items-center gap-2">
<div class="icon-box rounded text-purple-600 border-purple-100">
<i data-lucide="languages" class="w-5 h-5"></i>
</div>
<h3 class="card-title">论文翻译</h3>
</div>
<span class="num-watermark">12</span>
</div>
<p class="desc-text">
结合目标杂志,提供专业化的翻译并进行润色,给出合理化的修改建议。
</p>
</div>
</div>
</div>
</div>
</main>
<footer class="mt-8 py-4 border-t border-slate-100 text-center text-[10px] text-slate-400">
<p>&copy; 2025 临床研究平台 - 医学研究专属大模型</p>
</footer>
<script>
lucide.createIcons();
</script>
</body>
</html>

View File

@@ -0,0 +1,540 @@
# AIA 模块 V2.1 开发计划
> **版本**V2.1
> **创建日期**2026-01-11
> **计划周期**:约 8-11 天
> **策略**:重写后端 + 复用数据库 + 新开发前端
> **关联PRD**`01-需求分析/AIA模块PRD.md`
> **关联原型**`01-需求分析/AI智能问答V2.html`
---
## 📋 开发策略概述
### 核心决策
| 组件 | 策略 | 理由 |
|------|------|------|
| **后端** | 🔴 **重写** | 旧版不符合云原生规范console.log、未使用平台能力 |
| **数据库** | ✅ **复用** | aia_schema 已完善,字段满足需求 |
| **前端** | 🔴 **新开发** | 复用 shared/components/Chat 通用组件全新UI |
### 技术规范
| 规范 | 要求 |
|------|------|
| **日志** | 使用 `logger` from `@/common/logging`(禁止 console.log |
| **存储** | 使用 `storage` from `@/common/storage` |
| **缓存** | 使用 `cache` from `@/common/cache` |
| **队列** | 使用 `jobQueue` from `@/common/jobs` |
| **Prompt** | 使用 `promptService.get()` from `@/common/prompts` |
| **代码位置** | `backend/src/modules/aia/` |
| **API路由** | `/api/v2/aia/*` |
---
## 📊 数据库状态(✅ 已完成)
### 表结构3个表
```
aia_schema.projects ✅ 保留
aia_schema.conversations ✅ 保留
aia_schema.messages ✅ 保留 + 新增字段
```
### 新增字段2026-01-11 已迁移)
| 表 | 字段 | 类型 | 说明 |
|----|------|------|------|
| messages | `thinking_content` | TEXT | 深度思考内容 `<think>...</think>` |
| messages | `attachments` | JSONB | 附件数组上限5个单个≤20MB文本≤30K tokens |
### 已删除表
| 表 | 原因 |
|----|------|
| `general_conversations` | 功能重叠,使用 conversations.project_id = NULL |
| `general_messages` | 功能重叠 |
---
## 🗓️ 开发阶段
### Phase 1: 后端重写3-4天
#### Day 1: 模块骨架 + 核心服务
**目标**:搭建模块结构,迁移核心对话服务
**任务清单**
- [ ] **1.1 创建模块目录结构**
```
backend/src/modules/aia/
├── controllers/
│ ├── conversationController.ts
│ ├── agentController.ts
│ └── projectController.ts
├── services/
│ ├── conversationService.ts
│ ├── agentService.ts
│ └── projectService.ts
├── routes/
│ └── index.ts
├── types/
│ └── index.ts
└── index.ts
```
- [ ] **1.2 重写 conversationService**
- 从 legacy 复制核心逻辑
- 替换 `console.log` → `logger`
- 使用 `prisma.message`(已在 aia_schema
- 添加 `thinkingContent` 处理逻辑
- 保持流式输出能力
- [ ] **1.3 重写 agentService**
- 改用 `promptService.get()` 获取 Prompt
- 缓存智能体配置(使用 `cache`
- [ ] **1.4 注册 v2 路由**
- 注册到 `/api/v2/aia/*`
- 保持 legacy 路由兼容(逐步迁移)
**验收标准**
- [ ] 基础对话功能可用
- [ ] 流式输出正常
- [ ] 日志输出到 logger
---
#### Day 2: 深度思考 + 附件上传
**目标**:实现 V2.1 新增功能
**任务清单**
- [ ] **2.1 深度思考模式**
- 检测 LLM 输出中的 `<think>...</think>` 标签
- 提取并存储到 `messages.thinking_content`
- 从 `content` 中移除 think 标签
- 流式输出时分离 thinking 和 content
- [ ] **2.2 附件上传服务**
- 使用 `storage.upload()` 上传到 OSS
- 调用 Python 微服务提取文本
- Token 计数(使用 tiktoken
- 截断处理(超过 30K tokens
- 存储附件信息到 `messages.attachments`
- [ ] **2.3 附件注入 LLM 上下文**
- 组装附件文本到 User Prompt
- 控制总 Token 长度
**技术规格**
```typescript
// 附件处理配置
const ATTACHMENT_CONFIG = {
maxCount: 5, // 每条消息最多5个附件
maxSizePerFile: 20 * 1024 * 1024, // 单个文件20MB
maxTokens: 30000, // 提取文本最多30K tokens
supportedTypes: ['pdf', 'docx', 'txt', 'xlsx'],
};
```
**验收标准**
- [ ] 深度思考内容正确分离存储
- [ ] 附件上传成功
- [ ] 附件文本正确注入 LLM
---
#### Day 3: 意图路由 + 知识库集成
**目标**:实现全局意图路由,完善知识库引用
**任务清单**
- [ ] **3.1 意图路由服务**
- 新建 `intentRouterService.ts`
- 调用 Router Agent 识别意图
- 返回目标 Agent ID + 预填 Prompt
- 添加 500ms 防抖(前端实现)
- [ ] **3.2 完善知识库集成**
- 复用 PKB 模块的 RAG 检索
- 智能引用系统([来源N]
- 引用清单格式化
- [ ] **3.3 API 端点完善**
```
POST /api/v2/aia/intent/route # 意图路由
POST /api/v2/aia/conversations # 创建对话
GET /api/v2/aia/conversations # 对话列表
GET /api/v2/aia/conversations/:id # 对话详情
POST /api/v2/aia/conversations/:id/messages/stream # 发送消息(流式)
POST /api/v2/aia/conversations/:id/attachments # 上传附件
GET /api/v2/aia/agents # 智能体列表
GET /api/v2/aia/agents/:id # 智能体详情
```
**验收标准**
- [ ] 意图路由正确识别并跳转
- [ ] 知识库引用正确显示
- [ ] 所有 API 端点可用
---
#### Day 4: 测试 + 文档
**目标**:完成后端测试和文档
**任务清单**
- [ ] **4.1 单元测试**
- conversationService 测试
- 深度思考解析测试
- 附件处理测试
- [ ] **4.2 集成测试**
- 完整对话流程
- 附件上传流程
- 知识库检索流程
- [ ] **4.3 API 文档**
- 更新 REST Client 测试文件
- 编写 API 使用示例
**验收标准**
- [ ] 测试覆盖核心功能
- [ ] API 文档完整
---
### Phase 2: 前端开发5-7天
#### Day 5-6: 智能体大厅Dashboard
**目标**:实现首页智能体大厅
**任务清单**
- [ ] **5.1 创建模块目录结构**
```
frontend-v2/src/modules/aia/
├── pages/
│ ├── Dashboard.tsx # 智能体大厅
│ └── Workspace.tsx # 对话工作台
├── components/
│ ├── AgentPipeline.tsx # 5阶段流水线
│ ├── AgentCard.tsx # 智能体卡片
│ ├── IntentSearch.tsx # 意图搜索框
│ ├── ConversationList.tsx # 历史会话列表
│ ├── ThinkingBlock.tsx # 深度思考折叠块
│ ├── AttachmentUpload.tsx # 附件上传
│ ├── AttachmentCard.tsx # 附件卡片
│ └── SlashCommands.tsx # 快捷指令
├── hooks/
│ ├── useConversation.ts
│ ├── useAgents.ts
│ └── useIntentRouter.ts
├── api/
│ └── index.ts
├── types/
│ └── index.ts
└── index.tsx
```
- [ ] **5.2 智能体流水线AgentPipeline**
- 5阶段布局严格还原 V11 原型)
- 3色视觉体系
- 卡片点击跳转 Workspace
- 工具卡片跳转外部模块DC/ST
- [ ] **5.3 意图搜索框IntentSearch**
- 顶部大搜索框
- 500ms 防抖
- 调用意图路由 API
- 自动跳转目标 Agent
**验收标准**
- [ ] 5阶段流水线正确展示
- [ ] 意图搜索功能可用
- [ ] 与原型图一致
---
#### Day 7-8: 对话工作台Workspace
**目标**:实现沉浸式对话界面
**任务清单**
- [ ] **7.1 工作台布局**
- Gemini 风格(大留白、少分割线)
- 左侧侧边栏(历史会话,可折叠)
- 主对话区Header + 消息列表 + 输入框)
- [ ] **7.2 复用 Chat 通用组件**
```tsx
import { ChatContainer } from '@/shared/components/Chat';
<ChatContainer
providerConfig={{
apiEndpoint: '/api/v2/aia/conversations/:id/messages/stream',
requestFn: sendMessageWithStream,
}}
customMessageRenderer={renderAIAMessage}
/>
```
- [ ] **7.3 深度思考折叠块ThinkingBlock**
- 可折叠灰色引用块
- 生成中展开,完成后自动收起
- 显示"已深度思考 (耗时 Xs)"
- [ ] **7.4 附件上传组件**
- 支持拖拽上传
- 文件类型/大小校验
- 上传进度显示
- 消息气泡下方附件卡片
- [ ] **7.5 历史会话列表**
- 按时间分组今天、昨天、7天前
- 桌面端固定显示
- 移动端抽屉滑出
**验收标准**
- [ ] Gemini 风格 UI
- [ ] 深度思考正确展示
- [ ] 附件上传完整流程
- [ ] 历史会话切换
---
#### Day 9: Markdown 增强 + 快捷指令
**目标**:增强对话体验
**任务清单**
- [ ] **9.1 Markdown 渲染增强**
- KaTeX 公式渲染(医学公式)
- 表格横向滚动
- 代码块语法高亮 + 一键复制
- [ ] **9.2 快捷指令SlashCommands**
- 输入 `/` 弹出菜单
- 支持:/润色, /扩写, /翻译, /导出Word
- 键盘导航
- [ ] **9.3 结果操作栏**
- Hover 显示工具栏
- 复制Markdown 源码)
- 重新生成
- 导出 Word调用 RVW 导出服务)
**验收标准**
- [ ] 公式正确渲染
- [ ] 快捷指令可用
- [ ] 操作栏功能完整
---
#### Day 10: 移动端适配
**目标**:响应式布局适配
**任务清单**
- [ ] **10.1 Dashboard 移动端**
- 隐藏复杂导航
- 卡片单列流式布局
- 时间轴样式调整
- [ ] **10.2 Workspace 移动端**
- 侧边栏改为抽屉
- 输入框键盘适配scrollIntoView
- 发送按钮始终可见
- [ ] **10.3 触控优化**
- 按钮 active 态
- 触控区域优化
**验收标准**
- [ ] md (768px) 断点响应正确
- [ ] 移动端交互流畅
---
#### Day 11: 集成测试 + 优化
**目标**:完成整体测试和优化
**任务清单**
- [ ] **11.1 端到端测试**
- 完整对话流程
- 附件上传流程
- 深度思考流程
- 知识库引用流程
- [ ] **11.2 性能优化**
- TTFB < 1.5s
- 移动端 LCP < 1s
- 意图搜索防抖
- [ ] **11.3 Bug 修复**
- [ ] **11.4 文档更新**
- 更新模块状态文档
- 更新系统当前状态
**验收标准**
- [ ] 所有功能正常
- [ ] 性能达标
- [ ] 文档完整
---
## 📁 文件清单
### 后端新增文件
```
backend/src/modules/aia/
├── controllers/
│ ├── conversationController.ts # ~300行
│ ├── agentController.ts # ~150行
│ └── projectController.ts # ~200行
├── services/
│ ├── conversationService.ts # ~500行核心
│ ├── agentService.ts # ~200行
│ ├── projectService.ts # ~150行
│ ├── intentRouterService.ts # ~100行
│ └── attachmentService.ts # ~200行
├── routes/
│ └── index.ts # ~100行
├── types/
│ └── index.ts # ~100行
└── index.ts # ~20行
预计总计:~2000行
```
### 前端新增文件
```
frontend-v2/src/modules/aia/
├── pages/
│ ├── Dashboard.tsx # ~400行
│ └── Workspace.tsx # ~500行
├── components/
│ ├── AgentPipeline.tsx # ~300行
│ ├── AgentCard.tsx # ~100行
│ ├── IntentSearch.tsx # ~150行
│ ├── ConversationList.tsx # ~200行
│ ├── ThinkingBlock.tsx # ~100行
│ ├── AttachmentUpload.tsx # ~200行
│ ├── AttachmentCard.tsx # ~80行
│ └── SlashCommands.tsx # ~150行
├── hooks/
│ ├── useConversation.ts # ~150行
│ ├── useAgents.ts # ~100行
│ └── useIntentRouter.ts # ~80行
├── api/
│ └── index.ts # ~200行
├── types/
│ └── index.ts # ~100行
└── index.tsx # ~50行
预计总计:~2900行
```
---
## 🔗 依赖关系
### 后端依赖
| 依赖 | 来源 | 说明 |
|------|------|------|
| `logger` | `@/common/logging` | 日志服务 |
| `storage` | `@/common/storage` | OSS存储 |
| `cache` | `@/common/cache` | Redis/PG缓存 |
| `jobQueue` | `@/common/jobs` | 异步任务 |
| `promptService` | `@/common/prompts` | Prompt管理 |
| `LLMFactory` | `@/common/llm` | LLM适配器 |
| `prisma` | `@/config/database` | 数据库 |
| `ExtractionClient` | `@/clients` | 文档提取 |
| `TokenService` | `@/services` | Token计数 |
### 前端依赖
| 依赖 | 来源 | 说明 |
|------|------|------|
| `ChatContainer` | `@/shared/components/Chat` | 通用对话组件 |
| `Ant Design` | `antd` | UI组件库 |
| `Ant Design X` | `@ant-design/x` | AI对话组件 |
| `KaTeX` | `katex` | 公式渲染 |
| `react-markdown` | `react-markdown` | Markdown渲染 |
---
## 📈 风险评估
| 风险 | 概率 | 影响 | 缓解措施 |
|------|------|------|---------|
| 流式输出兼容性 | 中 | 高 | 复用已验证的 SSE 代码 |
| 附件提取超时 | 中 | 中 | 使用 jobQueue 异步处理 |
| 移动端适配问题 | 低 | 中 | 提前规划断点和布局 |
| Prompt管理服务未就绪 | 低 | 中 | 可临时回退到文件读取 |
---
## ✅ 验收标准
### 功能验收
- [ ] 智能体大厅完整展示
- [ ] 意图搜索正确路由
- [ ] 多轮对话正常
- [ ] 流式输出流畅
- [ ] 深度思考正确折叠
- [ ] 附件上传完整
- [ ] 知识库引用正确
- [ ] 快捷指令可用
- [ ] 移动端适配正常
### 性能验收
- [ ] TTFB < 1.5s
- [ ] 移动端 LCP < 1s
- [ ] 意图搜索响应 < 500ms
### 质量验收
- [ ] 无 console.log使用 logger
- [ ] 代码符合云原生规范
- [ ] API 文档完整
- [ ] 单元测试覆盖核心功能
---
## 📝 更新日志
| 日期 | 版本 | 内容 |
|------|------|------|
| 2026-01-11 | V1.0 | 创建开发计划 |
---
**计划制定人**AI Assistant
**审核人**:待定
**批准人**:待定

View File

@@ -0,0 +1,568 @@
# AIA V2.1 后端 API 设计
> **版本**V2.1
> **创建日期**2026-01-11
> **基础路径**`/api/v2/aia`
---
## 📋 API 概览
| 方法 | 路径 | 描述 | 认证 |
|------|------|------|------|
| GET | `/agents` | 获取智能体列表 | ✅ |
| GET | `/agents/:id` | 获取智能体详情 | ✅ |
| POST | `/intent/route` | 意图路由 | ✅ |
| GET | `/conversations` | 获取对话列表 | ✅ |
| POST | `/conversations` | 创建对话 | ✅ |
| GET | `/conversations/:id` | 获取对话详情 | ✅ |
| DELETE | `/conversations/:id` | 删除对话 | ✅ |
| POST | `/conversations/:id/messages/stream` | 发送消息(流式) | ✅ |
| POST | `/conversations/:id/attachments` | 上传附件 | ✅ |
| GET | `/projects` | 获取项目列表 | ✅ |
| GET | `/projects/:id` | 获取项目详情 | ✅ |
---
## 🔐 认证
所有 API 需要在请求头中携带 JWT Token
```
Authorization: Bearer <token>
```
---
## 📖 API 详细定义
### 1. 智能体相关
#### 1.1 获取智能体列表
```http
GET /api/v2/aia/agents
```
**查询参数**
| 参数 | 类型 | 必填 | 描述 |
|------|------|------|------|
| stage | string | 否 | 筛选阶段:`design`, `data`, `analysis`, `write`, `publish` |
**响应**
```json
{
"code": 0,
"data": {
"agents": [
{
"id": "research-design",
"name": "科研设计小助手",
"description": "帮助您完成研究方案设计、文献检索、方法学指导",
"icon": "🔬",
"stage": "design",
"color": "#3B82F6",
"knowledgeBaseId": "kb-001",
"isTool": false
},
{
"id": "dc-tool",
"name": "数据采集工具",
"description": "跳转到数据采集模块",
"icon": "📊",
"stage": "data",
"color": "#8B5CF6",
"targetModule": "/data-collection",
"isTool": true
}
]
}
}
```
#### 1.2 获取智能体详情
```http
GET /api/v2/aia/agents/:id
```
**响应**
```json
{
"code": 0,
"data": {
"id": "research-design",
"name": "科研设计小助手",
"description": "帮助您完成研究方案设计、文献检索、方法学指导",
"icon": "🔬",
"stage": "design",
"color": "#3B82F6",
"knowledgeBaseId": "kb-001",
"systemPrompt": "你是一个专业的医学科研设计专家...",
"welcomeMessage": "您好!我是科研设计小助手,我可以帮您:\n- 设计研究方案\n- 检索相关文献\n- 指导研究方法",
"suggestedQuestions": [
"如何设计一个RCT研究",
"帮我检索近5年糖尿病研究文献",
"什么情况下使用倾向性评分匹配?"
]
}
}
```
---
### 2. 意图路由
#### 2.1 智能意图识别
```http
POST /api/v2/aia/intent/route
```
**请求体**
```json
{
"query": "帮我分析一下这份数据"
}
```
**响应**
```json
{
"code": 0,
"data": {
"agentId": "data-analysis",
"agentName": "统计分析小助手",
"confidence": 0.92,
"prefillPrompt": "请帮我分析这份数据,包括描述性统计和相关性分析"
}
}
```
---
### 3. 对话管理
#### 3.1 获取对话列表
```http
GET /api/v2/aia/conversations
```
**查询参数**
| 参数 | 类型 | 必填 | 描述 |
|------|------|------|------|
| agentId | string | 否 | 按智能体筛选 |
| projectId | string | 否 | 按项目筛选NULL 表示通用对话) |
| page | number | 否 | 页码,默认 1 |
| pageSize | number | 否 | 每页数量,默认 20 |
**响应**
```json
{
"code": 0,
"data": {
"conversations": [
{
"id": "conv-001",
"title": "RCT研究设计咨询",
"agentId": "research-design",
"agentName": "科研设计小助手",
"projectId": null,
"messageCount": 12,
"lastMessage": "好的,我来帮您设计研究方案...",
"createdAt": "2026-01-11T10:00:00Z",
"updatedAt": "2026-01-11T12:30:00Z"
}
],
"pagination": {
"total": 25,
"page": 1,
"pageSize": 20,
"totalPages": 2
}
}
}
```
#### 3.2 创建对话
```http
POST /api/v2/aia/conversations
```
**请求体**
```json
{
"agentId": "research-design",
"projectId": null,
"title": "新对话"
}
```
**响应**
```json
{
"code": 0,
"data": {
"id": "conv-002",
"title": "新对话",
"agentId": "research-design",
"projectId": null,
"createdAt": "2026-01-11T14:00:00Z"
}
}
```
#### 3.3 获取对话详情(含历史消息)
```http
GET /api/v2/aia/conversations/:id
```
**查询参数**
| 参数 | 类型 | 必填 | 描述 |
|------|------|------|------|
| limit | number | 否 | 获取最近N条消息默认 50 |
**响应**
```json
{
"code": 0,
"data": {
"id": "conv-001",
"title": "RCT研究设计咨询",
"agentId": "research-design",
"agentName": "科研设计小助手",
"projectId": null,
"createdAt": "2026-01-11T10:00:00Z",
"updatedAt": "2026-01-11T12:30:00Z",
"messages": [
{
"id": "msg-001",
"role": "user",
"content": "帮我设计一个关于糖尿病的RCT研究",
"attachments": [],
"createdAt": "2026-01-11T10:00:00Z"
},
{
"id": "msg-002",
"role": "assistant",
"content": "好的我来帮您设计一个糖尿病RCT研究方案...",
"thinkingContent": "用户想设计RCT研究需要考虑1研究目的 2入排标准 3样本量 4随机化方法 5盲法 6结局指标...",
"model": "deepseek-v3",
"tokens": 1250,
"createdAt": "2026-01-11T10:00:30Z"
}
]
}
}
```
#### 3.4 删除对话
```http
DELETE /api/v2/aia/conversations/:id
```
**响应**
```json
{
"code": 0,
"data": {
"deleted": true
}
}
```
---
### 4. 消息发送(流式)
#### 4.1 发送消息并获取流式响应
```http
POST /api/v2/aia/conversations/:id/messages/stream
```
**请求头**
```
Content-Type: application/json
Accept: text/event-stream
```
**请求体**
```json
{
"content": "帮我分析这份数据",
"attachmentIds": ["att-001", "att-002"],
"enableDeepThinking": true
}
```
**响应SSE 格式)**
```
event: thinking_start
data: {}
event: thinking_delta
data: {"content": "用户上传了数据文件,需要"}
event: thinking_delta
data: {"content": "进行描述性统计分析..."}
event: thinking_end
data: {"duration": 3200}
event: message_start
data: {"id": "msg-003"}
event: delta
data: {"content": "根据您上传的数据,"}
event: delta
data: {"content": "我来为您进行分析..."}
event: message_end
data: {"id": "msg-003", "tokens": 850, "model": "deepseek-v3"}
event: done
data: {}
```
**SSE 事件类型**
| 事件 | 描述 | 数据格式 |
|------|------|---------|
| `thinking_start` | 开始深度思考 | `{}` |
| `thinking_delta` | 思考内容片段 | `{"content": "..."}` |
| `thinking_end` | 思考结束 | `{"duration": number}` |
| `message_start` | 开始生成回复 | `{"id": "..."}` |
| `delta` | 回复内容片段 | `{"content": "..."}` |
| `message_end` | 回复结束 | `{"id": "...", "tokens": number, "model": "..."}` |
| `error` | 发生错误 | `{"code": "...", "message": "..."}` |
| `done` | 流结束 | `{}` |
---
### 5. 附件上传
#### 5.1 上传附件
```http
POST /api/v2/aia/conversations/:id/attachments
```
**请求头**
```
Content-Type: multipart/form-data
```
**请求体FormData**
| 字段 | 类型 | 必填 | 描述 |
|------|------|------|------|
| file | File | 是 | 文件PDF/Word/TXT/Excel最大20MB |
**响应**
```json
{
"code": 0,
"data": {
"id": "att-001",
"filename": "研究数据.xlsx",
"mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"size": 1024000,
"ossUrl": "https://oss.example.com/attachments/att-001.xlsx",
"textExtracted": true,
"tokenCount": 15000,
"truncated": false,
"createdAt": "2026-01-11T14:00:00Z"
}
}
```
**错误码**
| 错误码 | 描述 |
|--------|------|
| `ATTACHMENT_TOO_LARGE` | 文件超过 20MB |
| `ATTACHMENT_TYPE_NOT_SUPPORTED` | 不支持的文件类型 |
| `ATTACHMENT_LIMIT_EXCEEDED` | 附件数量超过上限5个 |
| `TEXT_EXTRACTION_FAILED` | 文本提取失败 |
---
### 6. 项目管理
#### 6.1 获取项目列表
```http
GET /api/v2/aia/projects
```
**响应**
```json
{
"code": 0,
"data": {
"projects": [
{
"id": "proj-001",
"name": "糖尿病研究项目",
"description": "2型糖尿病患者生活方式干预研究",
"conversationCount": 5,
"createdAt": "2026-01-01T10:00:00Z",
"updatedAt": "2026-01-11T12:00:00Z"
}
]
}
}
```
---
## 🔧 错误处理
### 错误响应格式
```json
{
"code": -1,
"error": {
"code": "CONVERSATION_NOT_FOUND",
"message": "对话不存在"
}
}
```
### 通用错误码
| 错误码 | HTTP 状态码 | 描述 |
|--------|------------|------|
| `UNAUTHORIZED` | 401 | 未授权 |
| `FORBIDDEN` | 403 | 无权限 |
| `NOT_FOUND` | 404 | 资源不存在 |
| `VALIDATION_ERROR` | 400 | 参数验证失败 |
| `INTERNAL_ERROR` | 500 | 服务器内部错误 |
| `LLM_ERROR` | 500 | LLM 调用失败 |
| `RATE_LIMITED` | 429 | 请求过于频繁 |
---
## 📊 数据模型
### Attachment附件
```typescript
interface Attachment {
id: string; // 附件ID
filename: string; // 原始文件名
mimeType: string; // MIME 类型
size: number; // 文件大小(字节)
ossUrl: string; // OSS 存储地址
textContent?: string; // 提取的文本内容(存储时截断)
tokenCount: number; // 文本 Token 数
truncated: boolean; // 是否被截断超过30K tokens
createdAt: string; // 创建时间
}
```
### Message消息
```typescript
interface Message {
id: string;
conversationId: string;
role: 'user' | 'assistant';
content: string;
thinkingContent?: string; // 深度思考内容
attachments?: Attachment[];
model?: string;
tokens?: number;
isPinned: boolean;
createdAt: string;
}
```
---
## 🧪 测试示例REST Client
```http
###
GET {{baseUrl}}/api/v2/aia/agents
Authorization: Bearer {{token}}
###
POST {{baseUrl}}/api/v2/aia/intent/route
Authorization: Bearer {{token}}
Content-Type: application/json
{
"query": ""
}
###
POST {{baseUrl}}/api/v2/aia/conversations
Authorization: Bearer {{token}}
Content-Type: application/json
{
"agentId": "research-design",
"title": ""
}
###
POST {{baseUrl}}/api/v2/aia/conversations/{{conversationId}}/messages/stream
Authorization: Bearer {{token}}
Content-Type: application/json
Accept: text/event-stream
{
"content": "RCT",
"enableDeepThinking": true
}
###
POST {{baseUrl}}/api/v2/aia/conversations/{{conversationId}}/attachments
Authorization: Bearer {{token}}
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="data.xlsx"
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
< ./data.xlsx
------WebKitFormBoundary--
```
---
## 📝 更新日志
| 日期 | 版本 | 内容 |
|------|------|------|
| 2026-01-11 | V1.0 | 创建 API 设计文档 |

View File

@@ -0,0 +1,883 @@
# 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 | 创建前端组件设计文档 |