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:
parent
b243e67870
commit
d63399f4c9
6 changed files with 313 additions and 4 deletions
|
|
@ -30,7 +30,8 @@
|
|||
Code,
|
||||
Gear,
|
||||
QuestionMarkCircled,
|
||||
CrossCircled
|
||||
CrossCircled,
|
||||
ChatBubble,
|
||||
} from "radix-icons-svelte"
|
||||
|
||||
$: t = (key: string) => translate($translations, key)
|
||||
|
|
@ -85,6 +86,66 @@
|
|||
let apiKeyPicovoice = ""
|
||||
let apiKeyOpenai = ""
|
||||
|
||||
// ── AI backends (new tab) ─────────────────────────────────────────────
|
||||
interface ActiveBackends {
|
||||
tts: string
|
||||
llm: string
|
||||
llm_model: string | null
|
||||
profile: string
|
||||
}
|
||||
|
||||
let activeBackends: ActiveBackends | null = null
|
||||
let selectedLlmBackend = "" // "" = auto, "groq", "ollama"
|
||||
let selectedTtsBackend = "" // "" = auto, "sapi", "piper", "silero"
|
||||
let backendSwapBusy = false
|
||||
let backendSwapError = ""
|
||||
|
||||
async function refreshActiveBackends() {
|
||||
try {
|
||||
activeBackends = await invoke<ActiveBackends>("get_active_backends")
|
||||
} catch (err) {
|
||||
console.error("Failed to read active backends:", err)
|
||||
}
|
||||
}
|
||||
|
||||
async function applyLlmBackend(target: string) {
|
||||
backendSwapError = ""
|
||||
if (target === "") {
|
||||
try {
|
||||
await invoke("db_write", { key: "llm_backend", val: "" })
|
||||
} catch (err) {
|
||||
backendSwapError = String(err)
|
||||
}
|
||||
await refreshActiveBackends()
|
||||
return
|
||||
}
|
||||
backendSwapBusy = true
|
||||
try {
|
||||
await invoke<string>("set_llm_backend", { name: target })
|
||||
await refreshActiveBackends()
|
||||
} catch (err) {
|
||||
backendSwapError = String(err)
|
||||
} finally {
|
||||
backendSwapBusy = false
|
||||
}
|
||||
}
|
||||
|
||||
async function applyTtsBackend(target: string) {
|
||||
try {
|
||||
await invoke<string>("set_tts_backend", { name: target })
|
||||
} catch (err) {
|
||||
backendSwapError = String(err)
|
||||
}
|
||||
}
|
||||
|
||||
async function resetLlmContext() {
|
||||
try {
|
||||
await invoke("llm_reset_context")
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// subscribe to stores
|
||||
assistantVoice.subscribe(value => {
|
||||
voiceVal = value
|
||||
|
|
@ -215,6 +276,19 @@
|
|||
gainNormalizerEnabled = gainNormalizer === "true"
|
||||
apiKeyPicovoice = pico
|
||||
apiKeyOpenai = openai
|
||||
|
||||
// load AI backend prefs
|
||||
try {
|
||||
const [llm_pref, tts_pref] = await Promise.all([
|
||||
invoke<string>("db_read", { key: "llm_backend" }),
|
||||
invoke<string>("db_read", { key: "tts_backend" }),
|
||||
])
|
||||
selectedLlmBackend = llm_pref ?? ""
|
||||
selectedTtsBackend = tts_pref ?? ""
|
||||
} catch (err) {
|
||||
console.error("Failed to load backend prefs:", err)
|
||||
}
|
||||
await refreshActiveBackends()
|
||||
} catch (err) {
|
||||
console.error("failed to load settings:", err)
|
||||
}
|
||||
|
|
@ -307,6 +381,102 @@
|
|||
/>
|
||||
</Tabs.Tab>
|
||||
|
||||
<Tabs.Tab label={t('settings-ai-backends') || 'AI Backends'} icon={ChatBubble}>
|
||||
<Space h="sm" />
|
||||
|
||||
{#if activeBackends}
|
||||
<Alert title={t('settings-ai-active') || 'Активный движок'} color="cyan" variant="outline">
|
||||
<Text size="sm" color="gray">
|
||||
LLM: <strong>{activeBackends.llm}</strong>
|
||||
{#if activeBackends.llm_model}({activeBackends.llm_model}){/if}
|
||||
· TTS: <strong>{activeBackends.tts}</strong>
|
||||
· {t('settings-profile') || 'Profile'}: <strong>{activeBackends.profile}</strong>
|
||||
</Text>
|
||||
</Alert>
|
||||
<Space h="md" />
|
||||
{/if}
|
||||
|
||||
<NativeSelect
|
||||
data={[
|
||||
{ label: t('settings-ai-auto') || 'Авто (по env / GROQ_TOKEN)', value: "" },
|
||||
{ label: 'Groq (cloud, fast, free tier)', value: "groq" },
|
||||
{ label: 'Ollama (local, offline, private)', value: "ollama" },
|
||||
]}
|
||||
label={t('settings-llm-backend') || 'LLM движок'}
|
||||
description={t('settings-llm-backend-desc') || 'Где исполнять LLM-запросы. Hot-swap, без рестарта.'}
|
||||
variant="filled"
|
||||
bind:value={selectedLlmBackend}
|
||||
on:change={() => applyLlmBackend(selectedLlmBackend)}
|
||||
/>
|
||||
|
||||
<Space h="md" />
|
||||
|
||||
<NativeSelect
|
||||
data={[
|
||||
{ label: t('settings-ai-auto') || 'Авто (по env / Piper detect)', value: "" },
|
||||
{ label: 'SAPI (Windows, robotic)', value: "sapi" },
|
||||
{ label: 'Piper (neural, recommended)', value: "piper" },
|
||||
{ label: 'Silero (PyTorch helper)', value: "silero" },
|
||||
]}
|
||||
label={t('settings-tts-backend') || 'TTS движок'}
|
||||
description={t('settings-tts-backend-desc') || 'Применится при следующем запуске jarvis-app.'}
|
||||
variant="filled"
|
||||
bind:value={selectedTtsBackend}
|
||||
on:change={() => applyTtsBackend(selectedTtsBackend)}
|
||||
/>
|
||||
|
||||
<Space h="md" />
|
||||
|
||||
{#if backendSwapBusy}
|
||||
<Text size="xs" color="gray">
|
||||
{t('settings-ai-applying') || 'Применяю...'}
|
||||
</Text>
|
||||
{/if}
|
||||
|
||||
{#if backendSwapError}
|
||||
<Notification
|
||||
title={t('settings-ai-error') || 'Ошибка переключения'}
|
||||
icon={CrossCircled}
|
||||
color="red"
|
||||
withCloseButton={true}
|
||||
on:close={() => { backendSwapError = "" }}
|
||||
>
|
||||
{backendSwapError}
|
||||
</Notification>
|
||||
{/if}
|
||||
|
||||
<Space h="md" />
|
||||
|
||||
<InputWrapper label={t('settings-llm-context') || 'Контекст разговора'}>
|
||||
<Text size="sm" color="gray">
|
||||
{t('settings-llm-context-desc') || 'LLM помнит предыдущие реплики. Сбросить, если ответы становятся странными.'}
|
||||
</Text>
|
||||
<Space h="xs" />
|
||||
<Button
|
||||
color="gray"
|
||||
radius="md"
|
||||
size="xs"
|
||||
uppercase
|
||||
on:click={resetLlmContext}
|
||||
>
|
||||
{t('settings-llm-reset') || 'Сбросить контекст'}
|
||||
</Button>
|
||||
</InputWrapper>
|
||||
|
||||
<Space h="xl" />
|
||||
|
||||
<Alert title={t('settings-ai-tips') || 'Подсказки'} color="gray" variant="outline">
|
||||
<Text size="sm" color="gray">
|
||||
• <strong>Ollama</strong>: установи с <a href="https://ollama.com" target="_blank">ollama.com</a>,
|
||||
выполни <code>ollama pull qwen2.5:3b</code>.<br />
|
||||
• <strong>Piper</strong>: запусти <code>pwsh tools/piper/install.ps1</code> чтобы скачать голос.<br />
|
||||
• <strong>Голосом</strong>: «переключись на локальный» / «переключись на облако»,
|
||||
«какой у тебя мозг» — спрашивает статус.<br />
|
||||
• <strong>Сброс/повтор</strong>: «сбрось контекст» / «повтори последнее».
|
||||
</Text>
|
||||
</Alert>
|
||||
</Tabs.Tab>
|
||||
|
||||
<Tabs.Tab label={t('settings-neural-networks')} icon={Cube}>
|
||||
<Space h="sm" />
|
||||
<NativeSelect
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue