fix: GUI max-width on wrapper + visible TTS swap result + warning cleanup
Some checks are pending
Rust CI / cargo test (jarvis-core) (push) Waiting to run
Rust CI / cargo clippy (push) Waiting to run
Rust CI / cargo check (workspace) (push) Waiting to run

GUI MAX-WIDTH (was still stretched)

Root cause v2: my earlier rule applied max-width to #header and main
INDIVIDUALLY, but they live inside <Container fluid id="wrapper"> from
svelteui which goes full viewport width. Both children inherited that
full width from the flex layout. Constraining each child wouldn't help
when their PARENT was unconstrained.

Fix: max-width:1280px !important on #wrapper itself, with margin:0 auto.
Now the whole app (header + main) sits in a centred 1280px column on
wide monitors. !important is unfortunate but Container's width:100%
sometimes wins under HMR otherwise.

VISIBLE TTS SWAP RESULT (was failing silently)

User saved 'Piper' in dropdown → green 'Saved!' banner → active engine
chip still showed 'sapi'. Root cause: when Piper isn't installed,
`tts::build_backend("piper")` falls back to SAPI internally, but
`set_tts_backend` just logged a warning and returned a generic string.
The GUI couldn't tell the difference between success and silent fallback.

Fix:
- `set_tts_backend` now returns `TtsSwapResult { requested, applied,
  fell_back, note }`. The Svelte page reads `applied` (actual installed
  backend), checks `fell_back`, and shows an ORANGE notification with
  the concrete fix ("запусти tools/piper/install.ps1 чтобы скачать
  бинарь") instead of a green 'saved' lie.
- Saves now also call `refreshActiveBackends()` so the live chip
  ("Активный движок: TTS: sapi") updates without page reload.
- Green notification when swap succeeded normally.

WARNING CLEANUP

- jarvis-app/main.rs: removed unreachable `_ => {}` arm (all IpcAction
  variants are explicit now).
- jarvis-app/tray.rs: moved `info!("Tray initialized.")` to BEFORE the
  Windows message-pump loop (the loop runs forever, so the original
  position was unreachable).
- jarvis-gui/main.rs: dropped unused #[macro_use] on simple_log.
- jarvis-gui/events.rs: #[allow(dead_code)] on Payload struct +
  EventTypes impl + `play` fn — they're intentional API surface for
  future event-emitter wiring, not actual dead code.
- Plus a `cargo fix` pass for misc unused imports.

Down from 12 warnings to 1 (npm-build-completion notice — informational).

Tests: 140 rust + 115 python (was 104, +11 for new wave59_handlers).
Release builds of jarvis-app + jarvis-gui both green.
This commit is contained in:
Bossiara13 2026-05-24 23:11:49 +03:00
parent 944cfcc891
commit 965441d4db
12 changed files with 110 additions and 37 deletions

View file

@ -2,7 +2,6 @@ use std::sync::mpsc::Receiver;
use std::time::SystemTime;
use jarvis_core::{audio_buffer::AudioRingBuffer, audio_processing, audio_processing::vad::listen_window::{ListenWindow, WindowDecision}, audio_processing::vad::webrtc::WebRtcVad, commands, config, listener, recorder, stt, COMMANDS_LIST, intent, voices, ipc::{self, IpcEvent}, i18n, slots};
use rand::seq::SliceRandom;
use crate::should_stop;

View file

@ -240,7 +240,6 @@ fn main() -> Result<(), String> {
version: config::APP_VERSION.map(|s| s.to_string()),
});
}
_ => {}
}
});

View file

