2023-06-11 19:18:58 +05:00
|
|
|
|
<script lang="ts">
|
2026-01-04 22:50:34 +05:00
|
|
|
|
import { onMount } from "svelte"
|
|
|
|
|
|
import { invoke } from "@tauri-apps/api/core"
|
|
|
|
|
|
import { goto } from "@roxi/routify"
|
|
|
|
|
|
import { setTimeout } from "worker-timers"
|
|
|
|
|
|
|
2026-01-06 23:32:58 +05:00
|
|
|
|
import { showInExplorer } from "@/functions"
|
2026-01-07 05:04:04 +05:00
|
|
|
|
import { appInfo, assistantVoice, translations, translate } from "@/stores"
|
2026-01-04 22:50:34 +05:00
|
|
|
|
|
|
|
|
|
|
import HDivider from "@/components/elements/HDivider.svelte"
|
|
|
|
|
|
import Footer from "@/components/Footer.svelte"
|
|
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
|
Notification,
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Text,
|
|
|
|
|
|
Tabs,
|
|
|
|
|
|
Space,
|
|
|
|
|
|
Alert,
|
|
|
|
|
|
Input,
|
|
|
|
|
|
InputWrapper,
|
2026-01-06 23:32:58 +05:00
|
|
|
|
NativeSelect,
|
|
|
|
|
|
Switch
|
2026-01-04 22:50:34 +05:00
|
|
|
|
} from "@svelteuidev/core"
|
|
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
|
Check,
|
|
|
|
|
|
Mix,
|
|
|
|
|
|
Cube,
|
|
|
|
|
|
Code,
|
|
|
|
|
|
Gear,
|
|
|
|
|
|
QuestionMarkCircled,
|
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.
2026-05-15 17:06:46 +03:00
|
|
|
|
CrossCircled,
|
|
|
|
|
|
ChatBubble,
|
2026-01-04 22:50:34 +05:00
|
|
|
|
} from "radix-icons-svelte"
|
|
|
|
|
|
|
2026-01-07 05:04:04 +05:00
|
|
|
|
$: t = (key: string) => translate($translations, key)
|
|
|
|
|
|
|
2026-01-07 23:29:46 +05:00
|
|
|
|
interface VoiceMeta {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
name: string
|
|
|
|
|
|
author: string
|
|
|
|
|
|
languages: string[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface VoiceConfig {
|
|
|
|
|
|
voice: VoiceMeta
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let availableVoices: VoiceMeta[] = []
|
|
|
|
|
|
|
|
|
|
|
|
async function selectVoice(voiceId: string) {
|
|
|
|
|
|
voiceVal = voiceId
|
|
|
|
|
|
|
|
|
|
|
|
// play preview sound
|
|
|
|
|
|
try {
|
|
|
|
|
|
await invoke("preview_voice", { voiceId })
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error("Failed to preview voice:", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 22:50:34 +05:00
|
|
|
|
// ### STATE
|
|
|
|
|
|
interface MicrophoneOption {
|
|
|
|
|
|
label: string
|
|
|
|
|
|
value: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let availableMicrophones: MicrophoneOption[] = []
|
2026-01-05 03:38:04 +05:00
|
|
|
|
let availableVoskModels: { label: string; value: string }[] = []
|
2026-02-11 07:21:50 +05:00
|
|
|
|
let availableGlinerModels: { label: string; value: string }[] = []
|
2026-01-04 22:50:34 +05:00
|
|
|
|
let settingsSaved = false
|
|
|
|
|
|
let saveButtonDisabled = false
|
|
|
|
|
|
|
2026-01-06 23:32:58 +05:00
|
|
|
|
// form values (state vars)
|
2026-01-04 22:50:34 +05:00
|
|
|
|
let voiceVal = ""
|
|
|
|
|
|
let selectedMicrophone = ""
|
|
|
|
|
|
let selectedWakeWordEngine = ""
|
2026-01-05 01:22:45 +05:00
|
|
|
|
let selectedIntentRecognitionEngine = ""
|
2026-02-11 07:21:50 +05:00
|
|
|
|
let selectedSlotExtractionEngine = ""
|
|
|
|
|
|
let selectedGlinerModel = ""
|
2026-01-05 03:38:04 +05:00
|
|
|
|
let selectedVoskModel = ""
|
2026-01-06 23:32:58 +05:00
|
|
|
|
let selectedNoiseSuppression = ""
|
|
|
|
|
|
let selectedVad = ""
|
|
|
|
|
|
let gainNormalizerEnabled = false
|
2026-01-04 22:50:34 +05:00
|
|
|
|
let apiKeyPicovoice = ""
|
|
|
|
|
|
let apiKeyOpenai = ""
|
|
|
|
|
|
|
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.
2026-05-15 17:06:46 +03:00
|
|
|
|
// ── 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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 22:50:34 +05:00
|
|
|
|
// subscribe to stores
|
|
|
|
|
|
assistantVoice.subscribe(value => {
|
|
|
|
|
|
voiceVal = value
|
|
|
|
|
|
})
|
|
|
|
|
|
|
chore(gui): rebrand — strip TG/Boosty/Patreon/feedback-bot, keep CC BY-NC-SA attribution
The previous feature/gui-rebrand-polish work only updated the appInfo store
keys; Footer.svelte, settings, and the commands page were still rendering
upstream's Telegram channel, Boosty/Patreon support links, feedback bot,
and the original author byline. This finishes the job.
Rust side (applies after `cargo build` — currently a no-op until MSVC Build
Tools are reinstalled):
- Cargo.toml: authors = ["Bossiara13"], repository = J.A.R.V.I.S-rust fork.
Original attribution stays in LICENSE.txt per CC BY-NC-SA 4.0; the UI
no longer renders an author byline.
- config.rs: dropped TG_OFFICIAL_LINK / FEEDBACK_LINK / SUPPORT_BOOSTY_LINK
/ SUPPORT_PATREON_LINK. Added UPSTREAM_REPOSITORY_LINK (Priler/jarvis for
the licence attribution), ISSUES_LINK (this fork's GH issues), and
PYTHON_FORK_LINK.
- tauri_commands/etc.rs: dropped get_tg_official_link/get_boosty_link/
get_patreon_link/get_feedback_link. Added get_upstream_link, get_issues_link,
get_python_fork_link. Tidied up the Option<&str> -> String boilerplate
with unwrap_or.
- main.rs: invoke_handler updated to match.
Frontend side (visible only after Rust rebuild — Tauri bundles the dist into
the exe):
- stores.ts: appInfo shape reduced to repositoryLink / upstreamLink /
issuesLink / pythonForkLink / logFilePath. loadAppInfo() uses fetchOr()
with hardcoded fallbacks so the UI still populates correctly when run
against an un-rebuilt binary; fetchRepoOr() additionally overrides
CARGO_PKG_REPOSITORY if it still resolves to upstream's Priler/jarvis.
- Footer.svelte: rewritten — © year + "J.A.R.V.I.S." + repo link + issues
link + small "fork of Priler/jarvis · CC BY-NC-SA 4.0" attribution line.
No author byline (it's in LICENSE.txt).
- routes/settings/index.svelte: feedback link now points to GitHub Issues.
- routes/commands/index.svelte: TG-channel reference replaced with a link
to the Python fork (where the command builder lives) plus a pointer to
resources/commands/ in this repo. Bruh GIF removed.
- locales/{ru,en,ua}.ftl: removed footer-author / footer-telegram /
footer-support, added footer-issues / footer-fork-of, rewrote
commands-wip-* strings, retargeted settings-beta-bot to "GitHub Issues".
Bundle identifier (com.priler.jarvis) kept intact to avoid moving the app
data directory.
2026-05-15 01:09:14 +03:00
|
|
|
|
let issuesLink = ""
|
2026-01-04 22:50:34 +05:00
|
|
|
|
let logFilePath = ""
|
|
|
|
|
|
appInfo.subscribe(info => {
|
chore(gui): rebrand — strip TG/Boosty/Patreon/feedback-bot, keep CC BY-NC-SA attribution
The previous feature/gui-rebrand-polish work only updated the appInfo store
keys; Footer.svelte, settings, and the commands page were still rendering
upstream's Telegram channel, Boosty/Patreon support links, feedback bot,
and the original author byline. This finishes the job.
Rust side (applies after `cargo build` — currently a no-op until MSVC Build
Tools are reinstalled):
- Cargo.toml: authors = ["Bossiara13"], repository = J.A.R.V.I.S-rust fork.
Original attribution stays in LICENSE.txt per CC BY-NC-SA 4.0; the UI
no longer renders an author byline.
- config.rs: dropped TG_OFFICIAL_LINK / FEEDBACK_LINK / SUPPORT_BOOSTY_LINK
/ SUPPORT_PATREON_LINK. Added UPSTREAM_REPOSITORY_LINK (Priler/jarvis for
the licence attribution), ISSUES_LINK (this fork's GH issues), and
PYTHON_FORK_LINK.
- tauri_commands/etc.rs: dropped get_tg_official_link/get_boosty_link/
get_patreon_link/get_feedback_link. Added get_upstream_link, get_issues_link,
get_python_fork_link. Tidied up the Option<&str> -> String boilerplate
with unwrap_or.
- main.rs: invoke_handler updated to match.
Frontend side (visible only after Rust rebuild — Tauri bundles the dist into
the exe):
- stores.ts: appInfo shape reduced to repositoryLink / upstreamLink /
issuesLink / pythonForkLink / logFilePath. loadAppInfo() uses fetchOr()
with hardcoded fallbacks so the UI still populates correctly when run
against an un-rebuilt binary; fetchRepoOr() additionally overrides
CARGO_PKG_REPOSITORY if it still resolves to upstream's Priler/jarvis.
- Footer.svelte: rewritten — © year + "J.A.R.V.I.S." + repo link + issues
link + small "fork of Priler/jarvis · CC BY-NC-SA 4.0" attribution line.
No author byline (it's in LICENSE.txt).
- routes/settings/index.svelte: feedback link now points to GitHub Issues.
- routes/commands/index.svelte: TG-channel reference replaced with a link
to the Python fork (where the command builder lives) plus a pointer to
resources/commands/ in this repo. Bruh GIF removed.
- locales/{ru,en,ua}.ftl: removed footer-author / footer-telegram /
footer-support, added footer-issues / footer-fork-of, rewrote
commands-wip-* strings, retargeted settings-beta-bot to "GitHub Issues".
Bundle identifier (com.priler.jarvis) kept intact to avoid moving the app
data directory.
2026-05-15 01:09:14 +03:00
|
|
|
|
issuesLink = info.issuesLink
|
2026-01-04 22:50:34 +05:00
|
|
|
|
logFilePath = info.logFilePath
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// ### FUNCTIONS
|
|
|
|
|
|
async function saveSettings() {
|
|
|
|
|
|
saveButtonDisabled = true
|
|
|
|
|
|
settingsSaved = false
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await Promise.all([
|
|
|
|
|
|
invoke("db_write", { key: "assistant_voice", val: voiceVal }),
|
|
|
|
|
|
invoke("db_write", { key: "selected_microphone", val: selectedMicrophone }),
|
|
|
|
|
|
invoke("db_write", { key: "selected_wake_word_engine", val: selectedWakeWordEngine }),
|
2026-01-05 01:22:45 +05:00
|
|
|
|
invoke("db_write", { key: "selected_intent_recognition_engine", val: selectedIntentRecognitionEngine }),
|
2026-02-11 07:21:50 +05:00
|
|
|
|
invoke("db_write", { key: "selected_slot_extraction_engine", val: selectedSlotExtractionEngine }),
|
|
|
|
|
|
invoke("db_write", { key: "selected_gliner_model", val: selectedGlinerModel }),
|
2026-01-05 03:38:04 +05:00
|
|
|
|
invoke("db_write", { key: "selected_vosk_model", val: selectedVoskModel }),
|
2026-01-06 23:32:58 +05:00
|
|
|
|
|
|
|
|
|
|
invoke("db_write", { key: "noise_suppression", val: selectedNoiseSuppression }),
|
|
|
|
|
|
invoke("db_write", { key: "vad", val: selectedVad }),
|
|
|
|
|
|
invoke("db_write", { key: "gain_normalizer", val: gainNormalizerEnabled.toString() }),
|
|
|
|
|
|
|
2026-01-04 22:50:34 +05:00
|
|
|
|
invoke("db_write", { key: "api_key__picovoice", val: apiKeyPicovoice }),
|
|
|
|
|
|
invoke("db_write", { key: "api_key__openai", val: apiKeyOpenai })
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
// update shared store
|
|
|
|
|
|
assistantVoice.set(voiceVal)
|
|
|
|
|
|
settingsSaved = true
|
|
|
|
|
|
|
|
|
|
|
|
// hide alert after 5 seconds
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
settingsSaved = false
|
|
|
|
|
|
}, 5000)
|
|
|
|
|
|
|
|
|
|
|
|
// restart listening with new settings
|
2026-01-06 23:32:58 +05:00
|
|
|
|
// stopListening(() => startListening())
|
2026-01-04 22:50:34 +05:00
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error("failed to save settings:", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
saveButtonDisabled = false
|
|
|
|
|
|
}, 1000)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ### INIT
|
|
|
|
|
|
onMount(async () => {
|
2026-01-07 23:29:46 +05:00
|
|
|
|
// load voices
|
|
|
|
|
|
try {
|
|
|
|
|
|
const voices = await invoke<VoiceConfig[]>("list_voices")
|
|
|
|
|
|
availableVoices = voices.map(v => v.voice)
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error("Failed to load voices:", err)
|
|
|
|
|
|
availableVoices = []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 22:50:34 +05:00
|
|
|
|
try {
|
|
|
|
|
|
// load microphones
|
|
|
|
|
|
const mics = await invoke<string[]>("pv_get_audio_devices")
|
2026-01-06 23:32:58 +05:00
|
|
|
|
availableMicrophones = [
|
2026-01-07 23:29:46 +05:00
|
|
|
|
{ label: t('settings-mic-default'), value: "-1" }, // system default
|
2026-01-06 23:32:58 +05:00
|
|
|
|
...mics.map((name, idx) => ({
|
|
|
|
|
|
label: name,
|
|
|
|
|
|
value: String(idx)
|
|
|
|
|
|
}))
|
|
|
|
|
|
]
|
2026-01-04 22:50:34 +05:00
|
|
|
|
|
2026-01-05 03:38:04 +05:00
|
|
|
|
// load vosk models
|
2026-01-13 02:21:59 +05:00
|
|
|
|
const languageNames: Record<string, string> = {
|
|
|
|
|
|
us: 'English',
|
|
|
|
|
|
ru: 'Русский',
|
|
|
|
|
|
uk: 'Українська',
|
|
|
|
|
|
de: 'German',
|
|
|
|
|
|
fr: 'French',
|
|
|
|
|
|
es: 'Spanish',
|
|
|
|
|
|
// ..
|
|
|
|
|
|
};
|
2026-01-05 03:38:04 +05:00
|
|
|
|
const voskModels = await invoke<{ name: string; language: string; size: string }[]>("list_vosk_models")
|
|
|
|
|
|
availableVoskModels = voskModels.map(m => ({
|
2026-01-13 02:21:59 +05:00
|
|
|
|
label: `${m.name} (${languageNames[m.language] ?? m.language}, ${m.size})`,
|
2026-01-05 03:38:04 +05:00
|
|
|
|
value: m.name
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
2026-02-11 07:21:50 +05:00
|
|
|
|
// load gliner models
|
|
|
|
|
|
const glinerModels = await invoke<{ display_name: string; value: string }[]>("list_gliner_models")
|
|
|
|
|
|
availableGlinerModels = glinerModels.map(m => ({
|
|
|
|
|
|
label: m.display_name,
|
|
|
|
|
|
value: m.value,
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
2026-01-04 22:50:34 +05:00
|
|
|
|
// load settings from db
|
2026-02-11 07:21:50 +05:00
|
|
|
|
const [mic, wakeWord, intentReco, slotEngine, glinerModel, voskModel,
|
2026-01-06 23:32:58 +05:00
|
|
|
|
noiseSuppression, vad, gainNormalizer,
|
|
|
|
|
|
pico, openai] = await Promise.all([
|
2026-01-04 22:50:34 +05:00
|
|
|
|
invoke<string>("db_read", { key: "selected_microphone" }),
|
|
|
|
|
|
invoke<string>("db_read", { key: "selected_wake_word_engine" }),
|
2026-01-05 01:22:45 +05:00
|
|
|
|
invoke<string>("db_read", { key: "selected_intent_recognition_engine" }),
|
2026-02-11 07:21:50 +05:00
|
|
|
|
invoke<string>("db_read", { key: "selected_slot_extraction_engine" }),
|
|
|
|
|
|
invoke<string>("db_read", { key: "selected_gliner_model" }),
|
2026-01-05 03:38:04 +05:00
|
|
|
|
invoke<string>("db_read", { key: "selected_vosk_model" }),
|
2026-01-06 23:32:58 +05:00
|
|
|
|
|
|
|
|
|
|
invoke<string>("db_read", { key: "noise_suppression" }),
|
|
|
|
|
|
invoke<string>("db_read", { key: "vad" }),
|
|
|
|
|
|
invoke<string>("db_read", { key: "gain_normalizer" }),
|
|
|
|
|
|
|
2026-01-04 22:50:34 +05:00
|
|
|
|
invoke<string>("db_read", { key: "api_key__picovoice" }),
|
|
|
|
|
|
invoke<string>("db_read", { key: "api_key__openai" })
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
selectedMicrophone = mic
|
|
|
|
|
|
selectedWakeWordEngine = wakeWord
|
2026-01-05 01:22:45 +05:00
|
|
|
|
selectedIntentRecognitionEngine = intentReco
|
2026-02-11 07:21:50 +05:00
|
|
|
|
selectedSlotExtractionEngine = slotEngine
|
2026-01-05 03:38:04 +05:00
|
|
|
|
selectedVoskModel = voskModel
|
2026-02-11 07:21:50 +05:00
|
|
|
|
selectedGlinerModel = glinerModel
|
2026-01-06 23:32:58 +05:00
|
|
|
|
selectedNoiseSuppression = noiseSuppression
|
|
|
|
|
|
selectedVad = vad
|
|
|
|
|
|
gainNormalizerEnabled = gainNormalizer === "true"
|
2026-01-04 22:50:34 +05:00
|
|
|
|
apiKeyPicovoice = pico
|
|
|
|
|
|
apiKeyOpenai = openai
|
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.
2026-05-15 17:06:46 +03:00
|
|
|
|
|
|
|
|
|
|
// 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()
|
2026-01-04 22:50:34 +05:00
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error("failed to load settings:", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2023-06-11 19:18:58 +05:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<Space h="xl" />
|
|
|
|
|
|
|
2026-01-04 22:50:34 +05:00
|
|
|
|
<Notification
|
2026-01-07 05:04:04 +05:00
|
|
|
|
title={t('settings-beta-title')}
|
2026-01-04 22:50:34 +05:00
|
|
|
|
icon={QuestionMarkCircled}
|
|
|
|
|
|
color="blue"
|
|
|
|
|
|
withCloseButton={false}
|
|
|
|
|
|
>
|
2026-01-07 05:04:04 +05:00
|
|
|
|
{t('settings-beta-desc')}<br />
|
chore(gui): rebrand — strip TG/Boosty/Patreon/feedback-bot, keep CC BY-NC-SA attribution
The previous feature/gui-rebrand-polish work only updated the appInfo store
keys; Footer.svelte, settings, and the commands page were still rendering
upstream's Telegram channel, Boosty/Patreon support links, feedback bot,
and the original author byline. This finishes the job.
Rust side (applies after `cargo build` — currently a no-op until MSVC Build
Tools are reinstalled):
- Cargo.toml: authors = ["Bossiara13"], repository = J.A.R.V.I.S-rust fork.
Original attribution stays in LICENSE.txt per CC BY-NC-SA 4.0; the UI
no longer renders an author byline.
- config.rs: dropped TG_OFFICIAL_LINK / FEEDBACK_LINK / SUPPORT_BOOSTY_LINK
/ SUPPORT_PATREON_LINK. Added UPSTREAM_REPOSITORY_LINK (Priler/jarvis for
the licence attribution), ISSUES_LINK (this fork's GH issues), and
PYTHON_FORK_LINK.
- tauri_commands/etc.rs: dropped get_tg_official_link/get_boosty_link/
get_patreon_link/get_feedback_link. Added get_upstream_link, get_issues_link,
get_python_fork_link. Tidied up the Option<&str> -> String boilerplate
with unwrap_or.
- main.rs: invoke_handler updated to match.
Frontend side (visible only after Rust rebuild — Tauri bundles the dist into
the exe):
- stores.ts: appInfo shape reduced to repositoryLink / upstreamLink /
issuesLink / pythonForkLink / logFilePath. loadAppInfo() uses fetchOr()
with hardcoded fallbacks so the UI still populates correctly when run
against an un-rebuilt binary; fetchRepoOr() additionally overrides
CARGO_PKG_REPOSITORY if it still resolves to upstream's Priler/jarvis.
- Footer.svelte: rewritten — © year + "J.A.R.V.I.S." + repo link + issues
link + small "fork of Priler/jarvis · CC BY-NC-SA 4.0" attribution line.
No author byline (it's in LICENSE.txt).
- routes/settings/index.svelte: feedback link now points to GitHub Issues.
- routes/commands/index.svelte: TG-channel reference replaced with a link
to the Python fork (where the command builder lives) plus a pointer to
resources/commands/ in this repo. Bruh GIF removed.
- locales/{ru,en,ua}.ftl: removed footer-author / footer-telegram /
footer-support, added footer-issues / footer-fork-of, rewrote
commands-wip-* strings, retargeted settings-beta-bot to "GitHub Issues".
Bundle identifier (com.priler.jarvis) kept intact to avoid moving the app
data directory.
2026-05-15 01:09:14 +03:00
|
|
|
|
{t('settings-beta-feedback')} <a href={issuesLink} target="_blank" rel="noopener noreferrer">{t('settings-beta-bot')}</a>.
|
2026-01-04 22:50:34 +05:00
|
|
|
|
<Space h="sm" />
|
|
|
|
|
|
<Button
|
|
|
|
|
|
color="gray"
|
|
|
|
|
|
radius="md"
|
|
|
|
|
|
size="xs"
|
|
|
|
|
|
uppercase
|
|
|
|
|
|
on:click={() => showInExplorer(logFilePath)}
|
|
|
|
|
|
>
|
2026-01-07 05:04:04 +05:00
|
|
|
|
{t('settings-open-logs')}
|
2026-01-04 22:50:34 +05:00
|
|
|
|
</Button>
|
2023-06-11 19:18:58 +05:00
|
|
|
|
</Notification>
|
|
|
|
|
|
|
|
|
|
|
|
<Space h="xl" />
|
|
|
|
|
|
|
2026-01-04 22:50:34 +05:00
|
|
|
|
{#if settingsSaved}
|
|
|
|
|
|
<Notification
|
2026-01-07 05:04:04 +05:00
|
|
|
|
title={t('notification-saved')}
|
2026-01-04 22:50:34 +05:00
|
|
|
|
icon={Check}
|
|
|
|
|
|
color="teal"
|
|
|
|
|
|
on:close={() => { settingsSaved = false }}
|
2023-06-11 19:18:58 +05:00
|
|
|
|
/>
|
|
|
|
|
|
<Space h="xl" />
|
2026-01-04 22:50:34 +05:00
|
|
|
|
{/if}
|
2023-06-11 19:18:58 +05:00
|
|
|
|
|
2026-01-04 22:50:34 +05:00
|
|
|
|
<Tabs class="form" color="#8AC832" position="left">
|
2026-01-07 05:04:04 +05:00
|
|
|
|
<Tabs.Tab label={t('settings-general')} icon={Gear}>
|
2026-01-04 22:50:34 +05:00
|
|
|
|
<Space h="sm" />
|
2026-01-07 23:29:46 +05:00
|
|
|
|
<div class="voice-select">
|
|
|
|
|
|
<label>{t('settings-voice')}</label>
|
|
|
|
|
|
<p class="description">{t('settings-voice-desc')}</p>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="voice-options">
|
|
|
|
|
|
{#each availableVoices as voice}
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="voice-option"
|
|
|
|
|
|
class:selected={voiceVal === voice.id}
|
|
|
|
|
|
on:click={() => selectVoice(voice.id)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="voice-info">
|
|
|
|
|
|
<span class="voice-name">{voice.name}</span>
|
|
|
|
|
|
{#if voice.author}
|
|
|
|
|
|
<span class="voice-author">by {voice.author}</span>
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="voice-languages">
|
|
|
|
|
|
{#each voice.languages as lang}
|
|
|
|
|
|
<img
|
|
|
|
|
|
src="/media/flags/{lang.toUpperCase()}.png"
|
|
|
|
|
|
alt={lang}
|
|
|
|
|
|
width="20"
|
|
|
|
|
|
title={lang}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
|
|
|
|
{#if availableVoices.length === 0}
|
|
|
|
|
|
<p class="no-voices">{t('settings-no-voices')}</p>
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-04 22:50:34 +05:00
|
|
|
|
</Tabs.Tab>
|
|
|
|
|
|
|
2026-01-07 05:04:04 +05:00
|
|
|
|
<Tabs.Tab label={t('settings-devices')} icon={Mix}>
|
2026-01-04 22:50:34 +05:00
|
|
|
|
<Space h="sm" />
|
|
|
|
|
|
<NativeSelect
|
|
|
|
|
|
data={availableMicrophones}
|
2026-01-07 05:04:04 +05:00
|
|
|
|
label={t('settings-microphone')}
|
|
|
|
|
|
description={t('settings-microphone-desc')}
|
2026-01-04 22:50:34 +05:00
|
|
|
|
variant="filled"
|
|
|
|
|
|
bind:value={selectedMicrophone}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Tabs.Tab>
|
|
|
|
|
|
|
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.
2026-05-15 17:06:46 +03:00
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-01-07 05:04:04 +05:00
|
|
|
|
<Tabs.Tab label={t('settings-neural-networks')} icon={Cube}>
|
2026-01-04 22:50:34 +05:00
|
|
|
|
<Space h="sm" />
|
|
|
|
|
|
<NativeSelect
|
|
|
|
|
|
data={[
|
|
|
|
|
|
{ label: "Rustpotter", value: "Rustpotter" },
|
2026-01-07 05:04:04 +05:00
|
|
|
|
{ label: "Vosk", value: "Vosk" },
|
|
|
|
|
|
{ label: "Picovoice Porcupine", value: "Picovoice" }
|
2026-01-04 22:50:34 +05:00
|
|
|
|
]}
|
2026-01-07 05:04:04 +05:00
|
|
|
|
label={t('settings-wake-word-engine')}
|
|
|
|
|
|
description={t('settings-wake-word-desc')}
|
2026-01-04 22:50:34 +05:00
|
|
|
|
variant="filled"
|
|
|
|
|
|
bind:value={selectedWakeWordEngine}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{#if selectedWakeWordEngine === "picovoice"}
|
|
|
|
|
|
<Space h="sm" />
|
2026-01-07 05:04:04 +05:00
|
|
|
|
<Alert title={t('settings-attention')} color="#868E96" variant="outline">
|
2026-01-04 22:50:34 +05:00
|
|
|
|
<Notification
|
2026-01-07 05:04:04 +05:00
|
|
|
|
title={t('settings-picovoice-warning')}
|
2026-01-04 22:50:34 +05:00
|
|
|
|
icon={CrossCircled}
|
|
|
|
|
|
color="orange"
|
|
|
|
|
|
withCloseButton={false}
|
|
|
|
|
|
>
|
2026-01-07 05:04:04 +05:00
|
|
|
|
{t('settings-picovoice-waiting')}
|
2026-01-04 22:50:34 +05:00
|
|
|
|
</Notification>
|
|
|
|
|
|
<Space h="sm" />
|
|
|
|
|
|
<Text size="sm" color="gray">
|
2026-01-07 05:04:04 +05:00
|
|
|
|
{t('settings-picovoice-key-desc')}
|
2026-01-04 22:50:34 +05:00
|
|
|
|
<a href="https://console.picovoice.ai/" target="_blank">Picovoice Console</a>.
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<Space h="sm" />
|
|
|
|
|
|
<Input
|
|
|
|
|
|
icon={Code}
|
2026-01-07 05:04:04 +05:00
|
|
|
|
placeholder={t('settings-picovoice-key')}
|
2026-01-04 22:50:34 +05:00
|
|
|
|
variant="filled"
|
|
|
|
|
|
autocomplete="off"
|
|
|
|
|
|
bind:value={apiKeyPicovoice}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Alert>
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
2026-01-05 03:38:04 +05:00
|
|
|
|
<Space h="xl" />
|
|
|
|
|
|
{#key availableVoskModels}
|
|
|
|
|
|
<NativeSelect
|
|
|
|
|
|
data={[
|
2026-01-07 05:04:04 +05:00
|
|
|
|
{ label: t('settings-auto-detect'), value: "" },
|
2026-01-05 03:38:04 +05:00
|
|
|
|
...availableVoskModels
|
|
|
|
|
|
]}
|
2026-01-07 05:04:04 +05:00
|
|
|
|
label={t('settings-vosk-model')}
|
|
|
|
|
|
description={t('settings-vosk-model-desc')}
|
2026-01-05 03:38:04 +05:00
|
|
|
|
variant="filled"
|
|
|
|
|
|
bind:value={selectedVoskModel}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/key}
|
|
|
|
|
|
|
|
|
|
|
|
{#if availableVoskModels.length === 0}
|
|
|
|
|
|
<Space h="sm" />
|
2026-01-07 05:04:04 +05:00
|
|
|
|
<Alert title={t('settings-models-not-found')} color="orange" variant="outline">
|
2026-01-05 03:38:04 +05:00
|
|
|
|
<Text size="sm" color="gray">
|
2026-01-07 05:04:04 +05:00
|
|
|
|
{t('settings-models-hint')}
|
2026-01-05 03:38:04 +05:00
|
|
|
|
</Text>
|
|
|
|
|
|
</Alert>
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
2026-01-04 22:50:34 +05:00
|
|
|
|
<Space h="xl" />
|
2026-01-05 01:22:45 +05:00
|
|
|
|
<NativeSelect
|
|
|
|
|
|
data={[
|
|
|
|
|
|
{ label: "Intent Classifier", value: "IntentClassifier" },
|
2026-02-08 07:16:03 +05:00
|
|
|
|
{ label: "Embedding Classifier", value: "EmbeddingClassifier" }
|
2026-01-05 01:22:45 +05:00
|
|
|
|
]}
|
2026-01-07 05:04:04 +05:00
|
|
|
|
label={t('settings-intent-engine')}
|
|
|
|
|
|
description={t('settings-intent-engine-desc')}
|
2026-01-05 01:22:45 +05:00
|
|
|
|
variant="filled"
|
|
|
|
|
|
bind:value={selectedIntentRecognitionEngine}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<Space h="xl" />
|
2026-02-11 07:21:50 +05:00
|
|
|
|
<NativeSelect
|
|
|
|
|
|
data={[
|
|
|
|
|
|
{ label: t('settings-disabled'), value: "None" },
|
|
|
|
|
|
{ label: "GLiNER (NER)", value: "GLiNER" }
|
|
|
|
|
|
]}
|
|
|
|
|
|
label={t('settings-slot-engine')}
|
|
|
|
|
|
description={t('settings-slot-engine-desc')}
|
|
|
|
|
|
variant="filled"
|
|
|
|
|
|
bind:value={selectedSlotExtractionEngine}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{#if selectedSlotExtractionEngine === "GLiNER"}
|
|
|
|
|
|
<Space h="sm" />
|
|
|
|
|
|
{#key availableGlinerModels}
|
|
|
|
|
|
<NativeSelect
|
|
|
|
|
|
data={[
|
|
|
|
|
|
{ label: t('settings-auto-detect'), value: "" },
|
|
|
|
|
|
...availableGlinerModels
|
|
|
|
|
|
]}
|
|
|
|
|
|
label={t('settings-gliner-model')}
|
|
|
|
|
|
description={t('settings-gliner-model-desc')}
|
|
|
|
|
|
variant="filled"
|
|
|
|
|
|
bind:value={selectedGlinerModel}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/key}
|
|
|
|
|
|
|
|
|
|
|
|
{#if availableGlinerModels.length === 0}
|
|
|
|
|
|
<Space h="sm" />
|
|
|
|
|
|
<Alert title={t('settings-models-not-found')} color="orange" variant="outline">
|
|
|
|
|
|
<Text size="sm" color="gray">
|
|
|
|
|
|
{t('settings-gliner-models-hint')}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</Alert>
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
{/if}
|
2026-01-06 23:32:58 +05:00
|
|
|
|
|
2026-02-11 07:21:50 +05:00
|
|
|
|
<Space h="xl" />
|
2026-01-06 23:32:58 +05:00
|
|
|
|
<NativeSelect
|
|
|
|
|
|
data={[
|
2026-01-07 05:04:04 +05:00
|
|
|
|
{ label: t('settings-disabled'), value: "None" },
|
2026-01-06 23:32:58 +05:00
|
|
|
|
{ label: "Nnnoiseless", value: "Nnnoiseless" }
|
|
|
|
|
|
]}
|
2026-01-07 05:04:04 +05:00
|
|
|
|
label={t('settings-noise-suppression')}
|
|
|
|
|
|
description={t('settings-noise-suppression-desc')}
|
2026-01-06 23:32:58 +05:00
|
|
|
|
variant="filled"
|
|
|
|
|
|
bind:value={selectedNoiseSuppression}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<Space h="md" />
|
|
|
|
|
|
|
|
|
|
|
|
<NativeSelect
|
|
|
|
|
|
data={[
|
2026-01-07 05:04:04 +05:00
|
|
|
|
{ label: t('settings-disabled'), value: "None" },
|
|
|
|
|
|
{ label: "Energy", value: "Energy" },
|
|
|
|
|
|
{ label: "Nnnoiseless", value: "Nnnoiseless" }
|
2026-01-06 23:32:58 +05:00
|
|
|
|
]}
|
2026-01-07 05:04:04 +05:00
|
|
|
|
label={t('settings-vad')}
|
|
|
|
|
|
description={t('settings-vad-desc')}
|
2026-01-06 23:32:58 +05:00
|
|
|
|
variant="filled"
|
|
|
|
|
|
bind:value={selectedVad}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<Space h="md" />
|
|
|
|
|
|
|
2026-01-07 05:04:04 +05:00
|
|
|
|
<InputWrapper label={t('settings-gain-normalizer')}>
|
2026-01-06 23:32:58 +05:00
|
|
|
|
<Text size="sm" color="gray">
|
2026-01-07 05:04:04 +05:00
|
|
|
|
{t('settings-gain-normalizer-desc')}
|
2026-01-06 23:32:58 +05:00
|
|
|
|
</Text>
|
|
|
|
|
|
<Space h="xs" />
|
|
|
|
|
|
<Switch
|
2026-01-07 05:04:04 +05:00
|
|
|
|
label={gainNormalizerEnabled ? t('settings-enabled') : t('settings-disabled')}
|
2026-01-06 23:32:58 +05:00
|
|
|
|
bind:checked={gainNormalizerEnabled}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</InputWrapper>
|
|
|
|
|
|
|
|
|
|
|
|
<Space h="xl" />
|
2026-01-04 22:50:34 +05:00
|
|
|
|
|
2026-01-07 05:04:04 +05:00
|
|
|
|
<InputWrapper label={t('settings-openai-key')}>
|
2026-01-04 22:50:34 +05:00
|
|
|
|
<Text size="sm" color="gray">
|
2026-01-07 05:04:04 +05:00
|
|
|
|
{t('settings-openai-not-supported')}
|
2026-01-04 22:50:34 +05:00
|
|
|
|
</Text>
|
|
|
|
|
|
<Space h="sm" />
|
|
|
|
|
|
<Input
|
|
|
|
|
|
icon={Code}
|
2026-01-07 05:04:04 +05:00
|
|
|
|
placeholder={t('settings-openai-key')}
|
2026-01-04 22:50:34 +05:00
|
|
|
|
variant="filled"
|
|
|
|
|
|
autocomplete="off"
|
|
|
|
|
|
bind:value={apiKeyOpenai}
|
|
|
|
|
|
disabled
|
|
|
|
|
|
/>
|
|
|
|
|
|
</InputWrapper>
|
|
|
|
|
|
</Tabs.Tab>
|
2023-06-11 19:18:58 +05:00
|
|
|
|
</Tabs>
|
|
|
|
|
|
|
|
|
|
|
|
<Space h="xl" />
|
|
|
|
|
|
|
2026-01-04 22:50:34 +05:00
|
|
|
|
<Button
|
|
|
|
|
|
color="lime"
|
|
|
|
|
|
radius="md"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
uppercase
|
|
|
|
|
|
ripple
|
|
|
|
|
|
fullSize
|
|
|
|
|
|
on:click={saveSettings}
|
|
|
|
|
|
disabled={saveButtonDisabled}
|
|
|
|
|
|
>
|
2026-01-07 05:04:04 +05:00
|
|
|
|
{t('settings-save')}
|
2023-06-11 19:18:58 +05:00
|
|
|
|
</Button>
|
2026-01-04 22:50:34 +05:00
|
|
|
|
|
2023-06-11 19:18:58 +05:00
|
|
|
|
<Space h="sm" />
|
2026-01-04 22:50:34 +05:00
|
|
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
|
color="gray"
|
|
|
|
|
|
radius="md"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
uppercase
|
|
|
|
|
|
fullSize
|
|
|
|
|
|
on:click={() => $goto("/")}
|
|
|
|
|
|
>
|
2026-01-07 05:04:04 +05:00
|
|
|
|
{t('settings-back')}
|
2023-06-11 19:18:58 +05:00
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
|
|
<HDivider />
|
2026-01-04 22:50:34 +05:00
|
|
|
|
<Footer />
|
2026-01-07 23:29:46 +05:00
|
|
|
|
|
|
|
|
|
|
<style lang="scss">
|
|
|
|
|
|
.voice-select {
|
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
|
|
|
|
|
|
|
label {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin-bottom: 0.25rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.description {
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: rgba(255,255,255,0.5);
|
|
|
|
|
|
margin: 0 0 0.75rem;
|
|
|
|
|
|
white-space: pre-line;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$voice-item-height: 70px;
|
|
|
|
|
|
$voice-item-gap: 0.5rem;
|
|
|
|
|
|
$voice-max-visible: 3;
|
|
|
|
|
|
|
|
|
|
|
|
.voice-options {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: $voice-item-gap;
|
|
|
|
|
|
max-height: $voice-item-height * $voice-max-visible;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
|
|
|
|
|
|
&::-webkit-scrollbar {
|
|
|
|
|
|
width: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&::-webkit-scrollbar-track {
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.05);
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&::-webkit-scrollbar-thumb {
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.2);
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.3);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.voice-option {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 0.75rem 1rem;
|
|
|
|
|
|
background: rgba(30, 40, 45, 0.8);
|
|
|
|
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background: rgba(40, 55, 60, 0.9);
|
|
|
|
|
|
border-color: rgba(255,255,255,0.2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.selected {
|
|
|
|
|
|
background: rgba(82, 254, 254, 0.1);
|
|
|
|
|
|
border-color: rgba(82, 254, 254, 0.4);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.voice-info {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
gap: 0.15rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.voice-name {
|
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.voice-author {
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
color: rgba(255,255,255,0.4);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.voice-languages {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
|
|
|
|
|
|
|
img {
|
|
|
|
|
|
opacity: 0.8;
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.no-voices {
|
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
|
color: rgba(255,255,255,0.4);
|
|
|
|
|
|
font-style: italic;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|