diff --git a/.gitignore b/.gitignore index e9f1919..1e74878 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ frontend/dist/ # runtime config/runtime_state.yaml +config/frontend_settings.yaml logs/ backups/ output/ diff --git "a/StudyAgent_\345\211\215\347\253\257\345\217\214\347\211\210\346\234\254\345\212\237\350\203\275\351\234\200\346\261\202\345\237\272\347\272\277_PRD.md" "b/StudyAgent_\345\211\215\347\253\257\345\217\214\347\211\210\346\234\254\345\212\237\350\203\275\351\234\200\346\261\202\345\237\272\347\272\277_PRD.md" new file mode 100644 index 0000000..b725a8d --- /dev/null +++ "b/StudyAgent_\345\211\215\347\253\257\345\217\214\347\211\210\346\234\254\345\212\237\350\203\275\351\234\200\346\261\202\345\237\272\347\272\277_PRD.md" @@ -0,0 +1,1925 @@ +# Study Agent 前端双版本功能需求基线 + +> 文档类型:产品需求文档(PRD)/ 前端重构功能对齐基线 +> 适用仓库:`2002yy/study-agent` +> 文档版本:V1.0 +> 编制日期:2026-06-15 +> 对比范围:旧版 Streamlit 前端与新版 React + Vite + TypeScript 前端 +> 核心原则:**以旧版完整业务闭环作为功能基线,以新版前后端分离架构和工程化能力作为最终承载方式。** + +--- + +## 1. 文档目的 + +Study Agent 当前同时存在两套前端: + +1. **旧版前端:Streamlit** + - 入口:`app.py` + - 主要页面:`src/ui/` + - 特点:功能覆盖完整,已经形成单人学习、角色群聊、联网新闻、长期记忆、课后总结、本地知识库等业务闭环。 + - 问题:页面状态与 Python 业务逻辑耦合较深,不利于复杂交互、移动端适配、独立部署和长期维护。 + +2. **新版前端:React + Vite + TypeScript** + - 入口:`frontend/src/App.tsx` + - API:`src/api.py` + - 特点:已实现前后端分离、三栏工作台、RAG 上传与来源展示、工具预览/调用、工作流审计和 API 鉴权基础。 + - 问题:当前主要覆盖工程控制台场景,尚未恢复旧版的完整学习产品能力。 + +本需求文档用于: + +- 固化两套前端已经存在的全部主要功能; +- 明确新版相对旧版的功能缺口; +- 定义最终统一前端必须保留、补齐和升级的功能; +- 为后续接口设计、开发拆分、回归测试和验收提供统一依据; +- 防止重构过程中出现“页面更漂亮,但业务能力倒退”的情况。 + +--- + +## 2. 版本定义与目标形态 + +### 2.1 版本 A:旧版 Streamlit 完整业务前端 + +版本 A 是当前业务功能参考基线,主要代码包括: + +- `app.py` +- `src/ui/sidebar.py` +- `src/ui/status_bar.py` +- `src/ui/chat_panel.py` +- `src/ui/wechat_panel.py` +- `src/ui/wechat_news_panel.py` +- `src/ui/rag_panel.py` +- `src/ui/after_session_panel.py` + +版本 A 的核心价值是:**证明 Study Agent 已经不只是聊天页面,而是一个具有学习状态、角色互动、记忆沉淀和资料检索能力的完整应用。** + +### 2.2 版本 B:新版 React 工程控制台 + +版本 B 是最终技术架构方向,主要代码包括: + +- `frontend/src/App.tsx` +- `frontend/src/api.ts` +- `frontend/src/types.ts` +- `frontend/src/styles.css` +- `src/api.py` + +版本 B 当前已经实现: + +- React + Vite + TypeScript; +- FastAPI 前后端分离; +- API Token 请求头; +- RAG 文档上传与索引; +- 非流式单人聊天; +- RAG 引用来源表; +- 工具预览与正式调用; +- 工作流列表与事件详情; +- 基础 API、知识库和错误状态显示。 + +### 2.3 最终目标:统一 React 产品前端 + +最终版本不得简单复制旧版页面,也不得只保留新版控制台能力,而应满足: + +- **业务功能不低于 Streamlit 版本;** +- **工程能力不低于 React 版本;** +- 所有业务通过稳定 API 接入,避免 React 直接依赖 Python 内部实现; +- 保留工具调用、来源引用、工作流审计等新版优势; +- 恢复角色、模式、微信群、新闻、课后总结、长期记忆等旧版核心能力; +- Streamlit 在功能对齐完成前继续作为回归参考,不应提前删除。 + +--- + +## 3. 用户与使用场景 + +### 3.1 主要用户 + +#### U1:学习者 + +希望使用 AI 完成: + +- 概念学习; +- 苏格拉底式追问; +- 费曼式复述; +- 项目推进; +- 论文分析; +- 知识结构整理; +- 本地资料问答; +- 学习进度沉淀; +- 多角色讨论。 + +#### U2:项目维护者/开发者 + +希望检查: + +- 模型路由结果; +- RAG 检索结果; +- 引用来源; +- 工具调用; +- 工作流事件; +- 调用耗时; +- 模型调用次数和估算成本; +- 安全模式与写入状态; +- API、索引和 Provider 健康状态。 + +### 3.2 核心使用闭环 + +```text +启动系统 + → 检查配置、API、索引和记忆状态 + → 选择单人聊天或微信群 + → 选择角色、学习模式、模型、性能模式和互动氛围 + → 提问或上传本地资料 + → 获得流式回答、来源引用和路由说明 + → 继续讨论或发起多角色群聊 + → 生成课后总结和长期记忆候选 + → 用户预览、选择、确认写入 + → 保存会话日志 + → 下次从当前重点和历史进度继续 +``` + +--- + +# 4. 版本 A:Streamlit 前端完整功能需求 + +## 4.1 应用启动与全局状态 + +### FR-A-001 页面初始化 + +系统启动时应: + +- 设置页面标题和宽屏布局; +- 注入统一主题; +- 初始化会话状态; +- 加载运行模式; +- 读取长期记忆; +- 执行轻量健康检查; +- 显示配置缺失警告; +- 根据聊天入口进入单人聊天或微信群。 + +### FR-A-002 配置检查 + +当 API Key、模型、Provider 或必要配置不完整时: + +- 页面不得直接崩溃; +- 应在顶部显示逐项警告; +- 不影响可用的本地功能; +- 用户执行强制刷新后应重新读取配置。 + +### FR-A-003 角色人设查看 + +当用户手动选择具体角色时: + +- 应提供“查看角色人设”入口; +- 展示角色提示词摘要; +- 自动角色模式下不强制显示固定角色人设。 + +### FR-A-004 全局状态看板 + +页面应显示: + +- 当前入口; +- 当前角色或自动路由后的角色; +- 当前学习模式; +- 当前模型; +- 当前性能模式; +- 当前互动氛围; +- 当前版本; +- 当前学习重点; +- 微信群状态和未读数量。 + +--- + +## 4.2 侧栏设置与运行控制 + +### FR-A-010 聊天入口选择 + +用户可在以下入口间切换: + +- 单人主讲; +- 微信群。 + +切换后应刷新主内容区,但不得丢失已持久化数据。 + +### FR-A-011 角色选择 + +可选项包括: + +- 自动; +- 三月七; +- 刻晴; +- 纳西妲; +- 流萤。 + +“自动”表示由路由器根据用户输入选择角色。 + +### FR-A-012 学习模式选择 + +可选项包括: + +- 自动; +- 普通; +- 苏格拉底; +- 费曼; +- 项目; +- 论文; +- 概念地图。 + +### FR-A-013 模型档位选择 + +可选项包括: + +- 自动; +- Flash(快速); +- Pro(高质量)。 + +即使用户选择 Flash,后端仍可根据代码、论文、报错、架构等高风险任务自动升档。 + +### FR-A-014 性能模式选择 + +可选项包括: + +- 快速; +- 标准; +- 深度。 + +性能模式应影响: + +- 上下文读取范围; +- 最大输出 token; +- 新闻正文读取数量; +- 模型调用预算; +- 部分缓存和日志策略。 + +### FR-A-015 互动氛围选择 + +可选项包括: + +- 自然; +- 温和; +- 贴近。 + +互动氛围应影响: + +- 单人聊天措辞; +- 微信群角色互动; +- 群聊开场; +- 群聊反馈; +- 长期关系状态。 + +### FR-A-016 设置批量应用 + +设置应通过统一“应用设置”按钮提交: + +- 仅在值变化时更新运行状态; +- 影响路由的设置变化后应清空旧路由结果; +- 性能模式变化后应刷新记忆上下文缓存; +- 互动氛围变化后应写入会话日志。 + +### FR-A-017 调试模式 + +启用后应显示: + +- 入口模式; +- 性能模式; +- 记忆模式; +- 路由模式; +- 微信群模式; +- 互动氛围; +- 路由耗时; +- 记忆读取耗时; +- 上下文构建耗时; +- 首 token 时间; +- 模型总耗时; +- UI 渲染耗时; +- 总耗时。 + +### FR-A-018 安全模式 + +启用安全模式后: + +- 禁止长期记忆写入; +- 禁止群聊记忆候选正式提交; +- 禁止课后总结正式提交; +- 仍允许预览和只读操作; +- 页面应明确显示阻止原因。 + +### FR-A-019 微信群记忆提取开关 + +用户可独立启用或关闭微信群记忆候选提取,关闭后不显示自动提取入口。 + +### FR-A-020 健康检查和强制刷新 + +用户可: + +- 执行完整健康检查; +- 查看健康检查报告; +- 强制重新加载配置; +- 清空记忆缓存; +- 清空群聊页面缓存; +- 强制刷新健康状态。 + +--- + +## 4.3 单人聊天 + +### FR-A-030 欢迎区 + +无聊天记录时应显示: + +- 当前重点; +- 上次结束位置; +- 当前版本; +- 最近推进; +- 下一步建议; +- 角色视觉元素; +- 快捷提问入口。 + +### FR-A-031 快捷入口 + +至少支持: + +- 分析当前 Agent 架构; +- 继续上次工作; +- 生成课后更新预览。 + +快捷入口点击后应直接进入聊天处理流程。 + +### FR-A-032 聊天输入 + +应支持: + +- 多轮对话; +- Enter/发送操作; +- 空输入拦截; +- 聊天历史展示; +- 用户和角色头像区分。 + +### FR-A-033 流式输出 + +模型回答必须支持逐段流式展示,并记录: + +- 首 token 时间; +- 总生成时间; +- 调用模型; +- 输出 token 估算; +- 调用成本估算。 + +### FR-A-034 自动路由 + +每轮输入应根据当前设置和输入内容解析: + +- 实际角色; +- 实际学习模式; +- 实际模型; +- 是否人工覆盖; +- 路由原因; +- 命中关键词; +- 是否调用 LLM 路由器。 + +### FR-A-035 路由锁定 + +系统内部支持锁定角色、模式和模型时: + +- 本轮应严格使用锁定结果; +- 页面应标识为人工/锁定覆盖; +- 不再执行自动路由。 + +### FR-A-036 长期记忆注入 + +构建提示词时应根据上下文模式读取: + +- 学习者画像; +- 当前重点; +- 学习进度; +- 核心摘要; +- 修订记录; +- 会话归档; +- 关系状态。 + +### FR-A-037 RAG 聊天注入 + +启用“用于聊天回答”后: + +- 用户问题应先检索本地资料; +- 检索结果应注入提示词; +- 回答后显示引用数量; +- 显示未找到、索引缺失或检索异常状态; +- 可展开查看引用来源。 + +### FR-A-038 本轮路由详情 + +回答完成后可展开查看: + +- 角色; +- 模式; +- 模型; +- 是否人工覆盖; +- 路由原因; +- RAG 状态; +- RAG 引用数量; +- RAG 来源。 + +### FR-A-039 会话日志 + +每轮成功回复后应记录: + +- 会话 ID; +- 用户输入; +- 模型回答; +- 角色; +- 模式; +- 模型; +- 路由信息; +- 是否使用记忆; +- RAG 状态; +- 性能模式和调试状态。 + +### FR-A-040 模型统计 + +页面应显示: + +- Flash 调用次数; +- Pro 调用次数; +- 路由模型调用次数; +- 上次延迟; +- 估算成本; +- 重置统计入口。 + +--- + +## 4.4 微信群聊 + +### FR-A-050 群聊状态 + +应显示: + +- 群聊模式; +- 未读数量; +- 当前互动氛围; +- 当前视图是未读还是完整线程。 + +### FR-A-051 群聊开场 + +群聊尚未开始时: + +- 用户可选择开场氛围; +- 可生成角色之间的群聊开场; +- 开场生成前角色不知道用户即将加入; +- 开场应受当前角色、模型和性能模式影响。 + +### FR-A-052 群聊基础操作 + +支持: + +- 刷新群聊; +- 标记已读; +- 新建群聊; +- 查看未读; +- 清空未读; +- 保存完整群聊线程。 + +### FR-A-053 用户加入群聊 + +用户可在群里发送消息: + +- 用户消息先写入线程; +- 角色回复采用流式生成; +- 页面显示“正在输入”; +- 回复完成后追加到群聊记录; +- 首次加入状态应被记录。 + +### FR-A-054 多角色气泡 + +群聊消息应区分: + +- 三月七; +- 刻晴; +- 纳西妲; +- 流萤; +- 用户。 + +不同角色应拥有独立名称、视觉样式和人设。 + +### FR-A-055 群聊 RAG + +启用 RAG 后,群聊中的用户问题应支持: + +- 本地知识检索; +- 引用数量提示; +- 引用状态提示; +- 引用来源展开查看; +- 检索上下文注入多角色回复。 + +### FR-A-056 群聊搜索 + +用户可输入关键词搜索群聊记录,并展示最多若干条命中消息。 + +### FR-A-057 群聊摘要 + +用户可生成当前群聊的简短摘要,用于快速回顾。 + +### FR-A-058 群聊记忆自动提取 + +启用后可从群聊中提取: + +- 核心摘要候选; +- 学习进度候选; +- 当前重点候选; +- 学习者画像候选; +- 修订笔记候选; +- 会话归档候选。 + +### FR-A-059 群聊记忆逐条确认 + +每个候选必须: + +- 展示候选类型和内容; +- 支持逐条确认; +- 在安全模式或记忆锁定时禁止写入; +- 写入后显示目标文件。 + +### FR-A-060 手动引用到记忆候选 + +用户可将某条角色消息加入记忆候选: + +- 已加入项目应有状态标识; +- 不允许重复加入; +- 页面应显示角色和内容摘要。 + +--- + +## 4.5 联网搜索与新闻群聊 + +### FR-A-070 自定义联网搜索 + +用户可输入任意主题,如: + +- 技术新闻; +- 软件版本; +- 硬件产品; +- 学习概念; +- 项目相关信息。 + +可选择是否尝试读取正文。 + +### FR-A-071 最近新闻快捷入口 + +用户可一键搜索最近一天的新闻并进入处理流程。 + +### FR-A-072 分阶段新闻流程 + +新闻处理必须按状态机执行: + +1. 搜索结果获取; +2. 正文读取; +3. 摘要生成; +4. 群聊讨论生成。 + +每个阶段应显示完成或等待状态。 + +### FR-A-073 搜索结果展示 + +搜索结果应显示: + +- 序号; +- 标题; +- 来源; +- 发布时间; +- 是否成功读取正文。 + +### FR-A-074 正文读取 + +当用户启用正文读取时: + +- 读取数量受性能模式控制; +- 读取失败不得导致整个流程崩溃; +- 应保留仅标题和来源的降级结果。 + +### FR-A-075 新闻摘要 + +摘要应展示: + +- 综合摘要; +- 来源列表; +- 正文覆盖率; +- 警告信息; +- 可选耗时信息。 + +### FR-A-076 新闻群聊讨论 + +摘要完成后,用户可生成多角色讨论: + +- 讨论必须基于新闻摘要和来源; +- 讨论结果进入微信群完整线程; +- 会话日志记录主题和来源; +- 页面提示已围绕该主题拉起讨论。 + +--- + +## 4.6 本地知识库与 RAG + +### FR-A-080 支持文件类型 + +支持导入: + +- Markdown; +- TXT; +- DOCX; +- PDF。 + +### FR-A-081 文件导入方式 + +支持: + +- 浏览器多文件上传; +- 输入本地文件绝对路径; +- 同时导入上传文件和路径文件。 + +### FR-A-082 文件名安全处理 + +上传文件名必须经过清洗,防止: + +- 路径穿越; +- 非法字符; +- 空文件名; +- 覆盖同名文件。 + +### FR-A-083 建立索引 + +用户点击建立索引后应: + +- 加载文档; +- 切分 chunk; +- 生成索引; +- 返回文档数和 chunk 数; +- 更新向量后端状态; +- 显示成功或错误信息。 + +### FR-A-084 索引概览 + +应展示: + +- 索引路径; +- 是否存在; +- 文档数; +- chunk 数; +- 文档标题; +- 文件类型; +- 源路径; +- 文件大小; +- 内容哈希摘要; +- 每文档 chunk 数。 + +### FR-A-085 Chunk 预览 + +应展示: + +- 文档标题; +- 行号范围; +- 字符数; +- 源路径; +- 内容摘要。 + +### FR-A-086 检索模式 + +支持: + +- 关键词检索; +- 混合检索; +- 本地向量检索; +- 向量后端检索。 + +### FR-A-087 检索参数 + +允许设置: + +- query; +- top_k; +- 最低分; +- 聊天引用数; +- 是否显示调试; +- 检索模式。 + +### FR-A-088 检索结果 + +每条结果显示: + +- 排名; +- 标题; +- 行号; +- 分数; +- 命中词; +- 源路径; +- chunk 正文。 + +### FR-A-089 来源与上下文 + +应分别展示: + +- 格式化来源列表; +- 注入模型的引用上下文; +- 检索调试数据。 + +### FR-A-090 检索调试 + +调试信息应包含: + +- query terms; +- retrieval mode; +- top_k; +- min_score; +- candidate count; +- returned count; +- lexical score; +- vector score; +- combined score; +- backend score; +- 权重信息。 + +### FR-A-091 清空检索结果 + +用户可清空: + +- 结果列表; +- 引用上下文; +- 来源块; +- 索引摘要缓存; +- 调试信息。 + +--- + +## 4.7 课后总结与长期记忆 + +### FR-A-100 生成课后更新 + +用户可基于当前对话生成: + +- 学习进度更新; +- 学习者画像更新; +- 当前重点更新; +- 修订笔记; +- 本轮归档; +- 角色动态观察。 + +### FR-A-101 质量检查 + +生成结果应执行质量检查,并展示问题数量和具体警告。 + +### FR-A-102 分项预览与选择 + +用户可逐项: + +- 勾选是否写入; +- 展开查看完整内容; +- 取消不需要的更新。 + +### FR-A-103 当前重点差异对比 + +`current_focus` 更新必须展示旧内容与新内容的对比。 + +### FR-A-104 角色动态记录 + +角色动态观察应: + +- 按角色展示; +- 忽略“本轮无需更新”; +- 写入成功后提示已更新角色。 + +### FR-A-105 写入保护 + +正式写入前必须检查: + +- safe_mode; +- memory_mode; +- 用户是否明确点击确认; +- 更新内容是否为空或解析失败。 + +### FR-A-106 写入策略 + +- `current_focus` 使用覆盖写; +- 其他记忆使用追加写; +- 学习者画像支持“待确认观察”标记; +- 写入失败应逐项报告。 + +### FR-A-107 写入验证 + +写入成功后应重新读取并展示: + +- 当前重点; +- 学习进度; +- 核心摘要。 + +### FR-A-108 生成微信群反馈 + +长期记忆写入成功后,用户可选择: + +- 简短; +- 标准; +- 稍微有温度。 + +随后生成微信群反馈并写入未读/群聊记录。 + +--- + +## 4.8 会话、日志和未读管理 + +### FR-A-110 结束本轮 + +用户点击结束本轮后应: + +- 保存日志; +- 清空当前消息; +- 重置课后更新; +- 重置写入确认状态; +- 重置临时群聊视图; +- 保留已经持久化的长期记忆。 + +### FR-A-111 未读消息 + +应支持: + +- 读取未读数量; +- 切换到未读视图; +- 标记已读; +- 清空未读; +- 将状态写入会话日志。 + +--- + +# 5. 版本 B:React 前端现有功能需求 + +## 5.1 应用框架 + +### FR-B-001 三栏工作台 + +页面由以下区域构成: + +- 左侧导航和系统状态; +- 中间单人聊天; +- 右侧引用、工作流、工具和记忆检查器。 + +### FR-B-002 API 快照加载 + +页面启动时并行加载: + +- `/health`; +- `/rag/status`; +- `/tools`; +- `/workflows/runs`。 + +任何接口失败时应展示统一 API 错误状态。 + +### FR-B-003 API Token + +前端支持通过环境变量配置 API Token,并通过: + +- `X-Study-Agent-Token` + +发送给 FastAPI。 + +### FR-B-004 CORS 和鉴权反馈 + +除 `/health` 外的接口遇到 401、403 或 CORS 错误时: + +- 页面应显示可理解的错误; +- 不得只显示空白; +- 不得把 Token 明文写入页面日志。 + +--- + +## 5.2 React 单人聊天 + +### FR-B-010 非流式聊天 + +当前通过 `/chat` 完成单人聊天: + +- 支持多轮消息; +- 保存 session_id; +- 支持发送中状态; +- 支持请求失败回显; +- 当前固定使用自动角色、自动模式、自动模型和自然氛围。 + +### FR-B-011 RAG 开关 + +用户可选择是否在聊天中启用 RAG。 + +### FR-B-012 聊天来源联动 + +聊天成功后: + +- 保存最后一次 RAG 响应; +- 右侧引用面板自动显示来源; +- 刷新工作流和知识库状态。 + +--- + +## 5.3 React 文档上传与知识库状态 + +### FR-B-020 多文件上传 + +支持浏览器多文件选择,并调用 `/rag/upload`。 + +### FR-B-021 上传状态 + +应显示: + +- 正在索引的文件数量; +- 成功后的文档数; +- 成功后的 chunk 数; +- 上传失败原因。 + +### FR-B-022 知识库概览 + +左侧显示: + +- 文档数; +- chunk 数; +- 向量后端名称; +- API 服务状态。 + +### FR-B-023 手动来源检索 + +用户可基于当前输入或最近问题调用 `/rag/query`,默认: + +- hybrid; +- top_k=5。 + +--- + +## 5.4 React 引用来源 + +### FR-B-030 来源表 + +显示: + +- 排名; +- 标题; +- 行号; +- 分数; +- 命中词; +- 源路径。 + +### FR-B-031 数据降级 + +当 debug results 不存在时,应从普通 results 构建来源表。 + +### FR-B-032 空状态 + +无来源时应提示: + +- 启用聊天 RAG; +- 发送问题; +- 或点击检索按钮。 + +--- + +## 5.5 React 工作流 + +### FR-B-040 工作流列表 + +显示最近工作流: + +- workflow_name; +- run_id; +- elapsed_ms; +- 状态。 + +### FR-B-041 工作流详情 + +点击某个 run 后,调用 `/workflows/runs/{run_id}`,展示: + +- event_type; +- status; +- message; +- step_id; +- error; +- elapsed_ms。 + +--- + +## 5.6 React 工具调用 + +### FR-B-050 工具列表 + +从 `/tools` 获取允许调用的工具数量。 + +### FR-B-051 工具预览 + +调用 `/tools/retrieve_local_knowledge/preview`: + +- 不执行正式副作用; +- 显示状态、原因和结果摘要; +- 使用当前输入作为 query。 + +### FR-B-052 工具正式调用 + +仅在已有预览结果后允许正式调用: + +- 调用 `/tools/retrieve_local_knowledge/call`; +- 写入工作流审计; +- 刷新工作流; +- 自动打开对应 run 详情。 + +--- + +## 5.7 React 记忆状态 + +### FR-B-060 记忆安全提示 + +当前仅显示: + +- 记忆采用先预览再写入; +- 写入受运行模式保护。 + +当前尚未实现真实候选、预览、选择和确认写入交互。 + +--- + +# 6. 双版本功能对照矩阵 + +| 功能域 | Streamlit 旧版 | React 新版 | 最终要求 | 优先级 | +|---|---|---|---|---| +| 单人聊天 | 已实现,流式 | 已实现,非流式 | React 恢复流式 | P0 | +| 多轮会话 | 已实现 | 已实现,页面内状态 | 增加历史恢复 | P0 | +| 角色选择 | 已实现 | 固定 auto | 补齐 | P0 | +| 学习模式 | 已实现 | 固定 auto | 补齐 | P0 | +| 模型选择 | 已实现 | 固定 auto | 补齐 | P0 | +| 性能模式 | 已实现 | 无 UI | 补齐 | P0 | +| 互动氛围 | 已实现 | 固定 standard | 补齐 | P0 | +| 自动路由详情 | 已实现 | API 返回但未展示 | 展示完整路由 | P0 | +| 角色人设查看 | 已实现 | 未实现 | 补齐 | P1 | +| 当前重点/进度 | 已实现 | 未实现 | 首页恢复 | P0 | +| 快捷提问 | 已实现 | 未实现 | 补齐 | P1 | +| 模型统计/成本 | 已实现 | 未实现 | 补齐 | P1 | +| 性能调试 | 已实现 | 工作流局部替代 | 两者整合 | P1 | +| 微信群聊 | 已实现 | 未实现 | 完整迁移 | P0 | +| 群聊开场 | 已实现 | 未实现 | 完整迁移 | P0 | +| 群聊未读 | 已实现 | 未实现 | 完整迁移 | P0 | +| 群聊搜索/摘要 | 已实现 | 未实现 | 完整迁移 | P1 | +| 群聊 RAG | 已实现 | 未实现 | 完整迁移 | P0 | +| 联网搜索 | 已实现 | 未实现 | 完整迁移 | P0 | +| 新闻分阶段流程 | 已实现 | 未实现 | 完整迁移 | P0 | +| 文档上传 | 已实现 | 已实现 | 保留 React 实现 | P0 | +| 本地路径导入 | 已实现 | 未实现 | 本地桌面部署补齐 | P1 | +| 索引文档列表 | 已实现 | 仅统计 | 补齐列表 | P0 | +| Chunk 预览 | 已实现 | 未实现 | 补齐 | P1 | +| 检索模式切换 | 4 种可选 | 固定 hybrid | 补齐 | P0 | +| top_k/min_score | 可配置 | 固定 | 补齐 | P0 | +| 来源结果表 | 已实现 | 已实现且更清晰 | 保留 React | P0 | +| 引用上下文 | 已实现 | 未展示 | 补齐调试抽屉 | P1 | +| 分数 breakdown | 已实现 | 类型已支持但未展开 | 补齐 | P1 | +| 工具预览/调用 | 后端能力弱展示 | 已实现 | 保留 React | P0 | +| 工作流时间线 | 无独立完整面板 | 已实现 | 保留 React | P0 | +| 课后更新生成 | 已实现 | 未实现 | 完整迁移 | P0 | +| 记忆质量检查 | 已实现 | 未实现 | 完整迁移 | P0 | +| 记忆分项选择 | 已实现 | 未实现 | 完整迁移 | P0 | +| current_focus diff | 已实现 | 未实现 | 完整迁移 | P0 | +| 记忆预览接口 | 页面内逻辑 | 后端已提供 | React 接入 | P0 | +| 记忆提交接口 | 页面内逻辑 | 后端已提供 | React 接入 | P0 | +| 安全模式保护 | 已实现 | 仅文字提示 | 补齐真实状态 | P0 | +| 群聊记忆候选 | 已实现 | 未实现 | 完整迁移 | P0 | +| 手动引用到记忆 | 已实现 | 未实现 | 完整迁移 | P1 | +| 会话保存 | 已实现 | 后端已记录 | 补齐页面操作 | P0 | +| 会话列表 | 无完整页面 | API 已提供 | 新增历史页 | P0 | +| 健康检查 | 轻量+完整 | 基础 health | 补齐详细健康页 | P1 | +| API Token | 无独立前端 | 已实现 | 保留 | P0 | +| CORS | 同进程无需 | 后端已支持 | 保留 | P0 | +| 响应式布局 | Streamlit 自适应有限 | CSS 响应式基础 | 强化移动端 | P1 | +| 错误/加载状态 | 分散实现 | 基础实现 | 统一设计 | P0 | + +--- + +# 7. 最终 React 统一前端需求 + +## 7.1 产品信息架构 + +建议一级导航: + +1. **首页** +2. **单人学习** +3. **微信群** +4. **本地知识库** +5. **学习记忆** +6. **会话历史** +7. **工作流与工具** +8. **设置与健康** + +移动端使用底部导航或抽屉,不强制保持桌面三栏。 + +--- + +## 7.2 首页 + +### FR-T-001 学习状态首页 + +首页显示: + +- 当前重点; +- 最近推进; +- 下一步建议; +- 当前版本; +- 最近会话; +- 未读群聊; +- 知识库状态; +- API 和 Provider 状态; +- 快捷继续入口。 + +### FR-T-002 快捷动作 + +支持: + +- 继续上次学习; +- 新建单人对话; +- 查看群聊未读; +- 上传资料; +- 生成课后总结; +- 打开最近工作流。 + +--- + +## 7.3 单人学习页 + +### FR-T-010 完整设置 + +聊天页应可设置: + +- 角色; +- 学习模式; +- 模型档位; +- 性能模式; +- 互动氛围; +- RAG 开关; +- 检索模式; +- 引用数量; +- 路由锁定。 + +设置应默认从运行配置读取,并在刷新后保持。 + +### FR-T-011 流式接口 + +新增流式聊天接口,建议: + +```text +POST /chat/stream +Content-Type: text/event-stream +``` + +事件至少包括: + +- `route`; +- `rag`; +- `token`; +- `usage`; +- `done`; +- `error`。 + +### FR-T-012 回答检查器 + +每轮回答旁或右侧显示: + +- 实际角色; +- 实际模式; +- 实际模型; +- 路由原因; +- RAG 来源; +- 工具调用; +- token; +- 延迟; +- session_id。 + +### FR-T-013 会话恢复 + +刷新页面或重新进入时: + +- 可恢复当前 session; +- 可从历史会话继续; +- 不应只依赖 React 内存状态。 + +--- + +## 7.4 微信群页 + +### FR-T-020 微信群 API 化 + +需要新增或封装以下能力: + +- 获取群聊状态; +- 获取完整线程; +- 获取未读; +- 标记已读; +- 新建群聊; +- 生成开场; +- 发送用户消息; +- 流式生成角色回复; +- 搜索群聊; +- 生成群聊摘要; +- 提取记忆候选; +- 提交单条记忆候选。 + +### FR-T-021 群聊表现 + +应保留角色差异,并支持: + +- 多角色头像; +- 不同气泡; +- 正在输入; +- 流式追加; +- 未读标记; +- 来源引用; +- 记忆候选标记。 + +### FR-T-022 新闻讨论 + +新闻流程在 React 中继续采用显式阶段: + +- 搜索; +- 正文读取; +- 摘要; +- 群聊讨论。 + +不得把所有步骤隐藏成一个长时间无反馈的按钮。 + +--- + +## 7.5 本地知识库页 + +### FR-T-030 导入与索引 + +支持: + +- 多文件上传; +- 拖拽上传; +- 文件类型提示; +- 索引进度; +- 重名处理; +- 失败文件列表; +- 文档数和 chunk 数。 + +本地路径导入仅在本地桌面/可信环境开放,浏览器远程部署不得任意读取服务器路径。 + +### FR-T-031 文档管理 + +应显示: + +- 文档名; +- 类型; +- 大小; +- 导入时间; +- chunk 数; +- 索引状态; +- 源路径; +- 删除/重建操作。 + +删除和重建属于副作用操作,必须确认。 + +### FR-T-032 检索实验台 + +允许配置: + +- retrieval_mode; +- top_k; +- min_score; +- context_max_chars; +- query rewrite; +- weak score threshold。 + +应同时展示: + +- 结果表; +- chunk 内容; +- 来源; +- score breakdown; +- 注入上下文; +- 检索耗时。 + +--- + +## 7.6 学习记忆页 + +### FR-T-040 记忆概览 + +展示: + +- learner profile; +- summary; +- progress; +- current focus; +- revision notes; +- session archive; +- 关系状态; +- 角色动态。 + +默认只读,不应直接无确认编辑原文件。 + +### FR-T-041 课后总结预览 + +调用新的课后总结预览接口,返回: + +- 各目标文件更新; +- 质量警告; +- old/new diff; +- 角色动态; +- 建议写入策略。 + +### FR-T-042 选择与确认 + +用户可以: + +- 逐项勾选; +- 编辑候选内容; +- 查看目标文件; +- 查看追加或覆盖动作; +- 二次确认; +- 提交写入。 + +### FR-T-043 安全状态 + +页面必须从后端读取真实: + +- safe_mode; +- memory_mode; +- writable; +- 禁止原因。 + +不得只用静态文字表示“受到保护”。 + +--- + +## 7.7 会话历史页 + +### FR-T-050 会话列表 + +基于 `/sessions` 展示: + +- 当前/已归档; +- 文件名; +- 修改时间; +- 大小; +- 会话摘要; +- 角色; +- 模式; +- 消息数。 + +### FR-T-051 会话详情 + +新增安全的会话详情 API,用于: + +- 查看聊天记录; +- 查看路由; +- 查看来源; +- 查看课后总结; +- 继续该会话。 + +不得让前端直接访问任意本地文件路径。 + +### FR-T-052 结束与归档 + +用户可: + +- 结束当前会话; +- 强制 flush; +- 归档; +- 新建会话。 + +--- + +## 7.8 工作流与工具页 + +### FR-T-060 工作流审计 + +保留新版能力并扩展: + +- 列表筛选; +- 状态筛选; +- workflow_name 筛选; +- run 详情; +- step 详情; +- 输入摘要; +- 输出摘要; +- 错误; +- 耗时; +- trace_id。 + +### FR-T-061 工具调用策略 + +所有工具分类: + +- 只读:允许预览后直接调用; +- 写入:必须预览、确认、调用; +- 高风险:必须额外确认并显示影响范围。 + +### FR-T-062 工具参数可见 + +页面应显示: + +- 工具名称; +- 权限; +- 参数摘要; +- 调用原因; +- 预计影响; +- 调用结果; +- 对应工作流。 + +--- + +## 7.9 设置与健康页 + +### FR-T-070 运行设置 + +设置页管理: + +- Provider profile; +- 模型; +-角色; +- 学习模式; +- 性能模式; +- 上下文模式; +- 互动氛围; +- RAG 默认参数; +- safe_mode; +- debug_mode; +- 微信群记忆提取。 + +API Key 默认从服务器环境变量读取,不建议通过普通页面回显完整值。 + +### FR-T-071 健康状态 + +显示: + +- API; +- LLM Provider; +- RAG index; +- embedding provider; +- vector backend; +- memory directory; +- session logger; +- news source; +- workflow store; +- 配置错误。 + +### FR-T-072 调用统计 + +显示: + +- Flash/Pro 调用; +- 路由调用; +- token; +- 估算成本; +- 首 token; +- 模型耗时; +- RAG 耗时; +- 工具耗时; +- 错误率。 + +--- + +# 8. API 缺口与接口需求 + +## 8.1 当前已有接口 + +当前可直接复用: + +- `GET /health` +- `GET /rag/status` +- `POST /rag/upload` +- `POST /rag/index` +- `POST /rag/query` +- `POST /rag/local-knowledge` +- `POST /chat` +- `POST /memory/preview` +- `POST /memory/commit` +- `GET /sessions` +- `POST /sessions/{session_id}/flush` +- `GET /tools` +- `POST /tools/{tool_name}/preview` +- `POST /tools/{tool_name}/call` +- `GET /workflows/runs` +- `GET /workflows/runs/{run_id}` + +## 8.2 建议新增接口 + +### P0 + +- `POST /chat/stream` +- `GET /runtime/settings` +- `PATCH /runtime/settings` +- `GET /roles` +- `GET /roles/{role_id}` +- `POST /after-session/preview` +- `POST /after-session/commit` +- `GET /memory` +- `GET /wechat/state` +- `GET /wechat/thread` +- `GET /wechat/unread` +- `POST /wechat/read` +- `POST /wechat/reset` +- `POST /wechat/opening` +- `POST /wechat/messages` +- `POST /wechat/messages/stream` +- `POST /wechat/memory/preview` +- `POST /wechat/memory/commit` +- `POST /news/search` +- `POST /news/enrich` +- `POST /news/digest` +- `POST /news/discuss` + +### P1 + +- `GET /sessions/{session_id}` +- `POST /sessions/{session_id}/archive` +- `GET /stats` +- `POST /stats/reset` +- `GET /health/full` +- `GET /rag/documents` +- `DELETE /rag/documents/{document_id}` +- `POST /rag/rebuild` + +--- + +# 9. 状态管理要求 + +## 9.1 前端临时状态 + +可保存在 React 状态管理中的内容: + +- 当前输入; +- 面板展开状态; +- 当前选中的工作流; +- 上传进度; +- 未提交的候选编辑内容。 + +## 9.2 会话级状态 + +必须由后端或可恢复存储管理: + +- session_id; +- 消息历史; +- 角色/模式/模型选择; +- route; +- 群聊线程; +- 新闻阶段; +- 记忆候选; +- 当前课后总结; +- 工作流状态。 + +## 9.3 持久化状态 + +必须由后端持久化: + +- 长期记忆; +- runtime settings; +- 会话日志; +- 群聊线程; +- 未读消息; +- RAG index; +- 工作流审计; +- 模型统计。 + +--- + +# 10. 非功能需求 + +## 10.1 本地优先与隐私 + +- 本地文件默认只在本地处理; +- 页面明确提示哪些内容会发送给外部模型; +- API Token 不写入浏览器日志; +- 不通过前端暴露服务器任意文件读取能力; +- 文件上传限制类型、数量和大小。 + +## 10.2 安全 + +- 非健康接口支持 Token 鉴权; +- CORS 使用明确允许列表; +- 记忆写入必须服务端二次检查; +- 文件路径必须规范化; +- 工具调用按权限分级; +- 所有副作用写入工作流审计。 + +## 10.3 性能 + +建议目标: + +- 首屏基础壳加载小于 2 秒; +- 健康状态接口不阻塞主页面; +- 聊天首 token 在 Provider 正常时尽快显示; +- 工作流和来源面板延迟加载; +- 群聊长线程使用虚拟列表或分页; +- 1000 条消息下滚动和输入保持可用; +- 多文件上传显示逐文件状态。 + +## 10.4 响应式 + +桌面端: + +- 可使用三栏; +- 右侧检查器可折叠。 + +平板端: + +- 左侧抽屉; +- 聊天主区; +- 检查器 Tab 化。 + +手机端: + +- 单栏; +- 底部主导航; +- 输入框不被软键盘遮挡; +- 面板不得覆盖聊天主体; +- 长路径和长错误信息可换行; +- 发送、上传、停止按钮触控区域足够大。 + +## 10.5 可访问性 + +- 所有图标按钮具有 aria-label; +- 支持键盘操作; +- 焦点状态清晰; +- 错误不能只用颜色表示; +- 加载状态可被辅助技术识别。 + +--- + +# 11. 回归验收清单 + +## 11.1 单人聊天 + +- [ ] 可选择全部角色; +- [ ] 可选择全部学习模式; +- [ ] 可选择 auto/flash/pro; +- [ ] 可选择 fast/standard/deep; +- [ ] 可选择 standard/warm/close; +- [ ] 回答支持流式; +- [ ] 路由结果可见; +- [ ] RAG 来源可见; +- [ ] 刷新后可恢复会话; +- [ ] 失败时保留用户消息并显示错误; +- [ ] 模型统计可查看和重置。 + +## 11.2 微信群 + +- [ ] 可生成群聊开场; +- [ ] 可创建新群聊; +- [ ] 可发送用户消息; +- [ ] 多角色回复支持流式; +- [ ] 可查看和清空未读; +- [ ] 可搜索群聊; +- [ ] 可生成群聊摘要; +- [ ] 群聊支持 RAG; +- [ ] 可提取记忆候选; +- [ ] 可逐条确认写入。 + +## 11.3 新闻 + +- [ ] 可搜索自定义主题; +- [ ] 可选择读取正文; +- [ ] 搜索、正文、摘要、讨论四阶段可见; +- [ ] 正文失败可降级; +- [ ] 来源和发布时间可见; +- [ ] 正文覆盖率可见; +- [ ] 摘要可生成群聊讨论。 + +## 11.4 RAG + +- [ ] MD/TXT/DOCX/PDF 可上传; +- [ ] 多文件上传可用; +- [ ] 可查看文档列表; +- [ ] 可查看 chunk; +- [ ] 四种检索模式可选; +- [ ] top_k 和 min_score 可配置; +- [ ] score breakdown 可见; +- [ ] 可注入单人聊天; +- [ ] 可注入微信群; +- [ ] 索引不存在时提示明确; +- [ ] 同名文件不会静默覆盖。 + +## 11.5 记忆与课后总结 + +- [ ] 可生成五类主要更新; +- [ ] 可查看质量警告; +- [ ] 可逐项勾选; +- [ ] current_focus 可对比; +- [ ] safe_mode 能阻止写入; +- [ ] memory locked 能阻止写入; +- [ ] 写入结果可验证; +- [ ] 可生成微信群反馈; +- [ ] 页面展示后端真实 writable 状态。 + +## 11.6 工具与工作流 + +- [ ] 工具必须先预览; +- [ ] 只读工具调用成功; +- [ ] 写入工具必须确认; +- [ ] 每次正式调用产生 run; +- [ ] run 可查看完整事件; +- [ ] 错误事件显示原因和耗时; +- [ ] 聊天、RAG、记忆和新闻关键流程可关联 trace/run。 + +--- + +# 12. 自动化测试要求 + +## 12.1 前端单元测试 + +建议使用 Vitest + React Testing Library,覆盖: + +- 设置表单; +- Chat 消息状态; +- RAG 来源表; +- 上传状态; +- 工作流详情; +- 工具预览/确认; +- 记忆候选选择; +- safe_mode 禁用状态; +- API 错误处理; +- 状态翻译和格式化函数。 + +## 12.2 API 契约测试 + +为每个 React 使用的 API 固化: + +- 请求字段; +- 响应字段; +- 枚举值; +- 错误码; +- 空状态; +- 鉴权状态。 + +TypeScript 类型应由 OpenAPI 自动生成或通过 CI 对比,避免字段漂移。 + +## 12.3 端到端测试 + +建议使用 Playwright,至少覆盖: + +1. 上传文档 → 建索引 → 检索 → 聊天引用; +2. 单人聊天 → 生成课后更新 → 预览 → 确认写入; +3. 创建微信群 → 发送消息 → 多角色回复 → 提取记忆; +4. 新闻搜索 → 正文 → 摘要 → 群聊; +5. 工具预览 → 调用 → 工作流详情; +6. safe_mode 下所有写入操作被阻止; +7. API Token 错误时显示登录/配置提示; +8. 移动端输入框和导航可用。 + +## 12.4 性能安全测试 + +每次新增功能至少检查: + +- 首屏 bundle 变化; +- API 调用数量; +- 长会话渲染; +- 长群聊渲染; +- 多文件上传; +- 工作流列表数量; +- 内存候选数量; +- 弱网和 API 超时。 + +无法自动化的功能必须给出人工验收步骤和性能安全边界。 + +--- + +# 13. 迁移实施顺序 + +## 阶段 0:冻结基线 + +- 将本文档加入 `docs/`; +- 为 Streamlit 关键流程录屏和截图; +- 建立功能对照测试; +- 不再向旧版增加大功能,只修严重缺陷。 + +## 阶段 1:单人学习功能对齐 + +- 运行设置 API; +- 角色/模式/模型/性能/氛围 UI; +- 流式聊天; +- 路由详情; +- 会话恢复; +- 模型统计。 + +## 阶段 2:RAG 功能对齐 + +- 文档列表; +- chunk 预览; +- 检索参数; +- 四种模式; +- score breakdown; +- 本地路径安全策略; +- 文档删除和重建确认。 + +## 阶段 3:长期记忆和课后总结 + +- 记忆概览; +- 课后总结预览 API; +- 分项选择; +- diff; +- 安全状态; +- 正式提交; +- 写入验证。 + +## 阶段 4:微信群和联网新闻 + +- 群聊 API; +- 群聊线程; +- 未读; +- 流式角色回复; +- 群聊 RAG; +- 群聊记忆; +- 新闻四阶段流程。 + +## 阶段 5:工程控制台整合 + +- 会话历史; +- 工作流筛选; +- 工具权限; +- trace_id; +- 健康检查; +- Provider 状态; +- 成本和性能统计。 + +## 阶段 6:弃用 Streamlit + +只有满足以下条件才可弃用: + +- P0 功能全部通过; +- 双版本回归矩阵无阻塞缺口; +- 关键 E2E 全部通过; +- 移动端和桌面端人工验收通过; +- 记忆写入和工具调用安全测试通过; +- 用户确认新版能够完成旧版全部主要学习流程。 + +--- + +# 14. 明确不应发生的重构退化 + +以下情况视为重构失败: + +1. 新版只能聊天,不能选择角色、模式和模型; +2. 新版只能上传资料,不能查看完整索引和检索参数; +3. 新版只显示“记忆受保护”,但不能生成和确认记忆; +4. 新版删除微信群和联网新闻; +5. 新版把流式输出改成长期无反馈的阻塞请求; +6. 新版不显示来源、路由或工具调用依据; +7. 新版把所有业务状态只保存在浏览器内存中; +8. 新版为了页面简洁隐藏 safe_mode、memory_mode 和失败原因; +9. 新版绕过服务端确认直接写长期记忆; +10. 在功能尚未对齐前删除 Streamlit 参考实现。 + +--- + +# 15. 当前结论 + +当前 React 版本已经在以下方面超过旧版: + +- 前后端分离; +- TypeScript 类型约束; +- API Token 和 CORS; +- RAG 来源表; +- 工具预览和正式调用; +- 工作流事件明细; +- 更适合扩展和独立部署。 + +当前 Streamlit 版本仍在以下方面显著领先: + +- 完整学习设置; +- 流式单人聊天; +- 自动路由可视化; +- 当前重点和学习进度; +- 微信群完整交互; +- 联网新闻四阶段流程; +- 群聊 RAG; +- 课后总结; +- 长期记忆候选、差异、确认和写入验证; +- 未读、会话结束和模型统计。 + +因此,后续正确方向不是“选一个版本删除另一个”,而是: + +> **用 React 重新承载 Streamlit 已验证的完整业务闭环,并保留 React 已实现的 RAG、工具、工作流和 API 工程优势。** + +--- + +## 附录 A:主要代码依据 + +### Streamlit 旧版 + +- `app.py` +- `src/ui/sidebar.py` +- `src/ui/status_bar.py` +- `src/ui/chat_panel.py` +- `src/ui/wechat_panel.py` +- `src/ui/wechat_news_panel.py` +- `src/ui/rag_panel.py` +- `src/ui/after_session_panel.py` +- `src/constants.py` + +### React 新版 + +- `frontend/src/App.tsx` +- `frontend/src/api.ts` +- `frontend/src/types.ts` +- `frontend/src/styles.css` + +### 后端 API + +- `src/api.py` + +### React 重构关键提交 + +- `863e5315625b306bef439f08e8a168b51e55fe98`:Add P9 React console shell +- `e714a28e42144cc5717e444d2d98808a15af5cd6`:Complete P9 React console workflows diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 368d801..8e4dff1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -25,21 +25,37 @@ import { import { ChangeEvent, FormEvent, useEffect, useMemo, useRef, useState } from "react"; import { callLocalKnowledge, + createWechatOpening, loadApiSnapshot, + loadRole, loadWorkflowRun, + lookupNews, + markWechatRead, previewLocalKnowledge, queryRag, + resetWechat, + runNewsSearch, + saveRuntimeSettings, sendChat, + sendWechatMessage, uploadDocuments } from "./api"; import type { ApiSnapshot, ChatMessage, ChatResponse, + ChatSettings, + MemoryStatusResponse, + NewsLookupResponse, + NewsSearchResponse, RagDebugResult, RagQueryResponse, RagResult, + RagSettings, + RoleResponse, + SessionRow, ToolInvocationResponse, + WechatStateResponse, WorkflowRunDetail, WorkflowRunSummary } from "./types"; @@ -49,17 +65,148 @@ const INITIAL_SNAPSHOT: ApiSnapshot = { ragStatus: null, tools: [], workflowRuns: [], + sessions: [], + runtimeSettings: null, + memoryStatus: null, + wechat: null, error: "" }; const seedMessages: ChatMessage[] = [ { role: "assistant", + avatarRole: "nahida", content: "本地学习工作台已就绪。你可以提问、上传资料、查看引用来源,并在右侧检查工具调用与工作流状态。" } ]; +const CHAT_SETTINGS_DEFAULTS: ChatSettings = { + selectedRole: "auto", + selectedMode: "auto", + selectedModel: "auto", + relationshipMode: "standard", + contextMode: "" +}; + +const RAG_SETTINGS_DEFAULTS: RagSettings = { + retrievalMode: "hybrid", + topK: 5, + minScore: 0.01, + chatTopK: 3 +}; + +const roleOptions = [ + ["auto", "自动"], + ["march7", "三月七"], + ["keqing", "刻晴"], + ["nahida", "纳西妲"], + ["firefly", "流萤"] +] as const; + +const roleDescriptions: Record = { + auto: "后端根据问题自动选择合适角色。", + march7: "更轻快、鼓励式的学习伙伴。", + keqing: "偏执行、判断和推进项目。", + nahida: "偏概念解释、连接知识脉络。", + firefly: "偏陪伴、感受整理和收束。" +}; + +const modeOptions = [ + ["auto", "自动"], + ["普通", "直接讲解"], + ["苏格拉底", "苏格拉底"], + ["费曼", "费曼"], + ["项目", "项目推进"] +] as const; + +const modeDescriptions: Record = { + auto: "后端根据问题自动判断学习方式。", + 普通: "直接回答问题,适合快速确认事实或步骤。", + 苏格拉底: "通过连续追问帮你自己推理出答案,适合概念卡住时使用。", + 费曼: "要求你用简单语言复述,再帮你找漏洞,适合检查是否真正理解。", + 项目: "围绕目标、任务、风险和下一步推进。" +}; + +const modelOptions = [ + ["auto", "自动"], + ["flash", "Flash"], + ["pro", "Pro"] +] as const; + +const modelDescriptions: Record = { + auto: "按当前性能设置和任务自动选模型。", + flash: "响应更快,适合日常问答和轻量检索。", + pro: "质量更高,适合复杂分析、写作和长上下文。" +}; + +const contextModeOptions = [ + ["", "自动"], + ["fast", "快速"], + ["light", "标准"], + ["deep", "深度"] +] as const; + +const contextModeDescriptions: Record = { + "": "沿用后端当前运行档位。", + fast: "优先速度,减少上下文和输出预算。", + light: "平衡速度和质量,适合大多数学习对话。", + deep: "读取更多上下文,适合复杂问题和复盘。" +}; + +const relationshipOptions = [ + ["standard", "自然"], + ["warm", "温和"], + ["close", "贴近"] +] as const; + +const relationshipDescriptions: Record = { + standard: "自然克制,保持学习导向。", + warm: "更鼓励、更柔和,但仍然聚焦任务。", + close: "更有陪伴感,适合复盘和情绪整理。" +}; + +const retrievalOptions = [ + ["lexical", "关键词"], + ["hybrid", "混合"], + ["vector", "本地向量"], + ["backend_vector", "向量后端"] +] as const; + +const retrievalDescriptions: Record = { + lexical: "按关键词命中,稳定、可解释。", + hybrid: "关键词和向量结合,通常最稳妥。", + vector: "使用本地向量语义检索。", + backend_vector: "调用外部向量后端,取决于后端配置。" +}; + +const roleAvatarPaths: Record = { + march7: "/assets/avatars/march7.png", + keqing: "/assets/avatars/keqing.png", + nahida: "/assets/avatars/nahida.png", + firefly: "/assets/avatars/firefly.png" +}; + +const speakerToRole: Record = { + 三月七: "march7", + 刻晴: "keqing", + 纳西妲: "nahida", + 流萤: "firefly", + 用户: "user" +}; + +const quickPrompts = [ + "继续上次学习,先给我一个下一步建议", + "分析当前 Study Agent 架构,并列出最该推进的三件事", + "根据本地资料解释 RAG 工作流时间线的作用" +]; + +const roadmapItems = [ + "微信群、新闻讨论和课后总结仍需要 PRD 中的新增 API 才能完整迁移。", + "React 当前先补齐单人学习设置、路由检查、RAG 参数和会话状态。", + "Streamlit 暂时保留为业务闭环回归参考。" +]; + type SourceRow = { key: string; rank: number; @@ -105,6 +252,84 @@ function basename(path: string): string { return parts.length ? parts[parts.length - 1] : path; } +function displayValue(value: unknown): string { + if (value === null || typeof value === "undefined" || value === "") { + return "-"; + } + if (Array.isArray(value)) { + return value.length ? value.join(", ") : "-"; + } + if (typeof value === "object") { + return JSON.stringify(value); + } + return String(value); +} + +function formatBytes(value: number | undefined): string { + if (!value) { + return "0 B"; + } + if (value < 1024) { + return `${value} B`; + } + if (value < 1024 * 1024) { + return `${(value / 1024).toFixed(1)} KB`; + } + return `${(value / 1024 / 1024).toFixed(1)} MB`; +} + +function formatMtime(ns: number | undefined): string { + if (!ns) { + return "-"; + } + return new Date(Math.floor(ns / 1_000_000)).toLocaleString(); +} + +function roleAvatarUrl(roleId: string | undefined): string { + return roleId ? roleAvatarPaths[roleId] ?? "" : ""; +} + +function roleLabel(roleId: string | undefined): string { + if (!roleId || roleId === "auto") { + return "Study Agent"; + } + return roleOptions.find(([value]) => value === roleId)?.[1] ?? roleId; +} + +function RoleAvatar({ roleId, fallback }: { roleId?: string; fallback: "user" | "assistant" }) { + const avatarUrl = roleAvatarUrl(roleId); + return ( +
+ {avatarUrl ? ( + {roleLabel(roleId)} + ) : fallback === "user" ? ( + + ) : ( + + )} +
+ ); +} + +function parseWechatMessages(content: string): Array<{ speaker: string; roleId: string; text: string }> { + const blocks: Array<{ speaker: string; roleId: string; text: string }> = []; + const pattern = /【([^】]+)】\s*\n?([\s\S]*?)(?=\n*【[^】]+】|\s*$)/g; + let match: RegExpExecArray | null; + while ((match = pattern.exec(content)) !== null) { + const speaker = match[1].trim(); + const text = match[2].trim(); + if (!text) { + continue; + } + blocks.push({ + speaker, + roleId: speakerToRole[speaker] ?? "", + text + }); + } + return blocks; +} + function sourceRowsFromDebug(debugResults: RagDebugResult[] | undefined, fallbackResults: RagResult[]): SourceRow[] { if (debugResults?.length) { return debugResults.map((item, index) => ({ @@ -140,18 +365,42 @@ function Sidebar({ snapshot, ragEnabled, setRagEnabled, + chatSettings, + setChatSettings, + ragSettings, + setRagSettings, + onSaveSettings, + isSavingSettings, + onLoadRole, + roleDetail, refresh, onUploadClick, - uploadState + uploadState, + lastChat }: { snapshot: ApiSnapshot; ragEnabled: boolean; setRagEnabled: (value: boolean) => void; + chatSettings: ChatSettings; + setChatSettings: (value: ChatSettings) => void; + ragSettings: RagSettings; + setRagSettings: (value: RagSettings) => void; + onSaveSettings: () => void; + isSavingSettings: boolean; + onLoadRole: () => void; + roleDetail: RoleResponse | null; refresh: () => void; onUploadClick: () => void; uploadState: string; + lastChat: ChatResponse | null; }) { const apiTone = snapshot.health?.status === "ok" ? "good" : snapshot.error ? "bad" : "neutral"; + const updateChatSetting = (key: keyof ChatSettings, value: string) => { + setChatSettings({ ...chatSettings, [key]: value }); + }; + const updateRagSetting = (key: K, value: RagSettings[K]) => { + setRagSettings({ ...ragSettings, [key]: value }); + }; return (