Merge pull request #135 from H-Chris233/main

fix(polish): 调整热词注入提示措辞
This commit is contained in:
Chris233 authored and GitHub committed 2026-05-01 23:46:33 +08:00
commit 08e48cc852
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 {
+61 -8
View File
@@ -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);
}
}
+11 -1
View File
@@ -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"));
}
}
+1 -1
View File
@@ -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',