docs: complete documentation system (250+ files)

- System architecture and design documentation
- Business module docs (ASL/AIA/PKB/RVW/DC/SSA/ST)
- ASL module complete design (quality assurance, tech selection)
- Platform layer and common capabilities docs
- Development standards and API specifications
- Deployment and operations guides
- Project management and milestone tracking
- Architecture implementation reports
- Documentation templates and guides
This commit is contained in:
2025-11-16 15:43:55 +08:00
parent 0fe6821a89
commit e52020409c
173 changed files with 46227 additions and 11964 deletions

View File

@@ -0,0 +1,527 @@
# AIA - AI智能问答模块数据库设计
> **版本:** v1.0
> **更新时间:** 2025-11-12
> **数据库Schema** `aia_schema`
> **状态:** ✅ 已实施并迁移
---
## 📋 目录
1. [模块概述](#模块概述)
2. [Schema信息](#schema信息)
3. [数据库表设计](#数据库表设计)
4. [表关系图](#表关系图)
5. [索引设计](#索引设计)
6. [数据类型说明](#数据类型说明)
7. [变更历史](#变更历史)
---
## 模块概述
### 功能定位
**AIAAI Intelligent Assistant- AI智能问答模块**是平台的核心对话引擎,提供:
1. **项目管理** - 研究项目的创建和管理
2. **智能对话** - 基于LLM的专业领域对话
3. **通用问答** - 不绑定项目的通用AI对话
4. **消息管理** - 对话历史记录和检索
### 核心业务场景
- 用户创建临床研究项目
- 在项目内与AI进行专业对话
- 使用通用对话功能快速咨询
- 查看和管理对话历史
---
## Schema信息
### Schema名称
```sql
aia_schema
```
### 创建语句
```sql
CREATE SCHEMA IF NOT EXISTS aia_schema;
GRANT ALL ON SCHEMA aia_schema TO aiclinical_admin;
```
### 数据迁移
- **迁移时间:** 2025-11-12
- **源Schema** public
- **迁移脚本:** `docs/09-架构实施/migration-scripts/003-migrate-aia.sql`
- **数据完整性:** ✅ 100%迁移成功
---
## 数据库表设计
### 表列表
| 表名 | 用途 | 行数(估计) | 状态 |
|------|------|------------|------|
| `projects` | 研究项目 | 1-100/用户 | ✅ 已部署 |
| `conversations` | 项目对话 | 10-500/项目 | ✅ 已部署 |
| `messages` | 对话消息 | 10-1000/对话 | ✅ 已部署 |
| `general_conversations` | 通用对话 | 10-100/用户 | ✅ 已部署 |
| `general_messages` | 通用对话消息 | 10-1000/对话 | ✅ 已部署 |
**总计:** 5个表
---
### 1. projects - 研究项目表
**用途:** 存储用户创建的临床研究项目
#### 表结构
| 字段名 | 数据类型 | 约束 | 说明 |
|--------|---------|------|------|
| id | TEXT | PRIMARY KEY | 项目唯一标识UUID |
| user_id | TEXT | NOT NULL, FK | 所属用户ID |
| name | TEXT | NOT NULL | 项目名称 |
| background | TEXT | NULL | 研究背景 |
| research_type | TEXT | NULL | 研究类型observational/experimental等 |
| created_at | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 创建时间 |
| updated_at | TIMESTAMPTZ | NOT NULL | 更新时间 |
| deleted_at | TIMESTAMPTZ | NULL | 软删除时间 |
#### Prisma Model
```prisma
model Project {
id String @id @default(uuid())
userId String @map("user_id")
name String
background String?
researchType String? @map("research_type")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
conversations Conversation[]
@@index([userId])
@@index([createdAt])
@@index([deletedAt])
@@map("projects")
@@schema("aia_schema")
}
```
#### 业务规则
1. **软删除机制** - 使用`deleted_at`标记删除,不物理删除
2. **级联删除** - 删除项目时,关联的对话也被软删除
3. **用户隔离** - 通过`user_id`实现数据隔离
---
### 2. conversations - 项目对话表
**用途:** 存储项目内的AI对话会话
#### 表结构
| 字段名 | 数据类型 | 约束 | 说明 |
|--------|---------|------|------|
| id | TEXT | PRIMARY KEY | 对话唯一标识UUID |
| user_id | TEXT | NOT NULL, FK | 所属用户ID |
| project_id | TEXT | NULL, FK | 所属项目ID可选 |
| agent_id | TEXT | NOT NULL | 智能体ID |
| title | TEXT | NOT NULL | 对话标题 |
| model_name | TEXT | NOT NULL, DEFAULT 'deepseek-v3' | 使用的LLM模型 |
| message_count | INTEGER | NOT NULL, DEFAULT 0 | 消息数量 |
| total_tokens | INTEGER | NOT NULL, DEFAULT 0 | 累计token数 |
| metadata | JSONB | NULL | 扩展元数据 |
| created_at | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 创建时间 |
| updated_at | TIMESTAMPTZ | NOT NULL | 更新时间 |
| deleted_at | TIMESTAMPTZ | NULL | 软删除时间 |
#### Prisma Model
```prisma
model Conversation {
id String @id @default(uuid())
userId String @map("user_id")
projectId String? @map("project_id")
agentId String @map("agent_id")
title String
modelName String @default("deepseek-v3") @map("model_name")
messageCount Int @default(0) @map("message_count")
totalTokens Int @default(0) @map("total_tokens")
metadata Json?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
messages Message[]
@@index([userId])
@@index([projectId])
@@index([agentId])
@@index([createdAt])
@@index([deletedAt])
@@map("conversations")
@@schema("aia_schema")
}
```
#### 业务规则
1. **项目关联可选** - `project_id`可为空,支持项目内外对话
2. **计数器字段** - `message_count``total_tokens`用于统计,需要实时更新
3. **模型选择** - 支持多种LLM模型deepseek-v3/gpt-5-pro/claude-4.5等)
4. **扩展元数据** - `metadata`用于存储配置参数、上下文等
---
### 3. messages - 对话消息表
**用途:** 存储对话中的每条消息
#### 表结构
| 字段名 | 数据类型 | 约束 | 说明 |
|--------|---------|------|------|
| id | TEXT | PRIMARY KEY | 消息唯一标识UUID |
| conversation_id | TEXT | NOT NULL, FK | 所属对话ID |
| role | TEXT | NOT NULL | 角色user/assistant/system |
| content | TEXT | NOT NULL | 消息内容 |
| model | TEXT | NULL | 使用的模型assistant消息 |
| metadata | JSONB | NULL | 扩展元数据 |
| tokens | INTEGER | NULL | 消息token数 |
| is_pinned | BOOLEAN | NOT NULL, DEFAULT false | 是否置顶 |
| created_at | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 创建时间 |
#### Prisma Model
```prisma
model Message {
id String @id @default(uuid())
conversationId String @map("conversation_id")
role String
content String @db.Text
model String?
metadata Json?
tokens Int?
isPinned Boolean @default(false) @map("is_pinned")
createdAt DateTime @default(now()) @map("created_at")
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
@@index([conversationId])
@@index([createdAt])
@@index([isPinned])
@@map("messages")
@@schema("aia_schema")
}
```
#### 业务规则
1. **角色类型** - `role`固定为`user``assistant``system`
2. **只读性** - 消息一旦创建不可修改(只有`is_pinned`可修改)
3. **级联删除** - 删除对话时,消息自动删除
4. **置顶功能** - 重要消息可通过`is_pinned`标记
---
### 4. general_conversations - 通用对话表
**用途:** 存储不绑定项目的通用AI对话
#### 表结构
| 字段名 | 数据类型 | 约束 | 说明 |
|--------|---------|------|------|
| id | TEXT | PRIMARY KEY | 对话唯一标识UUID |
| user_id | TEXT | NOT NULL, FK | 所属用户ID |
| title | TEXT | NOT NULL | 对话标题 |
| model_name | TEXT | NOT NULL, DEFAULT 'qwen-long' | 使用的LLM模型 |
| created_at | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 创建时间 |
| updated_at | TIMESTAMPTZ | NOT NULL | 更新时间 |
| deleted_at | TIMESTAMPTZ | NULL | 软删除时间 |
#### Prisma Model
```prisma
model GeneralConversation {
id String @id @default(uuid())
userId String @map("user_id")
title String
modelName String @default("qwen-long") @map("model_name")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
messages GeneralMessage[]
@@index([userId])
@@index([createdAt])
@@index([updatedAt])
@@map("general_conversations")
@@schema("aia_schema")
}
```
#### 业务规则
1. **轻量级对话** - 不需要项目和智能体关联
2. **快速咨询** - 用于用户的临时问题和快速查询
3. **独立管理** - 与项目对话分开管理
---
### 5. general_messages - 通用对话消息表
**用途:** 存储通用对话中的每条消息
#### 表结构
| 字段名 | 数据类型 | 约束 | 说明 |
|--------|---------|------|------|
| id | TEXT | PRIMARY KEY | 消息唯一标识UUID |
| conversation_id | TEXT | NOT NULL, FK | 所属对话ID |
| role | TEXT | NOT NULL | 角色user/assistant/system |
| content | TEXT | NOT NULL | 消息内容 |
| metadata | JSONB | NULL | 扩展元数据 |
| created_at | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 创建时间 |
#### Prisma Model
```prisma
model GeneralMessage {
id String @id @default(uuid())
conversationId String @map("conversation_id")
role String
content String @db.Text
metadata Json?
createdAt DateTime @default(now()) @map("created_at")
conversation GeneralConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
@@index([conversationId])
@@index([createdAt])
@@map("general_messages")
@@schema("aia_schema")
}
```
#### 业务规则
1. **简化设计** - 相比项目消息去掉了token统计和置顶功能
2. **级联删除** - 删除对话时,消息自动删除
---
## 表关系图
```mermaid
erDiagram
PLATFORM_USERS ||--o{ PROJECTS : "owns"
PLATFORM_USERS ||--o{ CONVERSATIONS : "owns"
PLATFORM_USERS ||--o{ GENERAL_CONVERSATIONS : "owns"
PROJECTS ||--o{ CONVERSATIONS : "contains"
CONVERSATIONS ||--o{ MESSAGES : "contains"
GENERAL_CONVERSATIONS ||--o{ GENERAL_MESSAGES : "contains"
PLATFORM_USERS {
text id PK
text email
text password
}
PROJECTS {
text id PK
text user_id FK
text name
text research_type
timestamptz created_at
timestamptz deleted_at
}
CONVERSATIONS {
text id PK
text user_id FK
text project_id FK
text agent_id
text title
text model_name
int message_count
timestamptz created_at
}
MESSAGES {
text id PK
text conversation_id FK
text role
text content
int tokens
boolean is_pinned
timestamptz created_at
}
GENERAL_CONVERSATIONS {
text id PK
text user_id FK
text title
text model_name
timestamptz created_at
}
GENERAL_MESSAGES {
text id PK
text conversation_id FK
text role
text content
timestamptz created_at
}
```
### 跨Schema引用
**外键关系:**
- `projects.user_id``platform_schema.users.id`
- `conversations.user_id``platform_schema.users.id`
- `general_conversations.user_id``platform_schema.users.id`
**说明:** Prisma自动处理跨Schema外键应用代码无需关心Schema前缀
---
## 索引设计
### 主键索引
所有表的`id`字段自动创建B-tree主键索引。
### 外键索引
| 表名 | 索引字段 | 用途 |
|------|---------|------|
| projects | user_id | 查询用户的所有项目 |
| conversations | user_id | 查询用户的所有对话 |
| conversations | project_id | 查询项目内的对话 |
| conversations | agent_id | 按智能体过滤对话 |
| messages | conversation_id | 查询对话的所有消息 |
| general_conversations | user_id | 查询用户的通用对话 |
| general_messages | conversation_id | 查询对话的所有消息 |
### 时间索引
| 表名 | 索引字段 | 用途 |
|------|---------|------|
| projects | created_at | 按时间排序项目 |
| projects | deleted_at | 过滤已删除项目 |
| conversations | created_at | 按时间排序对话 |
| conversations | updated_at | 按更新时间排序 |
| conversations | deleted_at | 过滤已删除对话 |
| messages | created_at | 按时间排序消息 |
| general_conversations | created_at | 按时间排序对话 |
| general_conversations | updated_at | 按更新时间排序 |
| general_messages | created_at | 按时间排序消息 |
### 功能索引
| 表名 | 索引字段 | 用途 |
|------|---------|------|
| messages | is_pinned | 快速查询置顶消息 |
---
## 数据类型说明
### 主键类型TEXT vs UUID
**当前实现:** TEXT
```sql
id TEXT PRIMARY KEY
```
**UUID生成** Prisma `@default(uuid())`
```prisma
id String @id @default(uuid())
```
**说明:**
- 存储格式字符串形式的UUID`"a6ce8b46-bac6-4284-a9ae-031d636086bc"`
- 优点:与现有代码兼容,无需迁移
- 索引性能与原生UUID类型相当
### 时间类型TIMESTAMPTZ
**所有时间字段使用`TIMESTAMPTZ`(带时区的时间戳):**
- 自动存储UTC时间
- 支持时区转换
- Prisma映射为`DateTime`
### JSONB类型
**用途:** 存储扩展元数据和配置
- `conversations.metadata`
- `messages.metadata`
- `general_messages.metadata`
**优点:**
- 灵活的数据结构
- 支持GIN索引按需添加
- 支持JSONB操作符查询
---
## 变更历史
### v1.0 - 2025-11-12 - 初始版本 ✅
**变更内容:**
1.`public` schema迁移到`aia_schema`
2. 5个表全部迁移
- projects
- conversations
- messages
- general_conversations
- general_messages
3. 在Prisma中添加`@@schema("aia_schema")`标签
4. 所有数据100%完整迁移
**迁移脚本:** `docs/09-架构实施/migration-scripts/003-migrate-aia.sql`
**验证状态:** ✅ 已验证,功能正常
---
## 📚 相关文档
- [Schema隔离架构设计](../../../09-架构实施/01-Schema隔离架构设计10个.md)
- [Schema迁移完成报告](../../../09-架构实施/Schema迁移完成报告.md)
- [Prisma配置完成报告](../../../09-架构实施/Prisma配置完成报告.md)
- [快速功能测试报告](../../../09-架构实施/快速功能测试报告.md)
---
**文档维护者:** AI助手
**最后更新:** 2025-11-12
**文档状态:** ✅ 已完成并验证

View File

@@ -0,0 +1,70 @@
# AIA - AI智能问答
> **模块代号:** AIA (AI Intelligent Answer)
> **开发状态:** ✅ 已完成
> **商业价值:** ⭐⭐⭐⭐
> **独立性:** ⭐⭐⭐
---
## 📋 模块概述
AI智能问答模块提供10+个专业AI智能体覆盖科研关键节点。
**核心价值:** 差异化AI能力覆盖科研全流程
---
## 🎯 核心功能
### 已完成功能
1.**12个智能体** - YAML配置框架
2.**多轮对话** - 上下文管理、历史记录
3.**流式输出** - SSE打字机效果
4.**模型切换** - DeepSeek、Qwen3、Qwen-Long
5.**@知识库问答** - RAG增强
### 主要智能体
- 选题评价智能体(四维度评价)
- PICO梳理智能体
- 样本量计算智能体
- 研究方案制定智能体
- 文章润色与翻译智能体
---
## 📂 文档结构
```
AIA-AI智能问答/
├── [AI对接] AIA快速上下文.md # ⏳ 待创建
├── 00-项目概述/
├── 01-设计文档/
└── README.md # ✅ 当前文档
```
---
## 🔗 依赖的通用能力
- **LLM网关** - 模型调用和切换
- **RAG引擎** - @知识库问答
---
**最后更新:** 2025-11-06
**维护人:** 技术架构师

View File

@@ -0,0 +1,540 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI科研助手产品原型 V6</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
body {
font-family: 'Inter', sans-serif;
}
.sidebar-icon {
stroke-width: 1.5;
}
.main-content {
transition: opacity 0.3s ease-in-out;
}
.agent-card, .kb-card {
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.agent-card:hover, .kb-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}
.project-list-item.active {
background-color: #eef2ff;
color: #4f46e5;
font-weight: 600;
}
.nav-item.active {
background-color: #4f46e5;
color: white;
}
.chat-bubble-actions {
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
.chat-bubble:hover .chat-bubble-actions {
opacity: 1;
}
.typing-indicator span {
animation: bounce 1.4s infinite ease-in-out both;
}
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1.0); }
}
/* Custom dropdown */
.dropdown { position: relative; display: inline-block; }
.dropdown-content {
display: none;
position: absolute;
background-color: white;
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.1);
z-index: 50;
border-radius: 0.5rem;
border: 1px solid #e5e7eb;
padding: 0.5rem;
}
.dropdown-top .dropdown-content {
bottom: 100%;
margin-bottom: 0.5rem;
}
.dropdown-content a { color: black; padding: 8px 12px; text-decoration: none; display: block; border-radius: 0.25rem; font-size: 0.875rem; }
.dropdown-content a:hover { background-color: #f3f4f6; }
.dropdown:hover .dropdown-content { display: block; }
/* Custom scrollbar for better aesthetics */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: #f1f5f9; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
</style>
</head>
<body class="bg-gray-100 text-gray-800 h-screen flex overflow-hidden">
<!-- Sidebar -->
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col">
<div class="px-6 py-4 border-b border-gray-200">
<h1 class="text-xl font-bold text-gray-900">AI科研助手</h1>
</div>
<nav class="flex-1 px-4 py-4 space-y-2">
<a href="#" id="nav-agents" class="nav-item flex items-center px-4 py-2 text-gray-700 rounded-lg hover:bg-gray-100">
<i data-lucide="brain-circuit" class="w-5 h-5 mr-3 sidebar-icon"></i>
智能体
</a>
<a href="#" id="nav-history" class="nav-item flex items-center px-4 py-2 text-gray-700 rounded-lg hover:bg-gray-100">
<i data-lucide="history" class="w-5 h-5 mr-3 sidebar-icon"></i>
历史记录
</a>
<a href="#" id="nav-kb" class="nav-item flex items-center px-4 py-2 text-gray-700 rounded-lg hover:bg-gray-100">
<i data-lucide="book-marked" class="w-5 h-5 mr-3 sidebar-icon"></i>
知识库
</a>
<div class="pt-4 mt-4 border-t border-gray-200">
<div class="flex justify-between items-center px-4 mb-2">
<h2 class="text-sm font-semibold text-gray-500 uppercase">我的项目</h2>
<button id="add-project-btn" class="text-gray-400 hover:text-indigo-600" title="创建新项目">
<i data-lucide="plus-circle" class="w-5 h-5"></i>
</button>
</div>
<div id="projectsList" class="space-y-1">
<!-- Projects will be rendered here -->
</div>
</div>
</nav>
<div class="px-4 py-4 border-t border-gray-200 space-y-2">
<a href="#" id="nav-quickstart" class="nav-item flex items-center px-4 py-2 text-gray-700 rounded-lg hover:bg-gray-100">
<i data-lucide="rocket" class="w-5 h-5 mr-3 sidebar-icon"></i>
快速上手
</a>
<a href="#" id="nav-help" class="nav-item flex items-center px-4 py-2 text-gray-700 rounded-lg hover:bg-gray-100">
<i data-lucide="help-circle" class="w-5 h-5 mr-3 sidebar-icon"></i>
帮助中心
</a>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 p-4 md:p-8 overflow-y-auto bg-gray-50">
<div id="agents-view" class="main-content"></div>
<div id="chat-view" class="main-content hidden h-full flex flex-col"></div>
<div id="knowledge-base-view" class="main-content hidden"></div>
<div id="kb-detail-view" class="main-content hidden"></div>
<div id="history-view" class="main-content hidden"></div>
<div id="quickstart-view" class="main-content hidden"></div>
<div id="help-view" class="main-content hidden"></div>
</main>
<!-- Modal for knowledge base selection -->
<div id="kb-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
<div class="bg-white rounded-lg shadow-xl w-full max-w-md p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900 mb-4">选择知识库</h3>
<div id="kb-modal-list" class="space-y-2 max-h-60 overflow-y-auto"></div>
<div class="mt-6 flex justify-end space-x-3">
<button id="kb-modal-cancel" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300">取消</button>
</div>
</div>
</div>
<!-- Modal for Project Edit -->
<div id="project-edit-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
<div class="bg-white rounded-lg shadow-xl w-full max-w-2xl p-6">
<h3 id="project-edit-title" class="text-lg font-medium leading-6 text-gray-900 mb-4">编辑项目信息</h3>
<textarea id="project-edit-textarea" class="w-full h-48 p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"></textarea>
<div class="mt-6 flex justify-end space-x-3">
<button id="project-edit-cancel" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300">取消</button>
<button id="project-edit-save" class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">保存</button>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// --- DATA ---
const agentsData = [ { id: 'agent-1', name: '选题评价', description: '从创新性、价值、可行性等维度评价临床问题。', icon: 'lightbulb' }, { id: 'agent-2', name: '科学问题梳理', description: '将模糊想法提炼成清晰、可验证的科学问题。', icon: 'list-filter' }, { id: 'agent-3', name: 'PICOS构建', description: '结构化地定义临床研究的核心要素。', icon: 'construction' }, { id: 'agent-4', name: '观察指标设计', description: '推荐主要、次要及安全性观察指标。', icon: 'target' }, { id: 'agent-5', name: 'CRF制定', description: '生成符合规范的病例报告表CRF框架。', icon: 'file-text' }, { id: 'agent-6', name: '样本量计算', description: '提供科学的样本量估算。', icon: 'calculator' }, { id: 'agent-7', name: '临床研究方案撰写', description: '生成结构完整的临床研究方案初稿。', icon: 'file-plus-2' }, { id: 'agent-8', name: '论文润色', description: '专业级语言润色,修正语法、拼写和表达。', icon: 'file-signature' }, { id: 'agent-9', name: '论文翻译', description: '精准的中英互译,优化科研术语。', icon: 'languages' }, { id: 'agent-10', name: '方法学评审', description: '对研究方案或论文进行全面的方法学评审。', icon: 'shield-check' }, { id: 'agent-11', name: '期刊方法学评审', description: '模拟期刊审稿人视角,提出可能被质疑的问题。', icon: 'file-search-2' }, { id: 'agent-12', name: '期刊稿约评审', description: '检查论文格式、字数等是否符合期刊要求。', icon: 'clipboard-check' } ];
let projectsData = [ { id: 'proj-1', name: 'XX药物III期临床试验', description: '一项多中心、随机、双盲、安慰剂平行对照的临床试验旨在评估XX药物在治疗阿尔兹海默症中的有效性和安全性。' }, { id: 'proj-2', name: '骨质疏松与肠道菌群关联研究', description: '探索特定肠道菌群与老年女性骨质疏松发生发展的关系。' } ];
let knowledgeBaseData = [ { id: 'kb-1', name: '骨质疏松专题', files: [{name:'文献综述.pdf', status:'ready'}, {name:'实验数据.docx', status:'ready'}]}, { id: 'kb-2', name: '肺癌研究资料', files: [{name:'临床指南_2024.pdf', status:'ready'}, {name:'病例报告汇总.docx', status:'processing'}, {name:'损坏的文件.pdf', status:'failed'}] }, { id: 'kb-3', name: '通用方法学', files: []} ];
let historyData = [ {id: 'hist-1', title: '关于XX药物的创新性评估', agentId: 'agent-1', projectId: 'proj-1', date: '2025-10-08'}, {id: 'hist-2', title: '润色论文摘要部分', agentId: 'agent-8', projectId: null, date: '2025-10-07'}, {id: 'hist-3', title: '骨质疏松研究PICOS构建', agentId: 'agent-3', projectId: 'proj-2', date: '2025-10-06'} ];
let chatData = { 'hist-1': [ {sender: 'user', text: '请帮我评估一下关于XX药物用于治疗阿尔兹海默症的选题。'}, {sender: 'ai', text: '好的,这是一个非常有前景的方向。从创新性来看,目前针对该靶点的研究尚属少数;从临床价值来看,若成功将为患者提供全新的治疗选择...'} ], 'hist-2': [ {sender: 'user', text: '请帮我润色这段摘要...'}, {sender: 'ai', text: '当然,这是修改后的版本...'} ], 'hist-3': [ {sender: 'user', text: '我们来为骨质疏松和肠道菌群的研究构建PICOS。'}, {sender: 'ai', text: '好的,我们开始吧。\n\n**P (Patient):** 绝经后、年龄在60-75岁、被诊断为骨质疏松的女性患者。\n**I (Intervention):** 接受特定益生菌制剂XXX治疗每日一次持续6个月。\n**C (Comparison):** 接受外观、味道、包装完全相同的安慰剂每日一次持续6个月。\n**O (Outcome):** 主要观察指标为治疗6个月后腰椎L1-4骨密度的变化值。次要观察指标包括股骨颈骨密度变化、血清骨转换标志物水平等。\n**S (Study Design):** 随机、双盲、安慰剂对照临床试验。'} ] };
// --- STATE ---
let state = {
activeView: 'agents', // agents, chat, kb, kb-detail, history, quickstart, help
activeProject: null,
activeChatId: null,
activeKbId: null,
activeModel: 'Gemini Pro',
editingProjectId: null,
isAiTyping: false,
};
const availableModels = ['Gemini Pro', 'DeepSeek-V2', 'Qwen2-72B'];
// --- DOM ELEMENTS ---
const dom = {
navAgents: document.getElementById('nav-agents'),
navHistory: document.getElementById('nav-history'),
navKb: document.getElementById('nav-kb'),
navQuickstart: document.getElementById('nav-quickstart'),
navHelp: document.getElementById('nav-help'),
projectsList: document.getElementById('projectsList'),
views: {
'agents': document.getElementById('agents-view'),
'chat': document.getElementById('chat-view'),
'kb': document.getElementById('knowledge-base-view'),
'kb-detail': document.getElementById('kb-detail-view'),
'history': document.getElementById('history-view'),
'quickstart': document.getElementById('quickstart-view'),
'help': document.getElementById('help-view'),
},
kbModal: document.getElementById('kb-modal'),
kbModalList: document.getElementById('kb-modal-list'),
kbModalCancel: document.getElementById('kb-modal-cancel'),
addProjectBtn: document.getElementById('add-project-btn'),
projectEditModal: {
modal: document.getElementById('project-edit-modal'),
title: document.getElementById('project-edit-title'),
textarea: document.getElementById('project-edit-textarea'),
cancel: document.getElementById('project-edit-cancel'),
save: document.getElementById('project-edit-save'),
}
};
// --- RENDER & LOGIC ---
function setState(newState) {
Object.assign(state, newState);
render();
}
function render() {
// Nav state
document.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active'));
if (['agents', 'chat'].includes(state.activeView)) dom.navAgents.classList.add('active');
if (['kb', 'kb-detail'].includes(state.activeView)) dom.navKb.classList.add('active');
if (state.activeView === 'history') dom.navHistory.classList.add('active');
if (state.activeView === 'quickstart') dom.navQuickstart.classList.add('active');
if (state.activeView === 'help') dom.navHelp.classList.add('active');
Object.values(dom.views).forEach(view => view.classList.add('hidden'));
// Render view using a stable switch statement
switch (state.activeView) {
case 'agents':
dom.views.agents.classList.remove('hidden');
renderAgentsView();
break;
case 'chat':
dom.views.chat.classList.remove('hidden');
renderChatView();
break;
case 'kb':
dom.views.kb.classList.remove('hidden');
renderKnowledgeBaseListView();
break;
case 'kb-detail':
dom.views['kb-detail'].classList.remove('hidden');
renderKbDetailView();
break;
case 'history':
dom.views.history.classList.remove('hidden');
renderHistoryView();
break;
case 'quickstart':
dom.views.quickstart.classList.remove('hidden');
renderQuickstartView();
break;
case 'help':
dom.views.help.classList.remove('hidden');
renderHelpView();
break;
}
renderProjectsList();
lucide.createIcons();
}
window.renderProjectsList = function() {
dom.projectsList.innerHTML = `
<div onclick="handleProjectClick(null)" class="project-list-item px-4 py-2 text-sm text-gray-600 rounded-md cursor-pointer hover:bg-gray-100 ${!state.activeProject ? 'active' : ''}">
<i data-lucide="globe" class="inline-block w-4 h-4 mr-2"></i> 全局快速问答
</div>
${projectsData.map(p => `
<div onclick="handleProjectClick('${p.id}')" class="project-list-item px-4 py-2 text-sm text-gray-600 rounded-md cursor-pointer hover:bg-gray-100 ${state.activeProject === p.id ? 'active' : ''}">
<i data-lucide="folder" class="inline-block w-4 h-4 mr-2"></i> ${p.name}
</div>
`).join('')}`;
};
window.renderAgentsView = function() {
const view = dom.views.agents;
const project = state.activeProject ? projectsData.find(p => p.id === state.activeProject) : null;
let breadcrumbHtml = project
? `<span class="cursor-pointer hover:text-indigo-600" onclick="handleProjectClick(null)">全局</span> > <span>${project.name}</span>`
: `<span>全局</span>`;
view.innerHTML = `
<header class="mb-8">
<div class="text-sm text-gray-500 mb-2">${breadcrumbHtml}</div>
<div class="flex items-center space-x-3">
<h1 class="text-3xl font-bold text-gray-900">${project ? project.name : '智能体'}</h1>
${project ? `<button onclick="handleEditProject('${project.id}')" class="text-gray-400 hover:text-indigo-600 p-1 rounded-full hover:bg-gray-200" title="编辑项目信息"><i data-lucide="pencil" class="w-5 h-5"></i></button>` : ''}
</div>
<p class="text-gray-500 mt-2 whitespace-pre-wrap">${project ? project.description : '你好!我可以为你做什么?选择一个智能体或项目开始吧。'}</p>
</header>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
${agentsData.map(agent => `
<div onclick="handleAgentClick('${agent.id}')" class="agent-card bg-white p-5 rounded-xl border border-gray-200 cursor-pointer flex items-start space-x-4">
<div class="bg-indigo-100 text-indigo-600 p-3 rounded-lg"><i data-lucide="${agent.icon}" class="w-6 h-6"></i></div>
<div>
<h3 class="font-semibold text-gray-900">${agent.name}</h3>
<p class="text-sm text-gray-500 mt-1">${agent.description}</p>
</div>
</div>
`).join('')}
</div>`;
};
window.renderChatView = function() {
const view = dom.views.chat;
const historyItem = historyData.find(h => h.id === state.activeChatId);
if (!historyItem) { view.innerHTML = "错误:找不到对话记录。"; return; }
const agent = agentsData.find(a => a.id === historyItem.agentId);
const project = state.activeProject ? projectsData.find(p => p.id === state.activeProject) : null;
const messages = chatData[state.activeChatId] || [];
let breadcrumbHtml = `<span class="cursor-pointer hover:text-indigo-600" onclick="handleProjectClick(null)">全局</span>`;
if (project) {
breadcrumbHtml += ` > <span class="cursor-pointer hover:text-indigo-600" onclick="handleProjectHomeClick('${project.id}')">${project.name}</span>`;
}
breadcrumbHtml += ` > <span>${agent.name}</span>`;
view.innerHTML = `
<header class="p-4 border-b border-gray-200 bg-white flex-shrink-0">
<div class="text-sm text-gray-500 mb-2">${breadcrumbHtml}</div>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="bg-indigo-100 text-indigo-600 p-2 rounded-lg"><i data-lucide="${agent.icon}" class="w-5 h-5"></i></div>
<div>
<h2 class="text-xl font-bold text-gray-900">${agent.name}</h2>
<p class="text-sm text-gray-500">${agent.description}</p>
</div>
</div>
</div>
</header>
<div id="chatMessages" class="flex-1 overflow-y-auto p-6 space-y-6">
${messages.map((msg, index) => renderChatMessage(msg, index)).join('')}
${state.isAiTyping ? renderTypingIndicator() : ''}
</div>
<div class="p-4 bg-gray-50 border-t border-gray-200 flex-shrink-0">
<div class="flex items-center bg-white border border-gray-300 rounded-lg p-2 focus-within:ring-2 focus-within:ring-indigo-500">
<textarea id="message-input" rows="1" class="flex-1 bg-transparent border-none focus:ring-0 resize-none text-sm" placeholder="输入您的问题..."></textarea>
<button onclick="document.getElementById('file-upload').click()" class="p-2 text-gray-500 hover:text-indigo-600 rounded-full hover:bg-gray-100" title="上传附件">
<i data-lucide="paperclip" class="w-5 h-5"></i>
</button>
<input type="file" id="file-upload" class="hidden"/>
<button id="kb-button" class="p-2 text-gray-500 hover:text-indigo-600 rounded-full hover:bg-gray-100" title="引用知识库"><i data-lucide="at-sign" class="w-5 h-5"></i></button>
<button id="send-button" class="ml-2 px-4 py-2 bg-indigo-600 text-white text-sm font-semibold rounded-lg hover:bg-indigo-700 disabled:bg-indigo-300">发送</button>
</div>
<div class="mt-2 flex items-center justify-between">
<div class="dropdown dropdown-top">
<button class="flex items-center space-x-2 px-3 py-1.5 border rounded-lg text-sm text-gray-600 hover:bg-gray-100">
<i data-lucide="cpu" class="w-4 h-4"></i>
<span>模型: ${state.activeModel}</span>
<i data-lucide="chevron-up" class="w-4 h-4"></i>
</button>
<div class="dropdown-content">
${availableModels.map(m => `<a href="#" onclick="handleModelChange('${m}')">${m === state.activeModel ? '<span class="font-bold text-indigo-600">'+m+'</span>' : m}</a>`).join('')}
</div>
</div>
<span class="text-xs text-gray-400">Shift+Enter 换行</span>
</div>
</div>`;
// Event listeners
const messageInput = document.getElementById('message-input');
messageInput.addEventListener('input', () => { messageInput.style.height = 'auto'; messageInput.style.height = `${messageInput.scrollHeight}px`; });
document.getElementById('send-button').onclick = () => handleSendMessage();
messageInput.onkeydown = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSendMessage(); } };
document.getElementById('kb-button').onclick = () => dom.kbModal.classList.remove('hidden');
document.getElementById('file-upload').onchange = (e) => alert(`已选择文件: ${e.target.files[0].name}`);
document.getElementById('chatMessages').scrollTop = document.getElementById('chatMessages').scrollHeight;
};
window.renderChatMessage = function(msg, index) {
const isUser = msg.sender === 'user';
return `
<div class="chat-bubble flex items-start gap-3 ${isUser ? 'justify-end' : ''}">
${!isUser ? `<div class="w-8 h-8 rounded-full bg-indigo-500 text-white flex items-center justify-center flex-shrink-0"><i data-lucide="brain" class="w-5 h-5"></i></div>` : ''}
<div class="flex flex-col ${isUser ? 'items-end' : 'items-start'}">
<div class="max-w-xl p-4 rounded-xl ${isUser ? 'bg-indigo-600 text-white' : 'bg-white border'}">
<p class="text-sm leading-relaxed whitespace-pre-wrap">${msg.text}</p>
</div>
<div class="chat-bubble-actions mt-1.5 flex items-center gap-2 px-2 h-5">
<button onclick="alert('内容已复制')" title="复制" class="text-gray-400 hover:text-gray-600"><i data-lucide="copy" class="w-3.5 h-3.5"></i></button>
${!isUser ? `
<button onclick="alert('正在重新生成...')" title="重新生成" class="text-gray-400 hover:text-gray-600"><i data-lucide="refresh-cw" class="w-3.5 h-3.5"></i></button>
${state.activeProject ? `<button onclick="handlePinMessage('${state.activeChatId}', ${index})" title="固定到项目背景" class="text-gray-400 hover:text-gray-600"><i data-lucide="pin" class="w-3.5 h-3.5"></i></button>` : ''}
` : ''}
</div>
</div>
${isUser ? `<div class="w-8 h-8 rounded-full bg-gray-200 text-gray-600 flex items-center justify-center flex-shrink-0"><i data-lucide="user" class="w-5 h-5"></i></div>` : ''}
</div>`;
};
window.renderTypingIndicator = function() { return `<div class="flex items-start gap-3"><div class="w-8 h-8 rounded-full bg-indigo-500 text-white flex items-center justify-center flex-shrink-0"><i data-lucide="brain" class="w-5 h-5"></i></div><div class="max-w-lg p-4 rounded-xl bg-white border"><div class="typing-indicator flex items-center space-x-1.5"><span class="w-2 h-2 bg-gray-400 rounded-full"></span><span class="w-2 h-2 bg-gray-400 rounded-full"></span><span class="w-2 h-2 bg-gray-400 rounded-full"></span></div></div></div>`; }
window.renderKnowledgeBaseListView = function() {
const v = dom.views['kb'];
v.innerHTML = `<header class="mb-8 flex justify-between items-center"><div><h1 class="text-3xl font-bold text-gray-900">个人知识库</h1><p class="text-gray-500 mt-2">在这里管理您的私人研究资料AI可随时调用。</p></div><button class="bg-indigo-600 text-white px-4 py-2 rounded-lg font-semibold flex items-center space-x-2 hover:bg-indigo-700"><i data-lucide="plus" class="w-5 h-5"></i><span>创建新知识库</span></button></header><div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">${knowledgeBaseData.map(kb => `<div onclick="handleKbClick('${kb.id}')" class="kb-card bg-white p-5 rounded-xl border border-gray-200 cursor-pointer flex flex-col justify-between h-32"><div><div class="flex items-center space-x-3"><i data-lucide="folder" class="w-6 h-6 text-indigo-500"></i><h3 class="font-semibold text-gray-900">${kb.name}</h3></div></div><p class="text-sm text-gray-500 mt-2">${kb.files.length}个文档</p></div>`).join('')}</div>`;
}
window.renderKbDetailView = function() {
const v = dom.views['kb-detail'];
const kb = knowledgeBaseData.find(k => k.id === state.activeKbId);
if(!kb) { v.innerHTML = '错误: 找不到知识库。'; return; }
const s={'ready':{bg:'bg-green-100',text:'text-green-800',icon:'check-circle-2'},'processing':{bg:'bg-yellow-100',text:'text-yellow-800',icon:'loader'},'failed':{bg:'bg-red-100',text:'text-red-800',icon:'alert-circle'}};
v.innerHTML = `<header class="mb-8"><div class="text-sm text-gray-500 mb-2 cursor-pointer hover:text-indigo-600 flex items-center" onclick="handleNavClick('kb')"><i data-lucide="arrow-left" class="w-4 h-4 mr-1"></i>返回知识库列表</div><div class="flex justify-between items-center mt-2"><div><h1 class="text-3xl font-bold text-gray-900">${kb.name}</h1><p class="text-gray-500 mt-2">管理"${kb.name}"知识库中的所有文档。</p></div><button class="bg-indigo-600 text-white px-4 py-2 rounded-lg font-semibold flex items-center space-x-2 hover:bg-indigo-700"><i data-lucide="upload-cloud" class="w-5 h-5"></i><span>上传文件</span></button></div></header><div class="bg-white border border-gray-200 rounded-lg"><ul class="divide-y divide-gray-200">${kb.files.length > 0 ? kb.files.map(f => `<li class="p-4 flex items-center justify-between"><div class="flex items-center space-x-3"><i data-lucide="file-text" class="w-5 h-5 text-gray-400"></i><p class="font-medium text-gray-800">${f.name}</p></div><div class="flex items-center space-x-4"><span class="px-2 py-1 text-xs font-medium rounded-full ${s[f.status].bg} ${s[f.status].text}"><i data-lucide="${s[f.status].icon}" class="inline-block w-3 h-3 mr-1 ${f.status === 'processing' ? 'animate-spin' : ''}"></i>${{ready:'已就绪',processing:'处理中',failed:'失败'}[f.status]}</span><button class="text-gray-400 hover:text-red-600" title="删除"><i data-lucide="trash-2" class="w-4 h-4"></i></button></div></li>`).join('') : `<li class="p-8 text-center text-gray-500">这个知识库还没有文件。</li>`}</ul></div>`;
}
window.renderHistoryView = function() {
const v = dom.views.history;
v.innerHTML = `<header class="mb-8"><h1 class="text-3xl font-bold text-gray-900">历史记录</h1><p class="text-gray-500 mt-2">查看您所有的对话记录。</p></header><div class="mb-6 flex space-x-4"><div class="relative flex-1"><i data-lucide="search" class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400"></i><input type="text" placeholder="搜索对话内容..." class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"></div><select class="border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"><option>所有项目</option><option>全局快速问答</option>${projectsData.map(p => `<option>${p.name}</option>`).join('')}</select><select class="border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"><option>所有智能体</option>${agentsData.map(a => `<option>${a.name}</option>`).join('')}</select></div><div class="bg-white border border-gray-200 rounded-lg"><ul class="divide-y divide-gray-200">${historyData.map(h => { const a = agentsData.find(ag => ag.id === h.agentId), p = projectsData.find(pr => pr.id === h.projectId); return `<li onclick="handleHistoryClick('${h.id}')" class="p-4 hover:bg-gray-50 cursor-pointer"><div class="flex justify-between items-center mb-1"><p class="font-medium text-gray-800">${h.title}</p><p class="text-xs text-gray-400">${h.date}</p></div><div class="flex items-center space-x-2 text-sm text-gray-500"><span class="px-2 py-0.5 rounded-full bg-gray-100 flex items-center space-x-1.5"><i data-lucide="${a.icon}" class="w-3.5 h-3.5"></i><span>${a.name}</span></span><span class="px-2 py-0.5 rounded-full ${p ? 'bg-indigo-100 text-indigo-800' : 'bg-green-100 text-green-800'} flex items-center space-x-1.5"><i data-lucide="${p ? 'folder' : 'globe'}" class="w-3.5 h-3.5"></i><span>${p ? p.name : '全局'}</span></span></div></li>`; }).join('')}</ul></div>`;
}
window.renderQuickstartView = function() { dom.views.quickstart.innerHTML = `<h1 class="text-3xl font-bold">快速上手</h1><p class="mt-2 text-gray-600">这里是产品引导和核心功能介绍...</p>`; }
window.renderHelpView = function() { dom.views.help.innerHTML = `<h1 class="text-3xl font-bold">帮助中心</h1><p class="mt-2 text-gray-600">这里是FAQ和用户手册...</p>`; }
// --- EVENT HANDLERS ---
window.handleProjectClick = (projectId) => setState({ activeProject: projectId, activeView: 'agents', activeChatId: null });
window.handleProjectHomeClick = (projectId) => setState({ activeProject: projectId, activeView: 'agents', activeChatId: null });
window.handleAgentClick = (agentId) => {
const newHistoryId = `hist-${Date.now()}`;
const agentName = agentsData.find(a => a.id === agentId).name;
historyData.unshift({ id: newHistoryId, title: `与"${agentName}"的新对话`, agentId: agentId, projectId: state.activeProject, date: new Date().toISOString().split('T')[0] });
chatData[newHistoryId] = [{sender: 'ai', text: `你好,我是${agentName},使用 ${state.activeModel} 模型为您服务。有什么可以帮助你?`}];
setState({ activeView: 'chat', activeChatId: newHistoryId });
};
window.handleNavClick = (view) => setState({ activeView: view, activeKbId: null, activeChatId: null });
window.handleKbClick = (kbId) => setState({ activeView: 'kb-detail', activeKbId: kbId });
window.handleHistoryClick = (historyId) => {
const historyItem = historyData.find(h => h.id === historyId);
setState({ activeView: 'chat', activeProject: historyItem.projectId, activeChatId: historyId });
};
window.handleSendMessage = function() {
const input = document.getElementById('message-input');
const text = input.value.trim();
if (!text || !state.activeChatId) return;
chatData[state.activeChatId].push({ sender: 'user', text });
input.value = ''; input.style.height = 'auto';
setState({ isAiTyping: true });
setTimeout(() => {
chatData[state.activeChatId].push({ sender: 'ai', text: `[${state.activeModel} 模型回复] 这是一个关于"${text}"的模拟回复。` });
setState({ isAiTyping: false });
}, 1500);
}
window.handleAddProject = function() {
const projectName = prompt("请输入新项目的名称:", `新研究项目 ${projectsData.length + 1}`);
if(projectName && projectName.trim()) {
const newProject = { id: `proj-${Date.now()}`, name: projectName.trim(), description: '新项目的简要描述。' };
projectsData.push(newProject);
setState({ activeProject: newProject.id, activeView: 'agents' });
}
}
window.handleEditProject = function(projectId) {
const project = projectsData.find(p => p.id === projectId);
if(!project) return;
state.editingProjectId = projectId;
const modal = dom.projectEditModal;
modal.title.textContent = `编辑项目: ${project.name}`;
modal.textarea.value = project.description;
modal.modal.classList.remove('hidden');
}
window.handleSaveProject = function() {
const project = projectsData.find(p => p.id === state.editingProjectId);
if(project) {
project.description = dom.projectEditModal.textarea.value;
}
dom.projectEditModal.modal.classList.add('hidden');
state.editingProjectId = null;
render();
}
window.handleCancelEditProject = function() {
dom.projectEditModal.modal.classList.add('hidden');
state.editingProjectId = null;
}
window.handlePinMessage = function(chatId, messageIndex) {
const project = projectsData.find(p => p.id === state.activeProject);
if (!project) { alert("错误:必须在项目中才能固定信息!"); return; }
const messageText = chatData[chatId][messageIndex].text;
const confirmation = confirm(`您确定要将以下内容追加到项目背景中吗?\n\n"${messageText.substring(0, 100)}..."`);
if(confirmation) {
project.description += `\n\n--- 来自对话的补充 ---\n${messageText}`;
alert("已成功追加到项目背景!");
}
}
window.handleModelChange = function(modelName) {
setState({ activeModel: modelName });
}
// --- MODAL LOGIC ---
function setupModal() {
dom.kbModalList.innerHTML = knowledgeBaseData.map(kb => `<div onclick="handleKbSelect('${kb.name}')" class="p-2 rounded-md hover:bg-gray-100 cursor-pointer">${kb.name}</div>`).join('');
dom.kbModalCancel.onclick = () => dom.kbModal.classList.add('hidden');
dom.projectEditModal.save.onclick = handleSaveProject;
dom.projectEditModal.cancel.onclick = handleCancelEditProject;
}
window.handleKbSelect = (kbName) => {
const input = document.getElementById('message-input');
input.value += ` @${kbName} `;
dom.kbModal.classList.add('hidden');
input.focus();
};
// --- INITIALIZATION ---
dom.navAgents.onclick = (e) => { e.preventDefault(); handleNavClick('agents'); };
dom.navHistory.onclick = (e) => { e.preventDefault(); handleNavClick('history'); };
dom.navKb.onclick = (e) => { e.preventDefault(); handleNavClick('kb'); };
dom.navQuickstart.onclick = (e) => { e.preventDefault(); handleNavClick('quickstart'); };
dom.navHelp.onclick = (e) => { e.preventDefault(); handleNavClick('help'); };
dom.addProjectBtn.onclick = handleAddProject;
// Make handlers globally accessible for inline onclicks
window.handleProjectClick = handleProjectClick;
window.handleProjectHomeClick = handleProjectHomeClick;
window.handleAgentClick = handleAgentClick;
window.handleNavClick = handleNavClick;
window.handleKbClick = handleKbClick;
window.handleHistoryClick = handleHistoryClick;
window.handleSendMessage = handleSendMessage;
window.handleAddProject = handleAddProject;
window.handleEditProject = handleEditProject;
window.handlePinMessage = handlePinMessage;
window.handleModelChange = handleModelChange;
window.handleKbSelect = handleKbSelect;
setupModal();
render();
});
</script>
</body>
</html>