From 07ba8c58337a551b8678efcd3233a2c0e62d0f3c Mon Sep 17 00:00:00 2001 From: Ethan Date: Sat, 11 Jul 2026 00:40:10 +0800 Subject: [PATCH 1/2] docs: define privacy data layout redesign --- PRODUCT.md | 30 +++++ .../plans/2026-07-11-privacy-data-layout.md | 103 ++++++++++++++++++ .../2026-07-11-privacy-data-layout-design.md | 48 ++++++++ 3 files changed, 181 insertions(+) create mode 100644 PRODUCT.md create mode 100644 docs/superpowers/plans/2026-07-11-privacy-data-layout.md create mode 100644 docs/superpowers/specs/2026-07-11-privacy-data-layout-design.md diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 0000000..7f3581f --- /dev/null +++ b/PRODUCT.md @@ -0,0 +1,30 @@ +# Whisper Input Product Context + +## Product Register + +- Register: Product UI +- Platform: Windows desktop application built with Tauri and React +- Primary users: Chinese-speaking workplace users who use voice input for daily writing and question answering +- Core purpose: turn speech into text, optionally polish or structure it with a configured LLM, and insert the result into the active application + +## Design Direction + +- Personality: quiet, trustworthy, efficient, and familiar +- Density: compact enough for a 1240 x 800 desktop window without sacrificing readable type or 40 px control targets +- Hierarchy: use spacing, alignment, typography, and lightweight separators before borders or shadows +- Component language: preserve the existing sidebar, settings tabs, cards, buttons, toggles, icon system, tokens, and Chinese/English localization +- Motion: restrained and functional; respect reduced-motion preferences + +## Current Iteration + +- Scope: Settings > Privacy & Data only +- Selected reference: the third generated layout shown on 2026-07-11 +- Goal: make all privacy explanations and five existing controls visible in the standard desktop viewport +- Functional constraint: do not change copy, handlers, confirmation dialogs, persistence, API keys, provider configuration behavior, or diagnostic export behavior +- Responsive constraint: preserve the compact desktop composition and fall back to a single column when the content area becomes narrow + +## Anti-References + +- Avoid one oversized vertical list with large empty areas +- Avoid nested cards, heavy borders, decorative shadows, loud danger styling, and unnecessary new labels +- Avoid new icons, features, routes, buttons, or rewritten explanatory text diff --git a/docs/superpowers/plans/2026-07-11-privacy-data-layout.md b/docs/superpowers/plans/2026-07-11-privacy-data-layout.md new file mode 100644 index 0000000..788a2b1 --- /dev/null +++ b/docs/superpowers/plans/2026-07-11-privacy-data-layout.md @@ -0,0 +1,103 @@ +# Privacy And Data Layout Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Rebuild the Privacy & Data settings surface to match the selected compact data-lifecycle layout without changing any behavior or copy. + +**Architecture:** Keep `PrivacySection` and all of its existing handlers in `Settings.tsx`. Replace only its JSX grouping with semantic layout classes, then implement the visual hierarchy in the existing replica stylesheet. Extend the existing source-contract test so future edits cannot silently remove the approved groups or restore the oversized privacy card width. + +**Tech Stack:** React 18, TypeScript, Vite, Tauri 2, Node built-in test runner, existing CSS tokens and UI components. + +## Global Constraints + +- Change only the Privacy & Data page structure and visual styling. +- Preserve every existing label, description, button, toggle, handler, confirmation, IPC call, busy state, and notification. +- Do not add dependencies, icons, routes, API calls, or new settings. +- Fit the standard 1240 x 800 desktop window without page scrolling. +- Keep a responsive single-column fallback for narrow content areas. + +--- + +### Task 1: Privacy Layout Contract + +**Files:** +- Modify: `src/lib/frontendReplicaContract.test.ts` +- Test: `src/lib/frontendReplicaContract.test.ts` + +**Interfaces:** +- Consumes: source text from `src/pages/Settings.tsx` and `src/styles/preview-replica.css` +- Produces: regression assertions for `wi-privacy-data-flow`, `wi-privacy-retention`, `wi-privacy-maintenance`, and the responsive privacy layout + +- [ ] **Step 1: Write the failing test** + +Add assertions that require the three approved semantic groups and their responsive CSS selectors. + +- [ ] **Step 2: Run the focused test and verify failure** + +Run: `node --test src/lib/frontendReplicaContract.test.ts` + +Expected: FAIL because the new privacy layout classes do not exist yet. + +- [ ] **Step 3: Keep the failing output as the implementation baseline** + +Confirm the failure names the missing privacy layout class rather than an unrelated test or runtime error. + +### Task 2: Selected Layout Implementation + +**Files:** +- Modify: `src/pages/Settings.tsx` +- Modify: `src/styles/preview-replica.css` +- Test: `src/lib/frontendReplicaContract.test.ts` + +**Interfaces:** +- Consumes: existing `Card`, `Toggle`, `Btn`, translations, handlers, and `privacyHelpItemsCopy` +- Produces: the compact privacy information band and grouped retention/maintenance actions + +- [ ] **Step 1: Restructure only the `PrivacySection` JSX** + +Wrap the existing help items in `wi-privacy-data-flow`, the history toggle and local clear actions in `wi-privacy-retention`, and configuration/diagnostics actions in `wi-privacy-maintenance`. Keep each existing control expression and callback unchanged. + +- [ ] **Step 2: Add the layout CSS** + +Use compact grid rows, lightweight separators, right-aligned controls, and a narrow-width media/container fallback. Remove the privacy card's restrictive 860 px width so the selected layout can use the available content area. + +- [ ] **Step 3: Run the focused test and verify it passes** + +Run: `node --test src/lib/frontendReplicaContract.test.ts` + +Expected: all source-contract tests pass. + +- [ ] **Step 4: Run production compilation** + +Run: `npm run build` + +Expected: TypeScript and Vite complete without errors. + +### Task 3: Desktop Runtime And Visual QA + +**Files:** +- Modify: `design-qa.md` + +**Interfaces:** +- Consumes: selected generated option 3 and the locally running Tauri application +- Produces: a same-state visual comparison and final QA result + +- [ ] **Step 1: Restart the Tauri desktop application** + +Stop only the running `src-tauri\\target\\debug\\whisper-input.exe` process and relaunch the existing Tauri dev command so current local configuration remains in use. + +- [ ] **Step 2: Capture the Privacy & Data page at 1240 x 800** + +Open Settings, select Privacy & Data, and capture the running desktop window in the same state as the selected reference. + +- [ ] **Step 3: Compare the selected reference and implementation together** + +Check content fit, hierarchy, spacing, row alignment, button visibility, clipping, borders, and scrollbar state. Fix P0-P2 issues and repeat the capture. + +- [ ] **Step 4: Record the result** + +Update `design-qa.md` with the reference path, implementation capture path, findings table, and the exact line `final result: passed` only after all blocking issues are resolved. + +- [ ] **Step 5: Keep the local application running for user inspection** + +Report the running executable path and local frontend URL used by the desktop application. diff --git a/docs/superpowers/specs/2026-07-11-privacy-data-layout-design.md b/docs/superpowers/specs/2026-07-11-privacy-data-layout-design.md new file mode 100644 index 0000000..8e5a0fc --- /dev/null +++ b/docs/superpowers/specs/2026-07-11-privacy-data-layout-design.md @@ -0,0 +1,48 @@ +# Privacy And Data Layout Design + +## Scope + +Redesign only the visual structure of `Settings > Privacy & Data`. Preserve all existing controls, labels, descriptions, state, async behavior, confirmations, IPC calls, and notification behavior. + +## Approved Direction + +The user selected the third generated concept: a clear data-lifecycle layout that separates privacy information, local retention, and maintenance actions while fitting the standard 1240 x 800 desktop window. + +## Information Architecture + +1. Keep the existing settings navigation and selected Privacy & Data tab unchanged. +2. Present the existing audio, recognized text, and local data explanations as a compact three-row information band. +3. Present local retention actions in one grouped surface: + - Save history, with the existing toggle. + - Clear history, with the existing button. + - Clear vocabulary, with the existing button. +4. Present maintenance actions in a second grouped surface: + - Clear configuration, with the existing button. + - Export diagnostic bundle, with the existing button. + +## Visual Rules + +- Use the existing design tokens, components, typography, and icon system. +- Use spacing, alignment, subtle surface tint, and row separators before borders and shadows. +- Keep body text at the existing readable product scale. +- Keep interactive targets at least 40 px high. +- Keep button alignment consistent on the right side of each action row. +- Avoid nested cards and avoid introducing new icons or decorative assets. +- At narrow content widths, switch grouped action rows to a single-column layout without clipping text or controls. + +## Functional Invariants + +- `onHistoryEnabledChange` continues to call `updatePrefs` and refresh on failure. +- Clear-history, clear-vocabulary, and clear-configuration confirmations remain unchanged. +- `runAction`, busy states, saved notifications, and failure notifications remain unchanged. +- Diagnostic filename generation and `exportDiagnosticBundle` invocation remain unchanged. +- Existing localization keys and `privacyHelpItemsCopy` content remain unchanged. + +## Acceptance Criteria + +- All three privacy explanations and all five controls are visible at 1240 x 800 without page scrolling. +- Existing actions remain keyboard reachable and retain their disabled/busy states. +- The page has no new visible heavy border or nested-card treatment. +- The layout remains usable at a narrower desktop content width through a responsive single-column fallback. +- TypeScript tests and the production build pass. +- Same-viewport visual QA compares the selected generated reference with the running desktop application and records `final result: passed` in `design-qa.md`. From ae9b33741bc2de53f96b2540f805e51b149c38ad Mon Sep 17 00:00:00 2001 From: Ethan Date: Sat, 11 Jul 2026 09:09:15 +0800 Subject: [PATCH 2/2] feat: polish settings layouts for v1.3.27 --- package-lock.json | 4 +- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 6 +- src/components/WindowChrome.tsx | 4 +- src/components/shell/PageContainer.tsx | 3 +- src/components/ui/Row.tsx | 2 +- src/components/ui/SettingRow.tsx | 2 +- src/lib/frontendReplicaContract.test.ts | 127 ++++- src/pages/Settings.tsx | 578 ++++++++++++--------- src/pages/Style.tsx | 42 +- src/pages/Vocab.tsx | 1 - src/styles/preview-replica.css | 660 +++++++++++++++++++++++- src/styles/tokens.css | 4 +- 15 files changed, 1133 insertions(+), 306 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4ccdefc..6db9d5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "whisper-input-app", - "version": "1.3.26", + "version": "1.3.27", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "whisper-input-app", - "version": "1.3.26", + "version": "1.3.27", "license": "Apache-2.0", "dependencies": { "@tauri-apps/api": "^2.1.1", diff --git a/package.json b/package.json index bcb09a1..8a1112d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "whisper-input-app", "private": true, - "version": "1.3.26", + "version": "1.3.27", "license": "Apache-2.0", "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4e984d2..7b737d7 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6840,7 +6840,7 @@ checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] name = "whisper-input" -version = "1.3.26" +version = "1.3.27" dependencies = [ "anyhow", "arboard", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 8b0cb7c..df1965e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "whisper-input" -version = "1.3.26" +version = "1.3.27" description = "Whisper Input - Windows AI voice input powered by cloud speech recognition and LLM polish" authors = ["Qingyu Input"] license = "Apache-2.0" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 229ede7..29ae108 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -2,7 +2,7 @@ "$schema": "https://schema.tauri.app/config/2", "productName": "Whisper Input", "mainBinaryName": "whisper-input", - "version": "1.3.26", + "version": "1.3.27", "identifier": "com.qingyu.input", "build": { "beforeDevCommand": "npm run dev", @@ -22,9 +22,9 @@ "minWidth": 980, "minHeight": 640, "resizable": true, - "decorations": true, + "decorations": false, "transparent": true, - "shadow": true, + "shadow": false, "hiddenTitle": true, "titleBarStyle": "Overlay", "trafficLightPosition": { "x": 14, "y": 20 }, diff --git a/src/components/WindowChrome.tsx b/src/components/WindowChrome.tsx index a537417..6aa0b88 100644 --- a/src/components/WindowChrome.tsx +++ b/src/components/WindowChrome.tsx @@ -61,9 +61,7 @@ export function WindowChrome({ height, position: 'relative', borderRadius: 'var(--ol-window-shell-radius)', - boxShadow: os === 'win' - ? '0 18px 42px -26px rgba(15, 17, 22, 0.42)' - : 'var(--ol-shadow-xl)', + boxShadow: os === 'win' ? 'none' : 'var(--ol-shadow-xl)', overflow: 'hidden', display: 'flex', flexDirection: 'column', diff --git a/src/components/shell/PageContainer.tsx b/src/components/shell/PageContainer.tsx index a37035d..96c124e 100644 --- a/src/components/shell/PageContainer.tsx +++ b/src/components/shell/PageContainer.tsx @@ -12,7 +12,8 @@ export function PageContainer({ children, phase }: PageContainerProps) { style={{ flex: 1, minHeight: 0, - overflow: 'hidden', + overflow: 'auto', + overflowX: 'hidden', padding: 0, position: 'relative', animation: phase === 'exiting' ? 'ol-page-fadeout 0.12s linear forwards' : undefined, diff --git a/src/components/ui/Row.tsx b/src/components/ui/Row.tsx index a272301..61f9363 100644 --- a/src/components/ui/Row.tsx +++ b/src/components/ui/Row.tsx @@ -13,7 +13,7 @@ export function Row({ label, desc, children }: RowProps) {
{label}
- {desc &&
{desc}
} + {desc &&
{desc}
}
{children}
diff --git a/src/components/ui/SettingRow.tsx b/src/components/ui/SettingRow.tsx index ed48bc5..c878500 100644 --- a/src/components/ui/SettingRow.tsx +++ b/src/components/ui/SettingRow.tsx @@ -26,7 +26,7 @@ export function SettingRow({ label, desc, children, controlWidth }: SettingRowPr {desc && (
{desc}
diff --git a/src/lib/frontendReplicaContract.test.ts b/src/lib/frontendReplicaContract.test.ts index 6624101..ce5218a 100644 --- a/src/lib/frontendReplicaContract.test.ts +++ b/src/lib/frontendReplicaContract.test.ts @@ -24,10 +24,29 @@ const iconSource = readFileSync(new URL('../components/Icon.tsx', import.meta.ur const sidebarSource = readFileSync(new URL('../components/shell/Sidebar.tsx', import.meta.url), 'utf8'); const commandsSource = readFileSync(new URL('../../src-tauri/src/commands.rs', import.meta.url), 'utf8'); const overviewSource = readFileSync(new URL('../pages/Overview.tsx', import.meta.url), 'utf8'); +const vocabSource = readFileSync(new URL('../pages/Vocab.tsx', import.meta.url), 'utf8'); +const styleSource = readFileSync(new URL('../pages/Style.tsx', import.meta.url), 'utf8'); const windowChromeSource = readFileSync(new URL('../components/WindowChrome.tsx', import.meta.url), 'utf8'); +const tauriConfig = JSON.parse( + readFileSync(new URL('../../src-tauri/tauri.conf.json', import.meta.url), 'utf8'), +) as { app: { windows: Array<{ label: string; decorations?: boolean; shadow?: boolean }> } }; const pageContainerSource = readFileSync(new URL('../components/shell/PageContainer.tsx', import.meta.url), 'utf8'); const previewCssSource = readFileSync(new URL('../styles/preview-replica.css', import.meta.url), 'utf8'); const ipcSource = readFileSync(new URL('../lib/ipc.ts', import.meta.url), 'utf8'); +const zhCnSource = readFileSync(new URL('../i18n/zh-CN.ts', import.meta.url), 'utf8'); + +const mainWindowConfig = tauriConfig.app.windows.find(window => window.label === 'main'); +assert( + mainWindowConfig?.decorations === false && mainWindowConfig.shadow === false, + 'Windows 主窗口使用自绘标题栏和缩放热区时必须关闭原生装饰与原生阴影,避免透明窗口出现黑色外边', +); + +assert( + windowChromeSource.includes("boxShadow: os === 'win' ? 'none'") && + windowChromeSource.includes("{os === 'win' && }") && + windowChromeSource.includes("{os === 'win' && }"), + 'Windows 自绘窗口必须去除外层深色阴影,并继续保留标题栏与八向缩放热区', +); assert( REQUIRED_APP_TABS.join(',') === 'overview,history,vocab,style,settings', @@ -147,11 +166,11 @@ assert( ); assert( - settingsSource.includes('wi-recording-settings-left') && - settingsSource.includes('wi-recording-settings-right') && - previewCssSource.includes('.wi-recording-settings-left,') && - previewCssSource.includes('flex-direction: column;'), - '录音与热键页必须使用左右两列独立堆叠,避免右侧启动卡片被左侧录音卡高度挤到下一行', + !settingsSource.includes('wi-recording-settings-left') && + !settingsSource.includes('wi-recording-settings-right') && + settingsSource.includes('wi-recording-settings-stream-startup') && + previewCssSource.includes('grid-template-areas:'), + '录音与热键页必须由同一个二维网格协调左右高度,不能恢复为彼此独立且底线不齐的两列堆叠', ); assert( @@ -181,8 +200,65 @@ assert( ); assert( - previewCssSource.includes('width: min(720px, calc(100% - 330px))'), - '设置二级 Tab 必须为右上工具区预留宽度,避免帮助/主题/语言按钮遮挡', + previewCssSource.includes('width: min(720px, 100%)'), + '设置二级 Tab 必须在窄窗口内自适应,不能依赖固定 330px 扣减', +); + +for (const privacyLayoutClass of [ + 'wi-privacy-data-flow', + 'wi-privacy-retention', + 'wi-privacy-maintenance', +]) { + assert( + settingsSource.includes(privacyLayoutClass), + `隐私与数据页必须保留方案 3 的语义分组 ${privacyLayoutClass}`, + ); +} + +assert( + previewCssSource.includes('.wi-privacy-action-row') && + previewCssSource.includes('@container (max-width: 700px)'), + '隐私与数据页必须使用紧凑操作行,并在窄内容区回退为单列布局', +); + +assert( + previewCssSource.includes('.wi-settings-panel-privacy > .ol-card') && + !previewCssSource.includes('width: min(860px, 100%)'), + '隐私与数据页必须使用完整可用宽度,不能恢复为导致纵向溢出的 860px 窄卡片', +); + +assert( + settingsSource.includes('wi-model-settings-compact') && + settingsSource.includes('wi-provider-inline-check') && + !settingsSource.includes('') && + previewCssSource.includes('.wi-model-settings-compact .wi-plan-card'), + '模型设置必须把 ASR/LLM 检查按钮并入对应快速配置行,并保持中等密度首屏布局', +); + +assert( + settingsSource.includes('wi-recording-settings-stream-startup') && + settingsSource.includes('wi-recording-settings-insert') && + settingsSource.includes('wi-recording-settings-history') && + settingsSource.includes('defaultOpen embedded') && + previewCssSource.includes('grid-template-areas:') && + previewCssSource.includes('"recording stream"') && + previewCssSource.includes('"insert history"'), + '录音与热键页必须使用两列共享行轨道,全部展开时上下卡片等高并在 1240x800 首屏完整显示', +); + +assert( + !vocabSource.includes('actions={') && + vocabSource.includes('refreshAll();') && + vocabSource.includes("listen('vocab:updated'") && + vocabSource.includes('await refresh();'), + '词汇表必须移除可见刷新按钮,同时保留初始加载、事件同步和增删后的自动刷新', +); + +assert( + styleSource.includes('wi-style-header-row') && + styleSource.includes('wi-style-master') && + !styleSource.includes('actions={\n
'), + '输出风格整体启用开关必须并入标题说明行,不能继续作为孤立的页头操作', ); assert( @@ -297,6 +373,43 @@ assert( 'PageContainer 不得叠加额外 padding;页面内边距由 preview shell 控制', ); +assert( + pageContainerSource.includes("overflow: 'auto'") && pageContainerSource.includes("overflowX: 'hidden'"), + 'PageContainer 必须允许窄窗口纵向滚动,同时禁止整页横向溢出', +); + +assert( + !previewCssSource.includes('min-width: 1180px') && + !previewCssSource.includes('min-height: 720px') && + previewCssSource.includes('@media (max-width: 1100px)'), + '应用壳层必须支持 Tauri 声明的 980x640 最小窗口,不能保留 1180x720 硬下限', +); + +assert( + settingsSource.includes('type="button"') && + settingsSource.includes('role="switch"') && + settingsSource.includes('aria-checked={on}') && + settingsSource.includes('aria-label={label}'), + '设置开关必须暴露 button 类型、switch 角色、选中状态和可访问名称', +); + +assert( + previewCssSource.includes('.wi-stage button:focus-visible') && + previewCssSource.includes('outline: 2px solid var(--wi-blue);'), + '所有可操作控件必须使用统一的蓝色键盘焦点环', +); + +const requiredStyleSamples = [ + "sample: '老板,那个项目验收我刚才说错了,不是周二,是周三下午两点。然后麻烦你看一下合同和付款节点。还有测试,这个地方要改一下。'", + "sample: '老板,那个项目验收时间是周三下午两点。麻烦你看一下合同和付款节点,还有测试,这个地方要改一下。'", + "sample: '老板:\\n1. 项目验收时间\\n1.1 周三下午两点。\\n2. 待办事项\\n2.1 麻烦你看一下合同和付款节点。\\n2.2 测试这个地方要改一下。'", + "sample: '老板您好:\\n\\n关于项目验收时间,此前信息有误,现更正为周三下午两点。烦请您查阅合同及付款节点。此外,测试部分需要调整。\\n\\n谢谢。'", +]; + +for (const sample of requiredStyleSamples) { + assert(zhCnSource.includes(sample), '输出风格示例文案和编号格式必须保持原版内容'); +} + const requiredCssTokens = [ `padding: ${PREVIEW_VISUAL_TOKENS.mainPadding}`, `padding: ${PREVIEW_VISUAL_TOKENS.sidebarPadding}`, diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 6328fcd..fe469bf 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -167,6 +167,7 @@ export function Settings({ embedded = false, initialSection = 'models' }: Settin /> )}
-
- -
{t('settings.recording.title')}
-
{t('settings.recording.desc')}
- {isHotkeyModeMigrationNoticeActive() && ( -
-
- {t('settings.recording.migrationNoticeTitle')} -
-
- {t('settings.recording.migrationNoticeDesc')} -
+ +
{t('settings.recording.title')}
+
{t('settings.recording.desc')}
+ {isHotkeyModeMigrationNoticeActive() && ( +
+
+ {t('settings.recording.migrationNoticeTitle')}
- )} - - { - await setDictationHotkey(binding); - await savePrefs({ ...prefs, dictationHotkey: binding }); - }} - /> - - -
- {choices.map(([v, l]) => ( - - ))} +
+ {t('settings.recording.migrationNoticeDesc')}
- - -
+
+ )} + + { + await setDictationHotkey(binding); + await savePrefs({ ...prefs, dictationHotkey: binding }); + }} + /> + + +
+ {choices.map(([v, l]) => ( - {!microphoneDevicesLoaded && ( -
{t('common.loading')}
- )} - {microphoneDevicesError && ( -
- {t('settings.recording.microphoneLoadError', { message: microphoneDevicesError })} -
- )} -
-
- {microphonePickerOpen && ( - setMicrophonePickerOpen(false)} - onRefresh={() => { - void loadMicrophoneDevices(); + ))} +
+
+ +
+ + {!microphoneDevicesLoaded && ( +
{t('common.loading')}
+ )} + {microphoneDevicesError && ( +
+ {t('settings.recording.microphoneLoadError', { message: microphoneDevicesError })} +
+ )} +
+
+ {microphonePickerOpen && ( + setMicrophonePickerOpen(false)} + onRefresh={() => void loadMicrophoneDevices()} + loading={!microphoneDevicesLoaded} + onSelect={onMicrophoneDeviceChange} + /> + )} + + + + + + +
+
+ {t(isLinux + ? 'settings.advanced.streamingInsertTitleLinux' + : 'settings.advanced.streamingInsertTitle')} +
+
+ {t('settings.advanced.streamingInsertDesc')} +
+ + - )} + - + - +
- - +
+
+ {t('settings.recording.startupGroupTitle')} +
+
+ {t('settings.recording.startupAtBootDesc')} +
+ + + + + {capability.statusHint && ( +
+ {capability.statusHint} +
+ )} +
+
+ + + - + {capability.adapter !== 'macEventTap' && ( )} + - + + - -
- -
- -
- {t(isLinux - ? 'settings.advanced.streamingInsertTitleLinux' - : 'settings.advanced.streamingInsertTitle')} -
-
- {t('settings.advanced.streamingInsertDesc')} -
- - - - - - -
- - -
- {t('settings.recording.startupGroupTitle')} -
-
- {t('settings.recording.startupAtBootDesc')} -
- - - - - {capability.statusHint && ( -
- {capability.statusHint} -
- )} -
-
+
); } @@ -609,6 +626,7 @@ function PrivacySection() { const { prefs, updatePrefs, refresh } = useHotkeySettings(); const [busyAction, setBusyAction] = useState(null); const helpItems = privacyHelpItemsCopy(); + const groupLabels = privacyGroupLabelsCopy(); if (!prefs) { return ( @@ -687,97 +705,143 @@ function PrivacySection() { const isBusy = (action: string) => busyAction === action; return ( - -
{t('settings.privacy.title')}
-
- {t('settings.privacy.desc')} -
-
- {t('settings.privacy.notice')} + +
+
+
{t('settings.privacy.title')}
+
{t('settings.privacy.desc')}
+
+
{t('settings.privacy.notice')}
-
+ +
{helpItems.map(item => (
+ + + {item.title} {item.desc}
))}
- - - - - - {isBusy('clearHistory') ? t('common.saving') : t('settings.privacy.clearHistoryBtn')} - - - - - {isBusy('clearVocab') ? t('common.saving') : t('settings.privacy.clearVocabBtn')} - - - - - {isBusy('clearProviderConfiguration') ? t('common.saving') : t('settings.privacy.clearConfigBtn')} - - - - - {isBusy('exportDiagnostics') ? t('common.saving') : t('settings.privacy.exportDiagnosticsBtn')} - - + +
+
+
{groupLabels.retention}
+
+ + + + + + {isBusy('clearHistory') ? t('common.saving') : t('settings.privacy.clearHistoryBtn')} + + + + + {isBusy('clearVocab') ? t('common.saving') : t('settings.privacy.clearVocabBtn')} + + +
+
+ +
+
{groupLabels.maintenance}
+
+ + + {isBusy('clearProviderConfiguration') ? t('common.saving') : t('settings.privacy.clearConfigBtn')} + + + + + {isBusy('exportDiagnostics') ? t('common.saving') : t('settings.privacy.exportDiagnosticsBtn')} + + +
+
+
); } +function PrivacyActionRow({ label, desc, children }: { label: string; desc: string; children: ReactNode }) { + return ( +
+
+
{label}
+
{desc}
+
+
{children}
+
+ ); +} + function privacyHelpItemsCopy() { const zh = i18n.language.toLowerCase().startsWith('zh'); return zh ? [ { + icon: 'mic', title: '音频数据', desc: '你的语音音频会发送到你配置的云 ASR 服务商,用于语音识别。', }, { + icon: 'doc', title: '识别文本', desc: '识别文本会发送到你配置的 LLM 服务商,用于生成结果。', }, { + icon: 'archive', title: '本地数据', desc: '历史记录与词汇表默认仅存储在本地设备。', }, ] : [ { + icon: 'mic', title: 'Audio data', desc: 'Your voice audio is sent to your configured cloud ASR provider for speech recognition.', }, { + icon: 'doc', title: 'Recognized text', desc: 'Recognized text is sent to your configured LLM provider to generate the result.', }, { + icon: 'archive', title: 'Local data', desc: 'History and vocabulary are stored only on this device by default.', }, ]; } +function privacyGroupLabelsCopy() { + return i18n.language.toLowerCase().startsWith('zh') + ? { retention: '本地保留与历史', maintenance: '维护与支持' } + : { retention: 'Local retention and history', maintenance: 'Maintenance and support' }; +} + function HotkeyRecorder({ binding, onCommit, @@ -1332,7 +1396,13 @@ function AutostartRow() { desc={t('settings.recording.startupAtBootDesc')} >
- {loaded ? : null} + {loaded ? ( + + ) : null} {error && (
{t('settings.recording.startupAtBootError', { message: error })} @@ -1343,25 +1413,40 @@ function AutostartRow() { ); } -export function Toggle({ on, onToggle, disabled = false }: { on: boolean; onToggle?: (next: boolean) => void; disabled?: boolean }) { +export function Toggle({ + label, + on, + onToggle, + disabled = false, +}: { + label: string; + on: boolean; + onToggle?: (next: boolean) => void; + disabled?: boolean; +}) { return (
); } @@ -2277,6 +2369,7 @@ function AdvancedSection({ llmSwitching, beginLlmSwitch, isCurrentLlmSwitch, end desc={isMac ? t('settings.advanced.qwen3Desc') : t('settings.advanced.notSupportedHere')}>
{ if (next) requestEnable('local-qwen3'); @@ -2293,6 +2386,7 @@ function AdvancedSection({ llmSwitching, beginLlmSwitch, isCurrentLlmSwitch, end desc={t('settings.advanced.foundryDesc')}>
{ if (next) requestEnable('foundry-local-whisper'); diff --git a/src/pages/Style.tsx b/src/pages/Style.tsx index 3f8ec14..cac5dc1 100644 --- a/src/pages/Style.tsx +++ b/src/pages/Style.tsx @@ -201,26 +201,28 @@ export function Style() {
- {t('style.masterToggle')} - - {saveError?.target === 'master' && ( - - {saveError.message} - - )} -
+ desc={ + + {t('style.desc')} + + {t('style.masterToggle')} + + {saveError?.target === 'master' && ( + + {saveError.message} + + )} + + } />
diff --git a/src/pages/Vocab.tsx b/src/pages/Vocab.tsx index b0d70f1..a02dd6f 100644 --- a/src/pages/Vocab.tsx +++ b/src/pages/Vocab.tsx @@ -121,7 +121,6 @@ export function Vocab() { {t('common.refresh')}} />
diff --git a/src/styles/preview-replica.css b/src/styles/preview-replica.css index 4d1c02b..fba8721 100644 --- a/src/styles/preview-replica.css +++ b/src/styles/preview-replica.css @@ -59,8 +59,8 @@ .wi-stage { width: 100%; height: 100%; - min-width: 1180px; - min-height: 720px; + min-width: 0; + min-height: 0; background: var(--wi-window); color: var(--wi-text); font-family: var(--wi-font); @@ -71,7 +71,7 @@ .wi-shell { display: grid; - grid-template-columns: 250px minmax(0, 1fr); + grid-template-columns: clamp(205px, 20vw, 250px) minmax(0, 1fr); min-height: 0; } @@ -611,13 +611,13 @@ .wi-settings-tabs { display: flex; justify-content: space-between; - width: min(720px, calc(100% - 330px)); + width: min(720px, 100%); height: 50px; min-height: 50px; flex: 0 0 auto; min-width: 0; border: 0; - border-radius: 16px; + border-radius: 12px; overflow: hidden; background: rgba(255, 255, 255, 0.66); box-shadow: var(--wi-inset-ring-soft), 0 10px 26px -24px rgba(15, 23, 42, 0.32); @@ -677,37 +677,99 @@ .wi-recording-settings-grid { display: grid; - grid-template-columns: minmax(0, 1.08fr) minmax(340px, 0.92fr); - gap: 12px; - align-items: start; + grid-template-areas: + "recording stream" + "insert history"; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px 12px; + align-items: stretch; min-width: 0; } -.wi-recording-settings-left, -.wi-recording-settings-right { +.wi-recording-settings-primary { + grid-area: recording; display: flex; flex-direction: column; - gap: 12px; - min-width: 0; +} + +.wi-recording-settings-primary .ol-setting-row { + flex: 1 1 0; +} + +.wi-recording-settings-stream-startup { + grid-area: stream; +} + +.wi-recording-settings-insert { + grid-area: insert; +} + +.wi-recording-settings-history { + grid-area: history; } .wi-recording-settings-primary, -.wi-recording-settings-stream, -.wi-recording-settings-startup, +.wi-recording-settings-stream-startup, .wi-recording-settings-secondary { min-width: 0; + height: 100%; +} + +.wi-recording-settings-primary, +.wi-recording-settings-stream-startup { + padding: 12px !important; +} + +.wi-settings-panel-recording .ol-setting-row { + grid-template-columns: minmax(0, 1fr) auto !important; + gap: 12px !important; + align-items: center; + padding: 4px 0 !important; +} + +.wi-settings-panel-recording .ol-setting-control { + align-items: center !important; + justify-content: flex-end; +} + +.wi-settings-panel-recording .ol-setting-desc { + margin-top: 2px !important; + font-size: 11.5px !important; + line-height: 1.4 !important; } .wi-recording-settings-secondary .ol-setting-row, .wi-recording-settings-stream .ol-setting-row, .wi-recording-settings-startup .ol-setting-row { - padding-top: 11px !important; - padding-bottom: 11px !important; + padding-top: 4px !important; + padding-bottom: 4px !important; } .wi-recording-settings-secondary [aria-expanded] { - padding-top: 12px !important; - padding-bottom: 12px !important; + padding: 7px 12px !important; +} + +.wi-recording-settings-primary > div:nth-child(2), +.wi-recording-settings-section > div:nth-child(2) { + margin-bottom: 3px !important; +} + +.wi-recording-settings-secondary [aria-expanded] + div > div > div { + padding: 0 12px 8px !important; +} + +.wi-recording-settings-section + .wi-recording-settings-section { + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid var(--ol-line-soft); +} + +.wi-recording-settings-status-hint { + margin-top: 2px; + color: var(--ol-ink-4); + font-size: 10.5px; + line-height: 1.35; + text-wrap: pretty; } .wi-mode-switch { @@ -754,6 +816,137 @@ margin-bottom: 10px; } +.wi-model-settings-compact { + display: flex; + min-width: 0; + flex-direction: column; + gap: 10px; +} + +.wi-model-storage-notice { + margin: 0; + color: var(--ol-ink-4); + font-size: 11.5px; + line-height: 1.45; +} + +.wi-model-settings-compact .wi-model-mode { + min-height: 60px; + margin-bottom: 0; + padding: 5px; + border-radius: 14px; +} + +.wi-model-settings-compact .wi-model-mode button { + border-radius: 8px; +} + +.wi-model-settings-compact .wi-model-mode button small { + margin-top: 3px; +} + +.wi-model-bundle-title, +.wi-quick-card-title { + color: var(--ol-ink); + font-size: 13px; + font-weight: 600; +} + +.wi-model-bundle-title { + margin-top: 1px; +} + +.wi-model-settings-compact .wi-plan-grid { + gap: 16px; +} + +.wi-model-settings-compact .wi-plan-card { + height: 196px; + grid-template-columns: 82px minmax(0, 1fr); + gap: 18px; + padding: 20px 48px 20px 22px; + border-radius: 16px; +} + +.wi-model-settings-compact .wi-radio-corner { + top: 14px; + right: 14px; + width: 20px; + height: 20px; +} + +.wi-model-settings-compact .wi-radio-corner.on { + border-width: 6px; +} + +.wi-model-settings-compact .wi-plan-logo { + width: 76px; + height: 76px; + border-radius: 15px; +} + +.wi-model-settings-compact .wi-plan-heading { + gap: 7px; + font-size: 18px; +} + +.wi-model-settings-compact .wi-plan-desc { + margin-top: 6px; + font-size: 13px; + line-height: 1.45; +} + +.wi-model-settings-compact .wi-plan-meta { + margin-top: 7px; + font-size: 12.5px; +} + +.wi-model-settings-compact .wi-quick-card { + margin-top: 0; + padding: 16px 18px !important; +} + +.wi-model-settings-compact .wi-quick-card-title { + margin-bottom: 7px; +} + +.wi-model-settings-compact .wi-provider-stack { + gap: 0; +} + +.wi-model-settings-compact .wi-provider-row { + grid-template-columns: minmax(120px, 0.72fr) minmax(220px, 1.35fr) minmax(142px, 0.78fr) minmax(96px, 0.5fr); + gap: 10px; + padding: 10px 0; + align-items: center; +} + +.wi-model-settings-compact .wi-provider-label { + gap: 3px; +} + +.wi-provider-inline-check, +.wi-provider-inline-check > div { + min-width: 0; + width: 100%; +} + +.wi-provider-inline-check > div { + align-items: stretch !important; + flex-direction: column; + gap: 4px !important; +} + +.wi-provider-inline-check button { + min-height: 40px; + max-width: none !important; +} + +.wi-provider-inline-check-pair { + display: grid; + gap: 6px; +} + .wi-model-mode button { min-width: 0; flex: 1; @@ -1604,7 +1797,7 @@ .wi-overview-page .wi-recent-row { grid-template-columns: 78px minmax(0, 1fr) 48px; gap: 12px; - margin: 0 -6px; + margin: 0; padding: 11px 6px; border-bottom: 0; border-radius: 12px; @@ -1688,7 +1881,18 @@ min-width: 0; display: flex; align-items: center; - gap: 10px; + gap: 8px; + padding: 5px 8px; + border-radius: 10px; + background: var(--wi-control-muted); +} + +.wi-style-header-row { + display: inline-flex; + max-width: 100%; + align-items: center; + flex-wrap: wrap; + gap: 8px 14px; } .wi-style-master-label { @@ -2544,3 +2748,419 @@ filter: blur(0); } } + +/* Shared readability and keyboard affordances. */ +.wi-stage button:focus-visible, +.wi-stage input:focus-visible, +.wi-stage select:focus-visible, +.wi-stage textarea:focus-visible, +.wi-stage [tabindex]:focus-visible { + outline: 2px solid var(--wi-blue); + outline-offset: 2px; +} + +.wi-settings-layout, +.wi-settings-panel { + min-width: 0; +} + +.wi-settings-panel > .ol-card { + width: 100%; + border-radius: 14px !important; + box-shadow: var(--wi-surface-shadow) !important; +} + +.wi-settings-panel-output > .ol-card, +.wi-settings-panel-about > .ol-card { + width: min(760px, 100%); + align-self: flex-start; +} + +.wi-settings-panel-privacy > .ol-card { + width: 100%; + align-self: flex-start; +} + +.wi-settings-panel-privacy { + container-type: inline-size; +} + +.wi-privacy-card { + display: flex; + flex-direction: column; + gap: 10px; +} + +.wi-privacy-heading { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(260px, 0.85fr); + gap: 20px; + align-items: start; +} + +.wi-privacy-title { + color: var(--wi-text); + font: 750 14px/1.4 var(--wi-font); +} + +.wi-privacy-desc, +.wi-privacy-notice { + color: var(--wi-muted); + font: 500 12px/1.55 var(--wi-font); + text-wrap: pretty; +} + +.wi-privacy-desc { + margin-top: 3px; +} + +.wi-privacy-notice { + padding-top: 20px; +} + +.wi-privacy-data-flow.wi-help-list { + gap: 0; + margin: 0; + overflow: hidden; + border-radius: 10px; + background: var(--wi-control-muted); +} + +.wi-privacy-data-flow .wi-help-item { + grid-template-columns: 30px minmax(88px, 112px) minmax(0, 1fr); + gap: 12px; + min-height: 44px; + align-items: center; + padding: 8px 12px; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; + font-size: 12.5px; +} + +.wi-privacy-data-flow .wi-help-item + .wi-help-item { + border-top: 1px solid var(--ol-line-soft); +} + +.wi-privacy-data-icon { + display: inline-flex; + width: 30px; + height: 30px; + align-items: center; + justify-content: center; + border-radius: 8px; + background: var(--wi-blue-soft); + color: var(--wi-blue); +} + +.wi-privacy-controls { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 8px; +} + +.wi-privacy-control-section { + display: grid; + min-width: 0; + gap: 3px; +} + +.wi-privacy-group-title { + padding: 0 2px; + color: var(--wi-muted); + font: 700 12.5px/1.4 var(--wi-font); +} + +.wi-privacy-control-group { + min-width: 0; + overflow: hidden; + border-radius: 10px; + background: var(--wi-control-muted); +} + +.wi-privacy-action-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 20px; + min-height: 54px; + align-items: center; + padding: 6px 12px; +} + +.wi-privacy-action-row + .wi-privacy-action-row { + border-top: 1px solid var(--ol-line-soft); +} + +.wi-privacy-action-copy { + min-width: 0; +} + +.wi-privacy-action-label { + color: var(--wi-text); + font: 700 13.5px/1.4 var(--wi-font); +} + +.wi-privacy-action-desc { + max-width: 720px; + margin-top: 3px; + color: var(--wi-muted); + font: 500 12.5px/1.5 var(--wi-font); + overflow-wrap: anywhere; + text-wrap: pretty; +} + +.wi-privacy-action-control { + display: flex; + min-width: 120px; + align-items: center; + justify-content: flex-end; +} + +.wi-privacy-action-control .wi-btn { + min-height: 40px; +} + +@container (max-width: 700px) { + .wi-privacy-heading { + grid-template-columns: minmax(0, 1fr); + gap: 4px; + } + + .wi-privacy-notice { + padding-top: 0; + } + + .wi-privacy-data-flow .wi-help-item { + grid-template-columns: 30px minmax(82px, 104px) minmax(0, 1fr); + gap: 10px; + } + + .wi-privacy-action-row { + gap: 14px; + } +} + +@container (max-width: 520px) { + .wi-privacy-data-flow .wi-help-item { + grid-template-columns: 30px minmax(0, 1fr); + } + + .wi-privacy-data-flow .wi-help-item > span:last-child { + grid-column: 2; + } + + .wi-privacy-action-row { + grid-template-columns: minmax(0, 1fr); + gap: 8px; + } + + .wi-privacy-action-control { + min-width: 0; + justify-content: flex-start; + } +} + +.wi-settings-toggle { + flex: 0 0 auto; +} + +/* Reduce oversized surfaces without changing their content or actions. */ +.wi-overview-page .wi-provider-card { + min-height: 112px; + padding: 18px 20px; + grid-template-columns: 60px minmax(0, 1fr) auto; + border-radius: 16px; +} + +.wi-overview-page .wi-model-logo { + width: 52px; + height: 52px; + border-radius: 13px; +} + +.wi-overview-page .wi-metric-grid { + margin-bottom: 14px; +} + +.wi-overview-page .wi-metric-card { + height: 98px; + padding: 16px 18px; + border-radius: 16px; +} + +.wi-overview-page .wi-week-card, +.wi-overview-page .wi-recent-card { + padding: 18px; + border-radius: 16px; +} + +.wi-overview-page .wi-week-y-axis, +.wi-overview-page .wi-week-point-value { + font-size: 11.5px; +} + +.wi-overview-page .wi-week-date, +.wi-overview-page .wi-week-label { + font-size: 12px; +} + +.wi-style-card { + padding: 16px; + border-radius: 14px; +} + +.wi-style-sample { + border-radius: 8px; + background: rgba(243, 246, 250, 0.72); + box-shadow: none; +} + +:root[data-theme='dark'] .wi-style-sample { + background: rgba(255, 255, 255, 0.045); +} + +.wi-vocab-main-only { + align-self: flex-start; + height: auto; + min-height: 300px; +} + +@media (max-width: 1100px) { + .wi-shell { + grid-template-columns: 205px minmax(0, 1fr); + } + + .wi-main { + padding: 24px 22px; + } + + .wi-top-tools { + top: 20px; + right: 22px; + gap: 9px; + } + + .wi-commandbar { + gap: 9px; + } + + .wi-commandbar .wi-btn { + min-width: 52px; + padding: 0 12px; + } + + .wi-page-head, + .wi-overview-page .wi-page-head { + padding-right: 225px; + margin-bottom: 18px; + } + + .wi-page-title-block h1, + .wi-overview-page .wi-page-title-block h1 { + font-size: 26px; + } + + .wi-overview-page, + .wi-history-page, + .wi-style-page, + .wi-vocab-page { + height: auto; + min-height: 100%; + overflow: visible; + } + + .wi-overview-page .wi-model-grid { + grid-template-columns: minmax(0, 1fr); + gap: 12px; + } + + .wi-overview-page .wi-metric-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + } + + .wi-overview-page .wi-overview-bottom { + grid-template-columns: minmax(0, 1fr); + gap: 14px; + } + + .wi-history-toolbar { + height: auto; + flex-wrap: wrap; + gap: 10px; + } + + .wi-history-search { + width: auto; + flex: 1 1 300px; + } + + .wi-history-toolbar .wi-btn:first-of-type { + margin-left: 0; + } + + .wi-table-card .wi-table { + min-width: 920px; + } + + .wi-settings-tabs { + max-width: 100%; + margin-top: 54px; + overflow-x: auto; + justify-content: flex-start; + } + + .wi-settings-tab { + flex: 1 0 auto; + } + + .wi-recording-settings-grid { + grid-template-columns: minmax(0, 1fr); + } + + .wi-style-grid { + flex: 0 0 auto; + grid-template-columns: minmax(0, 1fr); + grid-auto-rows: auto; + overflow: visible; + } + + .wi-vocab-layout { + flex: 0 0 auto; + grid-template-columns: minmax(0, 1fr); + overflow: visible; + } + + .wi-vocab-main, + .wi-vocab-side { + height: auto; + overflow: visible; + } + + .wi-vocab-side { + grid-template-rows: auto auto; + } + + .wi-chip-list, + .wi-correction-list { + max-height: 280px; + } +} + +@media (max-height: 700px) { + .wi-overview-page, + .wi-history-page, + .wi-style-page, + .wi-vocab-page { + height: auto; + min-height: 100%; + overflow: visible; + } + + .wi-style-grid, + .wi-vocab-layout { + flex: 0 0 auto; + overflow: visible; + } +} diff --git a/src/styles/tokens.css b/src/styles/tokens.css index ead90ec..8c01e26 100644 --- a/src/styles/tokens.css +++ b/src/styles/tokens.css @@ -13,7 +13,7 @@ --ol-ink: #0a0a0b; --ol-ink-2: #2a2a2d; --ol-ink-3: rgba(10, 10, 11, 0.62); - --ol-ink-4: rgba(10, 10, 11, 0.42); + --ol-ink-4: rgba(10, 10, 11, 0.56); --ol-ink-5: rgba(10, 10, 11, 0.24); --ol-blue: #2563eb; @@ -70,7 +70,7 @@ --ol-ink: #f4f4f5; --ol-ink-2: #d9dadd; --ol-ink-3: rgba(244, 244, 245, 0.68); - --ol-ink-4: rgba(244, 244, 245, 0.48); + --ol-ink-4: rgba(244, 244, 245, 0.62); --ol-ink-5: rgba(244, 244, 245, 0.30); --ol-blue: #6ea8fe; --ol-blue-hover: #8bbcff;