Merge pull request #423 from Open-Less/fix/v1.3.1-5
fix(v1.3.1-5): 胶囊动画 + Qwen3 灰显 + 5 SVG 重画 + 6 项 Windows 视觉修复 + polish 反降级
This commit is contained in:
12 files changed
+170
-44
No files matched your search
@@ -627,14 +627,16 @@ fn apply_windows_caption_color<R: Runtime>(window: &tauri::WebviewWindow<R>) {
|
||||
};
|
||||
let hwnd = HWND(handle.hwnd.get() as *mut core::ffi::c_void);
|
||||
|
||||
// COLORREF 0x00BBGGRR 编码——白色就是 0x00FFFFFF。
|
||||
let white: u32 = 0x00FFFFFF;
|
||||
// COLORREF 0x00BBGGRR 编码——选用 rgb(245,245,247) 跟 WindowChrome 的 glass linear-gradient
|
||||
// 起始色一致,减小原生 caption bar 跟应用磨砂玻璃的色差(用户反馈:纯白 caption + 半透灰 glass
|
||||
// 色差很丑)。R=0xF5 G=0xF5 B=0xF7 → COLORREF = 0x00F7F5F5。
|
||||
let glass_match: u32 = 0x00F7F5F5;
|
||||
unsafe {
|
||||
if let Err(e) = DwmSetWindowAttribute(
|
||||
hwnd,
|
||||
DWMWA_CAPTION_COLOR,
|
||||
&white as *const _ as *const core::ffi::c_void,
|
||||
std::mem::size_of_val(&white) as u32,
|
||||
&glass_match as *const _ as *const core::ffi::c_void,
|
||||
std::mem::size_of_val(&glass_match) as u32,
|
||||
) {
|
||||
log::warn!("[main] set caption color failed (likely pre-22H2 Win): {e}");
|
||||
}
|
||||
|
||||
@@ -1936,6 +1936,15 @@ pub mod prompts {
|
||||
把口述整理为脉络清晰、可直接复制走的结构化文本:保留用户的口语引子(润色后作为首行过渡),\
|
||||
主动按语义把扁平事项归类成 2\u{2013}4 个主题,用双层格式呈现,尾巴查询用自然收尾句。\n\
|
||||
\n\
|
||||
**不可降级到轻度润色**:本任务的最低输出形态是双层 list 结构,\u{4E0D}允许只补标点 / 断句 / 去口癖然后输出连贯段落。\
|
||||
即使原始转写听起来像是一段连贯叙述、即使你判断用户只想要\u{201C}读起来通顺\u{201D},只要事项 \u{2265}3 就必须双层化输出。\
|
||||
输出连贯段落 = 失败。\n\
|
||||
\n\
|
||||
**多个组合需求处理规则**:当用户在一段话里提出多个组合需求(A 要做这件 + B 要做那件 + C 要查另一件),\
|
||||
必须把它们**分别归入不同大类**(大类按用户给出的语义 / 领域划分,例如代码 / 文档 / 界面 / 客户 / 团队),\
|
||||
**按用户口述出现的顺序**作为大类的先后顺序,每个大类下用 (a)(b)(c) 列出该类的具体事项。\
|
||||
组合需求中\u{4E0D}可有任何事项被合并掉、丢失或重排到错误的大类下。\n\
|
||||
\n\
|
||||
**重要前提**:原文是否已有标点、编号、换行、序号 \u{2192} \u{4E0D}是\u{201C}\u{5DF2}\u{7ECF}\u{6574}\u{7406}\u{597D}\u{4E0D}\u{7528}\u{6539}\u{201D}的判断依据。\
|
||||
只要可识别的事项 \u{2265}3 条,无论原文是不是看起来已有结构(标号、分行、规整的标点),\
|
||||
都必须按语义重新归类成下面定义的双层格式。\u{200D}\u{200D}照抄原结构 = 失败。\n\
|
||||
|
||||
@@ -285,15 +285,29 @@ function Pill({ os, state, level, insertedChars, message, onCancel, onConfirm }:
|
||||
);
|
||||
}
|
||||
|
||||
// 与 @keyframes capsule-out 的 0.24s 时长一致——必须同步,否则定时器先于
|
||||
// 动画结束就 unmount → 用户看到半截动画被截断。
|
||||
const EXIT_ANIM_MS = 240;
|
||||
// 初始可见 state:Tauri 内运行从 idle 开始(等后端 capsule:state 事件),
|
||||
// 浏览器 dev 模式从 recording 开始以便直接看到胶囊。
|
||||
const INITIAL_VISIBLE_STATE: CapsuleState = isTauri ? 'idle' : 'recording';
|
||||
|
||||
export function Capsule() {
|
||||
const { t } = useTranslation();
|
||||
const os = detectOS();
|
||||
const metrics = getCapsulePillMetrics(os);
|
||||
const [state, setState] = useState<CapsuleState>(isTauri ? 'idle' : 'recording');
|
||||
const [state, setState] = useState<CapsuleState>(INITIAL_VISIBLE_STATE);
|
||||
const [level, setLevel] = useState<number>(isTauri ? 0 : 0.6);
|
||||
const [insertedChars, setInsertedChars] = useState<number>(0);
|
||||
const [message, setMessage] = useState<string | undefined>();
|
||||
const [translation, setTranslation] = useState<boolean>(false);
|
||||
// `leaving` 与 `lastVisibleState` 协同实现「退出动画」:
|
||||
// - 当 state 从非 idle 变成 idle 时,不立即卸载,而是把 leaving 置为 true 并保留
|
||||
// 最后一帧的可见 state(lastVisibleState),让胶囊用 capsule-out 动画收缩淡出。
|
||||
// - 动画结束(EXIT_ANIM_MS)后再把 leaving 置回 false,组件回到「真正未挂载」分支。
|
||||
// - 若期间 state 又切回非 idle(例如用户连按热键),立刻中止 leaving 并恢复显示。
|
||||
const [leaving, setLeaving] = useState<boolean>(false);
|
||||
const [lastVisibleState, setLastVisibleState] = useState<CapsuleState>(INITIAL_VISIBLE_STATE);
|
||||
// Windows 端 host 在翻译模式从 84 长到 118;macOS / Linux 上 capsuleLayout 已固定 42 忽略此参数。
|
||||
const hostMetrics = getCapsuleHostMetrics(os, translation);
|
||||
|
||||
@@ -320,6 +334,31 @@ export function Capsule() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 退出动画调度:在 state 真正进入 idle 时,先用 capsule-out 播放 EXIT_ANIM_MS,再卸载。
|
||||
// 设计要点:
|
||||
// 1. 进入非 idle:清掉 leaving,记录最新可见 state;
|
||||
// 2. 进入 idle 且之前可见:开启 leaving 并启动定时器;
|
||||
// 3. 期间又被打回非 idle:cleanup 直接 clearTimeout,定时器不会触发,
|
||||
// 新一轮 effect 会立即恢复可见态,避免错误地把可见状态切到 idle。
|
||||
useEffect(() => {
|
||||
if (state !== 'idle') {
|
||||
// 立即恢复可见,并取消上一轮可能挂着的离场。
|
||||
if (leaving) setLeaving(false);
|
||||
setLastVisibleState(state);
|
||||
return undefined;
|
||||
}
|
||||
// state === 'idle':判断是不是从可见态过渡过来。
|
||||
if (lastVisibleState === 'idle') return undefined;
|
||||
setLeaving(true);
|
||||
const timer = setTimeout(() => {
|
||||
setLeaving(false);
|
||||
setLastVisibleState('idle');
|
||||
}, EXIT_ANIM_MS);
|
||||
return () => clearTimeout(timer);
|
||||
// 故意只依赖 state —— lastVisibleState / leaving 是内部派生量,
|
||||
// 把它们加进依赖会让定时器被反复重建。
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [state]);
|
||||
|
||||
const onCancel = () => {
|
||||
void invokeOrMock<void>('cancel_dictation', undefined, () => undefined);
|
||||
@@ -329,10 +368,14 @@ export function Capsule() {
|
||||
void invokeOrMock<void>('stop_dictation', undefined, () => undefined);
|
||||
};
|
||||
|
||||
if (state === 'idle') {
|
||||
// 真正卸载:state 已是 idle,且不在离场动画中。
|
||||
if (state === 'idle' && !leaving) {
|
||||
return <div style={{ width: 0, height: 0 }} />;
|
||||
}
|
||||
|
||||
// 离场时用 lastVisibleState 渲染最后一帧内容,避免把 idle 当作 fallback 走到 AudioBars(0)。
|
||||
const renderedState: CapsuleState = state === 'idle' ? lastVisibleState : state;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -350,7 +393,15 @@ export function Capsule() {
|
||||
: 0,
|
||||
paddingBottom: os === 'win' ? hostMetrics.bottomInset : 0,
|
||||
background: 'transparent',
|
||||
animation: os === 'win' ? 'none' : 'capsule-in .22s cubic-bezier(.2,.9,.3,1.1)',
|
||||
// 入场:中央 scaleX 由 0.18 长到 1(视觉上像从中心向两端展开)+ 淡入。
|
||||
// 离场:scaleX 由 1 收缩回 0.18 + 向下偏移 8px + 淡出。
|
||||
// 三平台一致 —— 旧版 Windows 走 animation:'none' 的分支已删除。
|
||||
// transformOrigin 默认就是 50% 50%,所以 scaleX 天然以中央为锚点。
|
||||
animation: leaving
|
||||
? 'capsule-out .24s cubic-bezier(.4,0,.7,.2) forwards'
|
||||
: 'capsule-in .26s cubic-bezier(.2,.9,.3,1.1) both',
|
||||
transformOrigin: 'center',
|
||||
willChange: 'transform, opacity',
|
||||
}}
|
||||
>
|
||||
{/* "正在翻译" 徽章 — 嵌套两层:
|
||||
@@ -401,17 +452,26 @@ export function Capsule() {
|
||||
</div>
|
||||
<Pill
|
||||
os={os}
|
||||
state={state}
|
||||
level={level}
|
||||
state={renderedState}
|
||||
level={leaving ? 0 : level}
|
||||
insertedChars={insertedChars}
|
||||
message={message}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirm}
|
||||
/>
|
||||
<style>{`
|
||||
/* 入场:从中央很窄的一小条(scaleX 0.18)+ 略压扁(scaleY 0.95)+ 透明,
|
||||
长出到 scaleX 1 / scaleY 1 / 不透明。配合 wrapper 的 transformOrigin:center,
|
||||
视觉上是「从中心向左右展开」。 */
|
||||
@keyframes capsule-in {
|
||||
from { opacity: 0; transform: translateY(6px) scale(.96); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
from { opacity: 0; transform: scaleX(.18) scaleY(.95); }
|
||||
to { opacity: 1; transform: scaleX(1) scaleY(1); }
|
||||
}
|
||||
/* 离场:scaleX 由 1 收回 0.18 + 整体向下偏移 8px + 淡出。
|
||||
forwards 让最终帧(opacity:0、scaleX:.18)保持到组件被卸载。 */
|
||||
@keyframes capsule-out {
|
||||
from { opacity: 1; transform: scaleX(1) translateY(0); }
|
||||
to { opacity: 0; transform: scaleX(.18) translateY(8px); }
|
||||
}
|
||||
@keyframes cap-shine {
|
||||
0% { background-position: 200% center; }
|
||||
|
||||
@@ -4,12 +4,17 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
export const ICONS: Record<string, string> = {
|
||||
overview: 'M3 13l4-4 3 3 7-7M14 5h4v4',
|
||||
history: 'M3 12a9 9 0 1 0 3-6.7M3 4v4h4',
|
||||
vocab: 'M5 4h11a2 2 0 0 1 2 2v13l-3-2-3 2-3-2-3 2V6a2 2 0 0 1 2-2zM8 9h7M8 13h5',
|
||||
// overview — 仪表盘三卡 + 顶卡内 sparkline 数据线,区分纯三块拼图(layout)
|
||||
overview: 'M4 5h16v5H4zM4 13h7v6H4zM13 13h7v6h-7zM6 7.8l2 1 2-1.5 2 1',
|
||||
// history — 时钟表盘 + 左上角逆时针回拨箭头,强调"过去/回看"
|
||||
history: 'M12 8v4l3 2M3.5 12a8.5 8.5 0 1 0 2.8-6.3L3 8M3 4v4h4',
|
||||
// vocab — Feather 风格 open-book(书脊居中 + 左右两页),相比旧的"带书签的合上书"在 14px 下更易辨识
|
||||
vocab: 'M12 7v14M12 7a3 3 0 0 0-3-3H4v14h5a3 3 0 0 1 3 3M12 7a3 3 0 0 1 3-3h5v14h-5a3 3 0 0 0-3 3',
|
||||
style: 'M12 3a9 9 0 1 0 0 18 3 3 0 0 0 3-3v-1a2 2 0 0 1 2-2h1a3 3 0 0 0 3-3 9 9 0 0 0-9-9z',
|
||||
translate:'M3 5h7M6 4v2M5 8c0 4 3 6 5 6M9 8c0 4-3 6-5 6M13 20l4-10 4 10M14.5 17h5',
|
||||
selectionAsk:'M4 6h11M4 10h11M4 14h7M17 14a3 3 0 1 1 3 3v1M20 22h.01',
|
||||
// translate — 地球仪(圆 + 赤道 + 经线椭圆),通用的"语言/国际化"符号,比旧版"A+文+三角"在 14px 下更清晰
|
||||
translate:'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM3 12h18M12 3c2.5 2.5 4 5.5 4 9s-1.5 6.5-4 9c-2.5-2.5-4-5.5-4-9s1.5-6.5 4-9',
|
||||
// selectionAsk — 三行文本 + 右下角对话气泡(尾巴拉到 y≈23 防 viewBox 24 底边 stroke-cap 裁切)
|
||||
selectionAsk:'M3 5h12M3 9h12M3 13h7M14 14h6a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3.5l-2.5 2v-2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2z',
|
||||
settings:'M12 9.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1A2 2 0 1 1 7 4.9l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z',
|
||||
help: 'M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3M12 17h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z',
|
||||
mic: 'M12 2a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3zM19 11a7 7 0 0 1-14 0M12 18v3M8 21h8',
|
||||
|
||||
@@ -39,11 +39,12 @@ export function WindowChrome({
|
||||
// 两个平台用同一份半透明玻璃 background + backdropFilter,让 sidebar 透明地坐在
|
||||
// 磨砂底板上时有可见的玻璃感。
|
||||
// Windows: Tauri transparent:true + lib.rs apply_mica 提供 Win11 Mica 透出来;
|
||||
// macOS: NSVisualEffectView 提供材质。alpha 0.78 比之前的 0.92 更明显但不过透。
|
||||
// macOS: NSVisualEffectView 提供材质。
|
||||
// alpha 0.92:和原生 Win11 caption(lib.rs 设为 rgb(245,245,247))色差最小,玻璃感仍可见。
|
||||
const background = `
|
||||
radial-gradient(120% 80% at 0% 0%, rgba(255,255,255,0.55) 0%, rgba(255,255,255,0) 60%),
|
||||
radial-gradient(100% 70% at 100% 100%, rgba(37,99,235,0.07) 0%, rgba(37,99,235,0) 55%),
|
||||
linear-gradient(180deg, rgba(245,245,247,0.78) 0%, rgba(232,232,236,0.78) 100%)
|
||||
linear-gradient(180deg, rgba(245,245,247,0.92) 0%, rgba(232,232,236,0.92) 100%)
|
||||
`;
|
||||
|
||||
return (
|
||||
|
||||
@@ -668,6 +668,7 @@ export const en: typeof zhCN = {
|
||||
desc: 'Manage on-device ASR models. Windows can use Microsoft Foundry Local Whisper; Qwen3-ASR model management stays separate.',
|
||||
qwenTitle: 'Qwen3-ASR model manager',
|
||||
engineUnavailable: 'The Qwen3-ASR inference engine is not bundled on this platform. You can still download models, but Qwen3-ASR cannot be activated here yet.',
|
||||
qwenUnavailableOnWindows: 'Qwen3-ASR is not supported on Windows yet. Please use Foundry Local Whisper above instead.',
|
||||
foundryTitle: 'Windows Foundry Local Whisper',
|
||||
foundryDesc: 'Windows uses Microsoft Foundry Local Whisper to recognize speech on this device with no ASR API key. First prepare downloads local runtime components and a model, then loads it. LLM polishing still uses your configured LLM provider; if none is configured, the existing raw transcript fallback still applies.',
|
||||
foundryAvailable: 'Available on Windows',
|
||||
|
||||
@@ -670,6 +670,7 @@ export const ja: typeof zhCN = {
|
||||
desc: '本機の ASR モデルを管理します。Windows では Microsoft Foundry Local Whisper を使用でき、Qwen3-ASR のモデル管理は独立しています。',
|
||||
qwenTitle: 'Qwen3-ASR モデル管理',
|
||||
engineUnavailable: '現在のプラットフォームには Qwen3-ASR 推論エンジンが同梱されていません。モデルのダウンロードは可能ですが、ここではまだ Qwen3-ASR を有効化できません。',
|
||||
qwenUnavailableOnWindows: 'Windows では Qwen3-ASR にまだ対応していません。上記の Foundry Local Whisper をご利用ください。',
|
||||
foundryTitle: 'Windows Foundry Local Whisper',
|
||||
foundryDesc: 'Windows では Microsoft Foundry Local Whisper が本機上で音声を認識し、ASR API Key は不要です。初回準備ではローカル実行コンポーネントとモデルをダウンロードして読み込みます。LLM 整文は設定済みの LLM プロバイダーを引き続き使用し、未設定の場合は従来どおり生の転写結果にフォールバックします。',
|
||||
foundryAvailable: 'Windows で利用可能',
|
||||
|
||||
@@ -670,6 +670,7 @@ export const ko: typeof zhCN = {
|
||||
desc: '기기 내 ASR 모델을 관리합니다. Windows 에서는 Microsoft Foundry Local Whisper 를 사용할 수 있으며, Qwen3-ASR 모델 관리는 별도로 유지됩니다.',
|
||||
qwenTitle: 'Qwen3-ASR 모델 관리',
|
||||
engineUnavailable: '현재 플랫폼에는 Qwen3-ASR 추론 엔진이 포함되어 있지 않습니다. 모델은 다운로드할 수 있지만 여기서는 아직 Qwen3-ASR 을 활성화할 수 없습니다.',
|
||||
qwenUnavailableOnWindows: 'Windows 에서는 아직 Qwen3-ASR 을 지원하지 않습니다. 위의 Foundry Local Whisper 를 사용해 주세요.',
|
||||
foundryTitle: 'Windows Foundry Local Whisper',
|
||||
foundryDesc: 'Windows 는 Microsoft Foundry Local Whisper 로 이 기기에서 음성을 인식하며 ASR API Key 가 필요 없습니다. 첫 준비 시 로컬 런타임 구성 요소와 모델을 다운로드한 뒤 로드합니다. LLM 정리는 계속 설정된 LLM 공급자를 사용하며, 설정되지 않은 경우 기존 원문 전사 폴백을 그대로 사용합니다.',
|
||||
foundryAvailable: 'Windows 에서 사용 가능',
|
||||
|
||||
@@ -666,6 +666,7 @@ export const zhCN = {
|
||||
desc: '管理本机 ASR 模型。Windows 可使用 Microsoft Foundry Local Whisper;Qwen3-ASR 模型管理保持独立。',
|
||||
qwenTitle: 'Qwen3-ASR 模型管理',
|
||||
engineUnavailable: '当前平台暂未集成 Qwen3-ASR 推理引擎。可下载模型,但暂时无法启用 Qwen3-ASR。',
|
||||
qwenUnavailableOnWindows: 'Windows 暂不支持 Qwen3-ASR,请使用上方 Foundry Local Whisper。',
|
||||
foundryTitle: 'Windows Foundry Local Whisper',
|
||||
foundryDesc: 'Windows 使用 Microsoft Foundry Local Whisper 在本机识别语音,无需 ASR API Key。首次准备会在本机下载运行组件和模型并加载;LLM 润色仍使用你已配置的 LLM 提供商,未配置时沿用原始转写回退。',
|
||||
foundryAvailable: 'Windows 可用',
|
||||
|
||||
@@ -668,6 +668,7 @@ export const zhTW: typeof zhCN = {
|
||||
desc: '管理本機 ASR 模型。Windows 可使用 Microsoft Foundry Local Whisper;Qwen3-ASR 模型管理保持獨立。',
|
||||
qwenTitle: 'Qwen3-ASR 模型管理',
|
||||
engineUnavailable: '當前平臺暫未集成 Qwen3-ASR 推理引擎。可下載模型,但暫時無法啟用 Qwen3-ASR。',
|
||||
qwenUnavailableOnWindows: 'Windows 暫不支援 Qwen3-ASR,請使用上方的 Foundry Local Whisper。',
|
||||
foundryTitle: 'Windows Foundry Local Whisper',
|
||||
foundryDesc: 'Windows 使用 Microsoft Foundry Local Whisper 在本機識別語音,無需 ASR API Key。首次準備會在本機下載運行組件和模型並加載;LLM 潤色仍使用你已配置的 LLM 提供商,未配置時沿用原始轉寫回退。',
|
||||
foundryAvailable: 'Windows 可用',
|
||||
|
||||
@@ -758,12 +758,30 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Qwen3 模型管理区——只在 macOS 渲染(后端 #[cfg(target_os = "macos")] 独占)。
|
||||
Windows / Linux 看见镜像源 / 下载 / 模型列表都是 dead UI。Foundry 块自身已经
|
||||
被上方 IS_WINDOWS 守卫,错误 Card(共享 setError,被 Foundry handler 也写)
|
||||
保持无条件露出。 */}
|
||||
{IS_MAC && (<>
|
||||
{!engineAvailable && (
|
||||
{/* Qwen3 模型管理区——macOS 是实体后端(#[cfg(target_os = "macos")]),Windows
|
||||
上 backend 永远 unavailable,但 PO 要求 Windows 用户仍能看见这块,整体
|
||||
灰显 + 禁用交互 + 顶部横幅说明不可用,引导他们使用上方 Foundry Local Whisper。
|
||||
Linux 当前没有任何本地 ASR 提供,沿用 IS_MAC 隐藏(dead UI 仍然要藏)。 */}
|
||||
{(IS_MAC || IS_WINDOWS) && (<>
|
||||
{IS_WINDOWS && (
|
||||
<Card style={{ marginBottom: 16, background: 'rgba(255, 235, 200, 0.4)' }}>
|
||||
<div role="alert" style={{ fontSize: 13, color: 'var(--ol-ink-2)' }}>
|
||||
{t('localAsr.qwenUnavailableOnWindows')}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
<div
|
||||
aria-disabled={IS_WINDOWS || undefined}
|
||||
// @ts-expect-error — `inert` 是 HTML5 标准属性(React 19+ 一类型,TS lib.dom 旧版未收录)。
|
||||
// 跟 pointerEvents:none 配合让键盘 Tab 也跳过区域内所有 focusable 控件,避免 AT 用户
|
||||
// 激活无效后端的 IPC(review-qwen3 High)。
|
||||
inert={IS_WINDOWS || undefined}
|
||||
style={
|
||||
IS_WINDOWS
|
||||
? { opacity: 0.5, filter: 'grayscale(0.4)', pointerEvents: 'none' as const }
|
||||
: undefined
|
||||
}>
|
||||
{IS_MAC && !engineAvailable && (
|
||||
<Card style={{ marginBottom: 16, background: 'rgba(255, 235, 200, 0.4)' }}>
|
||||
<div style={{ fontSize: 13, color: 'var(--ol-ink-2)' }}>
|
||||
{t('localAsr.engineUnavailable')}
|
||||
@@ -851,6 +869,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</>)}
|
||||
|
||||
{error && (
|
||||
@@ -859,26 +878,47 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{IS_MAC && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{models.map(model => (
|
||||
<ModelRow
|
||||
key={model.id}
|
||||
model={model}
|
||||
remoteSize={remoteSizes[model.id]}
|
||||
progress={progress[model.id]}
|
||||
isActive={settings?.activeModel === model.id}
|
||||
engineAvailable={engineAvailable}
|
||||
disabled={busyModelId !== null && busyModelId !== model.id}
|
||||
testing={testingModelId === model.id}
|
||||
testResult={testResults[model.id]}
|
||||
onDownload={() => void handleDownload(model.id)}
|
||||
onCancel={() => void handleCancel(model.id)}
|
||||
onDelete={() => void handleDelete(model.id)}
|
||||
onSetActive={() => void handleSetActiveModel(model.id)}
|
||||
onTest={() => void handleTest(model.id)}
|
||||
/>
|
||||
))}
|
||||
{(IS_MAC || IS_WINDOWS) && (
|
||||
<div
|
||||
aria-disabled={IS_WINDOWS || undefined}
|
||||
// @ts-expect-error — `inert` 是 HTML5 标准属性(React 19+ 一类型,TS lib.dom 旧版未收录)。
|
||||
// 跟 pointerEvents:none 配合让键盘 Tab 也跳过区域内所有 focusable 控件,避免 AT 用户
|
||||
// 激活无效后端的 IPC(review-qwen3 High)。
|
||||
inert={IS_WINDOWS || undefined}
|
||||
style={
|
||||
IS_WINDOWS
|
||||
? { opacity: 0.5, filter: 'grayscale(0.4)', pointerEvents: 'none' as const }
|
||||
: undefined
|
||||
}>
|
||||
{IS_MAC ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{models.map(model => (
|
||||
<ModelRow
|
||||
key={model.id}
|
||||
model={model}
|
||||
remoteSize={remoteSizes[model.id]}
|
||||
progress={progress[model.id]}
|
||||
isActive={settings?.activeModel === model.id}
|
||||
engineAvailable={engineAvailable}
|
||||
disabled={busyModelId !== null && busyModelId !== model.id}
|
||||
testing={testingModelId === model.id}
|
||||
testResult={testResults[model.id]}
|
||||
onDownload={() => void handleDownload(model.id)}
|
||||
onCancel={() => void handleCancel(model.id)}
|
||||
onDelete={() => void handleDelete(model.id)}
|
||||
onSetActive={() => void handleSetActiveModel(model.id)}
|
||||
onTest={() => void handleTest(model.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
/* Windows 灰显占位:后端列表为空,给一个静态 Card 表明这里"本该是模型列表"。 */
|
||||
<Card>
|
||||
<div style={{ fontSize: 13, color: 'var(--ol-ink-3)' }}>
|
||||
{t('localAsr.qwenUnavailableOnWindows')}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Wrapper>
|
||||
|
||||
@@ -450,6 +450,10 @@ function VocabChip({ entry, onRemove, onToggle }: VocabChipProps) {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
// 父 flex 容器 minHeight: 80 会让 flex item 在 align-self 默认 stretch 下被拉到
|
||||
// 80px 高,chip borderRadius: 999 把高度变大渲染成"超大椭圆"。alignSelf:flex-start
|
||||
// 阻止拉伸,chip 始终保持 content 高度。
|
||||
alignSelf: 'flex-start',
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
padding: '5px 10px 5px 12px',
|
||||
borderRadius: 999,
|
||||
|
||||
Reference in new issue
Block a user