squash合并: - 意图识别层论证(8维度+10业界佐证) - 多主题上下文管理愿景+并存论证+补充论证(多轮agentic) - 架构设计文档物理分类(四子目录+INDEX+命名规范+引用同步+边界清晰化) - 前端架构技术债清单归档
13 KiB
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
- 重试:取上一条 user 消息重新发送(调
- 错误消息结构扩展:后端
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_llmmid-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 空状态/标题 | 打磨细节 |