Repository navigation
[feature] 划词语音问答(Selection + Voice Q&A) #118
Copy link
Copy link
Closed
Description
背景
调研了 Wispr Flow Command Mode / Superwhisper Super Mode / Raycast AI / Apple Writing Tools / DeepL / ChatGPT macOS / Cursor Cmd+K / Bob 共 8 个同类工具后形成的方案。
用户工作流
- 用户在任意 app(浏览器、Mail、IDE、PDF reader…)选中一段文字
- 按下配置好的 hotkey(默认 Cmd+Shift+;,可在设置改)
- 新建一个 QA 浮动窗口 出现在屏幕上现有语音胶囊正上方约 8px
- 同时进入语音录音状态(与现有 dictation 共用 ASR 通路),用户对着麦克风提问("这是什么意思 / 总结一下 / 跟 X 有什么区别")
- 按一次 hotkey 停止录音(toggle 行为,跟 dictation 一致)
- ASR 转写问题 + 选区 → LLM → markdown 答案显示在浮窗里
- 用户读完按 Esc / 点外部 / 30s 超时关闭,或按右上角 Pin 钉住久留
已确定的设计决策
| 维度 | 决策 | 理由 |
|---|---|---|
| 默认 hotkey | Cmd+Shift+; | Cmd+Opt+T 跟 macOS Mail/Pages "Show/Hide Fonts" 系统快捷键冲突;Fn+Q 是备选但 Fn 在外接键盘上不稳 |
| 用户可改 | ✅ 4 个预设 + 检测重复 | 不上自定义按键录入界面(实现复杂、用户学习成本高) |
| 取选区策略 | AX → AppleScript → Cmd+C 静音模拟 三级 fallback | 参考 SelectedTextKit;Electron app(VS Code/Slack/Notion)AX 返回空已知 bug,靠 Cmd+C 兜底 |
| Cmd+C 模拟前 | snapshot NSPasteboard,调用后 restore | 不污染用户剪贴板(与 issue #111 同款约定) |
| 答案窗口 | 新建 Tauri webview 窗口(不复用胶囊) | 胶囊是 transient state UI,QA 答案是 reading UI(要可滚动、可复制)。不引入 tauri-nspanel 新 dep——普通 webview + alwaysOnTop + skipTaskbar 即可 |
| 答案窗口位置 | 锚在胶囊上方 8px,水平居中对齐 | Coordinator 拿胶囊 frame 算出 QA frame |
| 答案窗口生命周期 | Esc / 点外部 / 30s 超时关,可 Pin 久留 | 参考 Raycast Quick AI |
| 答案格式 | Markdown,≤ 3 段,不超过 ~200 字 | LLM prompt 硬约束 |
| 没选区 | 静默降级为纯语音问答(不报错) | 参考 Raycast / Superwhisper |
| 静默录音 | 直接 cancel,不调 LLM、不弹窗 | 沿用现有 `.cancelled` 路径 |
| 选区超 4000 字符 | 截首+尾各 2000,加 `[…truncated…]` | Wispr Flow 是硬拒绝 1000 词,UX 差 |
| 选区持久化 | 不写 history.json,只活到 QA 窗关闭 | 隐私约束 |
| 历史记录 | Settings 加 toggle "Save Q&A history",默认关 | 用户主动 opt-in |
实现规划(agent teams 并行分工)
Track 1 — Backend: 新 hotkey 注册 + 选区捕获
文件改动:
- `Cargo.toml`:现有 `global-hotkey = "0.6"` 已在 deps,启用之
- `hotkey.rs`:新加 `QaHotkeyMonitor` 模块(独立于现有的 modifier-only `HotkeyMonitor`),用 global-hotkey crate 注册 Cmd+Shift+; 之类组合键
- `coordinator.rs`:加 `SelectionContext { text: String, source_app: Option }` + 新事件 `QaHotkeyEvent::{Pressed, Released}`
- 新文件 `selection.rs`:策略链取选区
- `#[cfg(target_os = "macos")]` AX via objc2-app-kit + Cmd+C fallback
- `#[cfg(target_os = "windows")]` SendInput Ctrl+C + restore clipboard
- `types.rs`:`UserPreferences` 加 `qa_hotkey: Option`,`QaHotkeyBinding { primary: KeyCode, modifiers: ModifiersFlags }`
Track 2 — Backend: QA 通路(ASR → LLM → 浮窗)
文件改动:
- `coordinator.rs`:新增 `SessionKind::QuestionAnswer { selection: SelectionContext }`
- `begin_session` 分流:dictation / translation / qa
- `polish.rs`:新加 `answer_with_selection(question: &str, selection: &str, working_languages: &[String], front_app: Option<&str>) -> Result<String, LLMError>`
-
Prompt 模板(中文,沿用现有 chat_completion 通路):
```
用户选中了以下文本:
"""{selection_truncated}"""
用户的语音提问:"{question}"请基于选中文本回答。如果选中文本与提问无关,按提问独立回答,不要编造选区里没有的信息。
用 Markdown,不超过 3 段,不超过 ~200 字。
```
-
- 选区超 4000 字符截断逻辑
Track 3 — Frontend: QA 答案浮窗
新文件:
- `tauri.conf.json`:新加 windows entry `label: "qa"`,`width: 380, height: 280`,`transparent: true`,`alwaysOnTop: true`,`skipTaskbar: true`,`focus: false`,`acceptFirstMouse: true`,`visible: false`
- 新文件 `src/pages/QaPanel.tsx`:
- 加载状态(gradient skeleton + "思考中...")
- 答案区(react-markdown 渲染,已在 deps?没有就加 `marked` 走轻量)
- 关闭按钮(×)+ Pin 按钮(📌)
- 监听 `qa:state` event 接收答案
- 监听全局 Esc + click-outside 关闭逻辑
- `src/App.tsx`:分发 window label `?window=qa` → `QaPanel`
- 答案窗位置:从 Rust 侧 `set_position` 计算 `(capsule.x + (220-380)/2, capsule.y - 280 - 8)`
Track 4 — Settings UI + i18n
文件改动:
- `src/pages/Translation.tsx` 旁边的 `Settings.tsx`:录音 section 加一行"提问模式快捷键"——下拉 4 个预设(Cmd+Shift+; / Cmd+Opt+; / Cmd+Shift+/ / Cmd+Opt+/)
- `src/pages/Settings.tsx` 新增 toggle "保存 Q&A 历史"
- `UserPreferences.qa_hotkey + qa_save_history`
- i18n key 全套:`settings.recording.qaHotkey*`, `qa.thinking`, `qa.error`, `qa.pinTooltip`, `qa.closeTooltip`
Test plan
- macOS:在 Mail / Safari / Xcode 各选一段文字,按 hotkey,问"用大白话总结",答案应正确显示在胶囊上方
- macOS:在 VS Code / Slack(Electron)测试 AX fallback 到 Cmd+C 路径
- hotkey 配置:UI 选预设,重启 OK,重复绑定(与 dictation hotkey 同键)应被拒
- 选区超 4000 字符:截断显示,answer 仍合理
- 没选区:自动降级为纯语音问答
- 静默录音:QA 窗关闭,无 LLM 调用
- Esc / 点外部 → 关闭;Pin 后保持
- 30s 超时关闭(未 pin 的情况)
- 隐私:选区不出现在 history.json,answer 默认不持久化
Out of scope(v1)
- 自定义按键录入界面(用预设下拉)
- 答案分段流式 / SSE
- 多轮对话(每次按 hotkey 是独立 Q&A)
- 答案窗口里点链接打开
- 选区超 4000 字时 "上传完整文本" 选项
- iOS / Linux 实现
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
feat(qa): v2 多轮对话 + 流式输出 + macOS 浮窗修复
Merge pull request #119 from appergb/feat/selection-voice-qa
feat: 实现划词语音问答(QA)后端 Rust 路径
feat: 划词语音问答前端 — closes Open-Less#118 前端部分
chore(qa): 集成 — 对齐 Rust↔TS 事件 payload 字段名
refactor(coordinator): 合并 take_*_combo_hotkey_on_main_thread 三处复制