296 lines
13 KiB
Markdown
296 lines
13 KiB
Markdown
# AIChat 交互体验改进方案
|
||
|
||
> 创建: 2026-06-14 | 状态: 待讨论
|
||
> 范围: 消息发送、流式渲染、对话管理、错误恢复、技能/Provider、窗口布局等非授权类交互
|
||
|
||
---
|
||
|
||
## 一、消息输入与发送
|
||
|
||
### 1.1 无法编辑已发送消息
|
||
|
||
**现象**:用户发出消息后发现措辞有误,只能重新打一条新消息。无法像 ChatGPT/Claude 那样编辑上一条用户消息并重新生成。
|
||
|
||
**根因**:`sendMessage`(useAiSend.ts)push 后的消息是只追加不可变的。前端没有编辑入口,后端 `ai_chat_send` 也没有"替换最后一条 user 消息并重跑"的语义。
|
||
|
||
**方案**:
|
||
- 用户消息气泡 hover 显示"编辑"按钮
|
||
- 点击后消息内容回填到输入框,用户修改后发送时后端截断该消息之后的所有历史(含 AI 回复),重新跑 agentic loop
|
||
- 后端新增 `ai_chat_edit` 命令,接收 `message + 截断位置`,替换 messages 数组中对应 user 消息并清掉后续消息,然后走正常 `run_agentic_loop`
|
||
|
||
### 1.2 无法重新生成 AI 回复
|
||
|
||
**现象**:AI 回答不满意时没有"重新生成"按钮,只能重新措辞追问。
|
||
|
||
**根因**:`AiChat.vue` 的 AI 消息气泡上没有任何操作按钮。后端也没有 `ai_regenerate` 命令。
|
||
|
||
**方案**:
|
||
- AI 消息气泡 hover 显示操作栏(复制 | 重新生成)
|
||
- "重新生成":后端删除最后一条 AI 消息,用倒数第二条 user 消息重新触发 `run_agentic_loop`
|
||
- 后端新增 `ai_regenerate` 命令
|
||
|
||
### 1.3 无法一键复制消息内容
|
||
|
||
**现象**:代码或文本只能手动选中复制。流式渲染中选中文字会被后续 delta 打断。
|
||
|
||
**根因**:消息气泡上没有复制按钮。
|
||
|
||
**方案**:
|
||
- AI 消息气泡 hover 显示"复制"按钮
|
||
- 点击后 `navigator.clipboard.writeText(msg.content)`,toast 提示"已复制"
|
||
- 代码块单独提供"复制代码"按钮(hover 代码块右上角浮出)
|
||
|
||
### 1.4 缺少 `@` 实体引用
|
||
|
||
**现象**:用户无法在输入时引用某个项目、任务或文件。描述需求时无法精确指定上下文,AI 可能猜错对象。
|
||
|
||
**根因**:输入框只有 `/` 技能联想,没有 `@` 实体引用机制。
|
||
|
||
**方案**:
|
||
- 输入框支持 `@` 触发实体联想浮层(复用技能联想的 popover 架构)
|
||
- 联想源:项目列表、任务列表、最近编辑的文件
|
||
- 选中后在消息中展开为 `[项目: u-desk]` 等标记文本,后端 system prompt 注入对应实体的上下文摘要
|
||
|
||
### 1.5 输入框高度过紧
|
||
|
||
**现象**:textarea 最大高度 120px(约 5-6 行),超过后内部滚动。用户写长提示词时看不到全貌。
|
||
|
||
**根因**:`autoResize` 中 `Math.min(el.scrollHeight, 120)` 限制太紧。
|
||
|
||
**方案**:
|
||
- 最大高度提升到 200px(约 10 行),超过后再内部滚动
|
||
- 或改为可拖拽调整高度(底部 resize handle)
|
||
|
||
---
|
||
|
||
## 二、流式渲染与消息展示
|
||
|
||
### 2.1 流式渲染中无法稳定选中文字
|
||
|
||
**现象**:AI 正在流式输出时,用户尝试选中已渲染的文字,新的 delta 触发 DOM 更新导致选区丢失。
|
||
|
||
**根因**:`renderContent` 在流式时返回 `streamingHtml.value`(rAF 节流重 parse),每次更新都 `v-html` 替换整个 DOM 子树,浏览器选区被清除。
|
||
|
||
**方案**:
|
||
- 方案 A(推荐):检测到用户正在选择文字时(`selectionchange` 事件 + 选区非空且在消息容器内),暂停 rAF 流式 parse,选区结束后恢复
|
||
- 方案 B:流式渲染时在已完成的块(blockCache 命中的块)上使用独立 DOM 节点不参与 v-html 替换,仅末块动态更新
|
||
|
||
### 2.2 代码块无语法高亮、无复制按钮
|
||
|
||
**现象**:Markdown 代码块只有纯文本渲染,没有语法高亮,也没有一键复制按钮。
|
||
|
||
**根因**:`useMarkdown` composable 配置了 marked + DOMPurify,但没有集成 highlight.js / Prism 等高亮器。
|
||
|
||
**方案**:
|
||
- 集成 highlight.js(体积小、语言全),在 marked renderer 的 `code` 回调中调用 `hljs.highlightAuto` 或按 info string 指定语言
|
||
- 代码块右上角浮出"复制"按钮(纯前端,hover 显示)
|
||
- 高亮器按需加载(与 marked 一样后台预热,不阻塞首屏)
|
||
|
||
### 2.3 历史消息无分页懒加载
|
||
|
||
**现象**:切换对话时 `switchConversation` 一次性加载全部 messages 到 `state.messages`。超长对话可能一次加载几百条消息。
|
||
|
||
**根因**:`switchConversation`(useAiConversations.ts)直接 `state.messages = rawMsgs.filter().map()`,无分页。
|
||
|
||
**方案**:
|
||
- 首次加载最近 50 条,滚动到顶部时加载更多(前端 slice + 后端支持 offset/limit 查询 messages)
|
||
- 或前端全量加载但虚拟滚动只渲染可视区域(见 7.2)
|
||
|
||
### 2.4 消息不显示时间戳
|
||
|
||
**现象**:消息气泡上不显示发送时间。用户无法判断某条消息是多久前发的。
|
||
|
||
**根因**:模板中 AI/用户消息都没有渲染 `msg.timestamp`。
|
||
|
||
**方案**:
|
||
- 消息气泡下方或侧边以极小字号(9px)+ dim 颜色展示相对时间(`formatRelativeZh`)
|
||
- hover 时 tooltip 展示完整时间
|
||
|
||
---
|
||
|
||
## 三、对话管理
|
||
|
||
### 3.1 对话搜索缺失
|
||
|
||
**现象**:侧栏对话列表只能滚动浏览,没有搜索框。对话多了之后找不到特定对话。
|
||
|
||
**根因**:侧栏 `.ai-conv-list` 直接渲染 `groupedActive`,上方只有"新建"按钮,没有搜索输入框。
|
||
|
||
**方案**:
|
||
- 侧栏 header 下方增加搜索输入框(实时过滤 `state.conversations`,匹配 title)
|
||
- 搜索时取消分组,按相关度/时间平铺展示
|
||
- 支持 `Ctrl+K` 快捷键聚焦搜索框
|
||
|
||
### 3.2 对话无法置顶
|
||
|
||
**现象**:没有置顶或收藏功能。重要对话会被新对话挤到下面。
|
||
|
||
**根因**:对话列表只按 `updated_at` 排序,无 `pinned` 字段。
|
||
|
||
**方案**:
|
||
- `ai_conversations` 表增加 `pinned INTEGER DEFAULT 0`
|
||
- 排序逻辑改为 `pinned DESC, updated_at DESC`
|
||
- 侧栏对话项 hover 显示"置顶/取消置顶"按钮(图钉图标)
|
||
|
||
### 3.3 对话无法导出
|
||
|
||
**现象**:无法将对话导出为 Markdown / JSON / 文本文件。
|
||
|
||
**根因**:没有导出 IPC 命令和前端入口。
|
||
|
||
**方案**:
|
||
- 后端新增 `ai_conversation_export(conv_id, format)` 命令,支持 `markdown` / `json` / `txt` 三种格式
|
||
- 前端侧栏对话项 hover 显示"导出"按钮,或对话操作菜单中提供
|
||
- Markdown 格式:`## 用户` / `## 助手` 交替,代码块保留围栏
|
||
|
||
### 3.4 新建对话强制中断已有生成
|
||
|
||
**现象**:用户在 AI 生成中点"新建对话",后端 `ai_conversation_create` 会强制复位 `generating=false` + 置 `stop_flag`,中断当前生成。
|
||
|
||
**根因**:`commands.rs ai_conversation_create` 中 `if session.generating` 分支主动结束生成(B-260615-10 设计)。
|
||
|
||
**方案**:
|
||
- 生成中点"新建对话"时弹 ConfirmDialog:"当前对话正在生成,确定要新建对话并中断吗?"
|
||
- 用户确认后才执行中断+新建;取消则不操作
|
||
- 或改为不中断:新对话仅切换视图,旧对话后台继续生成(需 AiSession 多实例化支持,属远期方案)
|
||
|
||
---
|
||
|
||
## 四、错误处理与恢复
|
||
|
||
### 4.1 错误气泡无操作入口
|
||
|
||
**现象**:错误气泡只显示一行文字(如 `[GLM-4] AI 调用失败(HTTP 401): Unauthorized`),没有"重试"按钮或"去设置"链接。
|
||
|
||
**根因**:`AiError` 事件只携带 `error: String`,前端 `handleEvent` push 一条 `isError: true` 的消息气泡,无操作按钮。
|
||
|
||
**方案**:
|
||
- 错误气泡底部增加操作按钮区:
|
||
- **重试**:取上一条 user 消息重新发送(调 `ai_chat_send`)
|
||
- **去设置**(仅 401/403/Provider 未配置时显示):跳转到 Settings → AI Tab
|
||
- 错误消息结构扩展:后端 `AiError` 增加 `error_type: Option<ErrorType>` 枚举(`auth` / `network` / `timeout` / `provider_config` / `unknown`),前端据此决定显示哪些按钮
|
||
|
||
### 4.2 流式中断丢失已生成文本
|
||
|
||
**现象**:网络波动导致流式中断(idle timeout 或 mid-stream error),已接收的文本被丢弃(`stream_llm` return None → `agentic.rs` emit AiError 并退出),用户看到错误气泡,之前生成的几百字全部丢失。
|
||
|
||
**根因**:`stream_llm` 遇到错误时 `return None`,不保留已接收的部分文本。`agentic.rs` 收到 None 后直接结束。
|
||
|
||
**方案**:
|
||
- `stream_llm` mid-stream error 时改为返回 `Some((partial_text, tool_calls, usage))` + 一个 `incomplete: bool` 标志
|
||
- `agentic.rs` 收到 incomplete=true 时:
|
||
- 已有文本正常入库(标注 `truncated`)
|
||
- emit `AiCompleted`(而非 AiError)让前端正常展示已生成内容
|
||
- 在消息末尾追加系统提示:"⚠ 响应因网络中断不完整"
|
||
- 前端 AI 消息气泡底部显示"继续生成"按钮(用最后一条 user 消息重新触发,后端识别不完整消息做续写或重跑)
|
||
|
||
---
|
||
|
||
## 五、技能与 Provider
|
||
|
||
### 5.1 Provider 切换零反馈
|
||
|
||
**现象**:点击 provider bar 循环切换 provider(`cycleProvider`),切换后只在 bar 上显示名字变化,没有 toast 或动画反馈。
|
||
|
||
**根因**:`cycleProvider` 直接调 `store.setProvider`,无 UI 反馈。
|
||
|
||
**方案**:
|
||
- 切换后 toast 提示"已切换到 {providerName}"
|
||
- provider bar 切换时加 0.15s 淡入动画
|
||
- bar 上增加 provider 状态指示(model 名称小字展示)
|
||
|
||
### 5.2 技能联想不展示参数用法
|
||
|
||
**现象**:技能联想浮层显示 name + description + source,但不显示技能的参数格式或示例。选中后 placeholder 里的 `argument_hint` 太短。
|
||
|
||
**根因**:浮层设计只展示概要信息,`argument_hint` 仅在选中后作为 placeholder 显示。
|
||
|
||
**方案**:
|
||
- 联想浮层每项增加一行参数提示(`argument_hint` 以等宽字体小字展示)
|
||
- 选中技能后输入框上方 chip 展示完整的参数格式说明(而非仅 name + description)
|
||
- 或选中技能后自动在输入框填入模板骨架(如 `/commit `),光标定位到参数位置
|
||
|
||
---
|
||
|
||
## 六、窗口与布局
|
||
|
||
### 6.1 侧栏宽度不可调
|
||
|
||
**现象**:侧栏固定 160px,不可拖拽调整宽度。长对话标题被截断。
|
||
|
||
**根因**:`.ai-conv-sidebar { width: 160px; min-width: 160px; }` 硬编码。
|
||
|
||
**方案**:
|
||
- 侧栏右边缘增加 2px 拖拽条(`cursor: col-resize`)
|
||
- 拖拽时实时更新 width,范围 120~280px
|
||
- 宽度持久化到 `df-ai-ui` 设置(与 sidebarOpen/maximized 同存)
|
||
|
||
### 6.2 分离窗口关闭后生成态不同步
|
||
|
||
**现象**:分离窗口关闭时,如果正在生成,主窗口虽然 `panelOpen=true` 恢复,但 `state.streaming` / `state.currentText` 可能不同步——如果生成中的对话不是主窗口当前活跃对话,主窗口看不到生成态。
|
||
|
||
**根因**:分离窗口关闭走 `closeDetachedWindow`,清了 localStorage 快照,但主窗口的状态依赖事件路由自然恢复。
|
||
|
||
**方案**:
|
||
- 分离窗口关闭时,主窗口检测 `state.generatingConvId` 非空,自动切换到正在生成的对话
|
||
- 或在主窗口 header 显示"对话 X 正在生成中"提示条,点击切换过去
|
||
|
||
---
|
||
|
||
## 七、其他交互细节
|
||
|
||
### 7.1 无键盘快捷键
|
||
|
||
**现象**:除了 Enter 发送 / Shift+Enter 换行,没有其他快捷键。
|
||
|
||
**方案**:
|
||
|
||
| 快捷键 | 功能 |
|
||
|--------|------|
|
||
| `Ctrl+N` | 新建对话 |
|
||
| `Ctrl+K` | 搜索对话 |
|
||
| `Ctrl+L` | 清空当前对话 |
|
||
| `Ctrl+Shift+C` | 复制最后一条 AI 消息 |
|
||
| `Ctrl+R` | 重新生成最后一条 AI 回复 |
|
||
| `Esc` | 关闭面板(嵌入模式)/ 关闭窗口(分离模式) |
|
||
| `Ctrl+B` | 切换侧栏 |
|
||
|
||
### 7.2 消息列表无虚拟滚动
|
||
|
||
**现象**:消息列表直接 `v-for` 渲染所有消息,长对话几百条消息全量渲染 DOM,滚动卡顿。
|
||
|
||
**根因**:没有使用虚拟滚动库。
|
||
|
||
**方案**:
|
||
- 集成 `vue-virtual-scroller` 或自研 IntersectionObserver 懒渲染
|
||
- 仅渲染可视区域 ±缓冲区的消息节点
|
||
- 注意:流式渲染的最后一条消息需要始终保持挂载
|
||
|
||
### 7.3 空状态无引导
|
||
|
||
**现象**:首次打开 AI Chat(有 provider 配置),空状态只显示标题+提示语,没有示例问题或快捷操作。
|
||
|
||
**方案**:
|
||
- 空状态展示 3-4 个示例问题卡片(如"帮我创建一个新项目"、"查看当前任务列表"、"分析这段代码的问题")
|
||
- 点击卡片自动填入输入框并发送
|
||
- 无 provider 配置时展示"去配置 AI Provider"引导按钮
|
||
|
||
### 7.4 对话标题生成对用户不透明
|
||
|
||
**现象**:对话标题由后端 `ensure_conversation_title` 异步生成,侧栏对话从"新对话"突然变成某个标题,没有过渡。
|
||
|
||
**方案**:
|
||
- 标题生成后加 0.3s 淡入动画
|
||
- 或在标题前加小图标标识"AI 自动生成"(可 hover 查看)
|
||
|
||
---
|
||
|
||
## 八、落地优先级
|
||
|
||
| 批次 | 痛点 | 理由 |
|
||
|------|------|------|
|
||
| **第一批** | 2.1 流式选中文字、1.2+1.3 消息操作栏(复制/重新生成)、4.1 错误重试、4.2 断线保文 | 每次对话都会遇到,最高频痛点 |
|
||
| **第二批** | 2.2 代码高亮+复制、3.1 对话搜索、7.1 键盘快捷键、3.4 新建不中断生成 | 显著提升日常效率 |
|
||
| **第三批** | 1.1 编辑重发、1.4 `@` 引用、3.3 导出、7.2 虚拟滚动、6.1 侧栏可调 | 按需推进,锦上添花 |
|
||
| **第四批** | 2.3 分页加载、2.4 时间戳、3.2 置顶、5.1+5.2 技能/Provider 反馈、7.3+7.4 空状态/标题 | 打磨细节 |
|