fix(ui): clean sidebar and translation settings

This commit is contained in:
baiqing committed 2026-05-03 10:03:40 +08:00
1 parent 91f1c5fae8
commit d7d274fa87
5 files changed
+191 -159

No files matched your search

@@ -22,16 +22,13 @@ import {
HOTKEY_MODE_MIGRATION_DEFERRED_KEY, HOTKEY_MODE_MIGRATION_DEFERRED_KEY,
shouldShowHotkeyModeMigrationPrompt, shouldShowHotkeyModeMigrationPrompt,
} from '../lib/hotkeyMigration'; } from '../lib/hotkeyMigration';
import { getHotkeyTriggerLabel } from '../lib/hotkey';
import { applyFontScale, readFontScale } from '../lib/fontScale'; import { applyFontScale, readFontScale } from '../lib/fontScale';
import { getCredentials, openExternal } from '../lib/ipc'; import { getCredentials, openExternal } from '../lib/ipc';
import { OL_DATA } from '../lib/mockData';
import { import {
PROVIDER_SETUP_PROMPT_DEFERRED_KEY, PROVIDER_SETUP_PROMPT_DEFERRED_KEY,
shouldShowProviderSetupPrompt, shouldShowProviderSetupPrompt,
} from '../lib/providerSetup'; } from '../lib/providerSetup';
import type { SettingsSectionId } from '../pages/Settings'; import type { SettingsSectionId } from '../pages/Settings';
import { useHotkeySettings } from '../state/HotkeySettingsContext';
import { useAppState, type AppTab } from '../state/useAppState'; import { useAppState, type AppTab } from '../state/useAppState';
interface NavItem { interface NavItem {
@@ -75,7 +72,6 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
const [providerPromptOpen, setProviderPromptOpen] = useState(false); const [providerPromptOpen, setProviderPromptOpen] = useState(false);
const [hotkeyModePromptOpen, setHotkeyModePromptOpen] = useState(false); const [hotkeyModePromptOpen, setHotkeyModePromptOpen] = useState(false);
const [helpPopoverOpen, setHelpPopoverOpen] = useState(false); const [helpPopoverOpen, setHelpPopoverOpen] = useState(false);
const { hotkey } = useHotkeySettings();
// 字体档位 — 启动时按 localStorage 应用一次;之后改动来自 Settings 的"个性化"section。 // 字体档位 — 启动时按 localStorage 应用一次;之后改动来自 Settings 的"个性化"section。
useEffect(() => { useEffect(() => {
@@ -212,12 +208,6 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
<Icon name={n.icon} size={14} /> <Icon name={n.icon} size={14} />
<span style={{ flex: 1 }}>{n.name}</span> <span style={{ flex: 1 }}>{n.name}</span>
{n.id === 'history' &&
<span style={{
fontSize: 10, fontFamily: 'var(--ol-font-mono)',
color: active ? 'var(--ol-ink-4)' : 'var(--ol-ink-5)',
}}>{OL_DATA.history.length}</span>
}
</button> </button>
); );
})} })}
@@ -225,21 +215,6 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
<div style={{ flex: 1 }} /> <div style={{ flex: 1 }} />
{/* shortcut hint — 不要 dashed 边框,否则会切断"整片磨砂玻璃"的视觉 */}
<div style={{ padding: '10px 10px 6px', marginTop: 6 }}>
<div style={{ fontSize: 10.5, color: 'var(--ol-ink-4)', marginBottom: 6, letterSpacing: '0.02em' }}>{t('shell.shortcutLabel')}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: 'var(--ol-ink-2)' }}>
<kbd style={{
padding: '2px 7px', fontSize: 10.5,
background: 'rgba(255,255,255,0.7)', borderRadius: 5,
border: '0.5px solid var(--ol-line-strong)',
fontFamily: 'var(--ol-font-mono)', color: 'var(--ol-ink)',
boxShadow: '0 1px 0 rgba(0,0,0,.04)',
}}>{getHotkeyTriggerLabel(hotkey?.trigger)}</kbd>
<span style={{ color: 'var(--ol-ink-4)' }}>{t('shell.shortcutHint')}</span>
</div>
</div>
{/* BETA 区域 — 去掉描边和实色背景,让它和底部 footer 一起浮在磨砂玻璃上 */} {/* BETA 区域 — 去掉描边和实色背景,让它和底部 footer 一起浮在磨砂玻璃上 */}
<div style={{ marginTop: 8, padding: '10px 10px 4px' }}> <div style={{ marginTop: 8, padding: '10px 10px 4px' }}>
<div style={{ fontSize: 10.5, fontWeight: 600, color: 'var(--ol-blue)', letterSpacing: '0.04em', textTransform: 'uppercase' }}>{t('shell.betaTag')}</div> <div style={{ fontSize: 10.5, fontWeight: 600, color: 'var(--ol-blue)', letterSpacing: '0.04em', textTransform: 'uppercase' }}>{t('shell.betaTag')}</div>
+3 -3
View File
@@ -200,13 +200,13 @@ export const en: typeof zhCN = {
title: 'How to use', title: 'How to use',
step1: 'Place the text cursor in another app (Notes, mail, chat — anything with a text field).', step1: 'Place the text cursor in another app (Notes, mail, chat — anything with a text field).',
step2: 'Press the recording hotkey (currently {{trigger}}) to start dictation.', step2: 'Press the recording hotkey (currently {{trigger}}) to start dictation.',
step3: 'Press Shift any time during the recording — once is enough, no need to hold it. You can press it before you start talking, mid-sentence, or right before stopping.', step3: 'Press Shift once any time during recording. You can press it before speaking, mid-sentence, or right before stopping.',
step4: 'Press the recording hotkey again to stop.', step4: 'Press the recording hotkey again to stop.',
step5: 'The transcript is sent to the LLM, translated into the target language above, then inserted at the original cursor position.', step5: 'The transcript is sent to the LLM, translated into the target language above, then inserted at the original cursor position.',
indicatorTitle: 'How to confirm translation mode is on', indicatorTitle: 'How to confirm translation mode is on',
indicatorDesc: 'The moment you press Shift, a blue "● Translating" pill floats above the recording capsule at the bottom of the screen. It stays visible until insertion completes, so you always know this run goes through the translation pipeline.', indicatorDesc: 'The moment you press Shift, a blue Translating indicator appears above the recording capsule and stays visible until insertion completes.',
fallbackTitle: 'Safety fallbacks', fallbackTitle: 'Safety fallbacks',
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.', fallbackDesc: 'When the target is Disabled, Shift does not trigger translation. If the LLM call fails, the raw transcript is inserted so nothing gets lost.',
}, },
}, },
selectionAsk: { selectionAsk: {
+5 -5
View File
@@ -197,14 +197,14 @@ export const zhCN = {
howto: { howto: {
title: '使用方法', title: '使用方法',
step1: '在另一个 app 的输入框里聚焦光标(备忘录、邮件、聊天窗口都行)。', step1: '在另一个 app 的输入框里聚焦光标(备忘录、邮件、聊天窗口都行)。',
step2: '按一下"录音快捷键"(当前是 {{trigger}}),开始录音。', step2: '按一下录音快捷键(当前:{{trigger}})开始录音。',
step3: '在录音过程中任意时刻按一下 Shift——按一下即可,不需要按住,可以在开口前、说到一半、快说完时按。', step3: '在录音过程中任意时刻按一下 Shift 即可,不需要按住;可以在开口前、说到一半或快结束时按。',
step4: '再按一下"录音快捷键"停止录音。', step4: '再按一次录音快捷键停止录音。',
step5: '系统会把转写交给大模型翻译成上面选的目标语言,然后插入到一开始那个输入框光标位置。', step5: '系统会把转写交给大模型翻译成上面选的目标语言,然后插入到一开始那个输入框光标位置。',
indicatorTitle: '怎么知道翻译模式生效了', indicatorTitle: '怎么知道翻译模式生效了',
indicatorDesc: '一旦按下 Shift,屏幕底部录音胶囊的上方会立刻悬浮一个蓝色"● 正在翻译"小药丸——它会一直显示到本次插入完成,让你确认这次输出会走翻译管线。', indicatorDesc: '一旦按下 Shift,屏幕底部录音胶囊上方会出现蓝色「正在翻译」提示,并持续到本次插入完成。',
fallbackTitle: '安全兜底', fallbackTitle: '安全兜底',
fallbackDesc: '翻译模式选「不启用」时 Shift 是没作用的;翻译过程中如果大模型调用失败,会回退到把原始中文转写直接插入,不会丢字。详见 issue #4。', fallbackDesc: '翻译模式选「不启用」时 Shift 不会触发翻译;如果大模型调用失败,会回退插入原始转写,不会丢字。',
}, },
}, },
selectionAsk: { selectionAsk: {
-23
View File
@@ -70,29 +70,6 @@ export function Overview({ onOpenHistory }: OverviewProps) {
kicker={t('overview.kicker')} kicker={t('overview.kicker')}
title={t('overview.title')} title={t('overview.title')}
desc={t('overview.desc')} desc={t('overview.desc')}
right={
<div
style={{
display: 'inline-flex', alignItems: 'center', gap: 8,
padding: '6px 12px',
borderRadius: 999,
border: '0.5px solid var(--ol-line-strong)',
background: 'var(--ol-surface-2)',
color: 'var(--ol-ink-3)',
fontSize: 12,
}}
>
<Icon name="cmd" size={12} />
{t('overview.pressPrefix')}
<kbd style={{
padding: '2px 7px', fontSize: 11, fontFamily: 'var(--ol-font-mono)',
background: '#fff', borderRadius: 5,
border: '0.5px solid var(--ol-line-strong)',
color: 'var(--ol-ink)',
}}>{getHotkeyTriggerLabel(hotkey?.trigger)}</kbd>
{t('overview.pressSuffix')}
</div>
}
/> />
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 18 }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 18 }}>
+183 -103
View File
@@ -4,8 +4,9 @@
// - 选一个翻译目标语言(单选;选"不启用"则 Shift 不触发翻译) // - 选一个翻译目标语言(单选;选"不启用"则 Shift 不触发翻译)
// - 看完整使用说明(怎么触发、按钮位置、胶囊显示) // - 看完整使用说明(怎么触发、按钮位置、胶囊显示)
import type { CSSProperties } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Card, PageHeader } from './_atoms'; import { Card, PageHeader, Pill } from './_atoms';
import { SUPPORTED_LANGUAGES } from '../lib/types'; import { SUPPORTED_LANGUAGES } from '../lib/types';
import { useHotkeySettings } from '../state/HotkeySettingsContext'; import { useHotkeySettings } from '../state/HotkeySettingsContext';
import { getHotkeyTriggerLabel } from '../lib/hotkey'; import { getHotkeyTriggerLabel } from '../lib/hotkey';
@@ -42,6 +43,13 @@ export function Translation() {
const triggerLabel = getHotkeyTriggerLabel(hotkey?.trigger); const triggerLabel = getHotkeyTriggerLabel(hotkey?.trigger);
const enabled = prefs.translationTargetLanguage.trim() !== ''; const enabled = prefs.translationTargetLanguage.trim() !== '';
const howtoSteps = [
t('translation.howto.step1'),
t('translation.howto.step2', { trigger: triggerLabel }),
t('translation.howto.step3'),
t('translation.howto.step4'),
t('translation.howto.step5'),
];
return ( return (
<> <>
@@ -52,32 +60,55 @@ export function Translation() {
/> />
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{/* 1. 翻译目标语言 */}
{/* 1. 工作语言 */}
<Card> <Card>
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 4 }}>{t('translation.working.title')}</div> <div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) 220px', gap: 18, alignItems: 'stretch' }}>
<div style={{ fontSize: 11.5, color: 'var(--ol-ink-4)', marginBottom: 12, lineHeight: 1.55 }}> <div>
{t('translation.working.desc')} <div style={settingsHeaderStyle}>
<div style={settingsTitleStyle}>{t('translation.target.title')}</div>
<Pill tone={enabled ? 'blue' : 'outline'} size="sm">
{enabled ? t('translation.statusEnabled') : t('translation.statusDisabled')}
</Pill>
</div>
<div style={settingsDescStyle}>{t('translation.target.desc')}</div>
<select
value={prefs.translationTargetLanguage}
onChange={e => onTargetChange(e.target.value)}
style={selectStyle}
>
<option value="">{t('translation.target.disabled')}</option>
{SUPPORTED_LANGUAGES.map(lang => (
<option key={lang} value={lang}>{lang}</option>
))}
</select>
</div>
<div style={translationStatusStyle(enabled)}>
<div style={{ fontSize: 11, color: enabled ? 'var(--ol-blue)' : 'var(--ol-ink-4)', fontWeight: 600, letterSpacing: '0.06em', textTransform: 'uppercase' }}>
{enabled ? t('translation.statusEnabled') : t('translation.statusDisabled')}
</div>
<div style={{ marginTop: 10, fontSize: 22, lineHeight: 1.1, fontWeight: 650, letterSpacing: '-0.03em', color: enabled ? 'var(--ol-blue)' : 'var(--ol-ink-2)' }}>
{enabled ? prefs.translationTargetLanguage : t('translation.statusDisabled')}
</div>
<div style={{ marginTop: 8, fontSize: 11.5, color: 'var(--ol-ink-4)', lineHeight: 1.5 }}>
Shift
</div>
</div>
</div> </div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}> </Card>
{/* 2. 工作语言 */}
<Card>
<div style={settingsTitleStyle}>{t('translation.working.title')}</div>
<div style={settingsDescStyle}>{t('translation.working.desc')}</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 7 }}>
{SUPPORTED_LANGUAGES.map(lang => { {SUPPORTED_LANGUAGES.map(lang => {
const checked = prefs.workingLanguages.includes(lang); const checked = prefs.workingLanguages.includes(lang);
return ( return (
<button <button
key={lang} key={lang}
onClick={() => toggleWorkingLanguage(lang)} onClick={() => toggleWorkingLanguage(lang)}
style={{ style={languageChipStyle(checked)}
padding: '6px 12px',
fontSize: 12.5,
fontWeight: checked ? 600 : 500,
border: 0,
borderRadius: 999,
background: checked ? 'var(--ol-blue)' : 'rgba(0,0,0,0.05)',
color: checked ? '#fff' : 'var(--ol-ink-2)',
cursor: 'default',
fontFamily: 'inherit',
transition: 'background 0.16s var(--ol-motion-quick), color 0.16s var(--ol-motion-quick)',
}}
> >
{lang} {lang}
</button> </button>
@@ -86,95 +117,144 @@ export function Translation() {
</div> </div>
</Card> </Card>
{/* 2. 翻译目标语言 */}
<Card>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
<div style={{ fontSize: 13, fontWeight: 600 }}>{t('translation.target.title')}</div>
<span
style={{
padding: '2px 8px',
fontSize: 10.5,
fontWeight: 600,
letterSpacing: '0.04em',
borderRadius: 999,
background: enabled ? 'rgba(37,99,235,0.10)' : 'rgba(0,0,0,0.05)',
color: enabled ? 'var(--ol-blue)' : 'var(--ol-ink-4)',
textTransform: 'uppercase',
}}
>
{enabled ? t('translation.statusEnabled') : t('translation.statusDisabled')}
</span>
</div>
<div style={{ fontSize: 11.5, color: 'var(--ol-ink-4)', marginBottom: 12, lineHeight: 1.55 }}>
{t('translation.target.desc')}
</div>
<select
value={prefs.translationTargetLanguage}
onChange={e => onTargetChange(e.target.value)}
style={{
width: '100%',
maxWidth: 360,
height: 32,
padding: '0 10px',
fontSize: 13,
border: '0.5px solid var(--ol-line-strong)',
borderRadius: 8,
background: '#fff',
color: 'var(--ol-ink)',
fontFamily: 'inherit',
cursor: 'default',
}}
>
<option value="">{t('translation.target.disabled')}</option>
{SUPPORTED_LANGUAGES.map(lang => (
<option key={lang} value={lang}>{lang}</option>
))}
</select>
</Card>
{/* 3. 使用方法 */} {/* 3. 使用方法 */}
<Card> <Card>
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 10 }}>{t('translation.howto.title')}</div> <div style={settingsTitleStyle}>{t('translation.howto.title')}</div>
<ol style={{ margin: 0, paddingLeft: 18, fontSize: 12.5, color: 'var(--ol-ink-2)', lineHeight: 1.7 }}> <div style={{ display: 'grid', gap: 8, marginTop: 12 }}>
<li>{t('translation.howto.step1', { trigger: triggerLabel })}</li> {howtoSteps.map((step, index) => (
<li>{t('translation.howto.step2')}</li> <div key={step} style={stepRowStyle}>
<li>{t('translation.howto.step3')}</li> <span style={stepNumberStyle}>{String(index + 1).padStart(2, '0')}</span>
<li>{t('translation.howto.step4')}</li> <span style={{ minWidth: 0 }}>{step}</span>
<li>{t('translation.howto.step5')}</li> </div>
</ol> ))}
<div
style={{
marginTop: 14,
padding: '10px 12px',
borderRadius: 10,
background: 'rgba(37,99,235,0.06)',
border: '0.5px solid rgba(37,99,235,0.15)',
fontSize: 11.5,
color: 'var(--ol-ink-2)',
lineHeight: 1.55,
}}
>
<div style={{ fontWeight: 600, color: 'var(--ol-blue)', marginBottom: 4 }}>{t('translation.howto.indicatorTitle')}</div>
{t('translation.howto.indicatorDesc')}
</div>
<div
style={{
marginTop: 10,
padding: '10px 12px',
borderRadius: 10,
background: 'rgba(0,0,0,0.04)',
fontSize: 11.5,
color: 'var(--ol-ink-3)',
lineHeight: 1.55,
}}
>
<div style={{ fontWeight: 600, color: 'var(--ol-ink-2)', marginBottom: 4 }}>{t('translation.howto.fallbackTitle')}</div>
{t('translation.howto.fallbackDesc')}
</div> </div>
</Card> </Card>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<InfoTile tone="blue" title={t('translation.howto.indicatorTitle')} body={t('translation.howto.indicatorDesc')} />
<InfoTile tone="neutral" title={t('translation.howto.fallbackTitle')} body={t('translation.howto.fallbackDesc')} />
</div>
</div> </div>
</> </>
); );
} }
const settingsHeaderStyle: CSSProperties = {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 12,
marginBottom: 5,
};
const settingsTitleStyle: CSSProperties = {
fontSize: 13,
fontWeight: 650,
color: 'var(--ol-ink)',
};
const settingsDescStyle: CSSProperties = {
fontSize: 11.5,
color: 'var(--ol-ink-4)',
marginBottom: 14,
lineHeight: 1.58,
};
const selectStyle: CSSProperties = {
width: '100%',
maxWidth: 380,
height: 34,
padding: '0 11px',
fontSize: 13,
border: '0.5px solid var(--ol-line-strong)',
borderRadius: 9,
background: '#fff',
color: 'var(--ol-ink)',
fontFamily: 'inherit',
cursor: 'default',
boxShadow: '0 1px 0 rgba(255,255,255,0.9) inset',
};
const stepRowStyle: CSSProperties = {
display: 'grid',
gridTemplateColumns: '34px minmax(0, 1fr)',
gap: 10,
alignItems: 'start',
padding: '9px 10px',
borderRadius: 11,
background: 'rgba(0,0,0,0.025)',
color: 'var(--ol-ink-2)',
fontSize: 12.5,
lineHeight: 1.55,
};
const stepNumberStyle: CSSProperties = {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: 24,
height: 24,
borderRadius: 999,
background: 'rgba(255,255,255,0.85)',
color: 'var(--ol-ink-4)',
fontFamily: 'var(--ol-font-mono)',
fontSize: 10.5,
fontWeight: 600,
boxShadow: '0 1px 2px rgba(0,0,0,0.04), 0 0 0 0.5px var(--ol-line)',
};
function languageChipStyle(checked: boolean): CSSProperties {
return {
padding: '6px 12px',
fontSize: 12.5,
fontWeight: checked ? 650 : 500,
border: checked ? '0.5px solid rgba(37,99,235,0.35)' : '0.5px solid var(--ol-line)',
borderRadius: 999,
background: checked ? 'var(--ol-blue)' : 'rgba(255,255,255,0.72)',
color: checked ? '#fff' : 'var(--ol-ink-2)',
cursor: 'default',
fontFamily: 'inherit',
boxShadow: checked ? '0 8px 18px -12px rgba(37,99,235,0.55)' : '0 1px 0 rgba(255,255,255,0.8) inset',
transition: 'background 0.16s var(--ol-motion-quick), color 0.16s var(--ol-motion-quick), border-color 0.16s var(--ol-motion-quick), box-shadow 0.18s var(--ol-motion-soft)',
};
}
function translationStatusStyle(enabled: boolean): CSSProperties {
return {
minHeight: 108,
borderRadius: 14,
padding: 14,
background: enabled
? 'linear-gradient(135deg, rgba(37,99,235,0.12), rgba(37,99,235,0.04))'
: 'linear-gradient(135deg, rgba(0,0,0,0.045), rgba(255,255,255,0.55))',
border: enabled ? '0.5px solid rgba(37,99,235,0.16)' : '0.5px solid var(--ol-line)',
boxShadow: '0 1px 0 rgba(255,255,255,0.85) inset',
};
}
function InfoTile({ tone, title, body }: { tone: 'blue' | 'neutral'; title: string; body: string }) {
const blue = tone === 'blue';
return (
<Card
padding={14}
style={{
background: blue ? 'rgba(37,99,235,0.055)' : 'rgba(0,0,0,0.025)',
border: blue ? '0.5px solid rgba(37,99,235,0.14)' : '0.5px solid var(--ol-line)',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
<span
style={{
width: 7,
height: 7,
borderRadius: 999,
background: blue ? 'var(--ol-blue)' : 'var(--ol-ink-4)',
boxShadow: blue ? '0 0 0 4px rgba(37,99,235,0.10)' : '0 0 0 4px rgba(0,0,0,0.045)',
}}
/>
<div style={{ fontSize: 12.5, fontWeight: 650, color: blue ? 'var(--ol-blue)' : 'var(--ol-ink-2)' }}>{title}</div>
</div>
<div style={{ fontSize: 11.5, color: 'var(--ol-ink-3)', lineHeight: 1.58 }}>{body}</div>
</Card>
);
}