Skip to content

[feature] 划词语音问答(Selection + Voice Q&A) #118

@appergb

Description

@appergb

背景

调研了 Wispr Flow Command Mode / Superwhisper Super Mode / Raycast AI / Apple Writing Tools / DeepL / ChatGPT macOS / Cursor Cmd+K / Bob 共 8 个同类工具后形成的方案。

用户工作流

  1. 用户在任意 app(浏览器、Mail、IDE、PDF reader…)选中一段文字
  2. 按下配置好的 hotkey(默认 Cmd+Shift+;,可在设置改)
  3. 新建一个 QA 浮动窗口 出现在屏幕上现有语音胶囊正上方约 8px
  4. 同时进入语音录音状态(与现有 dictation 共用 ASR 通路),用户对着麦克风提问("这是什么意思 / 总结一下 / 跟 X 有什么区别")
  5. 按一次 hotkey 停止录音(toggle 行为,跟 dictation 一致)
  6. ASR 转写问题 + 选区 → LLM → markdown 答案显示在浮窗里
  7. 用户读完按 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 实现

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions