Merge pull request #338 from H-Chris233/fix/issue-315-selectionask-save-feedback

修复划词追问保存失败无反馈
This commit is contained in:
Chris233 authored and GitHub committed 2026-05-08 07:16:52 +08:00
commit c4a7ebe62c
7 files changed
+141 -15

No files matched your search

@@ -8,10 +8,12 @@ export function ShortcutRecorder({
value,
onSave,
alignRecordButton = false,
disabled = false,
}: {
value: ShortcutBinding;
onSave: (binding: ShortcutBinding) => Promise<void>;
alignRecordButton?: boolean;
disabled?: boolean;
}) {
const { t } = useTranslation();
const [recording, setRecording] = useState(false);
@@ -39,6 +41,12 @@ export function ShortcutRecorder({
};
}, [recording]);
useEffect(() => {
if (!disabled || !recording) return;
setRecording(false);
clearPendingModifier();
}, [disabled, recording]);
const finish = async (binding: ShortcutBinding) => {
try {
await validateShortcutBinding(binding);
@@ -52,7 +60,7 @@ export function ShortcutRecorder({
};
const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
if (!recording) return;
if (!recording || disabled) return;
e.preventDefault();
e.stopPropagation();
if (e.key === 'Escape') {
@@ -80,7 +88,7 @@ export function ShortcutRecorder({
};
const onKeyUp = (e: KeyboardEvent<HTMLDivElement>) => {
if (!recording || !isModifierKey(e.key)) return;
if (!recording || disabled || !isModifierKey(e.key)) return;
e.preventDefault();
e.stopPropagation();
const primary = modifierPrimaryFromCode(e.code, e.key);
@@ -113,7 +121,8 @@ export function ShortcutRecorder({
borderRadius: 6,
fontFamily: 'inherit',
fontWeight: 500,
cursor: recording ? 'default' : 'pointer',
cursor: recording || disabled ? 'default' : 'pointer',
opacity: disabled ? 0.68 : 1,
marginLeft: alignRecordButton ? 'auto' : undefined,
};
@@ -125,11 +134,12 @@ export function ShortcutRecorder({
</span>
<button
onClick={() => {
if (disabled) return;
setRecording(true);
setError(null);
clearPendingModifier();
}}
disabled={recording}
disabled={recording || disabled}
style={recordButtonStyle}
>
{recording ? t('settings.recording.comboRecordHint') : t('settings.recording.comboRecordBtn')}
+5
View File
@@ -240,6 +240,11 @@ export const en: typeof zhCN = {
optionDisabled: 'Disabled',
chordWarning: '',
},
save: {
hotkeyRegisterFailed: 'Failed to register the Selection Ask shortcut. The preference was not saved.',
hotkeySaveFailed: 'Failed to save the Selection Ask shortcut. Please try again.',
historySaveFailed: 'Failed to save the Q&A history setting. Please try again.',
},
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.',
+5
View File
@@ -242,6 +242,11 @@ export const ja: typeof zhCN = {
optionDisabled: '無効',
chordWarning: '',
},
save: {
hotkeyRegisterFailed: '選択追問ショートカットの登録に失敗しました。設定は保存されていません。',
hotkeySaveFailed: '選択追問ショートカットの保存に失敗しました。もう一度お試しください。',
historySaveFailed: 'Q&A 履歴設定の保存に失敗しました。もう一度お試しください。',
},
history: {
title: '履歴を保存',
desc: 'ON にすると、各追問の「選択テキスト + あなたの音声質問 + AI 回答」をローカルに保存(クラウドへは送信しない)。デフォルト OFF では、フロートウィンドウを閉じると問答が消去されプライバシー優先。',
+5
View File
@@ -242,6 +242,11 @@ export const ko: typeof zhCN = {
optionDisabled: '비활성화',
chordWarning: '',
},
save: {
hotkeyRegisterFailed: '선택 질문 단축키 등록에 실패했습니다. 설정은 저장되지 않았습니다.',
hotkeySaveFailed: '선택 질문 단축키 저장에 실패했습니다. 다시 시도하세요.',
historySaveFailed: 'Q&A 기록 설정 저장에 실패했습니다. 다시 시도하세요.',
},
history: {
title: '기록 저장',
desc: '체크 시 매 후속 질문의 "선택 텍스트 + 음성 질문 + AI 답변"을 로컬 보관(클라우드 비전송). 기본 OFF 일 때는 창을 닫으면 문답이 즉시 사라져 프라이버시 우선.',
+5
View File
@@ -238,6 +238,11 @@ export const zhCN = {
optionDisabled: '不启用',
chordWarning: '',
},
save: {
hotkeyRegisterFailed: '划词追问快捷键注册失败,未继续保存。',
hotkeySaveFailed: '划词追问快捷键保存失败,请重试。',
historySaveFailed: 'Q&A 历史保存设置保存失败,请重试。',
},
history: {
title: '保存历史',
desc: '勾上则把每次追问的「选中文本 + 你的语音问题 + AI 答案」写入本地存档(不上云)。默认关,关闭时浮窗一关问答即遗忘,更注重隐私。',
+5
View File
@@ -240,6 +240,11 @@ export const zhTW: typeof zhCN = {
optionDisabled: '不啓用',
chordWarning: '',
},
save: {
hotkeyRegisterFailed: '劃詞追問快捷鍵註冊失敗,未繼續保存。',
hotkeySaveFailed: '劃詞追問快捷鍵保存失敗,請重試。',
historySaveFailed: 'Q&A 歷史保存設置保存失敗,請重試。',
},
history: {
title: '保存歷史',
desc: '勾上則把每次追問的「選中文本 + 你的語音問題 + AI 答案」寫入本地存檔(不上雲)。默認關,關閉時浮窗一關問答即遺忘,更注重隱私。',
+102 -11
View File
@@ -5,19 +5,83 @@
// 这一页把原本散在 Settings → 录音 里的两条配置(hotkey 预设 / 保存 Q&A 历史)
// 集中起来 + 加完整使用指南,跟"翻译"页平级。
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, PageHeader } from './_atoms';
import { useHotkeySettings } from '../state/HotkeySettingsContext';
import { setQaHotkey } from '../lib/ipc';
import { defaultQaShortcut, formatComboLabel } from '../lib/hotkey';
import { ShortcutRecorder } from '../components/ShortcutRecorder';
import type { QaHotkeyBinding, UserPreferences } from '../lib/types';
type SaveState = 'idle' | 'saving' | 'saved' | 'failed';
export function SelectionAsk() {
const { t } = useTranslation();
const { prefs, updatePrefs: savePrefs } = useHotkeySettings();
const { prefs, refresh, updatePrefs: savePrefs } = useHotkeySettings();
const [saveState, setSaveState] = useState<SaveState>('idle');
const [saveMessage, setSaveMessage] = useState('');
const statusTimer = useRef<number | null>(null);
const defaultQaHotkey = defaultQaShortcut();
const defaultHotkeyLabel = formatComboLabel(defaultQaHotkey);
const recordHotkeyLabel = prefs ? formatComboLabel(prefs.dictationHotkey) : '快捷键';
useEffect(() => () => {
if (statusTimer.current !== null) window.clearTimeout(statusTimer.current);
}, []);
const showSaveStatus = (state: SaveState, message: string, temporary = false) => {
if (statusTimer.current !== null) {
window.clearTimeout(statusTimer.current);
statusTimer.current = null;
}
setSaveState(state);
setSaveMessage(message);
if (temporary) {
statusTimer.current = window.setTimeout(() => {
setSaveState('idle');
setSaveMessage('');
statusTimer.current = null;
}, 1600);
}
};
const persistPrefs = async (
resolveNext: (current: UserPreferences) => UserPreferences,
failureMessage: string,
) => {
try {
await savePrefs(resolveNext);
showSaveStatus('saved', t('common.saved'), true);
return true;
} catch (error) {
console.error('[selection-ask] failed to save preferences', error);
showSaveStatus('failed', failureMessage);
await refresh().catch(refreshError => {
console.warn('[selection-ask] failed to refresh preferences after save error', refreshError);
});
return false;
}
};
const saveQaHotkey = async (nextHotkey: QaHotkeyBinding | null) => {
showSaveStatus('saving', t('common.saving'));
try {
await setQaHotkey(nextHotkey);
} catch (error) {
console.error('[selection-ask] failed to register QA hotkey', error);
showSaveStatus('failed', t('selectionAsk.save.hotkeyRegisterFailed'));
await refresh().catch(refreshError => {
console.warn('[selection-ask] failed to refresh preferences after hotkey error', refreshError);
});
return;
}
await persistPrefs(
current => ({ ...current, qaHotkey: nextHotkey }),
t('selectionAsk.save.hotkeySaveFailed'),
);
};
if (!prefs) {
return (
<>
@@ -36,11 +100,17 @@ export function SelectionAsk() {
);
}
const onSaveHistoryChange = (qaSaveHistory: boolean) =>
savePrefs({ ...prefs, qaSaveHistory });
const onSaveHistoryChange = (qaSaveHistory: boolean) => {
showSaveStatus('saving', t('common.saving'));
void persistPrefs(
current => ({ ...current, qaSaveHistory }),
t('selectionAsk.save.historySaveFailed'),
);
};
const enabled = prefs.qaHotkey !== null;
const currentLabel = prefs.qaHotkey ? formatComboLabel(prefs.qaHotkey) : defaultHotkeyLabel;
const saving = saveState === 'saving';
return (
<>
@@ -54,16 +124,34 @@ export function SelectionAsk() {
/>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{saveState !== 'idle' && (
<div
role={saveState === 'failed' ? 'alert' : 'status'}
style={{
padding: '8px 12px',
borderRadius: 10,
border: saveState === 'failed'
? '0.5px solid rgba(239,68,68,0.22)'
: '0.5px solid rgba(37,99,235,0.16)',
background: saveState === 'failed' ? 'rgba(239,68,68,0.07)' : 'rgba(37,99,235,0.06)',
color: saveState === 'failed' ? 'var(--ol-red, #ef4444)' : 'var(--ol-blue)',
fontSize: 11.5,
lineHeight: 1.5,
}}
>
{saveMessage}
</div>
)}
{/* 1. 触发快捷键 */}
<Card>
<CardHeaderToggle
title={t('selectionAsk.hotkey.title')}
checked={enabled}
onToggle={async () => {
disabled={saving}
onToggle={() => {
const nextHotkey = enabled ? null : defaultQaHotkey;
await setQaHotkey(nextHotkey);
await savePrefs({ ...prefs, qaHotkey: nextHotkey });
void saveQaHotkey(nextHotkey);
}}
/>
<div style={{ fontSize: 11.5, color: 'var(--ol-ink-4)', marginBottom: prefs.qaHotkey ? 12 : 0, lineHeight: 1.55 }}>
@@ -72,10 +160,8 @@ export function SelectionAsk() {
{prefs.qaHotkey && (
<ShortcutRecorder
value={prefs.qaHotkey}
onSave={async binding => {
await setQaHotkey(binding);
await savePrefs({ ...prefs, qaHotkey: binding });
}}
onSave={saveQaHotkey}
disabled={saving}
/>
)}
</Card>
@@ -85,6 +171,7 @@ export function SelectionAsk() {
<CardHeaderToggle
title={t('selectionAsk.history.title')}
checked={prefs.qaSaveHistory}
disabled={saving}
onToggle={() => onSaveHistoryChange(!prefs.qaSaveHistory)}
/>
<div style={{ fontSize: 11.5, color: 'var(--ol-ink-4)', lineHeight: 1.55 }}>
@@ -142,10 +229,12 @@ export function SelectionAsk() {
function CardHeaderToggle({
title,
checked,
disabled = false,
onToggle,
}: {
title: string;
checked: boolean;
disabled?: boolean;
onToggle: () => void;
}) {
return (
@@ -154,6 +243,7 @@ function CardHeaderToggle({
<button
onClick={onToggle}
aria-pressed={checked}
disabled={disabled}
style={{
position: 'relative',
width: 36,
@@ -161,7 +251,8 @@ function CardHeaderToggle({
borderRadius: 999,
border: 0,
background: checked ? 'var(--ol-blue)' : 'rgba(0,0,0,0.15)',
cursor: 'default',
cursor: disabled ? 'not-allowed' : 'default',
opacity: disabled ? 0.68 : 1,
transition: 'background 0.16s var(--ol-motion-quick)',
padding: 0,
}}