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:
527
docs/03-业务模块/AIA-AI智能问答/02-技术设计/01-数据库设计.md
Normal file
527
docs/03-业务模块/AIA-AI智能问答/02-技术设计/01-数据库设计.md
Normal file
@@ -0,0 +1,527 @@
|
||||
# AIA - AI智能问答模块:数据库设计
|
||||
|
||||
> **版本:** v1.0
|
||||
> **更新时间:** 2025-11-12
|
||||
> **数据库Schema:** `aia_schema`
|
||||
> **状态:** ✅ 已实施并迁移
|
||||
|
||||
---
|
||||
|
||||
## 📋 目录
|
||||
|
||||
1. [模块概述](#模块概述)
|
||||
2. [Schema信息](#schema信息)
|
||||
3. [数据库表设计](#数据库表设计)
|
||||
4. [表关系图](#表关系图)
|
||||
5. [索引设计](#索引设计)
|
||||
6. [数据类型说明](#数据类型说明)
|
||||
7. [变更历史](#变更历史)
|
||||
|
||||
---
|
||||
|
||||
## 模块概述
|
||||
|
||||
### 功能定位
|
||||
|
||||
**AIA(AI 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
|
||||
**文档状态:** ✅ 已完成并验证
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
70
docs/03-业务模块/AIA-AI智能问答/README.md
Normal file
70
docs/03-业务模块/AIA-AI智能问答/README.md
Normal 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
|
||||
**维护人:** 技术架构师
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
540
docs/03-业务模块/AIA-AI智能问答/用户端原型图.html
Normal file
540
docs/03-业务模块/AIA-AI智能问答/用户端原型图.html
Normal 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>
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user