Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions openless-all/app/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
}
Expand Down
9 changes: 9 additions & 0 deletions openless-all/app/src-tauri/src/polish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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\
Expand Down
74 changes: 67 additions & 7 deletions openless-all/app/src/components/Capsule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);
Expand All @@ -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={{
Expand All @@ -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',
}}
>
{/* "正在翻译" 徽章 — 嵌套两层:
Expand Down Expand Up @@ -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; }
Expand Down
15 changes: 10 additions & 5 deletions openless-all/app/src/components/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
5 changes: 3 additions & 2 deletions openless-all/app/src/components/WindowChrome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
1 change: 1 addition & 0 deletions openless-all/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions openless-all/app/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 で利用可能',
Expand Down
1 change: 1 addition & 0 deletions openless-all/app/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 에서 사용 가능',
Expand Down
1 change: 1 addition & 0 deletions openless-all/app/src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 可用',
Expand Down
1 change: 1 addition & 0 deletions openless-all/app/src/i18n/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 可用',
Expand Down
Loading
Loading