@ -7,10 +7,8 @@ use tray_icon::{
use image;
use std::process::Command;
#[cfg(target_os="windows")]
use winit::platform::windows::EventLoopBuilderExtWindows;
use jarvis_core::{config, i18n, voices, ipc::{self, IpcEvent}, SettingsManager};
use jarvis_core::{i18n, voices, ipc::{self, IpcEvent}, SettingsManager};
const TRAY_ICON_BYTES: &[u8] = include_bytes!("../../../resources/icons/32x32.png");
@ -29,6 +27,7 @@ pub fn init_blocking(settings: SettingsManager) {
.unwrap();
let menu_channel = MenuEvent::receiver();
info!("Tray initialized.");
#[cfg(target_os = "linux")]
{
@ -77,8 +76,6 @@ pub fn init_blocking(settings: SettingsManager) {
std::thread::sleep(std::time::Duration::from_millis(50));
}
}
info!("Tray initialized.");
}
fn handle_menu_event(event: &MenuEvent, settings: &SettingsManager, tray_state: &menu::TrayState) {

View file

@ -1,7 +1,6 @@
use tray_icon::menu::{CheckMenuItem, Menu, MenuItem, PredefinedMenuItem, Submenu};
use jarvis_core::{i18n, voices, SettingsManager};
use jarvis_core::config::structs::{WakeWordEngine, NoiseSuppressionBackend};
// RADIO GROUP

View file

@ -1,4 +1,4 @@
use fluent_bundle::{FluentBundle, FluentResource, FluentArgs, FluentValue};
use fluent_bundle::{FluentResource, FluentArgs, FluentValue};
use fluent_bundle::concurrent::FluentBundle as ConcurrentFluentBundle;
use once_cell::sync::OnceCell;
use parking_lot::RwLock;

View file

@ -1,6 +1,6 @@
use super::client::ChatMessage;
use std::fs;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
pub struct ConversationHistory {
system: Option<ChatMessage>,

View file

@ -1,6 +1,9 @@
use tauri::Emitter;
// the payload type must implement `Serialize` and `Clone`.
// Kept around as the canonical shape for future event emitter wiring even
// though nothing currently emits one — silence the dead-code lint.
#[allow(dead_code)]
#[derive(Clone, serde::Serialize)]
pub struct Payload {
pub data: String,
@ -16,6 +19,7 @@ pub enum EventTypes {
CommandEnd,
}
#[allow(dead_code)]
impl EventTypes {
pub fn get(&self) -> &str {
match self {
@ -29,6 +33,7 @@ impl EventTypes {
}
}
#[allow(dead_code)]
pub fn play(phrase: &str, app_handle: &tauri::AppHandle) {
app_handle
.emit(

View file

@ -3,7 +3,6 @@
use jarvis_core::{config, db, i18n, voices, DB, SettingsManager};
#[macro_use]
extern crate simple_log;
mod events;

View file

@ -46,8 +46,22 @@ pub fn set_llm_backend(name: String) -> Result<String, String> {
/// Set TTS backend. Persists to settings DB AND hot-swaps the live process
/// (in the GUI process; the running daemon picks it up via IPC SwitchTts).
///
/// Returns a struct describing what ACTUALLY happened — `requested` is what
/// the user asked for, `applied` is what the engine actually installed. They
/// differ when, e.g., Piper was requested but `piper.exe` isn't on disk; the
/// engine falls back to SAPI to keep speech working, and the GUI shows a
/// warning instead of pretending the swap succeeded.
#[derive(Serialize)]
pub struct TtsSwapResult {
pub requested: String,
pub applied: String,
pub fell_back: bool,
pub note: Option<String>,
}
#[tauri::command]
pub fn set_tts_backend(name: String) -> Result<String, String> {
pub fn set_tts_backend(name: String) -> Result<TtsSwapResult, String> {
let normalized = match name.trim().to_lowercase().as_str() {
"" | "auto" => "".to_string(),
"sapi" | "piper" | "silero" => name.to_lowercase(),
@ -67,18 +81,34 @@ pub fn set_tts_backend(name: String) -> Result<String, String> {
return Err("settings DB not initialised".into());
}
// 2. Apply RIGHT NOW in this GUI process (best-effort — log on failure
// but don't fail the call, the DB change already happened).
let apply_target = if normalized.is_empty() { "auto" } else { normalized.as_str() };
if let Err(e) = jarvis_core::tts::swap_to(apply_target) {
log::warn!("TTS hot-swap in GUI failed (DB still updated): {}", e);
// 2. Apply RIGHT NOW in this GUI process AND report what actually got
// installed. The fallback path in build_backend() may silently swap
// Piper→SAPI when the Piper binary is missing; without surfacing that
// the user sees "saved" but the active engine label stays SAPI and
// they don't know why.
let requested = if normalized.is_empty() { "auto".to_string() } else { normalized.clone() };
let applied = match jarvis_core::tts::swap_to(&requested) {
Ok(name) => name.to_string(),
Err(e) => {
log::warn!("TTS hot-swap failed: {}", e);
jarvis_core::tts::backend().name().to_string()
}
};
// 3. The running jarvis-app daemon is a SEPARATE process with its own
// TTS instance; it picks up the change via the IpcAction::SwitchTts
// message — fired from the Svelte page via sendAction() WebSocket.
let fell_back = applied.eq_ignore_ascii_case("sapi")
&& !requested.eq_ignore_ascii_case("sapi")
&& requested != "auto";
let note = if fell_back {
Some(match requested.as_str() {
"piper" => "Piper не установлен. Запусти tools/piper/install.ps1 чтобы скачать бинарь и голос.".into(),
"silero" => "Silero недоступен. Нужен Python + torch + silero_tts.py helper.".into(),
other => format!("'{}' недоступен — откатился на SAPI.", other),
})
} else {
None
};
Ok(if normalized.is_empty() { "auto".into() } else { normalized })
Ok(TtsSwapResult { requested, applied, fell_back, note })
}
/// Reset conversation context (clears LLM history turns, keeps system prompt).

View file

@ -2,8 +2,6 @@ use sysinfo::{System, Pid, ProcessRefreshKind, RefreshKind, CpuRefreshKind, Comp
use peak_alloc::PeakAlloc;
use std::sync::Mutex;
use once_cell::sync::Lazy;
use std::process::Command;
use std::env;
#[global_allocator]
static PEAK_ALLOC: PeakAlloc = PeakAlloc;

View file

@ -61,22 +61,29 @@ a {
margin: 0;
}
#header,
main {
padding: 0 30px;
// Cap content width on wide monitors so long labels don't stretch
// across 1900px+ screens. On smaller windows this is a no-op.
max-width: 1280px;
margin: 0 auto;
// Cap the WHOLE app (header + main) to 1280px on wide monitors and centre it,
// so labels in the nav don't sprawl across 1920px+ screens. The wrapper is
// `<Container fluid id="wrapper">` from svelteui fluid keeps it width: 100%
// inside the viewport, then we shrink the inner content here. Smaller windows
// are unaffected (max-width is a ceiling, not a floor).
//
// `!important` is unfortunate but svelteui's Container emits a width:100%
// rule that sometimes outweighs ours under HMR. The override is loud and
// localised only on the top-level wrapper.
#wrapper {
max-width: 1280px !important;
margin: 0 auto !important;
box-sizing: border-box;
}
#header,
main {
padding: 0 30px;
box-sizing: border-box;
}
// The home page's arc reactor + status bar can keep their own internal
// max-width while the page itself can still go full width the page
// layout below `main` already handles its own centering via flex.
.app-container.assist-page {
width: 100%;
max-width: 1280px;
margin: 0 auto;
}

View file

@ -138,12 +138,30 @@
}
}
interface TtsSwapResult {
requested: string
applied: string
fell_back: boolean
note: string | null
}
let ttsSwapInfo: TtsSwapResult | null = null
async function applyTtsBackend(target: string) {
backendSwapError = ""
ttsSwapInfo = null
try {
// 1. GUI Tauri command — persists to DB + hot-swaps GUI's TTS.
await invoke<string>("set_tts_backend", { name: target })
// Returns the actual installed backend (may differ from
// requested if Piper/Silero unavailable → fell back to SAPI).
const res = await invoke<TtsSwapResult>("set_tts_backend", { name: target })
ttsSwapInfo = res
// 2. Tell the running daemon (separate process) to switch too.
switchDaemonTts(target || "auto")
// 3. Refresh the "Активный движок" chip — without this, the user
// saves Piper, sees "saved!", but the active label stays SAPI
// because we never re-read the live engine.
await refreshActiveBackends()
} catch (err) {
backendSwapError = String(err)
}
@ -453,9 +471,31 @@
>
{backendSwapError}
</Notification>
<Space h="md" />
{/if}
{#if ttsSwapInfo && ttsSwapInfo.fell_back}
<Notification
title="TTS откатился на {ttsSwapInfo.applied.toUpperCase()}"
color="orange"
withCloseButton={true}
on:close={() => { ttsSwapInfo = null }}
>
Запросил <strong>{ttsSwapInfo.requested}</strong> — недоступен.
{#if ttsSwapInfo.note}<br/>{ttsSwapInfo.note}{/if}
</Notification>
<Space h="md" />
{:else if ttsSwapInfo && !ttsSwapInfo.fell_back}
<Notification
title="TTS переключён на {ttsSwapInfo.applied.toUpperCase()}"
color="teal"
withCloseButton={true}
on:close={() => { ttsSwapInfo = null }}
>
Применено сразу — без перезапуска.
</Notification>
<Space h="md" />
{/if}
<InputWrapper label={t('settings-llm-context') || 'Контекст разговора'}>
<Text size="sm" color="gray">