4 files changed
+117
-10
No files matched your search
@@ -111,6 +111,9 @@ struct Inner {
|
||||
qa_hotkey: Mutex<Option<QaHotkeyMonitor>>,
|
||||
/// QA 单独的 session 状态,与 dictation 的 SessionPhase 不冲突。
|
||||
qa_state: Mutex<QaSessionState>,
|
||||
/// 最近一次应用到 capsule 窗口的几何状态。避免录音 level tick 反复触发
|
||||
/// resize / reposition。
|
||||
capsule_layout: Mutex<Option<CapsuleLayoutState>>,
|
||||
/// QA 用的 ASR 句柄(始终是 Volcengine 流式)。
|
||||
qa_asr: Mutex<Option<Arc<VolcengineStreamingASR>>>,
|
||||
/// QA 用的 Recorder 句柄。
|
||||
@@ -181,6 +184,7 @@ impl Coordinator {
|
||||
translation_modifier_seen: AtomicBool::new(false),
|
||||
qa_hotkey: Mutex::new(None),
|
||||
qa_state: Mutex::new(QaSessionState::default()),
|
||||
capsule_layout: Mutex::new(None),
|
||||
qa_asr: Mutex::new(None),
|
||||
qa_recorder: Mutex::new(None),
|
||||
}),
|
||||
@@ -2451,6 +2455,7 @@ fn emit_capsule(
|
||||
let show_capsule = inner.prefs.get().show_capsule;
|
||||
if let Some(window) = app.get_webview_window("capsule") {
|
||||
let visible = !matches!(state, CapsuleState::Idle);
|
||||
maybe_position_capsule_bottom_center(inner, &window, payload.translation);
|
||||
if show_capsule && visible {
|
||||
if cfg!(target_os = "windows") {
|
||||
if !show_capsule_window_no_activate() {
|
||||
@@ -2471,6 +2476,45 @@ fn emit_capsule(
|
||||
let _ = app.emit_to("capsule", "capsule:state", payload);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct CapsuleLayoutState {
|
||||
translation_active: bool,
|
||||
monitor_x: i32,
|
||||
monitor_y: i32,
|
||||
monitor_width: u32,
|
||||
monitor_height: u32,
|
||||
scale_bits: u64,
|
||||
}
|
||||
|
||||
fn maybe_position_capsule_bottom_center<R: tauri::Runtime>(
|
||||
inner: &Arc<Inner>,
|
||||
window: &tauri::WebviewWindow<R>,
|
||||
translation_active: bool,
|
||||
) {
|
||||
let Some(monitor) = window.current_monitor().ok().flatten() else {
|
||||
return;
|
||||
};
|
||||
let next = CapsuleLayoutState {
|
||||
translation_active,
|
||||
monitor_x: monitor.position().x,
|
||||
monitor_y: monitor.position().y,
|
||||
monitor_width: monitor.size().width,
|
||||
monitor_height: monitor.size().height,
|
||||
scale_bits: monitor.scale_factor().to_bits(),
|
||||
};
|
||||
{
|
||||
let last = inner.capsule_layout.lock();
|
||||
if last.as_ref() == Some(&next) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if crate::position_capsule_bottom_center(window, translation_active).is_ok() {
|
||||
let mut last = inner.capsule_layout.lock();
|
||||
*last = Some(next);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────── audio bridge ───────────────────────────
|
||||
|
||||
struct DeferredAsrBridge {
|
||||
|
||||
@@ -35,7 +35,7 @@ use std::time::Duration;
|
||||
static QA_WINDOW_POSITIONED: AtomicBool = AtomicBool::new(false);
|
||||
use tauri::menu::{MenuBuilder, MenuItemBuilder};
|
||||
use tauri::tray::{MouseButton, TrayIconBuilder, TrayIconEvent};
|
||||
use tauri::{AppHandle, Emitter, LogicalPosition, Manager, RunEvent, Runtime};
|
||||
use tauri::{AppHandle, Emitter, LogicalPosition, LogicalSize, Manager, RunEvent, Runtime};
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
@@ -61,7 +61,7 @@ pub fn run() {
|
||||
// Capsule 启动时定位到屏幕底部居中并隐藏;coordinator 按需显示。
|
||||
// 与 Swift `CapsuleWindowController.repositionToBottomCenter` 同语义。
|
||||
if let Some(capsule) = app.get_webview_window("capsule") {
|
||||
if let Err(e) = position_capsule_bottom_center(&capsule) {
|
||||
if let Err(e) = position_capsule_bottom_center(&capsule, false) {
|
||||
log::warn!("[capsule] position failed: {e}");
|
||||
}
|
||||
let _ = capsule.hide();
|
||||
@@ -436,8 +436,6 @@ const QA_WINDOW_WIDTH: f64 = 380.0;
|
||||
const QA_WINDOW_HEIGHT: f64 = 440.0;
|
||||
/// 胶囊与 QA 窗口的间距,与设计稿一致。
|
||||
const QA_WINDOW_GAP_TO_CAPSULE: f64 = 8.0;
|
||||
/// 胶囊高度(与 `position_capsule_bottom_center` 中一致)。
|
||||
const CAPSULE_HEIGHT_FOR_QA: f64 = 96.0;
|
||||
/// 给 macOS Dock 留的下边距(与 capsule 同源)。
|
||||
const DOCK_BOTTOM_PADDING_FOR_QA: f64 = 80.0;
|
||||
|
||||
@@ -452,10 +450,11 @@ fn position_qa_window<R: tauri::Runtime>(window: &tauri::WebviewWindow<R>) -> ta
|
||||
let size = monitor.size();
|
||||
let logical_w = size.width as f64 / scale;
|
||||
let logical_h = size.height as f64 / scale;
|
||||
let capsule_height = capsule_window_size(false).1;
|
||||
let x = ((logical_w - QA_WINDOW_WIDTH) / 2.0).max(0.0);
|
||||
let y = (logical_h
|
||||
- DOCK_BOTTOM_PADDING_FOR_QA
|
||||
- CAPSULE_HEIGHT_FOR_QA
|
||||
- capsule_height
|
||||
- QA_WINDOW_GAP_TO_CAPSULE
|
||||
- QA_WINDOW_HEIGHT)
|
||||
.max(0.0);
|
||||
@@ -563,21 +562,75 @@ pub(crate) fn hide_qa_window<R: tauri::Runtime>(app: &AppHandle<R>) {
|
||||
|
||||
/// 把 capsule 窗口移到屏幕底部居中,与 Swift `CapsuleWindowController.repositionToBottomCenter` 同效。
|
||||
/// 留 80pt 给 macOS Dock;Windows 任务栏一般在底部 48pt 以内,整体也合适。
|
||||
fn position_capsule_bottom_center<R: tauri::Runtime>(
|
||||
pub(crate) fn position_capsule_bottom_center<R: tauri::Runtime>(
|
||||
window: &tauri::WebviewWindow<R>,
|
||||
translation_active: bool,
|
||||
) -> tauri::Result<()> {
|
||||
let monitor = match window.current_monitor()? {
|
||||
Some(m) => m,
|
||||
None => return Ok(()),
|
||||
};
|
||||
let (cap_w, cap_h) = capsule_window_size(translation_active);
|
||||
window.set_size(LogicalSize::new(cap_w, cap_h))?;
|
||||
|
||||
let scale = monitor.scale_factor();
|
||||
let size = monitor.size();
|
||||
let logical_w = size.width as f64 / scale;
|
||||
let logical_h = size.height as f64 / scale;
|
||||
let cap_w = 220.0_f64;
|
||||
let cap_h = 96.0_f64;
|
||||
let x = ((logical_w - cap_w) / 2.0).max(0.0);
|
||||
let y = (logical_h - cap_h - 80.0).max(0.0);
|
||||
window.set_position(LogicalPosition::new(x, y))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn capsule_window_size(translation_active: bool) -> (f64, f64) {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let height = if translation_active { 110.0 } else { 52.0 };
|
||||
(196.0, height)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let height = if translation_active { 110.0 } else { 42.0 };
|
||||
(176.0, height)
|
||||
}
|
||||
}
|
||||
|
||||
fn capsule_height_for_qa() -> f64 {
|
||||
capsule_window_size(false).1
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{capsule_height_for_qa, capsule_window_size};
|
||||
|
||||
#[test]
|
||||
fn capsule_window_size_matches_visible_pill_when_not_translating() {
|
||||
let (width, height) = capsule_window_size(false);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!((width, height), (196.0, 52.0));
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
assert_eq!((width, height), (176.0, 42.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capsule_window_size_expands_for_translation_badge() {
|
||||
let (width, height) = capsule_window_size(true);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!((width, height), (196.0, 110.0));
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
assert_eq!((width, height), (176.0, 110.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qa_anchor_uses_normal_capsule_height_source() {
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(capsule_height_for_qa(), 52.0);
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
assert_eq!(capsule_height_for_qa(), 42.0);
|
||||
}
|
||||
}
|
||||
@@ -411,7 +411,7 @@ fn compose_system_prompt(mode: PolishMode, hotwords: &[String]) -> String {
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
format!(
|
||||
"{}\n\n热词(用户提供的正确写法,仅当原始转写明显是其误识别时才纠正,不做机械替换):\n{}",
|
||||
"{}\n\n热词(用户希望以下写法在输出中保持准确;当转写中出现这些词的同音 / 近形误识别时,优先按上述写法输出,不做无关词的机械替换):\n{}",
|
||||
base, bullets
|
||||
)
|
||||
}
|
||||
@@ -888,4 +888,14 @@ mod tests {
|
||||
// 防回归:旧版"另外:"标签写法不能再出现在示例输出里。
|
||||
assert!(!prompt.contains("另外:检查一下当前还有哪些 issues"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compose_system_prompt_prefers_correct_spelling_for_hotwords() {
|
||||
let prompt = compose_system_prompt(PolishMode::Light, &["GitHub".into(), "OpenLess".into()]);
|
||||
|
||||
assert!(prompt.contains("用户希望以下写法在输出中保持准确"));
|
||||
assert!(prompt.contains("同音 / 近形误识别时,优先按上述写法输出"));
|
||||
assert!(prompt.contains("- GitHub"));
|
||||
assert!(prompt.contains("- OpenLess"));
|
||||
}
|
||||
}
|
||||
@@ -323,7 +323,7 @@ export function Capsule() {
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
// bottom = 50%(pill 中线)+ pill 半高 21px(capsuleLayout mac=42)+ 8px 间隔。
|
||||
// 胶囊窗口高度 110(tauri.conf.json)刚好装下 badge + 间隔 + pill。
|
||||
// 只有翻译徽章可见时才需要额外高度;普通录音/转写状态由后端缩到 pill 本体,避免透明死区。
|
||||
bottom: 'calc(50% + 21px + 8px)',
|
||||
transform: 'translateX(-50%)',
|
||||
pointerEvents: 'none',
|
||||
|
||||
Reference in new issue
Block a user