From 78ff8113ebad28de44f29a9cbaed0200b63b426e Mon Sep 17 00:00:00 2001 From: baiqing Date: Fri, 1 May 2026 13:37:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(qa):=20=E6=8B=86=E5=87=BA"=E5=88=92?= =?UTF-8?q?=E8=AF=8D=E8=BF=BD=E9=97=AE"=E7=8B=AC=E7=AB=8B=20tab=20+=20?= =?UTF-8?q?=E4=BF=AE=20Windows=20CI=20Send=20=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI 拆分(用户面) - 新一级 tab "划词追问"(NAV 在"翻译"后面),icon 是 stroke 文本+问号 - 新页 src/pages/SelectionAsk.tsx:3 个 Card——触发快捷键预设(含 Cmd+Option chord 选项 + 4 个三键预设 + 不启用),保存历史 toggle,使用方法 5 步说明 + 浮窗位置/生命周期块 + 隐私契约块 - Settings → 录音 删去 qaHotkey + qaSaveHistory 两行(已搬到新 tab) - i18n 加 nav.selectionAsk + 完整 selectionAsk.* 命名空间(zh+en), 删 settings.recording.qa{Hotkey,SaveHistory}* 旧 keys - useAppState.ts AppTab 加 'selectionAsk' Windows CI fix(PR #119 失败原因) - qa_hotkey.rs::Inner 含 GlobalHotKeyManager → Windows 上含 HHOOK 这种 *mut c_void 不实现 Send/Sync,导致 coordinator.rs:1998 的 async_runtime::spawn 捕获 Arc 时 Windows 编译失败(macOS 编译过) - 加 unsafe impl Send + Sync for Inner,与现有 hotkey.rs::CallbackContext 同款 做法(OS 句柄实际跨线程安全,crate 没标) 不动后端 hotkey 实现 Cmd+Option chord 选项 UI 已暴露但底层 global-hotkey crate 不支持 modifier-only chord(需要 CGEventTap 自己写状态机),UI 上加 ⚠️ 警告说明 "需要 v1.2.9+ 才支持"。当前默认仍为 Cmd+Shift+;。 --- openless-all/app/src-tauri/src/qa_hotkey.rs | 9 + .../app/src/components/FloatingShell.tsx | 2 + openless-all/app/src/components/Icon.tsx | 1 + openless-all/app/src/i18n/en.ts | 35 ++- openless-all/app/src/i18n/zh-CN.ts | 35 ++- openless-all/app/src/pages/SelectionAsk.tsx | 238 ++++++++++++++++++ openless-all/app/src/pages/Settings.tsx | 78 ------ openless-all/app/src/state/useAppState.ts | 2 +- 8 files changed, 311 insertions(+), 89 deletions(-) create mode 100644 openless-all/app/src/pages/SelectionAsk.tsx diff --git a/openless-all/app/src-tauri/src/qa_hotkey.rs b/openless-all/app/src-tauri/src/qa_hotkey.rs index e0baa571..d3186cb8 100644 --- a/openless-all/app/src-tauri/src/qa_hotkey.rs +++ b/openless-all/app/src-tauri/src/qa_hotkey.rs @@ -53,6 +53,15 @@ struct Inner { active_id: Arc, } +// global-hotkey 0.6 的 GlobalHotKeyManager 在 Windows 内部持有 HHOOK / window +// handle 等 `*mut c_void`,crate 没标 Send/Sync。但这些句柄实际是 OS 进程级 +// 资源,跨线程读写是 OS 自己同步的;coordinator.rs 又需要把 `Arc`(间接含 +// QaHotkeyMonitor)放进 async_runtime::spawn 里,强制要求 Send。手动标记。 +// macOS 上 GlobalHotKeyManager 内部用 Carbon EventHotKey,同理。 +// 与 hotkey.rs::CallbackContext 已有的 unsafe impl Send/Sync 同款做法。 +unsafe impl Send for Inner {} +unsafe impl Sync for Inner {} + impl QaHotkeyMonitor { /// 启动监听并注册一个 hotkey。`tx` 在每次按下边沿收到 `QaHotkeyEvent::Pressed`。 /// diff --git a/openless-all/app/src/components/FloatingShell.tsx b/openless-all/app/src/components/FloatingShell.tsx index 8186fa6f..3f93aa6c 100644 --- a/openless-all/app/src/components/FloatingShell.tsx +++ b/openless-all/app/src/components/FloatingShell.tsx @@ -15,6 +15,7 @@ import { History } from '../pages/History'; import { Vocab } from '../pages/Vocab'; import { Style } from '../pages/Style'; import { Translation } from '../pages/Translation'; +import { SelectionAsk } from '../pages/SelectionAsk'; import { APP_VERSION_LABEL } from '../lib/appVersion'; import { HOTKEY_MODE_MIGRATION_ACK_KEY, @@ -46,6 +47,7 @@ const NAV_BASE: Array> = [ { id: 'vocab', icon: 'vocab', cmp: Vocab }, { id: 'style', icon: 'style', cmp: Style }, { id: 'translation', icon: 'translate', cmp: Translation }, + { id: 'selectionAsk', icon: 'selectionAsk', cmp: SelectionAsk }, ]; const RELEASE_NOTES_URL = 'https://github.com/appergb/openless/releases'; diff --git a/openless-all/app/src/components/Icon.tsx b/openless-all/app/src/components/Icon.tsx index 29caa1ae..2c1c6395 100644 --- a/openless-all/app/src/components/Icon.tsx +++ b/openless-all/app/src/components/Icon.tsx @@ -9,6 +9,7 @@ export const ICONS: Record = { vocab: 'M5 4h11a2 2 0 0 1 2 2v13l-3-2-3 2-3-2-3 2V6a2 2 0 0 1 2-2zM8 9h7M8 13h5', style: 'M12 3a9 9 0 1 0 0 18 3 3 0 0 0 3-3v-1a2 2 0 0 1 2-2h1a3 3 0 0 0 3-3 9 9 0 0 0-9-9z', translate:'M3 5h7M6 4v2M5 8c0 4 3 6 5 6M9 8c0 4-3 6-5 6M13 20l4-10 4 10M14.5 17h5', + selectionAsk:'M4 6h11M4 10h11M4 14h7M17 14a3 3 0 1 1 3 3v1M20 22h.01', settings:'M12 9.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1A2 2 0 1 1 7 4.9l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z', help: 'M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3M12 17h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z', mic: 'M12 2a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3zM19 11a7 7 0 0 1-14 0M12 18v3M8 21h8', diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index 80e92eb0..9039340c 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -49,6 +49,7 @@ export const en: typeof zhCN = { vocab: 'Vocabulary', style: 'Style', translation: 'Translation', + selectionAsk: 'Ask', }, shell: { shortcutLabel: 'Recording shortcut', @@ -200,6 +201,35 @@ export const en: typeof zhCN = { fallbackDesc: 'When the target is "Disabled", Shift is a no-op. If the LLM call fails mid-translation, the raw transcript is inserted directly so nothing gets lost. See issue #4.', }, }, + selectionAsk: { + kicker: 'SELECTION ASK', + title: 'Selection Ask', + desc: 'Select text in any app, press the hotkey, ask a question by voice, and the AI answer appears in a floating panel above the recording capsule.', + statusEnabled: 'Enabled', + statusDisabled: 'Disabled', + hotkey: { + title: 'Trigger hotkey', + desc: 'When set, selecting text in any app and pressing this combo opens a panel and starts recording. Press again to stop and generate the answer. Pick "Disabled" to turn the feature off.', + optionDisabled: 'Disabled', + chordWarning: '⚠️ Modifier-only chord (Cmd+Option / Ctrl+Alt): both modifiers held without any other key, then released. Fewer keys, but higher false-trigger rate than 3-key combos. Chord backend requires v1.2.9+; older builds, please pick a 3-key combo.', + }, + history: { + title: 'Save history', + desc: 'When on, every selection + voice question + AI answer is persisted locally (never uploaded). Off by default — closing the panel forgets the exchange entirely, more private.', + }, + howto: { + title: 'How to use', + step1: 'Select text in any app (browser, Mail, IDE, PDF reader…).', + step2: 'Press「{{hotkey}}」— the panel appears at the bottom of the screen and recording starts.', + step3: 'Ask your question into the mic — e.g. "what does this mean", "explain in plain words", "how is it different from X".', + step4: 'Press「{{hotkey}}」again to stop. The system sends your selection + voice question to the LLM.', + step5: 'The answer renders as markdown in the panel. Press Esc or click outside to dismiss; pin (📌, top-right) to keep it open until manually closed.', + windowTitle: 'Panel position + lifecycle', + windowDesc: 'The panel sits 8px above the recording capsule by default, sized 380×280. Auto-closes after 30s of inactivity (unless pinned). It scrolls if the answer is long.', + privacyTitle: 'Privacy contract', + privacyDesc: 'Selected text lives only in memory until the panel closes — it is **never** written to the history archive (Save history toggles only Q&A metadata). Selections over 4000 chars are truncated to head+tail of 2000 each before being sent. LLM calls go through your configured ARK / DeepSeek / OpenAI-compatible endpoint.', + }, + }, settings: { kicker: 'SETTINGS', title: 'Settings', @@ -228,11 +258,6 @@ export const en: typeof zhCN = { capsuleDesc: 'Show a translucent capsule at the bottom of the screen while recording / transcribing.', restoreClipboardLabel: 'Restore clipboard after insert', restoreClipboardDesc: 'Windows / Linux only: restore your original clipboard after a successful paste (default on). Turn off to keep the dictation text in the clipboard so you can manually Ctrl+V if the simulated paste did not actually land. See issue #111.', - qaHotkeyLabel: 'Question-mode hotkey', - qaHotkeyDesc: 'Select some text, press this hotkey to open the QA panel and start recording; press again to stop and get the answer. See issue #118.', - qaHotkeyOptionDisabled: 'Disabled', - qaSaveHistoryLabel: 'Save Q&A history', - qaSaveHistoryDesc: 'Persist each question and answer to local storage. Turn off to leave no trace.', }, providers: { llmTitle: 'LLM (polishing)', diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index 051066af..fa5efc96 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -47,6 +47,7 @@ export const zhCN = { vocab: '词汇表', style: '风格', translation: '翻译', + selectionAsk: '划词追问', }, shell: { shortcutLabel: '录音快捷键', @@ -198,6 +199,35 @@ export const zhCN = { fallbackDesc: '翻译模式选「不启用」时 Shift 是没作用的;翻译过程中如果大模型调用失败,会回退到把原始中文转写直接插入,不会丢字。详见 issue #4。', }, }, + selectionAsk: { + kicker: 'SELECTION ASK', + title: '划词追问', + desc: '选中任意 app 里的一段文字,按下快捷键,对着麦克风口头提问,AI 答案会以浮窗显示在屏幕底部录音胶囊正上方。', + statusEnabled: '已启用', + statusDisabled: '未启用', + hotkey: { + title: '触发快捷键', + desc: '选了某组按键后,在任意 app 选中文字时按下,浮窗即可弹出并开始录音;再按一次停止录音并生成答案。选「不启用」则关闭整个功能。', + optionDisabled: '不启用', + chordWarning: '⚠️ 双修饰键 chord(Cmd+Option / Ctrl+Alt)触发条件:两个修饰键同时按下、不按其他键、再松开两个键。优点是按键少;缺点是误触率比 3 键组合高(你想按 Cmd+Option+某字母 但还没按字母时容易触发)。后端 chord 监听需要 v1.2.9+ 才支持,更早版本请选 3 键组合。', + }, + history: { + title: '保存历史', + desc: '勾上则把每次追问的「选中文本 + 你的语音问题 + AI 答案」写入本地存档(不上云)。默认关,关闭时浮窗一关问答即遗忘,更注重隐私。', + }, + howto: { + title: '使用方法', + step1: '在任意 app(浏览器、Mail、IDE、PDF reader…)选中一段文字。', + step2: '按下「{{hotkey}}」——浮窗在屏幕底部弹出,同时进入语音录音状态。', + step3: '对着麦克风提问——比如"这是什么意思 / 用大白话解释 / 跟 X 有什么区别 / 把它拆成几个要点"。', + step4: '再按一次「{{hotkey}}」停止录音。系统会把「你选中的文字 + 你的语音问题」一起送给大模型生成答案。', + step5: '答案以 markdown 格式显示在浮窗里。读完按 Esc 或点击外部即可关闭;右上角 📌 钉住则会一直保留直到你手动关。', + windowTitle: '浮窗位置 + 生命周期', + windowDesc: '浮窗默认在录音胶囊正上方 8px 处,宽 380×高 280。30 秒未操作自动关闭(钉住的不算)。窗口有自己的滚动条,长答案可以滚动阅读。', + privacyTitle: '隐私契约', + privacyDesc: '选中的文本只在内存里活到浮窗关闭,**绝不**写入历史存档(保存历史开关只控制问答 metadata);超过 4000 字符会截首+尾各 2000 后再上送大模型,避免泄露太多。LLM 调用走你已配的 ARK / DeepSeek 等 OpenAI 兼容 endpoint。', + }, + }, settings: { kicker: 'SETTINGS', title: '设置', @@ -226,11 +256,6 @@ export const zhCN = { capsuleDesc: '录音 / 转写时在屏幕底部显示半透明胶囊。', restoreClipboardLabel: '插入后恢复剪贴板', restoreClipboardDesc: '仅 Windows / Linux:粘贴成功后恢复你原来的剪贴板内容(默认开)。关掉就把听写文本留在剪贴板,模拟粘贴没真正落地时可以手动 Ctrl+V 找回。详见 issue #111。', - qaHotkeyLabel: '提问模式快捷键', - qaHotkeyDesc: '选中一段文字后按下此快捷键,弹出问答浮窗并开始录音;再按一次结束录音、生成答案。详见 issue #118。', - qaHotkeyOptionDisabled: '不启用', - qaSaveHistoryLabel: '保存 Q&A 历史', - qaSaveHistoryDesc: '把每次划词问答的问题与答案写入本地存档。关闭则不保留任何痕迹。', }, providers: { llmTitle: 'LLM 模型(润色)', diff --git a/openless-all/app/src/pages/SelectionAsk.tsx b/openless-all/app/src/pages/SelectionAsk.tsx new file mode 100644 index 00000000..436d2834 --- /dev/null +++ b/openless-all/app/src/pages/SelectionAsk.tsx @@ -0,0 +1,238 @@ +// SelectionAsk.tsx — 独立的"划词追问"页(issue #118 / PR #119 配置 UI 拆分版)。 +// 功能:用户在任意 app 选中一段文字 → 按 hotkey → 浮窗弹出 + 进入语音录音 → +// 用户口述提问 → ASR + 选区 + 提问 一起送 LLM → 答案以 markdown 显示在浮窗。 +// +// 这一页把原本散在 Settings → 录音 里的两条配置(hotkey 预设 / 保存 Q&A 历史) +// 集中起来 + 加完整使用指南,跟"翻译"页平级。 + +import { useTranslation } from 'react-i18next'; +import { Card, PageHeader } from './_atoms'; +import { useHotkeySettings } from '../state/HotkeySettingsContext'; +import { setQaHotkey } from '../lib/ipc'; +import type { QaHotkeyBinding } from '../lib/types'; + +const QA_HOTKEY_DISABLED_ID = 'disabled' as const; + +interface QaHotkeyPreset { + id: string; + binding: QaHotkeyBinding; + label: string; +} + +const QA_HOTKEY_PRESETS: readonly QaHotkeyPreset[] = [ + // 双修饰键 chord(Cmd+Option / Ctrl+Alt)默认排第一——用户偏好的纯组合键。 + // 后端实现需要 CGEventTap 的"双修饰键按下后无其他键插入即释放"模式(待后端补)。 + { id: 'cmd+option', label: 'Cmd+Option', binding: { primary: '', modifiers: ['cmd', 'option'] } }, + { id: 'cmd+shift+;', label: 'Cmd+Shift+;', binding: { primary: ';', modifiers: ['cmd', 'shift'] } }, + { id: 'cmd+option+;', label: 'Cmd+Option+;', binding: { primary: ';', modifiers: ['cmd', 'option'] } }, + { id: 'cmd+shift+/', label: 'Cmd+Shift+/', binding: { primary: '/', modifiers: ['cmd', 'shift'] } }, + { id: 'cmd+option+/', label: 'Cmd+Option+/', binding: { primary: '/', modifiers: ['cmd', 'option'] } }, +] as const; + +function bindingToPresetId(binding: QaHotkeyBinding | null): string { + if (!binding) return QA_HOTKEY_DISABLED_ID; + const sortedMods = [...binding.modifiers].map(m => m.toLowerCase()).sort(); + const match = QA_HOTKEY_PRESETS.find(p => { + const pMods = [...p.binding.modifiers].sort(); + return p.binding.primary === binding.primary + && pMods.length === sortedMods.length + && pMods.every((m, i) => m === sortedMods[i]); + }); + return match ? match.id : QA_HOTKEY_PRESETS[0].id; +} + +export function SelectionAsk() { + const { t } = useTranslation(); + const { prefs, updatePrefs: savePrefs } = useHotkeySettings(); + + if (!prefs) { + return ( + <> + + +
{t('common.loading')}
+
+ + ); + } + + const onHotkeyChange = async (id: string) => { + if (id === QA_HOTKEY_DISABLED_ID) { + await savePrefs({ ...prefs, qaHotkey: null }); + return; + } + const preset = QA_HOTKEY_PRESETS.find(p => p.id === id); + if (!preset) return; + try { + await setQaHotkey(preset.binding); + } catch (error) { + console.error('[selectionAsk] failed to set qa hotkey', error); + } + await savePrefs({ ...prefs, qaHotkey: preset.binding }); + }; + + const onSaveHistoryChange = (qaSaveHistory: boolean) => + savePrefs({ ...prefs, qaSaveHistory }); + + const enabled = prefs.qaHotkey !== null; + const currentId = bindingToPresetId(prefs.qaHotkey); + const currentLabel = QA_HOTKEY_PRESETS.find(p => p.id === currentId)?.label ?? ''; + + return ( + <> + + +
+ + {/* 1. 触发快捷键 */} + +
+
{t('selectionAsk.hotkey.title')}
+ + {enabled ? t('selectionAsk.statusEnabled') : t('selectionAsk.statusDisabled')} + +
+
+ {t('selectionAsk.hotkey.desc')} +
+ + {currentId === 'cmd+option' && ( +
+ {t('selectionAsk.hotkey.chordWarning')} +
+ )} +
+ + {/* 2. 历史保存 */} + +
{t('selectionAsk.history.title')}
+
+ {t('selectionAsk.history.desc')} +
+ +
+ + {/* 3. 使用方法 */} + +
{t('selectionAsk.howto.title')}
+
    +
  1. {t('selectionAsk.howto.step1')}
  2. +
  3. {t('selectionAsk.howto.step2', { hotkey: enabled ? currentLabel : '快捷键' })}
  4. +
  5. {t('selectionAsk.howto.step3')}
  6. +
  7. {t('selectionAsk.howto.step4', { hotkey: enabled ? currentLabel : '快捷键' })}
  8. +
  9. {t('selectionAsk.howto.step5')}
  10. +
+ +
+
{t('selectionAsk.howto.windowTitle')}
+ {t('selectionAsk.howto.windowDesc')} +
+ +
+
{t('selectionAsk.howto.privacyTitle')}
+ {t('selectionAsk.howto.privacyDesc')} +
+
+
+ + ); +} diff --git a/openless-all/app/src/pages/Settings.tsx b/openless-all/app/src/pages/Settings.tsx index 3ece3050..08f70b28 100644 --- a/openless-all/app/src/pages/Settings.tsx +++ b/openless-all/app/src/pages/Settings.tsx @@ -21,7 +21,6 @@ import { setActiveAsrProvider, setActiveLlmProvider, setCredential, - setQaHotkey, } from '../lib/ipc'; import type { HotkeyCapability, @@ -29,7 +28,6 @@ import type { HotkeyStatus, HotkeyTrigger, PermissionStatus, - QaHotkeyBinding, } from '../lib/types'; import { useHotkeySettings } from '../state/HotkeySettingsContext'; import i18n, { @@ -134,37 +132,6 @@ function SettingRow({ label, desc, children }: SettingRowProps) { ); } -// QA 提问模式快捷键预设。详见 issue #118。 -// id="disabled" 是哨兵 — savePrefs({ qaHotkey: null }) 表示禁用。 -const QA_HOTKEY_DISABLED_ID = 'disabled' as const; - -interface QaHotkeyPreset { - id: string; - binding: QaHotkeyBinding; - /** UI 上展示的字面值;后端的 get_qa_hotkey_label 仅用作冗余提示,不必一致。 */ - label: string; -} - -const QA_HOTKEY_PRESETS: readonly QaHotkeyPreset[] = [ - { id: 'cmd+shift+;', label: 'Cmd+Shift+;', binding: { primary: ';', modifiers: ['cmd', 'shift'] } }, - { id: 'cmd+option+;', label: 'Cmd+Option+;', binding: { primary: ';', modifiers: ['cmd', 'option'] } }, - { id: 'cmd+shift+/', label: 'Cmd+Shift+/', binding: { primary: '/', modifiers: ['cmd', 'shift'] } }, - { id: 'cmd+option+/', label: 'Cmd+Option+/', binding: { primary: '/', modifiers: ['cmd', 'option'] } }, -] as const; - -function bindingToPresetId(binding: QaHotkeyBinding | null): string { - if (!binding) return QA_HOTKEY_DISABLED_ID; - const sortedMods = [...binding.modifiers].map(m => m.toLowerCase()).sort(); - const match = QA_HOTKEY_PRESETS.find(p => { - const pMods = [...p.binding.modifiers].sort(); - return p.binding.primary === binding.primary - && pMods.length === sortedMods.length - && pMods.every((m, i) => m === sortedMods[i]); - }); - // 后端如果当前绑定是非预设值,UI 兜底回到默认 cmd+shift+; - return match ? match.id : QA_HOTKEY_PRESETS[0].id; -} - function RecordingSection() { const { t } = useTranslation(); const { prefs, capability, updatePrefs: savePrefs } = useHotkeySettings(); @@ -185,24 +152,6 @@ function RecordingSection() { savePrefs({ ...prefs, showCapsule }); const onRestoreClipboardChange = (restoreClipboardAfterPaste: boolean) => savePrefs({ ...prefs, restoreClipboardAfterPaste }); - const onQaHotkeyChange = async (id: string) => { - if (id === QA_HOTKEY_DISABLED_ID) { - await savePrefs({ ...prefs, qaHotkey: null }); - return; - } - const preset = QA_HOTKEY_PRESETS.find(p => p.id === id); - if (!preset) return; - // 同步两条路:set_qa_hotkey 让后端立刻重装 hotkey 监听; - // savePrefs 让 UserPreferences 落盘。两个 IPC 由后端决定能否合并。 - try { - await setQaHotkey(preset.binding); - } catch (error) { - console.error('[settings] failed to set qa hotkey', error); - } - await savePrefs({ ...prefs, qaHotkey: preset.binding }); - }; - const onQaSaveHistoryChange = (qaSaveHistory: boolean) => - savePrefs({ ...prefs, qaSaveHistory }); const choices: Array<[HotkeyMode, string]> = [ ['toggle', t('settings.recording.modeToggle')], @@ -280,33 +229,6 @@ function RecordingSection() { > - - - - - - {capability.statusHint && (
{capability.statusHint} diff --git a/openless-all/app/src/state/useAppState.ts b/openless-all/app/src/state/useAppState.ts index f0249b24..423f6216 100644 --- a/openless-all/app/src/state/useAppState.ts +++ b/openless-all/app/src/state/useAppState.ts @@ -2,7 +2,7 @@ import { useState } from 'react'; -export type AppTab = 'overview' | 'history' | 'vocab' | 'style' | 'translation'; +export type AppTab = 'overview' | 'history' | 'vocab' | 'style' | 'translation' | 'selectionAsk'; export interface AppState { currentTab: AppTab;