# 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 空状态/标题 | 打磨细节 |
