feat(gui): wire Footer + Settings to daemon over IPC (closes the gap from a6a098d)
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.
This commit is contained in:
parent
a6a098de15
commit
a2dfadf5c1
4 changed files with 90 additions and 6 deletions
|
|
@ -2,7 +2,10 @@
|
||||||
import { onMount, onDestroy } from "svelte"
|
import { onMount, onDestroy } from "svelte"
|
||||||
import { invoke } from "@tauri-apps/api/core"
|
import { invoke } from "@tauri-apps/api/core"
|
||||||
|
|
||||||
import { appInfo, translations, translate } from "@/stores"
|
import {
|
||||||
|
appInfo, translations, translate,
|
||||||
|
daemonHealth, ipcConnected, queryDaemonHealth,
|
||||||
|
} from "@/stores"
|
||||||
|
|
||||||
$: t = (key: string) => translate($translations, key)
|
$: t = (key: string) => translate($translations, key)
|
||||||
|
|
||||||
|
|
@ -29,10 +32,30 @@
|
||||||
|
|
||||||
let backends: ActiveBackends | null = null
|
let backends: ActiveBackends | null = null
|
||||||
let pollId: ReturnType<typeof setInterval> | 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() {
|
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 {
|
try {
|
||||||
backends = await invoke<ActiveBackends>("get_active_backends")
|
const fallback = await invoke<ActiveBackends>("get_active_backends")
|
||||||
|
if (!backends) backends = fallback
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
@ -49,9 +72,10 @@
|
||||||
|
|
||||||
function chipTitle(key: "tts" | "llm"): string {
|
function chipTitle(key: "tts" | "llm"): string {
|
||||||
if (!backends) return ""
|
if (!backends) return ""
|
||||||
if (key === "tts") return `TTS backend: ${backends.tts}`
|
const src = connected ? "daemon" : "gui-process"
|
||||||
|
if (key === "tts") return `TTS backend: ${backends.tts} (${src})`
|
||||||
const model = backends.llm_model ? ` (${backends.llm_model})` : ""
|
const model = backends.llm_model ? ` (${backends.llm_model})` : ""
|
||||||
return `LLM backend: ${backends.llm}${model}`
|
return `LLM backend: ${backends.llm}${model} (${src})`
|
||||||
}
|
}
|
||||||
|
|
||||||
function chipLabel(value: string): string {
|
function chipLabel(value: string): string {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,21 @@ export const lastRecognizedText = writable("")
|
||||||
export const lastExecutedCommand = writable("")
|
export const lastExecutedCommand = writable("")
|
||||||
export const lastError = writable("")
|
export const lastError = writable("")
|
||||||
|
|
||||||
|
// Daemon runtime state — populated by health_snapshot events from jarvis-app.
|
||||||
|
// `null` means we never received one (daemon not running or doesn't support
|
||||||
|
// QueryHealth yet).
|
||||||
|
export interface DaemonHealth {
|
||||||
|
tts_backend: string
|
||||||
|
llm_backend: string
|
||||||
|
llm_model: string | null
|
||||||
|
active_profile: string
|
||||||
|
memory_facts: number
|
||||||
|
scheduled_tasks: number
|
||||||
|
language: string
|
||||||
|
version: string | null
|
||||||
|
}
|
||||||
|
export const daemonHealth = writable<DaemonHealth | null>(null)
|
||||||
|
|
||||||
// ### CONNECTION ###
|
// ### CONNECTION ###
|
||||||
|
|
||||||
const IPC_URL = "ws://127.0.0.1:9712"
|
const IPC_URL = "ws://127.0.0.1:9712"
|
||||||
|
|
@ -133,6 +148,19 @@ function handleEvent(data: any) {
|
||||||
// bring window to foreground
|
// bring window to foreground
|
||||||
revealWindow()
|
revealWindow()
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case "health_snapshot":
|
||||||
|
daemonHealth.set({
|
||||||
|
tts_backend: data.tts_backend ?? "none",
|
||||||
|
llm_backend: data.llm_backend ?? "none",
|
||||||
|
llm_model: data.llm_model ?? null,
|
||||||
|
active_profile: data.active_profile ?? "default",
|
||||||
|
memory_facts: data.memory_facts ?? 0,
|
||||||
|
scheduled_tasks: data.scheduled_tasks ?? 0,
|
||||||
|
language: data.language ?? "ru",
|
||||||
|
version: data.version ?? null,
|
||||||
|
})
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -175,6 +203,24 @@ export function sendTextCommand(text: string): boolean {
|
||||||
return sendAction("text_command", { text })
|
return sendAction("text_command", { text })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IMBA: hot-swap daemon's LLM backend over IPC so the running listener picks
|
||||||
|
// the new backend immediately. Returns false if daemon not connected.
|
||||||
|
export function switchDaemonLlm(backend: "groq" | "ollama"): boolean {
|
||||||
|
return sendAction("switch_llm", { backend })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell daemon to re-read llm_backend from settings DB. Use after GUI changes
|
||||||
|
// the DB independently.
|
||||||
|
export function reloadDaemonLlm(): boolean {
|
||||||
|
return sendAction("reload_llm")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask daemon for a runtime snapshot. Daemon will respond with a
|
||||||
|
// "health_snapshot" event which fills the `daemonHealth` store.
|
||||||
|
export function queryDaemonHealth(): boolean {
|
||||||
|
return sendAction("query_health")
|
||||||
|
}
|
||||||
|
|
||||||
async function revealWindow() {
|
async function revealWindow() {
|
||||||
try {
|
try {
|
||||||
const window = getCurrentWindow()
|
const window = getCurrentWindow()
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,10 @@
|
||||||
import { setTimeout } from "worker-timers"
|
import { setTimeout } from "worker-timers"
|
||||||
|
|
||||||
import { showInExplorer } from "@/functions"
|
import { showInExplorer } from "@/functions"
|
||||||
import { appInfo, assistantVoice, translations, translate } from "@/stores"
|
import {
|
||||||
|
appInfo, assistantVoice, translations, translate,
|
||||||
|
switchDaemonLlm, reloadDaemonLlm,
|
||||||
|
} from "@/stores"
|
||||||
|
|
||||||
import HDivider from "@/components/elements/HDivider.svelte"
|
import HDivider from "@/components/elements/HDivider.svelte"
|
||||||
import Footer from "@/components/Footer.svelte"
|
import Footer from "@/components/Footer.svelte"
|
||||||
|
|
@ -113,6 +116,7 @@
|
||||||
if (target === "") {
|
if (target === "") {
|
||||||
try {
|
try {
|
||||||
await invoke("db_write", { key: "llm_backend", val: "" })
|
await invoke("db_write", { key: "llm_backend", val: "" })
|
||||||
|
reloadDaemonLlm() // tell daemon to re-read DB (now auto)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
backendSwapError = String(err)
|
backendSwapError = String(err)
|
||||||
}
|
}
|
||||||
|
|
@ -121,7 +125,11 @@
|
||||||
}
|
}
|
||||||
backendSwapBusy = true
|
backendSwapBusy = true
|
||||||
try {
|
try {
|
||||||
|
// 1. Swap on GUI process (persists to DB).
|
||||||
await invoke<string>("set_llm_backend", { name: target })
|
await invoke<string>("set_llm_backend", { name: target })
|
||||||
|
// 2. Tell daemon to swap too, so the running listener uses the
|
||||||
|
// new backend immediately. No-op if daemon not connected.
|
||||||
|
switchDaemonLlm(target as "groq" | "ollama")
|
||||||
await refreshActiveBackends()
|
await refreshActiveBackends()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
backendSwapError = String(err)
|
backendSwapError = String(err)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export {
|
||||||
lastRecognizedText,
|
lastRecognizedText,
|
||||||
lastExecutedCommand,
|
lastExecutedCommand,
|
||||||
lastError,
|
lastError,
|
||||||
|
daemonHealth,
|
||||||
connectIpc,
|
connectIpc,
|
||||||
enableIpc,
|
enableIpc,
|
||||||
disableIpc,
|
disableIpc,
|
||||||
|
|
@ -16,9 +17,14 @@ export {
|
||||||
sendIpcMessage,
|
sendIpcMessage,
|
||||||
sendTextCommand,
|
sendTextCommand,
|
||||||
stopJarvisApp,
|
stopJarvisApp,
|
||||||
reloadCommands
|
reloadCommands,
|
||||||
|
switchDaemonLlm,
|
||||||
|
reloadDaemonLlm,
|
||||||
|
queryDaemonHealth,
|
||||||
} from "./lib/ipc"
|
} from "./lib/ipc"
|
||||||
|
|
||||||
|
export type { DaemonHealth } from "./lib/ipc"
|
||||||
|
|
||||||
// re-export i18n
|
// re-export i18n
|
||||||
export {
|
export {
|
||||||
translations,
|
translations,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue