From d686406a7003c8a514aa1463ae0cf9739a23b119 Mon Sep 17 00:00:00 2001 From: sim Date: Tue, 8 Sep 2026 14:27:29 +0800 Subject: [PATCH] =?UTF-8?q?refactor(app):=20=E6=B8=85=E7=90=86=E5=BA=9F?= =?UTF-8?q?=E5=BC=83=E4=BB=A3=E7=A0=81=E5=B9=B6=E6=98=8E=E7=A1=AE=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OpenLessAccessibilityCommandReceiver.kt | 6 - .../kotlin/OpenLessAccessibilityService.kt | 6 - .../openless-core/src/asr/volcengine.rs | 6 +- .../openless-core/src/style_pack_store.rs | 17 +- openless-all/app/linux-egui/src/lib.rs | 16 +- .../app/scripts/check-hotkey-recorder.mjs | 22 -- .../app/scripts/frontend-test-runner.test.mjs | 2 +- .../src/asr/local/foundry_provider.rs | 4 +- .../src/asr/local/foundry_runtime.rs | 4 +- .../src-tauri/src/asr/local/qwen_engine.rs | 4 +- .../app/src-tauri/src/commands/history.rs | 2 +- .../app/src-tauri/src/commands/misc.rs | 5 - .../app/src-tauri/src/commands/mod.rs | 17 +- .../app/src-tauri/src/commands/settings.rs | 4 - openless-all/app/src-tauri/src/selection.rs | 5 +- openless-all/app/src/App.tsx | 7 +- .../app/src/components/FloatingShell.tsx | 31 +-- .../src/components/GlobalDownloadProgress.tsx | 2 +- .../app/src/components/SettingsModal.tsx | 13 +- .../app/src/components/ShortcutRecorder.tsx | 6 +- openless-all/app/src/components/ui/Modal.tsx | 2 +- .../app/src/components/ui/SelectLite.tsx | 3 +- openless-all/app/src/i18n/en.ts | 29 --- openless-all/app/src/i18n/ja.ts | 29 --- openless-all/app/src/i18n/ko.ts | 29 --- openless-all/app/src/i18n/zh-CN.ts | 31 +-- openless-all/app/src/i18n/zh-TW.ts | 29 --- openless-all/app/src/lib/audioCue.ts | 2 +- openless-all/app/src/lib/hotkey.ts | 9 +- .../app/src/lib/hotkeyRecorder.test.ts | 85 ------- openless-all/app/src/lib/hotkeyRecorder.ts | 70 ----- openless-all/app/src/lib/ipc/channels.ts | 9 +- openless-all/app/src/lib/ipc/style-packs.ts | 1 - openless-all/app/src/lib/ipc/vocab.ts | 1 - openless-all/app/src/lib/stylePackIcon.ts | 5 +- openless-all/app/src/lib/stylePrefs.test.ts | 240 ------------------ openless-all/app/src/lib/stylePrefs.ts | 120 --------- openless-all/app/src/lib/useExitMount.ts | 11 +- openless-all/app/src/pages/Corrections.tsx | 2 +- openless-all/app/src/pages/History.tsx | 12 +- .../app/src/pages/LocalAsr/components.tsx | 2 +- openless-all/app/src/pages/LocalAsr/index.tsx | 32 +-- openless-all/app/src/pages/Overview.tsx | 10 +- openless-all/app/src/pages/QaPanel.tsx | 2 +- openless-all/app/src/pages/Translation.tsx | 2 +- openless-all/app/src/pages/Vocab.tsx | 8 +- openless-all/app/src/pages/_atoms.tsx | 2 +- .../settings/ChannelEditorHostContext.ts | 4 + .../app/src/pages/settings/ChannelList.tsx | 39 +-- .../pages/settings/ClaudeConsoleSection.tsx | 2 +- .../src/pages/settings/DataStorageSection.tsx | 3 +- .../src/pages/settings/LocalModelSection.tsx | 8 +- .../src/pages/settings/ProvidersSection.tsx | 50 +--- .../pages/settings/RecordingInputSection.tsx | 6 +- .../src/pages/settings/RemoteInputSection.tsx | 2 +- .../app/src/pages/settings/shared.tsx | 4 +- openless-all/app/src/pages/settings/tabs.tsx | 6 +- openless-all/app/src/styles/global.css | 18 +- openless-all/app/src/styles/tokens.css | 40 +-- openless-all/app/tsconfig.json | 25 +- 60 files changed, 165 insertions(+), 998 deletions(-) delete mode 100644 openless-all/app/scripts/check-hotkey-recorder.mjs delete mode 100644 openless-all/app/src/lib/hotkeyRecorder.test.ts delete mode 100644 openless-all/app/src/lib/hotkeyRecorder.ts delete mode 100644 openless-all/app/src/lib/stylePrefs.test.ts delete mode 100644 openless-all/app/src/lib/stylePrefs.ts diff --git a/openless-all/app/android/kotlin/OpenLessAccessibilityCommandReceiver.kt b/openless-all/app/android/kotlin/OpenLessAccessibilityCommandReceiver.kt index 757a3d4e..b64b4afd 100644 --- a/openless-all/app/android/kotlin/OpenLessAccessibilityCommandReceiver.kt +++ b/openless-all/app/android/kotlin/OpenLessAccessibilityCommandReceiver.kt @@ -64,12 +64,6 @@ class OpenLessAccessibilityCommandReceiver : BroadcastReceiver() { const val EXTRA_RESULT_REASON = "result_reason" const val EXTRA_PASTE_TEXT = "paste_text" const val EXTRA_SELECTED_TEXT = "selected_text" - /** @deprecated Use [AccessibilityPasteResult] codes */ - const val EXTRA_PASTE_RESULT = "paste_result" - /** @deprecated Use [AccessibilityPasteResult.SUCCESS.code] */ - const val RESULT_PASTE_SUCCESS = 1 - /** @deprecated Use failure codes from [AccessibilityPasteResult] */ - const val RESULT_PASTE_FAILED = 4 private const val TAG = "OpenLessA11yCommand" } } diff --git a/openless-all/app/android/kotlin/OpenLessAccessibilityService.kt b/openless-all/app/android/kotlin/OpenLessAccessibilityService.kt index 474a61fe..7f5ecd08 100644 --- a/openless-all/app/android/kotlin/OpenLessAccessibilityService.kt +++ b/openless-all/app/android/kotlin/OpenLessAccessibilityService.kt @@ -463,12 +463,6 @@ class OpenLessAccessibilityService : AccessibilityService() { return pingResult == AccessibilityPasteResult.SUCCESS } - /** @deprecated Use [pingAccessibilityProcess] for UI; paste no longer gates on this. */ - @JvmStatic - fun isOperational(context: Context): Boolean { - return pingAccessibilityProcess(context) - } - internal fun performPasteFromCommand(pasteText: String? = null): AccessibilityPasteResult { return instance?.performPasteToFocusedFieldInternal(pasteText) ?: AccessibilityPasteResult.SERVICE_NOT_CONNECTED diff --git a/openless-all/app/crates/openless-core/src/asr/volcengine.rs b/openless-all/app/crates/openless-core/src/asr/volcengine.rs index f2f987b4..12f35c77 100644 --- a/openless-all/app/crates/openless-core/src/asr/volcengine.rs +++ b/openless-all/app/crates/openless-core/src/asr/volcengine.rs @@ -1,9 +1,9 @@ #![cfg_attr(target_os = "linux", allow(dead_code, unused_variables))] //! Volcengine SAUC bigmodel streaming ASR client. //! -//! Direct port of the Swift `VolcengineStreamingASR`. Battle-tested protocol -//! quirks are preserved verbatim — see comments tagged with `[asr]` for the -//! original learnings (especially the "definite=true is NOT stream end" bug). +//! Sends PCM frames, combines recognition updates and waits for the protocol's +//! final response. A `definite=true` utterance commits that segment only; it does +//! not end the stream or prevent later audio from being sent. use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; diff --git a/openless-all/app/crates/openless-core/src/style_pack_store.rs b/openless-all/app/crates/openless-core/src/style_pack_store.rs index 8852f1a6..f1f14b01 100644 --- a/openless-all/app/crates/openless-core/src/style_pack_store.rs +++ b/openless-all/app/crates/openless-core/src/style_pack_store.rs @@ -173,7 +173,14 @@ impl StylePackStore { Ok(updated) } - /// Store a rasterized user icon without accepting caller-supplied file paths. + /// Save a PNG icon in this pack's owned asset directory, or clear it with `None`. + /// + /// Metadata is committed before the old file is removed. A failed metadata + /// write leaves the previous image and in-memory pack unchanged. + /// + /// # Errors + /// Returns an error for an unknown/invalid pack ID, an invalid PNG, an image + /// over 64 KiB, an unavailable asset directory, or a failed filesystem write. pub fn update_icon(&self, id: &str, png: Option<&[u8]>) -> Result { if id.is_empty() || id == "." @@ -247,7 +254,13 @@ impl StylePackStore { Ok(saved) } - /// Return only bounded, validated image data from this repository's assets. + /// Read a pack icon as a PNG/JPEG/WebP data URL without exposing filesystem access. + /// + /// Missing icons return `None`. Reads are limited to 64 KiB and must resolve + /// inside the repository's asset directory. + /// + /// # Errors + /// Returns an error for an unknown pack, an invalid image or path, or a read failure. pub fn icon_data_url(&self, id: &str) -> Result, BackendError> { let pack = self.get(id)?; let Some(path) = pack.icon_path else { diff --git a/openless-all/app/linux-egui/src/lib.rs b/openless-all/app/linux-egui/src/lib.rs index 076611e9..228339b2 100644 --- a/openless-all/app/linux-egui/src/lib.rs +++ b/openless-all/app/linux-egui/src/lib.rs @@ -1,9 +1,8 @@ -//! Linux host seam for the egui frontend. +//! Linux platform adapters and runtime coordination for the egui frontend. //! -//! The egui team owns the `eframe::App` and all visual/UI code. This crate is -//! intentionally a small host adapter: it re-exports the core contract and -//! provides a place for Linux window, tray, input and resource adapters to be -//! added without making the core depend on egui or Tauri. +//! `main.rs` owns the egui UI. This library constructs Core with Linux audio, +//! credentials, input, settings and resource adapters, then forwards commands +//! and semantic events between that backend and the UI. mod audio; mod backend; @@ -48,11 +47,10 @@ pub use single_instance::{ pub use openless_core::contract::*; -/// Construction seam reserved for the Linux host implementation. +/// Coordinates Core operations with Linux settings and recording lifecycles. /// -/// Keeping this as a named type gives the egui package a stable home for -/// platform adapters while the UI is developed independently. No window or -/// egui object is stored here. +/// Capture state binds recorder callbacks to their owning Core session. Window +/// objects and egui widgets stay in the frontend. pub struct LinuxHost { backend: std::sync::Arc, settings_runtime: std::sync::Arc, diff --git a/openless-all/app/scripts/check-hotkey-recorder.mjs b/openless-all/app/scripts/check-hotkey-recorder.mjs deleted file mode 100644 index f14aff3a..00000000 --- a/openless-all/app/scripts/check-hotkey-recorder.mjs +++ /dev/null @@ -1,22 +0,0 @@ -import * as esbuild from 'esbuild'; -import { mkdtemp, rm } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { fileURLToPath, pathToFileURL } from 'node:url'; - -const tmp = await mkdtemp(join(tmpdir(), 'openless-hotkey-recorder-')); -const outfile = join(tmp, 'hotkey-recorder-test.mjs'); - -try { - await esbuild.build({ - entryPoints: [fileURLToPath(new URL('../src/lib/hotkeyRecorder.test.ts', import.meta.url))], - outfile, - bundle: true, - platform: 'node', - format: 'esm', - logLevel: 'silent', - }); - await import(pathToFileURL(outfile).href); -} finally { - await rm(tmp, { recursive: true, force: true }); -} diff --git a/openless-all/app/scripts/frontend-test-runner.test.mjs b/openless-all/app/scripts/frontend-test-runner.test.mjs index 97e4f0bf..61e376f4 100644 --- a/openless-all/app/scripts/frontend-test-runner.test.mjs +++ b/openless-all/app/scripts/frontend-test-runner.test.mjs @@ -13,7 +13,7 @@ for (const expected of [ 'scripts/macos-speech-usage-description-contract.test.mjs', 'scripts/repository-owner-contract.test.mjs', 'scripts/windows-ui-config.test.mjs', - 'src/lib/hotkeyRecorder.test.ts', + 'src/lib/hotkeySideModifiers.test.ts', 'src/lib/windowHotkeyFallback.test.ts', ]) { assert(discovered.includes(expected), `aggregate discovery omitted ${expected}`); diff --git a/openless-all/app/src-tauri/src/asr/local/foundry_provider.rs b/openless-all/app/src-tauri/src/asr/local/foundry_provider.rs index 978fa50f..d3be9f91 100644 --- a/openless-all/app/src-tauri/src/asr/local/foundry_provider.rs +++ b/openless-all/app/src-tauri/src/asr/local/foundry_provider.rs @@ -1,4 +1,6 @@ -#![allow(dead_code, unused_variables)] // Task 6 接入 coordinator 后这些路径会变成运行时路径。 +#![allow(dead_code, unused_variables)] +//! Foundry Local Whisper 的录音缓冲与分片转写适配。 +//! Windows 路径执行原生推理;其他目标保留类型兼容分支。 #[cfg(target_os = "windows")] use std::fs::{self, OpenOptions}; diff --git a/openless-all/app/src-tauri/src/asr/local/foundry_runtime.rs b/openless-all/app/src-tauri/src/asr/local/foundry_runtime.rs index 46da66ed..6338a93e 100644 --- a/openless-all/app/src-tauri/src/asr/local/foundry_runtime.rs +++ b/openless-all/app/src-tauri/src/asr/local/foundry_runtime.rs @@ -108,9 +108,7 @@ pub(crate) const FOUNDRY_FALLBACK_TERMINAL_USER_MESSAGE: &str = "本地识别失败: GPU 识别异常,且 CPU 回退未能完成(详情见日志)"; #[cfg(target_os = "windows")] -// PR #945 review P2-1:transcribe_audio_file 死代码已删除;此 allow 仍需保留, -// 覆盖 ensure_loaded 等其他 pre-existing 死代码(删除它们涉及公共 API 形状变更, -// 留作后续跟进)。 +// Windows 原生运行时模块同时暴露预加载与转写接口,调用入口按目标配置选择。 #[allow(dead_code)] mod imp { use super::{FoundryPrimaryRecoveryToken, FoundryRouteEpoch}; diff --git a/openless-all/app/src-tauri/src/asr/local/qwen_engine.rs b/openless-all/app/src-tauri/src/asr/local/qwen_engine.rs index 8b188d2f..d7832688 100644 --- a/openless-all/app/src-tauri/src/asr/local/qwen_engine.rs +++ b/openless-all/app/src-tauri/src/asr/local/qwen_engine.rs @@ -1,7 +1,7 @@ //! vendored Open-Less/qwen-asr 的安全 Rust 包装。 //! -//! 当前只暴露**最小可用面**:`load` / `transcribe_audio` / `transcribe_stream` -//! + token 回调。后续接 coordinator 时再扩 prompt/language 设置。 +//! 管理模型 context、批处理/流式转写和 token 回调;同一 context 的原生调用 +//! 由 run_lock 串行化,取消后的 worker 返回前也不能复用该 context。 use std::ffi::{CStr, CString}; use std::os::raw::{c_char, c_void}; diff --git a/openless-all/app/src-tauri/src/commands/history.rs b/openless-all/app/src-tauri/src/commands/history.rs index 012fea52..268297aa 100644 --- a/openless-all/app/src-tauri/src/commands/history.rs +++ b/openless-all/app/src-tauri/src/commands/history.rs @@ -208,7 +208,7 @@ fn copy_recording_to_mobile_url( /// 流程:读 `recordings/.wav` → 取 PCM(跳过 44 字节 WAV 头)→ 现 provider 重转 /// → 成功则原地回写该条历史的 rawTranscript / finalText、清除 error_code,返回新文本。 /// -/// 仅做 ASR,不自动二次润色(润色依赖 LLM 凭据且 issue 标为待定,留作后续)。失败时 +/// 仅重新转写音频,不调用 LLM 润色。失败时 /// 不动历史、不删录音,把错误返回给前端提示,用户可重试。返回更新后的整条记录给前端 /// 局部刷新。 #[tauri::command] diff --git a/openless-all/app/src-tauri/src/commands/misc.rs b/openless-all/app/src-tauri/src/commands/misc.rs index f0e15c41..2c92bcba 100644 --- a/openless-all/app/src-tauri/src/commands/misc.rs +++ b/openless-all/app/src-tauri/src/commands/misc.rs @@ -271,8 +271,3 @@ pub async fn debug_read_cursor_context( ); result } - -// ─────────────────────────── unused but exported (silences dead_code) ─────────────────────────── - -#[allow(dead_code)] -fn _ensure_snapshot_used(_: CredentialsSnapshot) {} diff --git a/openless-all/app/src-tauri/src/commands/mod.rs b/openless-all/app/src-tauri/src/commands/mod.rs index caab62f5..3295d622 100644 --- a/openless-all/app/src-tauri/src/commands/mod.rs +++ b/openless-all/app/src-tauri/src/commands/mod.rs @@ -1,12 +1,8 @@ #![cfg_attr(target_os = "linux", allow(dead_code, unused_variables))] -//! Tauri command surface — every IPC entry the React UI invokes lives here. +//! Tauri IPC 命令入口,按设置、凭据、历史等领域拆分子模块。 //! -//! issue: 历史上整个 IPC 表(127 个 `#[tauri::command]` + 跨域 helper)挤在单个 -//! 4800 行的 `commands.rs` 里。按单一职责拆成 `commands/` 下的域模块(settings / -//! credentials / providers / history / …),每个文件聚焦一个领域。对外路径保持不变: -//! 本模块用 `pub use ::*` glob 重导出每个子模块,`commands::` 仍然解析, -//! `lib.rs` 的 `generate_handler!` 清单与类型引用零改动。`#[tauri::command]` 生成的 -//! `__cmd__` 伴生项也随 glob 一并重导出——这是 Tauri 拆分命令文件的标准做法。 +//! 子模块的命令及宏生成的伴生项通过 glob 重导出,供 `lib.rs` 的 +//! `generate_handler!` 以 `commands::` 注册。平台条件须与注册清单保持一致。 use std::sync::Arc; @@ -16,8 +12,7 @@ use parking_lot::Mutex; use tauri::Manager; use tauri::State; -// 跨域共享的 crate 级导入:以 `pub(crate) use` 重导出,子模块用 `use super::*;` -// 即可拿到,避免在 16 个文件里重复同一组 import。 +// 子模块通过 use super::* 使用共享的 Host 状态、DTO 和依赖类型。 pub(crate) use serde::Serialize; pub(crate) use serde_json::Value; pub(crate) use tauri::{AppHandle, Emitter, Window}; @@ -100,9 +95,6 @@ pub use qa::*; #[cfg(not(mobile))] pub use remote_input::*; pub use settings::*; -// sherpa_onnx_asr_* 命令整组 `#[cfg(target_os = "windows")]`(见 lib.rs 的 -// generate_handler! 清单)。非 Windows 平台这组 glob 重导出无人引用,会触发 -// unused_imports;这是平台 cfg 的正常结果,不是真正的死代码。 #[cfg(all(not(mobile), debug_assertions))] pub use selection_polish::*; #[cfg(not(mobile))] @@ -110,6 +102,7 @@ pub use selection_polish_preview::*; #[cfg(all(not(mobile), target_os = "windows"))] pub use selection_voice::*; #[cfg(not(mobile))] +// Sherpa 命令只在 Windows 注册;其他桌面目标保留空重导出。 #[allow(unused_imports)] pub use sherpa_asr::*; pub use style_packs::*; diff --git a/openless-all/app/src-tauri/src/commands/settings.rs b/openless-all/app/src-tauri/src/commands/settings.rs index 2e16703a..8952dd2e 100644 --- a/openless-all/app/src-tauri/src/commands/settings.rs +++ b/openless-all/app/src-tauri/src/commands/settings.rs @@ -184,7 +184,6 @@ pub(crate) fn persist_strict_settings( pub async fn set_settings( coord: CoordinatorState<'_>, app: AppHandle, - tray_microphones: State<'_, TrayMicrophoneMenuState>, mut prefs: UserPreferences, ) -> Result<(), String> { // 捕获旧值用于远程输入服务的 diff(persist 后端口/开关变化时启停/重启)。 @@ -226,9 +225,6 @@ pub async fn set_settings( ); } }); - // 抑制 unused 警告:tray_microphones 现在改在闭包里通过 app.state 取, - // 但函数签名保留 State 入参,以便 Tauri 在调用前注入。 - let _ = tray_microphones; // 远程输入:开关 / 端口变化时启停或重启服务(PIN 变化走 regenerate_remote_pin 命令)。 if remote_prev.remote_input_enabled != prefs.remote_input_enabled || remote_prev.remote_input_port != prefs.remote_input_port diff --git a/openless-all/app/src-tauri/src/selection.rs b/openless-all/app/src-tauri/src/selection.rs index 7eb78403..12909add 100644 --- a/openless-all/app/src-tauri/src/selection.rs +++ b/openless-all/app/src-tauri/src/selection.rs @@ -963,7 +963,7 @@ mod macos_ax { buffer: *mut c_char, buffer_size: isize, encoding: u32, - ) -> bool; + ) -> u8; fn CFStringGetLength(s: CFStringRef) -> isize; fn CFStringGetMaximumSizeForEncoding(length: isize, encoding: u32) -> isize; } @@ -1053,7 +1053,8 @@ mod macos_ax { max_bytes, K_CF_STRING_ENCODING_UTF8, ); - if !ok { + // CoreFoundation's Boolean is an unsigned byte in the SDK ABI. + if ok == 0 { return None; } let cstr = CStr::from_ptr(buf.as_ptr() as *const c_char); diff --git a/openless-all/app/src/App.tsx b/openless-all/app/src/App.tsx index f3d954b2..0f2233e9 100644 --- a/openless-all/app/src/App.tsx +++ b/openless-all/app/src/App.tsx @@ -21,11 +21,8 @@ import { } from './lib/windowHotkeyFallback'; import { HotkeySettingsProvider } from './state/HotkeySettingsContext'; -// 各窗口/重页面懒加载,让每个 webview 只下载并解析自己用到的那部分代码。原本所有窗口 -// (主设置 / 胶囊 / QA / Less Computer / glow)共用一个打包产物,导致 5 个常驻 WebKit -// 进程都把整套设置 UI(FloatingShell + Style/Marketplace/LocalAsr…)和聊天面板加载进来, -// 常驻内存离谱。拆开后胶囊/glow 这类轻窗口不再加载设置/聊天代码。胶囊保持 eager: -// 它是听写实时反馈、对首帧延迟敏感,且体积很小。 +// 各 WebView 按窗口用途懒加载页面,减少常驻内存。 +// 胶囊承担录音的即时反馈,体积较小且对首帧延迟敏感,因此保持直接导入。 const AutoUpdateGate = lazy(() => import('./components/AutoUpdateGate').then(m => ({ default: m.AutoUpdateGate })), ); diff --git a/openless-all/app/src/components/FloatingShell.tsx b/openless-all/app/src/components/FloatingShell.tsx index f72b1dcc..83ffc56a 100644 --- a/openless-all/app/src/components/FloatingShell.tsx +++ b/openless-all/app/src/components/FloatingShell.tsx @@ -1,10 +1,7 @@ -// FloatingShell.tsx — frosted outer frame + raised inner console. -// Sidebar lives INSIDE the console card. -// Settings opens as a centered modal sheet from the sidebar bottom entry. -// -// Ported verbatim from design_handoff_openless/variants.jsx::FloatingShell. +// 主窗口外壳:组织侧栏导航、页面切换、移动端面板和设置弹窗。 +// 各页面共享偏好状态;业务读写通过 typed IPC 交给 Core。 -import { useEffect, useMemo, useRef, useState, type ComponentType, type CSSProperties } from 'react'; +import { useEffect, useRef, useState, type ComponentType, type CSSProperties } from 'react'; import { useTranslation } from 'react-i18next'; import { Icon } from './Icon'; import { Tooltip } from './Tooltip'; @@ -19,9 +16,6 @@ import { Marketplace } from '../pages/Marketplace'; import { Translation } from '../pages/Translation'; import { SelectionAsk } from '../pages/SelectionAsk'; import { Corrections } from '../pages/Corrections'; -// 风格市场(Marketplace)现在是侧栏「风格」展开组下的独立页面(不再是 Style 页面内 modal)。 -// LocalAsr 不再作为主 nav tab——本地 ASR 模型管理已合并到 Settings → Services 中 -// 通过 渲染。这里之前的 import 与 NAV_BASE 条目都已移除。 import { APP_VERSION_LABEL, IS_BETA_BUILD } from '../lib/appVersion'; import { HOTKEY_MODE_MIGRATION_ACK_KEY, @@ -65,10 +59,7 @@ const PAGE_CMP: Record, ComponentType> = { corrections: Corrections, }; -/** 侧栏导航树:扁平项 + 可展开分组(用户拍板的结构)。 - * - 概览 / 历史 / 词汇:扁平项,位置不变。 - * - 风格:展开组 → 润色模式(=style 页) + 风格市场(=marketplace 页,原为 Style 内 modal)。 - * - 工具:展开组 → 划词追问 + 翻译 + 纠正规则(2.0 UI 走查:纠正规则从词典页迁入)。 */ +/** 主导航分为直接入口与可展开分组;页面组件由 PAGE_CMP 统一解析。 */ type NavNode = | { kind: 'item'; id: AppTab; icon: string } | { kind: 'group'; key: string; icon: string; children: Array<{ id: AppTab }> }; @@ -105,7 +96,7 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia const [settingsInitialSection, setSettingsInitialSection] = useState(); const [providerPromptOpen, setProviderPromptOpen] = useState(false); const [hotkeyModePromptOpen, setHotkeyModePromptOpen] = useState(false); - // 退出动画门(2.0 UI 走查「从哪来回到哪去」):关闭时先反向播放入场动画再卸载。 + // 退出动画门:关闭时先反向播放入场动画再卸载。 const settingsMount = useExitMount(settingsOpen, 220); const providerPromptMount = useExitMount(providerPromptOpen); const hotkeyPromptMount = useExitMount(hotkeyModePromptOpen); @@ -235,9 +226,7 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia const styleTabActive = STYLE_TAB_IDS.includes(currentTab); return ( - // 不再为 macOS 红绿灯预留顶部 28px 空条(用户反馈「块上方多一条丑横条」): - // 侧栏与内容块都顶到窗口最上沿,原生红绿灯直接浮在侧栏左上角的块面上。 - // 侧栏 aside 在 mac 上加 topClearance(红绿灯竖直居中的避让带)让导航避开红绿灯。 + // 窗口内容从顶端铺开;macOS 仅在侧栏内部预留红绿灯区域。
{mobile && ( @@ -278,7 +267,7 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia padding: os === 'mac' ? `${MAC_TRAFFIC_LIGHT_CLEARANCE}px 10px 12px` : '10px 10px 12px', }}> - {/* 版本信息行(2.0 UI 走查):原「OpenLess」品牌位置直接改为显示版本信息, + {/* 版本信息行:原「OpenLess」品牌位置直接改为显示版本信息, 填掉导航上方的空档;BETA 徽章与版本号同基线。 */}
- {/* 底部只剩设置按钮(2.0 UI 走查:版本信息已上移到侧栏顶部原品牌位)。 */} + {/* 底部只剩设置按钮。 */}
}
} {/* key={section} 重挂载 → 每次切换分类播放轻微淡入(ol-tab-fade), - 与 tab 切换动画语言一致(2.0 UI 走查:补全动效)。 */} + 与 tab 切换动画语言一致。 */}
{section === 'general' && } {section === 'shortcuts' && } diff --git a/openless-all/app/src/components/ShortcutRecorder.tsx b/openless-all/app/src/components/ShortcutRecorder.tsx index f4a10e7e..0f0005ee 100644 --- a/openless-all/app/src/components/ShortcutRecorder.tsx +++ b/openless-all/app/src/components/ShortcutRecorder.tsx @@ -11,7 +11,7 @@ import type { ShortcutBinding } from '../lib/types'; const SLIDE_DISTANCE = 48; /** 下拉菜单展开后的固定高度(px):菜单按钮行高恒定,用固定值动画避免每次测量。 */ const MENU_HEIGHT = 34; -/** 滑动切换用 spring(与 Style.tsx 编辑抽屉同款)。只动 transform/opacity,不驱动布局,避免抽搐。 */ +/** 滑动切换用 spring(与 Style.tsx 编辑抽屉同款)。只动 transform/opacity,不驱动布局,避免抖动。 */ const slideSpring = { type: 'spring' as const, damping: 26, stiffness: 280 }; /** 下拉菜单展开/收起缓动,与 --ol-motion-soft 一致。 */ const menuEase = [0.22, 0.8, 0.22, 1] as const; @@ -231,7 +231,7 @@ export function ShortcutRecorder({ flexDirection: 'column', gap: 6, width: '100%', - // 2.0 UI 走查:设置行里的快捷键录制控件不再拉满整行(此前「Right ⌃」值贴左、 + // 设置行里的快捷键录制控件不再拉满整行(此前「Right ⌃」值贴左、 // 下拉箭头甩到最右缘),与输入框同宽上限,紧凑地跟在标签列之后。 maxWidth: 360, }; @@ -295,7 +295,7 @@ export function ShortcutRecorder({ return (
{/* mode="wait":主行与「正在录入」面板不重叠渲染;切换只做 transform/opacity 动画, - 不驱动布局,面板运动过程不抽搐。所有滑入/滑出统一向右。 */} + 不驱动布局,面板运动过程不抖动。所有滑入/滑出统一向右。 */} {recording ? ( ,避开 Win32 ComboBox -// 直角丑框(issue #418)以及 WKWebView 上原生 NSPopUpButton 的视觉割裂。 +// SelectLite 在各平台提供一致的下拉交互、键盘操作和浮层定位。 // // 设计: // - 触发器是 button(chevron + 当前值),样式可被 `style` 覆盖 diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index be043e69..ac4d1476 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -728,23 +728,9 @@ export const en: typeof zhCN = { }, }, selectionAsk: { - kicker: 'SELECTION ASK', title: 'Selection Ask', desc: 'Select text and ask questions by voice, with multi-turn follow-ups.', shortcutSettings: 'Shortcut settings', - intro: { - eyebrow: 'From reading to understanding', - title: 'Select a passage.\nAsk it out loud.', - desc: 'Explore a concept, find the key points, or go deeper. Answers appear in a floating panel.', - }, - example: { - title: 'Example conversation', - selection: 'Selected text', - term: 'The Feynman technique', - context: ' reveals gaps in your understanding by explaining a concept to someone else.', - question: 'Explain that in simpler terms.', - answer: 'Think of teaching a friend: the clearer your explanation, the better you understand it.', - }, guide: { openTitle: 'Open the panel', openDesc: 'Press {{hotkey}} to start a conversation.', @@ -755,17 +741,10 @@ export const en: typeof zhCN = { followup: 'Use the recording shortcut again to ask a follow-up.', dismiss: 'Close the panel and end this conversation', }, - statusEnabled: 'Enabled', - statusDisabled: 'Disabled', hotkey: { title: 'Hotkey to open the panel', - desc: 'Opens/closes the panel. Recording uses {{recordHotkey}}.', - 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: { @@ -774,15 +753,7 @@ export const en: typeof zhCN = { }, howto: { title: 'How to use', - step1: 'Press {{hotkey}} to open the panel.', step2: 'Select text in any app.', - step3: 'Press {{recordHotkey}} to record, press again to submit.', - step4: 'Continue pressing {{recordHotkey}} for follow-up questions.', - step5: 'Press Esc to close and clear history.', - windowTitle: 'Position, drag, and pin', - windowDesc: 'The panel is draggable and remembers its position. Pin to keep it open.', - privacyTitle: 'Privacy contract', - privacyDesc: 'Selected text lives only in memory until the panel closes. Text over 4000 chars is truncated.', }, }, settings: { diff --git a/openless-all/app/src/i18n/ja.ts b/openless-all/app/src/i18n/ja.ts index 546e7421..10f03b4b 100644 --- a/openless-all/app/src/i18n/ja.ts +++ b/openless-all/app/src/i18n/ja.ts @@ -730,23 +730,9 @@ export const ja: typeof zhCN = { }, }, selectionAsk: { - kicker: 'SELECTION ASK', title: '選択追問', desc: 'テキストを選択して音声で質問。複数ターンの追問対応。', shortcutSettings: 'ショートカット設定', - intro: { - eyebrow: '読むことから、理解することへ', - title: '気になる文章を選んで、\n声で質問。', - desc: '概念の説明や要点の整理、さらに深い質問も。回答はフロートパネルに表示されます。', - }, - example: { - title: '会話の例', - selection: '選択したテキスト', - term: 'ファインマン・テクニック', - context: 'は、他の人に説明することで理解の抜けを見つける学習法です。', - question: 'もっと簡単な言葉で説明して。', - answer: '友人に教えるようなものです。分かりやすく説明できるほど、理解が深まっています。', - }, guide: { openTitle: 'パネルを開く', openDesc: '{{hotkey}} で会話を始めます。', @@ -757,17 +743,10 @@ export const ja: typeof zhCN = { followup: '録音キーでもう一度、続けて質問できます。', dismiss: 'パネルを閉じて、この会話を終了', }, - statusEnabled: '有効', - statusDisabled: '無効', hotkey: { title: 'フロートウィンドウのショートカット', - desc: 'パネルの開閉を制御。パネル内の録音は {{recordHotkey}}。', - optionDisabled: '無効', - chordWarning: '', }, save: { - hotkeyRegisterFailed: '選択追問ショートカットの登録に失敗しました。設定は保存されていません。', - hotkeySaveFailed: '選択追問ショートカットの保存に失敗しました。もう一度お試しください。', historySaveFailed: 'Q&A 履歴設定の保存に失敗しました。もう一度お試しください。', }, history: { @@ -776,15 +755,7 @@ export const ja: typeof zhCN = { }, howto: { title: '使い方', - step1: '{{hotkey}} でパネルを開く。', step2: '任意のアプリでテキストを選択。', - step3: '{{recordHotkey}} で録音、再度押して送信。', - step4: '{{recordHotkey}} で続けて追問可能。', - step5: 'Esc でパネルを閉じ、履歴をクリア。', - windowTitle: 'ウィンドウの位置 + ドラッグ + ピン留め', - windowDesc: 'パネルはドラッグ可能で位置を記憶。ピン留めで開いたままに。', - privacyTitle: 'プライバシー契約', - privacyDesc: '選択テキストはメモリ上のみ。パネルを閉じると破棄。4000 字超は自動切り詰め。', }, }, settings: { diff --git a/openless-all/app/src/i18n/ko.ts b/openless-all/app/src/i18n/ko.ts index 3552a5c6..6e18df4b 100644 --- a/openless-all/app/src/i18n/ko.ts +++ b/openless-all/app/src/i18n/ko.ts @@ -730,23 +730,9 @@ export const ko: typeof zhCN = { }, }, selectionAsk: { - kicker: 'SELECTION ASK', title: '선택 질문', desc: '텍스트 선택 후 음성으로 질문. 다중 라운드 후속 질문 지원.', shortcutSettings: '단축키 설정', - intro: { - eyebrow: '읽기에서 이해로', - title: '궁금한 글을 선택하고,\n말로 질문하세요.', - desc: '개념을 이해하고, 핵심을 정리하거나 더 깊이 알아보세요. 답변은 플로팅 패널에 표시됩니다.', - }, - example: { - title: '대화 예시', - selection: '선택한 텍스트', - term: '파인만 학습법', - context: '은 다른 사람에게 설명하면서 이해가 부족한 부분을 찾는 방법입니다.', - question: '더 쉬운 말로 설명해 줘.', - answer: '친구에게 가르친다고 생각해 보세요. 명확하게 설명할수록 더 깊이 이해한 것입니다.', - }, guide: { openTitle: '질문 패널 열기', openDesc: '{{hotkey}}로 대화를 시작하세요.', @@ -757,17 +743,10 @@ export const ko: typeof zhCN = { followup: '녹음 단축키를 다시 눌러 후속 질문을 할 수 있어요.', dismiss: '패널을 닫고 이번 대화 종료', }, - statusEnabled: '활성화됨', - statusDisabled: '비활성화됨', hotkey: { title: '플로팅 창 단축키', - desc: '패널 열기/닫기 제어. 패널 내 녹음은 {{recordHotkey}} 사용.', - optionDisabled: '비활성화', - chordWarning: '', }, save: { - hotkeyRegisterFailed: '선택 질문 단축키 등록에 실패했습니다. 설정은 저장되지 않았습니다.', - hotkeySaveFailed: '선택 질문 단축키 저장에 실패했습니다. 다시 시도하세요.', historySaveFailed: 'Q&A 기록 설정 저장에 실패했습니다. 다시 시도하세요.', }, history: { @@ -776,15 +755,7 @@ export const ko: typeof zhCN = { }, howto: { title: '사용 방법', - step1: '{{hotkey}} 로 패널 열기.', step2: '아무 앱에서 텍스트 선택.', - step3: '{{recordHotkey}} 로 녹음, 다시 눌러 제출.', - step4: '{{recordHotkey}} 로 계속 후속 질문 가능.', - step5: 'Esc 로 패널 닫기 및 기록 삭제.', - windowTitle: '창 위치 + 드래그 + 고정', - windowDesc: '패널은 드래그 가능하며 위치를 기억합니다. 고정하면 열린 상태 유지.', - privacyTitle: '프라이버시 계약', - privacyDesc: '선택 텍스트는 메모리에만 존재하며 패널 닫으면 삭제. 4000 자 초과 시 자동 축소.', }, }, settings: { diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index feba1c7d..f2ec6ce0 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -1,5 +1,5 @@ // 简体中文资源 — 与产品当前文案保持一致。 -// 添加新 key 时,必须同步更新 en.ts,否则首次切换到 English 会回落到中文残留。 +// 五个语言文件共享同一结构;新增或删除 key 时同步更新全部语言。 export const zhCN = { app: { @@ -726,23 +726,9 @@ export const zhCN = { }, }, selectionAsk: { - kicker: 'SELECTION ASK', title: '划词追问', desc: '选中文字后语音提问,支持多轮追问。', shortcutSettings: '快捷键设置', - intro: { - eyebrow: '在文字与理解之间', - title: '选中一段文字,\n把问题说出来。', - desc: '解释概念、提炼要点,或继续深入。答案会出现在独立浮窗里。', - }, - example: { - title: '对话示例', - selection: '选中的文字', - term: '费曼学习法', - context: ':通过向他人讲解,发现自己理解中的空白。', - question: '用更简单的话解释一下。', - answer: '就像给朋友讲课:讲得越明白,你就理解得越透彻。', - }, guide: { openTitle: '打开追问浮窗', openDesc: '按 {{hotkey}},开始一轮对话。', @@ -753,17 +739,10 @@ export const zhCN = { followup: '继续使用录音快捷键,即可多轮追问。', dismiss: '关闭浮窗,结束本次对话', }, - statusEnabled: '已启用', - statusDisabled: '未启用', hotkey: { title: '弹出浮窗的快捷键', - desc: '控制浮窗开关。浮窗内录音用 {{recordHotkey}}。', - optionDisabled: '不启用', - chordWarning: '', }, save: { - hotkeyRegisterFailed: '划词追问快捷键注册失败,未继续保存。', - hotkeySaveFailed: '划词追问快捷键保存失败,请重试。', historySaveFailed: 'Q&A 历史保存设置保存失败,请重试。', }, history: { @@ -772,15 +751,7 @@ export const zhCN = { }, howto: { title: '使用方法', - step1: '按 {{hotkey}} 打开浮窗。', step2: '在任意 app 选中文字。', - step3: '按 {{recordHotkey}} 录音,再按一次提交。', - step4: '可继续按 {{recordHotkey}} 多轮追问。', - step5: '按 Esc 关闭浮窗并清空历史。', - windowTitle: '浮窗操作', - windowDesc: '浮窗可拖动,位置自动记忆。钉住可保持窗口不关。', - privacyTitle: '隐私', - privacyDesc: '选中文本仅存于内存,关闭浮窗即销毁。超 4000 字符自动截断。', }, }, settings: { diff --git a/openless-all/app/src/i18n/zh-TW.ts b/openless-all/app/src/i18n/zh-TW.ts index 1a79c58e..4d2d09f1 100644 --- a/openless-all/app/src/i18n/zh-TW.ts +++ b/openless-all/app/src/i18n/zh-TW.ts @@ -728,23 +728,9 @@ export const zhTW: typeof zhCN = { }, }, selectionAsk: { - kicker: 'SELECTION ASK', title: '劃詞追問', desc: '選中文字後語音提問,支援多輪追問。', shortcutSettings: '快捷鍵設定', - intro: { - eyebrow: '在文字與理解之間', - title: '選取一段文字,\n把問題說出來。', - desc: '解釋概念、整理重點,或繼續深入。答案會出現在獨立浮窗裡。', - }, - example: { - title: '對話範例', - selection: '選取的文字', - term: '費曼學習法', - context: ':透過向他人講解,發現自己理解中的空白。', - question: '用更簡單的話解釋一下。', - answer: '就像給朋友講課:講得越明白,你就理解得越透徹。', - }, guide: { openTitle: '開啟追問浮窗', openDesc: '按 {{hotkey}},開始一輪對話。', @@ -755,17 +741,10 @@ export const zhTW: typeof zhCN = { followup: '繼續使用錄音快捷鍵,即可多輪追問。', dismiss: '關閉浮窗,結束本次對話', }, - statusEnabled: '已啓用', - statusDisabled: '未啓用', hotkey: { title: '彈出浮窗的快捷鍵', - desc: '控制浮窗開關。浮窗內錄音用 {{recordHotkey}}。', - optionDisabled: '不啓用', - chordWarning: '', }, save: { - hotkeyRegisterFailed: '劃詞追問快捷鍵註冊失敗,未繼續保存。', - hotkeySaveFailed: '劃詞追問快捷鍵保存失敗,請重試。', historySaveFailed: 'Q&A 歷史保存設置保存失敗,請重試。', }, history: { @@ -774,15 +753,7 @@ export const zhTW: typeof zhCN = { }, howto: { title: '使用方法', - step1: '按 {{hotkey}} 開啟浮窗。', step2: '在任意 app 選中文字。', - step3: '按 {{recordHotkey}} 錄音,再按一次提交。', - step4: '可繼續按 {{recordHotkey}} 多輪追問。', - step5: '按 Esc 關閉浮窗並清空歷史。', - windowTitle: '浮窗位置 + 拖動 + 釘住', - windowDesc: '浮窗可拖動,位置自動記憶。釘住可保持視窗不關。', - privacyTitle: '隱私契約', - privacyDesc: '選中文本僅存於記憶體,關閉浮窗即銷毀。超 4000 字元自動截斷。', }, }, settings: { diff --git a/openless-all/app/src/lib/audioCue.ts b/openless-all/app/src/lib/audioCue.ts index 60a26ab6..bf91ed6f 100644 --- a/openless-all/app/src/lib/audioCue.ts +++ b/openless-all/app/src/lib/audioCue.ts @@ -137,7 +137,7 @@ function resolveAudioContextCtor(): AudioContextCtor | null { function getContext(): AudioContext | null { const Ctor = resolveAudioContextCtor(); if (!Ctor) return null; - // 已 closed 的 ctx 无法复活,丢弃后重建——否则后续所有提示音永久静默(本 bug 根因)。 + // 已 closed 的 ctx 无法复活,丢弃后重建——否则后续所有提示音永久静默。 // 旧 ctx 上挂着的 activeVoices 也一并清空,避免去叠音时操作已失效节点。 if (sharedCtx && audioContextActionForState(sharedCtx.state) === 'recreate') { sharedCtx = null; diff --git a/openless-all/app/src/lib/hotkey.ts b/openless-all/app/src/lib/hotkey.ts index 1196e018..7291ab7b 100644 --- a/openless-all/app/src/lib/hotkey.ts +++ b/openless-all/app/src/lib/hotkey.ts @@ -111,10 +111,7 @@ export function getHotkeyBindingLabel(binding: HotkeyBinding | null | undefined) export function getHotkeyCodeLabel(code: string): string { const zh = i18n.language.toLowerCase().startsWith('zh'); const isMac = currentPlatform().isMac; - // Alt 在 macOS 是 Option(⌥),Meta 在 macOS 是 Command(⌘),Linux 上 Meta 是 Super。 - // 之前对所有平台一律返回 "Alt" / "Win",QA 浮窗里 macOS 用户的"右 Option" 被显示成 - // "右 Alt","左 Cmd" 被显示成 "左 Win"——平台错配。下面按平台分流。 - // 用 ⌥/⌘ 与 formatPrimary 的输出("Right ⌥" 等)保持一致。 + // macOS 使用 Option/Command 符号,并与组合键标签 formatPrimary 保持一致。 const labels: Record = { ControlLeft: zh ? '左Ctrl' : 'Left Ctrl', ControlRight: zh ? '右Ctrl' : 'Right Ctrl', @@ -308,10 +305,6 @@ export function modifiersFromPressedCodes( : genericModifiersFromPressedCodes(codes); } -function modifierDisplayName(tag: string, platform: { isMac: boolean; isWindows: boolean }): string { - return sideModifierDisplayName(tag, platform); -} - function sideModifierDisplayName(tag: string, platform: { isMac: boolean; isWindows: boolean }): string { const sideLabels: Record = platform.isMac ? { diff --git a/openless-all/app/src/lib/hotkeyRecorder.test.ts b/openless-all/app/src/lib/hotkeyRecorder.test.ts deleted file mode 100644 index a1a5841a..00000000 --- a/openless-all/app/src/lib/hotkeyRecorder.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { - createHotkeyRecorderState, - orderHotkeyCodes, - updateHotkeyRecorderState, -} from './hotkeyRecorder'; - -function assertEqual(actual: T, expected: T, name: string) { - if (actual !== expected) { - throw new Error(`${name}: expected ${expected}, got ${actual}`); - } -} - -function assertDeepEqual(actual: unknown, expected: unknown, name: string) { - const actualJson = JSON.stringify(actual); - const expectedJson = JSON.stringify(expected); - if (actualJson !== expectedJson) { - throw new Error(`${name}: expected ${expectedJson}, got ${actualJson}`); - } -} - -function apply( - state = createHotkeyRecorderState(), - code: string, - pressed: boolean, -) { - const next = updateHotkeyRecorderState(state, code, pressed); - return next; -} - -{ - let result = apply(undefined, 'ControlLeft', true); - assertDeepEqual(result.state.draftCodes, ['ControlLeft'], 'tracks first pressed key'); - assertEqual(result.commitCodes, null, 'does not commit until release'); - - result = apply(result.state, 'ControlLeft', false); - assertDeepEqual(result.commitCodes, ['ControlLeft'], 'commits single key on release'); - - result = apply(createHotkeyRecorderState(), 'KeyK', true); - assertDeepEqual(result.state.draftCodes, ['KeyK'], 'starts a new recording state cleanly'); - assertEqual(result.commitCodes, null, 'new keydown does not include old released keys'); -} - -{ - let result = apply(undefined, 'ControlLeft', true); - result = apply(result.state, 'KeyK', true); - assertDeepEqual(result.state.draftCodes, ['ControlLeft', 'KeyK'], 'records keyboard combo draft'); - - result = apply(result.state, 'ControlLeft', false); - assertEqual(result.commitCodes, null, 'keyboard combo waits for final release'); - assertDeepEqual(result.state.draftCodes, ['ControlLeft', 'KeyK'], 'released combo member stays in draft only'); - - result = apply(result.state, 'KeyK', false); - assertDeepEqual(result.commitCodes, ['ControlLeft', 'KeyK'], 'keyboard combo commits after all keys release'); - assertDeepEqual(result.state, createHotkeyRecorderState(), 'state resets after commit'); -} - -{ - let result = apply(undefined, 'Mouse4', true); - assertDeepEqual(result.state.draftCodes, ['Mouse4'], 'tracks mouse button as draft'); - assertEqual(result.commitCodes, null, 'mouse button does not commit on mousedown'); - - result = apply(result.state, 'ControlLeft', true); - assertDeepEqual(result.state.draftCodes, ['ControlLeft', 'Mouse4'], 'records keyboard plus mouse combo'); - assertEqual(result.commitCodes, null, 'combo does not commit while inputs remain pressed'); - - result = apply(result.state, 'Mouse4', false); - assertEqual(result.commitCodes, null, 'releasing one combo member does not commit early'); - - result = apply(result.state, 'ControlLeft', false); - assertDeepEqual(result.commitCodes, ['ControlLeft', 'Mouse4'], 'commits combo after final release'); -} - -{ - let result = apply(undefined, 'ControlLeft', true); - result = apply(result.state, 'Mouse5', true); - assertDeepEqual(result.state.draftCodes, ['ControlLeft', 'Mouse5'], 'records mouse button pressed after keyboard'); - - result = apply(result.state, 'ControlLeft', false); - assertEqual(result.commitCodes, null, 'mouse combo keeps waiting while mouse remains pressed'); - - result = apply(result.state, 'Mouse5', false); - assertDeepEqual(result.commitCodes, ['ControlLeft', 'Mouse5'], 'commits mouse-last combo after mouse release'); -} - -assertDeepEqual(orderHotkeyCodes(['Mouse4', 'ControlLeft']), ['ControlLeft', 'Mouse4'], 'orders mouse after modifiers'); diff --git a/openless-all/app/src/lib/hotkeyRecorder.ts b/openless-all/app/src/lib/hotkeyRecorder.ts deleted file mode 100644 index 8ab692f5..00000000 --- a/openless-all/app/src/lib/hotkeyRecorder.ts +++ /dev/null @@ -1,70 +0,0 @@ -export interface HotkeyRecorderState { - pressedCodes: string[]; - draftCodes: string[]; -} - -export interface HotkeyRecorderUpdate { - state: HotkeyRecorderState; - commitCodes: string[] | null; -} - -export function createHotkeyRecorderState(): HotkeyRecorderState { - return { - pressedCodes: [], - draftCodes: [], - }; -} - -export function updateHotkeyRecorderState( - state: HotkeyRecorderState, - code: string, - pressed: boolean, -): HotkeyRecorderUpdate { - const active = new Set(state.pressedCodes); - if (pressed) { - active.add(code); - } else { - active.delete(code); - } - - const pressedCodes = orderHotkeyCodes([...active]); - const draftCodes = pressed ? pressedCodes : state.draftCodes; - const shouldCommit = !pressed && pressedCodes.length === 0 && draftCodes.length > 0; - - return { - state: shouldCommit ? createHotkeyRecorderState() : { pressedCodes, draftCodes }, - commitCodes: shouldCommit ? draftCodes : null, - }; -} - -export function orderHotkeyCodes(codes: string[]): string[] { - const seen = new Set(); - return codes - .filter(code => { - if (!code || seen.has(code)) return false; - seen.add(code); - return true; - }) - .sort((a, b) => hotkeyCodeRank(a) - hotkeyCodeRank(b)); -} - -function hotkeyCodeRank(code: string): number { - const index = HOTKEY_CODE_ORDER.indexOf(code); - if (index >= 0) return index; - if (/^Key[A-Z]$/.test(code)) return 100 + code.charCodeAt(3); - if (/^Digit[0-9]$/.test(code)) return 200 + Number(code.slice(5)); - if (/^F([1-9]|1[0-9]|2[0-4])$/.test(code)) return 300 + Number(code.slice(1)); - if (/^Numpad[0-9]$/.test(code)) return 400 + Number(code.slice(6)); - return 1000; -} - -const HOTKEY_CODE_ORDER = [ - 'ControlLeft', 'ControlRight', 'AltLeft', 'AltRight', 'ShiftLeft', 'ShiftRight', - 'MetaLeft', 'MetaRight', 'Fn', 'FnLock', 'CapsLock', 'ScrollLock', 'Pause', - 'PrintScreen', 'Backspace', 'Tab', 'Enter', 'Space', 'Insert', 'Delete', 'Home', - 'End', 'PageUp', 'PageDown', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', - 'ContextMenu', 'Backquote', 'Minus', 'Equal', 'BracketLeft', 'BracketRight', - 'Backslash', 'Semicolon', 'Quote', 'Comma', 'Period', 'Slash', 'NumpadAdd', - 'NumpadSubtract', 'NumpadMultiply', 'NumpadDivide', 'NumpadDecimal', 'NumpadEnter', - 'Mouse4', 'Mouse5', -]; diff --git a/openless-all/app/src/lib/ipc/channels.ts b/openless-all/app/src/lib/ipc/channels.ts index 14ca6484..805d6393 100644 --- a/openless-all/app/src/lib/ipc/channels.ts +++ b/openless-all/app/src/lib/ipc/channels.ts @@ -1,10 +1,5 @@ -// 渠道卡片的 IPC 封装。 -// -// 一张卡片 = 一份可命名、可排序、可开关的供应商配置;同一家厂商可以有多张卡片 -// (多把 key)。列表里**第一个启用的**就是当前生效的渠道 —— 后端不另存"当前选中", -// 排序即优先级(见 docs/provider-channels-plan.md)。 -// -// 凭据不走这里:按渠道 id 调 readCredential/setCredential(account, value, id)。 +// 渠道配置的 typed IPC:排序、启停、名称和手动验证结果。 +// Core 选择排序后的第一个启用渠道;凭据通过 readCredential/setCredential 按渠道 ID 读写。 import { invokeOrMock } from "./shared" diff --git a/openless-all/app/src/lib/ipc/style-packs.ts b/openless-all/app/src/lib/ipc/style-packs.ts index 0485b063..1274e3f5 100644 --- a/openless-all/app/src/lib/ipc/style-packs.ts +++ b/openless-all/app/src/lib/ipc/style-packs.ts @@ -2,7 +2,6 @@ import type { PolishMode, StylePack, StylePackRuntimeDiagnostics } from "../type import { invokeOrMock } from "./shared" import { cloneMockStylePacks, - cloneStylePack, composeMockStylePackRuntimeDiagnostics, mockSetDefaultPolishMode, mockSetStyleEnabled, diff --git a/openless-all/app/src/lib/ipc/vocab.ts b/openless-all/app/src/lib/ipc/vocab.ts index 8c5cc848..461c40f9 100644 --- a/openless-all/app/src/lib/ipc/vocab.ts +++ b/openless-all/app/src/lib/ipc/vocab.ts @@ -1,7 +1,6 @@ import type { CorrectionRule, DictionaryEntry, - PendingCorrection, VocabPresetStore, } from "../types" import { invokeOrMock } from "./shared" diff --git a/openless-all/app/src/lib/stylePackIcon.ts b/openless-all/app/src/lib/stylePackIcon.ts index 2741200f..7d1ed589 100644 --- a/openless-all/app/src/lib/stylePackIcon.ts +++ b/openless-all/app/src/lib/stylePackIcon.ts @@ -4,6 +4,7 @@ const MAX_SVG_BYTES = 256 * 1024; const MAX_PNG_BYTES = 64 * 1024; const ICON_SIZE = 128; +/** 清理 SVG 的可执行内容和外部引用,保留图形、渐变与内部遮罩。 */ export function sanitizeStyleSvg(source: string): string { if (/ !match[2].trim().startsWith('#')); } +/** 转成风格包已有的 PNG 资源格式;128px 输出仍须满足 Core 的 64 KiB 上限。 */ export async function rasterizeStyleSvg(file: File): Promise { if (!file.name.toLowerCase().endsWith('.svg') || (file.type && file.type !== 'image/svg+xml') || file.size > MAX_SVG_BYTES) { throw new Error('invalidSvg'); @@ -70,6 +72,7 @@ export async function rasterizeStyleSvg(file: File): Promise { } } +/** 图片仅通过 img 展示;拒绝可执行类型和超过资源上限的返回值。 */ export function isStyleIconDataUrl(value: string | null): value is string { return value !== null && value.length < 90_000 && /^data:image\/(?:png|jpeg|webp);base64,[A-Za-z0-9+/]+={0,2}$/.test(value); } diff --git a/openless-all/app/src/lib/stylePrefs.test.ts b/openless-all/app/src/lib/stylePrefs.test.ts deleted file mode 100644 index 83ad3653..00000000 --- a/openless-all/app/src/lib/stylePrefs.test.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { - applyStylePreferencesNotification, - isStyleMasterEnabled, - rollbackDefaultAndEnabledChange, - persistStylePreferenceChange, - rollbackStyleEnabledChange, - rollbackWholeStylePreferences, - styleDefaultModePreferences, - styleMasterFallbackModes, - styleMasterOffPreferences, -} from './stylePrefs'; -import type { UserPreferences } from './types'; - -function assert(condition: boolean, message: string) { - if (!condition) throw new Error(message); -} - -const previousPrefs: UserPreferences = { - hotkey: { trigger: 'rightOption', mode: 'toggle' }, - dictationHotkey: { primary: 'RightOption', modifiers: [] }, - pipelineMode: 'traditional', - multimodalPipelineEnabled: false, - activeOmniProvider: 'custom', - selectionPolishHotkey: { primary: 'RightControl', modifiers: [] }, - selectionPolishStylePackId: 'builtin.light', - selectionPolishOutputMode: 'directReplace', - selectionVoiceEnabled: false, - selectionVoiceIntentMode: 'auto', - selectionVoiceManualIntent: 'question', - selectionVoiceEditKeywords: ['翻译', '替换'], - cursorContextEnabled: false, - showOverviewActivityHeatmap: true, - stackedRowLayout: false, - conservativeLayout: false, - defaultMode: 'light', - enabledModes: ['raw', 'light', 'structured'], - activeStylePackId: '', - styleSystemPrompts: { - raw: 'raw system prompt', - light: 'light system prompt', - structured: 'structured system prompt', - formal: 'formal system prompt', - }, - customStylePrompts: { raw: '', light: '', structured: '', formal: '' }, - launchAtLogin: false, - showCapsule: true, - capsuleStyle: 'siri', - muteDuringRecording: false, - audioCueOnRecord: true, - silenceAutoStopEnabled: false, - silenceAutoStopSeconds: 3, - microphoneDeviceName: '', - activeAsrProvider: 'volcengine', - activeLlmProvider: 'ark', - llmThinkingEnabled: false, - useSystemProxy: true, - restoreClipboardAfterPaste: true, - pasteShortcut: 'ctrlV', - allowNonTsfInsertionFallback: true, - windowsInsertionMode: 'tsf', - windowsSendInputNewlineMode: 'enter', - macosNewlineMode: 'shiftReturn', - windowsSendInputInsertionOnly: false, - windowsShowOpenlessInKeyboardList: true, - workingLanguages: ['简体中文'], - translationTargetLanguage: '', - chineseScriptPreference: 'auto', - outputLanguagePreference: 'auto', - qaHotkey: null, - qaSaveHistory: false, - customComboHotkey: null, - translationHotkey: { primary: 'Shift', modifiers: [] }, - switchStyleHotkey: { primary: 'S', modifiers: ['alt'] }, - openAppHotkey: { primary: 'O', modifiers: ['alt'] }, - stylePackHotkeys: [], - codingAgentEnabled: false, - codingAgentProvider: 'claude-code-cli', - codingAgentModel: null, - codingAgentPermissionMode: 'acceptEdits', - codingAgentWorkdir: null, - codingAgentExe: null, - codingAgentVoiceHotkey: { primary: 'LeftControl', modifiers: [] }, - codingAgentPanelHotkey: { primary: 'Enter', modifiers: ['cmd', 'shift'] }, - codingAgentQuickHotkey: null, - localAsrActiveModel: '', - localWhisperActiveModel: 'whisper-large-v3-turbo', - localAsrMirror: 'huggingface', - localAsrKeepLoadedSecs: 300, - foundryLocalAsrModel: '', - foundryLocalRuntimeSource: 'auto', - foundryLocalAsrLanguageHint: '', - foundryLocalAsrKeepLoadedSecs: 300, - sherpaOnnxModel: 'sense-voice-small-zh', - sherpaOnnxLanguageHint: '', - sherpaOnnxKeepLoadedSecs: 300, - historyRetentionDays: 7, - polishContextWindowMinutes: 5, - startMinimized: false, - themeMode: 'system', - updateChannel: 'stable', - streamingInsert: true, - streamingInsertDefaultMigrated: true, - streamingInsertSaveClipboard: true, - autoUpdateCheck: true, - historyMaxEntries: null, - recordAudioForDebug: false, - audioRecordingMaxEntries: null, - marketplaceBaseUrl: '', - marketplaceDevLogin: '', - remoteInputEnabled: false, - remoteInputPort: 8443, - remoteInputPin: '000000', - remoteInputDefaultMode: 'toggle', - androidInsertStrategy: 'accessibility', - androidOverlayTrigger: 'background', - androidOverlayActivationMode: 'tap', - androidOverlayLeftSwipeAction: 'translation', - androidOverlayCancelSwipeDirection: 'up', - androidOverlaySizeDp: 72, -}; - -const nextPrefs: UserPreferences = { - ...previousPrefs, - enabledModes: [], -}; - -const states: UserPreferences[] = []; -const errors: string[] = []; -let firstCurrentPrefs: UserPreferences | null = previousPrefs; -const saved = await persistStylePreferenceChange( - nextPrefs, - async () => { - throw 'disk full'; - }, - update => { - firstCurrentPrefs = typeof update === 'function' ? update(firstCurrentPrefs) : update; - if (firstCurrentPrefs) states.push(firstCurrentPrefs); - }, - message => errors.push(message), - rollbackWholeStylePreferences(previousPrefs, nextPrefs), -); - -assert(saved === false, 'setSettings reject should report save failure'); -assert(states.length === 2, `expected optimistic state then rollback, got ${states.length} updates`); -assert(states[0] === nextPrefs, 'first state update should be the optimistic next prefs'); -assert( - states[1].enabledModes.join(',') === previousPrefs.enabledModes.join(','), - 'second state update should roll back enabled modes to previous prefs', -); -assert(errors[0] === 'disk full', `expected backend error message, got ${errors[0]}`); - -let currentPrefs: UserPreferences | null = previousPrefs; -const disableLightPrefs: UserPreferences = { - ...previousPrefs, - enabledModes: ['raw', 'structured'], -}; -const disableStructuredAfterLightPrefs: UserPreferences = { - ...previousPrefs, - enabledModes: ['raw'], -}; -const overlapSaved = await persistStylePreferenceChange( - disableLightPrefs, - async () => { - currentPrefs = disableStructuredAfterLightPrefs; - throw 'slow failure'; - }, - update => { - currentPrefs = typeof update === 'function' ? update(currentPrefs) : update; - }, - () => undefined, - rollbackStyleEnabledChange('light', previousPrefs, disableLightPrefs), -); - -assert(overlapSaved === false, 'overlapped style save should still report failure'); -assert( - currentPrefs?.enabledModes.includes('light') === true, - 'failed light toggle should roll back only the light mode', -); -assert( - currentPrefs?.enabledModes.includes('structured') === false, - 'failed light toggle should preserve newer structured edit', -); - -const notifiedPrefs: UserPreferences = { - ...previousPrefs, - defaultMode: 'formal', - enabledModes: ['raw', 'formal'], -}; -const syncedPrefs = applyStylePreferencesNotification(previousPrefs, notifiedPrefs); -assert(syncedPrefs === notifiedPrefs, 'prefs notification should replace stale style page prefs'); - -const masterOffPrefs = styleMasterOffPreferences(previousPrefs); -assert( - masterOffPrefs.enabledModes.join(',') === 'raw,light', - `master toggle off should persist raw and current default, got ${masterOffPrefs.enabledModes.join(',')}`, -); - -const masterFallback = styleMasterFallbackModes('light'); -assert( - masterFallback.join(',') === 'raw,light', - `master toggle off should preserve raw and current default, got ${masterFallback.join(',')}`, -); -assert( - isStyleMasterEnabled({ ...previousPrefs, enabledModes: masterFallback }) === false, - 'master toggle should render off when only raw and default remain enabled', -); -assert( - isStyleMasterEnabled(previousPrefs) === true, - 'master toggle should render on when extra styles remain enabled', -); -const rawFallback = styleMasterFallbackModes('raw'); -assert( - rawFallback.join(',') === 'raw', - `raw default fallback should not duplicate raw, got ${rawFallback.join(',')}`, -); - - -const defaultAfterMasterOff = styleDefaultModePreferences( - { ...previousPrefs, enabledModes: ['raw', 'light'] }, - 'formal', -); -assert( - defaultAfterMasterOff.defaultMode === 'formal' && defaultAfterMasterOff.enabledModes.join(',') === 'raw,formal', - `default change while master is off should refresh fallback modes, got ${defaultAfterMasterOff.defaultMode}/${defaultAfterMasterOff.enabledModes.join(',')}`, -); -assert( - isStyleMasterEnabled(defaultAfterMasterOff) === false, - 'master toggle should stay off after changing default while off', -); - -let rolledBackDefaultAndEnabled: UserPreferences | null = defaultAfterMasterOff; -const rollbackDefaultAndEnabled = rollbackDefaultAndEnabledChange( - { ...previousPrefs, enabledModes: ['raw', 'light'] }, - defaultAfterMasterOff, -); -rolledBackDefaultAndEnabled = rollbackDefaultAndEnabled(rolledBackDefaultAndEnabled); -assert( - rolledBackDefaultAndEnabled?.defaultMode === 'light' && rolledBackDefaultAndEnabled.enabledModes.join(',') === 'raw,light', - 'failed off-state default save should roll back both default mode and enabled modes', -); diff --git a/openless-all/app/src/lib/stylePrefs.ts b/openless-all/app/src/lib/stylePrefs.ts deleted file mode 100644 index bec0a844..00000000 --- a/openless-all/app/src/lib/stylePrefs.ts +++ /dev/null @@ -1,120 +0,0 @@ -import type { PolishMode, UserPreferences } from './types'; - -type StylePrefsState = UserPreferences | null; -type StylePrefsSetter = (prefs: StylePrefsState | ((current: StylePrefsState) => StylePrefsState)) => void; -type StylePrefsRollback = (current: StylePrefsState) => StylePrefsState; - -export function styleSaveErrorMessage(error: unknown): string { - if (error instanceof Error) return error.message; - if (typeof error === 'string') return error; - return String(error); -} - -export async function persistStylePreferenceChange( - nextPrefs: UserPreferences, - persist: () => Promise, - setPrefs: StylePrefsSetter, - onFailure: (message: string) => void, - rollback: StylePrefsRollback, -): Promise { - setPrefs(nextPrefs); - try { - await persist(); - return true; - } catch (error) { - setPrefs(current => rollback(current)); - onFailure(styleSaveErrorMessage(error)); - return false; - } -} - -export function applyStylePreferencesNotification( - _current: UserPreferences | null, - incoming: UserPreferences, -): UserPreferences { - return incoming; -} - -export function styleMasterFallbackModes(defaultMode: PolishMode): PolishMode[] { - return defaultMode === 'raw' ? ['raw'] : ['raw', defaultMode]; -} - -export function isStyleMasterEnabled(prefs: UserPreferences): boolean { - return !sameModeSet(prefs.enabledModes, styleMasterFallbackModes(prefs.defaultMode)); -} - -export function styleMasterOffPreferences(prefs: UserPreferences): UserPreferences { - return { ...prefs, enabledModes: styleMasterFallbackModes(prefs.defaultMode) }; -} - -export function styleDefaultModePreferences(prefs: UserPreferences, mode: PolishMode): UserPreferences { - const nextPrefs = { ...prefs, defaultMode: mode }; - return isStyleMasterEnabled(prefs) ? nextPrefs : styleMasterOffPreferences(nextPrefs); -} - -export function rollbackDefaultModeChange( - previousPrefs: UserPreferences, - nextPrefs: UserPreferences, -): StylePrefsRollback { - return current => { - if (!current || current.defaultMode !== nextPrefs.defaultMode) return current; - return { ...current, defaultMode: previousPrefs.defaultMode }; - }; -} - -export function rollbackStyleEnabledChange( - mode: PolishMode, - previousPrefs: UserPreferences, - nextPrefs: UserPreferences, -): StylePrefsRollback { - const previousEnabled = previousPrefs.enabledModes.includes(mode); - const nextEnabled = nextPrefs.enabledModes.includes(mode); - return current => { - if (!current || current.enabledModes.includes(mode) !== nextEnabled) return current; - const enabledModes = previousEnabled - ? Array.from(new Set([...current.enabledModes, mode])) - : current.enabledModes.filter(m => m !== mode); - return { ...current, enabledModes }; - }; -} - -export function rollbackWholeStylePreferences( - previousPrefs: UserPreferences, - nextPrefs: UserPreferences, -): StylePrefsRollback { - return current => { - if (!current || !sameModes(current.enabledModes, nextPrefs.enabledModes)) return current; - return { ...current, enabledModes: previousPrefs.enabledModes }; - }; -} - -export function rollbackDefaultAndEnabledChange( - previousPrefs: UserPreferences, - nextPrefs: UserPreferences, -): StylePrefsRollback { - return current => { - if ( - !current || - current.defaultMode !== nextPrefs.defaultMode || - !sameModes(current.enabledModes, nextPrefs.enabledModes) - ) { - return current; - } - return { - ...current, - defaultMode: previousPrefs.defaultMode, - enabledModes: previousPrefs.enabledModes, - }; - }; -} - -function sameModeSet(left: PolishMode[], right: PolishMode[]): boolean { - if (left.length !== right.length) return false; - const rightSet = new Set(right); - return left.every(mode => rightSet.has(mode)); -} - -function sameModes(left: PolishMode[], right: PolishMode[]): boolean { - if (left.length !== right.length) return false; - return left.every((mode, index) => mode === right[index]); -} diff --git a/openless-all/app/src/lib/useExitMount.ts b/openless-all/app/src/lib/useExitMount.ts index 446e8c93..b0084673 100644 --- a/openless-all/app/src/lib/useExitMount.ts +++ b/openless-all/app/src/lib/useExitMount.ts @@ -1,12 +1,5 @@ -// useExitMount — 条件渲染弹窗/浮层的退出动画门(2.0 UI 走查「从哪来回到哪去」)。 -// -// 问题:`{open && }` 在 open 变 false 的瞬间直接卸载,入场动画没有对应的 -// 退场可播。本钩子把「渲染中」与「打开」解耦:open 变 false 时保持挂载并返回 -// closing=true,让组件反向播放入场动画,exitMs 后才真正卸载。 -// -// 用法: -// const gate = useExitMount(open); -// {gate.mounted && } +// 关闭浮层时保持挂载至退出动画结束;再次打开会取消待卸载计时器。 +// 调用方用 mounted 控制渲染、closing 控制退场样式,exitMs 与 CSS 动画时长保持一致。 import { useEffect, useState } from 'react'; diff --git a/openless-all/app/src/pages/Corrections.tsx b/openless-all/app/src/pages/Corrections.tsx index fcf7ad44..040d4c3d 100644 --- a/openless-all/app/src/pages/Corrections.tsx +++ b/openless-all/app/src/pages/Corrections.tsx @@ -1,4 +1,4 @@ -// Corrections.tsx — 「纠正规则」独立页(2.0 UI 走查:从词典页迁出,归入侧栏「工具」组)。 +// Corrections.tsx — 「纠正规则」独立页。 // 修正常见 ASR 误识别:pattern → replacement,支持 {num} 一个数字通配。 // 自动收集(source === 'learned')的规则可单独筛出并一键清空 —— 与词典页的 // 「自动添加」筛选同一套信任前提:用户随时能看清、能整块撤销。 diff --git a/openless-all/app/src/pages/History.tsx b/openless-all/app/src/pages/History.tsx index c331e09f..c062823b 100644 --- a/openless-all/app/src/pages/History.tsx +++ b/openless-all/app/src/pages/History.tsx @@ -322,7 +322,7 @@ export function History() {
{( !mobile || !mobileDetailOpen) && ( - {/* 2.0 UI 走查:左列整体是一个滚动容器,搜索框吸顶且自带不透明底 —— + {/* 左列整体是一个滚动容器,搜索框吸顶且自带不透明底 —— 列表内容直接从搜索框下方滚过去;样式筛选 chips 与「共 N 条」计数行 删掉,只保留搜索。 */}
@@ -376,7 +376,7 @@ export function History() { setSelectedId(s.id); if (mobile) setMobileDetailOpen(true); }} - // 2.0 UI 走查:选中项不再用蓝色左条 + 淡蓝底 —— 与渠道行同一套 + // 选中项不再用蓝色左条 + 淡蓝底 —— 与渠道行同一套 // 中性语言:圆角 + 灰底 + 细描边。 style={{ width: '100%', padding: '10px 12px', textAlign: 'left', @@ -386,7 +386,7 @@ export function History() { background: selectedId === s.id ? 'var(--ol-surface-2)' : 'transparent', cursor: 'default', fontFamily: 'inherit', marginBottom: 4, transition: 'background 0.16s var(--ol-motion-quick), border-color 0.16s var(--ol-motion-quick)', - // 搜索过滤/新记录插入时,进入结果的行淡入轻降(2.0 UI 走查)。 + // 搜索过滤/新记录插入时,进入结果的行淡入轻降。 animation: 'ol-item-in 0.22s var(--ol-motion-spring) both', }} > @@ -532,7 +532,7 @@ export function History() { {item.rawTranscript || t('history.rawEmpty')}

- {/* 润色结果框同样去蓝:中性 surface-2 底 + 细线描边(2.0 UI 走查)。 */} + {/* 润色结果框同样去蓝:中性 surface-2 底 + 细线描边。 */}
@@ -680,7 +680,7 @@ function RepolishPanel({ session, mobile, allPacks, packsError }: {
{t('history.repolish.title')} - {/* 2.0 UI 走查:常驻说明文字收进「?」徽章,悬停/聚焦才展开全文(与设置页同款)。 */} + {/* 常驻说明文字收进「?」徽章,悬停/聚焦才展开全文(与设置页同款)。 */} {t('history.repolish.noPacks')} ) : ( <> - {/* 2.0 UI 走查:统一用官方 SelectLite,不再混用原生 。 */} (null) // 下载弹框开关:点侧栏「下载新模型」/ 看板「下载」打开。 const [downloadDialogOpen, setDownloadDialogOpen] = useState(false) - const [modelDirs, setModelDirs] = useState>({}) const [progress, setProgress] = useState< Record >({}) @@ -511,21 +508,6 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { ) setSettings(s) setModels(supportedModels) - void Promise.all( - supportedModels.map(async (m) => { - try { - const dir = await getLocalAsrModelDir(m.id) - if (!isCurrent()) return - setModelDirs((current) => - current[m.id] === dir - ? current - : { ...current, [m.id]: dir }, - ) - } catch (err) { - console.warn("[localAsr] Qwen3 model dir query failed", err) - } - }), - ) void refreshEngineStatus() if (IS_WINDOWS) { void refreshFoundryStatus() @@ -1487,8 +1469,6 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { try { setError(null) await revealLocalAsrModelDir(modelId) - const dir = await getLocalAsrModelDir(modelId) - setModelDirs((current) => ({ ...current, [modelId]: dir })) } catch (e) { setError(e instanceof Error ? e.message : String(e)) } finally { @@ -2174,7 +2154,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { {t("localAsr.mirrorDesc")}
- {/* 2.0 UI 走查:统一官方 SelectLite。 */} + void handleMirrorChange(v)} @@ -2292,7 +2272,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { {t("localAsr.keepLoadedDesc")}
- {/* 2.0 UI 走查:统一官方 SelectLite。 */} + {t("localAsr.foundrySelectedModel")} - {/* 2.0 UI 走查:统一官方 SelectLite;滚动保持 + {/* 统一官方 SelectLite;滚动保持 改从行容器 ref 向上找滚动祖先。 */} {t("localAsr.foundryRuntimeSourceLabel")} - {/* 2.0 UI 走查:统一官方 SelectLite。 */} + { @@ -2683,7 +2663,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { }} > {t("localAsr.foundryLanguageLabel")} - {/* 2.0 UI 走查:统一官方 SelectLite。 */} + { @@ -3074,7 +3054,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { }} > {t("localAsr.mirrorLabel")} - {/* 2.0 UI 走查:统一官方 SelectLite。 */} + diff --git a/openless-all/app/src/pages/Overview.tsx b/openless-all/app/src/pages/Overview.tsx index f33534dd..7ad91ef8 100644 --- a/openless-all/app/src/pages/Overview.tsx +++ b/openless-all/app/src/pages/Overview.tsx @@ -229,14 +229,14 @@ export function Overview({ onOpenHistory, onOpenSettings }: OverviewProps) { hasShortcut: Boolean(prefs?.dictationHotkey.primary.trim()), }); const openSettings = (section: OverviewSettingsSection) => onOpenSettings?.(section); - // 2.0 UI 走查:已配置完成的服务商卡不再常驻(没有信息价值),只展示仍待配置的 + // 已配置完成的服务商卡不再常驻(没有信息价值),只展示仍待配置的 // 卡作为提醒;全部配置完成后整组「当前语音服务」隐藏。凭据加载中/拉取失败 // (providers 为空)时保留占位卡,避免页面闪空。 const pendingProviders = setup.providers.filter(p => !p.configured); const showProvidersSection = setup.providers.length === 0 || pendingProviders.length > 0; return ( - // 单屏固定页(2.0 UI 走查):不滚动,撑满外壳给定的高度,所有仪表盘在一屏内 + // 单屏固定页:不滚动,撑满外壳给定的高度,所有仪表盘在一屏内 // 弹性分配;窗口压到很矮时由底部行内部收缩(最近识别列表内滚),页面本身不出滚动条。
{ - // 2.0 UI 走查:年历按日历年铺满——1 月 1 日起、12 月 31 日止,从最左排到最右; + // 年历按日历年铺满——1 月 1 日起、12 月 31 日止,从最左排到最右; // 此前的滚动 365 天窗口会让月份标号从年中开始、右侧留空,观感像「缺数据」。 const now = new Date(); const year = now.getFullYear(); @@ -414,10 +414,10 @@ function ActivityHeatmapCard({ activity }: { activity: ActivityDay[] }) { {t('overview.activityTitle')} - {/* 格子不再封顶:53 列随卡片宽度铺满到右缘(2.0 UI 走查:长度要排到最右侧)。 + {/* 格子不再封顶:53 列随卡片宽度铺满到右缘。 卡片高度收紧为内容高度(flex 默认 0 1 auto:不拉伸、窗口压矮时才收缩), 四边 padding 一致;腾出的高度全部让给上方周期/最近识别行,热力图整体沉底 - (2.0 UI 走查:消除卡片底部大片留白、上方两卡拉高)。 */} + 。 */} - {/* 2.0 UI 走查:宽屏下「工作语言 / 目标语言」并排两栏(窄屏自动叠成一栏), + {/* 宽屏下「工作语言 / 目标语言」并排两栏(窄屏自动叠成一栏), 语言 chips 用对齐的均匀网格代替自由换行,避免参差的标签云。 */}
{/* 1. 工作语言 */} diff --git a/openless-all/app/src/pages/Vocab.tsx b/openless-all/app/src/pages/Vocab.tsx index 896e22ef..74024f81 100644 --- a/openless-all/app/src/pages/Vocab.tsx +++ b/openless-all/app/src/pages/Vocab.tsx @@ -1,4 +1,4 @@ -// Vocab.tsx — 「词典」页(2.0 UI 走查:由词汇表改版)。 +// Vocab.tsx — 「词典」页。 // 结构: // - 顶部:标题 + 右上「新词」入口(弹窗:直接输入 或 从预设模板批量导入) // - 工具行:所有 / 自动添加 / 手动添加 分段筛选 + 右侧圆形搜索(点击向左展开) @@ -61,7 +61,7 @@ export function Vocab() { const editMount = useExitMount(editingEntry !== null); const newWordMount = useExitMount(newWordOpen); - // 词条网格 FLIP(2.0 UI 走查):增删/筛选让行位移时,从旧位置滑到新位置; + // 词条网格 FLIP:增删/筛选让行位移时,从旧位置滑到新位置; // 新卡片入场走 .ol-word-card 的 CSS 动画,删除走 onRemove 里的退场动画。 const cardRefs = useRef(new Map()); const prevCardTops = useRef(new Map()); @@ -410,7 +410,7 @@ export function Vocab() { ))}
- {/* 2.0 UI 走查:圆形控件原地展开成搜索框 —— 放大镜固定在右缘不动, + {/* 圆形控件原地展开成搜索框 —— 放大镜固定在右缘不动, 占位文字「搜索」在框内;收起走同一条 width 过渡(从哪来回到哪去)。 */}
)} - {/* 独立滚动区(2.0 UI 走查):词条多时只有这一格在滚,底部面板固定在视口 + {/* 独立滚动区:词条多时只有这一格在滚,底部面板固定在视口 底缘、白底天然遮挡滚过去的内容;之前网格作为页根 flex item 被压扁、内容 溢出到下方区块背后的穿帮也从根上消除。 */}
void) | null) => void; } | null>(null); diff --git a/openless-all/app/src/pages/settings/ChannelList.tsx b/openless-all/app/src/pages/settings/ChannelList.tsx index f02156b7..0ff07310 100644 --- a/openless-all/app/src/pages/settings/ChannelList.tsx +++ b/openless-all/app/src/pages/settings/ChannelList.tsx @@ -1,11 +1,5 @@ -// 渠道卡片列表 —— LLM 润色与 ASR 语音转写共用同一套交互。 -// -// 心智只有一条:**排序即优先级,列表里第一个启用的就是当前生效的渠道**。 -// 开关关掉的渠道自动沉到列表末尾;后端不另存"当前选中",避免"列表第一张是 A、 -// 实际请求打的是 B"这种两处真相。详见 docs/provider-channels-plan.md。 -// -// 卡片解决的两件事:同一家厂商可以存多把 key;key 之间切换只是拖一下顺序, -// 而不是把旧 key 覆盖掉。 +// LLM 与 ASR 共用的渠道列表和编辑器。 +// Core 按排序选择第一个启用渠道;每个渠道独立保存凭据,支持同一供应商的多个账号。 import { useCallback, useContext, useEffect, useId, useLayoutEffect, useRef, useState, type CSSProperties } from 'react'; import { createPortal } from 'react-dom'; @@ -330,9 +324,8 @@ export function ChannelList({ channelsRef.current = channels; }, [channels]); - // 2.0 UI 走查:添加/删除/拖序都会让行位置突变,此前没有任何过渡。 - // FLIP:每次 channels 变化后对比各行上边缘,位移的行从旧位置滑到新位置, - // 新出现的行淡入下沉进场;被拖的行保留抬升 scale,避免动画盖掉拖拽态。 + // FLIP 动画比较更新前后的布局位置,平滑呈现增删与排序变化。 + // 正在拖动的行保留自己的 transform,避免滑位动画覆盖拖动态。 const prevRowTops = useRef(new Map()); useLayoutEffect(() => { // 同样只量布局位置(offsetTop):rect.top 会被上一帧仍在飞行的 FLIP @@ -368,11 +361,8 @@ export function ChannelList({ const dragCleanupRef = useRef<(() => void) | null>(null); - /** 指针移到哪张卡片上,就把被拖的那张插到那个位置 —— 卡片实时跟手。 - * 命中测试必须用布局坐标(offsetTop)而不是 getBoundingClientRect:拖动时 - * FLIP 滑位动画正在飞行,rect 里掺着动画 transform,会把动画反馈进命中 - * 判定 —— 同一指针位置交替命中两张卡、顺序来回翻转,整栏疯狂抽搐 - * (2.0 UI 走查修复)。offsetTop 只反映布局位置,不受 transform/动画影响。 */ + /** 按指针命中的行实时排序。使用 offsetTop 读取布局坐标,避免 FLIP 的 + * transform 改变命中结果,导致同一指针位置反复触发互换。 */ const moveDragTo = (pointerY: number) => { const dragId = dragIdRef.current; if (!dragId) return; @@ -481,7 +471,7 @@ export function ChannelList({ channels.find(c => c.id === (draftId ?? editingId)) ?? null; const isDraft = draftId != null; - // 弹窗退场门控(2.0 UI 走查「从哪来回到哪去」):closing 动画期间保留最后一次 + // 弹窗退场门控:closing 动画期间保留最后一次 // 打开的 channel/isDraft,避免动画播一半内容先消失、标题从「添加」闪回「编辑」。 const dialogMount = useExitMount(editingChannel !== null); const lastDialogRef = useRef<{ channel: Channel; isDraft: boolean } | null>(null); @@ -531,7 +521,7 @@ export function ChannelList({
)} - {/* 2.0 UI 走查:生效渠道不再用蓝底 + 左侧竖条(「当前使用」徽章已经说明问题, + {/* 生效渠道不再用蓝底 + 左侧竖条(「当前使用」徽章已经说明问题, 整行染色太花哨);行改为圆角卡片,选中态只用中性灰底 + 细描边。 */}
{channels.map(channel => { @@ -551,7 +541,7 @@ export function ChannelList({ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: '14px 20px', padding: '14px 12px', borderRadius: 12, // 拖动态:轻微抬升(scale + 大阴影 + 强描边 + 提高层级), - // 让「哪张在被拖、拖到哪了」一目了然(2.0 UI 走查)。 + // 让「哪张在被拖、拖到哪了」一目了然。 border: '0.5px solid', borderColor: draggingId === channel.id ? 'var(--ol-line-strong)' : isActive ? 'var(--ol-line)' : 'transparent', background: isActive ? 'var(--ol-surface-2)' : 'transparent', @@ -661,10 +651,7 @@ function ChannelTestResult({ channel, testing, t }: { } /** - * 「服务 → AI 提供商」面板:LLM 与 ASR 两张渠道列表。 - * - * 保留 `ProvidersSection` 这个名字与 `kind` 签名,让设置页 tabs 与新手引导的调用点 - * 不用改。渠道化之后它只是两个 的容器。 + * 为设置与新手引导提供 LLM/ASR 渠道列表;多模态管线启用时展示 Omni 配置。 */ export function ProvidersSection({ kind = 'all', @@ -698,10 +685,8 @@ export function ProvidersSection({ } /** - * 添加与编辑共用的同一个弹窗 —— 供应商、名字、凭据、测试连通都在这一屏里。 - * - * 刻意不做「先创建、再填凭据」的两步:那只是实现上需要先有渠道 id 才能写凭据, - * 不该变成用户多点一次。 + * 添加与编辑复用同一表单。设置中挂载为右侧子页,新手引导中使用独立弹窗。 + * 新建渠道先取得凭据所属的 ID,关闭时仅回收从未发生用户操作的空白草稿。 */ function ChannelModal({ kind, diff --git a/openless-all/app/src/pages/settings/ClaudeConsoleSection.tsx b/openless-all/app/src/pages/settings/ClaudeConsoleSection.tsx index 5629bd21..b38c34ff 100644 --- a/openless-all/app/src/pages/settings/ClaudeConsoleSection.tsx +++ b/openless-all/app/src/pages/settings/ClaudeConsoleSection.tsx @@ -203,7 +203,7 @@ export function ClaudeConsoleSection() { - {/* 2.0 UI 走查:统一官方 SelectLite。 */} + setPermMode(v as CodingAgentPermissionMode)} diff --git a/openless-all/app/src/pages/settings/DataStorageSection.tsx b/openless-all/app/src/pages/settings/DataStorageSection.tsx index 0ab28bd0..e0b2b808 100644 --- a/openless-all/app/src/pages/settings/DataStorageSection.tsx +++ b/openless-all/app/src/pages/settings/DataStorageSection.tsx @@ -1,5 +1,4 @@ -// 隐私 → 数据存储:本地保留的历史会话与对话上下文窗口。 -// 自 Settings.tsx 的 RecordingSection「历史与上下文」折叠组拆出,逻辑零改动。 +// 历史与上下文数据的保留、清理和存储设置。 import { useTranslation } from 'react-i18next'; import { detectOS } from '../../components/WindowChrome'; diff --git a/openless-all/app/src/pages/settings/LocalModelSection.tsx b/openless-all/app/src/pages/settings/LocalModelSection.tsx index d922165b..ae4e0746 100644 --- a/openless-all/app/src/pages/settings/LocalModelSection.tsx +++ b/openless-all/app/src/pages/settings/LocalModelSection.tsx @@ -1,9 +1,5 @@ -// 服务 → 本地模型:本地 ASR 模型的管理入口。含 Qwen3(macOS)/ Foundry Local + -// sherpa-onnx(Windows)三条本地引擎。 -// -// 2026-08 重构:本地 ASR 不再有「启用开关」——本地 ASR 始终可用,是否激活由 -// 「服务 → AI 提供商」的 ASR 语音转写供应商决定:选到本地模型供应商即使用本地 -// 引擎(与 Apple 语音同理)。这里只保留模型下载 / 管理看板()。 +// 本地 ASR 模型管理入口。下载与运行状态由 LocalAsr 展示, +// 是否使用该引擎由 AI 服务与模型中的语音识别渠道决定。 import { useEffect, useState } from 'react'; import type { PlatformCapabilities } from '../../lib/types'; diff --git a/openless-all/app/src/pages/settings/ProvidersSection.tsx b/openless-all/app/src/pages/settings/ProvidersSection.tsx index 6af5f7d8..da558fd0 100644 --- a/openless-all/app/src/pages/settings/ProvidersSection.tsx +++ b/openless-all/app/src/pages/settings/ProvidersSection.tsx @@ -1,10 +1,9 @@ -// 服务 → AI 提供商:LLM 润色模型 + ASR 语音转写两张卡片。 -// 自 Settings.tsx 整体迁出,逻辑零改动;i18n key 全部保持 `settings.providers.*`。 +// 渠道凭据、模型列表和连接验证表单。供应商能力与默认值由 Core 提供, +// 界面负责本地化、字段保存反馈,以及用户主动触发的模型拉取和验证。 import { useEffect, useId, useMemo, useRef, useState, type CSSProperties, type ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { Icon } from '../../components/Icon'; -import { detectOS } from '../../components/WindowChrome'; import { listProviderModels, listProviderDescriptors, @@ -32,27 +31,6 @@ import { serializeAdvancedAsrConfig, type AdvancedAsrConfig, } from '../../lib/advancedAsrConfig'; -import { - getFoundryLocalAsrCatalog, - getSherpaOnnxAsrCatalog, - listLocalAsrModels, - setFoundryLocalAsrModel, - setLocalAsrActiveModel, - setSherpaOnnxAsrModel, -} from '../../lib/localAsr'; - -// 本地模型供应商:在主下拉里标注「本地」后缀,与云端供应商区分开。 -const LOCAL_ASR_PRESET_IDS: ReadonlySet = new Set([ - 'local-qwen3-mlx', - 'local-qwen3-c', - 'local-whisper', - 'foundry-local-whisper', - 'sherpa-onnx-local', -]); -function isLocalAsrPreset(id: string): boolean { - return LOCAL_ASR_PRESET_IDS.has(id); -} - /** Channel-local form layout; do not change the shared settings row contract. */ export function ChannelFormRow({ label, htmlFor, children }: { label: string; htmlFor?: string; children: ReactNode }) { const stack = useLayoutStack(); @@ -116,12 +94,7 @@ export const LLM_LABELS = [ ['custom', 'custom'], ].map(([id, nameKey]) => ({ id, nameKey })) as readonly { id: string; nameKey: string }[]; -// 多模态(Omni)模型预设(issue #902):一个模型同时接收「提示词 + 音频」一步输出 -// 最终文本。凭据走独立 `omni.*` 命名空间,与上方 LLM/ASR 两套配置完全隔离。 -// - openai : OpenAI 官方(gpt-4o-audio-preview 等,input_audio part) -// - gemini : Gemini 原生 generateContent(inlineData audio/wav) -// - dashscope-omni: 阿里云百炼 OpenAI 兼容通道(qwen3-omni-flash 等) -// - custom : 任意 OpenAI 兼容多模态网关 +// 火山语音转写在资源 ID 为空时显示的默认值。 const ASR_DEFAULT_RESOURCE_ID = 'volc.seedasr.sauc.duration'; /** 模型预设下拉里的「自定义模型…」哨兵值:选中即切回输入框手输。 */ @@ -153,9 +126,6 @@ export function ChannelCredentialFields({ }) { const { t } = useTranslation(); const { prefs, updatePrefs } = useHotkeySettings(); - const baseLayoutStack = useLayoutStack(); - const conservative = useConservativeLayout(); - const layoutStack = conservative || baseLayoutStack; const [llmModelRevision, setLlmModelRevision] = useState(0); const [asrModelRevision, setAsrModelRevision] = useState(0); const unifiedBailian = providerType === 'bailian'; @@ -601,9 +571,6 @@ type ProviderToolStatus = 'idle' | 'loading' | 'success' | 'empty' | 'error'; function ProviderTools({ kind, modelAccount, provider, onModelSelected, onTested, onUserMutation, showFetchModels = true }: { kind: 'llm' | 'asr' | 'omni'; modelAccount: string; provider?: string; onModelSelected: () => void; onTested?: () => void; onUserMutation?: () => void; showFetchModels?: boolean }) { const { t } = useTranslation(); - const baseLayoutStack = useLayoutStack(); - const conservative = useConservativeLayout(); - const layoutStack = conservative || baseLayoutStack; const [models, setModels] = useState([]); const [selectedModel, setSelectedModel] = useState(''); const [status, setStatus] = useState('idle'); @@ -786,9 +753,6 @@ interface CredentialFieldProps { function CredentialField({ label, account, provider, placeholder, mono, mask, defaultValue, trailing, onValueChange, onUserMutation, options }: CredentialFieldProps) { const fieldId = useId(); const { t } = useTranslation(); - const baseLayoutStack = useLayoutStack(); - const conservative = useConservativeLayout(); - const layoutStack = conservative || baseLayoutStack; const [value, setValue] = useState(''); const [revealed, setRevealed] = useState(false); const [loaded, setLoaded] = useState(false); @@ -1046,11 +1010,9 @@ const iconBtnStyle: CSSProperties = { }; /** - * 多模态(Omni)配置卡片:仅在「高级 → 实验性」里打开多模态管线后出现。 - * - * 它不参与渠道排序——Omni 是独立命名空间,渠道化范围只覆盖 ASR/LLM - * (见 docs/provider-channels-plan.md 的分期);管道模式切换沿用既有语义: - * 多模态模式下隐藏传统 llm/asr 渠道列表,凭据两套并存但停用,切回即恢复。 + * 在「实验与扩展」启用多模态管线后,展示独立的 Omni 配置。 + * Omni 使用自己的凭据命名空间,不参与 ASR/LLM 渠道排序;传统渠道配置仍保留, + * 切回传统管线后继续使用。 */ export function OmniChannelSection() { const { t } = useTranslation(); diff --git a/openless-all/app/src/pages/settings/RecordingInputSection.tsx b/openless-all/app/src/pages/settings/RecordingInputSection.tsx index 8fc63cc7..3cc55c6a 100644 --- a/openless-all/app/src/pages/settings/RecordingInputSection.tsx +++ b/openless-all/app/src/pages/settings/RecordingInputSection.tsx @@ -1,6 +1,4 @@ -// 通用 → 录音与输入:录音快捷键 / 方式 / 麦克风 / 胶囊 / 静音, -// 外加「插入与剪贴板」「启动」两个折叠组。流式输入也并到这里(属于插入行为)。 -// 自 Settings.tsx 的 RecordingSection 拆出,录音相关逻辑零改动。 +// 录音与输入设置:录音方式、提示音、输入行为及平台专属选项。 import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -344,7 +342,7 @@ export function RecordingInputSection() { label={t('settings.recording.silenceAutoStopLabel')} desc={t('settings.recording.silenceAutoStopDesc')} > - {/* 开关行只放 Toggle:秒数下拉移出本行,避免开关时行内内容变宽导致抽搐。 */} + {/* 开关行只放 Toggle:秒数下拉移出本行,避免开关时行内内容变宽导致抖动。 */} savePrefs({ ...prefs, silenceAutoStopEnabled: next })} diff --git a/openless-all/app/src/pages/settings/RemoteInputSection.tsx b/openless-all/app/src/pages/settings/RemoteInputSection.tsx index e4336915..350a3e6f 100644 --- a/openless-all/app/src/pages/settings/RemoteInputSection.tsx +++ b/openless-all/app/src/pages/settings/RemoteInputSection.tsx @@ -102,7 +102,7 @@ export function RemoteInputSection() { const mode = prefs.remoteInputDefaultMode ?? 'toggle'; const viewState = getRemoteInputViewState(enabled, status, startError); - // 提交端口草稿:非法(非有限数/越界离谱)则丢弃还原显示,合法则取整并 clamp 到 [1024, 65535]。 + // 无效或非正数草稿还原显示;有效数值取整并限制在 [1024, 65535]。 const commitPort = () => { if (portDraft == null) return; const n = Math.round(Number(portDraft)); diff --git a/openless-all/app/src/pages/settings/shared.tsx b/openless-all/app/src/pages/settings/shared.tsx index b3dd6821..1a034ed7 100644 --- a/openless-all/app/src/pages/settings/shared.tsx +++ b/openless-all/app/src/pages/settings/shared.tsx @@ -88,7 +88,7 @@ export function SettingRow({ }} >
- {/* 2.0 UI 走查:行内长说明不再常驻占位,统一收进标题旁的「?」, + {/* 行内长说明不再常驻占位,统一收进标题旁的「?」, 悬停 / 点击弹出(与「录音与输入」标题的提示同一交互语言)。 */}
{label} @@ -146,7 +146,7 @@ export function Toggle({ onClick={() => onToggle?.(!on)} style={{ position: "relative", - // 2.0 UI 走查:此前写死 flex: 0 0 36px,在列方向的设置行包装里 + // 此前写死 flex: 0 0 36px,在列方向的设置行包装里 // flex-basis 作用在高度上,开关被拉成 36×36 的圆球(开机自启行)。 // 宽高由 width/height 显式锁定,flex 只负责不伸不缩。 flex: "0 0 auto", diff --git a/openless-all/app/src/pages/settings/tabs.tsx b/openless-all/app/src/pages/settings/tabs.tsx index cbcec625..4d07fa8b 100644 --- a/openless-all/app/src/pages/settings/tabs.tsx +++ b/openless-all/app/src/pages/settings/tabs.tsx @@ -64,9 +64,7 @@ export function AppearanceTab() { return <>; } -// 服务:AI 提供商 · 本地模型 · 扩展市场。 -// 本地模型是「语音识别由谁提供」的一种答案,和云端提供商属同一决策, -// 不再藏进「高级」。 +// AI 服务与模型:按当前管线和平台能力组织渠道、本地模型及网络设置。 export function ServicesTab() { const { t } = useTranslation(); const { prefs } = useHotkeySettings(); @@ -79,7 +77,7 @@ export function ServicesTab() { const contentRef = useRef(null); // 语言模型 / 语音识别是必配项:tab 上挂状态点 —— 未配置红、已配置黄 - // (2.0 UI 走查)。任何渠道增删改/启停后 ChannelList 会广播 + // 。任何渠道增删改/启停后 ChannelList 会广播 // ol-channels-changed,这里即时重算。 const [requiredConfigured, setRequiredConfigured] = useState<{ llm: boolean; asr: boolean }>({ llm: false, asr: false }); useEffect(() => { diff --git a/openless-all/app/src/styles/global.css b/openless-all/app/src/styles/global.css index 40b7f65f..6db5229b 100644 --- a/openless-all/app/src/styles/global.css +++ b/openless-all/app/src/styles/global.css @@ -27,7 +27,7 @@ body { background: transparent; user-select: none; -webkit-user-select: none; - /* 2.0 UI 走查:全局基准字号 14px(此前未设 → 浏览器默认 16 但组件几乎全部 + /* 全局基准字号 14px(此前未设 → 浏览器默认 16 但组件几乎全部 显式写 11–13px 小字,视觉偏小)。控件/正文的显式字号已同步上调到 13.5–14。 */ font-size: 14px; } @@ -350,7 +350,7 @@ html[data-ol-conservative-layout="true"] .ol-conservative-scope .ol-select-lite- .ol-advanced-settings-entry { transition: none; } } -/* 桌面端设置侧栏导航(2.0 UI 走查):选中底色由滑动 thumb 提供,按钮自身透明; +/* 桌面端设置侧栏导航:选中底色由滑动 thumb 提供,按钮自身透明; hover 给弱底色反馈;选中项文字蓝色加粗走 [aria-current]。 */ .ol-settings-rail-btn { color: var(--ol-ink-2); @@ -367,7 +367,7 @@ html[data-ol-conservative-layout="true"] .ol-conservative-scope .ol-select-lite- font-weight: 600; } -/* 翻译页顶部两栏(2.0 UI 走查):左栏略宽让语言 chips 每行 4 个, +/* 翻译页顶部两栏:左栏略宽让语言 chips 每行 4 个, 与右栏卡片高度更接近;窄屏叠成单栏。 */ .ol-translation-grid { display: grid; @@ -381,7 +381,7 @@ html[data-ol-conservative-layout="true"] .ol-conservative-scope .ol-select-lite- } } -/* 语言/标签选择 chip(2.0 UI 走查 · 翻译页):底色走 token 以兼容深色模式, +/* 语言/标签选择 chip:底色走 token 以兼容深色模式, 选中态走 [aria-pressed],悬浮给描边+加深反馈。 */ .ol-chip { min-width: 0; @@ -416,7 +416,7 @@ html[data-ol-conservative-layout="true"] .ol-conservative-scope .ol-select-lite- border-color: var(--ol-blue); } -/* 列表项通用入场(2.0 UI 走查「从哪来回到哪去」):新条目淡入轻降。 +/* 列表项通用入场:新条目淡入轻降。 key 稳定时只有真正新增/过滤变化的项会重播,存量项不动。 */ @keyframes ol-item-in { from { opacity: 0; transform: translateY(-4px); } @@ -451,7 +451,7 @@ html[data-ol-conservative-layout="true"] .ol-conservative-scope .ol-select-lite- to { opacity: 0; } } -/* ── 词典页(2.0 UI 走查)────────────────────────────────────────────── */ +/* ── 词典页────────────────────────────────────────────── */ /* 来源分段筛选:灰底胶囊容器 + 白色浮起选中片(参考 Typeless 词典稿)。 */ .ol-seg { @@ -489,7 +489,7 @@ html[data-ol-conservative-layout="true"] .ol-conservative-scope .ol-select-lite- box-shadow: var(--ol-shadow-sm); } -/* 圆形搜索(2.0 UI 走查重做):同一个胶囊容器原地变形 —— 收起 36px 圆钮, +/* 圆形搜索:同一个胶囊容器原地变形 —— 收起 36px 圆钮, 展开 220px 搜索框;放大镜图标始终贴右缘不动,输入文字在左侧淡入。 展开/收起共用同一条 width 过渡,两个方向都有动画(从哪来回到哪去)。 */ .ol-search { @@ -524,7 +524,7 @@ html[data-ol-conservative-layout="true"] .ol-conservative-scope .ol-select-lite- font-size: 13px; color: var(--ol-ink); /* 收起态 padding 必须为 0:否则输入框虽然宽度塌成 0,左右 padding 仍占位, - 会把 36px 的放大镜按钮顶出圆形容器、被 overflow 裁掉(2.0 UI 走查修复)。 */ + 会把 36px 的放大镜按钮顶出圆形容器、被 overflow 裁掉。 */ padding: 0; opacity: 0; transition: opacity 0.16s var(--ol-motion-quick), padding 0.28s var(--ol-motion-spring); @@ -670,7 +670,7 @@ html[data-ol-conservative-layout="true"] .ol-conservative-scope .ol-select-lite- cursor: pointer; } -/* 必配服务(语言模型/语音识别)tab 上的状态点:未配置红、已配置黄(2.0 UI 走查)。 */ +/* 必配服务(语言模型/语音识别)tab 上的状态点:未配置红、已配置黄。 */ .ol-service-status-dot { width: 7px; height: 7px; diff --git a/openless-all/app/src/styles/tokens.css b/openless-all/app/src/styles/tokens.css index 54a032ad..ca330a76 100755 --- a/openless-all/app/src/styles/tokens.css +++ b/openless-all/app/src/styles/tokens.css @@ -111,7 +111,7 @@ light/dark 取值不同需保持主题感知。 */ --ol-panel-glass-bg: rgba(255, 255, 255, 0.94); - /* 2.0 UI 走查(参考 Codex 侧栏):侧栏底色与右侧纯白内容区拉开明确层次, + /* (参考 Codex 侧栏):侧栏底色与右侧纯白内容区拉开明确层次, 此前 #fafafa 对白底几乎无区分。 */ --ol-sidebar-bg: #f0f0f1; --ol-sidebar-border: #e4e4e7; @@ -165,34 +165,16 @@ --ol-toggle-knob: #ffffff; --ol-overlay-bg: rgba(15,17,22,0.32); - --ol-style-shell-bg: linear-gradient(180deg, rgba(255,255,255,0.84), rgba(249,250,252,0.72)); - --ol-style-shell-border: rgba(255,255,255,0.72); - --ol-style-shell-shadow: 0 32px 80px -40px rgba(15, 23, 42, 0.26); --ol-style-card-bg: #fafafa; --ol-style-card-bg-active: #e4e4e7; --ol-style-card-border: #e4e4e7; --ol-style-card-border-active: #bfc0c5; - --ol-style-card-shadow: 0 24px 48px -34px rgba(15, 23, 42, 0.18), inset 0 1px 0 0 rgba(255,255,255,0.65); - --ol-style-card-shadow-active: none; --ol-style-card-ink: #09090b; - --ol-style-card-ink-2: rgba(9, 9, 11, 0.82); - --ol-style-card-ink-3: rgba(9, 9, 11, 0.56); --ol-style-card-icon-bg: rgba(228,228,231,0.62); --ol-style-card-icon-bg-active: #d4d4d8; - --ol-style-delete-bg: rgba(255,255,255,0.56); - --ol-style-delete-border: rgba(248,113,113,0.36); - --ol-style-add-tile-bg: rgba(255,255,255,0.12); - --ol-style-add-tile-icon-bg: rgba(255,255,255,0.48); - --ol-style-add-tile-border: rgba(255,255,255,0.32); --ol-style-input-bg: rgba(255,255,255,0.86); - --ol-style-input-border: rgba(255,255,255,0.74); - --ol-style-input-shadow: inset 0 1px 0 0 rgba(255,255,255,0.84), 0 10px 24px -22px rgba(15,23,42,0.16); --ol-style-subtle-bg: rgba(255,255,255,0.74); - --ol-style-subtle-border: rgba(255,255,255,0.62); - --ol-style-subtle-shadow: inset 0 1px 0 0 rgba(255,255,255,0.74); --ol-style-editor-bg: linear-gradient(180deg, rgba(255,255,255,0.92), rgba(247,249,252,0.82)); - --ol-style-editor-border: rgba(255,255,255,0.74); - --ol-style-editor-shadow: 0 32px 96px -32px rgba(15,23,42,0.34); /* Desktop capsule: follows app theme (light frosted pill / dark solid pill). */ --ol-capsule-pill-bg: rgba(255, 255, 255, 0.90); @@ -332,34 +314,16 @@ --ol-toggle-knob: #fafafa; --ol-overlay-bg: rgba(5, 8, 13, 0.64); - --ol-style-shell-bg: linear-gradient(180deg, rgba(24, 24, 27, 0.96), rgba(20, 20, 23, 0.94)); - --ol-style-shell-border: rgba(226,232,240,0.09); - --ol-style-shell-shadow: 0 26px 70px -42px rgba(0,0,0,0.66); --ol-style-card-bg: #252528; --ol-style-card-bg-active: #37373c; --ol-style-card-border: rgba(226,232,240,0.09); --ol-style-card-border-active: #62626a; - --ol-style-card-shadow: inset 0 1px 0 rgba(255,255,255,0.04); - --ol-style-card-shadow-active: none; --ol-style-card-ink: #fafafa; - --ol-style-card-ink-2: rgba(250,250,250,0.86); - --ol-style-card-ink-3: rgba(250,250,250,0.64); --ol-style-card-icon-bg: rgba(226,232,240,0.07); --ol-style-card-icon-bg-active: #45454d; - --ol-style-delete-bg: rgba(226,232,240,0.06); - --ol-style-delete-border: rgba(248,113,113,0.34); - --ol-style-add-tile-bg: rgba(226,232,240,0.035); - --ol-style-add-tile-icon-bg: rgba(226,232,240,0.10); - --ol-style-add-tile-border: rgba(226,232,240,0.12); --ol-style-input-bg: rgba(9, 9, 11, 0.38); - --ol-style-input-border: rgba(226,232,240,0.10); - --ol-style-input-shadow: inset 0 1px 0 rgba(255,255,255,0.03); --ol-style-subtle-bg: rgba(226,232,240,0.05); - --ol-style-subtle-border: rgba(226,232,240,0.08); - --ol-style-subtle-shadow: inset 0 1px 0 rgba(255,255,255,0.03); --ol-style-editor-bg: linear-gradient(180deg, rgba(24, 24, 27, 0.98), rgba(20, 20, 23, 0.96)); - --ol-style-editor-border: rgba(226,232,240,0.10); - --ol-style-editor-shadow: 0 30px 90px -42px rgba(0,0,0,0.70); --ol-capsule-pill-bg: #1b222c; --ol-capsule-pill-border: rgba(226, 232, 240, 0.12); @@ -392,7 +356,7 @@ body { .ol-noscrollbar { scrollbar-width: none; } /* Thin scrollbar */ -/* 2.0 UI 走查:内容变长出现 6px 细滚动条的瞬间会把内容宽度挤窄一格,整页「抽搐」。 +/* 内容变长出现 6px 细滚动条的瞬间会把内容宽度挤窄一格,整页「抖动」。 scrollbar-gutter: stable 让滚动条槽位常驻预留,滚动条出现/消失不再触发布局回流。 */ .ol-thinscroll { scrollbar-gutter: stable; } /* 概览是 overflow:hidden 的单屏固定页,永远不会有滚动条 —— 不占槽位。 */ diff --git a/openless-all/app/tsconfig.json b/openless-all/app/tsconfig.json index 8437b088..bca4a1e9 100644 --- a/openless-all/app/tsconfig.json +++ b/openless-all/app/tsconfig.json @@ -2,7 +2,11 @@ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", @@ -12,14 +16,23 @@ "noEmit": true, "jsx": "react-jsx", "strict": true, - "noUnusedLocals": false, - "noUnusedParameters": false, + "noUnusedLocals": true, + "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "baseUrl": ".", "paths": { - "@android/*": ["android/frontend/*"] + "@android/*": [ + "android/frontend/*" + ] } }, - "include": ["src", "android/frontend"], - "references": [{ "path": "./tsconfig.node.json" }] + "include": [ + "src", + "android/frontend" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] }