feat(gui): footer backend chips + Settings 'AI Backends' tab + i18n

Surfaces the LLM/TTS hot-swap UX in the GUI so the user doesn't need env vars
or voice commands. Tauri commands for this landed in b243e67; this commit
wires them to Svelte.

Footer (frontend/src/components/Footer.svelte)
  - Poll get_active_backends() every 5 s, show TTS / LLM / Profile chips.
  - Chips have border accents by kind (cyan = TTS, lime = LLM, orange = Profile).
  - Friendly title tooltips with full backend + model names.
  - Silent if jarvis-gui can't reach the command (e.g. cold start) — no UI flicker.

Settings — new "AI Backends" tab (frontend/src/routes/settings/index.svelte)
  - Status banner showing active LLM (with model name) + TTS + Profile.
  - LLM selector: Auto / Groq / Ollama → calls set_llm_backend on change
    (hot-swap, persisted to DB).
  - TTS selector: Auto / SAPI / Piper / Silero → calls set_tts_backend
    (persisted, effective on next jarvis-app restart).
  - "Reset context" button → llm_reset_context Tauri command.
  - Error toast for swap failures (e.g. Groq selected but no GROQ_TOKEN).
  - Tips alert: how to install Ollama / Piper / voice commands.
  - Loads prefs via db_read('llm_backend' / 'tts_backend') on mount.

i18n strings (ru/en/ua, .ftl files)
  - 14 new keys: settings-ai-backends, settings-ai-active, settings-ai-auto,
    settings-ai-applying, settings-ai-error, settings-ai-tips,
    settings-llm-backend{,-desc}, settings-tts-backend{,-desc},
    settings-llm-context{,-desc}, settings-llm-reset, settings-profile.

Build glue (crates/jarvis-gui/Cargo.toml)
  - jarvis-core dep now includes the `llm` feature so backends.rs can resolve
    `jarvis_core::llm::*` (was failing to compile after the global LLM module).

.gitignore
  - tools/piper/*.dll, piper.exe, voices/, espeak-ng-data/ — locally
    installed binaries from tools/piper/install.ps1, not committed.

Build: cargo build --release -p jarvis-gui green (4m, includes npm/Vite).
Test path: rebuild + launch jarvis-gui, /settings → "AI Backends" tab.
This commit is contained in:
Bossiara13 2026-05-15 17:06:46 +03:00
parent b243e67870
commit d63399f4c9
6 changed files with 313 additions and 4 deletions

View file

@ -1,4 +1,7 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte"
import { invoke } from "@tauri-apps/api/core"
import { appInfo, translations, translate } from "@/stores"
$: t = (key: string) => translate($translations, key)
@ -14,9 +17,72 @@
upstreamLink = info.upstreamLink
issuesLink = info.issuesLink
})
// Active backends — refreshed every 5 seconds. Silently fails if the
// Tauri command isn't registered (e.g. running against an old jarvis-app).
interface ActiveBackends {
tts: string
llm: string
llm_model: string | null
profile: string
}
let backends: ActiveBackends | null = null
let pollId: ReturnType<typeof setInterval> | null = null
async function refreshBackends() {
try {
backends = await invoke<ActiveBackends>("get_active_backends")
} catch (err) {
// ignore
}
}
onMount(() => {
refreshBackends()
pollId = setInterval(refreshBackends, 5000)
})
onDestroy(() => {
if (pollId !== null) clearInterval(pollId)
})
function chipTitle(key: "tts" | "llm"): string {
if (!backends) return ""
if (key === "tts") return `TTS backend: ${backends.tts}`
const model = backends.llm_model ? ` (${backends.llm_model})` : ""
return `LLM backend: ${backends.llm}${model}`
}
function chipLabel(value: string): string {
const map: Record<string, string> = {
sapi: "SAPI",
piper: "Piper",
silero: "Silero",
groq: "Groq",
ollama: "Ollama",
none: "—",
}
return map[value] ?? value
}
</script>
<footer id="footer">
{#if backends}
<p class="backends" title="Активные движки. Меняются в /settings и голосом.">
<span class="chip chip-tts" title={chipTitle("tts")}>
<small>TTS</small> {chipLabel(backends.tts)}
</span>
<span class="chip chip-llm" title={chipTitle("llm")}>
<small>LLM</small> {chipLabel(backends.llm)}
</span>
{#if backends.profile && backends.profile !== "default"}
<span class="chip chip-profile" title="Active profile">
<small>Profile</small> {backends.profile}
</span>
{/if}
</p>
{/if}
<p>
© {currentYear} J.A.R.V.I.S.
<span class="edition" title="Rust + Tauri build">Rust</span>
@ -62,6 +128,35 @@
vertical-align: middle;
}
.backends {
margin-bottom: 8px;
.chip {
display: inline-block;
margin: 0 3px;
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 500;
letter-spacing: 0.2px;
color: #cfd2d5;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
small {
color: #6c6e71;
margin-right: 3px;
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
&.chip-tts { border-color: rgba(82, 254, 254, 0.25); }
&.chip-llm { border-color: rgba(138, 200, 50, 0.25); }
&.chip-profile { border-color: rgba(255, 168, 60, 0.25); }
}
}
p {
margin: 0;
padding: 0;