Skip to content

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

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 实现

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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