samlax12 commited on
Commit
ad74240
·
verified ·
1 Parent(s): 914b14c

Upload 139 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +2 -0
  2. DEVELOPMENT_CHECKLIST.md +539 -0
  3. Dockerfile +23 -0
  4. README-local.md +176 -0
  5. _archive/templates/code_execution.html +718 -0
  6. _archive/templates/index.html +0 -0
  7. _archive/templates/login.html +504 -0
  8. _archive/templates/student.html +1651 -0
  9. _archive/templates/student_portal.html +1004 -0
  10. _archive/templates/token_verification.html +521 -0
  11. app.py +987 -0
  12. config.py +23 -0
  13. frontend/.gitignore +24 -0
  14. frontend/README.md +16 -0
  15. frontend/eslint.config.js +29 -0
  16. frontend/index.html +13 -0
  17. frontend/package-lock.json +0 -0
  18. frontend/package.json +53 -0
  19. frontend/postcss.config.js +7 -0
  20. frontend/public/vite.svg +1 -0
  21. frontend/src/App.jsx +99 -0
  22. frontend/src/assets/react.svg +1 -0
  23. frontend/src/components/WorkflowEditor.jsx +449 -0
  24. frontend/src/components/common/ProtectedRoute.jsx +28 -0
  25. frontend/src/components/plugins/CodeExecutor.jsx +251 -0
  26. frontend/src/components/plugins/MindmapViewer.jsx +135 -0
  27. frontend/src/components/plugins/Visualization3D.jsx +133 -0
  28. frontend/src/hooks/useTypewriter.js +108 -0
  29. frontend/src/index.css +189 -0
  30. frontend/src/main.jsx +10 -0
  31. frontend/src/pages/LoginPage.jsx +497 -0
  32. frontend/src/pages/RegisterPage.jsx +176 -0
  33. frontend/src/pages/student/AgentChat.css +159 -0
  34. frontend/src/pages/student/AgentChat.jsx +854 -0
  35. frontend/src/pages/student/StudentDashboard.jsx +14 -0
  36. frontend/src/pages/student/StudentPortal.jsx +389 -0
  37. frontend/src/pages/teacher/AgentList.jsx +660 -0
  38. frontend/src/pages/teacher/CreateAgent.jsx +696 -0
  39. frontend/src/pages/teacher/Dashboard.jsx +413 -0
  40. frontend/src/pages/teacher/KnowledgeBase.jsx +473 -0
  41. frontend/src/pages/teacher/StudentManagement.jsx +564 -0
  42. frontend/src/pages/teacher/TeacherDashboard.jsx +242 -0
  43. frontend/src/services/agentService.js +49 -0
  44. frontend/src/services/api.js +69 -0
  45. frontend/src/services/authService.js +60 -0
  46. frontend/src/services/knowledgeService.js +47 -0
  47. frontend/src/store/slices/agentSlice.js +59 -0
  48. frontend/src/store/slices/authSlice.js +73 -0
  49. frontend/src/store/slices/chatSlice.js +58 -0
  50. frontend/src/store/slices/uiSlice.js +57 -0
.gitattributes CHANGED
@@ -35,3 +35,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  uploads/496449c167344601ba87c24743701b99.pdf filter=lfs diff=lfs merge=lfs -text
37
  uploads/eb55dde866f64be4a1fdbc81d3d1df0e.pdf filter=lfs diff=lfs merge=lfs -text
 
 
 
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  uploads/496449c167344601ba87c24743701b99.pdf filter=lfs diff=lfs merge=lfs -text
37
  uploads/eb55dde866f64be4a1fdbc81d3d1df0e.pdf filter=lfs diff=lfs merge=lfs -text
38
+ uploads/1a9b23e3e2d34305b0acdbd3693d276d.pdf filter=lfs diff=lfs merge=lfs -text
39
+ uploads/1c64cc79a5694889ba38c29bd630746d.pdf filter=lfs diff=lfs merge=lfs -text
DEVELOPMENT_CHECKLIST.md ADDED
@@ -0,0 +1,539 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TeachAgent 新前端迁移开发清单
2
+
3
+ > 本文档记录从旧版 Flask 模板前端迁移到新版 React 前端的完整开发任务。
4
+ > 更新日期:2026-04-13
5
+
6
+ ---
7
+
8
+ ## 一、项目架构概览
9
+
10
+ | 层级 | 技术栈 | 端口 |
11
+ |------|--------|------|
12
+ | 新前端 | React 19 + Vite + Ant Design + TailwindCSS + Redux | 3000 |
13
+ | 旧前端 | Flask Jinja2 + Bootstrap 5 + 原生JS | 7860(与后端同源) |
14
+ | 后端 API | Flask + Blueprint | 7860 |
15
+ | 数据库 | Elasticsearch 9.3.3 (向量+文档) + SQLite (用户) | 9200 |
16
+ | AI 服务 | SiliconFlow (Embedding/Rerank) + DeepSeek (Chat) | 外部API |
17
+
18
+ ---
19
+
20
+ ## 二、迁移状态总览
21
+
22
+ ### 已完成模块
23
+
24
+ | 模块 | 新前端文件 | 状态 | 备注 |
25
+ |------|-----------|------|------|
26
+ | 登录 | `LoginPage.jsx` | ✅ 完成 | 邮箱/手机/密码三种方式 |
27
+ | 注册 | `RegisterPage.jsx` | ✅ 完成 | 用户名+密码 |
28
+ | 路由保护 | `ProtectedRoute.jsx` | ✅ 完成 | JWT + 角色校验 |
29
+ | 教师 Dashboard | `teacher/Dashboard.jsx` | ✅ 完成 | 统计卡片、快速入门 |
30
+ | Agent 列表 | `teacher/AgentList.jsx` | ✅ 完成 | CRUD、分发链接、详情查看 |
31
+ | Agent 创建 | `teacher/CreateAgent.jsx` | ✅ 完成 | 4步向导、AI工作流生成 |
32
+ | 工作流编辑器 | `components/common/WorkflowEditor.jsx` | ✅ 完成 | React Flow 可视化 |
33
+ | 知识库管理 | `teacher/KnowledgeBase.jsx` | ⚠️ 基本完成 | 缺批量上传、拖拽 |
34
+ | 学生管理 | `teacher/StudentManagement.jsx` | ✅ 完成 | CRUD、密码重置、进度 |
35
+ | 学生门户 | `student/StudentPortal.jsx` | ✅ 完成 | Agent列表、Token验证、活动记录 |
36
+ | Agent 对话 | `student/AgentChat.jsx` | ⚠️ 部分完成 | 流式对话可用,插件系统缺失 |
37
+ | 打字机 Hook | `hooks/useTypewriter.js` | ✅ 已实现 | 未接入使用 |
38
+
39
+ ### 未实现模块
40
+
41
+ | 模块 | 旧版位置 | 优先级 |
42
+ |------|----------|--------|
43
+ | 代码执行插件(对话内嵌) | `student.html` 侧面板 | P0 |
44
+ | 3D 可视化插件(对话内嵌) | `student.html` 侧面板 | P1 |
45
+ | 思维导图插件(对话内嵌) | `student.html` 侧面板 | P1 |
46
+ | 独立代码执行页 | `code_execution.html` | P1 |
47
+ | Token 直接访问入口(登录页) | `login.html` | P2 |
48
+
49
+ ---
50
+
51
+ ## 三、关键 BUG 与问题
52
+
53
+ ### 3.1 流式输出 SSE 实现问题(P0)
54
+
55
+ **当前实现**: `AgentChat.jsx` 使用 `fetch` + `ReadableStream reader` 手动解析 SSE
56
+
57
+ **后端发送格式**:
58
+ ```
59
+ data: {"type": "start", "plugins": [...], "references": [...]}
60
+ data: {"type": "content", "content": "文本片段"}
61
+ data: {"type": "metadata", "content": {...}}
62
+ data: {"type": "done"}
63
+ ```
64
+
65
+ **问题清单**:
66
+
67
+ | # | 问题 | 严重度 | 说明 |
68
+ |---|------|--------|------|
69
+ | 1 | `metadata` 事件未处理 | 中 | 前端只处理 start/content/done,metadata 被忽略 |
70
+ | 2 | 无 `error` 事件类型 | 高 | 后端出错时直接断流,前端会一直卡在"思考中" |
71
+ | 3 | `[DONE]` 哨兵检测无效 | 低 | 前端检查 `[DONE]` 但后端从不发送此标记 |
72
+ | 4 | 流结束时 buffer 未清空 | 中 | 如果流在 `\n\n` 结尾,最后一个事件可能丢失 |
73
+ | 5 | 断流时无 JSON 恢复 | 中 | 连接中断时 partial JSON 直接丢失 |
74
+ | 6 | 无超时/重连机制 | 中 | 网络波动时前端无法恢复 |
75
+
76
+ **建议重构方向**:
77
+ - 后端增加 `type: "error"` 事件
78
+ - 前端增加 metadata 事件处理
79
+ - 添加流超时检测(如 30s 无数据则提示)
80
+ - 清理无用的 `[DONE]` 检查
81
+
82
+ ### 3.2 API 路径不一致(P0)
83
+
84
+ | 前端调用 | 后端实际路径 | 问题 |
85
+ |----------|-------------|------|
86
+ | `axios.post('/api/verify_token')` (AgentChat.jsx:84) | `/api/verify_token` | axios baseURL 是 `/api`,实际请求变成 `/api/api/verify_token`(双重前缀)|
87
+ | `getProgress(taskId)` → `/knowledge/progress/${taskId}` | `/api/progress/${taskId}`(在 app.py 主路由) | 路径不在 knowledge blueprint 下 |
88
+ | `getDistributions(agentId)` → `/agent/${agentId}/distributions` | 后端未实现此端点 | 404 |
89
+ | `authService.refreshToken()` → `/auth/refresh` | 后端未实现 | 404 |
90
+ | `authService.getCurrentUser()` → `/auth/me` | 后端未实现 | 404 |
91
+ | `authService.changePassword()` → `/auth/change_password` | 后端未实现 | 404 |
92
+
93
+ ### 3.3 其他前端问题
94
+
95
+ | # | 问题 | 位置 | 说明 |
96
+ |---|------|------|------|
97
+ | 1 | Agent 编辑路由缺失 | `CreateAgent.jsx` | 编辑模式通过 localStorage 传参,无独立路由 |
98
+ | 2 | 知识库进度轮询路径错误 | `knowledgeService.js` | 应直接调 `/api/progress/` 而非通过 knowledge 前缀 |
99
+ | 3 | 学生 Dashboard 是空壳 | `StudentDashboard.jsx` | 只是路由容器,无独立内容 |
100
+
101
+ ---
102
+
103
+ ## 四、完整 API 端点对照表
104
+
105
+ ### 4.1 认证模块 `/api/auth/*`
106
+
107
+ | 端点 | 方法 | 后端 | 前端调用 | 状态 |
108
+ |------|------|------|---------|------|
109
+ | `/api/auth/login` | POST | ✅ 旧版密码登录 | ❌ 未使用 | 可废弃 |
110
+ | `/api/auth/login-password` | POST | ✅ | ✅ | 正常 |
111
+ | `/api/auth/login-email` | POST | ✅ | ✅ | 正常 |
112
+ | `/api/auth/login-phone` | POST | ✅ | ✅ | 正常 |
113
+ | `/api/auth/send-email-code` | POST | ✅ | ✅ | 正常(邮件服务不可用) |
114
+ | `/api/auth/send-phone-code` | POST | ✅ | ✅ | 正常(开发模式模拟) |
115
+ | `/api/auth/register` | POST | ✅ | ✅ | 正常 |
116
+ | `/api/auth/verify` | GET | ✅ | ❌ 未使用 | - |
117
+ | `/api/auth/logout` | POST | ✅ | ✅ | 正常 |
118
+ | `/api/auth/refresh` | POST | ❌ 未实现 | ✅ 有调用 | **需实现** |
119
+ | `/api/auth/me` | GET | ❌ 未实现 | ✅ 有调用 | **需实现** |
120
+ | `/api/auth/change_password` | POST | ❌ 未实现 | ✅ 有调用 | **需实现** |
121
+ | `/api/auth/check` | GET | ✅ session检查 | ❌ 未使用 | 旧版遗留 |
122
+
123
+ ### 4.2 知识库模块 `/api/knowledge/*`
124
+
125
+ | 端点 | 方法 | 后端 | 前端调用 | 状态 |
126
+ |------|------|------|---------|------|
127
+ | `/api/knowledge/` | GET | ✅ | ✅ | 正常 |
128
+ | `/api/knowledge/` | POST | ✅ 文件上传创建 | ✅ | 正常 |
129
+ | `/api/knowledge/<id>` | DELETE | ✅ | ✅ | 正常 |
130
+ | `/api/knowledge/<id>/documents` | GET | ✅ | ✅ | 正常 |
131
+ | `/api/knowledge/<id>/documents` | POST | ✅ | ✅ | 正常 |
132
+ | `/api/knowledge/<id>/documents/<file>` | DELETE | ✅ | ✅ | 正常 |
133
+ | `/api/progress/<task_id>` | GET | ✅(app.py主路由) | ⚠️ 路径可能错 | **需核实** |
134
+
135
+ ### 4.3 Agent 模块 `/api/agent/*`
136
+
137
+ | 端点 | 方法 | 后端 | 前端调用 | 状态 |
138
+ |------|------|------|---------|------|
139
+ | `/api/agent/create` | POST | ✅ | ✅ | 正常 |
140
+ | `/api/agent/list` | GET | ✅ | ✅ | 正常 |
141
+ | `/api/agent/<id>` | GET | ✅ | ✅ | 正常 |
142
+ | `/api/agent/<id>` | PUT | ✅ | ✅ | 正常 |
143
+ | `/api/agent/<id>` | DELETE | ✅ | ✅ | 正常 |
144
+ | `/api/agent/<id>/distribute` | POST | ✅ | ✅ | 正常 |
145
+ | `/api/agent/<id>/distributions` | GET | ❌ 未实现 | ✅ 有调用 | **需实现** |
146
+ | `/api/agent/ai-assist` | POST | ✅ | ✅ | 正常 |
147
+
148
+ ### 4.4 代码执行模块 `/api/code/*`
149
+
150
+ | 端点 | 方法 | 后端 | 前端调用 | 状态 |
151
+ |------|------|------|---------|------|
152
+ | `/api/code/execute` | POST | ✅ | ❌ 未接入 | **需前端实现** |
153
+ | `/api/code/input` | POST | ✅ | ❌ 未接入 | **需前端实现** |
154
+ | `/api/code/stop` | POST | ✅ | ❌ 未接入 | **需前端实现** |
155
+ | `/api/code/generate` | POST | ✅ | ❌ 未接入 | **需前端实现** |
156
+
157
+ ### 4.5 可视化模块 `/api/visualization/*`
158
+
159
+ | 端点 | 方法 | 后端 | 前端调用 | 状态 |
160
+ |------|------|------|---------|------|
161
+ | `/api/visualization/mindmap` | POST | ✅ | ❌ 未接入 | **需前端实现** |
162
+ | `/api/visualization/3d-surface` | POST | ✅ | ❌ 未接入 | **需前端实现** |
163
+
164
+ ### 4.6 学生模块
165
+
166
+ | 端点 | 方法 | 后端 | 前端调用 | 状态 |
167
+ |------|------|------|---------|------|
168
+ | `/api/student/agents` | GET | ✅ | ✅ | 正常 |
169
+ | `/api/student/activities` | GET | ✅ | ✅ | 正常 |
170
+ | `/api/student/chat/<id>` | POST (SSE) | ✅ | ✅ | ⚠️ 有解析问题 |
171
+ | `/api/verify_token` | POST | ✅ (app.py) | ⚠️ 路径双重前缀 | **需修复** |
172
+
173
+ ### 4.7 学生管理模块 `/api/teacher/students/*`
174
+
175
+ | 端点 | 方法 | 后端 | 前端调用 | 状态 |
176
+ |------|------|------|---------|------|
177
+ | `/api/teacher/students` | GET | ✅ | ✅ | 正常 |
178
+ | `/api/teacher/students` | POST | ✅ | ✅ | 正常 |
179
+ | `/api/teacher/students/<id>` | PUT | ✅ | ✅ | 正常 |
180
+ | `/api/teacher/students/<id>` | DELETE | ✅ | ✅ | 正常 |
181
+ | `/api/teacher/students/<id>/reset-password` | POST | ✅ | ✅ | 正常 |
182
+ | `/api/teacher/students/<id>/progress` | GET | ✅ | ✅ | 正常 |
183
+
184
+ ---
185
+
186
+ ## 五、分阶段开发计划
187
+
188
+ ### 第一阶段:基础修复(让现有功能跑通)
189
+
190
+ - [ ] **修复 API 路径双重前缀问题**
191
+ - `AgentChat.jsx` 中 `axios.post('/api/verify_token')` 改为用 api 实例或去掉 `/api` 前缀
192
+ - 排查所有直接使用 `axios` 而非 `api` 实例的调用
193
+
194
+ - [ ] **修复知识库进度轮询路径**
195
+ - `knowledgeService.js` 中 `getProgress()` 路径对齐后端 `/api/progress/<task_id>`
196
+
197
+ - [ ] **修复 SSE 流式输出**
198
+ - 前端增加 `metadata` 事件处理(至少不报错)
199
+ - 后端增加 `type: "error"` 事件,出错时通知前端
200
+ - 移除前端无用的 `[DONE]` 检查
201
+ - 添加流超时检测(30s 无数据提示用户)
202
+ - 修复流结束时 buffer 未清空问题
203
+
204
+ - [ ] **实现缺失的后端端点**
205
+ - `GET /api/agent/<id>/distributions` — 获取 Agent 的分发链接列表
206
+ - `GET /api/auth/me` — 返回当前登录用户信息
207
+ - `POST /api/auth/refresh` — JWT token 刷新(或前端去掉此调用)
208
+ - `POST /api/auth/change_password` — 修改密码(或前端暂时隐藏入口)
209
+
210
+ ### 第二阶段:插件系统迁移(核心差异)
211
+
212
+ - [ ] **AgentChat 插件框架设计**
213
+ - 在对话界面右侧增加可展开的插件面板(Ant Design Drawer 或自定义侧栏)
214
+ - 根据后端 SSE `start` 事件返回的 `plugins` 字段决定显示哪些��件 Tab
215
+ - 插件自动激活:后端检测到关键词后在 `start` 事件中标记 `suggested_plugins`
216
+ - 插件面板状态:折叠/展开/全屏
217
+ - 布局:左侧对话区 + 右侧插件面板,移动端改为底部抽屉
218
+
219
+ - [ ] **代码执行插件**
220
+ - 使用 CodeMirror 6 (`@uiw/react-codemirror` + `@codemirror/lang-python`)
221
+ - 上半区:代码编辑器(Python语法高亮、行号)
222
+ - 下半区:终端输出面板(深色背景、绿色提示符、支持 ANSI 颜色)
223
+ - 交互式输入:检测到 `input()` 时显示输入框
224
+ - 控制按钮:运行 / 停止 / 清除
225
+ - 对接 API:
226
+ - `POST /api/code/execute` → 发送代码,轮询输出
227
+ - `POST /api/code/input` → 发送用户输入
228
+ - `POST /api/code/stop` → 终止执行
229
+ - AI 回复中的代码块可一键复制到编辑器
230
+ - AI 代码生成按钮 → `POST /api/code/generate`
231
+
232
+ - [ ] **3D 可视化插件**(保持旧版 Plotly iframe 方案)
233
+ - 从 AI 回复中提取 Python 代码(包含 `create_3d_plot` 函数)
234
+ - `POST /api/visualization/3d-surface` 发送代码
235
+ - 返回 `html_url` 后在插件面板中用 `<iframe>` 加载
236
+ - 支持全屏查看(Ant Design Modal 全屏模式)
237
+ - 加载中显示 Spin 组件
238
+ - Plotly 原生交互:旋转、缩放、平移、悬浮提示、导出
239
+
240
+ - [ ] **思维导图插件**(markmap 前端渲染,替代旧版 PlantUML)
241
+ - 安装 `markmap-lib` + `markmap-view`
242
+ - 从 AI 回复中提取 Markdown 层级结构
243
+ - 使用 `Transformer.transform()` 解析为树结构
244
+ - 使用 `Markmap.create()` 渲染到 SVG 容器
245
+ - 交互支持:缩放、平移、节点展开/折叠
246
+ - 支持全屏查看和导出为 SVG/PNG
247
+ - 后端修改:让 LLM prompt 输出 Markdown 标题格式而非 PlantUML 语法
248
+
249
+ ### 第三阶段:独立功能页面
250
+
251
+ - [ ] **独立代码执行页 `/code`**
252
+ - 完整的 Python IDE 界面(编辑器 + 终端)
253
+ - 语法高亮、行号、Tab 缩进
254
+ - 运行/停止/清除控制按钮
255
+ - 从旧版 `code_execution.html` 迁移逻辑
256
+
257
+ - [ ] **Token 直接访问入口**
258
+ - 登录页增加"使用 Token 直接访问"选项
259
+ - 输入 Token → 验证 → 跳转到对应 Agent 对话页
260
+
261
+ ### 第四阶段:体验优化
262
+
263
+ - [ ] **打字机效果**
264
+ - 将 `useTypewriter.js` hook 接入 AgentChat 的流式渲染
265
+ - 支持暂停/继续/加速控制
266
+ - 或直接在流式输出时模拟打字效果(当前逐 chunk 渲染已有类似效果,评估是否需要)
267
+
268
+ - [ ] **知识库批量上传**
269
+ - 支持多文件选择和拖拽上传
270
+ - 批量进度追踪
271
+
272
+ - [ ] **Agent 编辑流程优化**
273
+ - 为编辑模式添加独立路由 `/teacher/agent/edit/:id`
274
+ - 替换 localStorage 传参方式为 URL 参数 + API 获取
275
+
276
+ - [ ] **学生 Dashboard 充实内容**
277
+ - 学习统计、最近活动、推荐 Agent 等
278
+
279
+ - [ ] **LaTeX 公式渲染**
280
+ - 旧版使用 KaTeX,新版 AgentChat 的 Markdown 渲染中需集成
281
+ - 支持行内公式 `$...$` 和块级公式 `$$...$$`
282
+
283
+ - [ ] **消息中的引用源展示优化**
284
+ - 旧版有可折叠的引用卡片,显示文件名+摘要
285
+ - 新版已有基础实现,但样式和交互可优化
286
+
287
+ ### 第五阶段:后端优化
288
+
289
+ - [ ] **统一认证机制**
290
+ - 旧版 app.py 中的硬编码用户和 session 装饰器与新版 JWT 并存,需要统一
291
+ - 决策:全部迁移到 JWT,废弃 session 方式
292
+ - 更新 `@login_required`、`@teacher_required`、`@student_required` 装饰器为 JWT 验证
293
+
294
+ - [ ] **密码哈希升级**
295
+ - 当前使用 SHA256 直接哈希,建议升级为 bcrypt 或 argon2
296
+ - 需要兼容迁移方案(登录时自动升级哈希)
297
+
298
+ - [ ] **插件检测逻辑优化**
299
+ - 当前基于消息关键词匹配(如包含"代码"就触发代码插件),过于粗糙
300
+ - 考虑让 LLM 在回复中标记是否需要插件,或使用更精确的意图识别
301
+
302
+ ---
303
+
304
+ ## 六、旧版功能对照清单
305
+
306
+ ### 教师端功能
307
+
308
+ | 功能 | 旧版 | 新版 | 差距 |
309
+ |------|------|------|------|
310
+ | 统计面板 | 知识库数/Agent数/分发数 | ✅ 相同 | - |
311
+ | 知识库创建 | 多文件上传+进度条 | ⚠️ 单文件 | 缺批量 |
312
+ | 知识库文件管理 | 添加/删除文件 | ✅ | - |
313
+ | Agent 创建 | 基本信息+插件+知识库+工作流 | ✅ 4步向导 | - |
314
+ | Agent 工作流 | jsPlumb 可视化 | ✅ React Flow | 升级 |
315
+ | Agent 管理 | 列表+详情+编辑+删除 | ✅ | 编辑入口可优化 |
316
+ | 分发链接 | 创建+复制+过期设置 | ✅ | - |
317
+ | 学生管理 | CRUD+密码重置+进度 | ✅ | - |
318
+
319
+ ### 学生端功能
320
+
321
+ | 功能 | 旧版 | 新版 | 差距 |
322
+ |------|------|------|------|
323
+ | 学生门户 | Agent列表+Token验证+活动记录 | ✅ | - |
324
+ | Agent 对话 | SSE流式+Markdown+代码高亮 | ✅ 基本可用 | SSE有bug |
325
+ | 打字机效果 | 暂停/加速控制 | ❌ Hook已写未接入 | 需接入 |
326
+ | LaTeX 公式 | KaTeX 渲染 | ❌ | 需集成 |
327
+ | 代码执行插件 | 侧面板内嵌 IDE | ❌ | **核心缺失** |
328
+ | 3D 可视化插件 | Plotly 侧面板 | ❌ | **核心缺失** |
329
+ | 思维导图插件 | PlantUML 侧面板 | ❌ | **核心缺失** |
330
+ | 引用源展示 | 可折叠卡片 | ⚠️ 基础实现 | 可优化 |
331
+ | 独立代码页 | 完整Python IDE | ❌ | 需新建页面 |
332
+
333
+ ---
334
+
335
+ ## 七、技术选型
336
+
337
+ ### 7.1 思维导图:markmap(推荐)
338
+
339
+ **废弃旧方案**:旧版使用 PlantUML 语法 → 发送到外部 HuggingFace Space API → 返回 PNG 图片。问题:依赖外部服务、不可交互、渲染慢。
340
+
341
+ **新方案**:使用 markmap 在前端直接从 Markdown 生成交互式 SVG 思维导图。
342
+
343
+ | 对比项 | 旧方案 (PlantUML) | 新方案 (markmap) |
344
+ |--------|-------------------|------------------|
345
+ | 渲染位置 | 后端调外部API | 前端本地渲染 |
346
+ | 输出格式 | 静态 PNG | 交互式 SVG |
347
+ | 交互能力 | 无 | 缩放/平移/展开折叠 |
348
+ | 外部依赖 | HuggingFace Space | 无 |
349
+ | 延迟 | 3-10秒 | 毫秒级 |
350
+ | 输入格式 | PlantUML 专有语法 | Markdown 标题层级 |
351
+
352
+ **安装**:
353
+ ```bash
354
+ npm install markmap-lib markmap-view d3@7
355
+ ```
356
+
357
+ **核心用法**:
358
+ ```jsx
359
+ import { Transformer } from 'markmap-lib';
360
+ import { Markmap } from 'markmap-view';
361
+
362
+ // Markdown → 思维导图数据
363
+ const transformer = new Transformer();
364
+ const { root } = transformer.transform(markdownText);
365
+
366
+ // 渲染到 SVG 元素
367
+ Markmap.create(svgElement, null, root);
368
+ ```
369
+
370
+ **后端改动**:
371
+ - 让 LLM 在回复中直接输出 Markdown 层级结构(而非 PlantUML 语法)
372
+ - 或者保留后端端点,改为返回 Markdown 文本而非图片 URL
373
+ - 前端从 AI 回复中提取 Markdown 标题结构,直接渲染
374
+
375
+ **备选库对比**:
376
+
377
+ | 库 | Stars | 特点 | 适合场景 |
378
+ |-----|-------|------|---------|
379
+ | markmap | 9k+ | Markdown原生、SVG交互、轻量 | ✅ 本项目(AI输出Markdown) |
380
+ | simple-mind-map | 11k+ | 功能最全、主题丰富、插件多 | 需要完整思维导图编辑器 |
381
+ | mind-elixir | 3k | 零依赖、有React组件 | 需要拖拽编辑节点 |
382
+ | React Flow | 24k+ | 项目已在用、高度可定制 | 已用于工作流,可复用 |
383
+
384
+ 选择 markmap 的理由:
385
+ 1. AI 天然输出 Markdown,无需格式转换
386
+ 2. 纯前端渲染,不依赖外部服务
387
+ 3. 交互体验远超静态图片
388
+ 4. 包体积小,集成简单
389
+
390
+ ---
391
+
392
+ ### 7.2 3D 可视化:保持 Plotly iframe 方案
393
+
394
+ 旧版方案已经很成熟,保持不变:
395
+
396
+ ```
397
+ 前端提取代码 → POST /api/visualization/3d-surface → 后端沙箱执行 Python
398
+ → 生成 Plotly HTML(内嵌 plotly.js) → 返回 HTML 文件 URL → 前端 iframe 加载
399
+ ```
400
+
401
+ **交互能力**(Plotly 原生支持):
402
+ - 3D 旋转(鼠标拖拽)
403
+ - 缩放(滚轮)
404
+ - 平移(Shift+拖拽)
405
+ - 悬浮提示(坐标值)
406
+ - 图例切换
407
+ - 导出 PNG
408
+ - 双击重置视角
409
+
410
+ **React 迁移要点**:
411
+ - 不需要安装 `react-plotly.js`(图表在 iframe 中独立运行)
412
+ - 只需在 AgentChat 插件面板中嵌入 `<iframe src={htmlUrl} />`
413
+ - 添加全屏按钮和关闭按钮
414
+
415
+ **后端数据流**:
416
+ ```
417
+ 输入: { code: "def create_3d_plot():\n return {'x':[], 'y':[], 'z':[], 'type':'surface'}" }
418
+ 输出: { success: true, html_url: "/static/3d_plot_1713012345.html" }
419
+ ```
420
+
421
+ ---
422
+
423
+ ### 7.3 代码编辑器:CodeMirror 6(推荐)
424
+
425
+ | 对比项 | Monaco Editor | CodeMirror 6 |
426
+ |--------|--------------|--------------|
427
+ | 包体积 | ~2.5MB (gzip) | ~150KB (gzip) |
428
+ | 加载速度 | 较慢 | 快 |
429
+ | 移动端 | 差 | 好 |
430
+ | Python 支持 | 完整 | 完整 |
431
+ | React 集成 | @monaco-editor/react | @uiw/react-codemirror |
432
+ | 适合场景 | 完整 IDE | 轻量代码片段编辑 |
433
+
434
+ 选择 CodeMirror 的理由:
435
+ 1. 本项目只需要 Python 代码编辑,不需要完整 IDE 功能
436
+ 2. 包体积小 15 倍,对教育平台的加载速度至关重要
437
+ 3. 移动端适配更好(学生可能用手机访问)
438
+
439
+ **安装**:
440
+ ```bash
441
+ npm install @uiw/react-codemirror @codemirror/lang-python
442
+ ```
443
+
444
+ ---
445
+
446
+ ### 7.4 LaTeX 公式:rehype-katex(推荐)
447
+
448
+ 当前 AgentChat 已使用 `react-markdown` + `remark-gfm`,集成 KaTeX 只需加两个插件:
449
+
450
+ ```bash
451
+ npm install remark-math rehype-katex katex
452
+ ```
453
+
454
+ ```jsx
455
+ import remarkMath from 'remark-math';
456
+ import rehypeKatex from 'rehype-katex';
457
+ import 'katex/dist/katex.min.css';
458
+
459
+ <ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]} rehypePlugins={[rehypeKatex]}>
460
+ {content}
461
+ </ReactMarkdown>
462
+ ```
463
+
464
+ ---
465
+
466
+ ### 7.5 技术选型汇总
467
+
468
+ | 组件 | 选型 | npm 包 | 理由 |
469
+ |------|------|--------|------|
470
+ | 思维导图 | **markmap** | `markmap-lib` + `markmap-view` | Markdown原生、前端渲染、交互式 |
471
+ | 3D 可视化 | **Plotly iframe** | 无需前端安装 | 保持旧版成熟方案,后端生成HTML |
472
+ | 代码编辑器 | **CodeMirror 6** | `@uiw/react-codemirror` | 轻量、移动端友好 |
473
+ | LaTeX 公式 | **KaTeX** | `remark-math` + `rehype-katex` | 与现有 react-markdown 无缝集成 |
474
+ | 代码高亮 | **react-syntax-highlighter** | 已安装 | 项目已在用 |
475
+ | 工作流编辑 | **React Flow** | 已安装 | 项目已在用 |
476
+
477
+ ---
478
+
479
+ ## 八、文件结构参考
480
+
481
+ ```
482
+ frontend/src/
483
+ ├── App.jsx # 路由配置
484
+ ├── index.css # 全局样式
485
+ ├── services/
486
+ │ ├── api.js # axios 实例 + 拦截器
487
+ │ ├── authService.js # 认证 API
488
+ │ ├── agentService.js # Agent API
489
+ │ └── knowledgeService.js # 知识库 API
490
+ ├── store/
491
+ │ ├── store.js # Redux store
492
+ │ └── slices/
493
+ │ ├── authSlice.js # 认证状态
494
+ │ ├── agentSlice.js # Agent 状态
495
+ │ ├── chatSlice.js # 对话状态
496
+ │ ├── workflowSlice.js # 工作流状态
497
+ │ └── uiSlice.js # UI 状态
498
+ ├── pages/
499
+ │ ├── LoginPage.jsx # 登录页
500
+ │ ├── RegisterPage.jsx # 注册页
501
+ │ ├── teacher/
502
+ │ │ ├── TeacherDashboard.jsx # 教师端框架
503
+ │ │ ├── Dashboard.jsx # 概览页
504
+ │ │ ├── AgentList.jsx # Agent 列表
505
+ │ │ ├── CreateAgent.jsx # Agent 创建向导
506
+ │ │ ├── KnowledgeBase.jsx # 知识库管理
507
+ │ │ └── StudentManagement.jsx # 学生管理
508
+ │ └── student/
509
+ │ ├── StudentDashboard.jsx # 学生端框架
510
+ │ ├── StudentPortal.jsx # 学生门户
511
+ │ └── AgentChat.jsx # Agent 对话
512
+ ├── components/
513
+ │ └── common/
514
+ │ ├── ProtectedRoute.jsx # 路由保护
515
+ │ └── WorkflowEditor.jsx # 工作流编辑器
516
+ └── hooks/
517
+ └── useTypewriter.js # 打字机效果 hook
518
+
519
+ agent/ # 后端
520
+ ├── app.py # Flask 主应用 + 路由
521
+ ├── config.py # 配置
522
+ ├── auth.db # SQLite 用户库
523
+ ├── student_management.py # 学生管理路由
524
+ ├── modules/
525
+ │ ├── auth/routes.py # 认证路由
526
+ │ ├── knowledge_base/
527
+ │ │ ├── routes.py # 知识库路由
528
+ │ │ ├── retriever.py # ES 检索器
529
+ │ │ ├── reranker.py # 重排序器
530
+ │ │ ├── generator.py # LLM 生成器
531
+ │ │ ├── processor.py # 文档处理器
532
+ │ │ └── vector_store.py # ES 向量存储
533
+ │ ├── agent_builder/routes.py # Agent 构建路由
534
+ │ ├── code_executor/routes.py # 代码执行路由
535
+ │ └── visualization/routes.py # 可视化路由
536
+ ├── agents/ # Agent JSON 配置存储
537
+ ├── uploads/ # 上传文件存储
538
+ └── templates/ # 旧版 HTML 模板(待废弃)
539
+ ```
Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # 安装系统依赖
6
+ RUN apt-get update && apt-get install -y --no-install-recommends \
7
+ build-essential \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # 安装 Python 依赖
11
+ COPY requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # 复制应用代码
15
+ COPY . .
16
+
17
+ # 确保目录存在
18
+ RUN mkdir -p static uploads agents
19
+
20
+ EXPOSE 7860
21
+
22
+ # 使用 gunicorn 运行,支持 SSE 流式输出
23
+ CMD ["gunicorn", "--bind", "0.0.0.0:7860", "--workers", "2", "--threads", "4", "--timeout", "120", "app:app"]
README-local.md ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TeachAgent - 智能教育助手平台
2
+
3
+ AI 驱动的个性化学习平台,支持教师创建 AI Agent、管理知识库,学生通过对话式交互进行学习。
4
+
5
+ ## 功能特性
6
+
7
+ - **教师端**:创建/管理 AI Agent、知识库管理、学生管理、分发链接、工作流编辑
8
+ - **学生端**:与 AI Agent 对话、代码执行、3D 可视化、思维导图
9
+ - **RAG 系统**:基于 Elasticsearch 的向量检索 + 重排序
10
+ - **流式对话**:SSE 实时流式输出
11
+ - **插件系统**:代码执行(CodeMirror)、3D 可视化(Plotly)、思维导图(markmap)
12
+
13
+ ## 项目结构
14
+
15
+ ```
16
+ teachagent/
17
+ ├── ELASTIC/ # Elasticsearch 容器(独立部署)
18
+ │ └── Dockerfile
19
+ ├── teachagent-app/ # 主应用
20
+ │ ├── app.py # Flask 入口
21
+ │ ├── config.py # 配置
22
+ │ ├── requirements.txt # Python 依赖
23
+ │ ├── Dockerfile # 应用容器
24
+ │ ├── .env # 环境变量
25
+ │ ├── modules/ # 后端模块
26
+ │ │ ├── auth/ # 认证(JWT + 密码/邮箱/手机登录)
27
+ │ │ ├── agent_builder/ # Agent 构建
28
+ │ │ ├── code_executor/ # Python 代码执行
29
+ │ │ ├── knowledge_base/ # RAG 知识库(检索/重排/生成)
30
+ │ │ └── visualization/ # 3D 可视化 + 思维导图
31
+ │ ├── frontend/ # React 源码(开发用)
32
+ │ │ ├── src/
33
+ │ │ ├── package.json
34
+ │ │ └── vite.config.js
35
+ │ ├── static/dist/ # React 构建产物(生产环境前端)
36
+ │ ├── agents/ # Agent JSON 配置存储
37
+ │ ├── uploads/ # 用户上传文件
38
+ │ └── _archive/ # 旧版 Flask 模板归档
39
+ └── docker-compose.yml # 两容器编排
40
+ ```
41
+
42
+ ## 部署方式
43
+
44
+ ### 方式一:本地开发
45
+
46
+ 适合开发调试,前后端分离运行。
47
+
48
+ ```bash
49
+ # 1. 启动 Elasticsearch(Docker 或本地安装)
50
+ docker run -d --name es -p 9200:9200 \
51
+ -e discovery.type=single-node \
52
+ -e xpack.security.enabled=false \
53
+ elasticsearch:9.3.3
54
+
55
+ # 2. 启动 Flask 后端
56
+ cd teachagent-app
57
+ pip install -r requirements.txt
58
+ FLASK_ENV=development python app.py
59
+ # 后端运行在 http://localhost:7860
60
+
61
+ # 3. 启动 React 前端(开发模式,带热更新)
62
+ cd frontend
63
+ npm install
64
+ npm run dev
65
+ # 前端运行在 http://localhost:3000,API 请求代理到 7860
66
+ ```
67
+
68
+ **本地开发特点**:
69
+ - 前端 `localhost:3000`,后端 `localhost:7860`,通过 Vite 代理转发 API
70
+ - SSE 流式输出需要直连后端(绕过 Vite 代理)
71
+ - `FLASK_ENV=development` 开启 debug 模式和热重载
72
+ - 修改前端代码即时生效(HMR)
73
+
74
+ ### 方式二:云端容器部署(生产环境)
75
+
76
+ 两个容器,一个端口。
77
+
78
+ ```bash
79
+ # 在项目根目录(包含 docker-compose.yml 的目录)
80
+ docker-compose up -d
81
+
82
+ # 或分别构建
83
+ docker-compose build
84
+ docker-compose up -d
85
+ ```
86
+
87
+ **生产环境特点**:
88
+ - 单端口 `7860`:Flask 同时托管 API 和 React 静态文件
89
+ - 无跨域问题:前端和 API 同源
90
+ - SSE 流式直连:不经过代理,实时输出
91
+ - gunicorn 多 worker 运行,性能更好
92
+ - ES 通过 Docker 网络互通(`http://elasticsearch:9200`)
93
+
94
+ ### 方式三:HuggingFace Spaces 部署
95
+
96
+ 适用于 HuggingFace Spaces 平台。
97
+
98
+ 1. 将 `teachagent-app/` 作为 Space 仓库
99
+ 2. ES 单独部署为另一个 Space(`ELASTIC/`)
100
+ 3. `.env` 中配置 `ELASTICSEARCH_URL=https://your-es-space.hf.space`
101
+
102
+ ## 环境变量说明
103
+
104
+ | 变量 | 说明 | 示例 |
105
+ |------|------|------|
106
+ | `ELASTICSEARCH_URL` | Elasticsearch 地址 | `http://localhost:9200` 或 `https://xxx.hf.space` |
107
+ | `API_KEY` | SiliconFlow API Key(Embedding/Rerank) | `sk-xxx` |
108
+ | `BASE_URL` | SiliconFlow API 地址 | `https://api.siliconflow.cn/v1` |
109
+ | `STREAM_API_KEY` | DeepSeek API Key(对话生成) | `sk-xxx` |
110
+ | `STREAM_BASE_URL` | DeepSeek API 地址 | `https://api.deepseek.com/v1` |
111
+ | `STREAM_MODEL` | 对话模型 | `deepseek-chat` |
112
+ | `SECRET_KEY` | Flask Session 密钥 | 随机字符串 |
113
+ | `JWT_SECRET` | JWT 签名密钥 | 随机字符串 |
114
+ | `FLASK_ENV` | 运行环境 | `development` 或 `production` |
115
+
116
+ ## 跨域与 HTTPS
117
+
118
+ ### 生产环境(推荐)
119
+
120
+ - **无跨域问题**:前端构建产物由 Flask 直接托管,前端和 API 同源(同一个端口 7860)
121
+ - **HTTPS**:由反向代理(Nginx/Cloudflare/HuggingFace Spaces)统一处理 SSL 终止,Flask 本身只需 HTTP
122
+ - **SSE 流式**:同源直连,无需代理,数据实时到达
123
+
124
+ ### 本地开发
125
+
126
+ - **跨域**:Vite 代理 (`localhost:3000` → `localhost:7860`) 处理 API 请求,SSE 直连后端
127
+ - **HTTPS**:本地开发无需 HTTPS,全部 HTTP
128
+ - **注意**:HTML 中不要加 `upgrade-insecure-requests` meta 标签(已移除)
129
+
130
+ ### 带 HTTPS 反向代理部署
131
+
132
+ 如果在自有服务器上通过 Nginx 反向代理:
133
+
134
+ ```nginx
135
+ server {
136
+ listen 443 ssl;
137
+ server_name your-domain.com;
138
+
139
+ ssl_certificate /path/to/cert.pem;
140
+ ssl_certificate_key /path/to/key.pem;
141
+
142
+ location / {
143
+ proxy_pass http://127.0.0.1:7860;
144
+ proxy_http_version 1.1;
145
+ proxy_set_header Host $host;
146
+ proxy_set_header X-Real-IP $remote_addr;
147
+
148
+ # SSE 流式输出必须的配置
149
+ proxy_buffering off;
150
+ proxy_cache off;
151
+ proxy_set_header Connection '';
152
+ chunked_transfer_encoding on;
153
+ }
154
+ }
155
+ ```
156
+
157
+ 关键配置:`proxy_buffering off` 确保 SSE 数据不被 Nginx 缓冲。
158
+
159
+ ## 演示账号
160
+
161
+ | 角色 | 用户名 | 密码 |
162
+ |------|--------|------|
163
+ | 教师 | teacher | 123456 |
164
+ | 学生 | student1 | 123456 |
165
+
166
+ 也可通过注册页面创建新账号。
167
+
168
+ ## 技术栈
169
+
170
+ | 层 | 技术 |
171
+ |----|------|
172
+ | 前端 | React 19 + Vite + Ant Design 5 + TailwindCSS + Redux Toolkit |
173
+ | 后端 | Flask + gunicorn |
174
+ | 数据库 | Elasticsearch 9.x(向量检索) + SQLite(用户管理) |
175
+ | AI | DeepSeek(对话) + SiliconFlow(Embedding/Rerank) |
176
+ | 部署 | Docker + docker-compose |
_archive/templates/code_execution.html ADDED
@@ -0,0 +1,718 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AI代码助手 - Python执行环境</title>
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/vs2015.min.css">
10
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
11
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/languages/python.min.js"></script>
12
+ <style>
13
+ /* Base styles */
14
+ :root {
15
+ --primary-color: #4361ee;
16
+ --secondary-color: #3f37c9;
17
+ --accent-color: #4cc9f0;
18
+ --success-color: #4caf50;
19
+ --warning-color: #ff9800;
20
+ --danger-color: #f44336;
21
+ --light-color: #f8f9fa;
22
+ --dark-color: #212529;
23
+ --border-color: #dee2e6;
24
+ --border-radius: 0.375rem;
25
+ }
26
+
27
+ body {
28
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
29
+ margin: 0;
30
+ padding: 0;
31
+ height: 100vh;
32
+ background-color: #f5f7fa;
33
+ color: #333;
34
+ display: flex;
35
+ flex-direction: column;
36
+ }
37
+
38
+ /* Layout structure */
39
+ .workspace {
40
+ display: grid;
41
+ grid-template-columns: 1fr 1fr;
42
+ gap: 16px;
43
+ flex: 1;
44
+ padding: 16px;
45
+ }
46
+
47
+ @media (max-width: 992px) {
48
+ .workspace {
49
+ grid-template-columns: 1fr;
50
+ }
51
+ }
52
+
53
+ .section {
54
+ background: #fff;
55
+ border-radius: var(--border-radius);
56
+ overflow: hidden;
57
+ display: flex;
58
+ flex-direction: column;
59
+ border: 1px solid var(--border-color);
60
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
61
+ }
62
+
63
+ .section-header {
64
+ background: #f8f9fa;
65
+ padding: 12px 16px;
66
+ border-bottom: 1px solid var(--border-color);
67
+ display: flex;
68
+ justify-content: space-between;
69
+ align-items: center;
70
+ }
71
+
72
+ .section-title {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 8px;
76
+ font-size: 1rem;
77
+ font-weight: 500;
78
+ }
79
+
80
+ /* Code editor styles */
81
+ .editor-content {
82
+ flex: 1;
83
+ position: relative;
84
+ overflow: hidden;
85
+ }
86
+
87
+ .code-area {
88
+ position: absolute;
89
+ left: 40px;
90
+ right: 0;
91
+ top: 0;
92
+ bottom: 0;
93
+ padding: 12px 16px;
94
+ color: #333;
95
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
96
+ font-size: 14px;
97
+ line-height: 1.6;
98
+ overflow: auto;
99
+ }
100
+
101
+ .code-area pre {
102
+ margin: 0;
103
+ padding: 0;
104
+ background: none;
105
+ border: none;
106
+ }
107
+
108
+ .code-area code {
109
+ display: block;
110
+ padding: 0;
111
+ tab-size: 4;
112
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
113
+ outline: none;
114
+ position: relative;
115
+ min-height: 100%;
116
+ white-space: pre !important;
117
+ word-wrap: normal !important;
118
+ }
119
+
120
+ .line-numbers {
121
+ position: absolute;
122
+ left: 0;
123
+ top: 0;
124
+ bottom: 0;
125
+ width: 40px;
126
+ padding: 12px 0;
127
+ background: #f5f7fa;
128
+ border-right: 1px solid #e9ecef;
129
+ text-align: center;
130
+ color: #6c757d;
131
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
132
+ font-size: 14px;
133
+ line-height: 1.6;
134
+ user-select: none;
135
+ }
136
+
137
+ /* Terminal styles */
138
+ .output-content {
139
+ flex: 1;
140
+ background: #f8f9fa;
141
+ position: relative;
142
+ overflow: hidden;
143
+ display: flex;
144
+ flex-direction: column;
145
+ }
146
+
147
+ .terminal-window {
148
+ flex: 1;
149
+ overflow-y: auto;
150
+ background: #212529;
151
+ color: #f8f9fa;
152
+ }
153
+
154
+ #output {
155
+ color: #f8f9fa;
156
+ margin: 0;
157
+ padding: 12px 16px;
158
+ background: transparent;
159
+ border: none;
160
+ white-space: pre-wrap;
161
+ word-wrap: break-word;
162
+ line-height: 1.6;
163
+ font-size: 14px;
164
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
165
+ }
166
+
167
+ .console-input-container {
168
+ display: flex;
169
+ align-items: center;
170
+ background: #343a40;
171
+ border-top: 1px solid #495057;
172
+ padding: 8px 12px;
173
+ }
174
+
175
+ .console-prompt {
176
+ color: #4caf50;
177
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
178
+ margin-right: 8px;
179
+ font-size: 14px;
180
+ user-select: none;
181
+ }
182
+
183
+ .console-input {
184
+ flex: 1;
185
+ background: transparent;
186
+ border: none;
187
+ color: #f8f9fa;
188
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
189
+ font-size: 14px;
190
+ line-height: 1.5;
191
+ padding: 4px 0;
192
+ }
193
+
194
+ .console-input:focus {
195
+ outline: none;
196
+ }
197
+
198
+ /* Chat section */
199
+ .chat-section {
200
+ display: flex;
201
+ flex-direction: column;
202
+ height: 100%;
203
+ }
204
+
205
+ .chat-messages {
206
+ flex: 1;
207
+ overflow-y: auto;
208
+ padding: 16px;
209
+ }
210
+
211
+ .message {
212
+ margin-bottom: 16px;
213
+ padding: 12px;
214
+ border-radius: var(--border-radius);
215
+ max-width: 85%;
216
+ position: relative;
217
+ }
218
+
219
+ .message.user {
220
+ background-color: #e3f2fd;
221
+ color: #0d47a1;
222
+ align-self: flex-end;
223
+ margin-left: auto;
224
+ }
225
+
226
+ .message.bot {
227
+ background-color: #f5f5f5;
228
+ color: #333;
229
+ align-self: flex-start;
230
+ border-left: 3px solid var(--primary-color);
231
+ }
232
+
233
+ .chat-input-container {
234
+ padding: 16px;
235
+ border-top: 1px solid var(--border-color);
236
+ background-color: #f9f9f9;
237
+ }
238
+
239
+ .input-row {
240
+ display: flex;
241
+ gap: 8px;
242
+ }
243
+
244
+ .chat-input {
245
+ flex: 1;
246
+ padding: 12px;
247
+ border: 1px solid var(--border-color);
248
+ border-radius: var(--border-radius);
249
+ resize: none;
250
+ font-size: 14px;
251
+ height: 100px;
252
+ }
253
+
254
+ .chat-input:focus {
255
+ outline: none;
256
+ border-color: var(--primary-color);
257
+ box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.1);
258
+ }
259
+
260
+ /* Button styles */
261
+ .btn {
262
+ padding: 8px 16px;
263
+ border: none;
264
+ border-radius: var(--border-radius);
265
+ cursor: pointer;
266
+ font-weight: 500;
267
+ font-size: 14px;
268
+ display: flex;
269
+ align-items: center;
270
+ gap: 8px;
271
+ transition: all 0.2s ease;
272
+ }
273
+
274
+ .btn-primary {
275
+ background-color: var(--primary-color);
276
+ color: white;
277
+ }
278
+
279
+ .btn-primary:hover {
280
+ background-color: var(--secondary-color);
281
+ }
282
+
283
+ .btn-secondary {
284
+ background-color: #6c757d;
285
+ color: white;
286
+ }
287
+
288
+ .btn-secondary:hover {
289
+ background-color: #5a6268;
290
+ }
291
+
292
+ .btn-danger {
293
+ background-color: var(--danger-color);
294
+ color: white;
295
+ }
296
+
297
+ .btn-danger:hover {
298
+ background-color: #d32f2f;
299
+ }
300
+
301
+ /* Utilities */
302
+ .loading {
303
+ display: none;
304
+ align-items: center;
305
+ gap: 8px;
306
+ color: #6c757d;
307
+ font-size: 14px;
308
+ }
309
+
310
+ .loading.active {
311
+ display: flex;
312
+ }
313
+
314
+ @keyframes spin {
315
+ to { transform: rotate(360deg); }
316
+ }
317
+
318
+ .loading i {
319
+ animation: spin 1s linear infinite;
320
+ }
321
+
322
+ /* Custom scrollbar */
323
+ ::-webkit-scrollbar {
324
+ width: 8px;
325
+ height: 8px;
326
+ }
327
+
328
+ ::-webkit-scrollbar-track {
329
+ background: #f1f1f1;
330
+ }
331
+
332
+ ::-webkit-scrollbar-thumb {
333
+ background: #c1c1c1;
334
+ border-radius: 4px;
335
+ }
336
+
337
+ ::-webkit-scrollbar-thumb:hover {
338
+ background: #a8a8a8;
339
+ }
340
+
341
+ /* Terminal text styles */
342
+ .term-input {
343
+ color: #4caf50;
344
+ }
345
+
346
+ .term-output {
347
+ color: #f8f9fa;
348
+ }
349
+
350
+ .term-error {
351
+ color: #f44336;
352
+ }
353
+
354
+ .term-warning {
355
+ color: #ff9800;
356
+ }
357
+
358
+ .term-system {
359
+ color: #2196f3;
360
+ }
361
+
362
+ /* Header */
363
+ .header {
364
+ background-color: #fff;
365
+ border-bottom: 1px solid var(--border-color);
366
+ padding: 1rem;
367
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
368
+ }
369
+
370
+ .header-content {
371
+ max-width: 1200px;
372
+ margin: 0 auto;
373
+ display: flex;
374
+ justify-content: space-between;
375
+ align-items: center;
376
+ }
377
+
378
+ .header h1 {
379
+ margin: 0;
380
+ font-size: 1.5rem;
381
+ color: var(--primary-color);
382
+ }
383
+ </style>
384
+ </head>
385
+ <body>
386
+ <header class="header">
387
+ <div class="header-content">
388
+ <h1>AI代码助手</h1>
389
+ <div>
390
+ <span class="badge bg-primary">Python编程环境</span>
391
+ </div>
392
+ </div>
393
+ </header>
394
+
395
+ <div class="workspace">
396
+ <div class="section">
397
+ <div class="section-header">
398
+ <div class="section-title">
399
+ <i class="bi bi-code-square"></i>
400
+ 代码编辑器
401
+ </div>
402
+ <div style="display: flex; gap: 8px;">
403
+ <button class="btn btn-primary" id="runCode">
404
+ <i class="bi bi-play-fill"></i>
405
+ 运行
406
+ </button>
407
+ <button class="btn btn-danger" id="stopCode" style="display: none;">
408
+ <i class="bi bi-stop-fill"></i>
409
+ 停止
410
+ </button>
411
+ <button class="btn btn-secondary" id="clearCode">
412
+ <i class="bi bi-trash"></i>
413
+ 清除
414
+ </button>
415
+ </div>
416
+ </div>
417
+ <div class="editor-content">
418
+ <div class="line-numbers" id="lineNumbers">1</div>
419
+ <div class="code-area" id="codeArea">
420
+ <pre><code class="language-python" contenteditable="true" spellcheck="false" autocorrect="off" autocapitalize="off"># 您的代码将在这里显示</code></pre>
421
+ </div>
422
+ </div>
423
+ </div>
424
+
425
+ <div class="section">
426
+ <div class="section-header">
427
+ <div class="section-title">
428
+ <i class="bi bi-terminal"></i>
429
+ 终端输出
430
+ </div>
431
+ <div style="display: flex; gap: 8px;">
432
+ <button class="btn btn-secondary" id="clearTerminal">
433
+ <i class="bi bi-eraser"></i>
434
+ 清除
435
+ </button>
436
+ </div>
437
+ </div>
438
+ <div class="output-content">
439
+ <div class="terminal-window">
440
+ <pre id="output"></pre>
441
+ </div>
442
+ <div class="console-input-container" id="consoleInputContainer" style="display: none;">
443
+ <div class="console-prompt">>></div>
444
+ <input type="text" id="consoleInput" class="console-input" autocomplete="off" spellcheck="false" />
445
+ </div>
446
+ </div>
447
+ </div>
448
+ </div>
449
+
450
+ <script>
451
+ document.addEventListener('DOMContentLoaded', () => {
452
+ // Terminal elements
453
+ const terminalOutput = document.getElementById('output');
454
+ const consoleInputContainer = document.getElementById('consoleInputContainer');
455
+ const consoleInput = document.getElementById('consoleInput');
456
+ const clearTerminalBtn = document.getElementById('clearTerminal');
457
+ const stopCodeBtn = document.getElementById('stopCode');
458
+
459
+ // Editor elements
460
+ const codeArea = document.querySelector('#codeArea code');
461
+ const lineNumbers = document.getElementById('lineNumbers');
462
+ const runButton = document.getElementById('runCode');
463
+ const clearButton = document.getElementById('clearCode');
464
+
465
+ // State variables
466
+ let executionContext = null;
467
+ let isExecuting = false;
468
+ let executionStartTime = null;
469
+
470
+ // Initialize terminal
471
+ initializeTerminal();
472
+
473
+ // Initialize editor
474
+ updateLineNumbers();
475
+
476
+ // Function to update line numbers
477
+ function updateLineNumbers() {
478
+ const lines = codeArea.textContent.split('\n').length;
479
+ lineNumbers.innerHTML = Array.from({length: lines}, (_, i) => i + 1).join('<br>');
480
+ }
481
+
482
+ // Initialize terminal with welcome message
483
+ function initializeTerminal() {
484
+ clearTerminalOutput();
485
+ appendToTerminal("欢迎使用Python交互式终端", "term-system");
486
+ appendToTerminal("使用'运行'按钮执行您的代码", "term-system");
487
+ appendToTerminal("", "term-system"); // 空行
488
+ appendToTerminal(">>> 准备执行代码...", "term-system");
489
+ }
490
+
491
+ // Append text to terminal
492
+ function appendToTerminal(text, type = null) {
493
+ const lines = text.split('\n');
494
+ let html = '';
495
+
496
+ for (const line of lines) {
497
+ if (type) {
498
+ html += `<span class="${type}">${escapeHtml(line)}</span>\n`;
499
+ } else {
500
+ html += escapeHtml(line) + '\n';
501
+ }
502
+ }
503
+
504
+ terminalOutput.innerHTML += html;
505
+ terminalOutput.scrollTop = terminalOutput.scrollHeight;
506
+ }
507
+
508
+ // Clear terminal output
509
+ function clearTerminalOutput() {
510
+ terminalOutput.innerHTML = '';
511
+ }
512
+
513
+ // Escape HTML to prevent XSS
514
+ function escapeHtml(text) {
515
+ return text
516
+ .replace(/&/g, "&amp;")
517
+ .replace(/</g, "&lt;")
518
+ .replace(/>/g, "&gt;")
519
+ .replace(/"/g, "&quot;")
520
+ .replace(/'/g, "&#039;");
521
+ }
522
+
523
+ // Run code
524
+ runButton.addEventListener('click', async () => {
525
+ if (isExecuting) return; // Prevent multiple executions
526
+
527
+ const code = codeArea.textContent;
528
+
529
+ // Clear terminal and show execution start
530
+ clearTerminalOutput();
531
+ appendToTerminal("开始执行Python代码...", "term-system");
532
+
533
+ // Update UI state
534
+ isExecuting = true;
535
+ executionStartTime = performance.now();
536
+ runButton.style.display = 'none';
537
+ stopCodeBtn.style.display = 'flex';
538
+
539
+ try {
540
+ const response = await fetch('/api/code/execute', {
541
+ method: 'POST',
542
+ headers: { 'Content-Type': 'application/json' },
543
+ body: JSON.stringify({ code })
544
+ });
545
+
546
+ const data = await response.json();
547
+
548
+ if (data.success) {
549
+ // Show output (if any)
550
+ if (data.output && data.output.trim()) {
551
+ appendToTerminal(data.output, "term-output");
552
+ }
553
+
554
+ if (data.needsInput) {
555
+ // Code is waiting for input
556
+ executionContext = data.context_id;
557
+ consoleInputContainer.style.display = 'flex';
558
+ consoleInput.focus();
559
+ } else {
560
+ // Code has completed, no input needed
561
+ appendToTerminal("程序执行完成", "term-system");
562
+ finishExecution();
563
+ }
564
+ } else {
565
+ // Handle error
566
+ appendToTerminal(`错误: ${data.error}`, "term-error");
567
+ if (data.traceback) {
568
+ appendToTerminal(data.traceback, "term-error");
569
+ }
570
+ appendToTerminal("执行失败", "term-system");
571
+ finishExecution();
572
+ }
573
+ } catch (error) {
574
+ appendToTerminal(`系统错误: ${error.message}`, "term-error");
575
+ appendToTerminal("执行失败", "term-system");
576
+ finishExecution();
577
+ }
578
+ });
579
+
580
+ // Submit console input
581
+ consoleInput.addEventListener('keydown', async (e) => {
582
+ if (e.key === 'Enter' && executionContext) {
583
+ const input = consoleInput.value;
584
+ consoleInput.value = '';
585
+
586
+ // Add input to terminal
587
+ appendToTerminal(`>> ${input}`, "term-input");
588
+
589
+ try {
590
+ // Send input to backend
591
+ const response = await fetch('/api/code/input', {
592
+ method: 'POST',
593
+ headers: { 'Content-Type': 'application/json' },
594
+ body: JSON.stringify({
595
+ input: input,
596
+ context_id: executionContext
597
+ })
598
+ });
599
+
600
+ const data = await response.json();
601
+
602
+ if (data.success) {
603
+ // Show new output
604
+ if (data.output && data.output.trim()) {
605
+ appendToTerminal(data.output, "term-output");
606
+ }
607
+
608
+ if (data.needsInput) {
609
+ // Still waiting for more input
610
+ consoleInput.focus();
611
+ } else {
612
+ // Execution completed
613
+ appendToTerminal(">>> 程序执行完成", "term-system");
614
+ finishExecution();
615
+ }
616
+ } else {
617
+ // Handle error
618
+ appendToTerminal(`错误: ${data.error}`, "term-error");
619
+ if (data.traceback) {
620
+ appendToTerminal(data.traceback, "term-error");
621
+ }
622
+ finishExecution();
623
+ }
624
+ } catch (error) {
625
+ appendToTerminal(`系统错误: ${error.message}`, "term-error");
626
+ finishExecution();
627
+ }
628
+ }
629
+ });
630
+
631
+ // Stop code execution
632
+ stopCodeBtn.addEventListener('click', async () => {
633
+ if (!executionContext) return;
634
+
635
+ try {
636
+ // Send stop request to server
637
+ const response = await fetch('/api/code/stop', {
638
+ method: 'POST',
639
+ headers: { 'Content-Type': 'application/json' },
640
+ body: JSON.stringify({ context_id: executionContext })
641
+ });
642
+
643
+ // Clean up UI regardless of response
644
+ appendToTerminal("用户终止了执行", "term-warning");
645
+ finishExecution();
646
+ } catch (error) {
647
+ console.error('停止执行时出错:', error);
648
+ finishExecution();
649
+ }
650
+ });
651
+
652
+ // Clear code
653
+ clearButton.addEventListener('click', () => {
654
+ codeArea.textContent = '# 您的代码将在这里显示';
655
+ updateLineNumbers();
656
+ clearTerminalOutput();
657
+ initializeTerminal();
658
+ consoleInputContainer.style.display = 'none';
659
+ executionContext = null;
660
+ isExecuting = false;
661
+ runButton.style.display = 'flex';
662
+ stopCodeBtn.style.display = 'none';
663
+ });
664
+
665
+ // Clear terminal
666
+ clearTerminalBtn.addEventListener('click', () => {
667
+ if (!isExecuting) {
668
+ clearTerminalOutput();
669
+ initializeTerminal();
670
+ } else {
671
+ // If execution is in progress, just add a separator
672
+ appendToTerminal("\n--- 已清除终端 ---\n", "term-system");
673
+ }
674
+ });
675
+
676
+ // Update line numbers on code changes
677
+ codeArea.addEventListener('input', updateLineNumbers);
678
+
679
+ // Handle tab key
680
+ codeArea.addEventListener('keydown', (e) => {
681
+ if (e.key === 'Tab') {
682
+ e.preventDefault();
683
+ document.execCommand('insertText', false, ' ');
684
+ }
685
+ });
686
+
687
+ // Function to clean up after execution completes
688
+ function finishExecution() {
689
+ consoleInputContainer.style.display = 'none';
690
+ executionContext = null;
691
+ isExecuting = false;
692
+ runButton.style.display = 'flex';
693
+ stopCodeBtn.style.display = 'none';
694
+ }
695
+
696
+ // Check if code was provided via URL parameters
697
+ const urlParams = new URLSearchParams(window.location.search);
698
+ const initialCode = urlParams.get('code');
699
+ if (initialCode) {
700
+ try {
701
+ codeArea.textContent = decodeURIComponent(initialCode);
702
+ updateLineNumbers();
703
+ } catch (e) {
704
+ console.error('Failed to decode initial code:', e);
705
+ }
706
+ }
707
+
708
+ // Check for messages from parent frame
709
+ window.addEventListener('message', (event) => {
710
+ if (event.data && event.data.type === 'setCode') {
711
+ codeArea.textContent = event.data.code;
712
+ updateLineNumbers();
713
+ }
714
+ });
715
+ });
716
+ </script>
717
+ </body>
718
+ </html>
_archive/templates/index.html ADDED
The diff for this file is too large to render. See raw diff
 
_archive/templates/login.html ADDED
@@ -0,0 +1,504 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>教育AI助手平台 - 登录</title>
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
9
+ <style>
10
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
11
+
12
+ :root {
13
+ /* 优雅的配色方案 */
14
+ --primary-color: #0f2d49;
15
+ --primary-light: #234a70;
16
+ --secondary-color: #4a6cfd;
17
+ --secondary-light: #7b91ff;
18
+ --tertiary-color: #f7f9fe;
19
+ --success-color: #10b981;
20
+ --success-light: rgba(16, 185, 129, 0.1);
21
+ --warning-color: #f59e0b;
22
+ --warning-light: rgba(245, 158, 11, 0.1);
23
+ --info-color: #0ea5e9;
24
+ --info-light: rgba(14, 165, 233, 0.1);
25
+ --danger-color: #ef4444;
26
+ --danger-light: rgba(239, 68, 68, 0.1);
27
+ --neutral-50: #f9fafb;
28
+ --neutral-100: #f3f4f6;
29
+ --neutral-200: #e5e7eb;
30
+ --neutral-300: #d1d5db;
31
+ --neutral-400: #9ca3af;
32
+ --neutral-500: #6b7280;
33
+ --neutral-600: #4b5563;
34
+ --neutral-700: #374151;
35
+ --neutral-800: #1f2937;
36
+ --neutral-900: #111827;
37
+
38
+ /* 样式变量 */
39
+ --border-radius-sm: 0.25rem;
40
+ --border-radius: 0.375rem;
41
+ --border-radius-lg: 0.5rem;
42
+ --border-radius-xl: 0.75rem;
43
+ --border-radius-2xl: 1rem;
44
+ --card-shadow: 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 2px rgba(0, 0, 0, 0.1);
45
+ --card-shadow-hover: 0 10px 20px rgba(0, 0, 0, 0.05), 0 6px 6px rgba(0, 0, 0, 0.1);
46
+ --card-shadow-lg: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
47
+ --transition-base: all 0.2s ease-in-out;
48
+ --transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
49
+ --font-family: 'Inter', 'PingFang SC', 'Helvetica Neue', 'Microsoft YaHei', sans-serif;
50
+ }
51
+
52
+ /* 基础样式 */
53
+ body {
54
+ font-family: var(--font-family);
55
+ background-color: var(--neutral-50);
56
+ color: var(--neutral-800);
57
+ margin: 0;
58
+ padding: 0;
59
+ min-height: 100vh;
60
+ display: flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ -webkit-font-smoothing: antialiased;
64
+ -moz-osx-font-smoothing: grayscale;
65
+ }
66
+
67
+ h1, h2, h3, h4, h5, h6 {
68
+ font-weight: 600;
69
+ color: var(--neutral-900);
70
+ }
71
+
72
+ .text-gradient {
73
+ background: linear-gradient(135deg, var(--secondary-color), var(--secondary-light));
74
+ -webkit-background-clip: text;
75
+ -webkit-text-fill-color: transparent;
76
+ background-clip: text;
77
+ color: transparent;
78
+ }
79
+
80
+ /* 登录容器样式 */
81
+ .login-container {
82
+ width: 100%;
83
+ max-width: 450px;
84
+ padding: 2.5rem;
85
+ background-color: white;
86
+ border-radius: var(--border-radius-xl);
87
+ box-shadow: var(--card-shadow-lg);
88
+ transition: var(--transition-smooth);
89
+ position: relative;
90
+ overflow: hidden;
91
+ }
92
+
93
+ .login-container::before {
94
+ content: '';
95
+ position: absolute;
96
+ top: 0;
97
+ left: 0;
98
+ width: 100%;
99
+ height: 4px;
100
+ background: linear-gradient(to right, var(--secondary-color), var(--secondary-light));
101
+ border-radius: 4px 4px 0 0;
102
+ }
103
+
104
+ .login-header {
105
+ text-align: center;
106
+ margin-bottom: 2.5rem;
107
+ }
108
+
109
+ .login-header h1 {
110
+ font-size: 1.75rem;
111
+ font-weight: 700;
112
+ margin-bottom: 0.5rem;
113
+ background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));
114
+ -webkit-background-clip: text;
115
+ -webkit-text-fill-color: transparent;
116
+ letter-spacing: -0.01em;
117
+ }
118
+
119
+ .login-header p {
120
+ color: var(--neutral-600);
121
+ margin-bottom: 0;
122
+ font-size: 0.95rem;
123
+ }
124
+
125
+ .user-type-switch {
126
+ margin-bottom: 2rem;
127
+ }
128
+
129
+ .user-type-switch .btn-group {
130
+ width: 100%;
131
+ box-shadow: var(--card-shadow);
132
+ border-radius: var(--border-radius-lg);
133
+ padding: 0.25rem;
134
+ background-color: var(--neutral-100);
135
+ }
136
+
137
+ .user-type-switch .btn {
138
+ flex: 1;
139
+ background: transparent;
140
+ border: none;
141
+ padding: 0.75rem 1rem;
142
+ font-weight: 500;
143
+ color: var(--neutral-700);
144
+ border-radius: var(--border-radius);
145
+ transition: var(--transition-base);
146
+ }
147
+
148
+ .user-type-switch .btn:hover {
149
+ color: var(--primary-color);
150
+ }
151
+
152
+ .user-type-switch .btn.active {
153
+ background: white;
154
+ color: var(--secondary-color);
155
+ box-shadow: var(--card-shadow);
156
+ }
157
+
158
+ /* 表单样式 */
159
+ .form-floating {
160
+ margin-bottom: 1.25rem;
161
+ }
162
+
163
+ .form-floating > .form-control {
164
+ padding: 1rem 1rem;
165
+ height: calc(3.5rem + 2px);
166
+ border-radius: var(--border-radius-lg);
167
+ border: 1px solid var(--neutral-200);
168
+ font-size: 0.95rem;
169
+ transition: var(--transition-base);
170
+ }
171
+
172
+ .form-floating > .form-control:focus {
173
+ border-color: var(--secondary-color);
174
+ box-shadow: 0 0 0 3px rgba(74, 108, 253, 0.1);
175
+ }
176
+
177
+ .form-floating > label {
178
+ padding: 1rem;
179
+ color: var(--neutral-500);
180
+ }
181
+
182
+ .form-check {
183
+ display: flex;
184
+ align-items: center;
185
+ margin-bottom: 1.5rem;
186
+ }
187
+
188
+ .form-check-input {
189
+ width: 1.25em;
190
+ height: 1.25em;
191
+ margin-right: 0.75rem;
192
+ background-color: white;
193
+ border: 1px solid var(--neutral-300);
194
+ border-radius: 0.25em;
195
+ transition: all 0.15s ease-in-out;
196
+ }
197
+
198
+ .form-check-input:checked {
199
+ background-color: var(--secondary-color);
200
+ border-color: var(--secondary-color);
201
+ }
202
+
203
+ .form-check-label {
204
+ color: var(--neutral-700);
205
+ font-size: 0.95rem;
206
+ }
207
+
208
+ /* 按钮样式 */
209
+ .btn-primary {
210
+ background: linear-gradient(to right, var(--secondary-color), var(--secondary-light));
211
+ border: none;
212
+ width: 100%;
213
+ padding: 0.85rem;
214
+ font-weight: 500;
215
+ border-radius: var(--border-radius-lg);
216
+ transition: var(--transition-base);
217
+ }
218
+
219
+ .btn-primary:hover, .btn-primary:focus {
220
+ background: linear-gradient(to right, var(--secondary-light), var(--secondary-color));
221
+ box-shadow: 0 4px 10px rgba(74, 108, 253, 0.3);
222
+ transform: translateY(-1px);
223
+ }
224
+
225
+ .btn-outline-primary {
226
+ color: var(--secondary-color);
227
+ border-color: var(--secondary-color);
228
+ background-color: transparent;
229
+ }
230
+
231
+ .btn-outline-primary:hover, .btn-outline-primary:focus {
232
+ background-color: var(--secondary-color);
233
+ border-color: var(--secondary-color);
234
+ color: white;
235
+ box-shadow: 0 4px 10px rgba(74, 108, 253, 0.2);
236
+ }
237
+
238
+ /* 学生说明部分 */
239
+ .student-instructions {
240
+ margin-top: 1.5rem;
241
+ padding: 1.25rem;
242
+ background-color: var(--neutral-50);
243
+ border-radius: var(--border-radius-lg);
244
+ border: 1px solid var(--neutral-200);
245
+ font-size: 0.95rem;
246
+ color: var(--neutral-700);
247
+ transition: var(--transition-smooth);
248
+ }
249
+
250
+ .student-instructions p {
251
+ margin-bottom: 0.75rem;
252
+ }
253
+
254
+ .student-instructions p:last-child {
255
+ margin-bottom: 0;
256
+ }
257
+
258
+ .student-instructions strong {
259
+ color: var(--neutral-900);
260
+ }
261
+
262
+ /* 页脚样式 */
263
+ .login-footer {
264
+ text-align: center;
265
+ margin-top: 2rem;
266
+ color: var(--neutral-500);
267
+ font-size: 0.85rem;
268
+ }
269
+
270
+ /* 动画效果 */
271
+ @keyframes fadeIn {
272
+ from { opacity: 0; transform: translateY(8px); }
273
+ to { opacity: 1; transform: translateY(0); }
274
+ }
275
+
276
+ .fade-in {
277
+ opacity: 0;
278
+ animation: fadeIn 0.4s ease-out forwards;
279
+ }
280
+
281
+ /* 输入组样式 */
282
+ .input-group {
283
+ position: relative;
284
+ }
285
+
286
+ .input-group .form-control {
287
+ padding-right: 3.5rem;
288
+ border-radius: var(--border-radius-lg);
289
+ }
290
+
291
+ .input-group .btn {
292
+ position: absolute;
293
+ right: 0;
294
+ top: 0;
295
+ bottom: 0;
296
+ z-index: 5;
297
+ border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0;
298
+ color: var(--neutral-700);
299
+ background-color: var(--neutral-100);
300
+ border: 1px solid var(--neutral-200);
301
+ border-left: none;
302
+ transition: var(--transition-base);
303
+ }
304
+
305
+ .input-group .btn:hover, .input-group .btn:focus {
306
+ background-color: var(--neutral-200);
307
+ }
308
+ </style>
309
+ </head>
310
+ <body>
311
+ <div class="login-container fade-in">
312
+ <div class="login-header">
313
+ <h1>AI助教开发平台</h1>
314
+ <p id="login-subtitle">教师端登录</p>
315
+ </div>
316
+
317
+ <div class="user-type-switch">
318
+ <div class="btn-group" role="group" id="user-type-buttons">
319
+ <button type="button" class="btn active" id="teacher-button">教师</button>
320
+ <button type="button" class="btn" id="student-button">学生</button>
321
+ </div>
322
+ </div>
323
+
324
+ <form id="login-form">
325
+ <div class="form-floating">
326
+ <input type="text" class="form-control" id="username" placeholder="用户名" required>
327
+ <label for="username">用户名</label>
328
+ </div>
329
+
330
+ <div class="form-floating">
331
+ <input type="password" class="form-control" id="password" placeholder="密码" required>
332
+ <label for="password">密码</label>
333
+ </div>
334
+
335
+ <div class="form-check">
336
+ <input class="form-check-input" type="checkbox" id="remember-me">
337
+ <label class="form-check-label" for="remember-me">
338
+ 记住我
339
+ </label>
340
+ </div>
341
+
342
+ <div id="student-instructions" class="student-instructions" style="display: none;">
343
+ <p><strong>学生登录说明:</strong></p>
344
+ <p>您也可以直接使用教师分享的链接访问AI助手,无需登录。</p>
345
+ <div class="input-group mt-3">
346
+ <input type="text" class="form-control" id="access-token" placeholder="输入访问令牌">
347
+ <button class="btn" type="button" id="access-button">
348
+ <i class="bi bi-arrow-right"></i>
349
+ </button>
350
+ </div>
351
+ </div>
352
+
353
+ <button type="submit" class="btn btn-primary mt-3">
354
+ <i class="bi bi-box-arrow-in-right me-2"></i>登录
355
+ </button>
356
+ </form>
357
+
358
+ <div class="login-footer">
359
+ © 2025 教育AI助手开发平台 版权所有
360
+ </div>
361
+ </div>
362
+
363
+ <script>
364
+ // 硬编码用户
365
+ const users = {
366
+ teachers: [
367
+ { username: 'teacher', password: '123456', name: '李志刚' },
368
+ { username: 'admin', password: 'admin123', name: '管理员' }
369
+ ],
370
+ students: [
371
+ { username: 'student1', password: '123456', name: '张三' },
372
+ { username: 'student2', password: '123456', name: '李四' }
373
+ ]
374
+ };
375
+
376
+ // DOM元素
377
+ const teacherButton = document.getElementById('teacher-button');
378
+ const studentButton = document.getElementById('student-button');
379
+ const loginSubtitle = document.getElementById('login-subtitle');
380
+ const studentInstructions = document.getElementById('student-instructions');
381
+ const loginForm = document.getElementById('login-form');
382
+ const accessButton = document.getElementById('access-button');
383
+
384
+ // 切换用户类型
385
+ teacherButton.addEventListener('click', function() {
386
+ teacherButton.classList.add('active');
387
+ studentButton.classList.remove('active');
388
+ loginSubtitle.textContent = '教师端登录';
389
+ studentInstructions.style.display = 'none';
390
+ });
391
+
392
+ studentButton.addEventListener('click', function() {
393
+ studentButton.classList.add('active');
394
+ teacherButton.classList.remove('active');
395
+ loginSubtitle.textContent = '学生端登录';
396
+ studentInstructions.style.display = 'block';
397
+ });
398
+
399
+ // 处理登录表单提交
400
+ // 处理登录表单提交
401
+ loginForm.addEventListener('submit', async function(e) {
402
+ e.preventDefault();
403
+
404
+ const username = document.getElementById('username').value;
405
+ const password = document.getElementById('password').value;
406
+ const isTeacher = teacherButton.classList.contains('active');
407
+
408
+ try {
409
+ const response = await fetch('/api/auth/login', {
410
+ method: 'POST',
411
+ headers: {
412
+ 'Content-Type': 'application/json'
413
+ },
414
+ body: JSON.stringify({
415
+ username: username,
416
+ password: password,
417
+ type: isTeacher ? 'teacher' : 'student'
418
+ })
419
+ });
420
+
421
+ const data = await response.json();
422
+
423
+ if (data.success) {
424
+ // 登录成功,重定向到相应页面
425
+ window.location.href = isTeacher ? '/index.html' : '/student_portal.html';
426
+ } else {
427
+ showAlert('用户名或密码错误', 'danger');
428
+ }
429
+ } catch (error) {
430
+ console.error('登录失败:', error);
431
+ showAlert('登录请求失败,请重试', 'danger');
432
+ }
433
+ });
434
+ // 处理访问令牌
435
+ accessButton.addEventListener('click', function() {
436
+ const token = document.getElementById('access-token').value.trim();
437
+
438
+ if (token) {
439
+ // 简单验证令牌格式
440
+ if (token.length >= 32) {
441
+ // 这里应该向服务器验证令牌有效性,这里简化处理
442
+ // 直接从URL中提取agent_id (假设格式为: [agent_id]?token=[token])
443
+ if (token.includes('?token=')) {
444
+ window.location.href = '/student/' + token;
445
+ } else {
446
+ // 假设这是一个纯token,需要输入Agent ID
447
+ const agentId = prompt('请输入Agent ID');
448
+ if (agentId) {
449
+ window.location.href = `/student/${agentId}?token=${token}`;
450
+ }
451
+ }
452
+ } else {
453
+ showAlert('无效的访问令牌格式', 'warning');
454
+ }
455
+ } else {
456
+ showAlert('请输入访问令牌', 'warning');
457
+ }
458
+ });
459
+
460
+ // 显示提示信息
461
+ function showAlert(message, type) {
462
+ // 移除现有的提示
463
+ const existingAlert = document.querySelector('.alert');
464
+ if (existingAlert) {
465
+ existingAlert.remove();
466
+ }
467
+
468
+ // 创建新提示
469
+ const alert = document.createElement('div');
470
+ alert.className = `alert alert-${type} fade-in`;
471
+ alert.role = 'alert';
472
+ alert.style.marginBottom = '1.5rem';
473
+ alert.innerHTML = `
474
+ <i class="bi ${type === 'danger' ? 'bi-exclamation-triangle' : 'bi-info-circle'} me-2"></i>
475
+ ${message}
476
+ `;
477
+
478
+ // 添加到表单上方
479
+ loginForm.insertAdjacentElement('beforebegin', alert);
480
+
481
+ // 3秒后自动移除
482
+ setTimeout(() => {
483
+ alert.style.opacity = '0';
484
+ setTimeout(() => alert.remove(), 300);
485
+ }, 3000);
486
+ }
487
+
488
+ // 页面加载时检查是否已经登录
489
+ // window.addEventListener('DOMContentLoaded', function() {
490
+ // const currentUser = localStorage.getItem('currentUser');
491
+
492
+ // if (currentUser) {
493
+ // const userData = JSON.parse(currentUser);
494
+
495
+ // if (userData.type === 'teacher') {
496
+ // window.location.href = '/index.html';
497
+ // } else if (userData.type === 'student') {
498
+ // window.location.href = '/student_portal.html';
499
+ // }
500
+ // }
501
+ // });
502
+ </script>
503
+ </body>
504
+ </html>
_archive/templates/student.html ADDED
@@ -0,0 +1,1651 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AI学习助手</title>
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
9
+ <!-- KaTeX 用于渲染数学公式 -->
10
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/katex.min.css">
11
+ <!-- Highlight.js 样式 -->
12
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css">
13
+ <style>
14
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
15
+
16
+ :root {
17
+ /* 主要配色 */
18
+ --primary-color: #0f2d49;
19
+ --primary-light: #234a70;
20
+ --secondary-color: #4a6cfd;
21
+ --secondary-light: #7b91ff;
22
+ --tertiary-color: #f7f9fe;
23
+
24
+ /* 功能颜色 */
25
+ --success-color: #10b981;
26
+ --success-light: #d1fae5;
27
+ --warning-color: #f59e0b;
28
+ --warning-light: #fef3c7;
29
+ --danger-color: #ef4444;
30
+ --danger-light: #fee2e2;
31
+ --info-color: #3b82f6;
32
+ --info-light: #dbeafe;
33
+
34
+ /* 中性色 */
35
+ --neutral-50: #f9fafb;
36
+ --neutral-100: #f3f4f6;
37
+ --neutral-200: #e5e7eb;
38
+ --neutral-300: #d1d5db;
39
+ --neutral-400: #9ca3af;
40
+ --neutral-500: #6b7280;
41
+ --neutral-600: #4b5563;
42
+ --neutral-700: #374151;
43
+ --neutral-800: #1f2937;
44
+ --neutral-900: #111827;
45
+
46
+ /* 界面元素 */
47
+ --border-radius-sm: 0.25rem;
48
+ --border-radius: 0.375rem;
49
+ --border-radius-lg: 0.5rem;
50
+ --border-radius-xl: 0.75rem;
51
+ --border-radius-xxl: 1rem;
52
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
53
+ --shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
54
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
55
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
56
+ --shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
57
+ --transition-base: all 0.2s ease-in-out;
58
+ --transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
59
+ --font-family: 'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif;
60
+ }
61
+
62
+ body {
63
+ font-family: var(--font-family);
64
+ margin: 0;
65
+ padding: 0;
66
+ height: 100vh;
67
+ background-color: var(--neutral-50);
68
+ color: var(--neutral-800);
69
+ display: flex;
70
+ flex-direction: column;
71
+ -webkit-font-smoothing: antialiased;
72
+ -moz-osx-font-smoothing: grayscale;
73
+ }
74
+
75
+ /* 头部样式 */
76
+ .header {
77
+ background: linear-gradient(135deg, var(--primary-color), var(--primary-light));
78
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
79
+ color: white;
80
+ padding: 1rem 1.5rem;
81
+ box-shadow: var(--shadow-md);
82
+ position: relative;
83
+ z-index: 10;
84
+ }
85
+
86
+ .header-content {
87
+ max-width: 1600px;
88
+ margin: 0 auto;
89
+ display: flex;
90
+ justify-content: space-between;
91
+ align-items: center;
92
+ }
93
+
94
+ .header h1 {
95
+ margin: 0;
96
+ font-size: 1.5rem;
97
+ font-weight: 600;
98
+ color: white;
99
+ display: flex;
100
+ align-items: center;
101
+ }
102
+
103
+ .header h1 i {
104
+ margin-right: 0.75rem;
105
+ color: rgba(255, 255, 255, 0.8);
106
+ }
107
+
108
+ .header .badge {
109
+ background-color: rgba(255, 255, 255, 0.15);
110
+ color: white;
111
+ font-weight: 500;
112
+ padding: 0.35em 0.75em;
113
+ font-size: 0.85rem;
114
+ border-radius: 9999px;
115
+ }
116
+
117
+ /* 主容器样式 */
118
+ .main-container {
119
+ flex: 1;
120
+ display: flex;
121
+ max-width: 1600px;
122
+ margin: 0 auto;
123
+ padding: 1.5rem;
124
+ width: 100%;
125
+ box-sizing: border-box;
126
+ height: calc(100vh - 70px); /* Header height + padding */
127
+ overflow: hidden;
128
+ }
129
+
130
+ .chat-container {
131
+ flex: 1;
132
+ display: flex;
133
+ flex-direction: column;
134
+ background-color: white;
135
+ border-radius: var(--border-radius-xl);
136
+ box-shadow: var(--shadow);
137
+ overflow: hidden;
138
+ height: 100%;
139
+ max-width: 800px;
140
+ margin: 0 auto;
141
+ transition: var(--transition-smooth);
142
+ position: relative;
143
+ }
144
+
145
+ /* 聊天对话区域样式 */
146
+ .chat-messages {
147
+ flex: 1;
148
+ overflow-y: auto;
149
+ padding: 1.5rem;
150
+ display: flex;
151
+ flex-direction: column;
152
+ background-color: var(--neutral-50);
153
+ gap: 1rem;
154
+ scroll-behavior: smooth;
155
+ }
156
+
157
+ /* 自定义滚动条 */
158
+ .chat-messages::-webkit-scrollbar {
159
+ width: 6px;
160
+ height: 6px;
161
+ }
162
+
163
+ .chat-messages::-webkit-scrollbar-track {
164
+ background: var(--neutral-100);
165
+ border-radius: 10px;
166
+ }
167
+
168
+ .chat-messages::-webkit-scrollbar-thumb {
169
+ background: var(--neutral-300);
170
+ border-radius: 10px;
171
+ }
172
+
173
+ .chat-messages::-webkit-scrollbar-thumb:hover {
174
+ background: var(--neutral-400);
175
+ }
176
+
177
+ /* 消息气泡样式 */
178
+ .message {
179
+ max-width: 85%;
180
+ border-radius: var(--border-radius-lg);
181
+ padding: 1rem;
182
+ position: relative;
183
+ line-height: 1.5;
184
+ box-shadow: var(--shadow-sm);
185
+ overflow-wrap: break-word;
186
+ word-wrap: break-word;
187
+ hyphens: auto;
188
+ }
189
+
190
+ .message .message-content {
191
+ padding: 0 10px;
192
+ margin: 0;
193
+ }
194
+
195
+ .user-message {
196
+ background: linear-gradient(135deg, #e9f5ff, #c2e4ff);
197
+ align-self: flex-end;
198
+ color: var(--primary-color);
199
+ border-bottom-right-radius: 0.2rem;
200
+ border-left: 1px solid rgba(74, 108, 253, 0.1);
201
+ border-top: 1px solid rgba(255, 255, 255, 0.5);
202
+ }
203
+
204
+ .bot-message {
205
+ background-color: white;
206
+ align-self: flex-start;
207
+ color: var(--neutral-800);
208
+ border-bottom-left-radius: 0.2rem;
209
+ border-left: 3px solid var(--secondary-color);
210
+ box-shadow: var(--shadow);
211
+ }
212
+
213
+ /* 输入区域样式 */
214
+ .input-container {
215
+ padding: 1rem 1.5rem;
216
+ border-top: 1px solid var(--neutral-200);
217
+ background-color: white;
218
+ position: relative;
219
+ }
220
+
221
+ .input-row {
222
+ display: flex;
223
+ gap: 0.75rem;
224
+ position: relative;
225
+ }
226
+
227
+ .input-field {
228
+ flex: 1;
229
+ padding: 0.85rem 1rem;
230
+ border: 1px solid var(--neutral-200);
231
+ border-radius: var(--border-radius-lg);
232
+ resize: none;
233
+ font-size: 0.95rem;
234
+ box-shadow: var(--shadow-inner);
235
+ transition: var(--transition-base);
236
+ }
237
+
238
+ .input-field:focus {
239
+ outline: none;
240
+ border-color: var(--secondary-color);
241
+ box-shadow: 0 0 0 3px rgba(74, 108, 253, 0.15);
242
+ }
243
+
244
+ .send-button {
245
+ padding: 0 1.25rem;
246
+ background: linear-gradient(135deg, var(--secondary-color), var(--secondary-light));
247
+ color: white;
248
+ border: none;
249
+ border-radius: var(--border-radius-lg);
250
+ cursor: pointer;
251
+ font-weight: 500;
252
+ transition: var(--transition-base);
253
+ display: flex;
254
+ align-items: center;
255
+ justify-content: center;
256
+ }
257
+
258
+ .send-button:hover {
259
+ box-shadow: 0 4px 10px rgba(74, 108, 253, 0.25);
260
+ transform: translateY(-1px);
261
+ }
262
+
263
+ .send-button:disabled {
264
+ background: var(--neutral-300);
265
+ cursor: not-allowed;
266
+ transform: none;
267
+ box-shadow: none;
268
+ }
269
+
270
+ /* 插件容器样式 */
271
+ .plugin-container {
272
+ display: none;
273
+ flex-direction: column;
274
+ background-color: white;
275
+ border-radius: var(--border-radius-xl);
276
+ box-shadow: var(--shadow);
277
+ overflow: hidden;
278
+ height: 100%;
279
+ flex: 2;
280
+ margin-left: 1.5rem;
281
+ transition: var(--transition-smooth);
282
+ }
283
+
284
+ .plugin-header {
285
+ padding: 0.85rem 1.25rem;
286
+ border-bottom: 1px solid var(--neutral-200);
287
+ background-color: var(--neutral-50);
288
+ display: flex;
289
+ justify-content: space-between;
290
+ align-items: center;
291
+ }
292
+
293
+ .plugin-title {
294
+ margin: 0;
295
+ font-size: 1rem;
296
+ font-weight: 600;
297
+ color: var(--primary-color);
298
+ display: flex;
299
+ align-items: center;
300
+ }
301
+
302
+ .plugin-title i {
303
+ margin-right: 0.5rem;
304
+ color: var(--secondary-color);
305
+ }
306
+
307
+ .plugin-close {
308
+ width: 32px;
309
+ height: 32px;
310
+ background: var(--neutral-100);
311
+ border: none;
312
+ border-radius: 50%;
313
+ cursor: pointer;
314
+ font-size: 0.85rem;
315
+ color: var(--neutral-700);
316
+ display: flex;
317
+ align-items: center;
318
+ justify-content: center;
319
+ transition: var(--transition-base);
320
+ }
321
+
322
+ .plugin-close:hover {
323
+ background-color: var(--neutral-200);
324
+ color: var(--neutral-900);
325
+ }
326
+
327
+ .plugin-content {
328
+ flex: 1;
329
+ overflow-y: auto;
330
+ padding: 0;
331
+ background-color: var(--neutral-50);
332
+ }
333
+
334
+ /* 当插件激活时的样式 */
335
+ .main-container.with-plugin .chat-container {
336
+ max-width: 380px;
337
+ margin: 0;
338
+ }
339
+
340
+ /* 代码块样式 */
341
+ pre {
342
+ margin: 1rem 0;
343
+ padding: 1rem;
344
+ background-color: var(--neutral-800);
345
+ border-radius: var(--border-radius);
346
+ overflow-x: auto;
347
+ position: relative;
348
+ }
349
+
350
+ pre code {
351
+ font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
352
+ font-size: 0.9rem;
353
+ color: #e5e7eb;
354
+ padding: 0;
355
+ background: none;
356
+ }
357
+
358
+ .copy-button {
359
+ position: absolute;
360
+ top: 0.5rem;
361
+ right: 0.5rem;
362
+ padding: 0.25rem 0.5rem;
363
+ background-color: rgba(255, 255, 255, 0.1);
364
+ color: rgba(255, 255, 255, 0.6);
365
+ border: none;
366
+ border-radius: var(--border-radius-sm);
367
+ font-size: 0.75rem;
368
+ cursor: pointer;
369
+ transition: var(--transition-base);
370
+ display: flex;
371
+ align-items: center;
372
+ gap: 0.25rem;
373
+ }
374
+
375
+ .copy-button:hover {
376
+ background-color: rgba(255, 255, 255, 0.2);
377
+ color: rgba(255, 255, 255, 0.9);
378
+ }
379
+
380
+ /* 代码执行结果样式 */
381
+ .code-result {
382
+ margin-top: 0.5rem;
383
+ padding: 0.75rem;
384
+ background-color: var(--neutral-900);
385
+ border-radius: var(--border-radius);
386
+ color: white;
387
+ font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
388
+ font-size: 0.85rem;
389
+ white-space: pre-wrap;
390
+ }
391
+
392
+ /* 内联代码样式 */
393
+ code:not(pre code) {
394
+ font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
395
+ font-size: 0.9em;
396
+ color: var(--primary-color);
397
+ background-color: var(--neutral-100);
398
+ padding: 0.2em 0.4em;
399
+ border-radius: 3px;
400
+ }
401
+
402
+ /* 参考资料样式 */
403
+ .reference-container {
404
+ margin-top: 1rem;
405
+ border-top: 1px dashed var(--neutral-300);
406
+ padding-top: 0.75rem;
407
+ }
408
+
409
+ .reference-toggle {
410
+ color: var(--neutral-600);
411
+ font-size: 0.85rem;
412
+ font-weight: 500;
413
+ cursor: pointer;
414
+ display: flex;
415
+ align-items: center;
416
+ transition: var(--transition-base);
417
+ }
418
+
419
+ .reference-toggle:hover {
420
+ color: var(--secondary-color);
421
+ }
422
+
423
+ .reference-toggle i {
424
+ margin-right: 0.35rem;
425
+ font-size: 0.9rem;
426
+ }
427
+
428
+ .reference-content {
429
+ display: none;
430
+ margin-top: 0.75rem;
431
+ padding: 0.75rem;
432
+ background-color: var(--neutral-100);
433
+ border-radius: var(--border-radius);
434
+ font-size: 0.85rem;
435
+ }
436
+
437
+ .reference-item {
438
+ margin-bottom: 0.75rem;
439
+ padding-bottom: 0.75rem;
440
+ border-bottom: 1px solid var(--neutral-200);
441
+ }
442
+
443
+ .reference-item:last-child {
444
+ margin-bottom: 0;
445
+ padding-bottom: 0;
446
+ border-bottom: none;
447
+ }
448
+
449
+ .reference-item-source {
450
+ color: var(--neutral-600);
451
+ font-size: 0.8rem;
452
+ margin-top: 0.25rem;
453
+ }
454
+
455
+ /* 欢迎消息样式 */
456
+ .welcome-message {
457
+ display: flex;
458
+ flex-direction: column;
459
+ align-items: center;
460
+ text-align: center;
461
+ margin-bottom: 1.5rem;
462
+ }
463
+
464
+ .welcome-icon {
465
+ width: 64px;
466
+ height: 64px;
467
+ background: linear-gradient(135deg, var(--secondary-color), var(--secondary-light));
468
+ border-radius: 50%;
469
+ display: flex;
470
+ align-items: center;
471
+ justify-content: center;
472
+ margin-bottom: 1rem;
473
+ color: white;
474
+ font-size: 2rem;
475
+ box-shadow: 0 4px 12px rgba(74, 108, 253, 0.25);
476
+ }
477
+
478
+ .welcome-title {
479
+ font-size: 1.5rem;
480
+ font-weight: 600;
481
+ margin-bottom: 0.5rem;
482
+ color: var(--primary-color);
483
+ }
484
+
485
+ .welcome-description {
486
+ color: var(--neutral-700);
487
+ max-width: 400px;
488
+ margin-bottom: 0;
489
+ }
490
+
491
+ /* 思维导图和可视化插件内容样式 */
492
+ .visualization-result, .mindmap-result {
493
+ padding: 1.5rem;
494
+ text-align: center;
495
+ }
496
+
497
+ .visualization-image, .mindmap-image {
498
+ max-width: 100%;
499
+ border-radius: var(--border-radius);
500
+ box-shadow: var(--shadow);
501
+ }
502
+
503
+ /* 响应式样式 */
504
+ @media (max-width: 1200px) {
505
+ .main-container.with-plugin .chat-container {
506
+ max-width: 320px;
507
+ }
508
+ }
509
+
510
+ @media (max-width: 992px) {
511
+ .main-container {
512
+ flex-direction: column;
513
+ padding: 1rem;
514
+ }
515
+
516
+ .main-container.with-plugin .chat-container {
517
+ max-width: 100%;
518
+ margin-bottom: 1rem;
519
+ height: 40vh;
520
+ }
521
+
522
+ .plugin-container {
523
+ margin-left: 0;
524
+ height: calc(60vh - 32px);
525
+ }
526
+ }
527
+
528
+ @media (max-width: 768px) {
529
+ .main-container {
530
+ padding: 0.75rem;
531
+ }
532
+
533
+ .message {
534
+ max-width: 95%;
535
+ }
536
+ }
537
+
538
+ /* 增强的卡塔赫和Markdown样式 */
539
+ .katex {
540
+ font-size: 1.1em;
541
+ }
542
+
543
+ .message h1, .message h2, .message h3,
544
+ .message h4, .message h5, .message h6 {
545
+ margin-top: 1em;
546
+ margin-bottom: 0.5em;
547
+ line-height: 1.3;
548
+ }
549
+
550
+ .message h1 {
551
+ font-size: 1.6em;
552
+ border-bottom: 1px solid var(--neutral-200);
553
+ padding-bottom: 0.3em;
554
+ }
555
+
556
+ .message h2 {
557
+ font-size: 1.4em;
558
+ border-bottom: 1px solid var(--neutral-200);
559
+ padding-bottom: 0.3em;
560
+ }
561
+
562
+ .message h3 {
563
+ font-size: 1.2em;
564
+ }
565
+
566
+ .message h4 {
567
+ font-size: 1.1em;
568
+ }
569
+
570
+ .message h5, .message h6 {
571
+ font-size: 1em;
572
+ }
573
+
574
+ .message p {
575
+ margin: 0.5em 0;
576
+ }
577
+
578
+ .message ul, .message ol {
579
+ margin: 0.5em 0;
580
+ padding-left: 1.5em;
581
+ }
582
+
583
+ .message li {
584
+ margin: 0.25em 0;
585
+ }
586
+
587
+ .message blockquote {
588
+ margin: 0.5em 0;
589
+ padding-left: 1em;
590
+ border-left: 4px solid var(--neutral-300);
591
+ color: var(--neutral-700);
592
+ }
593
+
594
+ .message img {
595
+ max-width: 100%;
596
+ border-radius: var(--border-radius);
597
+ }
598
+
599
+ .message table {
600
+ border-collapse: collapse;
601
+ margin: 1em 0;
602
+ width: 100%;
603
+ }
604
+
605
+ .message table th,
606
+ .message table td {
607
+ border: 1px solid var(--neutral-300);
608
+ padding: 0.5em;
609
+ }
610
+
611
+ .message table th {
612
+ background-color: var(--neutral-100);
613
+ font-weight: 600;
614
+ }
615
+
616
+ .message table tr:nth-child(even) {
617
+ background-color: var(--neutral-50);
618
+ }
619
+
620
+ /* 动画效果 */
621
+ @keyframes fadeIn {
622
+ from { opacity: 0; transform: translateY(10px); }
623
+ to { opacity: 1; transform: translateY(0); }
624
+ }
625
+
626
+ .message {
627
+ animation: fadeIn 0.3s ease forwards;
628
+ }
629
+
630
+ /* 输入提示区域 */
631
+ .input-suggestions {
632
+ position: absolute;
633
+ bottom: 100%;
634
+ left: 0;
635
+ right: 0;
636
+ background-color: white;
637
+ border-top-left-radius: var(--border-radius-lg);
638
+ border-top-right-radius: var(--border-radius-lg);
639
+ box-shadow: var(--shadow-md);
640
+ padding: 0.75rem;
641
+ display: none;
642
+ border: 1px solid var(--neutral-200);
643
+ border-bottom: none;
644
+ }
645
+
646
+ .suggestion-title {
647
+ font-size: 0.85rem;
648
+ font-weight: 600;
649
+ color: var(--neutral-700);
650
+ margin-bottom: 0.5rem;
651
+ }
652
+
653
+ .suggestion-buttons {
654
+ display: flex;
655
+ flex-wrap: wrap;
656
+ gap: 0.5rem;
657
+ }
658
+
659
+ .suggestion-button {
660
+ padding: 0.5rem 0.75rem;
661
+ background-color: var(--neutral-100);
662
+ border: 1px solid var(--neutral-200);
663
+ border-radius: var(--border-radius);
664
+ font-size: 0.85rem;
665
+ color: var(--neutral-800);
666
+ cursor: pointer;
667
+ transition: var(--transition-base);
668
+ }
669
+
670
+ .suggestion-button:hover {
671
+ background-color: var(--secondary-light);
672
+ color: white;
673
+ border-color: var(--secondary-light);
674
+ }
675
+
676
+ /* 打字机效果 */
677
+ .typing-indicator {
678
+ display: inline-block;
679
+ margin-left: 5px;
680
+ }
681
+
682
+ .typing-dot {
683
+ display: inline-block;
684
+ width: 6px;
685
+ height: 6px;
686
+ border-radius: 50%;
687
+ background-color: var(--neutral-500);
688
+ margin-right: 3px;
689
+ animation: typingDot 1.4s infinite ease-in-out;
690
+ }
691
+
692
+ .typing-dot:nth-child(1) { animation-delay: 0s; }
693
+ .typing-dot:nth-child(2) { animation-delay: 0.2s; }
694
+ .typing-dot:nth-child(3) { animation-delay: 0.4s; }
695
+
696
+ @keyframes typingDot {
697
+ 0%, 60%, 100% { transform: translateY(0); }
698
+ 30% { transform: translateY(-5px); }
699
+ }
700
+
701
+ /* 打字机控制样式 */
702
+ .typewriter-controls {
703
+ display: flex;
704
+ gap: 8px;
705
+ margin-top: 12px;
706
+ align-items: center;
707
+ justify-content: flex-end;
708
+ }
709
+
710
+ .typewriter-btn {
711
+ width: 32px;
712
+ height: 32px;
713
+ border-radius: 50%;
714
+ border: none;
715
+ background-color: var(--neutral-100);
716
+ color: var(--neutral-600);
717
+ cursor: pointer;
718
+ display: flex;
719
+ align-items: center;
720
+ justify-content: center;
721
+ transition: var(--transition-base);
722
+ }
723
+
724
+ .typewriter-btn:hover {
725
+ background-color: var(--neutral-200);
726
+ color: var(--neutral-900);
727
+ }
728
+
729
+ .typewriter-btn.pause-btn {
730
+ color: var(--danger-color);
731
+ }
732
+
733
+ .typewriter-btn.continue-btn {
734
+ color: var(--success-color);
735
+ }
736
+
737
+ .typewriter-btn.speed-btn {
738
+ color: var(--warning-color);
739
+ }
740
+
741
+ /* 打字机光标效果 */
742
+ .typing-cursor {
743
+ display: inline-block;
744
+ width: 2px;
745
+ height: 1em;
746
+ background-color: var(--neutral-700);
747
+ margin-left: 2px;
748
+ vertical-align: middle;
749
+ animation: blink 1s infinite;
750
+ }
751
+
752
+ @keyframes blink {
753
+ 0%, 100% { opacity: 1; }
754
+ 50% { opacity: 0; }
755
+ }
756
+
757
+ /* 确保代码块内文本溢出有滚动条 */
758
+ pre {
759
+ position: relative;
760
+ white-space: pre;
761
+ word-wrap: normal;
762
+ overflow-x: auto;
763
+ }
764
+
765
+ /* 保证消息区内容正常显示 */
766
+ .message-content p {
767
+ word-break: break-word;
768
+ }
769
+
770
+ /* 确保代码复制按钮正确显示 */
771
+ .copy-button {
772
+ opacity: 0.8;
773
+ z-index: 10;
774
+ }
775
+ </style>
776
+ </head>
777
+ <body>
778
+ <header class="header">
779
+ <div class="header-content">
780
+ <h1><i class="bi bi-mortarboard"></i> {{ agent_name }}</h1>
781
+ <div>
782
+ <span class="badge">AI学习助手</span>
783
+ </div>
784
+ </div>
785
+ </header>
786
+
787
+ <div class="main-container" id="main-container">
788
+ <div class="chat-container">
789
+ <div class="chat-messages" id="chat-messages">
790
+ <div class="welcome-message">
791
+ <div class="welcome-icon">
792
+ <i class="bi bi-robot"></i>
793
+ </div>
794
+ <h2 class="welcome-title">欢迎使用 {{ agent_name }}</h2>
795
+ {% if agent_description %}
796
+ <p class="welcome-description">{{ agent_description }}</p>
797
+ {% else %}
798
+ <p class="welcome-description">我是您的AI学习助手,有任何问题都可以随时向我提问</p>
799
+ {% endif %}
800
+ </div>
801
+
802
+ <div class="message bot-message">
803
+ <div class="message-content">
804
+ <p>您好!我是{{ agent_name }},很高兴能够帮助您学习。请问有什么我可以协助您的问题吗?</p>
805
+ </div>
806
+ </div>
807
+ </div>
808
+
809
+ <div class="input-container">
810
+ <div class="input-suggestions" id="input-suggestions">
811
+ <div class="suggestion-title">推荐问题:</div>
812
+ <div class="suggestion-buttons">
813
+ <button class="suggestion-button">介绍一下这门课程的主要内容</button>
814
+ <button class="suggestion-button">这门课程有哪些重点知识?</button>
815
+ <button class="suggestion-button">请给我一些学习建议</button>
816
+ </div>
817
+ </div>
818
+ <div class="input-row">
819
+ <textarea class="input-field" id="user-input" placeholder="输入您的问题..." rows="2"></textarea>
820
+ <button class="send-button" id="send-button">
821
+ <i class="bi bi-send"></i>
822
+ </button>
823
+ </div>
824
+ </div>
825
+ </div>
826
+
827
+ <!-- 代码执行插件 -->
828
+ <div class="plugin-container code-plugin" id="code-plugin">
829
+ <div class="plugin-header">
830
+ <h3 class="plugin-title"><i class="bi bi-code-square"></i> Python代码执行</h3>
831
+ <button class="plugin-close" id="close-code-plugin">
832
+ <i class="bi bi-x-lg"></i>
833
+ </button>
834
+ </div>
835
+ <div class="plugin-content">
836
+ <iframe id="code-execution-frame" src="" style="width: 100%; height: 100%; border: none;"></iframe>
837
+ </div>
838
+ </div>
839
+
840
+ <!-- 3D可视化插件 -->
841
+ <div class="plugin-container visualization-plugin" id="visualization-plugin">
842
+ <div class="plugin-header">
843
+ <h3 class="plugin-title"><i class="bi bi-graph-up"></i> 3D可视化</h3>
844
+ <button class="plugin-close" id="close-visualization-plugin">
845
+ <i class="bi bi-x-lg"></i>
846
+ </button>
847
+ </div>
848
+ <div class="plugin-content">
849
+ <div class="visualization-result" id="visualization-result">
850
+ <div class="text-center py-4">
851
+ <div class="spinner-border text-primary" role="status">
852
+ <span class="visually-hidden">加载中...</span>
853
+ </div>
854
+ <p class="mt-3">正在准备3D可视化...</p>
855
+ </div>
856
+ </div>
857
+ </div>
858
+ </div>
859
+
860
+ <!-- 思维导图插件 -->
861
+ <div class="plugin-container mindmap-plugin" id="mindmap-plugin">
862
+ <div class="plugin-header">
863
+ <h3 class="plugin-title"><i class="bi bi-diagram-3"></i> 思维导图</h3>
864
+ <button class="plugin-close" id="close-mindmap-plugin">
865
+ <i class="bi bi-x-lg"></i>
866
+ </button>
867
+ </div>
868
+ <div class="plugin-content">
869
+ <div class="mindmap-result" id="mindmap-result">
870
+ <div class="text-center py-4">
871
+ <div class="spinner-border text-primary" role="status">
872
+ <span class="visually-hidden">加载中...</span>
873
+ </div>
874
+ <p class="mt-3">正在生成思维导图...</p>
875
+ </div>
876
+ </div>
877
+ </div>
878
+ </div>
879
+ </div>
880
+
881
+ <script src="https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/katex.min.js"></script>
882
+ <script src="https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/contrib/auto-render.min.js"></script>
883
+ <script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js"></script>
884
+ <script>
885
+ // 全局变量
886
+ const agentId = "{{ agent_id }}";
887
+ const token = "{{ token }}";
888
+ let executionContext = null;
889
+ const mainContainer = document.getElementById('main-container');
890
+ let isTyping = false;
891
+ let typewriterInterval = null;
892
+ let typewriterPaused = false;
893
+ let currentMessageQueue = [];
894
+ let typewriterSpeed = 20; // 打字速度(ms)
895
+ let currentMessageElement = null;
896
+
897
+ // DOM元素
898
+ const chatMessages = document.getElementById('chat-messages');
899
+ const userInput = document.getElementById('user-input');
900
+ const sendButton = document.getElementById('send-button');
901
+
902
+ // 代码执行插件元素
903
+ const codePlugin = document.getElementById('code-plugin');
904
+ const closeCodePlugin = document.getElementById('close-code-plugin');
905
+ const codeExecutionFrame = document.getElementById('code-execution-frame');
906
+
907
+ // 3D可视化插件元素
908
+ const visualizationPlugin = document.getElementById('visualization-plugin');
909
+ const closeVisualizationPlugin = document.getElementById('close-visualization-plugin');
910
+ const visualizationResult = document.getElementById('visualization-result');
911
+
912
+ // 思维导图插件元素
913
+ const mindmapPlugin = document.getElementById('mindmap-plugin');
914
+ const closeMindmapPlugin = document.getElementById('close-mindmap-plugin');
915
+ const mindmapResult = document.getElementById('mindmap-result');
916
+
917
+ // 输入建议
918
+ const inputSuggestions = document.getElementById('input-suggestions');
919
+
920
+ // Markdown-it 实例
921
+ const md = window.markdownit({
922
+ html: false,
923
+ linkify: true,
924
+ typographer: true,
925
+ breaks: true,
926
+ highlight: function (str, lang) {
927
+ if (lang && hljs.getLanguage(lang)) {
928
+ try {
929
+ const highlighted = hljs.highlight(str, { language: lang }).value;
930
+ return `<pre><code class="hljs language-${lang}">${highlighted}</code><button class="copy-button" onclick="copyToClipboard(this)"><i class="bi bi-clipboard"></i> 复制</button></pre>`;
931
+ } catch (__) {}
932
+ }
933
+ return `<pre><code class="hljs">${md.utils.escapeHtml(str)}</code><button class="copy-button" onclick="copyToClipboard(this)"><i class="bi bi-clipboard"></i> 复制</button></pre>`;
934
+ }
935
+ });
936
+
937
+ // 页面加载完成后执行
938
+ document.addEventListener('DOMContentLoaded', function() {
939
+ // 初始化发送按钮事件
940
+ sendButton.addEventListener('click', sendMessage);
941
+
942
+ // 初始化输入框回车事件
943
+ userInput.addEventListener('keydown', function(e) {
944
+ if (e.key === 'Enter' && !e.shiftKey) {
945
+ e.preventDefault();
946
+ sendMessage();
947
+ }
948
+ });
949
+
950
+ // 初始化问题建议按钮
951
+ document.querySelectorAll('.suggestion-button').forEach(button => {
952
+ button.addEventListener('click', function() {
953
+ userInput.value = this.textContent;
954
+ sendMessage();
955
+ });
956
+ });
957
+
958
+ // 初始化插件关闭事件
959
+ closeCodePlugin.addEventListener('click', () => {
960
+ codePlugin.style.display = 'none';
961
+ // 清空iframe源以停止任何运行中的代码
962
+ codeExecutionFrame.src = '';
963
+ updateMainContainerLayout();
964
+ });
965
+
966
+ closeVisualizationPlugin.addEventListener('click', () => {
967
+ visualizationPlugin.style.display = 'none';
968
+ updateMainContainerLayout();
969
+ });
970
+
971
+ closeMindmapPlugin.addEventListener('click', () => {
972
+ mindmapPlugin.style.display = 'none';
973
+ updateMainContainerLayout();
974
+ });
975
+
976
+ // 焦点事件
977
+ userInput.addEventListener('focus', function() {
978
+ // inputSuggestions.style.display = 'block';
979
+ });
980
+
981
+ userInput.addEventListener('blur', function(e) {
982
+ // 延迟执行,以便点击建议按钮事件先触发
983
+ setTimeout(() => {
984
+ // inputSuggestions.style.display = 'none';
985
+ }, 100);
986
+ });
987
+ });
988
+
989
+ // 更新主容器布局
990
+ function updateMainContainerLayout() {
991
+ // 检查是否有任何插件处于显示状态
992
+ const isAnyPluginVisible =
993
+ codePlugin.style.display === 'flex' ||
994
+ visualizationPlugin.style.display === 'flex' ||
995
+ mindmapPlugin.style.display === 'flex';
996
+
997
+ // 更新主容器类名
998
+ if (isAnyPluginVisible) {
999
+ mainContainer.classList.add('with-plugin');
1000
+ } else {
1001
+ mainContainer.classList.remove('with-plugin');
1002
+ }
1003
+ }
1004
+
1005
+ // 发送消息
1006
+ async function sendMessage() {
1007
+ const message = userInput.value.trim();
1008
+ if (!message || isTyping) return;
1009
+
1010
+ // 添加用户消息
1011
+ addMessage(message, true);
1012
+
1013
+ // 清空输入框
1014
+ userInput.value = '';
1015
+
1016
+ // 禁用发送按钮
1017
+ sendButton.disabled = true;
1018
+ isTyping = true;
1019
+
1020
+ // 添加机器人正在输入的指示
1021
+ const typingIndicator = addTypingIndicator();
1022
+
1023
+ try {
1024
+ // 发送请求
1025
+ const response = await fetch(`/api/student/chat/${agentId}`, {
1026
+ method: 'POST',
1027
+ headers: {
1028
+ 'Content-Type': 'application/json'
1029
+ },
1030
+ body: JSON.stringify({
1031
+ message: message,
1032
+ token: token
1033
+ })
1034
+ });
1035
+
1036
+ const data = await response.json();
1037
+
1038
+ // 移除输入指示器
1039
+ if (typingIndicator) {
1040
+ typingIndicator.remove();
1041
+ }
1042
+
1043
+ if (data.success) {
1044
+ // 处理回复内容,移除参考来源部分
1045
+ const processedContent = processResponseContent(data.message);
1046
+
1047
+ // 添加机器人回复(带打字机效果)
1048
+ const messageElement = addMessage(processedContent, false);
1049
+
1050
+ // 添加参考信息(如果有)
1051
+ if (data.references && data.references.length > 0) {
1052
+ // 等待打字机效果完成后添加参考信息
1053
+ const checkTypewriterComplete = setInterval(() => {
1054
+ if (!typewriterInterval) {
1055
+ clearInterval(checkTypewriterComplete);
1056
+ addReferences(messageElement, data.references);
1057
+ }
1058
+ }, 100);
1059
+ }
1060
+
1061
+ // 检查是否需要显示工具
1062
+ if (data.tools && data.tools.length > 0) {
1063
+ // 隐藏所有插件
1064
+ hideAllPlugins();
1065
+
1066
+ // 使用新的插件激活函数
1067
+ activatePlugins(data.message, data.tools);
1068
+ }
1069
+ } else {
1070
+ // 添加错误消息
1071
+ addMessage(`错误: ${data.message}`, false);
1072
+ }
1073
+ } catch (error) {
1074
+ console.error('发送请求出错:', error);
1075
+ // 移除输入指示器
1076
+ if (typingIndicator) {
1077
+ typingIndicator.remove();
1078
+ }
1079
+ addMessage('发送请求时出错,请重试', false);
1080
+ } finally {
1081
+ // 恢复发送按钮
1082
+ sendButton.disabled = false;
1083
+ isTyping = false;
1084
+ }
1085
+ }
1086
+
1087
+ // 添加打字指示器
1088
+ function addTypingIndicator() {
1089
+ const botMessage = document.createElement('div');
1090
+ botMessage.className = 'message bot-message';
1091
+
1092
+ const content = document.createElement('div');
1093
+ content.className = 'message-content';
1094
+ content.innerHTML = `
1095
+ <p>
1096
+ <span class="typing-indicator">
1097
+ <span class="typing-dot"></span>
1098
+ <span class="typing-dot"></span>
1099
+ <span class="typing-dot"></span>
1100
+ </span>
1101
+ </p>
1102
+ `;
1103
+
1104
+ botMessage.appendChild(content);
1105
+ chatMessages.appendChild(botMessage);
1106
+
1107
+ // 滚动到底部
1108
+ chatMessages.scrollTop = chatMessages.scrollHeight;
1109
+
1110
+ return botMessage;
1111
+ }
1112
+
1113
+ // 处理回复内容,移除参考来源部分
1114
+ function processResponseContent(content) {
1115
+ // 移除 "===参考来源开始===" 到 "===参考来源结束===" 之间的内容
1116
+ return content.replace(/===参考来源开始===[\s\S]*?===参考来源结束===/, '');
1117
+ }
1118
+
1119
+ // 添加参考信息显示函数
1120
+ function addReferences(messageElement, references) {
1121
+ // 创建参考容器
1122
+ const referenceContainer = document.createElement('div');
1123
+ referenceContainer.className = 'reference-container';
1124
+
1125
+ // 创建参考切换按钮
1126
+ const toggleButton = document.createElement('div');
1127
+ toggleButton.className = 'reference-toggle';
1128
+ toggleButton.innerHTML = '<i class="bi bi-journal-text"></i> 参考来源';
1129
+
1130
+ // 创建参考内容
1131
+ const referenceContent = document.createElement('div');
1132
+ referenceContent.className = 'reference-content';
1133
+
1134
+ // 添加参考项
1135
+ references.forEach(ref => {
1136
+ const refItem = document.createElement('div');
1137
+ refItem.className = 'reference-item';
1138
+
1139
+ refItem.innerHTML = `
1140
+ <div><strong>[${ref.index}]</strong> ${ref.summary}</div>
1141
+ <div class="reference-item-source">来源: ${ref.file_name}</div>
1142
+ `;
1143
+
1144
+ referenceContent.appendChild(refItem);
1145
+ });
1146
+
1147
+ // 添加切换功能
1148
+ toggleButton.addEventListener('click', () => {
1149
+ if (referenceContent.style.display === 'block') {
1150
+ referenceContent.style.display = 'none';
1151
+ toggleButton.innerHTML = '<i class="bi bi-journal-text"></i> 参考来源';
1152
+ } else {
1153
+ referenceContent.style.display = 'block';
1154
+ toggleButton.innerHTML = '<i class="bi bi-journal-arrow-up"></i> 收起参考来源';
1155
+ }
1156
+ });
1157
+
1158
+ // 组装并添加到消息
1159
+ referenceContainer.appendChild(toggleButton);
1160
+ referenceContainer.appendChild(referenceContent);
1161
+ messageElement.appendChild(referenceContainer);
1162
+ }
1163
+
1164
+ // 添加消息函数
1165
+ function addMessage(content, isUser) {
1166
+ const messageDiv = document.createElement('div');
1167
+ messageDiv.className = isUser ? 'message user-message' : 'message bot-message';
1168
+
1169
+ const messageContent = document.createElement('div');
1170
+ messageContent.className = 'message-content';
1171
+
1172
+ if (isUser) {
1173
+ // 直接显示用户消息,仅替换换行符
1174
+ messageContent.innerHTML = `<p>${content.replace(/\n/g, '<br>')}</p>`;
1175
+ } else {
1176
+ // 为AI消息添加打字机效果
1177
+ messageContent.innerHTML = '<p></p>';
1178
+ // 添加控制按钮
1179
+ const controlsDiv = document.createElement('div');
1180
+ controlsDiv.className = 'typewriter-controls';
1181
+ controlsDiv.innerHTML = `
1182
+ <button class="typewriter-btn pause-btn" title="暂停"><i class="bi bi-pause-fill"></i></button>
1183
+ <button class="typewriter-btn continue-btn" style="display:none;" title="继续"><i class="bi bi-play-fill"></i></button>
1184
+ <button class="typewriter-btn speed-btn" title="加速"><i class="bi bi-lightning-fill"></i></button>
1185
+ `;
1186
+ messageContent.appendChild(controlsDiv);
1187
+
1188
+ // 添加事件监听器
1189
+ const pauseBtn = controlsDiv.querySelector('.pause-btn');
1190
+ const continueBtn = controlsDiv.querySelector('.continue-btn');
1191
+ const speedBtn = controlsDiv.querySelector('.speed-btn');
1192
+
1193
+ pauseBtn.addEventListener('click', () => {
1194
+ pauseTypewriter();
1195
+ pauseBtn.style.display = 'none';
1196
+ continueBtn.style.display = 'inline-block';
1197
+ });
1198
+
1199
+ continueBtn.addEventListener('click', () => {
1200
+ continueTypewriter();
1201
+ continueBtn.style.display = 'none';
1202
+ pauseBtn.style.display = 'inline-block';
1203
+ });
1204
+
1205
+ speedBtn.addEventListener('click', () => {
1206
+ toggleTypewriterSpeed();
1207
+ });
1208
+
1209
+ // 开始打字机效果
1210
+ startTypewriter(content, messageContent.querySelector('p'));
1211
+ }
1212
+
1213
+ messageDiv.appendChild(messageContent);
1214
+ chatMessages.appendChild(messageDiv);
1215
+
1216
+ // 滚动到底部
1217
+ chatMessages.scrollTop = chatMessages.scrollHeight;
1218
+
1219
+ return messageDiv;
1220
+ }
1221
+
1222
+ // 打字机效果函数
1223
+ function startTypewriter(text, element) {
1224
+ // 清除之前的打字机效果(如果有)
1225
+ if (typewriterInterval) {
1226
+ clearInterval(typewriterInterval);
1227
+ }
1228
+
1229
+ // 保存当前消息元素引用
1230
+ currentMessageElement = element;
1231
+
1232
+ // 初始化
1233
+ typewriterPaused = false;
1234
+ let mdContent = md.render(text);
1235
+
1236
+ // 将HTML内容转换为文本节点和元素节点的队列
1237
+ parseHTMLToQueue(mdContent);
1238
+
1239
+ // 开始逐字显示
1240
+ typewriterInterval = setInterval(typeNextChar, typewriterSpeed);
1241
+ }
1242
+
1243
+ // 解析HTML到队列
1244
+ function parseHTMLToQueue(html) {
1245
+ // 清空队列
1246
+ currentMessageQueue = [];
1247
+
1248
+ // 创建临时容器
1249
+ const tempDiv = document.createElement('div');
1250
+ tempDiv.innerHTML = html;
1251
+
1252
+ // 递归处理所有子节点
1253
+ processNode(tempDiv);
1254
+ }
1255
+
1256
+ // 递归处理节点
1257
+ function processNode(node) {
1258
+ // 遍历节点的所有子节点
1259
+ for (let i = 0; i < node.childNodes.length; i++) {
1260
+ const child = node.childNodes[i];
1261
+
1262
+ if (child.nodeType === Node.TEXT_NODE) {
1263
+ // 文本节点,将每个字符加入队列
1264
+ for (let j = 0; j < child.textContent.length; j++) {
1265
+ currentMessageQueue.push({
1266
+ type: 'text',
1267
+ content: child.textContent[j]
1268
+ });
1269
+ }
1270
+ } else if (child.nodeType === Node.ELEMENT_NODE) {
1271
+ // 元素节点,将开始标签加入队列
1272
+ currentMessageQueue.push({
1273
+ type: 'element-start',
1274
+ tagName: child.tagName.toLowerCase(),
1275
+ attributes: Array.from(child.attributes)
1276
+ });
1277
+
1278
+ // 递归处理子节点
1279
+ processNode(child);
1280
+
1281
+ // 将结束标签加入队列
1282
+ currentMessageQueue.push({
1283
+ type: 'element-end',
1284
+ tagName: child.tagName.toLowerCase()
1285
+ });
1286
+ }
1287
+ }
1288
+ }
1289
+
1290
+ // 显示下一个字符
1291
+ function typeNextChar() {
1292
+ if (typewriterPaused || currentMessageQueue.length === 0 || !currentMessageElement) {
1293
+ return;
1294
+ }
1295
+
1296
+ let item = currentMessageQueue.shift();
1297
+
1298
+ if (item.type === 'text') {
1299
+ // 处理文本
1300
+ currentMessageElement.innerHTML += item.content;
1301
+ } else if (item.type === 'element-start') {
1302
+ // 处理元素开始标签
1303
+ const el = document.createElement(item.tagName);
1304
+ if (item.attributes) {
1305
+ item.attributes.forEach(attr => {
1306
+ el.setAttribute(attr.name, attr.value);
1307
+ });
1308
+ }
1309
+ currentMessageElement.appendChild(el);
1310
+ // 更新当前元素引用
1311
+ currentMessageElement = el;
1312
+ } else if (item.type === 'element-end') {
1313
+ // 处理元素结束标签
1314
+ // 将当前元素引用更新为父元素
1315
+ if (currentMessageElement.parentElement) {
1316
+ currentMessageElement = currentMessageElement.parentElement;
1317
+ }
1318
+ }
1319
+
1320
+ // 滚动到底部
1321
+ chatMessages.scrollTop = chatMessages.scrollHeight;
1322
+
1323
+ // 如果队列为空,需要渲染数学公式
1324
+ if (currentMessageQueue.length === 0) {
1325
+ clearInterval(typewriterInterval);
1326
+ typewriterInterval = null;
1327
+
1328
+ // 找到最近添加的机器人消息
1329
+ const botMessage = chatMessages.querySelector('.bot-message:last-child');
1330
+ if (botMessage) {
1331
+ try {
1332
+ renderMathInElement(botMessage.querySelector('.message-content'), {
1333
+ delimiters: [
1334
+ {left: '$$', right: '$$', display: true},
1335
+ {left: '$', right: '$', display: false},
1336
+ {left: '\\(', right: '\\)', display: false},
1337
+ {left: '\\[', right: '\\]', display: true}
1338
+ ],
1339
+ throwOnError: false
1340
+ });
1341
+ } catch (e) {
1342
+ console.error('渲染LaTeX出错:', e);
1343
+ }
1344
+
1345
+ // 为代码块添加复制按钮
1346
+ botMessage.querySelectorAll('pre').forEach(pre => {
1347
+ if (!pre.querySelector('.copy-button')) {
1348
+ const copyButton = document.createElement('button');
1349
+ copyButton.className = 'copy-button';
1350
+ copyButton.innerHTML = '<i class="bi bi-clipboard"></i> 复制';
1351
+ copyButton.onclick = function() { copyToClipboard(this); };
1352
+ pre.appendChild(copyButton);
1353
+ }
1354
+ });
1355
+ }
1356
+ }
1357
+ }
1358
+
1359
+ // 暂停打字机效果
1360
+ function pauseTypewriter() {
1361
+ typewriterPaused = true;
1362
+ }
1363
+
1364
+ // 继续打字机效果
1365
+ function continueTypewriter() {
1366
+ typewriterPaused = false;
1367
+
1368
+ // 如果之前的定时器已清除,重新启动
1369
+ if (!typewriterInterval && currentMessageQueue.length > 0) {
1370
+ typewriterInterval = setInterval(typeNextChar, typewriterSpeed);
1371
+ }
1372
+ }
1373
+
1374
+ // 切换打字速度
1375
+ function toggleTypewriterSpeed() {
1376
+ if (typewriterSpeed === 20) {
1377
+ typewriterSpeed = 5; // 快速
1378
+ } else {
1379
+ typewriterSpeed = 20; // 正常速度
1380
+ }
1381
+
1382
+ // 如果正在运行,更新速度
1383
+ if (typewriterInterval) {
1384
+ clearInterval(typewriterInterval);
1385
+ typewriterInterval = setInterval(typeNextChar, typewriterSpeed);
1386
+ }
1387
+ }
1388
+
1389
+ // 复制代码到剪贴板
1390
+ function copyToClipboard(button) {
1391
+ const codeBlock = button.parentNode.querySelector('code');
1392
+ const text = codeBlock.textContent;
1393
+
1394
+ navigator.clipboard.writeText(text).then(() => {
1395
+ button.innerHTML = '<i class="bi bi-check"></i> 已复制';
1396
+
1397
+ setTimeout(() => {
1398
+ button.innerHTML = '<i class="bi bi-clipboard"></i> 复制';
1399
+ }, 2000);
1400
+ }).catch(err => {
1401
+ console.error('复制失败:', err);
1402
+ button.innerHTML = '<i class="bi bi-exclamation-triangle"></i> 失败';
1403
+
1404
+ setTimeout(() => {
1405
+ button.innerHTML = '<i class="bi bi-clipboard"></i> 复制';
1406
+ }, 2000);
1407
+ });
1408
+ }
1409
+
1410
+ // 隐藏所有插件
1411
+ function hideAllPlugins() {
1412
+ codePlugin.style.display = 'none';
1413
+ visualizationPlugin.style.display = 'none';
1414
+ mindmapPlugin.style.display = 'none';
1415
+ updateMainContainerLayout();
1416
+ }
1417
+
1418
+ // 提取代码块
1419
+ function extractCodeBlocks(message) {
1420
+ const codeBlocks = [];
1421
+ const codeRegex = /```python\n([\s\S]*?)\n```/g;
1422
+
1423
+ let match;
1424
+ while ((match = codeRegex.exec(message)) !== null) {
1425
+ codeBlocks.push(match[1]);
1426
+ }
1427
+
1428
+ return codeBlocks;
1429
+ }
1430
+
1431
+ // 提取3D可视化函数代码
1432
+ function extract3DVisualizationCode(message) {
1433
+ // 尝试提取函数代码块
1434
+ const codeRegex = /```python\s*(import[\s\S]*?def create_3d_plot\(\):[\s\S]*?return[\s\S]*?})\s*```/i;
1435
+ const match = codeRegex.exec(message);
1436
+
1437
+ if (match) {
1438
+ return match[1].trim();
1439
+ }
1440
+
1441
+ return null;
1442
+ }
1443
+
1444
+ // 提取思维导图内容
1445
+ function extractMindmapContent(message) {
1446
+ // 尝试提取@startmindmap和@endmindmap之间的内容
1447
+ const mindmapRegex = /@startmindmap\n([\s\S]*?)@endmindmap/;
1448
+ const match = mindmapRegex.exec(message);
1449
+
1450
+ if (match) {
1451
+ return `@startmindmap\n${match[1]}\n@endmindmap`;
1452
+ }
1453
+
1454
+ return null;
1455
+ }
1456
+
1457
+ // 激活代码执行插件
1458
+ function activateCodePlugin(message) {
1459
+ // 提取代码块
1460
+ const codeBlocks = extractCodeBlocks(message);
1461
+
1462
+ if (codeBlocks.length > 0) {
1463
+ const isAlreadyVisible = codePlugin.style.display === 'flex';
1464
+
1465
+ // 显示插件容器
1466
+ codePlugin.style.display = 'flex';
1467
+ updateMainContainerLayout();
1468
+
1469
+ // 获取iframe
1470
+ const iframe = document.getElementById('code-execution-frame');
1471
+
1472
+ // 如果插件已经可见且iframe已加载
1473
+ if (isAlreadyVisible && iframe.contentWindow) {
1474
+ // 发送新代码到现有iframe
1475
+ iframe.contentWindow.postMessage({
1476
+ type: 'setCode',
1477
+ code: codeBlocks[0]
1478
+ }, '*');
1479
+ } else {
1480
+ // 设置iframe源
1481
+ let src = '/code_execution.html';
1482
+ if (codeBlocks.length > 0) {
1483
+ src += `?code=${encodeURIComponent(codeBlocks[0])}`;
1484
+ }
1485
+
1486
+ iframe.src = src;
1487
+
1488
+ // 设置iframe加载事件处理程序
1489
+ iframe.onload = function() {
1490
+ if (codeBlocks.length > 0) {
1491
+ iframe.contentWindow.postMessage({
1492
+ type: 'setCode',
1493
+ code: codeBlocks[0]
1494
+ }, '*');
1495
+ }
1496
+ };
1497
+ }
1498
+ }
1499
+ }
1500
+
1501
+ // 激活3D可视化插件
1502
+ function activate3DVisualization(message) {
1503
+ const code = extract3DVisualizationCode(message);
1504
+
1505
+ if (code) {
1506
+ // 显示插件容器
1507
+ visualizationPlugin.style.display = 'flex';
1508
+ updateMainContainerLayout();
1509
+
1510
+ // 显示加载状态
1511
+ visualizationResult.innerHTML = `
1512
+ <div class="text-center py-4">
1513
+ <div class="spinner-border" style="color: var(--secondary-color);" role="status">
1514
+ <span class="visually-hidden">生成中...</span>
1515
+ </div>
1516
+ <p class="mt-3">正在生成3D图形,请稍候...</p>
1517
+ </div>
1518
+ `;
1519
+
1520
+ // 自动生成可视化
1521
+ fetch('/api/visualization/3d-surface', {
1522
+ method: 'POST',
1523
+ headers: {
1524
+ 'Content-Type': 'application/json'
1525
+ },
1526
+ body: JSON.stringify({ code: code })
1527
+ })
1528
+ .then(response => response.json())
1529
+ .then(data => {
1530
+ if (data.success) {
1531
+ // 直接嵌入HTML
1532
+ visualizationResult.innerHTML = `
1533
+ <div class="visualization-iframe-container" style="width:100%; height:500px;">
1534
+ <iframe src="${data.html_url}" style="width:100%; height:100%; border:none;"></iframe>
1535
+ </div>
1536
+ <div class="text-center mt-3">
1537
+ <p>3D图形生成成功</p>
1538
+ <a href="${data.html_url}" class="btn btn-sm btn-outline-primary" target="_blank">
1539
+ <i class="bi bi-arrows-fullscreen"></i> 全屏查看
1540
+ </a>
1541
+ </div>
1542
+ `;
1543
+ } else {
1544
+ visualizationResult.innerHTML = `
1545
+ <div class="alert alert-danger">
1546
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>
1547
+ 生成失败: ${data.message}
1548
+ </div>
1549
+ `;
1550
+ }
1551
+ })
1552
+ .catch(error => {
1553
+ console.error('生成3D图形出错:', error);
1554
+ visualizationResult.innerHTML = `
1555
+ <div class="alert alert-danger">
1556
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>
1557
+ 生成3D图形时发生错误,请重试
1558
+ </div>
1559
+ `;
1560
+ });
1561
+ }
1562
+ }
1563
+
1564
+ // 激活思维导图插件
1565
+ function activateMindmap(message) {
1566
+ const content = extractMindmapContent(message);
1567
+
1568
+ if (content) {
1569
+ // 显示插件容器
1570
+ mindmapPlugin.style.display = 'flex';
1571
+ updateMainContainerLayout();
1572
+
1573
+ // 显示加载状态
1574
+ mindmapResult.innerHTML = `
1575
+ <div class="text-center py-4">
1576
+ <div class="spinner-border" style="color: var(--secondary-color);" role="status">
1577
+ <span class="visually-hidden">生成中...</span>
1578
+ </div>
1579
+ <p class="mt-3">正在生成思维导图,请稍候...</p>
1580
+ </div>
1581
+ `;
1582
+
1583
+ // 自动生成思维导图
1584
+ fetch('/api/visualization/mindmap', {
1585
+ method: 'POST',
1586
+ headers: {
1587
+ 'Content-Type': 'application/json'
1588
+ },
1589
+ body: JSON.stringify({ content: content })
1590
+ })
1591
+ .then(response => response.json())
1592
+ .then(data => {
1593
+ if (data.success) {
1594
+ mindmapResult.innerHTML = `
1595
+ <div class="text-center">
1596
+ <img src="${data.url}" class="mindmap-image" alt="思维导图">
1597
+ <p class="mt-3">生成成功!</p>
1598
+ <a href="${data.url}" class="btn btn-sm btn-outline-primary mt-2" target="_blank">
1599
+ <i class="bi bi-download"></i> 下载图片
1600
+ </a>
1601
+ </div>
1602
+ `;
1603
+ } else {
1604
+ mindmapResult.innerHTML = `
1605
+ <div class="alert alert-danger">
1606
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>
1607
+ 生成失败: ${data.message}
1608
+ </div>
1609
+ `;
1610
+ }
1611
+ })
1612
+ .catch(error => {
1613
+ console.error('生成思维导图出错:', error);
1614
+ mindmapResult.innerHTML = `
1615
+ <div class="alert alert-danger">
1616
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>
1617
+ 生成思维导图时发生错误,请重试
1618
+ </div>
1619
+ `;
1620
+ });
1621
+ }
1622
+ }
1623
+
1624
+ // 根据消息内容激活适当的插件
1625
+ function activatePlugins(message, tools) {
1626
+ // 检查并激活代码执行插件
1627
+ if (tools.includes('code') && message.includes('```python')) {
1628
+ activateCodePlugin(message);
1629
+ }
1630
+
1631
+ // 检查并激活3D可视化插件
1632
+ if (tools.includes('visualization') &&
1633
+ (message.includes('def create_3d_plot') ||
1634
+ message.includes('3D') || message.includes('可视化'))) {
1635
+ activate3DVisualization(message);
1636
+ }
1637
+
1638
+ // 检查并激活思维导图插件
1639
+ if (tools.includes('mindmap') &&
1640
+ (message.includes('@startmindmap') ||
1641
+ message.includes('思维导图'))) {
1642
+ activateMindmap(message);
1643
+ }
1644
+ }
1645
+ </script>
1646
+
1647
+ <!-- 添加highlight.js用于代码高亮 -->
1648
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
1649
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/languages/python.min.js"></script>
1650
+ </body>
1651
+ </html>
_archive/templates/student_portal.html ADDED
@@ -0,0 +1,1004 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>学生端门户 - 教育AI助手平台</title>
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
9
+ <style>
10
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
11
+
12
+ :root {
13
+ /* 优雅的配色方案 */
14
+ --primary-color: #0f2d49;
15
+ --primary-light: #234a70;
16
+ --secondary-color: #4a6cfd;
17
+ --secondary-light: #7b91ff;
18
+ --tertiary-color: #f7f9fe;
19
+ --success-color: #10b981;
20
+ --success-light: rgba(16, 185, 129, 0.1);
21
+ --warning-color: #f59e0b;
22
+ --warning-light: rgba(245, 158, 11, 0.1);
23
+ --info-color: #0ea5e9;
24
+ --info-light: rgba(14, 165, 233, 0.1);
25
+ --danger-color: #ef4444;
26
+ --danger-light: rgba(239, 68, 68, 0.1);
27
+ --neutral-50: #f9fafb;
28
+ --neutral-100: #f3f4f6;
29
+ --neutral-200: #e5e7eb;
30
+ --neutral-300: #d1d5db;
31
+ --neutral-400: #9ca3af;
32
+ --neutral-500: #6b7280;
33
+ --neutral-600: #4b5563;
34
+ --neutral-700: #374151;
35
+ --neutral-800: #1f2937;
36
+ --neutral-900: #111827;
37
+
38
+ /* 样式变量 */
39
+ --border-radius-sm: 0.25rem;
40
+ --border-radius: 0.375rem;
41
+ --border-radius-lg: 0.5rem;
42
+ --border-radius-xl: 0.75rem;
43
+ --border-radius-2xl: 1rem;
44
+ --card-shadow: 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 2px rgba(0, 0, 0, 0.1);
45
+ --card-shadow-hover: 0 10px 20px rgba(0, 0, 0, 0.05), 0 6px 6px rgba(0, 0, 0, 0.1);
46
+ --card-shadow-lg: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
47
+ --transition-base: all 0.2s ease-in-out;
48
+ --transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
49
+ --font-family: 'Inter', 'PingFang SC', 'Helvetica Neue', 'Microsoft YaHei', sans-serif;
50
+ }
51
+
52
+ /* 基础样式 */
53
+ body {
54
+ font-family: var(--font-family);
55
+ background-color: var(--neutral-50);
56
+ color: var(--neutral-800);
57
+ margin: 0;
58
+ padding: 0;
59
+ min-height: 100vh;
60
+ display: flex;
61
+ flex-direction: column;
62
+ -webkit-font-smoothing: antialiased;
63
+ -moz-osx-font-smoothing: grayscale;
64
+ }
65
+
66
+ h1, h2, h3, h4, h5, h6 {
67
+ font-weight: 600;
68
+ color: var(--neutral-900);
69
+ }
70
+
71
+ .text-gradient {
72
+ background: linear-gradient(135deg, var(--secondary-color), var(--secondary-light));
73
+ -webkit-background-clip: text;
74
+ -webkit-text-fill-color: transparent;
75
+ background-clip: text;
76
+ color: transparent;
77
+ }
78
+
79
+ /* 滚动条样式 */
80
+ ::-webkit-scrollbar {
81
+ width: 6px;
82
+ height: 6px;
83
+ }
84
+
85
+ ::-webkit-scrollbar-track {
86
+ background: var(--neutral-100);
87
+ border-radius: 10px;
88
+ }
89
+
90
+ ::-webkit-scrollbar-thumb {
91
+ background: var(--neutral-300);
92
+ border-radius: 10px;
93
+ }
94
+
95
+ ::-webkit-scrollbar-thumb:hover {
96
+ background: var(--neutral-400);
97
+ }
98
+
99
+ /* 头部样式 */
100
+ .header {
101
+ background-color: white;
102
+ border-bottom: 1px solid var(--neutral-200);
103
+ padding: 1rem 1.5rem;
104
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
105
+ position: sticky;
106
+ top: 0;
107
+ z-index: 100;
108
+ }
109
+
110
+ .header-content {
111
+ max-width: 1400px;
112
+ margin: 0 auto;
113
+ display: flex;
114
+ justify-content: space-between;
115
+ align-items: center;
116
+ }
117
+
118
+ .header h1 {
119
+ margin: 0;
120
+ font-size: 1.5rem;
121
+ font-weight: 700;
122
+ background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));
123
+ -webkit-background-clip: text;
124
+ -webkit-text-fill-color: transparent;
125
+ letter-spacing: -0.01em;
126
+ }
127
+
128
+ .user-info {
129
+ display: flex;
130
+ align-items: center;
131
+ gap: 0.75rem;
132
+ }
133
+
134
+ .user-info .badge {
135
+ font-size: 0.85rem;
136
+ padding: 0.4rem 0.75rem;
137
+ border-radius: var(--border-radius-lg);
138
+ background: linear-gradient(to right, var(--secondary-color), var(--secondary-light));
139
+ color: white;
140
+ font-weight: 500;
141
+ }
142
+
143
+ .user-info button {
144
+ color: var(--neutral-600);
145
+ text-decoration: none;
146
+ font-size: 0.9rem;
147
+ transition: var(--transition-base);
148
+ }
149
+
150
+ .user-info button:hover {
151
+ color: var(--secondary-color);
152
+ }
153
+
154
+ /* 主容器样式 */
155
+ .main-container {
156
+ flex: 1;
157
+ max-width: 1400px;
158
+ margin: 2rem auto;
159
+ padding: 0 1.5rem;
160
+ width: 100%;
161
+ box-sizing: border-box;
162
+ }
163
+
164
+ /* 欢迎消息样式 */
165
+ .welcome-message {
166
+ background-color: white;
167
+ border-radius: var(--border-radius-xl);
168
+ box-shadow: var(--card-shadow);
169
+ padding: 2rem;
170
+ margin-bottom: 2.5rem;
171
+ position: relative;
172
+ overflow: hidden;
173
+ border: none;
174
+ transition: var(--transition-smooth);
175
+ }
176
+
177
+ .welcome-message:hover {
178
+ box-shadow: var(--card-shadow-hover);
179
+ transform: translateY(-3px);
180
+ }
181
+
182
+ .welcome-message::before {
183
+ content: '';
184
+ position: absolute;
185
+ top: 0;
186
+ bottom: 0;
187
+ left: 0;
188
+ width: 4px;
189
+ background: linear-gradient(to bottom, var(--info-color), var(--secondary-light));
190
+ border-radius: 4px 0 0 4px;
191
+ }
192
+
193
+ .welcome-message h2 {
194
+ margin-top: 0;
195
+ margin-bottom: 1rem;
196
+ font-size: 1.6rem;
197
+ font-weight: 700;
198
+ color: var(--primary-color);
199
+ }
200
+
201
+ .welcome-message p {
202
+ color: var(--neutral-600);
203
+ margin-bottom: 0.75rem;
204
+ font-size: 0.95rem;
205
+ line-height: 1.5;
206
+ }
207
+
208
+ .welcome-message p:last-child {
209
+ margin-bottom: 0;
210
+ }
211
+
212
+ /* 令牌输入样式 */
213
+ .token-input {
214
+ background-color: white;
215
+ border-radius: var(--border-radius-xl);
216
+ box-shadow: var(--card-shadow);
217
+ padding: 2rem;
218
+ margin-bottom: 2.5rem;
219
+ position: relative;
220
+ overflow: hidden;
221
+ transition: var(--transition-smooth);
222
+ }
223
+
224
+ .token-input:hover {
225
+ box-shadow: var(--card-shadow-hover);
226
+ transform: translateY(-3px);
227
+ }
228
+
229
+ .token-input::before {
230
+ content: '';
231
+ position: absolute;
232
+ top: 0;
233
+ bottom: 0;
234
+ left: 0;
235
+ width: 4px;
236
+ background: linear-gradient(to bottom, var(--warning-color), var(--secondary-light));
237
+ border-radius: 4px 0 0 4px;
238
+ }
239
+
240
+ .token-input h3 {
241
+ margin-top: 0;
242
+ margin-bottom: 1rem;
243
+ font-size: 1.35rem;
244
+ font-weight: 600;
245
+ color: var(--neutral-900);
246
+ }
247
+
248
+ .token-input p {
249
+ color: var(--neutral-600);
250
+ margin-bottom: 1.25rem;
251
+ }
252
+
253
+ .token-input .input-group {
254
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
255
+ border-radius: var(--border-radius-lg);
256
+ margin-bottom: 0;
257
+ }
258
+
259
+ .token-input .form-control {
260
+ border-radius: var(--border-radius-lg) 0 0 var(--border-radius-lg);
261
+ border: 1px solid var(--neutral-200);
262
+ padding: 0.75rem 1rem;
263
+ font-size: 0.95rem;
264
+ }
265
+
266
+ .token-input .form-control:focus {
267
+ box-shadow: none;
268
+ border-color: var(--secondary-color);
269
+ }
270
+
271
+ .token-input .btn {
272
+ background: linear-gradient(to right, var(--secondary-color), var(--secondary-light));
273
+ color: white;
274
+ border: none;
275
+ border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0;
276
+ padding: 0.75rem 1.25rem;
277
+ transition: var(--transition-base);
278
+ }
279
+
280
+ .token-input .btn:hover {
281
+ background: linear-gradient(to right, var(--secondary-light), var(--secondary-color));
282
+ box-shadow: 0 4px 10px rgba(74, 108, 253, 0.2);
283
+ }
284
+
285
+ /* 标题样式 */
286
+ .section-header {
287
+ margin-bottom: 1.5rem;
288
+ display: flex;
289
+ flex-direction: column;
290
+ }
291
+
292
+ .section-header h2 {
293
+ margin: 0;
294
+ font-size: 1.5rem;
295
+ font-weight: 700;
296
+ color: var(--primary-color);
297
+ margin-bottom: 0.5rem;
298
+ }
299
+
300
+ .section-header p {
301
+ color: var(--neutral-600);
302
+ margin: 0;
303
+ font-size: 0.95rem;
304
+ }
305
+
306
+ /* Agent卡片样式 */
307
+ .agent-card {
308
+ background-color: white;
309
+ border-radius: var(--border-radius-xl);
310
+ margin-bottom: 1.5rem;
311
+ box-shadow: var(--card-shadow);
312
+ overflow: hidden;
313
+ transition: var(--transition-smooth);
314
+ position: relative;
315
+ border: none;
316
+ }
317
+
318
+ .agent-card:hover {
319
+ transform: translateY(-5px);
320
+ box-shadow: var(--card-shadow-hover);
321
+ }
322
+
323
+ .agent-card::before {
324
+ content: '';
325
+ position: absolute;
326
+ top: 0;
327
+ bottom: 0;
328
+ left: 0;
329
+ width: 4px;
330
+ background: linear-gradient(to bottom, var(--secondary-color), var(--secondary-light));
331
+ border-radius: 4px 0 0 4px;
332
+ }
333
+
334
+ .agent-card-header {
335
+ padding: 1.5rem;
336
+ background-color: rgba(74, 108, 253, 0.03);
337
+ border-bottom: 1px solid var(--neutral-100);
338
+ }
339
+
340
+ .agent-card-title {
341
+ margin: 0;
342
+ font-size: 1.25rem;
343
+ font-weight: 600;
344
+ color: var(--primary-color);
345
+ display: flex;
346
+ align-items: center;
347
+ }
348
+
349
+ .agent-card-title i {
350
+ margin-right: 0.75rem;
351
+ font-size: 1.2rem;
352
+ color: var(--secondary-color);
353
+ }
354
+
355
+ .agent-card-content {
356
+ padding: 1.5rem;
357
+ }
358
+
359
+ .agent-card-description {
360
+ margin-bottom: 1.25rem;
361
+ color: var(--neutral-700);
362
+ font-size: 0.95rem;
363
+ line-height: 1.5;
364
+ }
365
+
366
+ .agent-meta {
367
+ display: flex;
368
+ flex-wrap: wrap;
369
+ margin-bottom: 1.25rem;
370
+ gap: 1.5rem;
371
+ }
372
+
373
+ .agent-meta-item {
374
+ display: flex;
375
+ align-items: center;
376
+ font-size: 0.9rem;
377
+ color: var(--neutral-600);
378
+ }
379
+
380
+ .agent-meta-item i {
381
+ margin-right: 0.5rem;
382
+ font-size: 0.95rem;
383
+ color: var(--secondary-color);
384
+ }
385
+
386
+ .agent-tags {
387
+ display: flex;
388
+ flex-wrap: wrap;
389
+ gap: 0.5rem;
390
+ margin-bottom: 1.5rem;
391
+ }
392
+
393
+ .agent-tag {
394
+ display: inline-flex;
395
+ align-items: center;
396
+ padding: 0.35em 0.75em;
397
+ background-color: rgba(74, 108, 253, 0.1);
398
+ color: var(--secondary-color);
399
+ border-radius: 20px;
400
+ font-size: 0.85rem;
401
+ font-weight: 500;
402
+ }
403
+
404
+ .agent-tag i {
405
+ margin-right: 0.4rem;
406
+ }
407
+
408
+ .agent-actions {
409
+ display: flex;
410
+ justify-content: flex-end;
411
+ }
412
+
413
+ .agent-actions .btn {
414
+ padding: 0.65em 1.25em;
415
+ font-weight: 500;
416
+ background: linear-gradient(to right, var(--secondary-color), var(--secondary-light));
417
+ border: none;
418
+ color: white;
419
+ border-radius: var(--border-radius-lg);
420
+ transition: var(--transition-base);
421
+ }
422
+
423
+ .agent-actions .btn:hover {
424
+ background: linear-gradient(to right, var(--secondary-light), var(--secondary-color));
425
+ transform: translateY(-2px);
426
+ box-shadow: 0 4px 10px rgba(74, 108, 253, 0.2);
427
+ }
428
+
429
+ .agent-actions .btn i {
430
+ margin-right: 0.5rem;
431
+ }
432
+
433
+ /* 活动记录样式 */
434
+ .recent-activity {
435
+ background-color: white;
436
+ border-radius: var(--border-radius-xl);
437
+ box-shadow: var(--card-shadow);
438
+ padding: 1.5rem;
439
+ margin-bottom: 2rem;
440
+ position: relative;
441
+ overflow: hidden;
442
+ transition: var(--transition-smooth);
443
+ }
444
+
445
+ .recent-activity:hover {
446
+ box-shadow: var(--card-shadow-hover);
447
+ transform: translateY(-3px);
448
+ }
449
+
450
+ .recent-activity::before {
451
+ content: '';
452
+ position: absolute;
453
+ top: 0;
454
+ bottom: 0;
455
+ left: 0;
456
+ width: 4px;
457
+ background: linear-gradient(to bottom, var(--success-color), var(--secondary-light));
458
+ border-radius: 4px 0 0 4px;
459
+ }
460
+
461
+ .recent-activity h3 {
462
+ margin-top: 0;
463
+ margin-bottom: 1.5rem;
464
+ font-size: 1.35rem;
465
+ font-weight: 600;
466
+ color: var(--neutral-900);
467
+ }
468
+
469
+ .activity-list {
470
+ list-style: none;
471
+ padding: 0;
472
+ margin: 0;
473
+ }
474
+
475
+ .activity-item {
476
+ display: flex;
477
+ padding: 1rem 0;
478
+ border-bottom: 1px solid var(--neutral-100);
479
+ align-items: center;
480
+ }
481
+
482
+ .activity-item:last-child {
483
+ border-bottom: none;
484
+ padding-bottom: 0;
485
+ }
486
+
487
+ .activity-icon {
488
+ width: 40px;
489
+ height: 40px;
490
+ border-radius: 50%;
491
+ background-color: rgba(74, 108, 253, 0.1);
492
+ display: flex;
493
+ align-items: center;
494
+ justify-content: center;
495
+ margin-right: 1rem;
496
+ flex-shrink: 0;
497
+ }
498
+
499
+ .activity-icon i {
500
+ color: var(--secondary-color);
501
+ font-size: 1.1rem;
502
+ }
503
+
504
+ .activity-content {
505
+ flex: 1;
506
+ }
507
+
508
+ .activity-title {
509
+ margin: 0;
510
+ margin-bottom: 0.35rem;
511
+ font-size: 0.95rem;
512
+ font-weight: 500;
513
+ color: var(--neutral-800);
514
+ }
515
+
516
+ .activity-time {
517
+ font-size: 0.85rem;
518
+ color: var(--neutral-500);
519
+ }
520
+
521
+ /* 动画效果 */
522
+ @keyframes fadeIn {
523
+ from { opacity: 0; transform: translateY(10px); }
524
+ to { opacity: 1; transform: translateY(0); }
525
+ }
526
+
527
+ .fade-in {
528
+ opacity: 0;
529
+ animation: fadeIn 0.4s ease-out forwards;
530
+ }
531
+
532
+ .animate-delay-1 { animation-delay: 0.1s; }
533
+ .animate-delay-2 { animation-delay: 0.2s; }
534
+ .animate-delay-3 { animation-delay: 0.3s; }
535
+ .animate-delay-4 { animation-delay: 0.4s; }
536
+ .animate-delay-5 { animation-delay: 0.5s; }
537
+
538
+ /* 响应式样式 */
539
+ @media (max-width: 992px) {
540
+ .main-container {
541
+ padding: 0 1rem;
542
+ }
543
+
544
+ .agent-meta {
545
+ flex-direction: column;
546
+ gap: 0.75rem;
547
+ }
548
+ }
549
+
550
+ @media (max-width: 768px) {
551
+ .header h1 {
552
+ font-size: 1.25rem;
553
+ }
554
+
555
+ .welcome-message h2 {
556
+ font-size: 1.35rem;
557
+ }
558
+
559
+ .agent-card-title {
560
+ font-size: 1.1rem;
561
+ }
562
+
563
+ .section-header h2 {
564
+ font-size: 1.3rem;
565
+ }
566
+
567
+ .recent-activity h3,
568
+ .token-input h3 {
569
+ font-size: 1.2rem;
570
+ }
571
+ }
572
+ </style>
573
+ </head>
574
+ <body>
575
+ <header class="header">
576
+ <div class="header-content">
577
+ <h1>教育AI助手平台 - 学生端</h1>
578
+ <div class="user-info">
579
+ <span class="badge" id="user-name">张三</span>
580
+ <button class="btn btn-sm btn-link text-decoration-none" id="logout-btn">登出</button>
581
+ </div>
582
+ </div>
583
+ </header>
584
+
585
+ <div class="main-container">
586
+ <div class="welcome-message fade-in">
587
+ <h2>欢迎回来,<span id="welcome-name">张三</span>!</h2>
588
+ <p>教育AI助手平台为您提供智能学习辅助工具,帮助您更高效地学习和解决问题。</p>
589
+ <p>下方是您的教师分享给您的AI助手,点击"开始对话"与它们互动。</p>
590
+ </div>
591
+
592
+ <div class="token-input fade-in animate-delay-1">
593
+ <h3>访问新的AI助手</h3>
594
+ <p>输入教师分享的访问令牌,即可使用新的AI助手。</p>
595
+ <div class="input-group">
596
+ <input type="text" class="form-control" id="access-token" placeholder="输入访问令牌">
597
+ <button class="btn" id="access-button">
598
+ <i class="bi bi-arrow-right-circle me-1"></i> 访问
599
+ </button>
600
+ </div>
601
+ </div>
602
+
603
+ <div class="section-header fade-in animate-delay-2">
604
+ <h2>我的AI助手</h2>
605
+ <p>教师分享给您的智能学习助手</p>
606
+ </div>
607
+
608
+ <div class="row" id="agents-container">
609
+ <!-- 代码将在JavaScript中动态生成 -->
610
+ </div>
611
+
612
+ <div class="recent-activity fade-in animate-delay-4">
613
+ <h3>最近活动</h3>
614
+ <ul class="activity-list" id="activity-list">
615
+ <!-- 代码将在JavaScript中动态生成 -->
616
+ </ul>
617
+ </div>
618
+ </div>
619
+
620
+ <script>
621
+ // DOM元素
622
+ const userNameElement = document.getElementById('user-name');
623
+ const welcomeNameElement = document.getElementById('welcome-name');
624
+ const logoutBtn = document.getElementById('logout-btn');
625
+ const accessButton = document.getElementById('access-button');
626
+ const agentsContainer = document.getElementById('agents-container');
627
+ const activityList = document.getElementById('activity-list');
628
+
629
+ // 页面加载函数
630
+ document.addEventListener('DOMContentLoaded', function() {
631
+ // 检查登录状态
632
+ checkAuthStatus();
633
+
634
+ // 加载用户的AI助手列表
635
+ loadAgents();
636
+
637
+ // 初始化登出按钮
638
+ logoutBtn.addEventListener('click', logout);
639
+
640
+ // 初始化访问令牌按钮
641
+ accessButton.addEventListener('click', accessWithToken);
642
+ });
643
+
644
+ // 检查认证状态
645
+ async function checkAuthStatus() {
646
+ try {
647
+ const response = await fetch('/api/auth/check');
648
+ const data = await response.json();
649
+
650
+ if (!data.success) {
651
+ // 未登录,跳转到登录页面
652
+ window.location.href = '/login.html';
653
+ return;
654
+ }
655
+
656
+ // 更新用户信息显示
657
+ userNameElement.textContent = data.user.name;
658
+ welcomeNameElement.textContent = data.user.name;
659
+
660
+ // 如果不是学生,跳转到教师端
661
+ if (data.user.type !== 'student') {
662
+ window.location.href = '/index.html';
663
+ }
664
+ } catch (error) {
665
+ console.error('验证登录状态出错:', error);
666
+ window.location.href = '/login.html';
667
+ }
668
+ }
669
+
670
+ // 登出
671
+ async function logout() {
672
+ try {
673
+ const response = await fetch('/api/auth/logout', {
674
+ method: 'POST'
675
+ });
676
+
677
+ const data = await response.json();
678
+
679
+ if (data.success) {
680
+ window.location.href = '/login.html';
681
+ }
682
+ } catch (error) {
683
+ console.error('登出出错:', error);
684
+ alert('登出失败,请重试');
685
+ }
686
+ }
687
+
688
+ // 加载Agent列表
689
+ async function loadAgents() {
690
+ try {
691
+ // 显示加载状态
692
+ agentsContainer.innerHTML = `
693
+ <div class="col-12 text-center py-4">
694
+ <div class="spinner-border text-primary" role="status">
695
+ <span class="visually-hidden">加载中...</span>
696
+ </div>
697
+ <p class="mt-3">正在加载AI助手列表...</p>
698
+ </div>
699
+ `;
700
+
701
+ // 获取Agent列表
702
+ const response = await fetch('/api/student/agents');
703
+ const result = await response.json();
704
+
705
+ if (result.success) {
706
+ const agents = result.agents || [];
707
+
708
+ if (agents.length === 0) {
709
+ agentsContainer.innerHTML = `
710
+ <div class="col-12">
711
+ <div class="alert alert-info">
712
+ <i class="bi bi-info-circle me-2"></i>
713
+ 您还没有可用的AI助手。请向您的教师获取访问令牌。
714
+ </div>
715
+ </div>
716
+ `;
717
+
718
+ // 无Agent时,清空活动列表
719
+ activityList.innerHTML = `
720
+ <li class="text-center py-3 text-muted">
721
+ 暂无活动记录
722
+ </li>
723
+ `;
724
+
725
+ return;
726
+ }
727
+
728
+ // 渲染Agent列表
729
+ agentsContainer.innerHTML = '';
730
+
731
+ agents.forEach(agent => {
732
+ const col = document.createElement('div');
733
+ col.className = 'col-lg-6 col-12 fade-in';
734
+
735
+ // 格式化最后使用时间
736
+ let lastUsedText = '从未使用';
737
+ if (agent.last_used) {
738
+ const lastUsedDate = new Date(agent.last_used * 1000);
739
+ lastUsedText = lastUsedDate.toLocaleString();
740
+ }
741
+
742
+ // 构建插件标签
743
+ let pluginsHtml = '';
744
+ if (agent.plugins && agent.plugins.length > 0) {
745
+ pluginsHtml = '<div class="agent-tags">';
746
+
747
+ agent.plugins.forEach(plugin => {
748
+ let pluginName = '未知插件';
749
+ let pluginIcon = 'puzzle';
750
+
751
+ if (plugin === 'code') {
752
+ pluginName = '代码执行';
753
+ pluginIcon = 'code-square';
754
+ } else if (plugin === 'visualization') {
755
+ pluginName = '3D可视化';
756
+ pluginIcon = 'bar-chart';
757
+ } else if (plugin === 'mindmap') {
758
+ pluginName = '思维导图';
759
+ pluginIcon = 'diagram-3';
760
+ }
761
+
762
+ pluginsHtml += `
763
+ <span class="agent-tag">
764
+ <i class="bi bi-${pluginIcon}"></i> ${pluginName}
765
+ </span>
766
+ `;
767
+ });
768
+
769
+ pluginsHtml += '</div>';
770
+ }
771
+
772
+ // 构建主题和教师标签
773
+ let metaHtml = '<div class="agent-meta">';
774
+
775
+ if (agent.subject) {
776
+ metaHtml += `
777
+ <div class="agent-meta-item">
778
+ <i class="bi bi-book"></i> ${agent.subject}
779
+ </div>
780
+ `;
781
+ }
782
+
783
+ if (agent.instructor) {
784
+ metaHtml += `
785
+ <div class="agent-meta-item">
786
+ <i class="bi bi-person"></i> ${agent.instructor}
787
+ </div>
788
+ `;
789
+ }
790
+
791
+ metaHtml += `
792
+ <div class="agent-meta-item">
793
+ <i class="bi bi-clock"></i> 最后使用: ${lastUsedText}
794
+ </div>
795
+ </div>`;
796
+
797
+ // 构建Agent卡片
798
+ col.innerHTML = `
799
+ <div class="agent-card">
800
+ <div class="agent-card-header">
801
+ <h3 class="agent-card-title">
802
+ <i class="bi bi-robot"></i> ${agent.name}
803
+ </h3>
804
+ </div>
805
+ <div class="agent-card-content">
806
+ <div class="agent-card-description">
807
+ ${agent.description || '暂无描述'}
808
+ </div>
809
+ ${metaHtml}
810
+ ${pluginsHtml}
811
+ <div class="agent-actions">
812
+ <a href="/student/${agent.id}?token=${agent.token}" class="btn">
813
+ <i class="bi bi-chat-dots me-1"></i> 开始对话
814
+ </a>
815
+ </div>
816
+ </div>
817
+ </div>
818
+ `;
819
+
820
+ agentsContainer.appendChild(col);
821
+ });
822
+
823
+ // 生成简单的活动记录(实际中应从API获取)
824
+ loadActivityRecords();
825
+ } else {
826
+ agentsContainer.innerHTML = `
827
+ <div class="col-12">
828
+ <div class="alert alert-danger">
829
+ <i class="bi bi-exclamation-triangle me-2"></i>
830
+ 加载Agent列表失败: ${result.message}
831
+ </div>
832
+ </div>
833
+ `;
834
+
835
+ // 加载失败时,清空活动列表
836
+ activityList.innerHTML = `
837
+ <li class="text-center py-3 text-muted">
838
+ 暂无活动记录
839
+ </li>
840
+ `;
841
+ }
842
+ } catch (error) {
843
+ console.error('加载Agent列表出错:', error);
844
+ agentsContainer.innerHTML = `
845
+ <div class="col-12">
846
+ <div class="alert alert-danger">
847
+ <i class="bi bi-exclamation-triangle me-2"></i>
848
+ 加载Agent列表时发生错误,请刷新页面重试
849
+ </div>
850
+ </div>
851
+ `;
852
+ }
853
+ }
854
+
855
+ // 加载活动记录(实际中应从API获取)
856
+ // 加载活动记录
857
+ async function loadActivityRecords() {
858
+ try {
859
+ // 从API获取活动记录
860
+ const response = await fetch('/api/student/activities');
861
+ const result = await response.json();
862
+
863
+ if (result.success) {
864
+ const activities = result.activities || [];
865
+
866
+ if (activities.length === 0) {
867
+ activityList.innerHTML = `
868
+ <li class="text-center py-3 text-muted">
869
+ 暂无活动记录
870
+ </li>
871
+ `;
872
+ return;
873
+ }
874
+
875
+ // 渲染活动记录
876
+ activityList.innerHTML = '';
877
+
878
+ activities.forEach(activity => {
879
+ let iconClass = 'bi-activity';
880
+
881
+ switch (activity.type) {
882
+ case 'chat':
883
+ iconClass = 'bi-chat-dots';
884
+ break;
885
+ case 'code':
886
+ iconClass = 'bi-code-square';
887
+ break;
888
+ case 'viz':
889
+ iconClass = 'bi-bar-chart';
890
+ break;
891
+ case 'mindmap':
892
+ iconClass = 'bi-diagram-3';
893
+ break;
894
+ }
895
+
896
+ const li = document.createElement('li');
897
+ li.className = 'activity-item';
898
+
899
+ // 如果有agent_id和token,添加链接
900
+ let titleHtml = `<h4 class="activity-title">${activity.title}</h4>`;
901
+ if (activity.agent_id) {
902
+ // 查找对应Agent的token
903
+ const agent = agents.find(a => a.id === activity.agent_id);
904
+ if (agent && agent.token) {
905
+ titleHtml = `
906
+ <h4 class="activity-title">
907
+ <a href="/student/${activity.agent_id}?token=${agent.token}">${activity.title}</a>
908
+ </h4>
909
+ `;
910
+ }
911
+ }
912
+
913
+ li.innerHTML = `
914
+ <div class="activity-icon">
915
+ <i class="bi ${iconClass}"></i>
916
+ </div>
917
+ <div class="activity-content">
918
+ ${titleHtml}
919
+ <div class="activity-time">${activity.time}</div>
920
+ </div>
921
+ `;
922
+
923
+ activityList.appendChild(li);
924
+ });
925
+ } else {
926
+ activityList.innerHTML = `
927
+ <li class="text-center py-3 text-muted">
928
+ <i class="bi bi-exclamation-circle me-2"></i>
929
+ 无法加载活动记录
930
+ </li>
931
+ `;
932
+ }
933
+ } catch (error) {
934
+ console.error('加载活动记录出错:', error);
935
+ activityList.innerHTML = `
936
+ <li class="text-center py-3 text-muted">
937
+ <i class="bi bi-exclamation-circle me-2"></i>
938
+ 加载活动记录时出错
939
+ </li>
940
+ `;
941
+ }
942
+ }
943
+ // 使用令牌访问AI助手
944
+ async function accessWithToken() {
945
+ const token = document.getElementById('access-token').value.trim();
946
+
947
+ if (!token) {
948
+ showMessage('请输入访问令牌', 'warning');
949
+ return;
950
+ }
951
+
952
+ try {
953
+ // 验证令牌有效性
954
+ const response = await fetch('/api/verify_token', {
955
+ method: 'POST',
956
+ headers: {
957
+ 'Content-Type': 'application/json'
958
+ },
959
+ body: JSON.stringify({ token })
960
+ });
961
+
962
+ const result = await response.json();
963
+
964
+ if (result.success) {
965
+ // 令牌有效,重定向到Agent页面
966
+ const agent = result.agent;
967
+ window.location.href = `/student/${agent.id}?token=${token}`;
968
+ } else {
969
+ showMessage(result.message || '无效的访问令牌', 'danger');
970
+ }
971
+ } catch (error) {
972
+ console.error('验证���牌出错:', error);
973
+ showMessage('验证令牌时发生错误,请重试', 'danger');
974
+ }
975
+ }
976
+
977
+ // 显示提示消息
978
+ function showMessage(message, type) {
979
+ // 移除现有的提示
980
+ const existingAlert = document.querySelector('.token-input .alert');
981
+ if (existingAlert) {
982
+ existingAlert.remove();
983
+ }
984
+
985
+ // 创建新提示
986
+ const alertDiv = document.createElement('div');
987
+ alertDiv.className = `alert alert-${type} mt-3`;
988
+ alertDiv.innerHTML = `
989
+ <i class="bi ${type === 'danger' ? 'bi-exclamation-triangle' : 'bi-info-circle'} me-2"></i>
990
+ ${message}
991
+ `;
992
+
993
+ // 添加到令牌输入区域
994
+ document.querySelector('.token-input .input-group').insertAdjacentElement('afterend', alertDiv);
995
+
996
+ // 3秒后自动移除
997
+ setTimeout(() => {
998
+ alertDiv.style.opacity = '0';
999
+ setTimeout(() => alertDiv.remove(), 300);
1000
+ }, 3000);
1001
+ }
1002
+ </script>
1003
+ </body>
1004
+ </html>
_archive/templates/token_verification.html ADDED
@@ -0,0 +1,521 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>访问验证 - 教育AI助手平台</title>
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
9
+ <style>
10
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
11
+
12
+ :root {
13
+ /* 优雅的配色方案 */
14
+ --primary-color: #0f2d49;
15
+ --primary-light: #234a70;
16
+ --secondary-color: #4a6cfd;
17
+ --secondary-light: #7b91ff;
18
+ --tertiary-color: #f7f9fe;
19
+ --success-color: #10b981;
20
+ --success-light: rgba(16, 185, 129, 0.1);
21
+ --warning-color: #f59e0b;
22
+ --warning-light: rgba(245, 158, 11, 0.1);
23
+ --info-color: #0ea5e9;
24
+ --info-light: rgba(14, 165, 233, 0.1);
25
+ --danger-color: #ef4444;
26
+ --danger-light: rgba(239, 68, 68, 0.1);
27
+ --neutral-50: #f9fafb;
28
+ --neutral-100: #f3f4f6;
29
+ --neutral-200: #e5e7eb;
30
+ --neutral-300: #d1d5db;
31
+ --neutral-400: #9ca3af;
32
+ --neutral-500: #6b7280;
33
+ --neutral-600: #4b5563;
34
+ --neutral-700: #374151;
35
+ --neutral-800: #1f2937;
36
+ --neutral-900: #111827;
37
+
38
+ /* 样式变量 */
39
+ --border-radius-sm: 0.25rem;
40
+ --border-radius: 0.375rem;
41
+ --border-radius-lg: 0.5rem;
42
+ --border-radius-xl: 0.75rem;
43
+ --border-radius-2xl: 1rem;
44
+ --card-shadow: 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 2px rgba(0, 0, 0, 0.1);
45
+ --card-shadow-hover: 0 10px 20px rgba(0, 0, 0, 0.05), 0 6px 6px rgba(0, 0, 0, 0.1);
46
+ --card-shadow-lg: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
47
+ --transition-base: all 0.2s ease-in-out;
48
+ --transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
49
+ --font-family: 'Inter', 'PingFang SC', 'Helvetica Neue', 'Microsoft YaHei', sans-serif;
50
+ }
51
+
52
+ /* 基础样式 */
53
+ body {
54
+ font-family: var(--font-family);
55
+ background-color: var(--neutral-50);
56
+ color: var(--neutral-800);
57
+ margin: 0;
58
+ padding: 0;
59
+ min-height: 100vh;
60
+ display: flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ -webkit-font-smoothing: antialiased;
64
+ -moz-osx-font-smoothing: grayscale;
65
+ }
66
+
67
+ h1, h2, h3, h4, h5, h6 {
68
+ font-weight: 600;
69
+ color: var(--neutral-900);
70
+ }
71
+
72
+ .text-gradient {
73
+ background: linear-gradient(135deg, var(--secondary-color), var(--secondary-light));
74
+ -webkit-background-clip: text;
75
+ -webkit-text-fill-color: transparent;
76
+ background-clip: text;
77
+ color: transparent;
78
+ }
79
+
80
+ /* 验证容器样式 */
81
+ .verification-container {
82
+ width: 100%;
83
+ max-width: 500px;
84
+ padding: 2.5rem;
85
+ background-color: white;
86
+ border-radius: var(--border-radius-xl);
87
+ box-shadow: var(--card-shadow-lg);
88
+ transition: var(--transition-smooth);
89
+ position: relative;
90
+ overflow: hidden;
91
+ }
92
+
93
+ .verification-container:hover {
94
+ box-shadow: var(--card-shadow-hover);
95
+ }
96
+
97
+ .verification-container::before {
98
+ content: '';
99
+ position: absolute;
100
+ top: 0;
101
+ left: 0;
102
+ width: 100%;
103
+ height: 4px;
104
+ background: linear-gradient(to right, var(--secondary-color), var(--secondary-light));
105
+ border-radius: 4px 4px 0 0;
106
+ }
107
+
108
+ .verification-header {
109
+ text-align: center;
110
+ margin-bottom: 2rem;
111
+ }
112
+
113
+ .verification-header h1 {
114
+ font-size: 1.75rem;
115
+ font-weight: 700;
116
+ margin-bottom: 0.5rem;
117
+ background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));
118
+ -webkit-background-clip: text;
119
+ -webkit-text-fill-color: transparent;
120
+ letter-spacing: -0.01em;
121
+ }
122
+
123
+ .verification-header p {
124
+ color: var(--neutral-600);
125
+ margin-bottom: 0;
126
+ font-size: 0.95rem;
127
+ }
128
+
129
+ /* 加载状态 */
130
+ .loading-container {
131
+ text-align: center;
132
+ margin-bottom: 1.5rem;
133
+ }
134
+
135
+ .spinner-border {
136
+ width: 3rem;
137
+ height: 3rem;
138
+ color: var(--secondary-color);
139
+ margin-bottom: 1.5rem;
140
+ }
141
+
142
+ .loading-container h2 {
143
+ margin-bottom: 0.75rem;
144
+ font-size: 1.4rem;
145
+ color: var(--primary-color);
146
+ }
147
+
148
+ .loading-container p {
149
+ color: var(--neutral-600);
150
+ font-size: 0.95rem;
151
+ }
152
+
153
+ /* Agent信息卡片 */
154
+ .agent-info {
155
+ padding: 1.5rem;
156
+ background-color: var(--neutral-50);
157
+ border-radius: var(--border-radius-lg);
158
+ margin-bottom: 1.75rem;
159
+ border: 1px solid var(--neutral-200);
160
+ position: relative;
161
+ overflow: hidden;
162
+ transition: var(--transition-base);
163
+ }
164
+
165
+ .agent-info:hover {
166
+ border-color: var(--secondary-color);
167
+ box-shadow: 0 4px 10px rgba(74, 108, 253, 0.1);
168
+ }
169
+
170
+ .agent-info::before {
171
+ content: '';
172
+ position: absolute;
173
+ top: 0;
174
+ bottom: 0;
175
+ left: 0;
176
+ width: 4px;
177
+ background: linear-gradient(to bottom, var(--secondary-color), var(--secondary-light));
178
+ border-radius: 4px 0 0 4px;
179
+ }
180
+
181
+ .agent-title {
182
+ font-size: 1.25rem;
183
+ font-weight: 600;
184
+ margin-bottom: 0.75rem;
185
+ color: var(--primary-color);
186
+ display: flex;
187
+ align-items: center;
188
+ }
189
+
190
+ .agent-title i {
191
+ margin-right: 0.75rem;
192
+ font-size: 1.1rem;
193
+ color: var(--secondary-color);
194
+ }
195
+
196
+ .agent-description {
197
+ color: var(--neutral-700);
198
+ margin-bottom: 1rem;
199
+ font-size: 0.95rem;
200
+ line-height: 1.5;
201
+ }
202
+
203
+ .agent-meta {
204
+ display: flex;
205
+ flex-wrap: wrap;
206
+ gap: 1rem;
207
+ font-size: 0.9rem;
208
+ color: var(--neutral-600);
209
+ }
210
+
211
+ .agent-meta-item {
212
+ display: flex;
213
+ align-items: center;
214
+ }
215
+
216
+ .agent-meta-item i {
217
+ margin-right: 0.5rem;
218
+ font-size: 0.95rem;
219
+ color: var(--secondary-color);
220
+ }
221
+
222
+ /* 验证操作按钮 */
223
+ .verification-actions {
224
+ margin-bottom: 1.5rem;
225
+ }
226
+
227
+ .btn {
228
+ font-weight: 500;
229
+ padding: 0.75rem 1.25rem;
230
+ border-radius: var(--border-radius-lg);
231
+ transition: var(--transition-base);
232
+ }
233
+
234
+ .btn-primary {
235
+ background: linear-gradient(to right, var(--secondary-color), var(--secondary-light));
236
+ border: none;
237
+ color: white;
238
+ }
239
+
240
+ .btn-primary:hover, .btn-primary:focus {
241
+ background: linear-gradient(to right, var(--secondary-light), var(--secondary-color));
242
+ box-shadow: 0 4px 10px rgba(74, 108, 253, 0.3);
243
+ transform: translateY(-2px);
244
+ }
245
+
246
+ .btn-outline-secondary {
247
+ color: var(--neutral-700);
248
+ border-color: var(--neutral-300);
249
+ background-color: white;
250
+ }
251
+
252
+ .btn-outline-secondary:hover, .btn-outline-secondary:focus {
253
+ background-color: var(--neutral-100);
254
+ border-color: var(--neutral-400);
255
+ color: var(--neutral-900);
256
+ }
257
+
258
+ /* 过期/错误状态 */
259
+ .expired-container {
260
+ text-align: center;
261
+ color: var(--danger-color);
262
+ margin-bottom: 1.5rem;
263
+ }
264
+
265
+ .expired-container i {
266
+ font-size: 3rem;
267
+ margin-bottom: 1rem;
268
+ display: block;
269
+ }
270
+
271
+ .expired-container h2 {
272
+ margin-bottom: 0.75rem;
273
+ font-size: 1.5rem;
274
+ color: var(--danger-color);
275
+ }
276
+
277
+ .expired-container p {
278
+ color: var(--neutral-700);
279
+ margin-bottom: 1.5rem;
280
+ }
281
+
282
+ /* 页脚样式 */
283
+ .verification-footer {
284
+ text-align: center;
285
+ margin-top: 2rem;
286
+ color: var(--neutral-500);
287
+ font-size: 0.85rem;
288
+ }
289
+
290
+ .verification-footer a {
291
+ color: var(--secondary-color);
292
+ text-decoration: none;
293
+ transition: var(--transition-base);
294
+ }
295
+
296
+ .verification-footer a:hover {
297
+ text-decoration: underline;
298
+ color: var(--secondary-light);
299
+ }
300
+
301
+ /* 动画效果 */
302
+ @keyframes fadeIn {
303
+ from { opacity: 0; transform: translateY(10px); }
304
+ to { opacity: 1; transform: translateY(0); }
305
+ }
306
+
307
+ .fade-in {
308
+ opacity: 0;
309
+ animation: fadeIn 0.4s ease-out forwards;
310
+ }
311
+
312
+ /* 响应式样式 */
313
+ @media (max-width: 576px) {
314
+ .verification-container {
315
+ padding: 1.5rem;
316
+ margin: 0 1rem;
317
+ }
318
+
319
+ .verification-header h1 {
320
+ font-size: 1.5rem;
321
+ }
322
+
323
+ .agent-title {
324
+ font-size: 1.15rem;
325
+ }
326
+
327
+ .agent-meta {
328
+ flex-direction: column;
329
+ gap: 0.5rem;
330
+ }
331
+ }
332
+ </style>
333
+ </head>
334
+ <body>
335
+ <div class="verification-container fade-in" id="main-container">
336
+ <div class="loading-container" id="loading-container">
337
+ <div class="spinner-border" role="status">
338
+ <span class="visually-hidden">验证中...</span>
339
+ </div>
340
+ <h2>正在验证访问权限</h2>
341
+ <p class="text-muted">请稍候,我们正在验证您的访问令牌...</p>
342
+ </div>
343
+
344
+ <div class="verification-header" id="verification-header" style="display: none;">
345
+ <h1>访问 AI 助手</h1>
346
+ <p>您正在使用访问令牌访问以下 AI 助手</p>
347
+ </div>
348
+
349
+ <div class="agent-info" id="agent-info" style="display: none;">
350
+ <!-- 将由JavaScript填充 -->
351
+ </div>
352
+
353
+ <div class="verification-actions" id="verification-actions" style="display: none;">
354
+ <div class="row g-3">
355
+ <div class="col-12">
356
+ <button class="btn btn-primary w-100" id="access-btn">
357
+ <i class="bi bi-robot me-2"></i>开始对话
358
+ </button>
359
+ </div>
360
+ <div class="col-12">
361
+ <button class="btn btn-outline-secondary w-100" id="back-btn">
362
+ <i class="bi bi-arrow-left me-2"></i>返回
363
+ </button>
364
+ </div>
365
+ </div>
366
+ </div>
367
+
368
+ <div class="expired-container" id="expired-container" style="display: none;">
369
+ <i class="bi bi-exclamation-triangle"></i>
370
+ <h2>访问令牌无效</h2>
371
+ <p>您使用的访问令牌无效或已过期,请联系教师获取新的访问令牌。</p>
372
+ <button class="btn btn-outline-secondary mt-3" id="back-btn-expired">
373
+ <i class="bi bi-arrow-left me-2"></i>返回
374
+ </button>
375
+ </div>
376
+
377
+ <div class="verification-footer" id="verification-footer" style="display: none;">
378
+ <p>教育 AI 助手平台 | <a href="/login.html">登录</a></p>
379
+ </div>
380
+ </div>
381
+
382
+ <script>
383
+ // 解析URL参数
384
+ function getUrlParams() {
385
+ const queryString = window.location.search;
386
+ const urlParams = new URLSearchParams(queryString);
387
+ const pathParts = window.location.pathname.split('/');
388
+
389
+ return {
390
+ agentId: pathParts[pathParts.length - 1] || '',
391
+ token: urlParams.get('token') || ''
392
+ };
393
+ }
394
+
395
+ // 页面加载函数
396
+ document.addEventListener('DOMContentLoaded', async function() {
397
+ const { agentId, token } = getUrlParams();
398
+
399
+ if (!token) {
400
+ showExpiredState('未提供访问令牌');
401
+ return;
402
+ }
403
+
404
+ try {
405
+ // 验证令牌
406
+ const response = await fetch('/api/verify_token', {
407
+ method: 'POST',
408
+ headers: {
409
+ 'Content-Type': 'application/json'
410
+ },
411
+ body: JSON.stringify({
412
+ token: token,
413
+ agent_id: agentId
414
+ })
415
+ });
416
+
417
+ const result = await response.json();
418
+
419
+ if (result.success) {
420
+ // 显示Agent信息
421
+ showAgentInfo(result.agent);
422
+ } else {
423
+ showExpiredState(result.message);
424
+ }
425
+ } catch (error) {
426
+ console.error('验证出错:', error);
427
+ showExpiredState('验证过程中出错');
428
+ }
429
+ });
430
+
431
+ // 显示Agent信息
432
+ function showAgentInfo(agent) {
433
+ // 隐藏加载区域
434
+ document.getElementById('loading-container').style.display = 'none';
435
+
436
+ // 显示验证内容
437
+ document.getElementById('verification-header').style.display = 'block';
438
+ document.getElementById('agent-info').style.display = 'block';
439
+ document.getElementById('verification-actions').style.display = 'block';
440
+ document.getElementById('verification-footer').style.display = 'block';
441
+
442
+ // 填充Agent信息
443
+ const agentInfoElement = document.getElementById('agent-info');
444
+
445
+ // 构建主题和教师信息
446
+ let metaHtml = '<div class="agent-meta">';
447
+
448
+ if (agent.subject) {
449
+ metaHtml += `
450
+ <div class="agent-meta-item">
451
+ <i class="bi bi-book"></i>
452
+ ${agent.subject}
453
+ </div>
454
+ `;
455
+ }
456
+
457
+ if (agent.instructor) {
458
+ metaHtml += `
459
+ <div class="agent-meta-item">
460
+ <i class="bi bi-person"></i>
461
+ ${agent.instructor}
462
+ </div>
463
+ `;
464
+ }
465
+
466
+ metaHtml += '</div>';
467
+
468
+ agentInfoElement.innerHTML = `
469
+ <div class="agent-title">
470
+ <i class="bi bi-robot"></i>${agent.name}
471
+ </div>
472
+ <div class="agent-description">
473
+ ${agent.description || '暂无描述'}
474
+ </div>
475
+ ${metaHtml}
476
+ `;
477
+
478
+ // 设置按钮链接
479
+ document.getElementById('access-btn').addEventListener('click', function() {
480
+ const { token } = getUrlParams();
481
+ window.location.href = `/student/${agent.id}?token=${token}`;
482
+ });
483
+
484
+ document.getElementById('back-btn').addEventListener('click', function() {
485
+ // 检查是否从学生门户进入
486
+ const hasHistory = document.referrer.includes('student_portal.html');
487
+ if (hasHistory) {
488
+ window.history.back();
489
+ } else {
490
+ window.location.href = '/student_portal.html';
491
+ }
492
+ });
493
+ }
494
+
495
+ // 显示过期状态
496
+ function showExpiredState(message) {
497
+ // 隐藏加载区域
498
+ document.getElementById('loading-container').style.display = 'none';
499
+
500
+ // 显示过期内容
501
+ document.getElementById('expired-container').style.display = 'block';
502
+ document.getElementById('verification-footer').style.display = 'block';
503
+
504
+ // 更新过期消息
505
+ const expiredContainer = document.getElementById('expired-container');
506
+ expiredContainer.querySelector('p').textContent = message || '您使用的访问令牌无效或已过期,请联系教师获取新的访问令牌。';
507
+
508
+ // 设置返回按钮
509
+ document.getElementById('back-btn-expired').addEventListener('click', function() {
510
+ // 检查是否从学生门户进入
511
+ const hasHistory = document.referrer.includes('student_portal.html');
512
+ if (hasHistory) {
513
+ window.history.back();
514
+ } else {
515
+ window.location.href = '/login.html';
516
+ }
517
+ });
518
+ }
519
+ </script>
520
+ </body>
521
+ </html>
app.py ADDED
@@ -0,0 +1,987 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify, send_from_directory, redirect, url_for, session
2
+ from student_management import register_student_routes
3
+ from flask_cors import CORS
4
+ import os
5
+ import time
6
+ import traceback
7
+ import json
8
+ import re
9
+ import sys
10
+ import io
11
+ import threading
12
+ import queue
13
+ import contextlib
14
+ import signal
15
+ import psutil
16
+ from dotenv import load_dotenv
17
+
18
+ # 导入模块路由
19
+ from modules.knowledge_base.routes import knowledge_bp
20
+ from modules.code_executor.routes import code_executor_bp
21
+ from modules.visualization.routes import visualization_bp
22
+ from modules.agent_builder.routes import agent_builder_bp
23
+ from modules.auth.routes import auth_bp
24
+
25
+ # 加载环境变量
26
+ load_dotenv()
27
+
28
+ app = Flask(__name__)
29
+ CORS(app)
30
+
31
+ # 设置session密钥
32
+ app.secret_key = os.getenv("SECRET_KEY", "your_secret_key_here")
33
+
34
+ # 注册蓝图
35
+ app.register_blueprint(knowledge_bp, url_prefix='/api/knowledge')
36
+ app.register_blueprint(code_executor_bp, url_prefix='/api/code')
37
+ app.register_blueprint(visualization_bp, url_prefix='/api/visualization')
38
+ app.register_blueprint(agent_builder_bp, url_prefix='/api/agent')
39
+ app.register_blueprint(auth_bp, url_prefix='/api/auth')
40
+
41
+ # 确保目录存在
42
+ os.makedirs('static', exist_ok=True)
43
+ os.makedirs('uploads', exist_ok=True)
44
+ os.makedirs('agents', exist_ok=True)
45
+
46
+ # 用于代码执行的上下文
47
+ execution_contexts = {}
48
+
49
+ # 硬编码用户(仅用于演示)
50
+ users = {
51
+ "teachers": [
52
+ {"username": "teacher", "password": "123456", "name": "李志刚"},
53
+ {"username": "admin", "password": "admin123", "name": "管理员"}
54
+ ],
55
+ "students": [
56
+ {"username": "student1", "password": "123456", "name": "张三"},
57
+ {"username": "student2", "password": "123456", "name": "李四"}
58
+ ]
59
+ }
60
+
61
+ # 学生活动记录存储(实际应用中应使用数据库)
62
+ student_activities = {}
63
+
64
+ def get_memory_usage():
65
+ """获取当前进程的内存使用情况"""
66
+ process = psutil.Process(os.getpid())
67
+ return f"{process.memory_info().rss / 1024 / 1024:.1f} MB"
68
+
69
+ class CustomStdin:
70
+ def __init__(self, input_queue):
71
+ self.input_queue = input_queue
72
+ self.buffer = ""
73
+
74
+ def readline(self):
75
+ if not self.buffer:
76
+ self.buffer = self.input_queue.get() + "\n"
77
+
78
+ result = self.buffer
79
+ self.buffer = ""
80
+ return result
81
+
82
+ class InteractiveExecution:
83
+ """管理Python代码的交互式执行"""
84
+ def __init__(self, code):
85
+ self.code = code
86
+ self.context_id = str(time.time())
87
+ self.is_complete = False
88
+ self.is_waiting_for_input = False
89
+ self.stdout_buffer = io.StringIO()
90
+ self.last_read_position = 0
91
+ self.input_queue = queue.Queue()
92
+ self.error = None
93
+ self.thread = None
94
+ self.should_terminate = False
95
+
96
+ def run(self):
97
+ """在单独的线程中启动执行"""
98
+ self.thread = threading.Thread(target=self._execute)
99
+ self.thread.daemon = True
100
+ self.thread.start()
101
+
102
+ # 给执行一点时间开始
103
+ time.sleep(0.1)
104
+ return self.context_id
105
+
106
+ def _execute(self):
107
+ """执行代码,处理标准输入输出"""
108
+ try:
109
+ # 保存原始的stdin/stdout
110
+ orig_stdin = sys.stdin
111
+ orig_stdout = sys.stdout
112
+
113
+ # 创建自定义stdin
114
+ custom_stdin = CustomStdin(self.input_queue)
115
+
116
+ # 重定向stdin和stdout
117
+ sys.stdin = custom_stdin
118
+ sys.stdout = self.stdout_buffer
119
+
120
+ try:
121
+ # 检查终止的函数
122
+ self._last_check_time = 0
123
+
124
+ def check_termination():
125
+ if self.should_terminate:
126
+ raise KeyboardInterrupt("Execution terminated by user")
127
+
128
+ # 设置一个模拟__main__模块的命名空间
129
+ shared_namespace = {
130
+ "__builtins__": __builtins__,
131
+ "_check_termination": check_termination,
132
+ "time": time,
133
+ "__name__": "__main__"
134
+ }
135
+
136
+ # 在这个命名空间中执行用户代码
137
+ try:
138
+ exec(self.code, shared_namespace)
139
+ except KeyboardInterrupt:
140
+ print("\nExecution terminated by user")
141
+
142
+ except Exception as e:
143
+ self.error = {
144
+ "error": str(e),
145
+ "traceback": traceback.format_exc()
146
+ }
147
+
148
+ finally:
149
+ # 恢复原始stdin/stdout
150
+ sys.stdin = orig_stdin
151
+ sys.stdout = orig_stdout
152
+
153
+ # 标记执行完成
154
+ self.is_complete = True
155
+
156
+ except Exception as e:
157
+ self.error = {
158
+ "error": str(e),
159
+ "traceback": traceback.format_exc()
160
+ }
161
+ self.is_complete = True
162
+
163
+ def terminate(self):
164
+ """终止执行"""
165
+ self.should_terminate = True
166
+
167
+ # 如果在等待输入,放入一些内容以解除阻塞
168
+ if self.is_waiting_for_input:
169
+ self.input_queue.put("\n")
170
+
171
+ # 给执行一点时间终止
172
+ time.sleep(0.2)
173
+
174
+ # 标记为完成
175
+ self.is_complete = True
176
+
177
+ return True
178
+
179
+ def provide_input(self, user_input):
180
+ """为运行的代码提供输入"""
181
+ self.input_queue.put(user_input)
182
+ self.is_waiting_for_input = False
183
+ return True
184
+
185
+ def get_output(self):
186
+ """获取stdout缓冲区的当前内容"""
187
+ output = self.stdout_buffer.getvalue()
188
+ return output
189
+
190
+ def get_new_output(self):
191
+ """只获取自上次读取以来的新输出"""
192
+ current_value = self.stdout_buffer.getvalue()
193
+ if self.last_read_position < len(current_value):
194
+ new_output = current_value[self.last_read_position:]
195
+ self.last_read_position = len(current_value)
196
+ return new_output
197
+ return ""
198
+
199
+ # 记录活动函数(可在各个操作点调用)
200
+ def record_student_activity(username, activity_type, title, agent_id=None, agent_name=None):
201
+ """记录学生活动"""
202
+ if username not in student_activities:
203
+ student_activities[username] = []
204
+
205
+ # 创建活动记录
206
+ activity = {
207
+ "type": activity_type, # 'chat', 'code', 'viz', 'mindmap'
208
+ "title": title,
209
+ "timestamp": int(time.time()),
210
+ "agent_id": agent_id,
211
+ "agent_name": agent_name
212
+ }
213
+
214
+ # 添加到用户活动列表(最多保存20条记录)
215
+ student_activities[username].insert(0, activity)
216
+ if len(student_activities[username]) > 20:
217
+ student_activities[username] = student_activities[username][:20]
218
+
219
+ return activity
220
+
221
+ # 登录相关路由
222
+ @app.route('/login.html')
223
+ def login_page():
224
+ """登录页面"""
225
+ return render_template('login.html')
226
+
227
+ @app.route('/api/auth/login', methods=['POST'])
228
+ def login():
229
+ """处理登录请求"""
230
+ data = request.json
231
+ username = data.get('username')
232
+ password = data.get('password')
233
+ user_type = data.get('type', 'teacher') # 默认为教师
234
+
235
+ if user_type == 'teacher':
236
+ user_list = users['teachers']
237
+ else:
238
+ user_list = users['students']
239
+
240
+ for user in user_list:
241
+ if user['username'] == username and user['password'] == password:
242
+ # 设置session
243
+ session['logged_in'] = True
244
+ session['username'] = username
245
+ session['user_type'] = user_type
246
+ session['user_name'] = user['name']
247
+
248
+ return jsonify({
249
+ 'success': True,
250
+ 'user': {
251
+ 'name': user['name'],
252
+ 'type': user_type
253
+ }
254
+ })
255
+
256
+ return jsonify({
257
+ 'success': False,
258
+ 'message': '用户名或密码错误'
259
+ }), 401
260
+
261
+ @app.route('/api/auth/logout', methods=['POST'])
262
+ def logout():
263
+ """处理登出请求"""
264
+ session.clear()
265
+ return jsonify({
266
+ 'success': True
267
+ })
268
+
269
+ @app.route('/api/auth/check', methods=['GET'])
270
+ def check_auth():
271
+ """检查用户是否已登录"""
272
+ if session.get('logged_in'):
273
+ return jsonify({
274
+ 'success': True,
275
+ 'user': {
276
+ 'name': session.get('user_name'),
277
+ 'type': session.get('user_type')
278
+ }
279
+ })
280
+
281
+ return jsonify({
282
+ 'success': False
283
+ }), 401
284
+
285
+ # 登录验证装饰器
286
+ def login_required(f):
287
+ def decorated_function(*args, **kwargs):
288
+ if not session.get('logged_in'):
289
+ return redirect(url_for('login_page'))
290
+ return f(*args, **kwargs)
291
+ decorated_function.__name__ = f.__name__
292
+ return decorated_function
293
+
294
+ # 教师角色验证装饰器
295
+ def teacher_required(f):
296
+ def decorated_function(*args, **kwargs):
297
+ if not session.get('logged_in') or session.get('user_type') != 'teacher':
298
+ return jsonify({
299
+ 'success': False,
300
+ 'message': '需要教师权限'
301
+ }), 403
302
+ return f(*args, **kwargs)
303
+ decorated_function.__name__ = f.__name__
304
+ return decorated_function
305
+
306
+ # 学生角色验证装饰器
307
+ def student_required(f):
308
+ def decorated_function(*args, **kwargs):
309
+ if not session.get('logged_in') or session.get('user_type') != 'student':
310
+ return jsonify({
311
+ 'success': False,
312
+ 'message': '需要学生权限'
313
+ }), 403
314
+ return f(*args, **kwargs)
315
+ decorated_function.__name__ = f.__name__
316
+ return decorated_function
317
+
318
+ # React 前端静态文件目录
319
+ FRONTEND_DIST = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static', 'dist')
320
+
321
+ # 首页路由 - 托管 React 前端
322
+ @app.route('/')
323
+ def root():
324
+ return send_from_directory(FRONTEND_DIST, 'index.html')
325
+
326
+ @app.route('/api/progress/<task_id>', methods=['GET'])
327
+ def get_progress(task_id):
328
+ """获取文档处理进度"""
329
+ try:
330
+ # 从知识库模块访问处理任务
331
+ from modules.knowledge_base.routes import processing_tasks
332
+
333
+ progress_data = processing_tasks.get(task_id, {
334
+ 'progress': 0,
335
+ 'status': '未找到任务',
336
+ 'error': True
337
+ })
338
+
339
+ return jsonify({"success": True, "data": progress_data})
340
+ except Exception as e:
341
+ traceback.print_exc()
342
+ return jsonify({"success": False, "message": str(e)}), 500
343
+
344
+ @app.route('/student/<agent_id>')
345
+ def student_view(agent_id):
346
+ """学生访问Agent界面"""
347
+ token = request.args.get('token', '')
348
+
349
+ # 验证Agent存在
350
+ agent_path = os.path.join('agents', f"{agent_id}.json")
351
+ if not os.path.exists(agent_path):
352
+ return render_template('error.html',
353
+ message="找不到指定的Agent",
354
+ error_code=404)
355
+
356
+ # 加载Agent配置
357
+ with open(agent_path, 'r', encoding='utf-8') as f:
358
+ try:
359
+ agent_config = json.load(f)
360
+ except:
361
+ return render_template('error.html',
362
+ message="Agent配置无效",
363
+ error_code=500)
364
+
365
+ # 验证访问令牌
366
+ if token:
367
+ valid_token = False
368
+ if "distributions" in agent_config:
369
+ for dist in agent_config["distributions"]:
370
+ if dist.get("token") == token:
371
+ valid_token = True
372
+ break
373
+
374
+ if not valid_token:
375
+ return render_template('token_verification.html',
376
+ message="访问令牌无效",
377
+ error_code=403)
378
+
379
+ # 更新使用统计
380
+ if "distributions" in agent_config:
381
+ for dist in agent_config["distributions"]:
382
+ if dist.get("token") == token:
383
+ # 更新分发使用次数
384
+ dist["usage_count"] = dist.get("usage_count", 0) + 1
385
+
386
+ # 更新Agent使用统计
387
+ if "stats" not in agent_config:
388
+ agent_config["stats"] = {}
389
+
390
+ agent_config["stats"]["usage_count"] = agent_config["stats"].get("usage_count", 0) + 1
391
+ agent_config["stats"]["last_used"] = int(time.time())
392
+
393
+ # 保存更新后的Agent配置
394
+ with open(agent_path, 'w', encoding='utf-8') as f:
395
+ json.dump(agent_config, f, ensure_ascii=False, indent=2)
396
+
397
+ break
398
+
399
+ # 渲染学生页面
400
+ return render_template('student.html',
401
+ agent_id=agent_id,
402
+ agent_name=agent_config.get('name', 'AI学习助手'),
403
+ agent_description=agent_config.get('description', ''),
404
+ token=token)
405
+
406
+ @app.route('/api/student/chat/<agent_id>', methods=['POST'])
407
+ def student_chat(agent_id):
408
+ """学生与Agent聊天的API"""
409
+ try:
410
+ data = request.json
411
+ message = data.get('message', '')
412
+ token = data.get('token', '')
413
+
414
+ if not message:
415
+ return jsonify({"success": False, "message": "消息不能为空"}), 400
416
+
417
+ # 验证Agent和令牌
418
+ agent_path = os.path.join('agents', f"{agent_id}.json")
419
+ if not os.path.exists(agent_path):
420
+ return jsonify({"success": False, "message": "Agent不存在"}), 404
421
+
422
+ with open(agent_path, 'r', encoding='utf-8') as f:
423
+ agent_config = json.load(f)
424
+
425
+ # 验证令牌(如果提供)
426
+ if token and "distributions" in agent_config:
427
+ valid_token = False
428
+ for dist in agent_config["distributions"]:
429
+ if dist.get("token") == token:
430
+ valid_token = True
431
+
432
+ # 更新使用计数
433
+ dist["usage_count"] = dist.get("usage_count", 0) + 1
434
+ break
435
+
436
+ if not valid_token:
437
+ return jsonify({"success": False, "message": "访问令牌无效"}), 403
438
+
439
+ # 更新Agent使用统计
440
+ if "stats" not in agent_config:
441
+ agent_config["stats"] = {}
442
+
443
+ agent_config["stats"]["usage_count"] = agent_config["stats"].get("usage_count", 0) + 1
444
+ agent_config["stats"]["last_used"] = int(time.time())
445
+
446
+ # 保存更新后的Agent配置
447
+ with open(agent_path, 'w', encoding='utf-8') as f:
448
+ json.dump(agent_config, f, ensure_ascii=False, indent=2)
449
+
450
+ # 获取Agent关联的知识库和插件
451
+ knowledge_bases = agent_config.get('knowledge_bases', [])
452
+ plugins = agent_config.get('plugins', [])
453
+
454
+ # 获取学科和指导者信息
455
+ subject = agent_config.get('subject', agent_config.get('name', '通用学科'))
456
+ instructor = agent_config.get('instructor', '教师')
457
+
458
+ # 创建Generator实例,传入学科和指导者信息
459
+ from modules.knowledge_base.generator import Generator
460
+ generator = Generator(subject=subject, instructor=instructor)
461
+
462
+ # 检测需要使用的插件
463
+ suggested_plugins = []
464
+
465
+ # 检测是否需要代码执行插件
466
+ if 'code' in plugins and ('代码' in message or 'python' in message.lower() or '编程' in message or 'code' in message.lower() or 'program' in message.lower()):
467
+ suggested_plugins.append('code')
468
+
469
+ # 检测是否需要3D可视化插件
470
+ if 'visualization' in plugins and ('3d' in message.lower() or '可视化' in message or '图形' in message):
471
+ suggested_plugins.append('visualization')
472
+
473
+ # 检测是否需要思维导图插件
474
+ if 'mindmap' in plugins and ('思维导图' in message or 'mindmap' in message.lower()):
475
+ suggested_plugins.append('mindmap')
476
+
477
+ # 记录活动(添加此部分代码)
478
+ if session.get('logged_in'):
479
+ username = session.get('username')
480
+ # 记录对话活动
481
+ record_student_activity(
482
+ username=username,
483
+ activity_type='chat',
484
+ title=f'与 {agent_config.get("name", "AI助手")} 进行了对话',
485
+ agent_id=agent_id,
486
+ agent_name=agent_config.get('name')
487
+ )
488
+
489
+ # 如果使用了插件,记录相应的插件活动
490
+ if 'code' in suggested_plugins:
491
+ record_student_activity(
492
+ username=username,
493
+ activity_type='code',
494
+ title='执行了Python代码',
495
+ agent_id=agent_id,
496
+ agent_name=agent_config.get('name')
497
+ )
498
+
499
+ if 'visualization' in suggested_plugins:
500
+ record_student_activity(
501
+ username=username,
502
+ activity_type='viz',
503
+ title='查看了3D可视化图形',
504
+ agent_id=agent_id,
505
+ agent_name=agent_config.get('name')
506
+ )
507
+
508
+ if 'mindmap' in suggested_plugins:
509
+ record_student_activity(
510
+ username=username,
511
+ activity_type='mindmap',
512
+ title='生成了思维导图',
513
+ agent_id=agent_id,
514
+ agent_name=agent_config.get('name')
515
+ )
516
+
517
+ # 检查是否有配置知识库
518
+ if not knowledge_bases:
519
+ # 没有知识库,直接使用模型进行回答
520
+ print(f"\n=== 处理查询: {message} (无知识库) ===")
521
+
522
+ # 创建SSE流式响应
523
+ def generate_sse():
524
+ try:
525
+ # 发送开始事件
526
+ yield f"data: {json.dumps({'type': 'start', 'plugins': suggested_plugins}, ensure_ascii=False)}\n\n"
527
+
528
+ # 流式生成回答
529
+ for chunk in generator.generate_stream(message, []):
530
+ if isinstance(chunk, dict):
531
+ yield f"data: {json.dumps({'type': 'metadata', 'content': chunk}, ensure_ascii=False)}\n\n"
532
+ else:
533
+ yield f"data: {json.dumps({'type': 'content', 'content': chunk}, ensure_ascii=False)}\n\n"
534
+
535
+ # 发送结束事件
536
+ yield f"data: {json.dumps({'type': 'done'}, ensure_ascii=False)}\n\n"
537
+ except Exception as e:
538
+ yield f"data: {json.dumps({'type': 'error', 'content': str(e)}, ensure_ascii=False)}\n\n"
539
+
540
+ # 返回SSE响应
541
+ from flask import Response
542
+ return Response(
543
+ generate_sse(),
544
+ mimetype='text/event-stream',
545
+ headers={
546
+ 'Cache-Control': 'no-cache',
547
+ 'X-Accel-Buffering': 'no',
548
+ 'Access-Control-Allow-Origin': '*'
549
+ }
550
+ )
551
+
552
+ # 有知识库配置,执行知识库查询流程
553
+ try:
554
+ # 导入RAG系统组件
555
+ from modules.knowledge_base.retriever import Retriever
556
+ from modules.knowledge_base.reranker import Reranker
557
+
558
+ retriever = Retriever()
559
+ reranker = Reranker()
560
+
561
+ # 构建工具定义 - 将所有知识库作为工具
562
+ tools = []
563
+
564
+ # 创建工具名称到索引的映射
565
+ tool_to_index = {}
566
+
567
+ for i, index in enumerate(knowledge_bases):
568
+ display_name = index[4:] if index.startswith('rag_') else index
569
+
570
+ # 判断是否是视频知识库
571
+ is_video = "视频" in display_name or "video" in display_name.lower()
572
+
573
+ # 根据内容类型生成适当的工具名称
574
+ if is_video:
575
+ tool_name = f"video_knowledge_base_{i+1}"
576
+ description = f"在'{display_name}'视频知识库中搜索,返回带时间戳的视频链接。适用于需要视频讲解的问题。"
577
+ else:
578
+ tool_name = f"knowledge_base_{i+1}"
579
+ description = f"在'{display_name}'知识库中搜索专业知识、概念和原理。适用于需要文本说明的问题。"
580
+
581
+ # 添加工具名到索引的映射
582
+ tool_to_index[tool_name] = index
583
+
584
+ tools.append({
585
+ "type": "function",
586
+ "function": {
587
+ "name": tool_name,
588
+ "description": description,
589
+ "parameters": {
590
+ "type": "object",
591
+ "properties": {
592
+ "keywords": {
593
+ "type": "array",
594
+ "items": {"type": "string"},
595
+ "description": "搜索的关键词列表"
596
+ }
597
+ },
598
+ "required": ["keywords"],
599
+ "additionalProperties": False
600
+ },
601
+ "strict": True
602
+ }
603
+ })
604
+
605
+ # 第一阶段:工具选择决策
606
+ print(f"\n=== 处理查询: {message} ===")
607
+ tool_calls = generator.extract_keywords_with_tools(message, tools)
608
+
609
+ # 如果不需要调用工具,直接回答
610
+ if not tool_calls:
611
+ print("未检测到需要使用知识库,直接回答")
612
+
613
+ # 创建SSE流式响应
614
+ def generate_sse_no_tools():
615
+ try:
616
+ yield f"data: {json.dumps({'type': 'start', 'plugins': suggested_plugins}, ensure_ascii=False)}\n\n"
617
+
618
+ for chunk in generator.generate_stream(message, []):
619
+ if isinstance(chunk, dict):
620
+ yield f"data: {json.dumps({'type': 'metadata', 'content': chunk}, ensure_ascii=False)}\n\n"
621
+ else:
622
+ yield f"data: {json.dumps({'type': 'content', 'content': chunk}, ensure_ascii=False)}\n\n"
623
+
624
+ yield f"data: {json.dumps({'type': 'done'}, ensure_ascii=False)}\n\n"
625
+ except Exception as e:
626
+ yield f"data: {json.dumps({'type': 'error', 'content': str(e)}, ensure_ascii=False)}\n\n"
627
+
628
+ # 返回SSE响应
629
+ from flask import Response
630
+ return Response(
631
+ generate_sse_no_tools(),
632
+ mimetype='text/event-stream',
633
+ headers={
634
+ 'Cache-Control': 'no-cache',
635
+ 'X-Accel-Buffering': 'no',
636
+ 'Access-Control-Allow-Origin': '*'
637
+ }
638
+ )
639
+
640
+ # 收集来自工具执行的所有文档
641
+ all_docs = []
642
+
643
+ # 执行每个工具调用
644
+ for tool_call in tool_calls:
645
+ try:
646
+ tool_name = tool_call["function"]["name"]
647
+ actual_index = tool_to_index.get(tool_name)
648
+
649
+ if not actual_index:
650
+ print(f"找不到工具名称 '{tool_name}' 对应的索引")
651
+ continue
652
+
653
+ print(f"\n执行工具 '{tool_name}' -> 使用索引 '{actual_index}'")
654
+
655
+ arguments = json.loads(tool_call["function"]["arguments"])
656
+ keywords = " ".join(arguments.get("keywords", []))
657
+
658
+ if not keywords:
659
+ print("没有提供关键词,跳过检索")
660
+ continue
661
+
662
+ print(f"检索关键词: {keywords}")
663
+
664
+ # 执行检索
665
+ retrieved_docs, _ = retriever.retrieve(keywords, specific_index=actual_index)
666
+ print(f"检索到 {len(retrieved_docs)} 个文档")
667
+
668
+ # 重排序文档
669
+ reranked_docs = reranker.rerank(message, retrieved_docs, actual_index)
670
+ print(f"重排序完成,排序后有 {len(reranked_docs)} 个文档")
671
+
672
+ # 添加结果
673
+ all_docs.extend(reranked_docs)
674
+
675
+ except Exception as e:
676
+ print(f"执行工具 '{tool_call.get('function', {}).get('name', '未知')}' 调用时出错: {str(e)}")
677
+ import traceback
678
+ traceback.print_exc()
679
+
680
+ # 如果没有检索到任何文档,直接回答
681
+ if not all_docs:
682
+ print("未检索到任何相关文档,直接回答")
683
+
684
+ # 创建SSE流式响应(即使没有文档也要流式输出)
685
+ def generate_sse_no_docs():
686
+ try:
687
+ yield f"data: {json.dumps({'type': 'start', 'plugins': suggested_plugins}, ensure_ascii=False)}\n\n"
688
+
689
+ for chunk in generator.generate_stream(message, []):
690
+ if isinstance(chunk, dict):
691
+ yield f"data: {json.dumps({'type': 'metadata', 'content': chunk}, ensure_ascii=False)}\n\n"
692
+ else:
693
+ yield f"data: {json.dumps({'type': 'content', 'content': chunk}, ensure_ascii=False)}\n\n"
694
+
695
+ yield f"data: {json.dumps({'type': 'done'}, ensure_ascii=False)}\n\n"
696
+ except Exception as e:
697
+ yield f"data: {json.dumps({'type': 'error', 'content': str(e)}, ensure_ascii=False)}\n\n"
698
+
699
+ # 返回SSE响应
700
+ from flask import Response
701
+ return Response(
702
+ generate_sse_no_docs(),
703
+ mimetype='text/event-stream',
704
+ headers={
705
+ 'Cache-Control': 'no-cache',
706
+ 'X-Accel-Buffering': 'no',
707
+ 'Access-Control-Allow-Origin': '*'
708
+ }
709
+ )
710
+
711
+ # 按相关性排序
712
+ all_docs.sort(key=lambda x: x.get('rerank_score', 0), reverse=True)
713
+ print(f"\n最终收集到 {len(all_docs)} 个文档用于生成回答")
714
+
715
+ # 提取参考信息
716
+ references = []
717
+ for i, doc in enumerate(all_docs[:3], 1): # 只展示前3个参考来源
718
+ file_name = doc['metadata'].get('file_name', '未知文件')
719
+ content = doc['content']
720
+
721
+ # 提取大约前100字符作为摘要
722
+ summary = content[:100] + ('...' if len(content) > 100 else '')
723
+
724
+ references.append({
725
+ 'index': i,
726
+ 'file_name': file_name,
727
+ 'content': content,
728
+ 'summary': summary
729
+ })
730
+
731
+ # 第二阶段:生成最终答案 - 流式响应
732
+ def generate_sse_with_kb():
733
+ try:
734
+ yield f"data: {json.dumps({'type': 'start', 'plugins': suggested_plugins, 'references': references}, ensure_ascii=False)}\n\n"
735
+
736
+ for chunk in generator.generate_stream(message, all_docs):
737
+ if isinstance(chunk, dict):
738
+ yield f"data: {json.dumps({'type': 'metadata', 'content': chunk}, ensure_ascii=False)}\n\n"
739
+ else:
740
+ yield f"data: {json.dumps({'type': 'content', 'content': chunk}, ensure_ascii=False)}\n\n"
741
+
742
+ yield f"data: {json.dumps({'type': 'done'}, ensure_ascii=False)}\n\n"
743
+ except Exception as e:
744
+ yield f"data: {json.dumps({'type': 'error', 'content': str(e)}, ensure_ascii=False)}\n\n"
745
+
746
+ # 返回SSE响应
747
+ from flask import Response
748
+ return Response(
749
+ generate_sse_with_kb(),
750
+ mimetype='text/event-stream',
751
+ headers={
752
+ 'Cache-Control': 'no-cache',
753
+ 'X-Accel-Buffering': 'no',
754
+ 'Access-Control-Allow-Origin': '*'
755
+ }
756
+ )
757
+
758
+ except Exception as e:
759
+ import traceback
760
+ traceback.print_exc()
761
+ return jsonify({
762
+ "success": False,
763
+ "message": f"处理查询时出错: {str(e)}"
764
+ }), 500
765
+
766
+ except Exception as e:
767
+ import traceback
768
+ traceback.print_exc()
769
+ return jsonify({"success": False, "message": str(e)}), 500
770
+
771
+ # API端点:获取学生活动记录
772
+ @app.route('/api/student/activities', methods=['GET'])
773
+ @student_required
774
+ def get_student_activities():
775
+ """获取学生活动记录"""
776
+ try:
777
+ username = session.get('username')
778
+
779
+ # 获取该学生的活动记录
780
+ activities = student_activities.get(username, [])
781
+
782
+ # 格式化输出
783
+ formatted_activities = []
784
+ for activity in activities:
785
+ # 格式化时间显示
786
+ timestamp = activity['timestamp']
787
+ current_time = int(time.time())
788
+
789
+ if current_time - timestamp < 86400: # 24小时内
790
+ if current_time - timestamp < 3600: # 1小时内
791
+ time_text = f"{(current_time - timestamp) // 60}分钟前"
792
+ else:
793
+ time_text = f"今天 {time.strftime('%H:%M', time.localtime(timestamp))}"
794
+ elif current_time - timestamp < 172800: # 48小时内
795
+ time_text = f"昨天 {time.strftime('%H:%M', time.localtime(timestamp))}"
796
+ else:
797
+ time_text = time.strftime('%m月%d日 %H:%M', time.localtime(timestamp))
798
+
799
+ formatted_activities.append({
800
+ "type": activity['type'],
801
+ "title": activity['title'],
802
+ "time": time_text,
803
+ "agent_id": activity.get('agent_id'),
804
+ "agent_name": activity.get('agent_name')
805
+ })
806
+
807
+ return jsonify({
808
+ "success": True,
809
+ "activities": formatted_activities
810
+ })
811
+
812
+ except Exception as e:
813
+ import traceback
814
+ traceback.print_exc()
815
+ return jsonify({
816
+ "success": False,
817
+ "message": str(e)
818
+ }), 500
819
+
820
+ # API端点:验证访问令牌
821
+ @app.route('/api/verify_token', methods=['POST'])
822
+ def verify_token():
823
+ """验证访问令牌有效性"""
824
+ try:
825
+ data = request.json
826
+ token = data.get('token', '')
827
+ agent_id = data.get('agent_id', '')
828
+
829
+ if not token:
830
+ return jsonify({
831
+ "success": False,
832
+ "message": "未提供访问令牌"
833
+ }), 400
834
+
835
+ # 如果提供了agent_id,验证特定Agent的令牌
836
+ if agent_id:
837
+ agent_path = os.path.join('agents', f"{agent_id}.json")
838
+ if not os.path.exists(agent_path):
839
+ return jsonify({
840
+ "success": False,
841
+ "message": "Agent不存在"
842
+ }), 404
843
+
844
+ with open(agent_path, 'r', encoding='utf-8') as f:
845
+ agent_config = json.load(f)
846
+
847
+ # 验证令牌
848
+ if "distributions" in agent_config:
849
+ for dist in agent_config["distributions"]:
850
+ if dist.get("token") == token:
851
+ # 检查是否过期
852
+ if dist.get("expires_at", 0) > 0 and dist.get("expires_at", 0) < time.time():
853
+ return jsonify({
854
+ "success": False,
855
+ "message": "访问令牌已过期"
856
+ })
857
+
858
+ return jsonify({
859
+ "success": True,
860
+ "agent": {
861
+ "id": agent_id,
862
+ "name": agent_config.get('name', 'AI学习助手'),
863
+ "description": agent_config.get('description', ''),
864
+ "subject": agent_config.get('subject', ''),
865
+ "instructor": agent_config.get('instructor', '教师')
866
+ }
867
+ })
868
+
869
+ return jsonify({
870
+ "success": False,
871
+ "message": "访问令牌无效"
872
+ })
873
+
874
+ # 如果没有提供agent_id,搜索所有Agent
875
+ valid_agent = None
876
+
877
+ for filename in os.listdir('agents'):
878
+ if filename.endswith('.json'):
879
+ agent_path = os.path.join('agents', filename)
880
+ with open(agent_path, 'r', encoding='utf-8') as f:
881
+ agent_config = json.load(f)
882
+
883
+ # 验证令牌
884
+ if "distributions" in agent_config:
885
+ for dist in agent_config["distributions"]:
886
+ if dist.get("token") == token:
887
+ # 检查是否过���
888
+ if dist.get("expires_at", 0) > 0 and dist.get("expires_at", 0) < time.time():
889
+ continue
890
+
891
+ valid_agent = {
892
+ "id": agent_config.get('id'),
893
+ "name": agent_config.get('name', 'AI学习助手'),
894
+ "description": agent_config.get('description', ''),
895
+ "subject": agent_config.get('subject', ''),
896
+ "instructor": agent_config.get('instructor', '教师')
897
+ }
898
+ break
899
+
900
+ if valid_agent:
901
+ break
902
+
903
+ if valid_agent:
904
+ return jsonify({
905
+ "success": True,
906
+ "agent": valid_agent
907
+ })
908
+
909
+ return jsonify({
910
+ "success": False,
911
+ "message": "未找到匹配的访问令牌"
912
+ })
913
+
914
+ except Exception as e:
915
+ import traceback
916
+ traceback.print_exc()
917
+ return jsonify({
918
+ "success": False,
919
+ "message": f"验证访问令牌时出错: {str(e)}"
920
+ }), 500
921
+
922
+ # API端点:获取学生的Agent列表
923
+ @app.route('/api/student/agents', methods=['GET'])
924
+ @student_required
925
+ def get_student_agents():
926
+ """获取学生可访问的Agent列表"""
927
+ try:
928
+ # 实际应用中应根据学生ID过滤
929
+ # 这里简化为获取所有Agent
930
+ agents = []
931
+
932
+ for filename in os.listdir('agents'):
933
+ if filename.endswith('.json'):
934
+ agent_path = os.path.join('agents', filename)
935
+ with open(agent_path, 'r', encoding='utf-8') as f:
936
+ agent_config = json.load(f)
937
+
938
+ # 简化信息
939
+ agent_info = {
940
+ "id": agent_config.get('id'),
941
+ "name": agent_config.get('name', 'AI学习助手'),
942
+ "description": agent_config.get('description', ''),
943
+ "subject": agent_config.get('subject', ''),
944
+ "instructor": agent_config.get('instructor', '教师'),
945
+ "plugins": agent_config.get('plugins', []),
946
+ "last_used": agent_config.get('stats', {}).get('last_used')
947
+ }
948
+
949
+ # 添加TOKEN(实际应用中应严格控制令牌访问)
950
+ if "distributions" in agent_config and agent_config["distributions"]:
951
+ # 仅添加第一个分发的令牌
952
+ agent_info["token"] = agent_config["distributions"][0].get("token")
953
+
954
+ agents.append(agent_info)
955
+
956
+ # 按最后使用时间排序
957
+ agents.sort(key=lambda x: x.get('last_used', 0) or 0, reverse=True)
958
+
959
+ return jsonify({
960
+ "success": True,
961
+ "agents": agents
962
+ })
963
+
964
+ except Exception as e:
965
+ import traceback
966
+ traceback.print_exc()
967
+ return jsonify({
968
+ "success": False,
969
+ "message": str(e)
970
+ }), 500
971
+
972
+ # 注册学生管理路由
973
+ register_student_routes(app)
974
+
975
+ # React SPA catch-all:非 API 路径都返回 index.html(支持前端路由)
976
+ @app.route('/<path:path>')
977
+ def catch_all(path):
978
+ # 静态资源文件直接返回
979
+ file_path = os.path.join(FRONTEND_DIST, path)
980
+ if os.path.isfile(file_path):
981
+ return send_from_directory(FRONTEND_DIST, path)
982
+ # 其他路径返回 index.html,交给 React Router 处理
983
+ return send_from_directory(FRONTEND_DIST, 'index.html')
984
+
985
+ if __name__ == '__main__':
986
+ debug = os.getenv('FLASK_ENV', 'production') == 'development'
987
+ app.run(debug=debug, host='0.0.0.0', port=7860)
config.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # config.py
2
+ import os
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ # API keys
8
+ API_KEY = os.getenv("API_KEY")
9
+ BASE_URL = os.getenv("BASE_URL")
10
+ STREAM_API_KEY = os.getenv("STREAM_API_KEY")
11
+ STREAM_BASE_URL = os.getenv("STREAM_BASE_URL")
12
+ STREAM_MODEL = os.getenv("STREAM_MODEL")
13
+ DEFAULT_MODEL = os.getenv("DEFAULT_MODEL")
14
+ OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
15
+
16
+ # Knowledge base configuration
17
+ EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "BAAI/bge-m3")
18
+ RERANK_MODEL = os.getenv("RERANK_MODEL", "BAAI/bge-reranker-v2-m3")
19
+
20
+ # System configuration
21
+ UPLOAD_FOLDER = "uploads"
22
+ AGENTS_FOLDER = "agents"
23
+ MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
frontend/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
frontend/README.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
frontend/eslint.config.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import { defineConfig, globalIgnores } from 'eslint/config'
6
+
7
+ export default defineConfig([
8
+ globalIgnores(['dist']),
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ extends: [
12
+ js.configs.recommended,
13
+ reactHooks.configs['recommended-latest'],
14
+ reactRefresh.configs.vite,
15
+ ],
16
+ languageOptions: {
17
+ ecmaVersion: 2020,
18
+ globals: globals.browser,
19
+ parserOptions: {
20
+ ecmaVersion: 'latest',
21
+ ecmaFeatures: { jsx: true },
22
+ sourceType: 'module',
23
+ },
24
+ },
25
+ rules: {
26
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27
+ },
28
+ },
29
+ ])
frontend/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>frontend</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.jsx"></script>
12
+ </body>
13
+ </html>
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@ant-design/icons": "^6.1.0",
14
+ "@codemirror/lang-python": "^6.2.1",
15
+ "@reduxjs/toolkit": "^2.10.1",
16
+ "@tailwindcss/forms": "^0.5.10",
17
+ "@tailwindcss/typography": "^0.5.19",
18
+ "@uiw/react-codemirror": "^4.25.9",
19
+ "antd": "^5.29.0",
20
+ "axios": "^1.13.2",
21
+ "dayjs": "^1.11.19",
22
+ "framer-motion": "^12.23.24",
23
+ "katex": "^0.16.45",
24
+ "markmap-lib": "^0.18.12",
25
+ "markmap-view": "^0.18.12",
26
+ "react": "^19.2.0",
27
+ "react-dom": "^19.2.0",
28
+ "react-markdown": "^9.1.0",
29
+ "react-redux": "^9.2.0",
30
+ "react-router-dom": "^7.9.6",
31
+ "react-syntax-highlighter": "^15.6.6",
32
+ "reactflow": "^11.11.4",
33
+ "recharts": "^3.4.1",
34
+ "rehype-katex": "^7.0.1",
35
+ "remark-gfm": "^4.0.1",
36
+ "remark-math": "^6.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "@eslint/js": "^9.39.1",
40
+ "@types/react": "^19.2.2",
41
+ "@types/react-dom": "^19.2.2",
42
+ "@vitejs/plugin-react": "^5.1.0",
43
+ "autoprefixer": "^10.4.22",
44
+ "eslint": "^9.39.1",
45
+ "eslint-plugin-react-hooks": "^5.2.0",
46
+ "eslint-plugin-react-refresh": "^0.4.24",
47
+ "globals": "^16.5.0",
48
+ "postcss": "^8.5.6",
49
+ "postcss-import": "^16.1.1",
50
+ "tailwindcss": "^3.4.18",
51
+ "vite": "^7.2.2"
52
+ }
53
+ }
frontend/postcss.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ 'postcss-import': {},
4
+ tailwindcss: {},
5
+ autoprefixer: {},
6
+ },
7
+ }
frontend/public/vite.svg ADDED
frontend/src/App.jsx ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
3
+ import { Provider } from 'react-redux';
4
+ import { ConfigProvider, message } from 'antd';
5
+ import zhCN from 'antd/locale/zh_CN';
6
+ import store from './store/store';
7
+ import LoginPage from './pages/LoginPage';
8
+ import RegisterPage from './pages/RegisterPage';
9
+ import TeacherDashboard from './pages/teacher/TeacherDashboard';
10
+ import StudentDashboard from './pages/student/StudentDashboard';
11
+ import AgentChat from './pages/student/AgentChat';
12
+ import ProtectedRoute from './components/common/ProtectedRoute';
13
+ import './index.css';
14
+
15
+ // Ant Design 主题配置
16
+ const theme = {
17
+ token: {
18
+ colorPrimary: '#10b981',
19
+ colorSuccess: '#10b981',
20
+ colorWarning: '#f97316',
21
+ colorError: '#f43f5e',
22
+ colorInfo: '#8b5cf6',
23
+ borderRadius: 8,
24
+ fontSize: 14,
25
+ fontFamily: "'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif",
26
+ },
27
+ components: {
28
+ Button: {
29
+ controlHeight: 40,
30
+ fontWeight: 500,
31
+ },
32
+ Input: {
33
+ controlHeight: 40,
34
+ },
35
+ Select: {
36
+ controlHeight: 40,
37
+ },
38
+ Menu: {
39
+ itemBorderRadius: 8,
40
+ subMenuItemBorderRadius: 8,
41
+ },
42
+ Card: {
43
+ borderRadiusLG: 12,
44
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)',
45
+ },
46
+ },
47
+ };
48
+
49
+ // 全局消息配置
50
+ message.config({
51
+ top: 80,
52
+ duration: 3,
53
+ maxCount: 3,
54
+ });
55
+
56
+ function App() {
57
+ return (
58
+ <Provider store={store}>
59
+ <ConfigProvider theme={theme} locale={zhCN}>
60
+ <BrowserRouter>
61
+ <Routes>
62
+ <Route path="/login" element={<LoginPage />} />
63
+ <Route path="/register" element={<RegisterPage />} />
64
+
65
+ {/* Agent对话页面 - 不需要登录保护,通过token访问 */}
66
+ {/* 注意:这个路由必须在 /student/* 之前,否则会被捕获并要求登录 */}
67
+ <Route path="/student/chat/:agentId" element={<AgentChat />} />
68
+
69
+ {/* 教师端路由 */}
70
+ <Route
71
+ path="/teacher/*"
72
+ element={
73
+ <ProtectedRoute requiredRole="teacher">
74
+ <TeacherDashboard />
75
+ </ProtectedRoute>
76
+ }
77
+ />
78
+
79
+ {/* 学生端路由 - 排除chat路径 */}
80
+ <Route
81
+ path="/student/*"
82
+ element={
83
+ <ProtectedRoute requiredRole="student">
84
+ <StudentDashboard />
85
+ </ProtectedRoute>
86
+ }
87
+ />
88
+
89
+ {/* 默认重定向到登录 */}
90
+ <Route path="/" element={<Navigate to="/login" replace />} />
91
+ <Route path="*" element={<Navigate to="/login" replace />} />
92
+ </Routes>
93
+ </BrowserRouter>
94
+ </ConfigProvider>
95
+ </Provider>
96
+ );
97
+ }
98
+
99
+ export default App;
frontend/src/assets/react.svg ADDED
frontend/src/components/WorkflowEditor.jsx ADDED
@@ -0,0 +1,449 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 工作流编辑器组件
3
+ * 使用React Flow实现可视化工作流编辑
4
+ */
5
+ import { useState, useCallback, useRef } from 'react';
6
+ import ReactFlow, {
7
+ ReactFlowProvider,
8
+ addEdge,
9
+ useNodesState,
10
+ useEdgesState,
11
+ Controls,
12
+ MiniMap,
13
+ Background,
14
+ Panel,
15
+ MarkerType
16
+ } from 'reactflow';
17
+ import 'reactflow/dist/style.css';
18
+ import {
19
+ Card,
20
+ Button,
21
+ Drawer,
22
+ Form,
23
+ Input,
24
+ Select,
25
+ Space,
26
+ Typography,
27
+ message,
28
+ Modal,
29
+ Divider,
30
+ Tag
31
+ } from 'antd';
32
+ import {
33
+ PlusOutlined,
34
+ SaveOutlined,
35
+ ClearOutlined,
36
+ PlayCircleOutlined,
37
+ QuestionCircleOutlined,
38
+ CodeOutlined,
39
+ FileTextOutlined,
40
+ BranchesOutlined,
41
+ CheckCircleOutlined
42
+ } from '@ant-design/icons';
43
+
44
+ const { Title, Text } = Typography;
45
+ const { TextArea } = Input;
46
+ const { Option } = Select;
47
+
48
+ // 节点类型定义
49
+ const nodeTypes = {
50
+ start: {
51
+ label: '开始节点',
52
+ icon: <PlayCircleOutlined />,
53
+ color: '#52c41a'
54
+ },
55
+ question: {
56
+ label: '提问节点',
57
+ icon: <QuestionCircleOutlined />,
58
+ color: '#1890ff'
59
+ },
60
+ knowledge: {
61
+ label: '知识查询',
62
+ icon: <FileTextOutlined />,
63
+ color: '#722ed1'
64
+ },
65
+ code: {
66
+ label: '代码执行',
67
+ icon: <CodeOutlined />,
68
+ color: '#fa8c16'
69
+ },
70
+ condition: {
71
+ label: '条件分支',
72
+ icon: <BranchesOutlined />,
73
+ color: '#f5222d'
74
+ },
75
+ end: {
76
+ label: '结束节点',
77
+ icon: <CheckCircleOutlined />,
78
+ color: '#52c41a'
79
+ }
80
+ };
81
+
82
+ // 自定义节点组件
83
+ const CustomNode = ({ data, selected }) => {
84
+ const nodeType = nodeTypes[data.type] || nodeTypes.question;
85
+
86
+ return (
87
+ <div
88
+ style={{
89
+ padding: '10px 15px',
90
+ borderRadius: '8px',
91
+ background: '#fff',
92
+ border: `2px solid ${selected ? nodeType.color : '#d9d9d9'}`,
93
+ boxShadow: selected ? `0 0 0 2px ${nodeType.color}20` : '0 2px 4px rgba(0,0,0,0.1)',
94
+ minWidth: '150px',
95
+ textAlign: 'center'
96
+ }}
97
+ >
98
+ <Space>
99
+ <span style={{ color: nodeType.color, fontSize: '16px' }}>
100
+ {nodeType.icon}
101
+ </span>
102
+ <Text strong>{data.label}</Text>
103
+ </Space>
104
+ {data.description && (
105
+ <div style={{ marginTop: '4px' }}>
106
+ <Text type="secondary" style={{ fontSize: '12px' }}>
107
+ {data.description}
108
+ </Text>
109
+ </div>
110
+ )}
111
+ </div>
112
+ );
113
+ };
114
+
115
+ // 初始节点
116
+ const initialNodes = [
117
+ {
118
+ id: 'start',
119
+ type: 'custom',
120
+ position: { x: 250, y: 50 },
121
+ data: {
122
+ type: 'start',
123
+ label: '开始',
124
+ description: '工作流起点'
125
+ }
126
+ }
127
+ ];
128
+
129
+ const WorkflowEditor = ({ value, onChange, onSave }) => {
130
+ const [nodes, setNodes, onNodesChange] = useNodesState(value?.nodes || initialNodes);
131
+ const [edges, setEdges, onEdgesChange] = useEdgesState(value?.edges || []);
132
+ const [drawerVisible, setDrawerVisible] = useState(false);
133
+ const [selectedNode, setSelectedNode] = useState(null);
134
+ const [nodeForm] = Form.useForm();
135
+ const reactFlowWrapper = useRef(null);
136
+ const reactFlowInstance = useRef(null);
137
+
138
+ // 连接节点
139
+ const onConnect = useCallback(
140
+ (params) => {
141
+ const newEdge = {
142
+ ...params,
143
+ type: 'smoothstep',
144
+ animated: true,
145
+ style: { stroke: '#1890ff', strokeWidth: 2 },
146
+ markerEnd: {
147
+ type: MarkerType.ArrowClosed,
148
+ color: '#1890ff'
149
+ }
150
+ };
151
+ setEdges((eds) => addEdge(newEdge, eds));
152
+ },
153
+ [setEdges]
154
+ );
155
+
156
+ // 添加节点
157
+ const addNode = (type) => {
158
+ const newNode = {
159
+ id: `${type}_${Date.now()}`,
160
+ type: 'custom',
161
+ position: {
162
+ x: Math.random() * 400 + 100,
163
+ y: Math.random() * 300 + 100
164
+ },
165
+ data: {
166
+ type,
167
+ label: nodeTypes[type].label,
168
+ description: '',
169
+ config: {}
170
+ }
171
+ };
172
+
173
+ setNodes((nds) => nds.concat(newNode));
174
+ message.success(`已添加${nodeTypes[type].label}`);
175
+ };
176
+
177
+ // 选中节点
178
+ const onNodeClick = (event, node) => {
179
+ setSelectedNode(node);
180
+ nodeForm.setFieldsValue({
181
+ label: node.data.label,
182
+ description: node.data.description,
183
+ ...node.data.config
184
+ });
185
+ setDrawerVisible(true);
186
+ };
187
+
188
+ // 删除节点
189
+ const deleteNode = (nodeId) => {
190
+ setNodes((nds) => nds.filter((n) => n.id !== nodeId));
191
+ setEdges((eds) => eds.filter((e) => e.source !== nodeId && e.target !== nodeId));
192
+ message.success('节点已删除');
193
+ };
194
+
195
+ // 更新节点
196
+ const updateNode = (values) => {
197
+ setNodes((nds) =>
198
+ nds.map((node) => {
199
+ if (node.id === selectedNode.id) {
200
+ return {
201
+ ...node,
202
+ data: {
203
+ ...node.data,
204
+ label: values.label,
205
+ description: values.description,
206
+ config: {
207
+ ...values,
208
+ label: undefined,
209
+ description: undefined
210
+ }
211
+ }
212
+ };
213
+ }
214
+ return node;
215
+ })
216
+ );
217
+ setDrawerVisible(false);
218
+ message.success('节点已更新');
219
+ };
220
+
221
+ // 清空画布
222
+ const clearCanvas = () => {
223
+ Modal.confirm({
224
+ title: '确认清空',
225
+ content: '确定要清空所有节点和连接吗?',
226
+ onOk: () => {
227
+ setNodes([initialNodes[0]]);
228
+ setEdges([]);
229
+ message.success('画布已清空');
230
+ }
231
+ });
232
+ };
233
+
234
+ // 保存工作流
235
+ const handleSave = () => {
236
+ const workflow = {
237
+ nodes,
238
+ edges,
239
+ timestamp: Date.now()
240
+ };
241
+
242
+ if (onChange) {
243
+ onChange(workflow);
244
+ }
245
+
246
+ if (onSave) {
247
+ onSave(workflow);
248
+ }
249
+
250
+ message.success('工作流已保存');
251
+ };
252
+
253
+ // 验证工作流
254
+ const validateWorkflow = () => {
255
+ if (nodes.length < 2) {
256
+ message.warning('工作流至少需要包含开始和结束节点');
257
+ return false;
258
+ }
259
+
260
+ const hasStart = nodes.some(n => n.data.type === 'start');
261
+ const hasEnd = nodes.some(n => n.data.type === 'end');
262
+
263
+ if (!hasStart || !hasEnd) {
264
+ message.warning('工作流必须包含开始和结束节点');
265
+ return false;
266
+ }
267
+
268
+ return true;
269
+ };
270
+
271
+ return (
272
+ <div style={{ width: '100%', height: '600px' }}>
273
+ <ReactFlowProvider>
274
+ <div ref={reactFlowWrapper} style={{ width: '100%', height: '100%' }}>
275
+ <ReactFlow
276
+ nodes={nodes}
277
+ edges={edges}
278
+ onNodesChange={onNodesChange}
279
+ onEdgesChange={onEdgesChange}
280
+ onConnect={onConnect}
281
+ onNodeClick={onNodeClick}
282
+ onInit={(instance) => { reactFlowInstance.current = instance; }}
283
+ nodeTypes={{ custom: CustomNode }}
284
+ fitView
285
+ >
286
+ <Controls />
287
+ <MiniMap
288
+ nodeColor={node => nodeTypes[node.data.type]?.color || '#ccc'}
289
+ style={{
290
+ height: 120,
291
+ background: '#f0f2f5'
292
+ }}
293
+ />
294
+ <Background variant="dots" gap={12} size={1} />
295
+
296
+ {/* 工具面板 */}
297
+ <Panel position="top-left">
298
+ <Card size="small" style={{ minWidth: '200px' }}>
299
+ <Space direction="vertical" style={{ width: '100%' }}>
300
+ <Text strong>添加节点</Text>
301
+ <Space wrap>
302
+ {Object.entries(nodeTypes).map(([key, type]) => (
303
+ <Button
304
+ key={key}
305
+ size="small"
306
+ icon={type.icon}
307
+ onClick={() => addNode(key)}
308
+ style={{ fontSize: '12px' }}
309
+ >
310
+ {type.label}
311
+ </Button>
312
+ ))}
313
+ </Space>
314
+ </Space>
315
+ </Card>
316
+ </Panel>
317
+
318
+ {/* 操作按钮 */}
319
+ <Panel position="top-right">
320
+ <Space>
321
+ <Button
322
+ type="primary"
323
+ icon={<SaveOutlined />}
324
+ onClick={handleSave}
325
+ disabled={!validateWorkflow()}
326
+ >
327
+ 保存工作流
328
+ </Button>
329
+ <Button
330
+ danger
331
+ icon={<ClearOutlined />}
332
+ onClick={clearCanvas}
333
+ >
334
+ 清空画布
335
+ </Button>
336
+ </Space>
337
+ </Panel>
338
+ </ReactFlow>
339
+ </div>
340
+ </ReactFlowProvider>
341
+
342
+ {/* 节点编辑抽屉 */}
343
+ <Drawer
344
+ title="编辑节点"
345
+ placement="right"
346
+ width={400}
347
+ open={drawerVisible}
348
+ onClose={() => setDrawerVisible(false)}
349
+ footer={
350
+ <Space>
351
+ <Button onClick={() => setDrawerVisible(false)}>取消</Button>
352
+ <Button
353
+ danger
354
+ onClick={() => {
355
+ deleteNode(selectedNode.id);
356
+ setDrawerVisible(false);
357
+ }}
358
+ >
359
+ 删除节点
360
+ </Button>
361
+ <Button type="primary" onClick={() => nodeForm.submit()}>
362
+ 保存
363
+ </Button>
364
+ </Space>
365
+ }
366
+ >
367
+ {selectedNode && (
368
+ <Form
369
+ form={nodeForm}
370
+ layout="vertical"
371
+ onFinish={updateNode}
372
+ >
373
+ <Form.Item
374
+ name="label"
375
+ label="节点名称"
376
+ rules={[{ required: true, message: '请输入节点名称' }]}
377
+ >
378
+ <Input />
379
+ </Form.Item>
380
+
381
+ <Form.Item
382
+ name="description"
383
+ label="节点描述"
384
+ >
385
+ <TextArea rows={2} />
386
+ </Form.Item>
387
+
388
+ <Divider />
389
+
390
+ {/* 根据节点类型显示不同的配置项 */}
391
+ {selectedNode.data.type === 'question' && (
392
+ <Form.Item
393
+ name="prompt"
394
+ label="提示词模板"
395
+ >
396
+ <TextArea rows={4} placeholder="输入提示词模板..." />
397
+ </Form.Item>
398
+ )}
399
+
400
+ {selectedNode.data.type === 'knowledge' && (
401
+ <Form.Item
402
+ name="knowledge_base"
403
+ label="知识库"
404
+ >
405
+ <Select placeholder="选择知识库">
406
+ <Option value="default">默认知识库</Option>
407
+ </Select>
408
+ </Form.Item>
409
+ )}
410
+
411
+ {selectedNode.data.type === 'code' && (
412
+ <Form.Item
413
+ name="code"
414
+ label="代码"
415
+ >
416
+ <TextArea rows={6} placeholder="输入Python代码..." style={{ fontFamily: 'monospace' }} />
417
+ </Form.Item>
418
+ )}
419
+
420
+ {selectedNode.data.type === 'condition' && (
421
+ <>
422
+ <Form.Item
423
+ name="condition"
424
+ label="条件表达式"
425
+ >
426
+ <Input placeholder="例如: score > 80" />
427
+ </Form.Item>
428
+ <Form.Item
429
+ name="true_branch"
430
+ label="满足条件时"
431
+ >
432
+ <Input placeholder="跳转到节点..." />
433
+ </Form.Item>
434
+ <Form.Item
435
+ name="false_branch"
436
+ label="不满足条件时"
437
+ >
438
+ <Input placeholder="跳转到节点..." />
439
+ </Form.Item>
440
+ </>
441
+ )}
442
+ </Form>
443
+ )}
444
+ </Drawer>
445
+ </div>
446
+ );
447
+ };
448
+
449
+ export default WorkflowEditor;
frontend/src/components/common/ProtectedRoute.jsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Navigate } from 'react-router-dom';
3
+ import { useSelector } from 'react-redux';
4
+
5
+ const ProtectedRoute = ({ children, requiredRole }) => {
6
+ const { isAuthenticated, user } = useSelector(state => state.auth);
7
+
8
+ // 未登录,重定向到登录页
9
+ if (!isAuthenticated) {
10
+ return <Navigate to="/login" replace />;
11
+ }
12
+
13
+ // 已登录但角色不匹配
14
+ if (requiredRole && user?.role !== requiredRole) {
15
+ // 根据用户角色重定向到对应页面
16
+ if (user?.role === 'teacher') {
17
+ return <Navigate to="/teacher" replace />;
18
+ } else if (user?.role === 'student') {
19
+ return <Navigate to="/student" replace />;
20
+ }
21
+ return <Navigate to="/login" replace />;
22
+ }
23
+
24
+ // 验证通过,渲染子组件
25
+ return children;
26
+ };
27
+
28
+ export default ProtectedRoute;
frontend/src/components/plugins/CodeExecutor.jsx ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 代码执行插件组件
3
+ * 使用 CodeMirror 编辑器 + 终端输出面板 + 交互式输入
4
+ */
5
+ import { useState, useRef, useEffect } from 'react';
6
+ import { Button, Space, Input, Typography } from 'antd';
7
+ import {
8
+ PlayCircleOutlined,
9
+ StopOutlined,
10
+ ClearOutlined,
11
+ CodeOutlined
12
+ } from '@ant-design/icons';
13
+ import CodeMirror from '@uiw/react-codemirror';
14
+ import { python } from '@codemirror/lang-python';
15
+ import api from '../../services/api';
16
+
17
+ const { Text } = Typography;
18
+
19
+ const CodeExecutor = ({ initialCode = '' }) => {
20
+ const [code, setCode] = useState(initialCode);
21
+ const [output, setOutput] = useState([]);
22
+ const [isRunning, setIsRunning] = useState(false);
23
+ const [needsInput, setNeedsInput] = useState(false);
24
+ const [contextId, setContextId] = useState(null);
25
+ const [userInput, setUserInput] = useState('');
26
+ const outputEndRef = useRef(null);
27
+
28
+ useEffect(() => {
29
+ if (initialCode) setCode(initialCode);
30
+ }, [initialCode]);
31
+
32
+ useEffect(() => {
33
+ outputEndRef.current?.scrollIntoView({ behavior: 'smooth' });
34
+ }, [output]);
35
+
36
+ const appendOutput = (type, text) => {
37
+ setOutput(prev => [...prev, { type, text }]);
38
+ };
39
+
40
+ const handleRun = async () => {
41
+ if (!code.trim()) return;
42
+ setIsRunning(true);
43
+ setNeedsInput(false);
44
+ setOutput([{ type: 'info', text: '>>> 运行中...' }]);
45
+
46
+ try {
47
+ const res = await api.post('/code/execute', { code });
48
+
49
+ if (!res.success) {
50
+ appendOutput('error', res.error || '执行失败');
51
+ if (res.traceback) appendOutput('error', res.traceback);
52
+ setIsRunning(false);
53
+ return;
54
+ }
55
+
56
+ // 显示初始输出
57
+ if (res.output) {
58
+ appendOutput('stdout', res.output);
59
+ }
60
+
61
+ if (res.needsInput) {
62
+ // 代码需要交互输入
63
+ setContextId(res.context_id);
64
+ setNeedsInput(true);
65
+ } else {
66
+ // 代码直接执行完了
67
+ appendOutput('info', '>>> 执行完成');
68
+ setIsRunning(false);
69
+ }
70
+ } catch (error) {
71
+ appendOutput('error', `错误: ${error.message}`);
72
+ setIsRunning(false);
73
+ }
74
+ };
75
+
76
+ const handleInput = async () => {
77
+ if (!contextId) return;
78
+
79
+ const inputText = userInput;
80
+ appendOutput('input', `<<< ${inputText}`);
81
+ setUserInput('');
82
+ setNeedsInput(false);
83
+
84
+ try {
85
+ const res = await api.post('/code/input', {
86
+ context_id: contextId,
87
+ input: inputText
88
+ });
89
+
90
+ if (!res.success) {
91
+ appendOutput('error', res.error || '输入处理失败');
92
+ if (res.traceback) appendOutput('error', res.traceback);
93
+ setIsRunning(false);
94
+ setContextId(null);
95
+ return;
96
+ }
97
+
98
+ if (res.output) {
99
+ appendOutput('stdout', res.output);
100
+ }
101
+
102
+ if (res.needsInput) {
103
+ // 还需要更多输入
104
+ setNeedsInput(true);
105
+ } else {
106
+ // 执行完成
107
+ appendOutput('info', '>>> 执行完成');
108
+ setIsRunning(false);
109
+ setContextId(null);
110
+ }
111
+ } catch (error) {
112
+ appendOutput('error', `错误: ${error.message}`);
113
+ setIsRunning(false);
114
+ setContextId(null);
115
+ }
116
+ };
117
+
118
+ const handleStop = async () => {
119
+ if (contextId) {
120
+ try {
121
+ const res = await api.post('/code/stop', { context_id: contextId });
122
+ if (res.output) {
123
+ appendOutput('stdout', res.output);
124
+ }
125
+ } catch (e) { /* ignore */ }
126
+ }
127
+ appendOutput('warning', '>>> 已终止');
128
+ setIsRunning(false);
129
+ setNeedsInput(false);
130
+ setContextId(null);
131
+ };
132
+
133
+ const handleClear = () => {
134
+ setOutput([]);
135
+ };
136
+
137
+ const getOutputColor = (type) => {
138
+ switch (type) {
139
+ case 'error': return '#ef4444';
140
+ case 'warning': return '#f59e0b';
141
+ case 'input': return '#10b981';
142
+ case 'info': return '#6b7280';
143
+ default: return '#e5e7eb';
144
+ }
145
+ };
146
+
147
+ return (
148
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%', gap: '8px' }}>
149
+ {/* 工具栏 */}
150
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
151
+ <Text strong style={{ fontSize: '13px' }}>
152
+ <CodeOutlined /> Python 编辑器
153
+ </Text>
154
+ <Space size="small">
155
+ {!isRunning ? (
156
+ <Button
157
+ type="primary"
158
+ size="small"
159
+ icon={<PlayCircleOutlined />}
160
+ onClick={handleRun}
161
+ style={{ background: '#10b981', borderColor: '#10b981' }}
162
+ >
163
+ 运行
164
+ </Button>
165
+ ) : (
166
+ <Button
167
+ danger
168
+ size="small"
169
+ icon={<StopOutlined />}
170
+ onClick={handleStop}
171
+ >
172
+ 停止
173
+ </Button>
174
+ )}
175
+ <Button
176
+ size="small"
177
+ icon={<ClearOutlined />}
178
+ onClick={handleClear}
179
+ >
180
+ 清除
181
+ </Button>
182
+ </Space>
183
+ </div>
184
+
185
+ {/* 代码编辑器 */}
186
+ <div style={{
187
+ flex: '0 0 auto',
188
+ maxHeight: '45%',
189
+ overflow: 'auto',
190
+ borderRadius: '6px',
191
+ border: '1px solid rgba(0,0,0,0.1)'
192
+ }}>
193
+ <CodeMirror
194
+ value={code}
195
+ height="auto"
196
+ minHeight="120px"
197
+ maxHeight="300px"
198
+ extensions={[python()]}
199
+ onChange={(val) => setCode(val)}
200
+ theme="light"
201
+ basicSetup={{
202
+ lineNumbers: true,
203
+ highlightActiveLine: true,
204
+ foldGutter: true,
205
+ }}
206
+ />
207
+ </div>
208
+
209
+ {/* 终端输出 */}
210
+ <div style={{
211
+ flex: 1,
212
+ minHeight: '120px',
213
+ background: '#1e1e1e',
214
+ borderRadius: '6px',
215
+ padding: '12px',
216
+ overflow: 'auto',
217
+ fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
218
+ fontSize: '13px',
219
+ }}>
220
+ {output.map((line, i) => (
221
+ <div key={i} style={{ color: getOutputColor(line.type), lineHeight: '1.6', whiteSpace: 'pre-wrap' }}>
222
+ {line.text}
223
+ </div>
224
+ ))}
225
+ {needsInput && (
226
+ <div style={{ display: 'flex', gap: '8px', marginTop: '8px', alignItems: 'center' }}>
227
+ <span style={{ color: '#10b981', flexShrink: 0 }}>{'>>>'}</span>
228
+ <Input
229
+ size="small"
230
+ value={userInput}
231
+ onChange={e => setUserInput(e.target.value)}
232
+ onPressEnter={handleInput}
233
+ placeholder="输入内容后按回车..."
234
+ style={{
235
+ flex: 1,
236
+ background: '#2d2d2d',
237
+ border: '1px solid #444',
238
+ color: '#e5e7eb',
239
+ fontFamily: 'monospace'
240
+ }}
241
+ autoFocus
242
+ />
243
+ </div>
244
+ )}
245
+ <div ref={outputEndRef} />
246
+ </div>
247
+ </div>
248
+ );
249
+ };
250
+
251
+ export default CodeExecutor;
frontend/src/components/plugins/MindmapViewer.jsx ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 思维导图插件组件
3
+ * 使用 markmap 从 Markdown 文本渲染交互式 SVG 思维导图
4
+ */
5
+ import { useState, useRef, useEffect, useCallback } from 'react';
6
+ import { Button, Empty, Typography, Modal } from 'antd';
7
+ import { ExpandOutlined, DownloadOutlined } from '@ant-design/icons';
8
+
9
+ const { Text } = Typography;
10
+
11
+ const MindmapViewer = ({ markdown = '' }) => {
12
+ const svgRef = useRef(null);
13
+ const markmapRef = useRef(null);
14
+ const [fullscreen, setFullscreen] = useState(false);
15
+ const [hasContent, setHasContent] = useState(false);
16
+
17
+ const renderMap = useCallback(async (svgEl, md) => {
18
+ if (!svgEl || !md) return;
19
+
20
+ try {
21
+ // 动态导入 markmap(避免 SSR 问题)
22
+ const { Transformer } = await import('markmap-lib');
23
+ const { Markmap } = await import('markmap-view');
24
+
25
+ const transformer = new Transformer();
26
+ const { root } = transformer.transform(md);
27
+
28
+ // 清除旧内容
29
+ svgEl.innerHTML = '';
30
+
31
+ // 渲染
32
+ markmapRef.current = Markmap.create(svgEl, {
33
+ autoFit: true,
34
+ duration: 300,
35
+ maxWidth: 300,
36
+ }, root);
37
+
38
+ setHasContent(true);
39
+ } catch (e) {
40
+ console.error('Markmap 渲染失败:', e);
41
+ }
42
+ }, []);
43
+
44
+ useEffect(() => {
45
+ if (markdown && svgRef.current) {
46
+ renderMap(svgRef.current, markdown);
47
+ }
48
+ }, [markdown, renderMap]);
49
+
50
+ const handleExport = () => {
51
+ if (!svgRef.current) return;
52
+ const svgData = new XMLSerializer().serializeToString(svgRef.current);
53
+ const blob = new Blob([svgData], { type: 'image/svg+xml' });
54
+ const url = URL.createObjectURL(blob);
55
+ const a = document.createElement('a');
56
+ a.href = url;
57
+ a.download = 'mindmap.svg';
58
+ a.click();
59
+ URL.revokeObjectURL(url);
60
+ };
61
+
62
+ return (
63
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%', gap: '8px' }}>
64
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
65
+ <Text strong style={{ fontSize: '13px' }}>思维导图</Text>
66
+ <div>
67
+ {hasContent && (
68
+ <>
69
+ <Button
70
+ size="small"
71
+ icon={<ExpandOutlined />}
72
+ onClick={() => setFullscreen(true)}
73
+ style={{ marginRight: '8px' }}
74
+ >
75
+ 全屏
76
+ </Button>
77
+ <Button
78
+ size="small"
79
+ icon={<DownloadOutlined />}
80
+ onClick={handleExport}
81
+ >
82
+ 导出 SVG
83
+ </Button>
84
+ </>
85
+ )}
86
+ </div>
87
+ </div>
88
+
89
+ <div style={{
90
+ flex: 1,
91
+ borderRadius: '6px',
92
+ border: '1px solid rgba(0,0,0,0.1)',
93
+ overflow: 'hidden',
94
+ background: '#fff',
95
+ minHeight: '300px',
96
+ display: 'flex',
97
+ alignItems: 'center',
98
+ justifyContent: 'center',
99
+ }}>
100
+ {markdown ? (
101
+ <svg
102
+ ref={svgRef}
103
+ style={{ width: '100%', height: '100%', minHeight: '300px' }}
104
+ />
105
+ ) : (
106
+ <Empty description="等待生成思维导图..." />
107
+ )}
108
+ </div>
109
+
110
+ {/* 全屏模态框 */}
111
+ <Modal
112
+ open={fullscreen}
113
+ onCancel={() => setFullscreen(false)}
114
+ footer={null}
115
+ width="90vw"
116
+ style={{ top: '5vh' }}
117
+ styles={{ body: { height: '80vh', padding: '12px' } }}
118
+ destroyOnClose
119
+ afterOpenChange={(open) => {
120
+ if (open && markdown) {
121
+ // 全屏模态框中重新渲染
122
+ setTimeout(() => {
123
+ const modalSvg = document.querySelector('.ant-modal-body svg');
124
+ if (modalSvg) renderMap(modalSvg, markdown);
125
+ }, 100);
126
+ }
127
+ }}
128
+ >
129
+ <svg style={{ width: '100%', height: '100%' }} />
130
+ </Modal>
131
+ </div>
132
+ );
133
+ };
134
+
135
+ export default MindmapViewer;
frontend/src/components/plugins/Visualization3D.jsx ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 3D 可视化插件组件
3
+ * 后端生成 Plotly HTML,前端通过 iframe 加载
4
+ */
5
+ import { useState, useEffect } from 'react';
6
+ import { Button, Spin, Typography, Modal, Empty } from 'antd';
7
+ import {
8
+ ExpandOutlined,
9
+ ReloadOutlined
10
+ } from '@ant-design/icons';
11
+ import api from '../../services/api';
12
+
13
+ const { Text } = Typography;
14
+
15
+ const Visualization3D = ({ code = '' }) => {
16
+ const [htmlUrl, setHtmlUrl] = useState(null);
17
+ const [loading, setLoading] = useState(false);
18
+ const [error, setError] = useState(null);
19
+ const [fullscreen, setFullscreen] = useState(false);
20
+
21
+ const generate = async (codeToRun) => {
22
+ const targetCode = codeToRun || code;
23
+ if (!targetCode) return;
24
+
25
+ setLoading(true);
26
+ setError(null);
27
+
28
+ try {
29
+ const response = await api.post('/visualization/3d-surface', { code: targetCode });
30
+ if (response.success) {
31
+ // html_url 是相对于后端的路径,需要加上后端地址
32
+ setHtmlUrl(`http://localhost:7860${response.html_url}`);
33
+ } else {
34
+ setError(response.message || '生成失败');
35
+ }
36
+ } catch (e) {
37
+ setError(e.message || '请求失败');
38
+ } finally {
39
+ setLoading(false);
40
+ }
41
+ };
42
+
43
+ // 当 code 变化时自动生成
44
+ useEffect(() => {
45
+ if (code) generate(code);
46
+ }, [code]);
47
+
48
+ return (
49
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%', gap: '8px' }}>
50
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
51
+ <Text strong style={{ fontSize: '13px' }}>3D 可视化</Text>
52
+ <div>
53
+ {htmlUrl && (
54
+ <Button
55
+ size="small"
56
+ icon={<ExpandOutlined />}
57
+ onClick={() => setFullscreen(true)}
58
+ style={{ marginRight: '8px' }}
59
+ >
60
+ 全屏
61
+ </Button>
62
+ )}
63
+ <Button
64
+ size="small"
65
+ icon={<ReloadOutlined />}
66
+ onClick={() => generate()}
67
+ disabled={!code || loading}
68
+ >
69
+ 重新生成
70
+ </Button>
71
+ </div>
72
+ </div>
73
+
74
+ <div style={{
75
+ flex: 1,
76
+ borderRadius: '6px',
77
+ border: '1px solid rgba(0,0,0,0.1)',
78
+ overflow: 'hidden',
79
+ position: 'relative',
80
+ background: '#fff',
81
+ minHeight: '300px'
82
+ }}>
83
+ {loading ? (
84
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
85
+ <Spin tip="正在生成3D图形..."><div style={{ height: 80 }} /></Spin>
86
+ </div>
87
+ ) : error ? (
88
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
89
+ <Empty description={<Text type="danger">{error}</Text>} />
90
+ </div>
91
+ ) : htmlUrl ? (
92
+ <iframe
93
+ src={htmlUrl}
94
+ style={{
95
+ position: 'absolute',
96
+ top: 0,
97
+ left: 0,
98
+ width: '100%',
99
+ height: '100%',
100
+ border: 'none',
101
+ }}
102
+ title="3D Visualization"
103
+ />
104
+ ) : (
105
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
106
+ <Empty description="等待生成3D图形..." />
107
+ </div>
108
+ )}
109
+ </div>
110
+
111
+ {/* 全屏模态框 */}
112
+ <Modal
113
+ open={fullscreen}
114
+ onCancel={() => setFullscreen(false)}
115
+ footer={null}
116
+ width="90vw"
117
+ style={{ top: '5vh' }}
118
+ styles={{ body: { height: '80vh', padding: 0 } }}
119
+ destroyOnClose
120
+ >
121
+ {htmlUrl && (
122
+ <iframe
123
+ src={htmlUrl}
124
+ style={{ width: '100%', height: '80vh', border: 'none' }}
125
+ title="3D Visualization Fullscreen"
126
+ />
127
+ )}
128
+ </Modal>
129
+ </div>
130
+ );
131
+ };
132
+
133
+ export default Visualization3D;
frontend/src/hooks/useTypewriter.js ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 打字机效果Hook
3
+ * 实现逐字显示文本的效果
4
+ */
5
+ import { useState, useEffect, useRef, useCallback } from 'react';
6
+
7
+ const useTypewriter = (text, options = {}) => {
8
+ const {
9
+ speed = 30, // 打字速度(毫秒)
10
+ onComplete = () => {}, // 完成回调
11
+ enabled = true // 是否启用打字机效果
12
+ } = options;
13
+
14
+ const [displayText, setDisplayText] = useState('');
15
+ const [isTyping, setIsTyping] = useState(false);
16
+ const [isPaused, setIsPaused] = useState(false);
17
+ const indexRef = useRef(0);
18
+ const intervalRef = useRef(null);
19
+
20
+ // 开始打字
21
+ const startTyping = useCallback(() => {
22
+ if (!enabled || !text) {
23
+ setDisplayText(text);
24
+ return;
25
+ }
26
+
27
+ setIsTyping(true);
28
+ setIsPaused(false);
29
+ indexRef.current = 0;
30
+ setDisplayText('');
31
+
32
+ intervalRef.current = setInterval(() => {
33
+ if (indexRef.current < text.length) {
34
+ setDisplayText(prev => prev + text[indexRef.current]);
35
+ indexRef.current++;
36
+ } else {
37
+ // 打字完成
38
+ clearInterval(intervalRef.current);
39
+ setIsTyping(false);
40
+ onComplete();
41
+ }
42
+ }, speed);
43
+ }, [text, speed, enabled, onComplete]);
44
+
45
+ // 暂停打字
46
+ const pauseTyping = useCallback(() => {
47
+ if (intervalRef.current) {
48
+ clearInterval(intervalRef.current);
49
+ setIsPaused(true);
50
+ }
51
+ }, []);
52
+
53
+ // 继续打字
54
+ const resumeTyping = useCallback(() => {
55
+ if (isPaused && indexRef.current < text.length) {
56
+ setIsPaused(false);
57
+ intervalRef.current = setInterval(() => {
58
+ if (indexRef.current < text.length) {
59
+ setDisplayText(prev => prev + text[indexRef.current]);
60
+ indexRef.current++;
61
+ } else {
62
+ clearInterval(intervalRef.current);
63
+ setIsTyping(false);
64
+ onComplete();
65
+ }
66
+ }, speed);
67
+ }
68
+ }, [isPaused, text, speed, onComplete]);
69
+
70
+ // 立即显示全部文本
71
+ const skipTyping = useCallback(() => {
72
+ if (intervalRef.current) {
73
+ clearInterval(intervalRef.current);
74
+ }
75
+ setDisplayText(text);
76
+ setIsTyping(false);
77
+ indexRef.current = text.length;
78
+ onComplete();
79
+ }, [text, onComplete]);
80
+
81
+ // 当文本改变时重新开始
82
+ useEffect(() => {
83
+ return () => {
84
+ if (intervalRef.current) {
85
+ clearInterval(intervalRef.current);
86
+ }
87
+ };
88
+ }, []);
89
+
90
+ useEffect(() => {
91
+ if (text && enabled) {
92
+ startTyping();
93
+ } else {
94
+ setDisplayText(text);
95
+ }
96
+ }, [text, enabled]);
97
+
98
+ return {
99
+ displayText,
100
+ isTyping,
101
+ isPaused,
102
+ pauseTyping,
103
+ resumeTyping,
104
+ skipTyping
105
+ };
106
+ };
107
+
108
+ export default useTypewriter;
frontend/src/index.css ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import 'tailwindcss/base';
2
+ @import 'tailwindcss/components';
3
+ @import 'tailwindcss/utilities';
4
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
5
+
6
+ /* 全局样式 */
7
+ @layer base {
8
+ :root {
9
+ /* 主题色 */
10
+ --color-primary: #10b981;
11
+ --color-primary-light: #d1fae5;
12
+ --color-secondary: #f97316;
13
+ --color-secondary-light: #fed7aa;
14
+ --color-tertiary: #8b5cf6;
15
+ --color-tertiary-light: #ddd6fe;
16
+ --color-accent: #14b8a6;
17
+ --color-accent-light: #ccfbf1;
18
+ --color-danger: #f43f5e;
19
+ --color-danger-light: #fecdd3;
20
+
21
+ /* 中性色 */
22
+ --color-neutral-50: #fafbfc;
23
+ --color-neutral-100: #f3f4f6;
24
+ --color-neutral-200: #e5e7eb;
25
+ --color-neutral-300: #d1d5db;
26
+ --color-neutral-400: #9ca3af;
27
+ --color-neutral-500: #6b7280;
28
+ --color-neutral-600: #4b5563;
29
+ --color-neutral-700: #374151;
30
+ --color-neutral-800: #1f2937;
31
+ --color-neutral-900: #111827;
32
+
33
+ /* 阴影 */
34
+ --shadow-soft: 0 2px 8px rgba(0, 0, 0, 0.04);
35
+ --shadow-medium: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
36
+ --shadow-large: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
37
+ }
38
+
39
+ * {
40
+ margin: 0;
41
+ padding: 0;
42
+ box-sizing: border-box;
43
+ }
44
+
45
+ body {
46
+ font-family: 'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif;
47
+ background-color: var(--color-neutral-50);
48
+ color: var(--color-neutral-800);
49
+ -webkit-font-smoothing: antialiased;
50
+ -moz-osx-font-smoothing: grayscale;
51
+ }
52
+
53
+ /* 自定义滚动条 */
54
+ ::-webkit-scrollbar {
55
+ width: 8px;
56
+ height: 8px;
57
+ }
58
+
59
+ ::-webkit-scrollbar-track {
60
+ background: var(--color-neutral-100);
61
+ }
62
+
63
+ ::-webkit-scrollbar-thumb {
64
+ background: var(--color-neutral-400);
65
+ border-radius: 4px;
66
+ }
67
+
68
+ ::-webkit-scrollbar-thumb:hover {
69
+ background: var(--color-neutral-500);
70
+ }
71
+ }
72
+
73
+ /* 卡片样式 */
74
+ @layer components {
75
+ .card {
76
+ @apply bg-white rounded-lg p-6 transition-all duration-300;
77
+ box-shadow: var(--shadow-soft);
78
+ }
79
+
80
+ .card:hover {
81
+ transform: translateY(-2px);
82
+ box-shadow: var(--shadow-medium);
83
+ }
84
+
85
+ /* 按钮基础样式 */
86
+ .btn {
87
+ @apply px-4 py-2 rounded-lg font-medium transition-all duration-300 inline-flex items-center justify-center;
88
+ }
89
+
90
+ .btn-primary {
91
+ @apply bg-primary text-white hover:bg-primary/90;
92
+ }
93
+
94
+ .btn-secondary {
95
+ @apply bg-secondary text-white hover:bg-secondary/90;
96
+ }
97
+
98
+ .btn-tertiary {
99
+ @apply bg-tertiary text-white hover:bg-tertiary/90;
100
+ }
101
+
102
+ .btn-outline {
103
+ @apply border-2 border-primary text-primary hover:bg-primary hover:text-white;
104
+ }
105
+
106
+ /* 浮动背景球 */
107
+ .floating-ball {
108
+ position: absolute;
109
+ border-radius: 50%;
110
+ filter: blur(100px);
111
+ opacity: 0.15;
112
+ animation: float 20s ease-in-out infinite;
113
+ pointer-events: none;
114
+ }
115
+
116
+ .floating-ball-1 {
117
+ width: 300px;
118
+ height: 300px;
119
+ background: linear-gradient(135deg, var(--color-primary), var(--color-accent));
120
+ top: -150px;
121
+ left: -150px;
122
+ }
123
+
124
+ .floating-ball-2 {
125
+ width: 400px;
126
+ height: 400px;
127
+ background: linear-gradient(135deg, var(--color-secondary), var(--color-tertiary));
128
+ bottom: -200px;
129
+ right: -200px;
130
+ animation-delay: 5s;
131
+ }
132
+
133
+ .floating-ball-3 {
134
+ width: 250px;
135
+ height: 250px;
136
+ background: linear-gradient(135deg, var(--color-tertiary), var(--color-primary));
137
+ top: 50%;
138
+ left: 50%;
139
+ transform: translate(-50%, -50%);
140
+ animation-delay: 10s;
141
+ }
142
+
143
+ /* 加载动画 */
144
+ .loading-spinner {
145
+ @apply inline-block w-8 h-8 border-4 border-primary border-t-transparent rounded-full;
146
+ animation: spin 1s linear infinite;
147
+ }
148
+ }
149
+
150
+ /* 动画 */
151
+ @layer utilities {
152
+ @keyframes float {
153
+ 0%, 100% {
154
+ transform: translate(0, 0) scale(1);
155
+ }
156
+ 33% {
157
+ transform: translate(30px, -30px) scale(1.1);
158
+ }
159
+ 66% {
160
+ transform: translate(-20px, 20px) scale(0.9);
161
+ }
162
+ }
163
+
164
+ @keyframes spin {
165
+ to {
166
+ transform: rotate(360deg);
167
+ }
168
+ }
169
+ }
170
+
171
+ /* Ant Design 覆盖 */
172
+ .ant-btn-primary {
173
+ background-color: var(--color-primary);
174
+ border-color: var(--color-primary);
175
+ }
176
+
177
+ .ant-btn-primary:hover {
178
+ background-color: var(--color-primary);
179
+ border-color: var(--color-primary);
180
+ opacity: 0.9;
181
+ }
182
+
183
+ .ant-menu-item-selected {
184
+ color: var(--color-primary) !important;
185
+ }
186
+
187
+ .ant-menu-item-selected::after {
188
+ border-color: var(--color-primary) !important;
189
+ }
frontend/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.jsx'
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
frontend/src/pages/LoginPage.jsx ADDED
@@ -0,0 +1,497 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { useDispatch } from 'react-redux';
4
+ import { Form, Input, Button, Tabs, message, Divider, Space } from 'antd';
5
+ import {
6
+ MailOutlined,
7
+ PhoneOutlined,
8
+ SafetyOutlined,
9
+ UserOutlined,
10
+ LockOutlined,
11
+ } from '@ant-design/icons';
12
+ import { motion } from 'framer-motion';
13
+ import authService from '../services/authService';
14
+ import { loginSuccess } from '../store/slices/authSlice';
15
+
16
+ const LoginPage = () => {
17
+ const [form] = Form.useForm();
18
+ const navigate = useNavigate();
19
+ const dispatch = useDispatch();
20
+ const [loading, setLoading] = useState(false);
21
+ const [activeTab, setActiveTab] = useState('email'); // 默认使用邮箱登录
22
+ const [userRole, setUserRole] = useState('student'); // 默认学生角色
23
+ const [countdown, setCountdown] = useState(0);
24
+
25
+ // 发送邮箱验证码
26
+ const handleSendEmailCode = async () => {
27
+ const email = form.getFieldValue('email');
28
+ if (!email) {
29
+ message.warning('请输入邮箱地址');
30
+ return;
31
+ }
32
+
33
+ try {
34
+ setLoading(true);
35
+ await authService.sendEmailCode(email);
36
+ message.success('验证码已发送到您的邮箱');
37
+
38
+ // 开始倒计时
39
+ let count = 60;
40
+ setCountdown(count);
41
+ const timer = setInterval(() => {
42
+ count--;
43
+ setCountdown(count);
44
+ if (count <= 0) {
45
+ clearInterval(timer);
46
+ }
47
+ }, 1000);
48
+ } catch (error) {
49
+ message.error('发送验证码失败');
50
+ } finally {
51
+ setLoading(false);
52
+ }
53
+ };
54
+
55
+ // 发送手机验证码(模拟)
56
+ const handleSendPhoneCode = async () => {
57
+ const phone = form.getFieldValue('phone');
58
+ if (!phone) {
59
+ message.warning('请输入手机号');
60
+ return;
61
+ }
62
+
63
+ setLoading(true);
64
+ // 模拟发送
65
+ setTimeout(() => {
66
+ message.success('测试验证码:123456(开发模式)');
67
+ setLoading(false);
68
+
69
+ // 开始倒计时
70
+ let count = 60;
71
+ setCountdown(count);
72
+ const timer = setInterval(() => {
73
+ count--;
74
+ setCountdown(count);
75
+ if (count <= 0) {
76
+ clearInterval(timer);
77
+ }
78
+ }, 1000);
79
+ }, 1000);
80
+ };
81
+
82
+ // 处理登录
83
+ const handleLogin = async (values) => {
84
+ try {
85
+ setLoading(true);
86
+ let response;
87
+
88
+ switch (activeTab) {
89
+ case 'email':
90
+ response = await authService.loginWithEmailCode(values.email, values.emailCode, userRole);
91
+ break;
92
+ case 'phone':
93
+ response = await authService.loginWithPhoneCode(values.phone, values.phoneCode, userRole);
94
+ break;
95
+ case 'password':
96
+ response = await authService.loginWithPassword(values.username, values.password);
97
+ break;
98
+ default:
99
+ break;
100
+ }
101
+
102
+ if (response.success) {
103
+ dispatch(loginSuccess({
104
+ user: response.data.user,
105
+ token: response.data.token,
106
+ }));
107
+ message.success('登录成功');
108
+
109
+ // 根据角色跳转
110
+ if (response.data.user.role === 'teacher') {
111
+ navigate('/teacher');
112
+ } else {
113
+ navigate('/student');
114
+ }
115
+ }
116
+ } catch (error) {
117
+ message.error(error.response?.data?.message || '登录失败');
118
+ } finally {
119
+ setLoading(false);
120
+ }
121
+ };
122
+
123
+ const tabItems = [
124
+ {
125
+ key: 'email',
126
+ label: (
127
+ <span className="text-base font-medium">
128
+ 邮箱登录
129
+ </span>
130
+ ),
131
+ children: (
132
+ <>
133
+ <Form.Item
134
+ name="email"
135
+ rules={[
136
+ { required: activeTab === 'email', message: '请输入邮箱' },
137
+ { type: 'email', message: '请输入有效的邮箱地址' },
138
+ ]}
139
+ >
140
+ <Input
141
+ prefix={<MailOutlined className="text-neutral-400" />}
142
+ placeholder="邮箱地址"
143
+ size="large"
144
+ className="rounded-lg"
145
+ />
146
+ </Form.Item>
147
+ <Form.Item
148
+ name="emailCode"
149
+ rules={[{ required: activeTab === 'email', message: '请输入验证码' }]}
150
+ >
151
+ <Space.Compact style={{ width: '100%' }}>
152
+ <Input
153
+ prefix={<SafetyOutlined className="text-neutral-400" />}
154
+ placeholder="验证码"
155
+ size="large"
156
+ className="rounded-l-lg"
157
+ />
158
+ <Button
159
+ size="large"
160
+ onClick={handleSendEmailCode}
161
+ disabled={countdown > 0}
162
+ className="rounded-r-lg px-6"
163
+ style={{
164
+ backgroundColor: 'rgba(16, 185, 129, 0.1)',
165
+ borderColor: 'rgba(16, 185, 129, 0.3)',
166
+ color: '#10b981'
167
+ }}
168
+ >
169
+ {countdown > 0 ? `${countdown}秒后重试` : '发送验证码'}
170
+ </Button>
171
+ </Space.Compact>
172
+ </Form.Item>
173
+ </>
174
+ ),
175
+ },
176
+ {
177
+ key: 'phone',
178
+ label: (
179
+ <span className="text-base font-medium">
180
+ 手机登录
181
+ </span>
182
+ ),
183
+ children: (
184
+ <>
185
+ <Form.Item
186
+ name="phone"
187
+ rules={[
188
+ { required: activeTab === 'phone', message: '请输入手机号' },
189
+ { pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号' },
190
+ ]}
191
+ >
192
+ <Input
193
+ prefix={<PhoneOutlined className="text-neutral-400" />}
194
+ placeholder="手机号"
195
+ size="large"
196
+ className="rounded-lg"
197
+ />
198
+ </Form.Item>
199
+ <Form.Item
200
+ name="phoneCode"
201
+ rules={[{ required: activeTab === 'phone', message: '请输入验证码' }]}
202
+ >
203
+ <Space.Compact style={{ width: '100%' }}>
204
+ <Input
205
+ prefix={<SafetyOutlined className="text-neutral-400" />}
206
+ placeholder="验证码"
207
+ size="large"
208
+ className="rounded-l-lg"
209
+ />
210
+ <Button
211
+ size="large"
212
+ onClick={handleSendPhoneCode}
213
+ disabled={countdown > 0}
214
+ className="rounded-r-lg px-6"
215
+ style={{
216
+ backgroundColor: 'rgba(249, 115, 22, 0.1)',
217
+ borderColor: 'rgba(249, 115, 22, 0.3)',
218
+ color: '#f97316'
219
+ }}
220
+ >
221
+ {countdown > 0 ? `${countdown}秒后重试` : '发送验证码'}
222
+ </Button>
223
+ </Space.Compact>
224
+ </Form.Item>
225
+ </>
226
+ ),
227
+ },
228
+ {
229
+ key: 'password',
230
+ label: (
231
+ <span className="text-base font-medium">
232
+ 密码登录
233
+ </span>
234
+ ),
235
+ children: (
236
+ <>
237
+ <Form.Item
238
+ name="username"
239
+ rules={[{ required: activeTab === 'password', message: '请输入用户名' }]}
240
+ >
241
+ <Input
242
+ prefix={<UserOutlined className="text-neutral-400" />}
243
+ placeholder="用户名"
244
+ size="large"
245
+ className="rounded-lg"
246
+ />
247
+ </Form.Item>
248
+ <Form.Item
249
+ name="password"
250
+ rules={[{ required: activeTab === 'password', message: '请输入密码' }]}
251
+ >
252
+ <Input.Password
253
+ prefix={<LockOutlined className="text-neutral-400" />}
254
+ placeholder="密码"
255
+ size="large"
256
+ className="rounded-lg"
257
+ />
258
+ </Form.Item>
259
+ </>
260
+ ),
261
+ },
262
+ ];
263
+
264
+ return (
265
+ <div className="min-h-screen flex relative overflow-hidden" style={{ backgroundColor: '#fafbfc' }}>
266
+ {/* 装饰性背景元素 - 使用单色极淡透明度 */}
267
+ <div
268
+ className="absolute top-20 left-20 w-96 h-96 rounded-full opacity-10"
269
+ style={{
270
+ background: '#10b981',
271
+ filter: 'blur(120px)'
272
+ }}
273
+ />
274
+ <div
275
+ className="absolute bottom-20 right-20 w-96 h-96 rounded-full opacity-10"
276
+ style={{
277
+ background: '#f97316',
278
+ filter: 'blur(120px)'
279
+ }}
280
+ />
281
+
282
+ {/* 左侧装饰区 */}
283
+ <div className="hidden lg:flex lg:w-1/2 relative p-12">
284
+ <div className="relative z-10 flex flex-col justify-center items-center w-full">
285
+ <motion.div
286
+ initial={{ opacity: 0, y: 20 }}
287
+ animate={{ opacity: 1, y: 0 }}
288
+ transition={{ duration: 0.6 }}
289
+ className="w-full max-w-lg"
290
+ >
291
+ <h1 className="text-5xl font-bold mb-6">
292
+ <span style={{ color: '#10b981' }}>智能</span>
293
+ <span style={{ color: '#f97316' }} className="mx-2">教育</span>
294
+ <span style={{ color: '#8b5cf6' }}>助手</span>
295
+ </h1>
296
+ <p className="text-xl text-neutral-600 mb-12">
297
+ AI驱动的个性化学习平台
298
+ </p>
299
+
300
+ {/* 特色卡片 - 符合设计要求的卡片 */}
301
+ <div className="grid grid-cols-2 gap-6">
302
+ <motion.div
303
+ className="p-6 bg-white rounded-xl transition-all duration-300"
304
+ style={{
305
+ border: '1px solid rgba(16, 185, 129, 0.2)',
306
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)'
307
+ }}
308
+ whileHover={{
309
+ y: -4,
310
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',
311
+ borderColor: 'rgba(16, 185, 129, 0.4)'
312
+ }}
313
+ >
314
+ <div className="w-12 h-12 rounded-lg flex items-center justify-center mb-4"
315
+ style={{ backgroundColor: 'rgba(16, 185, 129, 0.1)' }}>
316
+ <svg className="w-6 h-6" style={{ color: '#10b981' }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
317
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
318
+ </svg>
319
+ </div>
320
+ <h3 className="font-semibold text-neutral-800 mb-2">个性化学习</h3>
321
+ <p className="text-sm text-neutral-600">根据学习进度智能调整</p>
322
+ </motion.div>
323
+
324
+ <motion.div
325
+ className="p-6 bg-white rounded-xl transition-all duration-300"
326
+ style={{
327
+ border: '1px solid rgba(249, 115, 22, 0.2)',
328
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)'
329
+ }}
330
+ whileHover={{
331
+ y: -4,
332
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',
333
+ borderColor: 'rgba(249, 115, 22, 0.4)'
334
+ }}
335
+ >
336
+ <div className="w-12 h-12 rounded-lg flex items-center justify-center mb-4"
337
+ style={{ backgroundColor: 'rgba(249, 115, 22, 0.1)' }}>
338
+ <svg className="w-6 h-6" style={{ color: '#f97316' }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
339
+ <path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
340
+ </svg>
341
+ </div>
342
+ <h3 className="font-semibold text-neutral-800 mb-2">AI助教</h3>
343
+ <p className="text-sm text-neutral-600">24小时在线答疑解惑</p>
344
+ </motion.div>
345
+
346
+ <motion.div
347
+ className="p-6 bg-white rounded-xl transition-all duration-300"
348
+ style={{
349
+ border: '1px solid rgba(139, 92, 246, 0.2)',
350
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)'
351
+ }}
352
+ whileHover={{
353
+ y: -4,
354
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',
355
+ borderColor: 'rgba(139, 92, 246, 0.4)'
356
+ }}
357
+ >
358
+ <div className="w-12 h-12 rounded-lg flex items-center justify-center mb-4"
359
+ style={{ backgroundColor: 'rgba(139, 92, 246, 0.1)' }}>
360
+ <svg className="w-6 h-6" style={{ color: '#8b5cf6' }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
361
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
362
+ </svg>
363
+ </div>
364
+ <h3 className="font-semibold text-neutral-800 mb-2">实时反馈</h3>
365
+ <p className="text-sm text-neutral-600">即时评估学习效果</p>
366
+ </motion.div>
367
+
368
+ <motion.div
369
+ className="p-6 bg-white rounded-xl transition-all duration-300"
370
+ style={{
371
+ border: '1px solid rgba(20, 184, 166, 0.2)',
372
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)'
373
+ }}
374
+ whileHover={{
375
+ y: -4,
376
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',
377
+ borderColor: 'rgba(20, 184, 166, 0.4)'
378
+ }}
379
+ >
380
+ <div className="w-12 h-12 rounded-lg flex items-center justify-center mb-4"
381
+ style={{ backgroundColor: 'rgba(20, 184, 166, 0.1)' }}>
382
+ <svg className="w-6 h-6" style={{ color: '#14b8a6' }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
383
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
384
+ </svg>
385
+ </div>
386
+ <h3 className="font-semibold text-neutral-800 mb-2">知识管理</h3>
387
+ <p className="text-sm text-neutral-600">系统化知识体系构建</p>
388
+ </motion.div>
389
+ </div>
390
+ </motion.div>
391
+ </div>
392
+ </div>
393
+
394
+ {/* 右侧登录区 */}
395
+ <div className="flex-1 flex items-center justify-center p-8">
396
+ <motion.div
397
+ initial={{ opacity: 0, x: 20 }}
398
+ animate={{ opacity: 1, x: 0 }}
399
+ transition={{ duration: 0.6 }}
400
+ className="w-full max-w-md"
401
+ >
402
+ <div
403
+ className="bg-white rounded-2xl p-8 transition-all duration-300"
404
+ style={{
405
+ border: '1px solid rgba(0, 0, 0, 0.08)',
406
+ boxShadow: '0 4px 6px rgba(0, 0, 0, 0.04)'
407
+ }}
408
+ >
409
+ <div className="text-center mb-8">
410
+ <h2 className="text-3xl font-bold text-neutral-800">欢迎回来</h2>
411
+ <p className="text-neutral-500 mt-2">登录您的账户继续学习</p>
412
+ </div>
413
+
414
+ {/* 角色选择 */}
415
+ <div className="mb-6">
416
+ <div className="flex gap-3 p-1 bg-neutral-100 rounded-lg">
417
+ <button
418
+ type="button"
419
+ onClick={() => setUserRole('teacher')}
420
+ className={`flex-1 py-2.5 px-4 rounded-lg font-medium transition-all duration-200 ${
421
+ userRole === 'teacher'
422
+ ? 'bg-white text-emerald-600 shadow-sm'
423
+ : 'text-neutral-600 hover:text-neutral-800'
424
+ }`}
425
+ >
426
+ 教师
427
+ </button>
428
+ <button
429
+ type="button"
430
+ onClick={() => setUserRole('student')}
431
+ className={`flex-1 py-2.5 px-4 rounded-lg font-medium transition-all duration-200 ${
432
+ userRole === 'student'
433
+ ? 'bg-white text-orange-600 shadow-sm'
434
+ : 'text-neutral-600 hover:text-neutral-800'
435
+ }`}
436
+ >
437
+ 学生
438
+ </button>
439
+ </div>
440
+ </div>
441
+
442
+ <Form
443
+ form={form}
444
+ onFinish={handleLogin}
445
+ layout="vertical"
446
+ size="large"
447
+ >
448
+ <Tabs
449
+ activeKey={activeTab}
450
+ onChange={setActiveTab}
451
+ items={tabItems}
452
+ className="mb-4"
453
+ />
454
+
455
+ <Form.Item className="mb-4">
456
+ <Button
457
+ type="primary"
458
+ htmlType="submit"
459
+ loading={loading}
460
+ block
461
+ size="large"
462
+ className="rounded-lg h-12 text-base font-medium"
463
+ style={{
464
+ backgroundColor: activeTab === 'email' ? '#10b981' : activeTab === 'phone' ? '#f97316' : '#8b5cf6',
465
+ borderColor: activeTab === 'email' ? '#10b981' : activeTab === 'phone' ? '#f97316' : '#8b5cf6'
466
+ }}
467
+ >
468
+ 登录
469
+ </Button>
470
+ </Form.Item>
471
+
472
+ <Divider className="text-neutral-400">或</Divider>
473
+
474
+ <div className="text-center">
475
+ <span className="text-neutral-500">还没有账号?</span>
476
+ <Button
477
+ type="link"
478
+ onClick={() => navigate('/register')}
479
+ style={{ color: '#10b981' }}
480
+ className="font-medium"
481
+ >
482
+ 立即注册
483
+ </Button>
484
+ </div>
485
+ </Form>
486
+ </div>
487
+
488
+ <div className="text-center mt-8 text-sm text-neutral-400">
489
+ © 2024 智能教育助手 - AI驱动的未来教育
490
+ </div>
491
+ </motion.div>
492
+ </div>
493
+ </div>
494
+ );
495
+ };
496
+
497
+ export default LoginPage;
frontend/src/pages/RegisterPage.jsx ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { Form, Input, Button, message } from 'antd';
4
+ import { UserOutlined, LockOutlined } from '@ant-design/icons';
5
+ import { motion } from 'framer-motion';
6
+ import authService from '../services/authService';
7
+
8
+ const RegisterPage = () => {
9
+ const [form] = Form.useForm();
10
+ const navigate = useNavigate();
11
+ const [loading, setLoading] = useState(false);
12
+ const [userRole, setUserRole] = useState('student');
13
+
14
+ const handleRegister = async (values) => {
15
+ if (values.password !== values.confirmPassword) {
16
+ message.error('两次输入的密码不一致');
17
+ return;
18
+ }
19
+
20
+ try {
21
+ setLoading(true);
22
+ const response = await authService.register({
23
+ username: values.username,
24
+ password: values.password,
25
+ role: userRole,
26
+ });
27
+
28
+ if (response.success) {
29
+ message.success('注册成功,请登录');
30
+ navigate('/login');
31
+ }
32
+ } catch (error) {
33
+ message.error(error.response?.data?.message || '注册失败');
34
+ } finally {
35
+ setLoading(false);
36
+ }
37
+ };
38
+
39
+ return (
40
+ <div className="min-h-screen flex relative overflow-hidden" style={{ backgroundColor: '#fafbfc' }}>
41
+ <div
42
+ className="absolute top-20 left-20 w-96 h-96 rounded-full opacity-10"
43
+ style={{ background: '#8b5cf6', filter: 'blur(120px)' }}
44
+ />
45
+ <div
46
+ className="absolute bottom-20 right-20 w-96 h-96 rounded-full opacity-10"
47
+ style={{ background: '#10b981', filter: 'blur(120px)' }}
48
+ />
49
+
50
+ <div className="flex-1 flex items-center justify-center p-8">
51
+ <motion.div
52
+ initial={{ opacity: 0, y: 20 }}
53
+ animate={{ opacity: 1, y: 0 }}
54
+ transition={{ duration: 0.6 }}
55
+ className="w-full max-w-md"
56
+ >
57
+ <div
58
+ className="bg-white rounded-2xl p-8 transition-all duration-300"
59
+ style={{
60
+ border: '1px solid rgba(0, 0, 0, 0.08)',
61
+ boxShadow: '0 4px 6px rgba(0, 0, 0, 0.04)',
62
+ }}
63
+ >
64
+ <div className="text-center mb-8">
65
+ <h2 className="text-3xl font-bold text-neutral-800">创建账号</h2>
66
+ <p className="text-neutral-500 mt-2">注册一个新账号开始使用</p>
67
+ </div>
68
+
69
+ {/* 角色选择 */}
70
+ <div className="mb-6">
71
+ <div className="flex gap-3 p-1 bg-neutral-100 rounded-lg">
72
+ <button
73
+ type="button"
74
+ onClick={() => setUserRole('teacher')}
75
+ className={`flex-1 py-2.5 px-4 rounded-lg font-medium transition-all duration-200 ${
76
+ userRole === 'teacher'
77
+ ? 'bg-white text-emerald-600 shadow-sm'
78
+ : 'text-neutral-600 hover:text-neutral-800'
79
+ }`}
80
+ >
81
+ 教师
82
+ </button>
83
+ <button
84
+ type="button"
85
+ onClick={() => setUserRole('student')}
86
+ className={`flex-1 py-2.5 px-4 rounded-lg font-medium transition-all duration-200 ${
87
+ userRole === 'student'
88
+ ? 'bg-white text-orange-600 shadow-sm'
89
+ : 'text-neutral-600 hover:text-neutral-800'
90
+ }`}
91
+ >
92
+ 学生
93
+ </button>
94
+ </div>
95
+ </div>
96
+
97
+ <Form form={form} onFinish={handleRegister} layout="vertical" size="large">
98
+ <Form.Item
99
+ name="username"
100
+ rules={[{ required: true, message: '请输入用户名' }]}
101
+ >
102
+ <Input
103
+ prefix={<UserOutlined className="text-neutral-400" />}
104
+ placeholder="用户名"
105
+ size="large"
106
+ className="rounded-lg"
107
+ />
108
+ </Form.Item>
109
+
110
+ <Form.Item
111
+ name="password"
112
+ rules={[
113
+ { required: true, message: '请输入密码' },
114
+ { min: 6, message: '密码至少6位' },
115
+ ]}
116
+ >
117
+ <Input.Password
118
+ prefix={<LockOutlined className="text-neutral-400" />}
119
+ placeholder="密码"
120
+ size="large"
121
+ className="rounded-lg"
122
+ />
123
+ </Form.Item>
124
+
125
+ <Form.Item
126
+ name="confirmPassword"
127
+ rules={[
128
+ { required: true, message: '请确认密码' },
129
+ ]}
130
+ >
131
+ <Input.Password
132
+ prefix={<LockOutlined className="text-neutral-400" />}
133
+ placeholder="确认密码"
134
+ size="large"
135
+ className="rounded-lg"
136
+ />
137
+ </Form.Item>
138
+
139
+ <Form.Item className="mb-4">
140
+ <Button
141
+ type="primary"
142
+ htmlType="submit"
143
+ loading={loading}
144
+ block
145
+ size="large"
146
+ className="rounded-lg h-12 text-base font-medium"
147
+ style={{ backgroundColor: '#8b5cf6', borderColor: '#8b5cf6' }}
148
+ >
149
+ 注册
150
+ </Button>
151
+ </Form.Item>
152
+
153
+ <div className="text-center">
154
+ <span className="text-neutral-500">已有账号?</span>
155
+ <Button
156
+ type="link"
157
+ onClick={() => navigate('/login')}
158
+ style={{ color: '#10b981' }}
159
+ className="font-medium"
160
+ >
161
+ 返回登录
162
+ </Button>
163
+ </div>
164
+ </Form>
165
+ </div>
166
+
167
+ <div className="text-center mt-8 text-sm text-neutral-400">
168
+ &copy; 2024 智能教育助手 - AI驱动的未来教育
169
+ </div>
170
+ </motion.div>
171
+ </div>
172
+ </div>
173
+ );
174
+ };
175
+
176
+ export default RegisterPage;
frontend/src/pages/student/AgentChat.css ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* 思考中动画 */
2
+ .thinking-dots {
3
+ display: inline-flex;
4
+ align-items: center;
5
+ gap: 4px;
6
+ }
7
+
8
+ .thinking-dots span {
9
+ width: 8px;
10
+ height: 8px;
11
+ background-color: #8b5cf6;
12
+ border-radius: 50%;
13
+ animation: thinking 1.4s ease-in-out infinite;
14
+ }
15
+
16
+ .thinking-dots span:nth-child(1) {
17
+ animation-delay: -0.32s;
18
+ }
19
+
20
+ .thinking-dots span:nth-child(2) {
21
+ animation-delay: -0.16s;
22
+ }
23
+
24
+ @keyframes thinking {
25
+ 0%, 80%, 100% {
26
+ transform: scale(0);
27
+ opacity: 0.5;
28
+ }
29
+ 40% {
30
+ transform: scale(1);
31
+ opacity: 1;
32
+ }
33
+ }
34
+
35
+ /* 打字光标 */
36
+ .typing-cursor {
37
+ display: inline-block;
38
+ width: 2px;
39
+ height: 16px;
40
+ background-color: #8b5cf6;
41
+ animation: blink 1s infinite;
42
+ margin-left: 2px;
43
+ vertical-align: text-bottom;
44
+ }
45
+
46
+ @keyframes blink {
47
+ 0%, 50% {
48
+ opacity: 1;
49
+ }
50
+ 51%, 100% {
51
+ opacity: 0;
52
+ }
53
+ }
54
+
55
+ /* Markdown样式 */
56
+ .markdown-body {
57
+ font-size: 14px;
58
+ line-height: 1.6;
59
+ color: #374151;
60
+ }
61
+
62
+ .markdown-body h1,
63
+ .markdown-body h2,
64
+ .markdown-body h3,
65
+ .markdown-body h4,
66
+ .markdown-body h5,
67
+ .markdown-body h6 {
68
+ margin-top: 16px;
69
+ margin-bottom: 8px;
70
+ font-weight: 600;
71
+ }
72
+
73
+ .markdown-body h1 {
74
+ font-size: 1.5em;
75
+ border-bottom: 1px solid #e5e7eb;
76
+ padding-bottom: 8px;
77
+ }
78
+
79
+ .markdown-body h2 {
80
+ font-size: 1.3em;
81
+ }
82
+
83
+ .markdown-body h3 {
84
+ font-size: 1.15em;
85
+ }
86
+
87
+ .markdown-body pre {
88
+ margin: 12px 0;
89
+ }
90
+
91
+ .markdown-body table {
92
+ border-collapse: collapse;
93
+ width: 100%;
94
+ margin: 12px 0;
95
+ }
96
+
97
+ .markdown-body table th,
98
+ .markdown-body table td {
99
+ border: 1px solid #e5e7eb;
100
+ padding: 8px 12px;
101
+ }
102
+
103
+ .markdown-body table th {
104
+ background-color: #f9fafb;
105
+ font-weight: 600;
106
+ }
107
+
108
+ .markdown-body hr {
109
+ border: none;
110
+ border-top: 1px solid #e5e7eb;
111
+ margin: 16px 0;
112
+ }
113
+
114
+ /* 消息样式 */
115
+ .bot-message {
116
+ word-wrap: break-word;
117
+ word-break: break-word;
118
+ }
119
+
120
+ /* 自定义滚动条 */
121
+ ::-webkit-scrollbar {
122
+ width: 8px;
123
+ height: 8px;
124
+ }
125
+
126
+ ::-webkit-scrollbar-track {
127
+ background: #f3f4f6;
128
+ border-radius: 4px;
129
+ }
130
+
131
+ ::-webkit-scrollbar-thumb {
132
+ background: #d1d5db;
133
+ border-radius: 4px;
134
+ }
135
+
136
+ ::-webkit-scrollbar-thumb:hover {
137
+ background: #9ca3af;
138
+ }
139
+
140
+ /* 插件面板 Tabs 撑满高度 */
141
+ .ant-tabs {
142
+ display: flex;
143
+ flex-direction: column;
144
+ }
145
+
146
+ .ant-tabs-content-holder {
147
+ flex: 1;
148
+ overflow: hidden;
149
+ }
150
+
151
+ .ant-tabs-content {
152
+ height: 100%;
153
+ }
154
+
155
+ .ant-tabs-tabpane {
156
+ height: 100%;
157
+ padding: 12px 16px;
158
+ overflow: auto;
159
+ }
frontend/src/pages/student/AgentChat.jsx ADDED
@@ -0,0 +1,854 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 学生端 - Agent对话页面
3
+ * 支持SSE流式响应、参考文献显示、代码高亮、插件面板等功能
4
+ */
5
+ import { useState, useEffect, useRef, useCallback } from 'react';
6
+ import { useParams, useSearchParams } from 'react-router-dom';
7
+ import {
8
+ Layout,
9
+ Input,
10
+ Button,
11
+ Card,
12
+ Space,
13
+ Avatar,
14
+ Spin,
15
+ message,
16
+ Typography,
17
+ Tag,
18
+ Empty,
19
+ Tabs
20
+ } from 'antd';
21
+ import {
22
+ SendOutlined,
23
+ RobotOutlined,
24
+ UserOutlined,
25
+ CodeOutlined,
26
+ CopyOutlined,
27
+ BookOutlined,
28
+ FileTextOutlined,
29
+ AppstoreOutlined,
30
+ CloseOutlined,
31
+ BarChartOutlined,
32
+ BranchesOutlined
33
+ } from '@ant-design/icons';
34
+ import ReactMarkdown from 'react-markdown';
35
+ import remarkGfm from 'remark-gfm';
36
+ import remarkMath from 'remark-math';
37
+ import rehypeKatex from 'rehype-katex';
38
+ import 'katex/dist/katex.min.css';
39
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
40
+ import { tomorrow } from 'react-syntax-highlighter/dist/esm/styles/prism';
41
+ import api from '../../services/api';
42
+ import CodeExecutor from '../../components/plugins/CodeExecutor';
43
+ import Visualization3D from '../../components/plugins/Visualization3D';
44
+ import MindmapViewer from '../../components/plugins/MindmapViewer';
45
+ import './AgentChat.css';
46
+
47
+ const { Content } = Layout;
48
+ const { TextArea } = Input;
49
+ const { Title, Text, Paragraph } = Typography;
50
+
51
+ const AgentChat = () => {
52
+ const { agentId } = useParams();
53
+ const [searchParams] = useSearchParams();
54
+ const token = searchParams.get('token');
55
+
56
+ // 状态管理
57
+ const [agent, setAgent] = useState(null);
58
+ const [messages, setMessages] = useState([]);
59
+ const [isDragging, setIsDragging] = useState(false);
60
+ const [inputMessage, setInputMessage] = useState('');
61
+ const [isLoading, setIsLoading] = useState(false);
62
+ const [loadingAgent, setLoadingAgent] = useState(true);
63
+ const [error, setError] = useState(null);
64
+
65
+ // 插件面板状态
66
+ const [pluginPanelOpen, setPluginPanelOpen] = useState(false);
67
+ const [activePlugin, setActivePlugin] = useState('code');
68
+ const [availablePlugins, setAvailablePlugins] = useState([]);
69
+ const [pluginData, setPluginData] = useState({ code: '', vizCode: '', mindmapMd: '' });
70
+ const [panelRatio, setPanelRatio] = useState(4 / 7); // 插件面板占比 4/7(对话3:面板4)
71
+ const dragRef = useRef(null);
72
+
73
+ const messagesEndRef = useRef(null);
74
+
75
+ // 验证Token并加载Agent信息
76
+ useEffect(() => {
77
+ if (!agentId || !token) {
78
+ setError('无效的访问链接');
79
+ setLoadingAgent(false);
80
+ return;
81
+ }
82
+
83
+ verifyAndLoadAgent();
84
+ }, [agentId, token]);
85
+
86
+ // 自动滚动到底部
87
+ useEffect(() => {
88
+ scrollToBottom();
89
+ }, [messages]);
90
+
91
+ const scrollToBottom = () => {
92
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
93
+ };
94
+
95
+ // 验证Token并加载Agent
96
+ const verifyAndLoadAgent = async () => {
97
+ try {
98
+ setLoadingAgent(true);
99
+ const response = await api.post('/verify_token', {
100
+ agent_id: agentId,
101
+ token: token
102
+ });
103
+
104
+ if (response.success) {
105
+ setAgent(response.agent);
106
+ // 添加欢迎消息
107
+ setMessages([{
108
+ id: Date.now(),
109
+ type: 'bot',
110
+ content: `你好!我是${response.agent.name},${response.agent.description || '有什么可以帮助你的吗?'}`,
111
+ timestamp: new Date()
112
+ }]);
113
+ } else {
114
+ setError('访问令牌无效或已过期');
115
+ }
116
+ } catch (error) {
117
+ console.error('验证失败:', error);
118
+ setError('无法验证访问权限: ' + (error.message || '未知错误'));
119
+ } finally {
120
+ setLoadingAgent(false);
121
+ }
122
+ };
123
+
124
+ // 发送消息
125
+ const handleSendMessage = async () => {
126
+ if (!inputMessage.trim() || isLoading) return;
127
+
128
+ const userMessage = {
129
+ id: Date.now(),
130
+ type: 'user',
131
+ content: inputMessage,
132
+ timestamp: new Date()
133
+ };
134
+
135
+ setMessages(prev => [...prev, userMessage]);
136
+ const currentMessage = inputMessage;
137
+ setInputMessage('');
138
+ setIsLoading(true);
139
+
140
+ // 创建机器人消息占位
141
+ const botMessageId = Date.now() + 1;
142
+ const botMessage = {
143
+ id: botMessageId,
144
+ type: 'bot',
145
+ content: '',
146
+ timestamp: new Date(),
147
+ isStreaming: true,
148
+ references: []
149
+ };
150
+ setMessages(prev => [...prev, botMessage]);
151
+
152
+ try {
153
+ // 发送请求到后端(SSE流式响应)
154
+ // SSE 直连后端,绕过 Vite 代理避免缓冲
155
+ const SSE_BASE = window.location.hostname === 'localhost' ? 'http://localhost:7860' : '';
156
+ const response = await fetch(`${SSE_BASE}/api/student/chat/${agentId}`, {
157
+ method: 'POST',
158
+ headers: {
159
+ 'Content-Type': 'application/json',
160
+ },
161
+ body: JSON.stringify({
162
+ message: currentMessage,
163
+ token: token,
164
+ conversation_id: Date.now().toString()
165
+ })
166
+ });
167
+
168
+ if (!response.ok) {
169
+ throw new Error('发送失败');
170
+ }
171
+
172
+ // 读取SSE流
173
+ const reader = response.body.getReader();
174
+ const decoder = new TextDecoder();
175
+ let buffer = '';
176
+ let botContent = '';
177
+ let references = [];
178
+ let streamPlugins = [];
179
+ let pluginTriggered = false;
180
+ const STREAM_TIMEOUT = 60000; // 60秒无数据超时
181
+
182
+ const readWithTimeout = () => {
183
+ return Promise.race([
184
+ reader.read(),
185
+ new Promise((_, reject) =>
186
+ setTimeout(() => {
187
+ reader.cancel();
188
+ reject(new Error('响应超时,请重试'));
189
+ }, STREAM_TIMEOUT)
190
+ )
191
+ ]);
192
+ };
193
+
194
+ while (true) {
195
+ const { done, value } = await readWithTimeout();
196
+
197
+ if (done) {
198
+ // 处理 buffer 中可能残留的最后一条数据
199
+ if (buffer.trim()) {
200
+ const remaining = buffer.trim();
201
+ if (remaining.startsWith('data: ')) {
202
+ try {
203
+ const data = JSON.parse(remaining.slice(6));
204
+ if (data.type === 'content') {
205
+ botContent += data.content;
206
+ }
207
+ } catch (e) { /* ignore partial JSON */ }
208
+ }
209
+ }
210
+ // 标记流结束
211
+ setMessages(prev => prev.map(msg =>
212
+ msg.id === botMessageId
213
+ ? { ...msg, content: botContent, isStreaming: false, references }
214
+ : msg
215
+ ));
216
+ break;
217
+ }
218
+
219
+ buffer += decoder.decode(value, { stream: true });
220
+ const lines = buffer.split('\n');
221
+ buffer = lines.pop() || '';
222
+
223
+ for (const line of lines) {
224
+ if (!line.startsWith('data: ')) continue;
225
+ const dataStr = line.slice(6).trim();
226
+ if (!dataStr) continue;
227
+
228
+ try {
229
+ const data = JSON.parse(dataStr);
230
+
231
+ if (data.type === 'start') {
232
+ if (data.references) {
233
+ references = data.references;
234
+ }
235
+ // 记录可用插件
236
+ if (data.plugins && data.plugins.length > 0) {
237
+ streamPlugins = data.plugins;
238
+ setAvailablePlugins(data.plugins);
239
+ }
240
+ } else if (data.type === 'content') {
241
+ botContent += data.content;
242
+ setMessages(prev => prev.map(msg =>
243
+ msg.id === botMessageId
244
+ ? { ...msg, content: botContent }
245
+ : msg
246
+ ));
247
+
248
+ // 流式过程中实时检测代码块
249
+ if (streamPlugins.length > 0) {
250
+ // 阶段1:检测到代码块开头,打开面板并实时填充编辑器
251
+ const openBlock = botContent.match(/```(python|mindmap|markdown)\n([\s\S]*)$/);
252
+ if (openBlock && !pluginTriggered) {
253
+ const lang = openBlock[1];
254
+ const partial = openBlock[2];
255
+ if (lang === 'python' && streamPlugins.includes('code') && !partial.includes('```')) {
256
+ setPluginData(prev => ({ ...prev, code: partial }));
257
+ setActivePlugin('code');
258
+ setPluginPanelOpen(true);
259
+ }
260
+ }
261
+
262
+ // 阶段2:代码块闭合,最终触发(3D渲染/思维导图等需要完整代码)
263
+ if (!pluginTriggered) {
264
+ const completeBlock = botContent.match(/```(?:python|mindmap|markdown)\n[\s\S]+?```/);
265
+ if (completeBlock) {
266
+ pluginTriggered = true;
267
+ extractPluginContent(botContent, streamPlugins);
268
+ }
269
+ }
270
+ }
271
+ } else if (data.type === 'metadata') {
272
+ // 处理元数据(token 用量等),暂记录到 console
273
+ console.log('Stream metadata:', data.content);
274
+ } else if (data.type === 'error') {
275
+ // 后端报错
276
+ throw new Error(data.content || '服务端处理出错');
277
+ } else if (data.type === 'done') {
278
+ setMessages(prev => prev.map(msg =>
279
+ msg.id === botMessageId
280
+ ? { ...msg, isStreaming: false, references }
281
+ : msg
282
+ ));
283
+ }
284
+ } catch (e) {
285
+ if (e.message?.includes('超时') || e.message?.includes('服务端')) throw e;
286
+ console.warn('解析SSE数据失败:', dataStr, e);
287
+ }
288
+ }
289
+ }
290
+
291
+ // 流结束后,如果还没触发过插件,最终检测一次
292
+ if (!pluginTriggered) {
293
+ extractPluginContent(botContent, streamPlugins);
294
+ }
295
+
296
+ } catch (error) {
297
+ console.error('发送消息失败:', error);
298
+ message.error('发送失败,请重试');
299
+
300
+ // 更新错误状态
301
+ setMessages(prev => prev.map(msg =>
302
+ msg.id === botMessageId
303
+ ? { ...msg, content: '抱歉,我遇到了一些问题。请稍后再试。', isStreaming: false, isError: true }
304
+ : msg
305
+ ));
306
+ } finally {
307
+ setIsLoading(false);
308
+ }
309
+ };
310
+
311
+ // 拖拽调整面板宽度
312
+ const handleDragStart = useCallback((e) => {
313
+ e.preventDefault();
314
+ const container = e.target.parentElement;
315
+ const containerWidth = container.getBoundingClientRect().width;
316
+ setIsDragging(true);
317
+
318
+ const onMouseMove = (moveEvent) => {
319
+ const rect = container.getBoundingClientRect();
320
+ const x = moveEvent.clientX - rect.left;
321
+ const ratio = 1 - (x / containerWidth);
322
+ setPanelRatio(Math.min(0.7, Math.max(0.25, ratio)));
323
+ };
324
+
325
+ const onMouseUp = () => {
326
+ document.removeEventListener('mousemove', onMouseMove);
327
+ document.removeEventListener('mouseup', onMouseUp);
328
+ document.body.style.cursor = '';
329
+ document.body.style.userSelect = '';
330
+ setIsDragging(false);
331
+ };
332
+
333
+ document.body.style.cursor = 'col-resize';
334
+ document.body.style.userSelect = 'none';
335
+ document.addEventListener('mousemove', onMouseMove);
336
+ document.addEventListener('mouseup', onMouseUp);
337
+ }, []);
338
+
339
+ // 从 AI 回复中提取插件相关内容
340
+ const extractPluginContent = useCallback((content, plugins) => {
341
+ if (!content) return;
342
+ const activePlugins = plugins || availablePlugins;
343
+
344
+ // 提取 3D 可视化代码(包含 create_3d_plot,优先匹配)
345
+ const vizMatch = content.match(/```python\n([\s\S]*?create_3d_plot[\s\S]*?)```/);
346
+ if (vizMatch && activePlugins.includes('visualization')) {
347
+ setPluginData(prev => ({ ...prev, vizCode: vizMatch[1].trim() }));
348
+ setActivePlugin('viz');
349
+ setPluginPanelOpen(true);
350
+ return; // 3D 优先,不再检查代码插件
351
+ }
352
+
353
+ // 提取 Python 代码块(用于代码执行插件)
354
+ const pythonMatch = content.match(/```python\n([\s\S]*?)```/);
355
+ if (pythonMatch && activePlugins.includes('code')) {
356
+ setPluginData(prev => ({ ...prev, code: pythonMatch[1].trim() }));
357
+ setActivePlugin('code');
358
+ setPluginPanelOpen(true);
359
+ }
360
+
361
+ // 提取思维导图内容
362
+ // 支持三种格式:```markdown 代码块、```mindmap 代码块、@startmindmap PlantUML
363
+ let mindmapMd = null;
364
+
365
+ // 格式1: markdown/mindmap 代码块
366
+ const mdBlockMatch = content.match(/```(?:markdown|mindmap)\n([\s\S]*?)```/);
367
+ if (mdBlockMatch) {
368
+ mindmapMd = mdBlockMatch[1].trim();
369
+ }
370
+
371
+ // 格式2: PlantUML @startmindmap 格式 → 转换为 Markdown 标题
372
+ if (!mindmapMd) {
373
+ const plantUmlMatch = content.match(/@startmindmap\s*\n([\s\S]*?)@endmindmap/);
374
+ if (plantUmlMatch) {
375
+ mindmapMd = plantUmlMatch[1]
376
+ .split('\n')
377
+ .filter(line => line.trim())
378
+ .map(line => {
379
+ // PlantUML: * = h1, ** = h2, *** = h3, ...
380
+ const match = line.match(/^(\*+)\s*(.*)/);
381
+ if (match) {
382
+ const level = match[1].length;
383
+ const text = match[2].replace(/^[:\s]+|;$/g, '').trim();
384
+ return '#'.repeat(level) + ' ' + text;
385
+ }
386
+ return line;
387
+ })
388
+ .join('\n');
389
+ }
390
+ }
391
+
392
+ if (mindmapMd && activePlugins.includes('mindmap')) {
393
+ setPluginData(prev => ({ ...prev, mindmapMd }));
394
+ setActivePlugin('mindmap');
395
+ setPluginPanelOpen(true);
396
+ }
397
+ }, [availablePlugins]);
398
+
399
+ // 复制代码
400
+ const copyCode = (code) => {
401
+ navigator.clipboard.writeText(code).then(() => {
402
+ message.success('代码已复制');
403
+ }).catch(() => {
404
+ message.error('复制失败');
405
+ });
406
+ };
407
+
408
+ // 渲染消息
409
+ const renderMessage = (msg) => {
410
+ const isBot = msg.type === 'bot';
411
+
412
+ // 处理参考来源(从消息内容中移除)
413
+ let displayContent = msg.content || '';
414
+
415
+ // 移除消息中的参考来源标记
416
+ displayContent = displayContent.replace(/===参考来源开始===[\s\S]*?===参考来源结束===/g, '').trim();
417
+
418
+ return (
419
+ <div
420
+ key={msg.id}
421
+ style={{
422
+ display: 'flex',
423
+ gap: '12px',
424
+ marginBottom: '24px',
425
+ flexDirection: isBot ? 'row' : 'row-reverse'
426
+ }}
427
+ >
428
+ <Avatar
429
+ icon={isBot ? <RobotOutlined /> : <UserOutlined />}
430
+ style={{
431
+ backgroundColor: isBot ? '#8b5cf6' : '#10b981',
432
+ flexShrink: 0,
433
+ border: `2px solid ${isBot ? 'rgba(139, 92, 246, 0.2)' : 'rgba(16, 185, 129, 0.2)'}`
434
+ }}
435
+ />
436
+
437
+ <div style={{ maxWidth: '70%', minWidth: '200px' }}>
438
+ <Card
439
+ style={{
440
+ border: `1px solid ${isBot ? 'rgba(139, 92, 246, 0.15)' : 'rgba(16, 185, 129, 0.2)'}`,
441
+ background: isBot ? '#ffffff' : 'rgba(16, 185, 129, 0.03)',
442
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)',
443
+ borderRadius: '8px'
444
+ }}
445
+ styles={{ body: { padding: '12px 16px' } }}
446
+ >
447
+ {isBot ? (
448
+ <div className="bot-message">
449
+ {msg.isStreaming && !displayContent ? (
450
+ // 只在没有内容时显示思考中
451
+ <Space align="center">
452
+ <div className="thinking-dots">
453
+ <span></span>
454
+ <span></span>
455
+ <span></span>
456
+ </div>
457
+ <Text type="secondary" style={{ fontSize: '14px' }}>正在思考中</Text>
458
+ </Space>
459
+ ) : (
460
+ <>
461
+ <ReactMarkdown
462
+ className="markdown-body"
463
+ remarkPlugins={[remarkGfm, remarkMath]}
464
+ rehypePlugins={[rehypeKatex]}
465
+ components={{
466
+ code({ node, inline, className, children, ...props }) {
467
+ const match = /language-(\w+)/.exec(className || '');
468
+ const language = match ? match[1] : '';
469
+
470
+ if (!inline && language) {
471
+ return (
472
+ <div style={{
473
+ position: 'relative',
474
+ marginTop: '12px',
475
+ marginBottom: '12px',
476
+ borderRadius: '6px',
477
+ overflow: 'hidden',
478
+ border: '1px solid rgba(139, 92, 246, 0.1)'
479
+ }}>
480
+ <div style={{
481
+ display: 'flex',
482
+ justifyContent: 'space-between',
483
+ alignItems: 'center',
484
+ padding: '8px 12px',
485
+ background: 'rgba(139, 92, 246, 0.05)',
486
+ borderBottom: '1px solid rgba(139, 92, 246, 0.1)'
487
+ }}>
488
+ <Text style={{ fontSize: '12px', color: '#6b7280' }}>{language}</Text>
489
+ <Button
490
+ size="small"
491
+ type="text"
492
+ icon={<CopyOutlined />}
493
+ onClick={() => copyCode(String(children).replace(/\n$/, ''))}
494
+ >
495
+ 复制
496
+ </Button>
497
+ </div>
498
+ <SyntaxHighlighter
499
+ language={language}
500
+ style={tomorrow}
501
+ PreTag="div"
502
+ customStyle={{
503
+ margin: 0,
504
+ background: '#fafbfc',
505
+ fontSize: '14px'
506
+ }}
507
+ {...props}
508
+ >
509
+ {String(children).replace(/\n$/, '')}
510
+ </SyntaxHighlighter>
511
+ </div>
512
+ );
513
+ }
514
+
515
+ return (
516
+ <code
517
+ style={{
518
+ background: 'rgba(139, 92, 246, 0.08)',
519
+ padding: '2px 6px',
520
+ borderRadius: '4px',
521
+ fontSize: '0.9em',
522
+ color: '#8b5cf6'
523
+ }}
524
+ {...props}
525
+ >
526
+ {children}
527
+ </code>
528
+ );
529
+ },
530
+ p: ({ children }) => <p style={{ marginBottom: '12px', lineHeight: '1.6' }}>{children}</p>,
531
+ ul: ({ children }) => <ul style={{ marginBottom: '12px', paddingLeft: '20px' }}>{children}</ul>,
532
+ ol: ({ children }) => <ol style={{ marginBottom: '12px', paddingLeft: '20px' }}>{children}</ol>,
533
+ li: ({ children }) => <li style={{ marginBottom: '4px' }}>{children}</li>,
534
+ blockquote: ({ children }) => (
535
+ <blockquote style={{
536
+ borderLeft: '4px solid rgba(139, 92, 246, 0.3)',
537
+ paddingLeft: '12px',
538
+ marginLeft: 0,
539
+ color: '#6b7280',
540
+ fontStyle: 'italic'
541
+ }}>
542
+ {children}
543
+ </blockquote>
544
+ )
545
+ }}
546
+ >
547
+ {displayContent}
548
+ </ReactMarkdown>
549
+
550
+ {msg.isStreaming && displayContent && (
551
+ // 流式传输时的光标
552
+ <span className="typing-cursor"></span>
553
+ )}
554
+ </>
555
+ )}
556
+ </div>
557
+ ) : (
558
+ <Text style={{ fontSize: '14px', lineHeight: '1.6' }}>{msg.content}</Text>
559
+ )}
560
+
561
+ {msg.isError && (
562
+ <div style={{ marginTop: '8px' }}>
563
+ <Text type="danger" style={{ fontSize: '12px' }}>
564
+ ⚠ 发生错误
565
+ </Text>
566
+ </div>
567
+ )}
568
+ </Card>
569
+
570
+ {/* 参考文献卡片 */}
571
+ {isBot && msg.references && msg.references.length > 0 && (
572
+ <Card
573
+ size="small"
574
+ style={{
575
+ marginTop: '8px',
576
+ border: '1px solid rgba(139, 92, 246, 0.1)',
577
+ background: 'rgba(139, 92, 246, 0.02)',
578
+ borderRadius: '8px'
579
+ }}
580
+ title={
581
+ <Space>
582
+ <FileTextOutlined style={{ color: '#8b5cf6' }} />
583
+ <Text style={{ fontSize: '13px', fontWeight: 500 }}>参考来源</Text>
584
+ </Space>
585
+ }
586
+ styles={{ body: { padding: '12px' } }}
587
+ >
588
+ {msg.references.map((ref, index) => (
589
+ <div key={index} style={{
590
+ marginBottom: index < msg.references.length - 1 ? '12px' : 0,
591
+ paddingBottom: index < msg.references.length - 1 ? '12px' : 0,
592
+ borderBottom: index < msg.references.length - 1 ? '1px solid rgba(139, 92, 246, 0.08)' : 'none'
593
+ }}>
594
+ <Space align="start">
595
+ <Tag color="purple" style={{ marginRight: 0 }}>{ref.index || index + 1}</Tag>
596
+ <div>
597
+ <Text style={{ fontSize: '13px', color: '#374151' }}>
598
+ {ref.summary}
599
+ </Text>
600
+ <br />
601
+ <Text type="secondary" style={{ fontSize: '12px' }}>
602
+ 来源: {ref.file_name}
603
+ </Text>
604
+ </div>
605
+ </Space>
606
+ </div>
607
+ ))}
608
+ </Card>
609
+ )}
610
+
611
+ <Text type="secondary" style={{ fontSize: '12px', display: 'block', marginTop: '8px' }}>
612
+ {new Date(msg.timestamp).toLocaleTimeString()}
613
+ </Text>
614
+ </div>
615
+ </div>
616
+ );
617
+ };
618
+
619
+ // 显示加载状态
620
+ if (loadingAgent) {
621
+ return (
622
+ <div style={{
623
+ height: '100vh',
624
+ display: 'flex',
625
+ justifyContent: 'center',
626
+ alignItems: 'center',
627
+ background: '#fafbfc'
628
+ }}>
629
+ <Spin size="large" tip="正在加载Agent信息...">
630
+ <div style={{ height: '100px' }} />
631
+ </Spin>
632
+ </div>
633
+ );
634
+ }
635
+
636
+ // 显示错误状态
637
+ if (error) {
638
+ return (
639
+ <div style={{
640
+ height: '100vh',
641
+ display: 'flex',
642
+ justifyContent: 'center',
643
+ alignItems: 'center',
644
+ background: '#fafbfc'
645
+ }}>
646
+ <Card style={{ textAlign: 'center', padding: '20px' }}>
647
+ <Title level={4} type="danger">加载失败</Title>
648
+ <Text type="secondary">{error}</Text>
649
+ <br />
650
+ <Button
651
+ type="primary"
652
+ style={{ marginTop: '20px' }}
653
+ onClick={() => window.location.reload()}
654
+ >
655
+ 重试
656
+ </Button>
657
+ </Card>
658
+ </div>
659
+ );
660
+ }
661
+
662
+ if (!agent) {
663
+ return (
664
+ <div style={{
665
+ height: '100vh',
666
+ display: 'flex',
667
+ justifyContent: 'center',
668
+ alignItems: 'center',
669
+ background: '#fafbfc'
670
+ }}>
671
+ <Empty description="Agent未找到" />
672
+ </div>
673
+ );
674
+ }
675
+
676
+ // 插件 Tab 配置
677
+ const pluginTabs = [
678
+ ...(availablePlugins.includes('code') ? [{
679
+ key: 'code',
680
+ label: <span><CodeOutlined /> 代码</span>,
681
+ children: <CodeExecutor initialCode={pluginData.code} />
682
+ }] : []),
683
+ ...(availablePlugins.includes('visualization') ? [{
684
+ key: 'viz',
685
+ label: <span><BarChartOutlined /> 3D</span>,
686
+ children: <Visualization3D code={pluginData.vizCode} />
687
+ }] : []),
688
+ ...(availablePlugins.includes('mindmap') ? [{
689
+ key: 'mindmap',
690
+ label: <span><BranchesOutlined /> 导图</span>,
691
+ children: <MindmapViewer markdown={pluginData.mindmapMd} />
692
+ }] : []),
693
+ ];
694
+
695
+ return (
696
+ <Layout style={{ height: '100vh', background: '#fafbfc' }}>
697
+ {/* 顶部栏 */}
698
+ <div style={{
699
+ height: '60px',
700
+ background: '#ffffff',
701
+ borderBottom: '1px solid rgba(0, 0, 0, 0.08)',
702
+ display: 'flex',
703
+ alignItems: 'center',
704
+ justifyContent: 'space-between',
705
+ padding: '0 24px',
706
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)'
707
+ }}>
708
+ <Space size="large">
709
+ <Avatar
710
+ size="large"
711
+ icon={<RobotOutlined />}
712
+ style={{ backgroundColor: '#8b5cf6' }}
713
+ />
714
+ <div>
715
+ <Title level={4} style={{ margin: 0 }}>{agent.name}</Title>
716
+ <Text type="secondary" style={{ fontSize: '12px' }}>{agent.description}</Text>
717
+ </div>
718
+ </Space>
719
+ {/* 插件面板切换按钮 */}
720
+ {availablePlugins.length > 0 && (
721
+ <Button
722
+ type={pluginPanelOpen ? 'primary' : 'default'}
723
+ icon={pluginPanelOpen ? <CloseOutlined /> : <AppstoreOutlined />}
724
+ onClick={() => setPluginPanelOpen(!pluginPanelOpen)}
725
+ style={pluginPanelOpen ? { background: '#8b5cf6', borderColor: '#8b5cf6' } : {}}
726
+ >
727
+ {pluginPanelOpen ? '关闭面板' : '插件面板'}
728
+ </Button>
729
+ )}
730
+ </div>
731
+
732
+ {/* 主体区域:对话 + 插件面板 */}
733
+ <div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
734
+ {/* 左侧对话区 */}
735
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
736
+ <Content style={{
737
+ flex: 1,
738
+ padding: '24px',
739
+ overflowY: 'auto',
740
+ background: '#fafbfc'
741
+ }}>
742
+ <div style={{ maxWidth: '900px', margin: '0 auto' }}>
743
+ {messages.map(renderMessage)}
744
+ <div ref={messagesEndRef} />
745
+ </div>
746
+ </Content>
747
+
748
+ {/* 输入区域 */}
749
+ <div style={{
750
+ padding: '16px 24px',
751
+ background: '#ffffff',
752
+ borderTop: '1px solid rgba(0, 0, 0, 0.08)',
753
+ boxShadow: '0 -2px 8px rgba(0, 0, 0, 0.04)'
754
+ }}>
755
+ <div style={{ maxWidth: '900px', margin: '0 auto' }}>
756
+ <Space.Compact style={{ width: '100%' }}>
757
+ <TextArea
758
+ value={inputMessage}
759
+ onChange={e => setInputMessage(e.target.value)}
760
+ onPressEnter={e => {
761
+ if (!e.shiftKey) {
762
+ e.preventDefault();
763
+ handleSendMessage();
764
+ }
765
+ }}
766
+ placeholder="输入你的问题... (Shift+Enter换行)"
767
+ autoSize={{ minRows: 1, maxRows: 4 }}
768
+ style={{
769
+ borderRadius: '8px 0 0 8px',
770
+ resize: 'none'
771
+ }}
772
+ />
773
+ <Button
774
+ type="primary"
775
+ icon={<SendOutlined />}
776
+ onClick={handleSendMessage}
777
+ loading={isLoading}
778
+ style={{
779
+ height: 'auto',
780
+ minHeight: '32px',
781
+ borderRadius: '0 8px 8px 0',
782
+ background: '#10b981'
783
+ }}
784
+ >
785
+ 发送
786
+ </Button>
787
+ </Space.Compact>
788
+ </div>
789
+ </div>
790
+ </div>
791
+
792
+ {/* 右侧:拖拽分隔条 + 插件面板 */}
793
+ {pluginPanelOpen && pluginTabs.length > 0 && (
794
+ <>
795
+ {/* 拖拽分隔条 */}
796
+ <div
797
+ ref={dragRef}
798
+ onMouseDown={handleDragStart}
799
+ style={{
800
+ width: '6px',
801
+ cursor: 'col-resize',
802
+ background: 'transparent',
803
+ position: 'relative',
804
+ flexShrink: 0,
805
+ zIndex: 10,
806
+ }}
807
+ >
808
+ <div style={{
809
+ position: 'absolute',
810
+ left: '2px',
811
+ top: 0,
812
+ bottom: 0,
813
+ width: '2px',
814
+ background: 'rgba(139, 92, 246, 0.2)',
815
+ transition: 'background 0.2s',
816
+ }} />
817
+ </div>
818
+ {/* 插件面板 */}
819
+ <div style={{
820
+ flex: `0 0 ${panelRatio * 100}%`,
821
+ minWidth: '300px',
822
+ maxWidth: '75%',
823
+ background: '#ffffff',
824
+ display: 'flex',
825
+ flexDirection: 'column',
826
+ overflow: 'hidden',
827
+ position: 'relative'
828
+ }}>
829
+ {/* 拖拽时遮罩,阻止 iframe 捕获鼠标 */}
830
+ {isDragging && (
831
+ <div style={{
832
+ position: 'absolute',
833
+ inset: 0,
834
+ zIndex: 100,
835
+ cursor: 'col-resize',
836
+ }} />
837
+ )}
838
+ <Tabs
839
+ activeKey={activePlugin}
840
+ onChange={setActivePlugin}
841
+ items={pluginTabs}
842
+ style={{ flex: 1, display: 'flex', flexDirection: 'column' }}
843
+ tabBarStyle={{ padding: '0 16px', marginBottom: 0 }}
844
+ tabBarGutter={16}
845
+ />
846
+ </div>
847
+ </>
848
+ )}
849
+ </div>
850
+ </Layout>
851
+ );
852
+ };
853
+
854
+ export default AgentChat;
frontend/src/pages/student/StudentDashboard.jsx ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 学生仪表板主路由组件
2
+ import { Routes, Route, Navigate } from 'react-router-dom';
3
+ import StudentPortal from './StudentPortal';
4
+
5
+ const StudentDashboard = () => {
6
+ return (
7
+ <Routes>
8
+ <Route path="/" element={<StudentPortal />} />
9
+ <Route path="*" element={<Navigate to="/student" replace />} />
10
+ </Routes>
11
+ );
12
+ };
13
+
14
+ export default StudentDashboard;
frontend/src/pages/student/StudentPortal.jsx ADDED
@@ -0,0 +1,389 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 学生端 - 门户页面
3
+ * 基于 student_portal.html 的React实现
4
+ * 包含Agent列表、活动记录、访问令牌输入等功能
5
+ */
6
+ import { useState, useEffect } from 'react';
7
+ import { useNavigate } from 'react-router-dom';
8
+ import {
9
+ Layout,
10
+ Card,
11
+ Row,
12
+ Col,
13
+ Input,
14
+ Button,
15
+ Space,
16
+ Typography,
17
+ Tag,
18
+ Empty,
19
+ message,
20
+ Spin,
21
+ Timeline,
22
+ Avatar,
23
+ Divider
24
+ } from 'antd';
25
+ import {
26
+ RobotOutlined,
27
+ ClockCircleOutlined,
28
+ CodeOutlined,
29
+ BarChartOutlined,
30
+ BulbOutlined,
31
+ LoginOutlined,
32
+ BookOutlined,
33
+ MessageOutlined,
34
+ ExperimentOutlined,
35
+ TeamOutlined,
36
+ CalendarOutlined
37
+ } from '@ant-design/icons';
38
+ import api from '../../services/api';
39
+ import { motion } from 'framer-motion';
40
+
41
+ const { Content } = Layout;
42
+ const { Title, Text, Paragraph } = Typography;
43
+ const { Search } = Input;
44
+
45
+ const StudentPortal = () => {
46
+ const navigate = useNavigate();
47
+ const [loading, setLoading] = useState(true);
48
+ const [agents, setAgents] = useState([]);
49
+ const [activities, setActivities] = useState([]);
50
+ const [accessToken, setAccessToken] = useState('');
51
+ const [loadingActivities, setLoadingActivities] = useState(false);
52
+
53
+ // 加载Agent列表和活动记录
54
+ useEffect(() => {
55
+ loadData();
56
+ }, []);
57
+
58
+ const loadData = async () => {
59
+ try {
60
+ setLoading(true);
61
+ // 并行加载Agent列表和活动记录
62
+ const [agentsRes, activitiesRes] = await Promise.all([
63
+ loadAgents(),
64
+ loadActivities()
65
+ ]);
66
+
67
+ setAgents(agentsRes || []);
68
+ setActivities(activitiesRes || []);
69
+ } catch (error) {
70
+ console.error('加载数据失败:', error);
71
+ } finally {
72
+ setLoading(false);
73
+ }
74
+ };
75
+
76
+ // 加载学生可访问的Agent列表
77
+ const loadAgents = async () => {
78
+ try {
79
+ const response = await api.get('/student/agents');
80
+ if (response.success) {
81
+ return response.data || [];
82
+ }
83
+ return [];
84
+ } catch (error) {
85
+ console.error('加载Agent列表失败:', error);
86
+ return [];
87
+ }
88
+ };
89
+
90
+ // 加载活动记录
91
+ const loadActivities = async () => {
92
+ try {
93
+ setLoadingActivities(true);
94
+ const response = await api.get('/student/activities');
95
+ if (response.success) {
96
+ return response.activities || [];
97
+ }
98
+ return [];
99
+ } catch (error) {
100
+ console.error('加载活动记录失败:', error);
101
+ return [];
102
+ } finally {
103
+ setLoadingActivities(false);
104
+ }
105
+ };
106
+
107
+ // 使用访问令牌访问Agent
108
+ const handleAccessWithToken = async () => {
109
+ if (!accessToken.trim()) {
110
+ message.warning('请输入访问令牌');
111
+ return;
112
+ }
113
+
114
+ try {
115
+ // 验证令牌
116
+ const response = await api.post('/verify_token', {
117
+ token: accessToken
118
+ });
119
+
120
+ if (response.success) {
121
+ const agent = response.agent;
122
+ message.success('验证成功,正在跳转...');
123
+ // 跳转到聊天界面
124
+ navigate(`/student/chat/${agent.id}?token=${accessToken}`);
125
+ } else {
126
+ message.error('访问令牌无效或已过期');
127
+ }
128
+ } catch (error) {
129
+ console.error('验证失败:', error);
130
+ message.error('验证失败,请检查令牌是否正确');
131
+ }
132
+ };
133
+
134
+ // 访问Agent
135
+ const handleAccessAgent = (agent) => {
136
+ // 如果有token,使用token访问
137
+ if (agent.token) {
138
+ navigate(`/student/chat/${agent.id}?token=${agent.token}`);
139
+ } else {
140
+ message.warning('该Agent暂无可用的访问令牌');
141
+ }
142
+ };
143
+
144
+ // 获取活动图标
145
+ const getActivityIcon = (type) => {
146
+ const icons = {
147
+ chat: <MessageOutlined style={{ color: '#10b981' }} />,
148
+ code: <CodeOutlined style={{ color: '#3b82f6' }} />,
149
+ viz: <BarChartOutlined style={{ color: '#f59e0b' }} />,
150
+ mindmap: <BulbOutlined style={{ color: '#8b5cf6' }} />
151
+ };
152
+ return icons[type] || <ClockCircleOutlined style={{ color: '#6b7280' }} />;
153
+ };
154
+
155
+ // 获取插件标签颜色
156
+ const getPluginColor = (plugin) => {
157
+ const colors = {
158
+ code: 'blue',
159
+ visualization: 'orange',
160
+ mindmap: 'purple'
161
+ };
162
+ return colors[plugin] || 'default';
163
+ };
164
+
165
+ if (loading) {
166
+ return (
167
+ <div style={{
168
+ height: '100vh',
169
+ display: 'flex',
170
+ justifyContent: 'center',
171
+ alignItems: 'center',
172
+ backgroundColor: '#f9fafb'
173
+ }}>
174
+ <Spin size="large" tip="正在加载..." />
175
+ </div>
176
+ );
177
+ }
178
+
179
+ return (
180
+ <Layout style={{ minHeight: '100vh', backgroundColor: '#f9fafb' }}>
181
+ <Content style={{ padding: '24px' }}>
182
+ {/* 欢迎区域 */}
183
+ <motion.div
184
+ initial={{ opacity: 0, y: -20 }}
185
+ animate={{ opacity: 1, y: 0 }}
186
+ transition={{ duration: 0.5 }}
187
+ >
188
+ <Card
189
+ style={{
190
+ marginBottom: '24px',
191
+ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
192
+ border: 'none'
193
+ }}
194
+ >
195
+ <Row align="middle">
196
+ <Col span={18}>
197
+ <Title level={2} style={{ color: '#fff', marginBottom: '8px' }}>
198
+ 欢迎来到智能学习平台
199
+ </Title>
200
+ <Paragraph style={{ color: 'rgba(255, 255, 255, 0.9)', fontSize: '16px', marginBottom: 0 }}>
201
+ 探索AI驱动的个性化学习体验,与智能助教互动,提升学习效率
202
+ </Paragraph>
203
+ </Col>
204
+ <Col span={6} style={{ textAlign: 'right' }}>
205
+ <Avatar
206
+ size={80}
207
+ icon={<BookOutlined />}
208
+ style={{ backgroundColor: 'rgba(255, 255, 255, 0.2)' }}
209
+ />
210
+ </Col>
211
+ </Row>
212
+ </Card>
213
+ </motion.div>
214
+
215
+ {/* 快速访问区域 */}
216
+ <Card
217
+ title="快速访问"
218
+ style={{ marginBottom: '24px' }}
219
+ extra={
220
+ <Text type="secondary">
221
+ 输入教师提供的访问令牌
222
+ </Text>
223
+ }
224
+ >
225
+ <Search
226
+ placeholder="请输入访问令牌"
227
+ enterButton={
228
+ <Button type="primary" icon={<LoginOutlined />}>
229
+ 访问
230
+ </Button>
231
+ }
232
+ size="large"
233
+ value={accessToken}
234
+ onChange={(e) => setAccessToken(e.target.value)}
235
+ onSearch={handleAccessWithToken}
236
+ style={{ maxWidth: '600px' }}
237
+ />
238
+ </Card>
239
+
240
+ <Row gutter={[24, 24]}>
241
+ {/* Agent列表 */}
242
+ <Col xs={24} lg={16}>
243
+ <Card
244
+ title={
245
+ <Space>
246
+ <RobotOutlined />
247
+ <span>可用的AI助教</span>
248
+ </Space>
249
+ }
250
+ bordered={false}
251
+ >
252
+ {agents.length > 0 ? (
253
+ <Row gutter={[16, 16]}>
254
+ {agents.map((agent, index) => (
255
+ <Col xs={24} sm={12} key={agent.id}>
256
+ <motion.div
257
+ initial={{ opacity: 0, scale: 0.9 }}
258
+ animate={{ opacity: 1, scale: 1 }}
259
+ transition={{ delay: index * 0.1 }}
260
+ >
261
+ <Card
262
+ hoverable
263
+ onClick={() => handleAccessAgent(agent)}
264
+ style={{ height: '100%' }}
265
+ >
266
+ <Space direction="vertical" style={{ width: '100%' }}>
267
+ <div>
268
+ <Avatar
269
+ size="large"
270
+ icon={<RobotOutlined />}
271
+ style={{
272
+ backgroundColor: '#8b5cf6',
273
+ marginRight: '12px'
274
+ }}
275
+ />
276
+ <Text strong style={{ fontSize: '16px' }}>
277
+ {agent.name}
278
+ </Text>
279
+ </div>
280
+
281
+ <Text type="secondary">
282
+ {agent.description || '智能学习助手'}
283
+ </Text>
284
+
285
+ <div>
286
+ {agent.plugins && agent.plugins.map(plugin => (
287
+ <Tag
288
+ key={plugin}
289
+ color={getPluginColor(plugin)}
290
+ style={{ marginRight: '8px' }}
291
+ >
292
+ {plugin === 'code' && <CodeOutlined />}
293
+ {plugin === 'visualization' && <BarChartOutlined />}
294
+ {plugin === 'mindmap' && <BulbOutlined />}
295
+ <span style={{ marginLeft: '4px' }}>
296
+ {plugin === 'code' && '代码执行'}
297
+ {plugin === 'visualization' && '3D可视化'}
298
+ {plugin === 'mindmap' && '思维导图'}
299
+ </span>
300
+ </Tag>
301
+ ))}
302
+ </div>
303
+
304
+ <div style={{ marginTop: '8px' }}>
305
+ <Space>
306
+ <TeamOutlined />
307
+ <Text type="secondary" style={{ fontSize: '12px' }}>
308
+ {agent.instructor || '教师'}
309
+ </Text>
310
+ <Divider type="vertical" />
311
+ <CalendarOutlined />
312
+ <Text type="secondary" style={{ fontSize: '12px' }}>
313
+ {agent.last_used || '未使用'}
314
+ </Text>
315
+ </Space>
316
+ </div>
317
+ </Space>
318
+ </Card>
319
+ </motion.div>
320
+ </Col>
321
+ ))}
322
+ </Row>
323
+ ) : (
324
+ <Empty
325
+ description="暂无可用的AI助教"
326
+ image={Empty.PRESENTED_IMAGE_SIMPLE}
327
+ >
328
+ <Button
329
+ type="primary"
330
+ icon={<LoginOutlined />}
331
+ onClick={() => document.querySelector('input').focus()}
332
+ >
333
+ 使用访问令牌
334
+ </Button>
335
+ </Empty>
336
+ )}
337
+ </Card>
338
+ </Col>
339
+
340
+ {/* 活动记录 */}
341
+ <Col xs={24} lg={8}>
342
+ <Card
343
+ title={
344
+ <Space>
345
+ <ClockCircleOutlined />
346
+ <span>最近活动</span>
347
+ </Space>
348
+ }
349
+ bordered={false}
350
+ loading={loadingActivities}
351
+ >
352
+ {activities.length > 0 ? (
353
+ <Timeline>
354
+ {activities.map((activity, index) => (
355
+ <Timeline.Item
356
+ key={index}
357
+ dot={getActivityIcon(activity.type)}
358
+ >
359
+ <div style={{ marginBottom: '12px' }}>
360
+ <Text strong>{activity.title}</Text>
361
+ <br />
362
+ {activity.agent_name && (
363
+ <Text type="secondary" style={{ fontSize: '12px' }}>
364
+ {activity.agent_name}
365
+ </Text>
366
+ )}
367
+ <br />
368
+ <Text type="secondary" style={{ fontSize: '12px' }}>
369
+ {activity.time}
370
+ </Text>
371
+ </div>
372
+ </Timeline.Item>
373
+ ))}
374
+ </Timeline>
375
+ ) : (
376
+ <Empty
377
+ description="暂无活动记录"
378
+ image={Empty.PRESENTED_IMAGE_SIMPLE}
379
+ />
380
+ )}
381
+ </Card>
382
+ </Col>
383
+ </Row>
384
+ </Content>
385
+ </Layout>
386
+ );
387
+ };
388
+
389
+ export default StudentPortal;
frontend/src/pages/teacher/AgentList.jsx ADDED
@@ -0,0 +1,660 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 教师端 - Agent列表页面
3
+ * 对应原始 index.html 的 agent-list section
4
+ * 复用原始HTML的Agent列表展示和管理逻辑
5
+ */
6
+ import { useState, useEffect } from 'react';
7
+ import { Card, Row, Col, Button, Space, Tag, Modal, Form, Select, message, Input, Descriptions, Popconfirm } from 'antd';
8
+ import {
9
+ RobotOutlined,
10
+ ShareAltOutlined,
11
+ EyeOutlined,
12
+ EditOutlined,
13
+ DeleteOutlined,
14
+ PlusOutlined,
15
+ ExclamationCircleOutlined,
16
+ CopyOutlined,
17
+ BookOutlined,
18
+ UserOutlined,
19
+ CodeOutlined,
20
+ BarChartOutlined,
21
+ BulbOutlined,
22
+ } from '@ant-design/icons';
23
+ import { motion } from 'framer-motion';
24
+ import agentService from '../../services/agentService';
25
+
26
+ const { confirm } = Modal;
27
+
28
+ const AgentList = () => {
29
+ const [agents, setAgents] = useState([]);
30
+ const [loading, setLoading] = useState(false);
31
+ const [detailModalVisible, setDetailModalVisible] = useState(false);
32
+ const [distributionModalVisible, setDistributionModalVisible] = useState(false);
33
+ const [selectedAgent, setSelectedAgent] = useState(null);
34
+ const [distributionForm] = Form.useForm();
35
+ const [createdDistribution, setCreatedDistribution] = useState(null);
36
+
37
+ useEffect(() => {
38
+ loadAgents();
39
+ }, []);
40
+
41
+ // 加载Agent列表
42
+ const loadAgents = async () => {
43
+ try {
44
+ setLoading(true);
45
+ const res = await agentService.getAgents();
46
+ setAgents(res.data || []);
47
+ } catch (error) {
48
+ console.error('加载Agent列表失败:', error);
49
+ message.error('加载Agent列表失败');
50
+ } finally {
51
+ setLoading(false);
52
+ }
53
+ };
54
+
55
+ // 获取Agent类型文本
56
+ const getAgentTypeText = (type) => {
57
+ const typeMap = {
58
+ educational: '教育辅导',
59
+ programming: '编程辅助',
60
+ math: '数学辅导',
61
+ general: '通用助手',
62
+ };
63
+ return typeMap[type] || type;
64
+ };
65
+
66
+ // 获取Agent类型颜色
67
+ const getAgentTypeColor = (type) => {
68
+ const colorMap = {
69
+ educational: '#10b981',
70
+ programming: '#3b82f6',
71
+ math: '#f59e0b',
72
+ general: '#8b5cf6',
73
+ };
74
+ return colorMap[type] || '#6b7280';
75
+ };
76
+
77
+ // 获取插件图标
78
+ const getPluginIcon = (pluginId) => {
79
+ const iconMap = {
80
+ code: <CodeOutlined style={{ fontSize: '18px' }} />,
81
+ visualization: <BarChartOutlined style={{ fontSize: '18px' }} />,
82
+ mindmap: <BulbOutlined style={{ fontSize: '18px' }} />,
83
+ };
84
+ return iconMap[pluginId] || <RobotOutlined />;
85
+ };
86
+
87
+ // 获取插件名称
88
+ const getPluginName = (pluginId) => {
89
+ const nameMap = {
90
+ code: '代码执行',
91
+ visualization: '3D可视化',
92
+ mindmap: '思维导图',
93
+ };
94
+ return nameMap[pluginId] || pluginId;
95
+ };
96
+
97
+ // 查看Agent详情
98
+ const handleShowDetail = async (agent) => {
99
+ try {
100
+ const res = await agentService.getAgentDetail(agent.id);
101
+ setSelectedAgent(res.data);
102
+ setDetailModalVisible(true);
103
+ } catch (error) {
104
+ message.error('获取Agent详情失败');
105
+ }
106
+ };
107
+
108
+ // 编辑Agent
109
+ const handleEdit = (agent) => {
110
+ // 将agent数据保存到localStorage
111
+ localStorage.setItem('editingAgent', JSON.stringify(agent));
112
+ // 跳转到创建页面(编辑模式)
113
+ window.location.href = '/teacher/create-agent?mode=edit';
114
+ };
115
+
116
+ // 删除Agent
117
+ const handleDelete = async (agentId) => {
118
+ try {
119
+ await agentService.deleteAgent(agentId);
120
+ message.success('Agent已删除');
121
+ loadAgents();
122
+ } catch (error) {
123
+ console.error('删除失败:', error);
124
+ message.error('删除失败');
125
+ }
126
+ };
127
+
128
+ // 显示创建分发链接对话框
129
+ const handleShowDistribution = (agent) => {
130
+ setSelectedAgent(agent);
131
+ setDistributionModalVisible(true);
132
+ distributionForm.resetFields();
133
+ };
134
+
135
+ // 创建分发链接
136
+ const handleCreateDistribution = async (values) => {
137
+ try {
138
+ const res = await agentService.createDistribution(selectedAgent.id, values.expiresIn);
139
+
140
+ if (res.success) {
141
+ // 后端返回的是 distribution 对象
142
+ const distribution = res.distribution;
143
+ const origin = window.location.origin;
144
+ const fullLink = `${origin}/student/chat/${selectedAgent.id}?token=${distribution.token}`;
145
+ const accessToken = distribution.token;
146
+
147
+ setCreatedDistribution({
148
+ fullLink,
149
+ accessToken,
150
+ });
151
+
152
+ message.success('分发链接创建成功');
153
+ loadAgents(); // 刷新列表
154
+
155
+ // 刷新当前 Agent 详情,确保 distributions 数组更新
156
+ try {
157
+ const detail = await agentService.getAgentDetail(selectedAgent.id);
158
+ setSelectedAgent(detail.data);
159
+ } catch (e) { /* ignore */ }
160
+ } else {
161
+ message.error(res.message || '创建分发链接失败');
162
+ }
163
+ } catch (error) {
164
+ console.error('创建分发链接失败:', error);
165
+ message.error('创建分发链接失败');
166
+ }
167
+ };
168
+
169
+ // 复制到剪贴板
170
+ const copyToClipboard = (text) => {
171
+ navigator.clipboard.writeText(text).then(() => {
172
+ message.success('已复制到剪贴板');
173
+ }).catch(() => {
174
+ message.error('复制失败');
175
+ });
176
+ };
177
+
178
+ return (
179
+ <div>
180
+ <div style={{ marginBottom: '32px' }}>
181
+ <h1 style={{ fontSize: '28px', fontWeight: 600, color: '#1f2937', margin: 0 }}>
182
+ Agent列表
183
+ </h1>
184
+ <p style={{ fontSize: '14px', color: '#6b7280', marginTop: '8px' }}>
185
+ 管理您创建的所有AI教学助手
186
+ </p>
187
+ </div>
188
+
189
+ {loading ? (
190
+ <Card loading={loading} />
191
+ ) : agents.length > 0 ? (
192
+ <Row gutter={[24, 24]}>
193
+ {agents.map((agent) => (
194
+ <Col xs={24} md={12} lg={8} key={agent.id}>
195
+ <motion.div
196
+ initial={{ opacity: 0, y: 20 }}
197
+ animate={{ opacity: 1, y: 0 }}
198
+ whileHover={{ y: -4 }}
199
+ transition={{ duration: 0.2 }}
200
+ >
201
+ <Card
202
+ style={{
203
+ border: `1px solid ${getAgentTypeColor(agent.type)}33`,
204
+ borderRadius: '12px',
205
+ height: '100%',
206
+ }}
207
+ bodyStyle={{ padding: '20px' }}
208
+ >
209
+ {/* Agent头部 */}
210
+ <div style={{ display: 'flex', alignItems: 'flex-start', marginBottom: '16px' }}>
211
+ <div
212
+ style={{
213
+ width: '48px',
214
+ height: '48px',
215
+ borderRadius: '12px',
216
+ background: `${getAgentTypeColor(agent.type)}15`,
217
+ display: 'flex',
218
+ alignItems: 'center',
219
+ justifyContent: 'center',
220
+ marginRight: '12px',
221
+ flexShrink: 0,
222
+ }}
223
+ >
224
+ <RobotOutlined
225
+ style={{
226
+ fontSize: '24px',
227
+ color: getAgentTypeColor(agent.type),
228
+ }}
229
+ />
230
+ </div>
231
+ <div style={{ flex: 1, minWidth: 0 }}>
232
+ <h3
233
+ style={{
234
+ fontSize: '18px',
235
+ fontWeight: 600,
236
+ margin: 0,
237
+ color: '#1f2937',
238
+ overflow: 'hidden',
239
+ textOverflow: 'ellipsis',
240
+ whiteSpace: 'nowrap',
241
+ }}
242
+ >
243
+ {agent.name}
244
+ </h3>
245
+ <Tag
246
+ color={getAgentTypeColor(agent.type)}
247
+ style={{ marginTop: '4px' }}
248
+ >
249
+ {getAgentTypeText(agent.type)}
250
+ </Tag>
251
+ </div>
252
+ </div>
253
+
254
+ {/* Agent描述 */}
255
+ <p
256
+ style={{
257
+ fontSize: '14px',
258
+ color: '#6b7280',
259
+ marginBottom: '16px',
260
+ lineHeight: '1.5',
261
+ display: '-webkit-box',
262
+ WebkitLineClamp: 2,
263
+ WebkitBoxOrient: 'vertical',
264
+ overflow: 'hidden',
265
+ }}
266
+ >
267
+ {agent.description || '暂无描述'}
268
+ </p>
269
+
270
+ {/* Agent标签信息 */}
271
+ <div style={{ marginBottom: '16px' }}>
272
+ {agent.subject && (
273
+ <div style={{ fontSize: '13px', color: '#6b7280', marginBottom: '4px' }}>
274
+ <BookOutlined style={{ marginRight: '6px' }} />
275
+ {agent.subject}
276
+ </div>
277
+ )}
278
+ {agent.instructor && (
279
+ <div style={{ fontSize: '13px', color: '#6b7280' }}>
280
+ <UserOutlined style={{ marginRight: '6px' }} />
281
+ {agent.instructor}
282
+ </div>
283
+ )}
284
+ </div>
285
+
286
+ {/* 插件标签 */}
287
+ {agent.plugins && agent.plugins.length > 0 && (
288
+ <div style={{ marginBottom: '16px' }}>
289
+ <Space size={[4, 4]} wrap>
290
+ {agent.plugins.map((pluginId) => (
291
+ <Tag
292
+ key={pluginId}
293
+ icon={getPluginIcon(pluginId)}
294
+ style={{ fontSize: '12px' }}
295
+ >
296
+ {getPluginName(pluginId)}
297
+ </Tag>
298
+ ))}
299
+ </Space>
300
+ </div>
301
+ )}
302
+
303
+ {/* ���计信息 */}
304
+ <div
305
+ style={{
306
+ paddingTop: '16px',
307
+ borderTop: '1px solid #e5e7eb',
308
+ marginBottom: '16px',
309
+ }}
310
+ >
311
+ <Row gutter={16}>
312
+ <Col span={12}>
313
+ <div style={{ fontSize: '12px', color: '#9ca3af' }}>分发链接</div>
314
+ <div style={{ fontSize: '18px', fontWeight: 600, color: '#1f2937' }}>
315
+ {agent.distribution_count || 0}
316
+ </div>
317
+ </Col>
318
+ <Col span={12}>
319
+ <div style={{ fontSize: '12px', color: '#9ca3af' }}>使用次数</div>
320
+ <div style={{ fontSize: '18px', fontWeight: 600, color: '#1f2937' }}>
321
+ {agent.usageCount || 0}
322
+ </div>
323
+ </Col>
324
+ </Row>
325
+ </div>
326
+
327
+ {/* 操作按钮 */}
328
+ <Space size="small" style={{ width: '100%' }} wrap>
329
+ <Button
330
+ type="primary"
331
+ size="small"
332
+ icon={<ShareAltOutlined />}
333
+ onClick={() => handleShowDistribution(agent)}
334
+ style={{ backgroundColor: '#10b981' }}
335
+ >
336
+ 分发
337
+ </Button>
338
+ <Button
339
+ size="small"
340
+ icon={<EyeOutlined />}
341
+ onClick={() => handleShowDetail(agent)}
342
+ >
343
+ 详情
344
+ </Button>
345
+ <Button
346
+ size="small"
347
+ icon={<EditOutlined />}
348
+ onClick={() => handleEdit(agent)}
349
+ >
350
+ 编辑
351
+ </Button>
352
+ <Popconfirm
353
+ title="确认删除"
354
+ description={`确定要删除Agent "${agent.name}" 吗?`}
355
+ onConfirm={() => handleDelete(agent.id)}
356
+ okText="删除"
357
+ cancelText="取消"
358
+ okButtonProps={{ danger: true }}
359
+ >
360
+ <Button
361
+ danger
362
+ size="small"
363
+ icon={<DeleteOutlined />}
364
+ >
365
+ 删除
366
+ </Button>
367
+ </Popconfirm>
368
+ </Space>
369
+ </Card>
370
+ </motion.div>
371
+ </Col>
372
+ ))}
373
+ </Row>
374
+ ) : (
375
+ <Card>
376
+ <div
377
+ style={{
378
+ textAlign: 'center',
379
+ padding: '60px 20px',
380
+ color: '#9ca3af',
381
+ }}
382
+ >
383
+ <RobotOutlined style={{ fontSize: '64px', marginBottom: '16px', opacity: 0.5 }} />
384
+ <div style={{ fontSize: '16px', marginBottom: '8px' }}>
385
+ 还没有创建任何Agent
386
+ </div>
387
+ <div style={{ fontSize: '14px', marginBottom: '24px' }}>
388
+ 创建您的第一个AI教学助手
389
+ </div>
390
+ <Button
391
+ type="primary"
392
+ size="large"
393
+ icon={<PlusOutlined />}
394
+ style={{ backgroundColor: '#10b981' }}
395
+ >
396
+ 创建Agent
397
+ </Button>
398
+ </div>
399
+ </Card>
400
+ )}
401
+
402
+ {/* Agent详情对话框 */}
403
+ <Modal
404
+ title={<span style={{ fontSize: '20px', fontWeight: 600 }}>Agent详情</span>}
405
+ open={detailModalVisible}
406
+ onCancel={() => setDetailModalVisible(false)}
407
+ footer={null}
408
+ width={800}
409
+ >
410
+ {selectedAgent && (
411
+ <div>
412
+ <Descriptions column={2} bordered>
413
+ <Descriptions.Item label="名称" span={2}>
414
+ {selectedAgent.name}
415
+ </Descriptions.Item>
416
+ <Descriptions.Item label="描述" span={2}>
417
+ {selectedAgent.description}
418
+ </Descriptions.Item>
419
+ <Descriptions.Item label="类型">
420
+ <Tag color={getAgentTypeColor(selectedAgent.type)}>
421
+ {getAgentTypeText(selectedAgent.type)}
422
+ </Tag>
423
+ </Descriptions.Item>
424
+ <Descriptions.Item label="学科">
425
+ {selectedAgent.subject || '-'}
426
+ </Descriptions.Item>
427
+ <Descriptions.Item label="指导教师">
428
+ {selectedAgent.instructor || '-'}
429
+ </Descriptions.Item>
430
+ <Descriptions.Item label="创建时间">
431
+ {selectedAgent.created_at || '-'}
432
+ </Descriptions.Item>
433
+ </Descriptions>
434
+
435
+ <div style={{ marginTop: '24px' }}>
436
+ <h4 style={{ fontSize: '16px', fontWeight: 600, marginBottom: '12px' }}>
437
+ 启用的插件
438
+ </h4>
439
+ {selectedAgent.plugins && selectedAgent.plugins.length > 0 ? (
440
+ <Space size={[8, 8]} wrap>
441
+ {selectedAgent.plugins.map((pluginId) => (
442
+ <Tag
443
+ key={pluginId}
444
+ icon={getPluginIcon(pluginId)}
445
+ style={{ fontSize: '14px', padding: '4px 12px' }}
446
+ >
447
+ {getPluginName(pluginId)}
448
+ </Tag>
449
+ ))}
450
+ </Space>
451
+ ) : (
452
+ <div style={{ color: '#9ca3af' }}>未启用任何插件</div>
453
+ )}
454
+ </div>
455
+
456
+ <div style={{ marginTop: '24px' }}>
457
+ <h4 style={{ fontSize: '16px', fontWeight: 600, marginBottom: '12px' }}>
458
+ 关联的知识库
459
+ </h4>
460
+ {selectedAgent.knowledgeBases && selectedAgent.knowledgeBases.length > 0 ? (
461
+ <Space direction="vertical" style={{ width: '100%' }}>
462
+ {selectedAgent.knowledgeBases.map((kb) => (
463
+ <div
464
+ key={kb.id}
465
+ style={{
466
+ padding: '12px',
467
+ background: '#f8fafc',
468
+ borderRadius: '8px',
469
+ border: '1px solid #e5e7eb',
470
+ }}
471
+ >
472
+ {kb.name}
473
+ </div>
474
+ ))}
475
+ </Space>
476
+ ) : (
477
+ <div style={{ color: '#9ca3af' }}>未关联任何知识库</div>
478
+ )}
479
+ </div>
480
+
481
+ {/* 已创建的分发链接 */}
482
+ <div style={{ marginTop: '24px' }}>
483
+ <h4 style={{ fontSize: '16px', fontWeight: 600, marginBottom: '12px' }}>
484
+ 已创建的分发链接 ({selectedAgent.distributions?.length || 0})
485
+ </h4>
486
+ {selectedAgent.distributions && selectedAgent.distributions.length > 0 ? (
487
+ <Space direction="vertical" style={{ width: '100%' }}>
488
+ {selectedAgent.distributions.map((dist) => (
489
+ <div
490
+ key={dist.id}
491
+ style={{
492
+ padding: '12px',
493
+ background: '#f0f9ff',
494
+ borderRadius: '8px',
495
+ border: '1px solid #bae6fd',
496
+ }}
497
+ >
498
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
499
+ <div>
500
+ <div style={{ fontSize: '12px', color: '#6b7280' }}>
501
+ Token: {dist.token}
502
+ </div>
503
+ <div style={{ fontSize: '12px', color: '#6b7280', marginTop: '4px' }}>
504
+ 创建时间: {new Date(dist.created_at * 1000).toLocaleString()}
505
+ </div>
506
+ {dist.expires_at > 0 && (
507
+ <div style={{ fontSize: '12px', color: '#6b7280', marginTop: '4px' }}>
508
+ 过期时间: {new Date(dist.expires_at * 1000).toLocaleString()}
509
+ </div>
510
+ )}
511
+ <div style={{ fontSize: '12px', color: '#6b7280', marginTop: '4px' }}>
512
+ 使用次数: {dist.usage_count || 0}
513
+ </div>
514
+ </div>
515
+ <Space>
516
+ <Button
517
+ size="small"
518
+ icon={<CopyOutlined />}
519
+ onClick={() => {
520
+ const link = `${window.location.origin}/student/chat/${selectedAgent.id}?token=${dist.token}`;
521
+ copyToClipboard(link);
522
+ }}
523
+ >
524
+ 复制链接
525
+ </Button>
526
+ <Button
527
+ size="small"
528
+ type="link"
529
+ onClick={() => {
530
+ const link = `${window.location.origin}/student/chat/${selectedAgent.id}?token=${dist.token}`;
531
+ window.open(link, '_blank');
532
+ }}
533
+ >
534
+ 测试
535
+ </Button>
536
+ </Space>
537
+ </div>
538
+ </div>
539
+ ))}
540
+ </Space>
541
+ ) : (
542
+ <div style={{ color: '#9ca3af' }}>还没有创建分发链接</div>
543
+ )}
544
+ </div>
545
+ </div>
546
+ )}
547
+ </Modal>
548
+
549
+ {/* 创建分发链接对话框 */}
550
+ <Modal
551
+ title={<span style={{ fontSize: '20px', fontWeight: 600 }}>创建分发链接</span>}
552
+ open={distributionModalVisible}
553
+ onCancel={() => {
554
+ setDistributionModalVisible(false);
555
+ setCreatedDistribution(null);
556
+ }}
557
+ footer={null}
558
+ width={600}
559
+ >
560
+ {!createdDistribution ? (
561
+ <Form
562
+ form={distributionForm}
563
+ layout="vertical"
564
+ onFinish={handleCreateDistribution}
565
+ >
566
+ <Form.Item
567
+ label="链接有效期"
568
+ name="expiresIn"
569
+ rules={[{ required: true, message: '请选择有效期' }]}
570
+ initialValue={7}
571
+ >
572
+ <Select size="large">
573
+ <Select.Option value={7}>7天</Select.Option>
574
+ <Select.Option value={30}>30天</Select.Option>
575
+ <Select.Option value={90}>90天</Select.Option>
576
+ <Select.Option value={0}>永不过期</Select.Option>
577
+ </Select>
578
+ </Form.Item>
579
+
580
+ <Form.Item>
581
+ <Button
582
+ type="primary"
583
+ htmlType="submit"
584
+ size="large"
585
+ block
586
+ style={{ backgroundColor: '#10b981' }}
587
+ >
588
+ 生成分发链接
589
+ </Button>
590
+ </Form.Item>
591
+ </Form>
592
+ ) : (
593
+ <div>
594
+ <div
595
+ style={{
596
+ padding: '16px',
597
+ background: '#f0fdf4',
598
+ borderRadius: '8px',
599
+ border: '1px solid #86efac',
600
+ marginBottom: '16px',
601
+ }}
602
+ >
603
+ <div style={{ color: '#15803d', marginBottom: '8px', fontWeight: 500 }}>
604
+ ✓ 分发链接创建成功
605
+ </div>
606
+ </div>
607
+
608
+ <div style={{ marginBottom: '16px' }}>
609
+ <div style={{ fontSize: '14px', fontWeight: 500, marginBottom: '8px' }}>
610
+ 完整链接:
611
+ </div>
612
+ <Input.TextArea
613
+ value={createdDistribution.fullLink}
614
+ readOnly
615
+ autoSize={{ minRows: 2 }}
616
+ style={{ marginBottom: '8px' }}
617
+ />
618
+ <Button
619
+ icon={<CopyOutlined />}
620
+ onClick={() => copyToClipboard(createdDistribution.fullLink)}
621
+ block
622
+ style={{ marginBottom: '8px' }}
623
+ >
624
+ 复制完整链接
625
+ </Button>
626
+ <Button
627
+ type="primary"
628
+ onClick={() => window.open(createdDistribution.fullLink, '_blank')}
629
+ block
630
+ >
631
+ 测试链接
632
+ </Button>
633
+ </div>
634
+
635
+ <div>
636
+ <div style={{ fontSize: '14px', fontWeight: 500, marginBottom: '8px' }}>
637
+ 访问令牌(学生可在登录页输入):
638
+ </div>
639
+ <Input.TextArea
640
+ value={createdDistribution.accessToken}
641
+ readOnly
642
+ autoSize={{ minRows: 2 }}
643
+ style={{ marginBottom: '8px' }}
644
+ />
645
+ <Button
646
+ icon={<CopyOutlined />}
647
+ onClick={() => copyToClipboard(createdDistribution.accessToken)}
648
+ block
649
+ >
650
+ 复制访问令牌
651
+ </Button>
652
+ </div>
653
+ </div>
654
+ )}
655
+ </Modal>
656
+ </div>
657
+ );
658
+ };
659
+
660
+ export default AgentList;
frontend/src/pages/teacher/CreateAgent.jsx ADDED
@@ -0,0 +1,696 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 教师端 - 创建Agent页面
3
+ * 对应原始 index.html 的 create-agent section (4步向导)
4
+ * 复用原始HTML的4步表单逻辑
5
+ */
6
+ import { useState, useEffect } from 'react';
7
+ import { Steps, Card, Form, Input, Select, Button, Space, Checkbox, message, Row, Col, Modal } from 'antd';
8
+ import {
9
+ InfoCircleOutlined,
10
+ AppstoreOutlined,
11
+ DatabaseOutlined,
12
+ BranchesOutlined,
13
+ ArrowLeftOutlined,
14
+ ArrowRightOutlined,
15
+ CheckOutlined,
16
+ } from '@ant-design/icons';
17
+ import { motion } from 'framer-motion';
18
+ import knowledgeService from '../../services/knowledgeService';
19
+ import agentService from '../../services/agentService';
20
+ import WorkflowEditor from '../../components/WorkflowEditor';
21
+
22
+ const { TextArea } = Input;
23
+
24
+ const CreateAgent = () => {
25
+ const [form] = Form.useForm();
26
+ const [currentStep, setCurrentStep] = useState(0);
27
+ const [submitting, setSubmitting] = useState(false); // 创建Agent的loading
28
+ const [aiGenerating, setAiGenerating] = useState(false); // AI生成工作流的loading
29
+ const [selectedPlugins, setSelectedPlugins] = useState([]);
30
+ const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState([]);
31
+ const [knowledgeBases, setKnowledgeBases] = useState([]);
32
+ const [workflow, setWorkflow] = useState(null);
33
+ const [showWorkflowEditor, setShowWorkflowEditor] = useState(false);
34
+
35
+ // 检查是否是编辑模式
36
+ const urlParams = new URLSearchParams(window.location.search);
37
+ const isEditMode = urlParams.get('mode') === 'edit';
38
+ const [editingAgentId, setEditingAgentId] = useState(null);
39
+
40
+ // 保存每个步骤的数据
41
+ const [formData, setFormData] = useState({
42
+ name: '',
43
+ description: '',
44
+ subject: '',
45
+ instructor: '',
46
+ type: 'general',
47
+ });
48
+
49
+ // 加载知识库列表和编辑数据
50
+ useEffect(() => {
51
+ loadKnowledgeBases();
52
+
53
+ // 如果是编辑模式,加载agent数据
54
+ if (isEditMode) {
55
+ const editingAgentStr = localStorage.getItem('editingAgent');
56
+ if (editingAgentStr) {
57
+ try {
58
+ const editingAgent = JSON.parse(editingAgentStr);
59
+ setEditingAgentId(editingAgent.id);
60
+
61
+ // 设置表单数据
62
+ setFormData({
63
+ name: editingAgent.name || '',
64
+ description: editingAgent.description || '',
65
+ subject: editingAgent.subject || '',
66
+ instructor: editingAgent.instructor || '',
67
+ type: editingAgent.type || 'general',
68
+ });
69
+
70
+ // 设置表单初始值
71
+ form.setFieldsValue({
72
+ name: editingAgent.name || '',
73
+ description: editingAgent.description || '',
74
+ subject: editingAgent.subject || '',
75
+ instructor: editingAgent.instructor || '',
76
+ type: editingAgent.type || 'general',
77
+ });
78
+
79
+ // 设置其他数据
80
+ setSelectedPlugins(editingAgent.plugins || []);
81
+ setSelectedKnowledgeBases(editingAgent.knowledgeBases || []);
82
+ setWorkflow(editingAgent.workflow || null);
83
+
84
+ // 清除localStorage
85
+ localStorage.removeItem('editingAgent');
86
+ } catch (error) {
87
+ console.error('加载编辑数据失败:', error);
88
+ }
89
+ }
90
+ }
91
+ }, [isEditMode]);
92
+
93
+ const loadKnowledgeBases = async () => {
94
+ try {
95
+ const res = await knowledgeService.getKnowledgeBases();
96
+ setKnowledgeBases(res.data || []);
97
+ } catch (error) {
98
+ console.error('加载知识库失败:', error);
99
+ }
100
+ };
101
+
102
+ // 可用插件列表(对应原始HTML的插件选项)
103
+ const availablePlugins = [
104
+ {
105
+ id: 'code',
106
+ name: '代码执行',
107
+ icon: '💻',
108
+ description: '允许学生编写和执行Python代码,实时获取结果',
109
+ color: '#10b981',
110
+ },
111
+ {
112
+ id: 'visualization',
113
+ name: '3D可视化',
114
+ icon: '📊',
115
+ description: '生成交互式3D图形,帮助学生理解数学概念',
116
+ color: '#f97316',
117
+ },
118
+ {
119
+ id: 'mindmap',
120
+ name: '思维导图',
121
+ icon: '🗺️',
122
+ description: '创建结构化的思维导图,帮助学生梳理知识点',
123
+ color: '#8b5cf6',
124
+ },
125
+ ];
126
+
127
+ // Agent类型选项
128
+ const agentTypes = [
129
+ { value: 'educational', label: '教育辅导' },
130
+ { value: 'programming', label: '编程辅助' },
131
+ { value: 'math', label: '数学辅导' },
132
+ { value: 'general', label: '通用助手' },
133
+ ];
134
+
135
+ // 步骤配置
136
+ const steps = [
137
+ {
138
+ title: '基本信息',
139
+ icon: <InfoCircleOutlined />,
140
+ },
141
+ {
142
+ title: '插件选择',
143
+ icon: <AppstoreOutlined />,
144
+ },
145
+ {
146
+ title: '知识库',
147
+ icon: <DatabaseOutlined />,
148
+ },
149
+ {
150
+ title: '工作流',
151
+ icon: <BranchesOutlined />,
152
+ },
153
+ ];
154
+
155
+ // 切换插件选择
156
+ const togglePlugin = (pluginId) => {
157
+ setSelectedPlugins((prev) =>
158
+ prev.includes(pluginId)
159
+ ? prev.filter((id) => id !== pluginId)
160
+ : [...prev, pluginId]
161
+ );
162
+ };
163
+
164
+ // 切换知识库���择
165
+ const toggleKnowledgeBase = (kbId) => {
166
+ setSelectedKnowledgeBases((prev) =>
167
+ prev.includes(kbId)
168
+ ? prev.filter((id) => id !== kbId)
169
+ : [...prev, kbId]
170
+ );
171
+ };
172
+
173
+ // 生成AI工作流
174
+ const handleGenerateAIWorkflow = async () => {
175
+ try {
176
+ setAiGenerating(true);
177
+ const values = form.getFieldsValue();
178
+ const res = await agentService.generateAIWorkflow({
179
+ name: values.name,
180
+ description: values.description,
181
+ type: values.type,
182
+ plugins: selectedPlugins,
183
+ knowledgeBases: selectedKnowledgeBases,
184
+ });
185
+
186
+ if (res.success) {
187
+ setWorkflow(res.data.workflow);
188
+ message.success('AI工作流生成成功');
189
+ }
190
+ } catch (error) {
191
+ message.error('生成工作流失败');
192
+ } finally {
193
+ setAiGenerating(false);
194
+ }
195
+ };
196
+
197
+ // 下一步
198
+ const handleNext = async () => {
199
+ try {
200
+ // 验证当前步骤的表单
201
+ if (currentStep === 0) {
202
+ await form.validateFields(['name', 'description', 'type']);
203
+ // 保存第一步的表单数据
204
+ const values = form.getFieldsValue();
205
+ setFormData({
206
+ name: values.name,
207
+ description: values.description,
208
+ subject: values.subject || '', // subject 可能为空
209
+ instructor: values.instructor || '', // instructor 可能为空
210
+ type: values.type,
211
+ });
212
+ }
213
+
214
+ setCurrentStep(currentStep + 1);
215
+ } catch (error) {
216
+ message.error('请填写完整信息');
217
+ }
218
+ };
219
+
220
+ // 上一步
221
+ const handlePrev = () => {
222
+ setCurrentStep(currentStep - 1);
223
+ // 返回第一步时,恢复表单数据
224
+ if (currentStep === 1) {
225
+ setTimeout(() => {
226
+ form.setFieldsValue(formData);
227
+ }, 0);
228
+ }
229
+ };
230
+
231
+ // 提交创建或更新Agent
232
+ const handleSubmit = async () => {
233
+ try {
234
+ setSubmitting(true);
235
+
236
+ const agentData = {
237
+ name: formData.name,
238
+ description: formData.description,
239
+ subject: formData.subject,
240
+ instructor: formData.instructor,
241
+ type: formData.type,
242
+ plugins: selectedPlugins,
243
+ knowledgeBases: selectedKnowledgeBases,
244
+ workflow: workflow || {
245
+ nodes: [],
246
+ edges: [],
247
+ },
248
+ };
249
+
250
+ console.log('发送Agent数据:', agentData);
251
+
252
+ let res;
253
+ if (isEditMode && editingAgentId) {
254
+ // 更新Agent
255
+ res = await agentService.updateAgent(editingAgentId, agentData);
256
+ if (res.success) {
257
+ message.success('Agent更新成功!');
258
+ // 跳转回列表页
259
+ window.location.href = '/teacher/agents';
260
+ }
261
+ } else {
262
+ // 创建新Agent
263
+ res = await agentService.createAgent(agentData);
264
+ if (res.success) {
265
+ message.success('Agent创建成功!');
266
+ // 重置表单
267
+ form.resetFields();
268
+ setCurrentStep(0);
269
+ setSelectedPlugins([]);
270
+ setSelectedKnowledgeBases([]);
271
+ setWorkflow(null);
272
+ setFormData({
273
+ name: '',
274
+ description: '',
275
+ subject: '',
276
+ instructor: '',
277
+ type: 'general',
278
+ });
279
+ }
280
+ }
281
+
282
+ if (!res.success) {
283
+ message.error(res.message || '操作失败');
284
+ }
285
+ } catch (error) {
286
+ console.error('操作失败:', error);
287
+ message.error(error.response?.data?.message || '操作失败');
288
+ } finally {
289
+ setSubmitting(false);
290
+ }
291
+ };
292
+
293
+ // 渲染步骤内容
294
+ const renderStepContent = () => {
295
+ switch (currentStep) {
296
+ case 0:
297
+ // 步骤1: 基本信息
298
+ return (
299
+ <motion.div
300
+ initial={{ opacity: 0, x: 20 }}
301
+ animate={{ opacity: 1, x: 0 }}
302
+ transition={{ duration: 0.3 }}
303
+ >
304
+ <Form form={form} layout="vertical" size="large">
305
+ <Form.Item
306
+ label="Agent名称"
307
+ name="name"
308
+ rules={[{ required: true, message: '请输入Agent名称' }]}
309
+ >
310
+ <Input placeholder="例如:Python编程助手" />
311
+ </Form.Item>
312
+
313
+ <Form.Item
314
+ label="描述"
315
+ name="description"
316
+ rules={[{ required: true, message: '请输入描述' }]}
317
+ >
318
+ <TextArea
319
+ rows={4}
320
+ placeholder="简要描述这个Agent的功能和用途"
321
+ />
322
+ </Form.Item>
323
+
324
+ <Row gutter={16}>
325
+ <Col span={12}>
326
+ <Form.Item label="学科/课程名称" name="subject">
327
+ <Input placeholder="例如:计算机科学" />
328
+ </Form.Item>
329
+ </Col>
330
+ <Col span={12}>
331
+ <Form.Item label="指导教师" name="instructor">
332
+ <Input placeholder="例如:张老师" />
333
+ </Form.Item>
334
+ </Col>
335
+ </Row>
336
+
337
+ <Form.Item
338
+ label="Agent类型"
339
+ name="type"
340
+ rules={[{ required: true, message: '请选择Agent类型' }]}
341
+ >
342
+ <Select placeholder="选择Agent类型" options={agentTypes} />
343
+ </Form.Item>
344
+ </Form>
345
+ </motion.div>
346
+ );
347
+
348
+ case 1:
349
+ // 步骤2: 插件选择
350
+ return (
351
+ <motion.div
352
+ initial={{ opacity: 0, x: 20 }}
353
+ animate={{ opacity: 1, x: 0 }}
354
+ transition={{ duration: 0.3 }}
355
+ >
356
+ <div style={{ marginBottom: '16px' }}>
357
+ <h3 style={{ fontSize: '16px', fontWeight: 600, color: '#1f2937' }}>
358
+ 选择Agent可以使用的插件
359
+ </h3>
360
+ <p style={{ fontSize: '14px', color: '#6b7280' }}>
361
+ 这些插件将增强Agent的能力,帮助学生更好地学习
362
+ </p>
363
+ </div>
364
+
365
+ <Row gutter={[16, 16]}>
366
+ {availablePlugins.map((plugin) => (
367
+ <Col span={24} key={plugin.id}>
368
+ <motion.div
369
+ whileHover={{ x: 4 }}
370
+ style={{
371
+ padding: '20px',
372
+ background: selectedPlugins.includes(plugin.id)
373
+ ? `${plugin.color}10`
374
+ : '#f8fafc',
375
+ border: selectedPlugins.includes(plugin.id)
376
+ ? `2px solid ${plugin.color}`
377
+ : '2px solid #e5e7eb',
378
+ borderRadius: '12px',
379
+ cursor: 'pointer',
380
+ transition: 'all 0.2s',
381
+ }}
382
+ onClick={() => togglePlugin(plugin.id)}
383
+ >
384
+ <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
385
+ <div
386
+ style={{
387
+ fontSize: '32px',
388
+ width: '56px',
389
+ height: '56px',
390
+ display: 'flex',
391
+ alignItems: 'center',
392
+ justifyContent: 'center',
393
+ background: `${plugin.color}15`,
394
+ borderRadius: '12px',
395
+ }}
396
+ >
397
+ {plugin.icon}
398
+ </div>
399
+ <div style={{ flex: 1 }}>
400
+ <div style={{ fontSize: '18px', fontWeight: 600, color: '#1f2937', marginBottom: '4px' }}>
401
+ {plugin.name}
402
+ </div>
403
+ <div style={{ fontSize: '14px', color: '#6b7280' }}>
404
+ {plugin.description}
405
+ </div>
406
+ </div>
407
+ <Checkbox checked={selectedPlugins.includes(plugin.id)} />
408
+ </div>
409
+ </motion.div>
410
+ </Col>
411
+ ))}
412
+ </Row>
413
+ </motion.div>
414
+ );
415
+
416
+ case 2:
417
+ // 步骤3: 知识库关联
418
+ return (
419
+ <motion.div
420
+ initial={{ opacity: 0, x: 20 }}
421
+ animate={{ opacity: 1, x: 0 }}
422
+ transition={{ duration: 0.3 }}
423
+ >
424
+ <div style={{ marginBottom: '16px' }}>
425
+ <h3 style={{ fontSize: '16px', fontWeight: 600, color: '#1f2937' }}>
426
+ 选择关联的知识库
427
+ </h3>
428
+ <p style={{ fontSize: '14px', color: '#6b7280' }}>
429
+ 选择与此Agent相关的知识库,Agent将基于这些知识回答问题
430
+ </p>
431
+ </div>
432
+
433
+ {knowledgeBases.length > 0 ? (
434
+ <Row gutter={[16, 16]}>
435
+ {knowledgeBases.map((kb) => (
436
+ <Col span={12} key={kb.id}>
437
+ <motion.div
438
+ whileHover={{ y: -4 }}
439
+ style={{
440
+ padding: '16px',
441
+ background: selectedKnowledgeBases.includes(kb.id)
442
+ ? 'rgba(16, 185, 129, 0.1)'
443
+ : 'white',
444
+ border: selectedKnowledgeBases.includes(kb.id)
445
+ ? '2px solid #10b981'
446
+ : '1px solid #e5e7eb',
447
+ borderRadius: '12px',
448
+ cursor: 'pointer',
449
+ transition: 'all 0.2s',
450
+ }}
451
+ onClick={() => toggleKnowledgeBase(kb.id)}
452
+ >
453
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
454
+ <div>
455
+ <div style={{ fontSize: '16px', fontWeight: 600, color: '#1f2937' }}>
456
+ {kb.name}
457
+ </div>
458
+ <div style={{ fontSize: '14px', color: '#6b7280', marginTop: '4px' }}>
459
+ {kb.fileCount || 0} 个文件
460
+ </div>
461
+ </div>
462
+ <Checkbox checked={selectedKnowledgeBases.includes(kb.id)} />
463
+ </div>
464
+ </motion.div>
465
+ </Col>
466
+ ))}
467
+ </Row>
468
+ ) : (
469
+ <div style={{ textAlign: 'center', padding: '40px', color: '#9ca3af' }}>
470
+ <DatabaseOutlined style={{ fontSize: '48px', marginBottom: '16px' }} />
471
+ <div>还没有创建任何知识库</div>
472
+ <Button type="link" style={{ marginTop: '8px' }}>
473
+ 去创建知识库
474
+ </Button>
475
+ </div>
476
+ )}
477
+ </motion.div>
478
+ );
479
+
480
+ case 3:
481
+ // 步骤4: 工作流设计
482
+ return (
483
+ <motion.div
484
+ initial={{ opacity: 0, x: 20 }}
485
+ animate={{ opacity: 1, x: 0 }}
486
+ transition={{ duration: 0.3 }}
487
+ >
488
+ <div style={{ marginBottom: '24px' }}>
489
+ <h3 style={{ fontSize: '16px', fontWeight: 600, color: '#1f2937' }}>
490
+ 工作流设计
491
+ </h3>
492
+ <p style={{ fontSize: '14px', color: '#6b7280' }}>
493
+ 定义Agent如何处理学生的问题和请求
494
+ </p>
495
+ </div>
496
+
497
+ <Card
498
+ style={{
499
+ border: '1px solid rgba(139, 92, 246, 0.2)',
500
+ borderRadius: '12px',
501
+ marginBottom: '16px',
502
+ }}
503
+ >
504
+ <Space direction="vertical" size="large" style={{ width: '100%' }}>
505
+ <div>
506
+ <h4 style={{ fontSize: '16px', fontWeight: 600, marginBottom: '8px' }}>
507
+ AI自动设计(推荐)
508
+ </h4>
509
+ <p style={{ fontSize: '14px', color: '#6b7280', marginBottom: '16px' }}>
510
+ 让AI根据您选择的插件和知识库自动生成最优工作流
511
+ </p>
512
+ <Button
513
+ type="primary"
514
+ icon={<BranchesOutlined />}
515
+ loading={aiGenerating}
516
+ onClick={(e) => {
517
+ e.stopPropagation();
518
+ handleGenerateAIWorkflow();
519
+ }}
520
+ style={{ backgroundColor: '#8b5cf6' }}
521
+ >
522
+ AI自动生成工作流
523
+ </Button>
524
+ </div>
525
+
526
+ {workflow && (
527
+ <div
528
+ style={{
529
+ padding: '16px',
530
+ background: '#f8fafc',
531
+ borderRadius: '8px',
532
+ border: '1px solid #e5e7eb',
533
+ }}
534
+ >
535
+ <div style={{ fontSize: '14px', fontWeight: 500, marginBottom: '8px' }}>
536
+ 工作流已生成
537
+ </div>
538
+ <div style={{ fontSize: '12px', color: '#6b7280' }}>
539
+ 包含 {workflow.nodes?.length || 0} 个节点,
540
+ {workflow.edges?.length || 0} 条连接
541
+ </div>
542
+ </div>
543
+ )}
544
+
545
+ <div style={{ paddingTop: '16px', borderTop: '1px solid #e5e7eb' }}>
546
+ <h4 style={{ fontSize: '16px', fontWeight: 600, marginBottom: '8px' }}>
547
+ 跳过工作流配置
548
+ </h4>
549
+ <p style={{ fontSize: '14px', color: '#6b7280', marginBottom: '16px' }}>
550
+ 暂时不配置工作流,后续可以在Agent详情中进行配置
551
+ </p>
552
+ <Button
553
+ type="default"
554
+ icon={<CheckOutlined />}
555
+ onClick={(e) => {
556
+ e.stopPropagation();
557
+ setWorkflow({ nodes: [], edges: [] });
558
+ message.success('已设置为空工作流,可以直接创建Agent');
559
+ }}
560
+ style={{
561
+ borderColor: '#10b981',
562
+ color: '#10b981',
563
+ }}
564
+ >
565
+ 跳过工作流配置
566
+ </Button>
567
+ </div>
568
+
569
+ <div style={{ paddingTop: '16px', borderTop: '1px solid #e5e7eb' }}>
570
+ <h4 style={{ fontSize: '16px', fontWeight: 600, marginBottom: '8px' }}>
571
+ 手动设计(高级)
572
+ </h4>
573
+ <p style={{ fontSize: '14px', color: '#6b7280' }}>
574
+ 使用可视化编辑器自定义工作流
575
+ </p>
576
+ <Button onClick={() => setShowWorkflowEditor(true)}>
577
+ 打开工作流编辑器
578
+ </Button>
579
+ </div>
580
+ </Space>
581
+ </Card>
582
+
583
+ {/* 提示:可以直接创建 */}
584
+ <div
585
+ style={{
586
+ marginTop: '16px',
587
+ padding: '16px',
588
+ background: '#f0f9ff',
589
+ borderRadius: '8px',
590
+ border: '1px solid #bae6fd',
591
+ }}
592
+ >
593
+ <div style={{ fontSize: '14px', color: '#0369a1', marginBottom: '4px', fontWeight: 500 }}>
594
+ 💡 提示
595
+ </div>
596
+ <div style={{ fontSize: '14px', color: '#075985' }}>
597
+ 工作流设计是可选的。您可以直接点击下方的"创建Agent"按钮,先创建一个基础Agent,后续再配置工作流。
598
+ </div>
599
+ </div>
600
+ </motion.div>
601
+ );
602
+
603
+ default:
604
+ return null;
605
+ }
606
+ };
607
+
608
+ return (
609
+ <div>
610
+ <div style={{ marginBottom: '32px' }}>
611
+ <h1 style={{ fontSize: '28px', fontWeight: 600, color: '#1f2937', margin: 0 }}>
612
+ {isEditMode ? '编辑Agent' : '创建新的Agent'}
613
+ </h1>
614
+ <p style={{ fontSize: '14px', color: '#6b7280', marginTop: '8px' }}>
615
+ {isEditMode ? '修改您的AI教学助手' : '按照步骤创建您的AI教学助手'}
616
+ </p>
617
+ </div>
618
+
619
+ {/* 步骤指示器 */}
620
+ <Card style={{ marginBottom: '24px', border: '1px solid #e5e7eb', borderRadius: '12px' }}>
621
+ <Steps current={currentStep} items={steps} />
622
+ </Card>
623
+
624
+ {/* 步骤内容 */}
625
+ <Card style={{ border: '1px solid #e5e7eb', borderRadius: '12px', minHeight: '400px' }}>
626
+ {renderStepContent()}
627
+
628
+ {/* 导航按钮 */}
629
+ <div
630
+ style={{
631
+ marginTop: '32px',
632
+ paddingTop: '24px',
633
+ borderTop: '1px solid #e5e7eb',
634
+ display: 'flex',
635
+ justifyContent: 'space-between',
636
+ }}
637
+ >
638
+ <Button
639
+ size="large"
640
+ icon={<ArrowLeftOutlined />}
641
+ onClick={handlePrev}
642
+ disabled={currentStep === 0}
643
+ >
644
+ 上一步
645
+ </Button>
646
+
647
+ {currentStep < steps.length - 1 ? (
648
+ <Button
649
+ type="primary"
650
+ size="large"
651
+ icon={<ArrowRightOutlined />}
652
+ onClick={handleNext}
653
+ style={{ backgroundColor: '#10b981' }}
654
+ >
655
+ 下一步
656
+ </Button>
657
+ ) : (
658
+ <Button
659
+ type="primary"
660
+ size="large"
661
+ icon={<CheckOutlined />}
662
+ onClick={handleSubmit}
663
+ loading={submitting}
664
+ style={{ backgroundColor: '#10b981' }}
665
+ >
666
+ {isEditMode ? '保存修改' : '创建Agent'}
667
+ </Button>
668
+ )}
669
+ </div>
670
+ </Card>
671
+
672
+ {/* 工作流编辑器Modal */}
673
+ <Modal
674
+ title="工作流编辑器"
675
+ open={showWorkflowEditor}
676
+ onCancel={() => setShowWorkflowEditor(false)}
677
+ width="90%"
678
+ style={{ top: 20 }}
679
+ footer={null}
680
+ destroyOnClose
681
+ >
682
+ <WorkflowEditor
683
+ value={workflow}
684
+ onChange={(newWorkflow) => setWorkflow(newWorkflow)}
685
+ onSave={(newWorkflow) => {
686
+ setWorkflow(newWorkflow);
687
+ setShowWorkflowEditor(false);
688
+ message.success('工作流已保存');
689
+ }}
690
+ />
691
+ </Modal>
692
+ </div>
693
+ );
694
+ };
695
+
696
+ export default CreateAgent;
frontend/src/pages/teacher/Dashboard.jsx ADDED
@@ -0,0 +1,413 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 教师端 - 仪表板页面
3
+ * 对应原始 index.html 的 dashboard section
4
+ */
5
+ import { useEffect, useState } from 'react';
6
+ import { Card, Row, Col, Statistic, Button, Space } from 'antd';
7
+ import {
8
+ RobotOutlined,
9
+ DatabaseOutlined,
10
+ ShareAltOutlined,
11
+ ArrowRightOutlined,
12
+ PlusCircleOutlined,
13
+ } from '@ant-design/icons';
14
+ import { motion } from 'framer-motion';
15
+ import agentService from '../../services/agentService';
16
+ import knowledgeService from '../../services/knowledgeService';
17
+
18
+ const Dashboard = () => {
19
+ const [stats, setStats] = useState({
20
+ agentCount: 0,
21
+ knowledgeCount: 0,
22
+ distributionCount: 0,
23
+ });
24
+ const [recentAgents, setRecentAgents] = useState([]);
25
+ const [loading, setLoading] = useState(true);
26
+
27
+ useEffect(() => {
28
+ loadDashboardData();
29
+ }, []);
30
+
31
+ const loadDashboardData = async () => {
32
+ try {
33
+ setLoading(true);
34
+ // 加载Agent列表
35
+ const agentsRes = await agentService.getAgents();
36
+ const agents = agentsRes.data || [];
37
+
38
+ // 加载知识库列表
39
+ const kbRes = await knowledgeService.getKnowledgeBases();
40
+ const knowledgeBases = kbRes.data || [];
41
+
42
+ // 计算统计数据
43
+ const distributionCount = agents.reduce((sum, agent) => {
44
+ return sum + (agent.distributions?.length || 0);
45
+ }, 0);
46
+
47
+ setStats({
48
+ agentCount: agents.length,
49
+ knowledgeCount: knowledgeBases.length,
50
+ distributionCount,
51
+ });
52
+
53
+ // 获取最近的3个Agent
54
+ const recent = agents.slice(0, 3);
55
+ setRecentAgents(recent);
56
+ } catch (error) {
57
+ console.error('加载仪表板数据失败:', error);
58
+ } finally {
59
+ setLoading(false);
60
+ }
61
+ };
62
+
63
+ const quickStartSteps = [
64
+ {
65
+ step: 1,
66
+ title: '创建知识库',
67
+ description: '上传课程资料、文档和教材',
68
+ icon: <DatabaseOutlined />,
69
+ color: '#10b981',
70
+ action: 'knowledge-base',
71
+ },
72
+ {
73
+ step: 2,
74
+ title: '创建Agent',
75
+ description: '设计您的AI助教',
76
+ icon: <RobotOutlined />,
77
+ color: '#f97316',
78
+ action: 'create-agent',
79
+ },
80
+ {
81
+ step: 3,
82
+ title: '分发链接',
83
+ description: '生成分享链接给学生',
84
+ icon: <ShareAltOutlined />,
85
+ color: '#8b5cf6',
86
+ action: 'agent-list',
87
+ },
88
+ ];
89
+
90
+ return (
91
+ <div>
92
+ {/* 页面标题 */}
93
+ <div style={{ marginBottom: '24px' }}>
94
+ <h1 style={{ fontSize: '28px', fontWeight: 600, color: '#1f2937', margin: 0 }}>
95
+ 欢迎回来
96
+ </h1>
97
+ <p style={{ fontSize: '14px', color: '#6b7280', marginTop: '8px' }}>
98
+ 这是您的教学助手控制面板
99
+ </p>
100
+ </div>
101
+
102
+ {/* 统计卡片 */}
103
+ <Row gutter={[24, 24]} style={{ marginBottom: '32px' }}>
104
+ <Col xs={24} sm={8}>
105
+ <motion.div
106
+ whileHover={{
107
+ y: -4,
108
+ transition: { duration: 0.2, ease: 'easeOut' }
109
+ }}
110
+ >
111
+ <Card
112
+ loading={loading}
113
+ style={{
114
+ border: '1px solid rgba(16, 185, 129, 0.2)',
115
+ borderRadius: '12px',
116
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)',
117
+ background: '#ffffff',
118
+ transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
119
+ }}
120
+ hoverable
121
+ >
122
+ <Statistic
123
+ title={
124
+ <span style={{ color: '#6b7280', fontSize: '14px', fontWeight: 500 }}>
125
+ AI助手数量
126
+ </span>
127
+ }
128
+ value={stats.agentCount}
129
+ prefix={
130
+ <div style={{
131
+ width: '48px',
132
+ height: '48px',
133
+ borderRadius: '12px',
134
+ background: 'rgba(16, 185, 129, 0.1)',
135
+ display: 'flex',
136
+ alignItems: 'center',
137
+ justifyContent: 'center',
138
+ marginRight: '12px',
139
+ }}>
140
+ <RobotOutlined style={{ color: '#10b981', fontSize: '24px' }} />
141
+ </div>
142
+ }
143
+ valueStyle={{ color: '#1f2937', fontSize: '32px', fontWeight: 600 }}
144
+ />
145
+ </Card>
146
+ </motion.div>
147
+ </Col>
148
+ <Col xs={24} sm={8}>
149
+ <motion.div
150
+ whileHover={{
151
+ y: -4,
152
+ transition: { duration: 0.2, ease: 'easeOut' }
153
+ }}
154
+ >
155
+ <Card
156
+ loading={loading}
157
+ style={{
158
+ border: '1px solid rgba(249, 115, 22, 0.2)',
159
+ borderRadius: '12px',
160
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)',
161
+ background: '#ffffff',
162
+ transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
163
+ }}
164
+ hoverable
165
+ >
166
+ <Statistic
167
+ title={
168
+ <span style={{ color: '#6b7280', fontSize: '14px', fontWeight: 500 }}>
169
+ 知识库数量
170
+ </span>
171
+ }
172
+ value={stats.knowledgeCount}
173
+ prefix={
174
+ <div style={{
175
+ width: '48px',
176
+ height: '48px',
177
+ borderRadius: '12px',
178
+ background: 'rgba(249, 115, 22, 0.1)',
179
+ display: 'flex',
180
+ alignItems: 'center',
181
+ justifyContent: 'center',
182
+ marginRight: '12px',
183
+ }}>
184
+ <DatabaseOutlined style={{ color: '#f97316', fontSize: '24px' }} />
185
+ </div>
186
+ }
187
+ valueStyle={{ color: '#1f2937', fontSize: '32px', fontWeight: 600 }}
188
+ />
189
+ </Card>
190
+ </motion.div>
191
+ </Col>
192
+ <Col xs={24} sm={8}>
193
+ <motion.div
194
+ whileHover={{
195
+ y: -4,
196
+ transition: { duration: 0.2, ease: 'easeOut' }
197
+ }}
198
+ >
199
+ <Card
200
+ loading={loading}
201
+ style={{
202
+ border: '1px solid rgba(139, 92, 246, 0.2)',
203
+ borderRadius: '12px',
204
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)',
205
+ background: '#ffffff',
206
+ transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
207
+ }}
208
+ hoverable
209
+ >
210
+ <Statistic
211
+ title={
212
+ <span style={{ color: '#6b7280', fontSize: '14px', fontWeight: 500 }}>
213
+ 分发链接数
214
+ </span>
215
+ }
216
+ value={stats.distributionCount}
217
+ prefix={
218
+ <div style={{
219
+ width: '48px',
220
+ height: '48px',
221
+ borderRadius: '12px',
222
+ background: 'rgba(139, 92, 246, 0.1)',
223
+ display: 'flex',
224
+ alignItems: 'center',
225
+ justifyContent: 'center',
226
+ marginRight: '12px',
227
+ }}>
228
+ <ShareAltOutlined style={{ color: '#8b5cf6', fontSize: '24px' }} />
229
+ </div>
230
+ }
231
+ valueStyle={{ color: '#1f2937', fontSize: '32px', fontWeight: 600 }}
232
+ />
233
+ </Card>
234
+ </motion.div>
235
+ </Col>
236
+ </Row>
237
+
238
+ {/* 快速开始指引 */}
239
+ <Row gutter={[24, 24]} style={{ marginBottom: '32px' }}>
240
+ {quickStartSteps.map((item) => (
241
+ <Col xs={24} md={8} key={item.step}>
242
+ <motion.div
243
+ whileHover={{
244
+ y: -4,
245
+ transition: { duration: 0.2, ease: 'easeOut' }
246
+ }}
247
+ >
248
+ <Card
249
+ style={{
250
+ border: `1px solid ${item.color}30`,
251
+ borderRadius: '12px',
252
+ background: '#ffffff',
253
+ height: '100%',
254
+ cursor: 'pointer',
255
+ transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
256
+ }}
257
+ hoverable
258
+ >
259
+ <div style={{ padding: '8px' }}>
260
+ <div
261
+ style={{
262
+ width: '56px',
263
+ height: '56px',
264
+ borderRadius: '12px',
265
+ background: `${item.color}15`,
266
+ display: 'flex',
267
+ alignItems: 'center',
268
+ justifyContent: 'center',
269
+ marginBottom: '16px',
270
+ }}
271
+ >
272
+ <span style={{ color: item.color, fontSize: '28px' }}>
273
+ {item.icon}
274
+ </span>
275
+ </div>
276
+ <div
277
+ style={{
278
+ fontSize: '12px',
279
+ color: item.color,
280
+ fontWeight: 600,
281
+ marginBottom: '8px',
282
+ textTransform: 'uppercase',
283
+ letterSpacing: '0.5px',
284
+ }}
285
+ >
286
+ 第 {item.step} 步
287
+ </div>
288
+ <div style={{ fontSize: '18px', fontWeight: 600, color: '#1f2937', marginBottom: '8px' }}>
289
+ {item.title}
290
+ </div>
291
+ <div style={{ fontSize: '14px', color: '#6b7280', lineHeight: 1.6 }}>
292
+ {item.description}
293
+ </div>
294
+ </div>
295
+ </Card>
296
+ </motion.div>
297
+ </Col>
298
+ ))}
299
+ </Row>
300
+
301
+ {/* 最近创建的Agent */}
302
+ <Card
303
+ title={
304
+ <span style={{
305
+ fontSize: '18px',
306
+ fontWeight: 600,
307
+ color: '#1f2937'
308
+ }}>
309
+ 最近创建的Agent
310
+ </span>
311
+ }
312
+ extra={
313
+ <Button
314
+ type="link"
315
+ icon={<ArrowRightOutlined />}
316
+ style={{ color: '#10b981' }}
317
+ >
318
+ 查看全部
319
+ </Button>
320
+ }
321
+ style={{
322
+ border: '1px solid #e5e7eb',
323
+ borderRadius: '12px',
324
+ marginBottom: '32px',
325
+ background: '#ffffff',
326
+ }}
327
+ >
328
+ {recentAgents.length > 0 ? (
329
+ <Space direction="vertical" style={{ width: '100%' }} size="middle">
330
+ {recentAgents.map((agent) => (
331
+ <motion.div
332
+ key={agent.id}
333
+ whileHover={{
334
+ x: 4,
335
+ transition: { duration: 0.2, ease: 'easeOut' }
336
+ }}
337
+ style={{
338
+ padding: '16px',
339
+ background: '#f9fafb',
340
+ borderRadius: '8px',
341
+ border: '1px solid #e5e7eb',
342
+ cursor: 'pointer',
343
+ transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
344
+ }}
345
+ >
346
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
347
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
348
+ <div
349
+ style={{
350
+ width: '40px',
351
+ height: '40px',
352
+ borderRadius: '8px',
353
+ background: 'rgba(16, 185, 129, 0.1)',
354
+ display: 'flex',
355
+ alignItems: 'center',
356
+ justifyContent: 'center',
357
+ border: '1px solid rgba(16, 185, 129, 0.2)',
358
+ }}
359
+ >
360
+ <RobotOutlined style={{ color: '#10b981', fontSize: '20px' }} />
361
+ </div>
362
+ <div>
363
+ <div style={{ fontSize: '16px', fontWeight: 500, color: '#1f2937' }}>
364
+ {agent.name}
365
+ </div>
366
+ <div style={{ fontSize: '14px', color: '#6b7280', marginTop: '2px' }}>
367
+ {agent.description || '暂无描述'}
368
+ </div>
369
+ </div>
370
+ </div>
371
+ <ArrowRightOutlined style={{ color: '#9ca3af' }} />
372
+ </div>
373
+ </motion.div>
374
+ ))}
375
+ </Space>
376
+ ) : (
377
+ <div style={{ textAlign: 'center', padding: '40px 0' }}>
378
+ <div
379
+ style={{
380
+ width: '64px',
381
+ height: '64px',
382
+ borderRadius: '16px',
383
+ background: 'rgba(16, 185, 129, 0.1)',
384
+ display: 'flex',
385
+ alignItems: 'center',
386
+ justifyContent: 'center',
387
+ margin: '0 auto 16px',
388
+ border: '1px solid rgba(16, 185, 129, 0.2)',
389
+ }}
390
+ >
391
+ <RobotOutlined style={{ fontSize: '32px', color: '#10b981' }} />
392
+ </div>
393
+ <div style={{ color: '#6b7280', fontSize: '14px', marginBottom: '16px' }}>
394
+ 还没有创建任何Agent
395
+ </div>
396
+ <Button
397
+ type="primary"
398
+ icon={<PlusCircleOutlined />}
399
+ style={{
400
+ backgroundColor: '#10b981',
401
+ borderColor: '#10b981'
402
+ }}
403
+ >
404
+ 创建第一个Agent
405
+ </Button>
406
+ </div>
407
+ )}
408
+ </Card>
409
+ </div>
410
+ );
411
+ };
412
+
413
+ export default Dashboard;
frontend/src/pages/teacher/KnowledgeBase.jsx ADDED
@@ -0,0 +1,473 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 教师端 - 知识库管理页面
3
+ * 对应原始 index.html 的 knowledge-base section
4
+ * 复用原始HTML的知识库创建和管理逻辑
5
+ */
6
+ import { useState, useEffect } from 'react';
7
+ import { Card, Form, Input, Upload, Button, Row, Col, message, Progress, Space, Modal, List, Tag } from 'antd';
8
+ import {
9
+ UploadOutlined,
10
+ DeleteOutlined,
11
+ FileTextOutlined,
12
+ FilePdfOutlined,
13
+ FileWordOutlined,
14
+ FileImageOutlined,
15
+ PlusOutlined,
16
+ ExclamationCircleOutlined,
17
+ } from '@ant-design/icons';
18
+ import { motion } from 'framer-motion';
19
+ import knowledgeService from '../../services/knowledgeService';
20
+
21
+ const { confirm } = Modal;
22
+
23
+ const KnowledgeBase = () => {
24
+ const [form] = Form.useForm();
25
+ const [loading, setLoading] = useState(false);
26
+ const [knowledgeBases, setKnowledgeBases] = useState([]);
27
+ const [uploadProgress, setUploadProgress] = useState(0);
28
+ const [uploading, setUploading] = useState(false);
29
+ const [fileList, setFileList] = useState([]);
30
+
31
+ useEffect(() => {
32
+ loadKnowledgeBases();
33
+ }, []);
34
+
35
+ // 加载知识库列表
36
+ const loadKnowledgeBases = async () => {
37
+ try {
38
+ setLoading(true);
39
+ const res = await knowledgeService.getKnowledgeBases();
40
+ setKnowledgeBases(res.data || []);
41
+ } catch (error) {
42
+ console.error('加载知识库失败:', error);
43
+ message.error('加载知识库失败');
44
+ } finally {
45
+ setLoading(false);
46
+ }
47
+ };
48
+
49
+ // 创建知识库
50
+ const handleCreateKnowledgeBase = async (values) => {
51
+ if (fileList.length === 0) {
52
+ message.warning('请选择要上传的文件');
53
+ return;
54
+ }
55
+
56
+ try {
57
+ setUploading(true);
58
+
59
+ // 后端只支持单文件上传,所以需要逐个上传
60
+ const firstFile = fileList[0];
61
+ const formData = new FormData();
62
+ formData.append('name', values.name);
63
+ formData.append('file', firstFile.originFileObj); // 注意:使用 'file' 不是 'files'
64
+
65
+ const res = await knowledgeService.createKnowledgeBase(formData);
66
+
67
+ if (res.success) {
68
+ message.success('知识库创建成功!');
69
+
70
+ // 如果有taskId,轮询进度
71
+ if (res.data?.task_id) {
72
+ pollProgress(res.data.task_id);
73
+ }
74
+
75
+ // 如果还有其他文件,需要逐个添加到知识库
76
+ if (fileList.length > 1) {
77
+ message.info(`正在上传剩余 ${fileList.length - 1} 个文件...`);
78
+ // 获取创建的知识库ID
79
+ const kbId = `rag_${values.name}`;
80
+
81
+ // 上传剩余文件
82
+ for (let i = 1; i < fileList.length; i++) {
83
+ const additionalFormData = new FormData();
84
+ additionalFormData.append('file', fileList[i].originFileObj);
85
+
86
+ try {
87
+ const addRes = await knowledgeService.addFileToKnowledgeBase(kbId, additionalFormData);
88
+ if (addRes.success && addRes.data?.task_id) {
89
+ pollProgress(addRes.data.task_id);
90
+ }
91
+ } catch (error) {
92
+ console.error(`上传文件 ${fileList[i].name} 失败:`, error);
93
+ message.error(`文件 ${fileList[i].name} 上传失败`);
94
+ }
95
+ }
96
+ }
97
+
98
+ form.resetFields();
99
+ setFileList([]);
100
+ setUploadProgress(0);
101
+ loadKnowledgeBases();
102
+ }
103
+ } catch (error) {
104
+ message.error('创建知识库失败');
105
+ console.error('创建知识库失败:', error);
106
+ } finally {
107
+ setUploading(false);
108
+ }
109
+ };
110
+
111
+ // 轮询文件处理进度
112
+ const pollProgress = async (taskId) => {
113
+ const maxAttempts = 60; // 最多轮询60次(约1分钟)
114
+ let attempts = 0;
115
+
116
+ const poll = async () => {
117
+ try {
118
+ const res = await knowledgeService.getProgress(taskId);
119
+
120
+ if (res.data?.status === 'completed') {
121
+ setUploadProgress(100);
122
+ message.success('文件处理完成');
123
+ loadKnowledgeBases();
124
+ return;
125
+ }
126
+
127
+ if (res.data?.status === 'failed') {
128
+ message.error('文件处理失败');
129
+ return;
130
+ }
131
+
132
+ if (res.data?.progress) {
133
+ setUploadProgress(res.data.progress);
134
+ }
135
+
136
+ attempts++;
137
+ if (attempts < maxAttempts) {
138
+ setTimeout(poll, 1000); // 每秒轮询一次
139
+ }
140
+ } catch (error) {
141
+ console.error('获取进度失败:', error);
142
+ }
143
+ };
144
+
145
+ poll();
146
+ };
147
+
148
+ // 删除知识库
149
+ const handleDeleteKnowledgeBase = (kbId, kbName) => {
150
+ confirm({
151
+ title: '确认删除',
152
+ icon: <ExclamationCircleOutlined />,
153
+ content: `确定要删除知识库 "${kbName}" 吗?此操作不可恢复。`,
154
+ okText: '删除',
155
+ okType: 'danger',
156
+ cancelText: '取消',
157
+ onOk: async () => {
158
+ try {
159
+ await knowledgeService.deleteKnowledgeBase(kbId);
160
+ message.success('知识库已删除');
161
+ loadKnowledgeBases();
162
+ } catch (error) {
163
+ message.error('删除失败');
164
+ }
165
+ },
166
+ });
167
+ };
168
+
169
+ // 删除知识库中的文件
170
+ const handleDeleteFile = (indexId, fileName, kbName) => {
171
+ confirm({
172
+ title: '确认删除文件',
173
+ icon: <ExclamationCircleOutlined />,
174
+ content: `确定要从 "${kbName}" 中删除文件 "${fileName}" 吗?`,
175
+ okText: '删除',
176
+ okType: 'danger',
177
+ cancelText: '取消',
178
+ onOk: async () => {
179
+ try {
180
+ await knowledgeService.deleteFile(indexId, fileName);
181
+ message.success('文件已删除');
182
+ loadKnowledgeBases();
183
+ } catch (error) {
184
+ message.error('删除文件失败');
185
+ }
186
+ },
187
+ });
188
+ };
189
+
190
+ // 添加文件到现有知识库
191
+ const handleAddFiles = async (kbId) => {
192
+ // TODO: 实现文件添加功能
193
+ message.info('添加文件功能开发中');
194
+ };
195
+
196
+ // 获取文件图标
197
+ const getFileIcon = (fileName) => {
198
+ if (!fileName || typeof fileName !== 'string') {
199
+ return <FileTextOutlined style={{ color: '#6b7280', fontSize: '20px' }} />;
200
+ }
201
+
202
+ const ext = fileName.split('.').pop().toLowerCase();
203
+
204
+ if (['txt', 'md'].includes(ext)) {
205
+ return <FileTextOutlined style={{ color: '#10b981', fontSize: '20px' }} />;
206
+ }
207
+ if (['pdf'].includes(ext)) {
208
+ return <FilePdfOutlined style={{ color: '#ef4444', fontSize: '20px' }} />;
209
+ }
210
+ if (['doc', 'docx'].includes(ext)) {
211
+ return <FileWordOutlined style={{ color: '#3b82f6', fontSize: '20px' }} />;
212
+ }
213
+ if (['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(ext)) {
214
+ return <FileImageOutlined style={{ color: '#f59e0b', fontSize: '20px' }} />;
215
+ }
216
+ return <FileTextOutlined style={{ color: '#6b7280', fontSize: '20px' }} />;
217
+ };
218
+
219
+ // 上传配置
220
+ const uploadProps = {
221
+ fileList,
222
+ onChange: ({ fileList: newFileList }) => setFileList(newFileList),
223
+ beforeUpload: (file) => {
224
+ const isValidType = [
225
+ 'text/plain',
226
+ 'text/markdown',
227
+ 'application/pdf',
228
+ 'application/msword',
229
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
230
+ 'image/jpeg',
231
+ 'image/png',
232
+ 'image/gif',
233
+ 'image/bmp',
234
+ ].includes(file.type);
235
+
236
+ if (!isValidType) {
237
+ message.error('不支持的文件类型');
238
+ return Upload.LIST_IGNORE;
239
+ }
240
+
241
+ const isLt16M = file.size / 1024 / 1024 < 16;
242
+ if (!isLt16M) {
243
+ message.error('文件大小不能超过 16MB');
244
+ return Upload.LIST_IGNORE;
245
+ }
246
+
247
+ return false; // 阻止自动上传
248
+ },
249
+ multiple: true,
250
+ };
251
+
252
+ return (
253
+ <div>
254
+ <div style={{ marginBottom: '32px' }}>
255
+ <h1 style={{ fontSize: '28px', fontWeight: 600, color: '#1f2937', margin: 0 }}>
256
+ 知识库管理
257
+ </h1>
258
+ <p style={{ fontSize: '14px', color: '#6b7280', marginTop: '8px' }}>
259
+ 创建和管理您的教学资料知识库
260
+ </p>
261
+ </div>
262
+
263
+ <Row gutter={24}>
264
+ {/* 左侧:创建知识库 */}
265
+ <Col xs={24} lg={10}>
266
+ <Card
267
+ title={
268
+ <span style={{ fontSize: '18px', fontWeight: 600 }}>
269
+ <PlusOutlined style={{ marginRight: '8px', color: '#10b981' }} />
270
+ 创建新知识库
271
+ </span>
272
+ }
273
+ style={{ border: '1px solid #e5e7eb', borderRadius: '12px' }}
274
+ >
275
+ <Form form={form} layout="vertical" onFinish={handleCreateKnowledgeBase}>
276
+ <Form.Item
277
+ label="知识库名称"
278
+ name="name"
279
+ rules={[{ required: true, message: '请输入知识库名称' }]}
280
+ >
281
+ <Input
282
+ placeholder="例如:Python编程资料"
283
+ size="large"
284
+ />
285
+ </Form.Item>
286
+
287
+ <Form.Item label="上传文件">
288
+ <Upload {...uploadProps}>
289
+ <Button icon={<UploadOutlined />} size="large" block>
290
+ 选择文件
291
+ </Button>
292
+ </Upload>
293
+ <div style={{ marginTop: '8px', fontSize: '12px', color: '#6b7280' }}>
294
+ 支持格式:TXT, MD, PDF, DOC, DOCX, JPG, PNG, GIF, BMP
295
+ <br />
296
+ 单个文件最大 16MB
297
+ </div>
298
+ </Form.Item>
299
+
300
+ {uploading && uploadProgress > 0 && (
301
+ <Form.Item>
302
+ <Progress
303
+ percent={uploadProgress}
304
+ status={uploadProgress === 100 ? 'success' : 'active'}
305
+ strokeColor="#10b981"
306
+ />
307
+ </Form.Item>
308
+ )}
309
+
310
+ <Form.Item>
311
+ <Button
312
+ type="primary"
313
+ htmlType="submit"
314
+ size="large"
315
+ loading={uploading}
316
+ block
317
+ style={{ backgroundColor: '#10b981' }}
318
+ >
319
+ 创���知识库
320
+ </Button>
321
+ </Form.Item>
322
+ </Form>
323
+ </Card>
324
+ </Col>
325
+
326
+ {/* 右侧:现有知识库列表 */}
327
+ <Col xs={24} lg={14}>
328
+ <Card
329
+ title={
330
+ <span style={{ fontSize: '18px', fontWeight: 600 }}>
331
+ 现有知识库
332
+ </span>
333
+ }
334
+ style={{ border: '1px solid #e5e7eb', borderRadius: '12px' }}
335
+ loading={loading}
336
+ >
337
+ {knowledgeBases.length > 0 ? (
338
+ <Space direction="vertical" size="large" style={{ width: '100%' }}>
339
+ {knowledgeBases.map((kb) => (
340
+ <motion.div
341
+ key={kb.id}
342
+ initial={{ opacity: 0, y: 10 }}
343
+ animate={{ opacity: 1, y: 0 }}
344
+ style={{
345
+ padding: '20px',
346
+ background: '#f8fafc',
347
+ borderRadius: '12px',
348
+ border: '1px solid #e5e7eb',
349
+ }}
350
+ >
351
+ {/* 知识库头部 */}
352
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
353
+ <div>
354
+ <h3 style={{ fontSize: '18px', fontWeight: 600, margin: 0, color: '#1f2937' }}>
355
+ {kb.name}
356
+ </h3>
357
+ <div style={{ fontSize: '12px', color: '#6b7280', marginTop: '4px' }}>
358
+ 知识库ID: {kb.id}
359
+ </div>
360
+ </div>
361
+ <Space>
362
+ <Button
363
+ type="primary"
364
+ size="small"
365
+ icon={<PlusOutlined />}
366
+ onClick={() => handleAddFiles(kb.id)}
367
+ >
368
+ 添加文件
369
+ </Button>
370
+ <Button
371
+ danger
372
+ size="small"
373
+ icon={<DeleteOutlined />}
374
+ onClick={() => handleDeleteKnowledgeBase(kb.id, kb.name)}
375
+ >
376
+ 删除
377
+ </Button>
378
+ </Space>
379
+ </div>
380
+
381
+ {/* 文件列表 */}
382
+ {kb.files && kb.files.length > 0 ? (
383
+ <div
384
+ style={{
385
+ background: 'white',
386
+ borderRadius: '8px',
387
+ padding: '12px',
388
+ border: '1px solid #e5e7eb',
389
+ }}
390
+ >
391
+ <List
392
+ size="small"
393
+ dataSource={kb.files}
394
+ renderItem={(file) => (
395
+ <List.Item
396
+ actions={[
397
+ <Button
398
+ type="text"
399
+ danger
400
+ size="small"
401
+ icon={<DeleteOutlined />}
402
+ onClick={() => handleDeleteFile(kb.id, file.name, kb.name)}
403
+ >
404
+ 删除
405
+ </Button>,
406
+ ]}
407
+ >
408
+ <List.Item.Meta
409
+ avatar={getFileIcon(file.name)}
410
+ title={
411
+ <span style={{ fontSize: '14px', color: '#1f2937' }}>
412
+ {file.name}
413
+ </span>
414
+ }
415
+ description={
416
+ <span style={{ fontSize: '12px', color: '#6b7280' }}>
417
+ {file.size || '未知大小'}
418
+ </span>
419
+ }
420
+ />
421
+ </List.Item>
422
+ )}
423
+ />
424
+ </div>
425
+ ) : (
426
+ <div
427
+ style={{
428
+ textAlign: 'center',
429
+ padding: '20px',
430
+ color: '#9ca3af',
431
+ background: 'white',
432
+ borderRadius: '8px',
433
+ border: '1px dashed #d1d5db',
434
+ }}
435
+ >
436
+ <FileTextOutlined style={{ fontSize: '32px', marginBottom: '8px' }} />
437
+ <div>暂无文件</div>
438
+ </div>
439
+ )}
440
+
441
+ {/* 知识库��数据 */}
442
+ <div style={{ marginTop: '12px', display: 'flex', gap: '12px' }}>
443
+ <Tag color="blue">{kb.fileCount || 0} 个文件</Tag>
444
+ <Tag color="green">索引: {kb.id}</Tag>
445
+ </div>
446
+ </motion.div>
447
+ ))}
448
+ </Space>
449
+ ) : (
450
+ <div
451
+ style={{
452
+ textAlign: 'center',
453
+ padding: '60px 20px',
454
+ color: '#9ca3af',
455
+ }}
456
+ >
457
+ <FileTextOutlined style={{ fontSize: '64px', marginBottom: '16px', opacity: 0.5 }} />
458
+ <div style={{ fontSize: '16px', marginBottom: '8px' }}>
459
+ 还没有创建任何知识库
460
+ </div>
461
+ <div style={{ fontSize: '14px' }}>
462
+ 在左侧创建您的第一个知识库
463
+ </div>
464
+ </div>
465
+ )}
466
+ </Card>
467
+ </Col>
468
+ </Row>
469
+ </div>
470
+ );
471
+ };
472
+
473
+ export default KnowledgeBase;
frontend/src/pages/teacher/StudentManagement.jsx ADDED
@@ -0,0 +1,564 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 学生管理组件
3
+ * 教师可以管理学生账号,查看学习进度
4
+ */
5
+ import { useState, useEffect } from 'react';
6
+ import {
7
+ Card,
8
+ Table,
9
+ Button,
10
+ Space,
11
+ Tag,
12
+ Modal,
13
+ Form,
14
+ Input,
15
+ Select,
16
+ message,
17
+ Popconfirm,
18
+ Tooltip,
19
+ Avatar,
20
+ Progress,
21
+ Row,
22
+ Col,
23
+ Statistic,
24
+ Typography,
25
+ Drawer,
26
+ Timeline,
27
+ Badge
28
+ } from 'antd';
29
+ import {
30
+ UserAddOutlined,
31
+ EditOutlined,
32
+ DeleteOutlined,
33
+ ReloadOutlined,
34
+ SearchOutlined,
35
+ TeamOutlined,
36
+ TrophyOutlined,
37
+ ClockCircleOutlined,
38
+ MailOutlined,
39
+ LockOutlined,
40
+ EyeOutlined,
41
+ FileTextOutlined,
42
+ CheckCircleOutlined,
43
+ ExclamationCircleOutlined
44
+ } from '@ant-design/icons';
45
+ import api from '../../services/api';
46
+ import { motion } from 'framer-motion';
47
+
48
+ const { Title, Text, Paragraph } = Typography;
49
+ const { Option } = Select;
50
+
51
+ const StudentManagement = () => {
52
+ const [students, setStudents] = useState([]);
53
+ const [loading, setLoading] = useState(false);
54
+ const [modalVisible, setModalVisible] = useState(false);
55
+ const [editingStudent, setEditingStudent] = useState(null);
56
+ const [selectedStudent, setSelectedStudent] = useState(null);
57
+ const [drawerVisible, setDrawerVisible] = useState(false);
58
+ const [searchText, setSearchText] = useState('');
59
+ const [form] = Form.useForm();
60
+
61
+ // 加载学生列表
62
+ const fetchStudents = async () => {
63
+ setLoading(true);
64
+ try {
65
+ const response = await api.get('/teacher/students');
66
+ if (response.success) {
67
+ setStudents(response.students || []);
68
+ } else {
69
+ message.error('加载学生列表失败');
70
+ }
71
+ } catch (error) {
72
+ console.error('加载学生列表失败:', error);
73
+ message.error('加载学生列表失败');
74
+ } finally {
75
+ setLoading(false);
76
+ }
77
+ };
78
+
79
+ useEffect(() => {
80
+ fetchStudents();
81
+ }, []);
82
+
83
+ // 创建或编辑学生
84
+ const handleSubmit = async (values) => {
85
+ try {
86
+ const url = editingStudent
87
+ ? `/teacher/students/${editingStudent.id}`
88
+ : '/teacher/students';
89
+
90
+ const method = editingStudent ? 'put' : 'post';
91
+
92
+ const response = await api[method](url, values);
93
+
94
+ if (response.success) {
95
+ message.success(editingStudent ? '学生信息已更新' : '学生已创建');
96
+ setModalVisible(false);
97
+ form.resetFields();
98
+ setEditingStudent(null);
99
+ fetchStudents();
100
+ } else {
101
+ message.error(response.message || '操作失败');
102
+ }
103
+ } catch (error) {
104
+ console.error('操作失败:', error);
105
+ message.error('操作失败');
106
+ }
107
+ };
108
+
109
+ // 删除学生
110
+ const handleDelete = async (studentId) => {
111
+ try {
112
+ const response = await api.delete(`/teacher/students/${studentId}`);
113
+ if (response.success) {
114
+ message.success('学生已删除');
115
+ fetchStudents();
116
+ } else {
117
+ message.error('删除失败');
118
+ }
119
+ } catch (error) {
120
+ console.error('删除失败:', error);
121
+ message.error('删除失败');
122
+ }
123
+ };
124
+
125
+ // 重置密码
126
+ const handleResetPassword = async (studentId) => {
127
+ try {
128
+ const response = await api.post(`/teacher/students/${studentId}/reset-password`);
129
+ if (response.success) {
130
+ Modal.success({
131
+ title: '密码重置成功',
132
+ content: `新密码:${response.newPassword}`,
133
+ });
134
+ } else {
135
+ message.error('重置密码失败');
136
+ }
137
+ } catch (error) {
138
+ console.error('重置密码失败:', error);
139
+ message.error('重置密码失败');
140
+ }
141
+ };
142
+
143
+ // 查看学生详情
144
+ const handleViewDetails = async (student) => {
145
+ setSelectedStudent(student);
146
+ setDrawerVisible(true);
147
+
148
+ // 加载学生的学习进度
149
+ try {
150
+ const response = await api.get(`/teacher/students/${student.id}/progress`);
151
+ if (response.success) {
152
+ setSelectedStudent({
153
+ ...student,
154
+ progress: response.progress
155
+ });
156
+ }
157
+ } catch (error) {
158
+ console.error('加载学生进度失败:', error);
159
+ }
160
+ };
161
+
162
+ // 表格列配置
163
+ const columns = [
164
+ {
165
+ title: '学生信息',
166
+ key: 'info',
167
+ render: (_, record) => (
168
+ <Space>
169
+ <Avatar style={{ backgroundColor: '#10b981' }}>
170
+ {record.name ? record.name[0] : 'S'}
171
+ </Avatar>
172
+ <div>
173
+ <div style={{ fontWeight: 500 }}>{record.name}</div>
174
+ <Text type="secondary" style={{ fontSize: '12px' }}>
175
+ {record.username}
176
+ </Text>
177
+ </div>
178
+ </Space>
179
+ ),
180
+ },
181
+ {
182
+ title: '邮箱',
183
+ dataIndex: 'email',
184
+ key: 'email',
185
+ render: (email) => (
186
+ <Space>
187
+ <MailOutlined />
188
+ <span>{email || '未设置'}</span>
189
+ </Space>
190
+ ),
191
+ },
192
+ {
193
+ title: '学号',
194
+ dataIndex: 'student_id',
195
+ key: 'student_id',
196
+ },
197
+ {
198
+ title: '班级',
199
+ dataIndex: 'class',
200
+ key: 'class',
201
+ render: (cls) => <Tag color="blue">{cls || '未分配'}</Tag>,
202
+ },
203
+ {
204
+ title: '学习进度',
205
+ key: 'progress',
206
+ render: (_, record) => {
207
+ const progress = record.total_interactions || 0;
208
+ const level = progress > 50 ? 'high' : progress > 20 ? 'medium' : 'low';
209
+ const color = level === 'high' ? '#52c41a' : level === 'medium' ? '#faad14' : '#f5222d';
210
+
211
+ return (
212
+ <Tooltip title={`总互动次数: ${progress}`}>
213
+ <Progress
214
+ percent={Math.min(progress, 100)}
215
+ size="small"
216
+ strokeColor={color}
217
+ format={() => `${progress}次`}
218
+ />
219
+ </Tooltip>
220
+ );
221
+ },
222
+ },
223
+ {
224
+ title: '状态',
225
+ key: 'status',
226
+ render: (_, record) => {
227
+ const isActive = record.last_active &&
228
+ (Date.now() - new Date(record.last_active).getTime()) < 7 * 24 * 60 * 60 * 1000;
229
+
230
+ return (
231
+ <Badge
232
+ status={isActive ? 'success' : 'default'}
233
+ text={isActive ? '活跃' : '不活跃'}
234
+ />
235
+ );
236
+ },
237
+ },
238
+ {
239
+ title: '操作',
240
+ key: 'action',
241
+ fixed: 'right',
242
+ render: (_, record) => (
243
+ <Space size="small">
244
+ <Tooltip title="查看详情">
245
+ <Button
246
+ type="text"
247
+ icon={<EyeOutlined />}
248
+ onClick={() => handleViewDetails(record)}
249
+ />
250
+ </Tooltip>
251
+ <Tooltip title="编辑">
252
+ <Button
253
+ type="text"
254
+ icon={<EditOutlined />}
255
+ onClick={() => {
256
+ setEditingStudent(record);
257
+ form.setFieldsValue(record);
258
+ setModalVisible(true);
259
+ }}
260
+ />
261
+ </Tooltip>
262
+ <Tooltip title="重置密码">
263
+ <Popconfirm
264
+ title="确定要重置该学生的密码吗?"
265
+ onConfirm={() => handleResetPassword(record.id)}
266
+ >
267
+ <Button type="text" icon={<LockOutlined />} />
268
+ </Popconfirm>
269
+ </Tooltip>
270
+ <Tooltip title="删除">
271
+ <Popconfirm
272
+ title="确定要删除该学生吗?"
273
+ onConfirm={() => handleDelete(record.id)}
274
+ >
275
+ <Button type="text" danger icon={<DeleteOutlined />} />
276
+ </Popconfirm>
277
+ </Tooltip>
278
+ </Space>
279
+ ),
280
+ },
281
+ ];
282
+
283
+ // 过滤学生
284
+ const filteredStudents = students.filter((student) =>
285
+ student.name?.toLowerCase().includes(searchText.toLowerCase()) ||
286
+ student.username?.toLowerCase().includes(searchText.toLowerCase()) ||
287
+ student.email?.toLowerCase().includes(searchText.toLowerCase()) ||
288
+ student.student_id?.toLowerCase().includes(searchText.toLowerCase())
289
+ );
290
+
291
+ // 统计数据
292
+ const stats = {
293
+ total: students.length,
294
+ active: students.filter(s => s.last_active &&
295
+ (Date.now() - new Date(s.last_active).getTime()) < 7 * 24 * 60 * 60 * 1000).length,
296
+ avgProgress: students.reduce((sum, s) => sum + (s.total_interactions || 0), 0) / (students.length || 1)
297
+ };
298
+
299
+ return (
300
+ <div style={{ padding: '24px' }}>
301
+ {/* 统计卡片 */}
302
+ <Row gutter={[16, 16]} style={{ marginBottom: '24px' }}>
303
+ <Col xs={24} sm={8}>
304
+ <Card>
305
+ <Statistic
306
+ title="总学生数"
307
+ value={stats.total}
308
+ prefix={<TeamOutlined />}
309
+ valueStyle={{ color: '#1890ff' }}
310
+ />
311
+ </Card>
312
+ </Col>
313
+ <Col xs={24} sm={8}>
314
+ <Card>
315
+ <Statistic
316
+ title="活跃学生"
317
+ value={stats.active}
318
+ prefix={<CheckCircleOutlined />}
319
+ valueStyle={{ color: '#52c41a' }}
320
+ />
321
+ </Card>
322
+ </Col>
323
+ <Col xs={24} sm={8}>
324
+ <Card>
325
+ <Statistic
326
+ title="平均互动次数"
327
+ value={stats.avgProgress}
328
+ precision={1}
329
+ prefix={<TrophyOutlined />}
330
+ valueStyle={{ color: '#faad14' }}
331
+ />
332
+ </Card>
333
+ </Col>
334
+ </Row>
335
+
336
+ {/* 学生列表 */}
337
+ <Card
338
+ title={
339
+ <Space>
340
+ <TeamOutlined />
341
+ <span>学生管理</span>
342
+ </Space>
343
+ }
344
+ extra={
345
+ <Space>
346
+ <Input
347
+ placeholder="搜索学生..."
348
+ prefix={<SearchOutlined />}
349
+ value={searchText}
350
+ onChange={(e) => setSearchText(e.target.value)}
351
+ style={{ width: 200 }}
352
+ />
353
+ <Button
354
+ icon={<ReloadOutlined />}
355
+ onClick={fetchStudents}
356
+ loading={loading}
357
+ >
358
+ 刷新
359
+ </Button>
360
+ <Button
361
+ type="primary"
362
+ icon={<UserAddOutlined />}
363
+ onClick={() => {
364
+ setEditingStudent(null);
365
+ form.resetFields();
366
+ setModalVisible(true);
367
+ }}
368
+ >
369
+ 添加学生
370
+ </Button>
371
+ </Space>
372
+ }
373
+ >
374
+ <Table
375
+ columns={columns}
376
+ dataSource={filteredStudents}
377
+ rowKey="id"
378
+ loading={loading}
379
+ pagination={{
380
+ pageSize: 10,
381
+ showSizeChanger: true,
382
+ showTotal: (total) => `共 ${total} 条记录`,
383
+ }}
384
+ />
385
+ </Card>
386
+
387
+ {/* 创建/编辑学生Modal */}
388
+ <Modal
389
+ title={editingStudent ? '编辑学生' : '添加学生'}
390
+ open={modalVisible}
391
+ onCancel={() => {
392
+ setModalVisible(false);
393
+ form.resetFields();
394
+ setEditingStudent(null);
395
+ }}
396
+ footer={null}
397
+ >
398
+ <Form
399
+ form={form}
400
+ layout="vertical"
401
+ onFinish={handleSubmit}
402
+ >
403
+ <Form.Item
404
+ name="username"
405
+ label="用户名"
406
+ rules={[{ required: true, message: '请输入用户名' }]}
407
+ >
408
+ <Input prefix={<UserAddOutlined />} placeholder="学生登录用户名" />
409
+ </Form.Item>
410
+
411
+ <Form.Item
412
+ name="password"
413
+ label="密码"
414
+ rules={[
415
+ { required: !editingStudent, message: '请输入密码' },
416
+ { min: 6, message: '密码至少6个字符' }
417
+ ]}
418
+ >
419
+ <Input.Password
420
+ prefix={<LockOutlined />}
421
+ placeholder={editingStudent ? '留空保持原密码' : '登录密码'}
422
+ />
423
+ </Form.Item>
424
+
425
+ <Form.Item
426
+ name="name"
427
+ label="姓名"
428
+ rules={[{ required: true, message: '请输入姓名' }]}
429
+ >
430
+ <Input placeholder="学生真实姓名" />
431
+ </Form.Item>
432
+
433
+ <Form.Item
434
+ name="student_id"
435
+ label="学号"
436
+ >
437
+ <Input placeholder="学生学号" />
438
+ </Form.Item>
439
+
440
+ <Form.Item
441
+ name="email"
442
+ label="邮箱"
443
+ rules={[{ type: 'email', message: '请输入有效的邮箱地址' }]}
444
+ >
445
+ <Input prefix={<MailOutlined />} placeholder="学生邮箱" />
446
+ </Form.Item>
447
+
448
+ <Form.Item
449
+ name="class"
450
+ label="班级"
451
+ >
452
+ <Input placeholder="所在班级" />
453
+ </Form.Item>
454
+
455
+ <Form.Item>
456
+ <Space style={{ width: '100%', justifyContent: 'flex-end' }}>
457
+ <Button onClick={() => setModalVisible(false)}>
458
+ 取消
459
+ </Button>
460
+ <Button type="primary" htmlType="submit">
461
+ {editingStudent ? '更新' : '创建'}
462
+ </Button>
463
+ </Space>
464
+ </Form.Item>
465
+ </Form>
466
+ </Modal>
467
+
468
+ {/* 学生详情Drawer */}
469
+ <Drawer
470
+ title="学生详情"
471
+ placement="right"
472
+ width={600}
473
+ open={drawerVisible}
474
+ onClose={() => setDrawerVisible(false)}
475
+ >
476
+ {selectedStudent && (
477
+ <div>
478
+ <Card style={{ marginBottom: '16px' }}>
479
+ <Space align="center" style={{ width: '100%' }}>
480
+ <Avatar size={64} style={{ backgroundColor: '#10b981' }}>
481
+ {selectedStudent.name ? selectedStudent.name[0] : 'S'}
482
+ </Avatar>
483
+ <div style={{ flex: 1 }}>
484
+ <Title level={4} style={{ margin: 0 }}>
485
+ {selectedStudent.name}
486
+ </Title>
487
+ <Text type="secondary">@{selectedStudent.username}</Text>
488
+ </div>
489
+ </Space>
490
+
491
+ <div style={{ marginTop: '24px' }}>
492
+ <Row gutter={[16, 16]}>
493
+ <Col span={12}>
494
+ <Text type="secondary">学号</Text>
495
+ <div>{selectedStudent.student_id || '未设置'}</div>
496
+ </Col>
497
+ <Col span={12}>
498
+ <Text type="secondary">班级</Text>
499
+ <div>{selectedStudent.class || '未分配'}</div>
500
+ </Col>
501
+ <Col span={12}>
502
+ <Text type="secondary">邮箱</Text>
503
+ <div>{selectedStudent.email || '未设置'}</div>
504
+ </Col>
505
+ <Col span={12}>
506
+ <Text type="secondary">注册��间</Text>
507
+ <div>{selectedStudent.created_at || '未知'}</div>
508
+ </Col>
509
+ </Row>
510
+ </div>
511
+ </Card>
512
+
513
+ {/* 学习进度 */}
514
+ {selectedStudent.progress && (
515
+ <Card title="学习进度" style={{ marginBottom: '16px' }}>
516
+ <Space direction="vertical" style={{ width: '100%' }}>
517
+ <div>
518
+ <Text type="secondary">总互动次数</Text>
519
+ <Title level={4}>{selectedStudent.progress.total_interactions || 0}</Title>
520
+ </div>
521
+ <div>
522
+ <Text type="secondary">最近活跃</Text>
523
+ <div>{selectedStudent.progress.last_active || '从未活跃'}</div>
524
+ </div>
525
+ <div>
526
+ <Text type="secondary">常用Agent</Text>
527
+ <div>
528
+ {selectedStudent.progress.frequently_used_agents?.map(agent => (
529
+ <Tag key={agent}>{agent}</Tag>
530
+ )) || '暂无'}
531
+ </div>
532
+ </div>
533
+ </Space>
534
+ </Card>
535
+ )}
536
+
537
+ {/* 最近活动 */}
538
+ <Card title="最近活动">
539
+ {selectedStudent.progress?.recent_activities?.length > 0 ? (
540
+ <Timeline>
541
+ {selectedStudent.progress.recent_activities.map((activity, index) => (
542
+ <Timeline.Item key={index}>
543
+ <div>
544
+ <Text strong>{activity.title}</Text>
545
+ <br />
546
+ <Text type="secondary" style={{ fontSize: '12px' }}>
547
+ {activity.time}
548
+ </Text>
549
+ </div>
550
+ </Timeline.Item>
551
+ ))}
552
+ </Timeline>
553
+ ) : (
554
+ <Empty description="暂无活动记录" />
555
+ )}
556
+ </Card>
557
+ </div>
558
+ )}
559
+ </Drawer>
560
+ </div>
561
+ );
562
+ };
563
+
564
+ export default StudentManagement;
frontend/src/pages/teacher/TeacherDashboard.jsx ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 教师端主框架
3
+ * 复用原始 index.html 的侧边栏和导航结构
4
+ */
5
+ import { useState } from 'react';
6
+ import { Layout, Menu, message } from 'antd';
7
+ import { useNavigate } from 'react-router-dom';
8
+ import { useDispatch, useSelector } from 'react-redux';
9
+ import {
10
+ DashboardOutlined,
11
+ PlusCircleOutlined,
12
+ DatabaseOutlined,
13
+ AppstoreOutlined,
14
+ LogoutOutlined,
15
+ UserOutlined,
16
+ } from '@ant-design/icons';
17
+ import { logout } from '../../store/slices/authSlice';
18
+ import authService from '../../services/authService';
19
+
20
+ // 导入子页面组件
21
+ import Dashboard from './Dashboard';
22
+ import CreateAgent from './CreateAgent';
23
+ import KnowledgeBase from './KnowledgeBase';
24
+ import StudentManagement from './StudentManagement';
25
+ import AgentList from './AgentList';
26
+
27
+ const { Sider, Content } = Layout;
28
+
29
+ const TeacherDashboard = () => {
30
+ const navigate = useNavigate();
31
+ const dispatch = useDispatch();
32
+ const user = useSelector((state) => state.auth.user);
33
+ const [currentSection, setCurrentSection] = useState('dashboard');
34
+
35
+ // 菜单项(对应原始HTML的侧边栏菜单)
36
+ const menuItems = [
37
+ {
38
+ key: 'dashboard',
39
+ icon: <DashboardOutlined />,
40
+ label: '仪表板',
41
+ },
42
+ {
43
+ key: 'create-agent',
44
+ icon: <PlusCircleOutlined />,
45
+ label: '创建Agent',
46
+ },
47
+ {
48
+ key: 'knowledge-base',
49
+ icon: <DatabaseOutlined />,
50
+ label: '知识库',
51
+ },
52
+ {
53
+ key: 'agent-list',
54
+ icon: <AppstoreOutlined />,
55
+ label: 'Agent列表',
56
+ },
57
+ {
58
+ key: 'students',
59
+ icon: <UserOutlined />,
60
+ label: '学生管理',
61
+ },
62
+ ];
63
+
64
+ // 登出处理
65
+ const handleLogout = async () => {
66
+ try {
67
+ await authService.logout();
68
+ dispatch(logout());
69
+ message.success('已退出登录');
70
+ navigate('/login');
71
+ } catch (error) {
72
+ message.error('退出登录失败');
73
+ }
74
+ };
75
+
76
+ // 渲染当前内容区域
77
+ const renderContent = () => {
78
+ switch (currentSection) {
79
+ case 'dashboard':
80
+ return <Dashboard onNavigate={setCurrentSection} />;
81
+ case 'create-agent':
82
+ return <CreateAgent />;
83
+ case 'knowledge-base':
84
+ return <KnowledgeBase />;
85
+ case 'agent-list':
86
+ return <AgentList />;
87
+ case 'students':
88
+ return <StudentManagement />;
89
+ default:
90
+ return <Dashboard onNavigate={setCurrentSection} />;
91
+ }
92
+ };
93
+
94
+ return (
95
+ <Layout style={{ minHeight: '100vh', backgroundColor: '#fafbfc' }}>
96
+ {/* 侧边栏 */}
97
+ <Sider
98
+ width={260}
99
+ style={{
100
+ background: '#ffffff',
101
+ boxShadow: '2px 0 8px rgba(0, 0, 0, 0.04)',
102
+ position: 'fixed',
103
+ height: '100vh',
104
+ left: 0,
105
+ top: 0,
106
+ bottom: 0,
107
+ }}
108
+ >
109
+ {/* Logo区域 */}
110
+ <div
111
+ style={{
112
+ height: '64px',
113
+ display: 'flex',
114
+ alignItems: 'center',
115
+ justifyContent: 'center',
116
+ borderBottom: '1px solid #f0f0f0',
117
+ padding: '0 24px',
118
+ }}
119
+ >
120
+ <div
121
+ style={{
122
+ width: '40px',
123
+ height: '40px',
124
+ borderRadius: '10px',
125
+ background: 'linear-gradient(135deg, #10b981, #14b8a6)',
126
+ display: 'flex',
127
+ alignItems: 'center',
128
+ justifyContent: 'center',
129
+ marginRight: '12px',
130
+ }}
131
+ >
132
+ <svg
133
+ width="24"
134
+ height="24"
135
+ viewBox="0 0 24 24"
136
+ fill="none"
137
+ stroke="white"
138
+ strokeWidth="2"
139
+ strokeLinecap="round"
140
+ strokeLinejoin="round"
141
+ >
142
+ <path d="M12 2L2 7l10 5 10-5-10-5z" />
143
+ <path d="M2 17l10 5 10-5" />
144
+ <path d="M2 12l10 5 10-5" />
145
+ </svg>
146
+ </div>
147
+ <div>
148
+ <div
149
+ style={{
150
+ fontSize: '16px',
151
+ fontWeight: 600,
152
+ color: '#1f2937',
153
+ lineHeight: '20px',
154
+ }}
155
+ >
156
+ 教育AI助手
157
+ </div>
158
+ <div style={{ fontSize: '12px', color: '#9ca3af' }}>开发平台</div>
159
+ </div>
160
+ </div>
161
+
162
+ {/* 菜单 */}
163
+ <Menu
164
+ mode="inline"
165
+ selectedKeys={[currentSection]}
166
+ items={menuItems}
167
+ onClick={({ key }) => setCurrentSection(key)}
168
+ style={{
169
+ border: 'none',
170
+ marginTop: '16px',
171
+ padding: '0 12px',
172
+ }}
173
+ />
174
+
175
+ {/* 登出按钮 */}
176
+ <div
177
+ style={{
178
+ position: 'absolute',
179
+ bottom: 0,
180
+ width: '100%',
181
+ padding: '16px',
182
+ borderTop: '1px solid #f0f0f0',
183
+ }}
184
+ >
185
+ <div
186
+ style={{
187
+ padding: '12px 16px',
188
+ background: '#f9fafb',
189
+ borderRadius: '8px',
190
+ marginBottom: '12px',
191
+ }}
192
+ >
193
+ <div style={{ fontSize: '14px', fontWeight: 500, color: '#1f2937' }}>
194
+ {user?.name || '教师'}
195
+ </div>
196
+ <div style={{ fontSize: '12px', color: '#9ca3af', marginTop: '2px' }}>
197
+ {user?.email || user?.phone || ''}
198
+ </div>
199
+ </div>
200
+ <button
201
+ onClick={handleLogout}
202
+ style={{
203
+ width: '100%',
204
+ padding: '10px 16px',
205
+ background: 'transparent',
206
+ border: '1px solid #e5e7eb',
207
+ borderRadius: '8px',
208
+ color: '#6b7280',
209
+ fontSize: '14px',
210
+ fontWeight: 500,
211
+ cursor: 'pointer',
212
+ display: 'flex',
213
+ alignItems: 'center',
214
+ justifyContent: 'center',
215
+ transition: 'all 0.2s',
216
+ }}
217
+ onMouseEnter={(e) => {
218
+ e.target.style.borderColor = '#f43f5e';
219
+ e.target.style.color = '#f43f5e';
220
+ }}
221
+ onMouseLeave={(e) => {
222
+ e.target.style.borderColor = '#e5e7eb';
223
+ e.target.style.color = '#6b7280';
224
+ }}
225
+ >
226
+ <LogoutOutlined style={{ marginRight: '8px' }} />
227
+ 退出登录
228
+ </button>
229
+ </div>
230
+ </Sider>
231
+
232
+ {/* 主内容区域 */}
233
+ <Layout style={{ marginLeft: 260, backgroundColor: '#fafbfc' }}>
234
+ <Content style={{ padding: '24px', minHeight: '100vh' }}>
235
+ {renderContent()}
236
+ </Content>
237
+ </Layout>
238
+ </Layout>
239
+ );
240
+ };
241
+
242
+ export default TeacherDashboard;
frontend/src/services/agentService.js ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Agent服务 - 对应后端 /api/agent 接口
3
+ * 复用原始 index.html 中的 Agent 相关逻辑
4
+ */
5
+ import api from './api';
6
+
7
+ const agentService = {
8
+ // 获取Agent列表
9
+ getAgents: async () => {
10
+ return api.get('/agent/list');
11
+ },
12
+
13
+ // 获取Agent详情
14
+ getAgentDetail: async (agentId) => {
15
+ return api.get(`/agent/${agentId}`);
16
+ },
17
+
18
+ // 创建Agent
19
+ createAgent: async (agentData) => {
20
+ return api.post('/agent/create', agentData);
21
+ },
22
+
23
+ // 更新Agent
24
+ updateAgent: async (agentId, agentData) => {
25
+ return api.put(`/agent/${agentId}`, agentData);
26
+ },
27
+
28
+ // 删除Agent
29
+ deleteAgent: async (agentId) => {
30
+ return api.delete(`/agent/${agentId}`);
31
+ },
32
+
33
+ // AI辅助生成工作流
34
+ generateAIWorkflow: async (agentData) => {
35
+ return api.post('/agent/ai-assist', agentData);
36
+ },
37
+
38
+ // 创建分发链接
39
+ createDistribution: async (agentId, expiresIn) => {
40
+ return api.post(`/agent/${agentId}/distribute`, { expires_in: expiresIn });
41
+ },
42
+
43
+ // 获取分发链接列表
44
+ getDistributions: async (agentId) => {
45
+ return api.get(`/agent/${agentId}/distributions`);
46
+ },
47
+ };
48
+
49
+ export default agentService;
frontend/src/services/api.js ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios from 'axios';
2
+ import { message } from 'antd';
3
+
4
+ // 创建axios实例
5
+ const api = axios.create({
6
+ baseURL: '/api',
7
+ timeout: 30000,
8
+ headers: {
9
+ 'Content-Type': 'application/json',
10
+ },
11
+ });
12
+
13
+ // 请求拦截器
14
+ api.interceptors.request.use(
15
+ (config) => {
16
+ const token = localStorage.getItem('token');
17
+ if (token) {
18
+ config.headers.Authorization = `Bearer ${token}`;
19
+ }
20
+ return config;
21
+ },
22
+ (error) => {
23
+ return Promise.reject(error);
24
+ }
25
+ );
26
+
27
+ // 响应拦截器
28
+ api.interceptors.response.use(
29
+ (response) => {
30
+ return response.data;
31
+ },
32
+ (error) => {
33
+ if (error.response) {
34
+ const { status, data } = error.response;
35
+
36
+ switch (status) {
37
+ case 401:
38
+ // Token过期或无效
39
+ localStorage.removeItem('token');
40
+ localStorage.removeItem('user');
41
+ window.location.href = '/login';
42
+ message.error('登录已过期,请重新登录');
43
+ break;
44
+ case 403:
45
+ message.error('没有权限访问该资源');
46
+ break;
47
+ case 404:
48
+ message.error('请求的资源不存在');
49
+ break;
50
+ case 422:
51
+ message.error(data.message || '数据验证失败');
52
+ break;
53
+ case 500:
54
+ message.error('服务器错误,请稍后重试');
55
+ break;
56
+ default:
57
+ message.error(data.message || '请求失败');
58
+ }
59
+ } else if (error.request) {
60
+ message.error('网络错误,请检查网络连接');
61
+ } else {
62
+ message.error('请求配置错误');
63
+ }
64
+
65
+ return Promise.reject(error);
66
+ }
67
+ );
68
+
69
+ export default api;
frontend/src/services/authService.js ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import api from './api';
2
+
3
+ const authService = {
4
+ // 密码登录
5
+ loginWithPassword: async (username, password) => {
6
+ return api.post('/auth/login-password', { username, password });
7
+ },
8
+
9
+ // 邮箱验证码登录
10
+ loginWithEmailCode: async (email, emailCode, role = 'student') => {
11
+ return api.post('/auth/login-email', { email, code: emailCode, role });
12
+ },
13
+
14
+ // 手机验证码登录
15
+ loginWithPhoneCode: async (phone, phoneCode, role = 'student') => {
16
+ return api.post('/auth/login-phone', { phone, code: phoneCode, role });
17
+ },
18
+
19
+ // 发送邮箱验证码
20
+ sendEmailCode: async (email) => {
21
+ return api.post('/auth/send-email-code', { email });
22
+ },
23
+
24
+ // 发送手机验证码
25
+ sendPhoneCode: async (phone) => {
26
+ return api.post('/auth/send-phone-code', { phone });
27
+ },
28
+
29
+ // 注册
30
+ register: async (data) => {
31
+ return api.post('/auth/register', data);
32
+ },
33
+
34
+ // 登出
35
+ logout: async () => {
36
+ return api.post('/auth/logout');
37
+ },
38
+
39
+ // 刷新token
40
+ refreshToken: async () => {
41
+ return api.post('/auth/refresh');
42
+ },
43
+
44
+ // 获取当前用户信息
45
+ getCurrentUser: async () => {
46
+ return api.get('/auth/me');
47
+ },
48
+
49
+ // 修改密码
50
+ changePassword: async (oldPassword, newPassword) => {
51
+ return api.post('/auth/change_password', { oldPassword, newPassword });
52
+ },
53
+
54
+ // 重置密码
55
+ resetPassword: async (token, newPassword) => {
56
+ return api.post('/auth/reset_password', { token, newPassword });
57
+ },
58
+ };
59
+
60
+ export default authService;
frontend/src/services/knowledgeService.js ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 知识库服务 - 对应后端 /api/knowledge 接口
3
+ * 复用原始 index.html 中的知识库相关逻辑
4
+ */
5
+ import api from './api';
6
+
7
+ const knowledgeService = {
8
+ // 获取知识库列表
9
+ getKnowledgeBases: async () => {
10
+ return api.get('/knowledge/');
11
+ },
12
+
13
+ // 创建知识库
14
+ createKnowledgeBase: async (formData) => {
15
+ return api.post('/knowledge/', formData, {
16
+ headers: {
17
+ 'Content-Type': 'multipart/form-data',
18
+ },
19
+ });
20
+ },
21
+
22
+ // 添加文件到知识库
23
+ addFileToKnowledgeBase: async (knowledgeId, formData) => {
24
+ return api.post(`/knowledge/${knowledgeId}/documents`, formData, {
25
+ headers: {
26
+ 'Content-Type': 'multipart/form-data',
27
+ },
28
+ });
29
+ },
30
+
31
+ // 删除知识库
32
+ deleteKnowledgeBase: async (knowledgeId) => {
33
+ return api.delete(`/knowledge/${knowledgeId}`);
34
+ },
35
+
36
+ // 删除知识库中的文件
37
+ deleteFile: async (indexId, fileName) => {
38
+ return api.delete(`/knowledge/${indexId}/documents/${fileName}`);
39
+ },
40
+
41
+ // 获取文件处理进度(端点在 app.py 主路由,不在 knowledge blueprint)
42
+ getProgress: async (taskId) => {
43
+ return api.get(`/progress/${taskId}`);
44
+ },
45
+ };
46
+
47
+ export default knowledgeService;
frontend/src/store/slices/agentSlice.js ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createSlice } from '@reduxjs/toolkit';
2
+
3
+ const initialState = {
4
+ agents: [],
5
+ currentAgent: null,
6
+ isLoading: false,
7
+ error: null,
8
+ };
9
+
10
+ const agentSlice = createSlice({
11
+ name: 'agent',
12
+ initialState,
13
+ reducers: {
14
+ setAgents: (state, action) => {
15
+ state.agents = action.payload;
16
+ state.error = null;
17
+ },
18
+ setCurrentAgent: (state, action) => {
19
+ state.currentAgent = action.payload;
20
+ },
21
+ addAgent: (state, action) => {
22
+ state.agents.push(action.payload);
23
+ },
24
+ updateAgent: (state, action) => {
25
+ const index = state.agents.findIndex(a => a.id === action.payload.id);
26
+ if (index !== -1) {
27
+ state.agents[index] = action.payload;
28
+ }
29
+ if (state.currentAgent?.id === action.payload.id) {
30
+ state.currentAgent = action.payload;
31
+ }
32
+ },
33
+ deleteAgent: (state, action) => {
34
+ state.agents = state.agents.filter(a => a.id !== action.payload);
35
+ if (state.currentAgent?.id === action.payload) {
36
+ state.currentAgent = null;
37
+ }
38
+ },
39
+ setLoading: (state, action) => {
40
+ state.isLoading = action.payload;
41
+ },
42
+ setError: (state, action) => {
43
+ state.error = action.payload;
44
+ state.isLoading = false;
45
+ }
46
+ },
47
+ });
48
+
49
+ export const {
50
+ setAgents,
51
+ setCurrentAgent,
52
+ addAgent,
53
+ updateAgent,
54
+ deleteAgent,
55
+ setLoading,
56
+ setError,
57
+ } = agentSlice.actions;
58
+
59
+ export default agentSlice.reducer;
frontend/src/store/slices/authSlice.js ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createSlice } from '@reduxjs/toolkit';
2
+
3
+ const initialState = {
4
+ user: JSON.parse(localStorage.getItem('user')) || null,
5
+ token: localStorage.getItem('token') || null,
6
+ isLoading: false,
7
+ error: null,
8
+ isAuthenticated: !!localStorage.getItem('token'),
9
+ };
10
+
11
+ const authSlice = createSlice({
12
+ name: 'auth',
13
+ initialState,
14
+ reducers: {
15
+ loginStart: (state) => {
16
+ state.isLoading = true;
17
+ state.error = null;
18
+ },
19
+ loginSuccess: (state, action) => {
20
+ state.isLoading = false;
21
+ state.isAuthenticated = true;
22
+ state.user = action.payload.user;
23
+ state.token = action.payload.token;
24
+ state.error = null;
25
+
26
+ // 保存到localStorage
27
+ localStorage.setItem('user', JSON.stringify(action.payload.user));
28
+ localStorage.setItem('token', action.payload.token);
29
+ },
30
+ loginFailure: (state, action) => {
31
+ state.isLoading = false;
32
+ state.isAuthenticated = false;
33
+ state.user = null;
34
+ state.token = null;
35
+ state.error = action.payload;
36
+ },
37
+ logout: (state) => {
38
+ state.user = null;
39
+ state.token = null;
40
+ state.isAuthenticated = false;
41
+ state.error = null;
42
+
43
+ // 清除localStorage
44
+ localStorage.removeItem('user');
45
+ localStorage.removeItem('token');
46
+ },
47
+ clearError: (state) => {
48
+ state.error = null;
49
+ },
50
+ updateUser: (state, action) => {
51
+ state.user = { ...state.user, ...action.payload };
52
+ localStorage.setItem('user', JSON.stringify(state.user));
53
+ }
54
+ },
55
+ });
56
+
57
+ export const {
58
+ loginStart,
59
+ loginSuccess,
60
+ loginFailure,
61
+ logout,
62
+ clearError,
63
+ updateUser,
64
+ } = authSlice.actions;
65
+
66
+ export default authSlice.reducer;
67
+
68
+ // Selectors
69
+ export const selectUser = (state) => state.auth.user;
70
+ export const selectIsAuthenticated = (state) => state.auth.isAuthenticated;
71
+ export const selectAuthLoading = (state) => state.auth.isLoading;
72
+ export const selectAuthError = (state) => state.auth.error;
73
+ export const selectUserRole = (state) => state.auth.user?.role;
frontend/src/store/slices/chatSlice.js ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createSlice } from '@reduxjs/toolkit';
2
+
3
+ const initialState = {
4
+ messages: [],
5
+ isStreaming: false,
6
+ currentStreamMessage: '',
7
+ executionContext: null,
8
+ isExecuting: false,
9
+ };
10
+
11
+ const chatSlice = createSlice({
12
+ name: 'chat',
13
+ initialState,
14
+ reducers: {
15
+ addMessage: (state, action) => {
16
+ state.messages.push(action.payload);
17
+ },
18
+ updateMessage: (state, action) => {
19
+ const { id, content } = action.payload;
20
+ const message = state.messages.find(m => m.id === id);
21
+ if (message) {
22
+ message.content = content;
23
+ }
24
+ },
25
+ clearMessages: (state) => {
26
+ state.messages = [];
27
+ state.currentStreamMessage = '';
28
+ },
29
+ setStreaming: (state, action) => {
30
+ state.isStreaming = action.payload;
31
+ },
32
+ setStreamMessage: (state, action) => {
33
+ state.currentStreamMessage = action.payload;
34
+ },
35
+ appendStreamMessage: (state, action) => {
36
+ state.currentStreamMessage += action.payload;
37
+ },
38
+ setExecutionContext: (state, action) => {
39
+ state.executionContext = action.payload;
40
+ },
41
+ setExecuting: (state, action) => {
42
+ state.isExecuting = action.payload;
43
+ }
44
+ },
45
+ });
46
+
47
+ export const {
48
+ addMessage,
49
+ updateMessage,
50
+ clearMessages,
51
+ setStreaming,
52
+ setStreamMessage,
53
+ appendStreamMessage,
54
+ setExecutionContext,
55
+ setExecuting,
56
+ } = chatSlice.actions;
57
+
58
+ export default chatSlice.reducer;
frontend/src/store/slices/uiSlice.js ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createSlice } from '@reduxjs/toolkit';
2
+
3
+ const initialState = {
4
+ sidebarOpen: true,
5
+ theme: 'light',
6
+ activeModal: null,
7
+ notifications: [],
8
+ };
9
+
10
+ const uiSlice = createSlice({
11
+ name: 'ui',
12
+ initialState,
13
+ reducers: {
14
+ toggleSidebar: (state) => {
15
+ state.sidebarOpen = !state.sidebarOpen;
16
+ },
17
+ setSidebarOpen: (state, action) => {
18
+ state.sidebarOpen = action.payload;
19
+ },
20
+ setTheme: (state, action) => {
21
+ state.theme = action.payload;
22
+ },
23
+ openModal: (state, action) => {
24
+ state.activeModal = action.payload;
25
+ },
26
+ closeModal: (state) => {
27
+ state.activeModal = null;
28
+ },
29
+ addNotification: (state, action) => {
30
+ state.notifications.push({
31
+ id: Date.now(),
32
+ ...action.payload,
33
+ });
34
+ },
35
+ removeNotification: (state, action) => {
36
+ state.notifications = state.notifications.filter(
37
+ n => n.id !== action.payload
38
+ );
39
+ },
40
+ clearNotifications: (state) => {
41
+ state.notifications = [];
42
+ }
43
+ },
44
+ });
45
+
46
+ export const {
47
+ toggleSidebar,
48
+ setSidebarOpen,
49
+ setTheme,
50
+ openModal,
51
+ closeModal,
52
+ addNotification,
53
+ removeNotification,
54
+ clearNotifications,
55
+ } = uiSlice.actions;
56
+
57
+ export default uiSlice.reducer;