diff --git a/openless-all/app/src/components/FloatingShell.tsx b/openless-all/app/src/components/FloatingShell.tsx index 452df961..bfb082e2 100644 --- a/openless-all/app/src/components/FloatingShell.tsx +++ b/openless-all/app/src/components/FloatingShell.tsx @@ -22,16 +22,13 @@ import { HOTKEY_MODE_MIGRATION_DEFERRED_KEY, shouldShowHotkeyModeMigrationPrompt, } from '../lib/hotkeyMigration'; -import { getHotkeyTriggerLabel } from '../lib/hotkey'; import { applyFontScale, readFontScale } from '../lib/fontScale'; import { getCredentials, openExternal } from '../lib/ipc'; -import { OL_DATA } from '../lib/mockData'; import { PROVIDER_SETUP_PROMPT_DEFERRED_KEY, shouldShowProviderSetupPrompt, } from '../lib/providerSetup'; import type { SettingsSectionId } from '../pages/Settings'; -import { useHotkeySettings } from '../state/HotkeySettingsContext'; import { useAppState, type AppTab } from '../state/useAppState'; interface NavItem { @@ -75,7 +72,6 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia const [providerPromptOpen, setProviderPromptOpen] = useState(false); const [hotkeyModePromptOpen, setHotkeyModePromptOpen] = useState(false); const [helpPopoverOpen, setHelpPopoverOpen] = useState(false); - const { hotkey } = useHotkeySettings(); // 字体档位 — 启动时按 localStorage 应用一次;之后改动来自 Settings 的"个性化"section。 useEffect(() => { @@ -212,12 +208,6 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia {n.name} - {n.id === 'history' && - {OL_DATA.history.length} - } ); })} @@ -225,21 +215,6 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
- {/* shortcut hint — 不要 dashed 边框,否则会切断"整片磨砂玻璃"的视觉 */} -
-
{t('shell.shortcutLabel')}
-
- {getHotkeyTriggerLabel(hotkey?.trigger)} - {t('shell.shortcutHint')} -
-
- {/* BETA 区域 — 去掉描边和实色背景,让它和底部 footer 一起浮在磨砂玻璃上 */}
{t('shell.betaTag')}
diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index 4b17addc..f979dd5f 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -200,13 +200,13 @@ export const en: typeof zhCN = { title: 'How to use', 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.', - 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.', 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', - 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', - 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: { diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index b08578c2..b6de7a05 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -197,14 +197,14 @@ export const zhCN = { howto: { title: '使用方法', step1: '在另一个 app 的输入框里聚焦光标(备忘录、邮件、聊天窗口都行)。', - step2: '按一下"录音快捷键"(当前是 {{trigger}}),开始录音。', - step3: '在录音过程中任意时刻按一下 Shift——按一下即可,不需要按住,可以在开口前、说到一半、快说完时按。', - step4: '再按一下"录音快捷键"停止录音。', + step2: '按一下录音快捷键(当前:{{trigger}})开始录音。', + step3: '在录音过程中任意时刻按一下 Shift 即可,不需要按住;可以在开口前、说到一半或快结束时按。', + step4: '再按一次录音快捷键停止录音。', step5: '系统会把转写交给大模型翻译成上面选的目标语言,然后插入到一开始那个输入框光标位置。', indicatorTitle: '怎么知道翻译模式生效了', - indicatorDesc: '一旦按下 Shift,屏幕底部录音胶囊的上方会立刻悬浮一个蓝色"● 正在翻译"小药丸——它会一直显示到本次插入完成,让你确认这次输出会走翻译管线。', + indicatorDesc: '一旦按下 Shift,屏幕底部录音胶囊上方会出现蓝色「正在翻译」提示,并持续到本次插入完成。', fallbackTitle: '安全兜底', - fallbackDesc: '翻译模式选「不启用」时 Shift 是没作用的;翻译过程中如果大模型调用失败,会回退到把原始中文转写直接插入,不会丢字。详见 issue #4。', + fallbackDesc: '翻译模式选「不启用」时 Shift 不会触发翻译;如果大模型调用失败,会回退插入原始转写,不会丢字。', }, }, selectionAsk: { diff --git a/openless-all/app/src/pages/Overview.tsx b/openless-all/app/src/pages/Overview.tsx index fbaa240c..32ac32f0 100644 --- a/openless-all/app/src/pages/Overview.tsx +++ b/openless-all/app/src/pages/Overview.tsx @@ -70,29 +70,6 @@ export function Overview({ onOpenHistory }: OverviewProps) { kicker={t('overview.kicker')} title={t('overview.title')} desc={t('overview.desc')} - right={ -
- - {t('overview.pressPrefix')} - {getHotkeyTriggerLabel(hotkey?.trigger)} - {t('overview.pressSuffix')} -
- } />
diff --git a/openless-all/app/src/pages/Translation.tsx b/openless-all/app/src/pages/Translation.tsx index d08eb7f5..a423f256 100644 --- a/openless-all/app/src/pages/Translation.tsx +++ b/openless-all/app/src/pages/Translation.tsx @@ -4,8 +4,9 @@ // - 选一个翻译目标语言(单选;选"不启用"则 Shift 不触发翻译) // - 看完整使用说明(怎么触发、按钮位置、胶囊显示) +import type { CSSProperties } from 'react'; import { useTranslation } from 'react-i18next'; -import { Card, PageHeader } from './_atoms'; +import { Card, PageHeader, Pill } from './_atoms'; import { SUPPORTED_LANGUAGES } from '../lib/types'; import { useHotkeySettings } from '../state/HotkeySettingsContext'; import { getHotkeyTriggerLabel } from '../lib/hotkey'; @@ -42,6 +43,13 @@ export function Translation() { const triggerLabel = getHotkeyTriggerLabel(hotkey?.trigger); 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 ( <> @@ -52,32 +60,55 @@ export function Translation() { />
- - {/* 1. 工作语言 */} + {/* 1. 翻译目标语言 */} -
{t('translation.working.title')}
-
- {t('translation.working.desc')} +
+
+
+
{t('translation.target.title')}
+ + {enabled ? t('translation.statusEnabled') : t('translation.statusDisabled')} + +
+
{t('translation.target.desc')}
+ +
+ +
+
+ {enabled ? t('translation.statusEnabled') : t('translation.statusDisabled')} +
+
+ {enabled ? prefs.translationTargetLanguage : t('translation.statusDisabled')} +
+
+ Shift +
+
-
+ + + {/* 2. 工作语言 */} + +
{t('translation.working.title')}
+
{t('translation.working.desc')}
+
{SUPPORTED_LANGUAGES.map(lang => { const checked = prefs.workingLanguages.includes(lang); return ( @@ -86,95 +117,144 @@ export function Translation() {
- {/* 2. 翻译目标语言 */} - -
-
{t('translation.target.title')}
- - {enabled ? t('translation.statusEnabled') : t('translation.statusDisabled')} - -
-
- {t('translation.target.desc')} -
- -
- {/* 3. 使用方法 */} -
{t('translation.howto.title')}
-
    -
  1. {t('translation.howto.step1', { trigger: triggerLabel })}
  2. -
  3. {t('translation.howto.step2')}
  4. -
  5. {t('translation.howto.step3')}
  6. -
  7. {t('translation.howto.step4')}
  8. -
  9. {t('translation.howto.step5')}
  10. -
- -
-
{t('translation.howto.indicatorTitle')}
- {t('translation.howto.indicatorDesc')} -
- -
-
{t('translation.howto.fallbackTitle')}
- {t('translation.howto.fallbackDesc')} +
{t('translation.howto.title')}
+
+ {howtoSteps.map((step, index) => ( +
+ {String(index + 1).padStart(2, '0')} + {step} +
+ ))}
+ +
+ + +
); } + +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 ( + +
+ +
{title}
+
+
{body}
+
+ ); +}