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
44 changes: 44 additions & 0 deletions openless-all/app/src-tauri/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 句柄。
Expand Down Expand Up @@ -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),
}),
Expand Down Expand Up @@ -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() {
Expand All @@ -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 {
Expand Down
69 changes: 61 additions & 8 deletions openless-all/app/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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();
Expand Down Expand Up @@ -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;

Expand All @@ -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);
Expand Down Expand Up @@ -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)
Comment thread
H-Chris233 marked this conversation as resolved.
}
}

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);
}
}
12 changes: 11 additions & 1 deletion openless-all/app/src-tauri/src/polish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
Expand Down Expand Up @@ -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"));
}
}
2 changes: 1 addition & 1 deletion openless-all/app/src/components/Capsule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading