a6a098d added daemon-side IPC handlers (SwitchLlm, ReloadLlm, QueryHealth)
but the GUI was still talking only to its own process. This commit wires
the frontend to actually use those handlers.
frontend/src/lib/ipc.ts
- New DaemonHealth interface + daemonHealth: writable<DaemonHealth | null>.
- handleEvent: "health_snapshot" case fills daemonHealth from daemon's IpcEvent.
- New senders:
switchDaemonLlm(backend) → action "switch_llm"
reloadDaemonLlm() → action "reload_llm"
queryDaemonHealth() → action "query_health"
frontend/src/stores.ts
- Re-exports daemonHealth + the three new senders + DaemonHealth type.
frontend/src/components/Footer.svelte
- Polls every 5s but PREFERS IPC: if daemon connected, calls queryDaemonHealth
(snapshot arrives via daemonHealth store).
- Falls back to invoke("get_active_backends") if daemon offline — shows
GUI-process view in that case, with the chip tooltip distinguishing
"daemon" vs "gui-process" source.
frontend/src/routes/settings/index.svelte (AI Backends tab)
- applyLlmBackend now ALSO fires switchDaemonLlm so the running listener
picks the new backend without restart.
- "Auto" (empty) path calls reloadDaemonLlm so daemon re-reads DB.
End-to-end flow now:
GUI dropdown → set_llm_backend (Tauri, persists DB + swaps GUI proc)
↘ switchDaemonLlm (IPC, swaps daemon proc too)
↘ daemon emits health_snapshot on next QueryHealth
↘ daemonHealth store updates
↘ Footer chips re-render with new state
Build: cargo build --release -p jarvis-gui green.
230 lines
6.5 KiB
Svelte
230 lines
6.5 KiB
Svelte
<script lang="ts">
|
|
import { onMount, onDestroy } from "svelte"
|
|
import { invoke } from "@tauri-apps/api/core"
|
|
|
|
import {
|
|
appInfo, translations, translate,
|
|
daemonHealth, ipcConnected, queryDaemonHealth,
|
|
} from "@/stores"
|
|
|
|
$: t = (key: string) => translate($translations, key)
|
|
|
|
let repoLink = ""
|
|
let upstreamLink = ""
|
|
let issuesLink = ""
|
|
|
|
const currentYear = new Date().getFullYear()
|
|
|
|
appInfo.subscribe(info => {
|
|
repoLink = info.repositoryLink
|
|
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
|
|
let connected = false
|
|
|
|
ipcConnected.subscribe(v => { connected = v })
|
|
daemonHealth.subscribe(h => {
|
|
if (h) {
|
|
backends = {
|
|
tts: h.tts_backend,
|
|
llm: h.llm_backend,
|
|
llm_model: h.llm_model,
|
|
profile: h.active_profile,
|
|
}
|
|
}
|
|
})
|
|
|
|
async function refreshBackends() {
|
|
// Prefer IPC — daemon's actual state.
|
|
if (connected) {
|
|
queryDaemonHealth() // response arrives via daemonHealth store
|
|
return
|
|
}
|
|
// Fallback to GUI-process view if daemon offline.
|
|
try {
|
|
const fallback = await invoke<ActiveBackends>("get_active_backends")
|
|
if (!backends) backends = fallback
|
|
} catch (err) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
onMount(() => {
|
|
refreshBackends()
|
|
pollId = setInterval(refreshBackends, 5000)
|
|
})
|
|
|
|
onDestroy(() => {
|
|
if (pollId !== null) clearInterval(pollId)
|
|
})
|
|
|
|
function chipTitle(key: "tts" | "llm"): string {
|
|
if (!backends) return ""
|
|
const src = connected ? "daemon" : "gui-process"
|
|
if (key === "tts") return `TTS backend: ${backends.tts} (${src})`
|
|
const model = backends.llm_model ? ` (${backends.llm_model})` : ""
|
|
return `LLM backend: ${backends.llm}${model} (${src})`
|
|
}
|
|
|
|
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>
|
|
</p>
|
|
<p class="links">
|
|
<a href={repoLink} target="_blank" rel="noopener noreferrer">
|
|
<img src="/media/icons/github-logo.png" alt="GitHub" width="18px" />
|
|
<span>{t('footer-github')}</span>
|
|
</a>
|
|
·
|
|
<a href={issuesLink} target="_blank" rel="noopener noreferrer">
|
|
<span>{t('footer-issues')}</span>
|
|
</a>
|
|
</p>
|
|
<p class="links last">
|
|
<small>
|
|
{t('footer-fork-of')}
|
|
<a href={upstreamLink} target="_blank" rel="noopener noreferrer">Priler/jarvis</a>
|
|
· CC BY-NC-SA 4.0
|
|
</small>
|
|
</p>
|
|
</footer>
|
|
|
|
<style lang="scss">
|
|
#footer {
|
|
text-align: center;
|
|
color: #6c6e71;
|
|
font-size: 13px;
|
|
font-weight: normal;
|
|
line-height: 1.7em;
|
|
margin-top: 15px;
|
|
|
|
.edition {
|
|
display: inline-block;
|
|
margin-left: 6px;
|
|
padding: 1px 7px;
|
|
border-radius: 4px;
|
|
background: #b7411a;
|
|
color: #fff;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.5px;
|
|
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;
|
|
|
|
&.links {
|
|
margin-top: 5px;
|
|
margin-bottom: 15px;
|
|
|
|
&.last {
|
|
margin-top: -5px;
|
|
color: #8a8d90;
|
|
font-size: 11px;
|
|
}
|
|
}
|
|
}
|
|
|
|
a {
|
|
color: #555759 !important;
|
|
text-decoration: none;
|
|
transition: 0.3s;
|
|
|
|
& > span {
|
|
color: #185876;
|
|
border-bottom: 1px solid #185876;
|
|
transition: 0.3s;
|
|
}
|
|
|
|
img {
|
|
opacity: 0.5;
|
|
transition: opacity 0.5s;
|
|
margin-top: -3px;
|
|
}
|
|
|
|
&:hover {
|
|
color: #777a7d !important;
|
|
|
|
& > span {
|
|
color: #2A9CD0;
|
|
}
|
|
|
|
img {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</style>
|