Multilingual support via Fluent + Frontend improvements + Rewrite of ArcReactor

This commit is contained in:
Priler 2026-01-07 05:04:04 +05:00
parent adec595cfa
commit 412acb7e2d
41 changed files with 2436 additions and 723 deletions

View file

@ -11,7 +11,8 @@
startStatsPolling,
stopStatsPolling,
connectIpc,
disconnectIpc
disconnectIpc,
loadTranslations
} from "@/stores"
onMount(() => {
@ -24,6 +25,9 @@
// connect to IPC
connectIpc()
// load language
loadTranslations()
})
onDestroy(() => {

View file

@ -1,17 +1,23 @@
<script lang="ts">
import { onMount } from "svelte"
import { invoke } from "@tauri-apps/api/core"
import { appInfo } from "@/stores"
import { appInfo, currentLanguage, translations, translate } from "@/stores"
$: t = (key: string) => translate($translations, key)
let authorName = ""
let tgLink = ""
let repoLink = ""
let boostyLink = ""
let patreonLink = ""
const currentYear = new Date().getFullYear()
appInfo.subscribe(info => {
tgLink = info.tgOfficialLink
repoLink = info.repositoryLink
boostyLink = info.boostySupportLink
patreonLink = info.patreonSupportLink
})
onMount(async () => {
@ -24,28 +30,42 @@
</script>
<footer id="footer">
<p>© {currentYear}. Автор проекта: {authorName}</p>
<p>© {currentYear}. {t('footer-author')}: <b>{authorName}</b></p>
<p class="links">
<a href={tgLink} target="_blank" class="special-link">
<img src="/media/icons/howdy-logo.png" alt="Telegram" width="20px" />
&nbsp;&nbsp;Наш телеграм
{#if $currentLanguage === "ru" || $currentLanguage === "ua"}
<a href={tgLink} target="_blank" class="telegram-link">
<img src="/media/icons/telegram.webp" alt="Telegram" width="18px" />
&nbsp;<span>{t('footer-telegram')}</span>
</a>
канал.
&nbsp;&nbsp;
&nbsp;
{/if}
<a href={repoLink} target="_blank">
<img src="/media/icons/github-logo.png" alt="GitHub" width="18px" />
&nbsp;Github репозиторий
&nbsp;<span>{t('footer-github')}</span>
</a>
проекта.
</p>
<p class="links last">
{#if $currentLanguage === "ru"}
{t('footer-support')} <a href={tgLink} target="_blank" class="telegram-link">
<img src="/media/icons/boosty.webp" alt="Boosty" width="18px" />
<span>Boosty</span>
</a>.
{/if}
{#if $currentLanguage === "ua" || $currentLanguage === "en"}
{t('footer-support')} <a href={tgLink} target="_blank" class="telegram-link">
<img src="/media/icons/patreon.png" alt="Patreon" width="18px" />
<span>Patreon</span>
</a>.
{/if}
</p>
</footer>
<style lang="scss">
#footer {
text-align: center;
color: #565759;
font-size: 12px;
font-weight: bold;
color: #6c6e71;
font-size: 13px;
font-weight: normal;
line-height: 1.7em;
margin-top: 15px;
@ -56,28 +76,54 @@
&.links {
margin-top: 5px;
margin-bottom: 15px;
&.last {
margin-top: -5px;
}
}
}
a {
color: #185876;
color: #555759!important;
text-decoration: none;
transition: opacity 0.5s;
transition: 0.3s;
& > span {
color: #185876;
border-bottom: 1px solid #185876;
transition: 0.3s;
}
img {
opacity: 0.5;
transition: opacity 0.5s;
margin-top: -4px;
margin-top: -3px;
}
&:hover {
color: #2A9CD0;
color: #777a7d!important;
& > span {
color: #2A9CD0;
}
img {
opacity: 1;
}
}
&.telegram-link {
color: #185876;
display: inline-block;
&:hover {
color: #2A9CD0;
// background: url(/media/images/bg/bg24.gif);
// background-repeat: no-repeat;
// background-size: contain;
}
}
&.special-link {
color: #941d92;
display: inline-block;

View file

@ -1,54 +1,185 @@
<script lang="ts">
import { onMount } from "svelte"
import { goto } from "@roxi/routify"
import { invoke } from "@tauri-apps/api/core"
import { isActive } from "@roxi/routify"
import { Dashboard, Gear } from "radix-icons-svelte"
import { onMount } from "svelte"
import { currentLanguage, setLanguage, translations, translate } from "@/stores"
let appVersion = ""
let commandsCount = 0
let selectedLang = "?"
let langDropdownOpen = false
const languages = [
{ code: "ru", label: "RU", flag: "🇷🇺", name: "Русский" },
{ code: "en", label: "EN", flag: "🇬🇧", name: "English" },
{ code: "ua", label: "UA", flag: "🇺🇦", name: "Українська" },
]
onMount(async () => {
try {
appVersion = await invoke<string>("get_app_version")
} catch (err) {
console.error("failed to get app version:", err)
commandsCount = await invoke<number>("get_commands_count")
// load saved language
const savedLang = await invoke<string>("db_read", { key: "language" })
if (savedLang) {
selectedLang = savedLang
}
} catch {
commandsCount = 0
}
})
async function selectLanguage(code: string) {
await setLanguage(code)
langDropdownOpen = false
}
function toggleLangDropdown() {
langDropdownOpen = !langDropdownOpen
}
function closeLangDropdown(e: MouseEvent) {
const target = e.target as HTMLElement
if (!target.closest('.lang-selector')) {
langDropdownOpen = false
}
}
$: currentLang = languages.find(l => l.code === $currentLanguage) || languages[0]
$: t = (key: string) => translate($translations, key)
</script>
<header id="header">
<div class="logo">
<a href="/" title="Проект канала Хауди Хо!">
<img src="/media/header-logo.png" alt="Jarvis Logo" />
</a>
<div>
<h1><a href="/">JARVIS</a></h1>
<h2>
v{appVersion}
<small class="beta-badge">BETA</small>
</h2>
<svelte:window on:click={closeLangDropdown} />
<header id="header" class="header">
<div class="header-left">
<div class="logo">
<a href="/" title="JARVIS">
<img src="/media/128x128.png" alt="Jarvis Logo" />
</a>
<div class="logo-text">
<span class="logo-title"><a href="/" id="jarvis-logo">&nbsp;</a></span>
<span class="logo-version"><small>v</small>{appVersion} <span class="v-badge">BETA</span></span>
</div>
</div>
</div>
<div class="header-right">
<button class="header-btn" on:click={() => $goto('/commands')}>
<span class="btn-text">{t('header-commands')}</span>
<span class="btn-badge purple">{commandsCount}+</span>
</button>
<button class="header-btn" on:click={() => $goto('/settings')}>
<span class="btn-text">{t('header-settings')}</span>
</button>
<nav class="top-menu">
<ul>
<li>
<a href="/commands" class:active={$isActive("/commands")}>
<Dashboard /> Команды
</a>
</li>
<li>
<a href="/settings" class:active={$isActive("/settings")}>
<Gear /> Настройки
</a>
</li>
</ul>
</nav>
<div class="lang-selector">
<button class="lang-btn" on:click|stopPropagation={toggleLangDropdown}>
<span class="lang-flag"><img src="/media/flags/{currentLang.label}.png" width="23px" alt="{currentLang.flag}"></span>
</button>
{#if langDropdownOpen}
<div class="lang-dropdown">
{#each languages as lang}
<button
class="lang-option"
class:active={lang.code === $currentLanguage}
on:click|stopPropagation={() => selectLanguage(lang.code)}
>
<span class="lang-flag"><img src="/media/flags/{lang.label}.png" width="20px" alt="{lang.flag}"></span>
<span class="lang-name">{lang.name}</span>
</button>
{/each}
</div>
{/if}
</div>
</div>
</header>
<style lang="scss">
.beta-badge {
color: #8AC832;
opacity: 0.9;
font-size: 13px;
.lang-selector {
position: relative;
}
</style>
.lang-btn {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.5rem 0.65rem;
background: transparent;
border: none;
border-radius: 6px;
color: #ffffff;
font-size: 0.7rem;
cursor: pointer;
&:hover {
background: rgba(35, 50, 55, 0.7);
}
}
.lang-flag {
font-size: 0.9rem;
line-height: 1;
}
.lang-code {
font-weight: 600;
letter-spacing: 0.5px;
}
.lang-arrow {
font-size: 0.8rem;
opacity: 0.6;
transition: transform 0.2s ease;
&.open {
transform: rotate(180deg);
}
}
.lang-dropdown {
position: absolute;
top: calc(100% + 0.35rem);
right: 0;
background: rgba(20, 30, 35, 0.98);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
overflow: hidden;
z-index: 100;
min-width: 130px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
}
.lang-option {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.6rem 0.85rem;
background: transparent;
border: none;
color: rgba(255, 255, 255, 0.75);
font-size: 0.75rem;
cursor: pointer;
transition: all 0.15s ease;
text-align: left;
&:hover {
background: rgba(82, 254, 254, 0.1);
color: #ffffff;
}
&.active {
background: rgba(82, 254, 254, 0.15);
color: #52fefe;
}
}
.lang-name {
font-weight: 500;
}
</style>

View file

@ -1,7 +1,6 @@
<script lang="ts">
import { jarvisState } from "@/stores"
// map state to class
$: stateClass = getStateClass($jarvisState)
function getStateClass(state: string): string {
@ -18,92 +17,72 @@
}
</script>
<!-- Based on: https://github.com/rembertdesigns/Iron-Man-Arc-Reactor-Pure-CSS -->
<!-- and https://codepen.io/FlyingEmu/pen/DZNqEj -->
<div class="arc-reactor-wrapper">
<div id="arc-reactor" class="reactor-container {stateClass}">
<div class="reactor-container-inner circle abs-center">
<ul class="marks">
{#each Array(60) as _, i}
<li></li>
{/each}
</ul>
<div class="e7">
<div class="semi_arc_3 e5_1">
<div class="semi_arc_3 e5_2">
<div class="semi_arc_3 e5_3">
<div class="semi_arc_3 e5_4"></div>
<div id="arc-reactor" class="reactor-container {stateClass} arc-white">
<div class="reactor-container-inner circle abs-center">
<ul class="marks">
{#each Array(60) as _, i}
<li></li>
{/each}
</ul>
<div class="e7">
<div class="semi_arc_3 e5_1">
<div class="semi_arc_3 e5_1_ghost"></div>
<div class="semi_arc_3 e5_2">
<div class="semi_arc_3 e5_2_ghost"></div>
<div class="semi_arc_3 e5_3">
<div class="semi_arc_3 e5_3_ghost"></div>
<div class="semi_arc_3 e5_4">
<div class="semi_arc_3 e5_4_ghost"></div>
</div>
</div>
</div>
</div>
</div>
<div class="tunnel circle abs-center"></div>
<div class="core-wrapper circle abs-center"></div>
<div class="core-outer circle abs-center"></div>
<div class="core-inner circle abs-center"></div>
<div class="coil-container">
{#each Array(8) as _, i}
<div class="coil coil-{i + 1}"></div>
{/each}
</div>
</div>
<div class="state-label">
<span class="status-dot"></span>
<span class="label-text">
{#if $jarvisState === "disconnected"}
Отключен
{:else if $jarvisState === "idle"}
Ожидание
{:else if $jarvisState === "listening"}
Слушаю
{:else if $jarvisState === "processing"}
Обработка
{/if}
</span>
<div class="tunnel circle abs-center"></div>
<div class="core-wrapper circle abs-center"></div>
<div class="core-outer circle abs-center"></div>
<div class="core-inner circle abs-center"></div>
<div class="coil-container">
{#each Array(8) as _, i}
<div class="coil coil-{i + 1}"></div>
{/each}
</div>
</div>
<style lang="scss" global>
// [ Variables ]--
$arc-radius: 130px;
$size3: 6px;
$cshadow: rgba(2, 254, 255, 0.8);
$marks-color-1: rgba(2, 254, 255, 1);
$marks-color-2: rgba(2, 254, 255, 0.3);
$colour1: rgba(2, 255, 255, 0.15);
$colour3: rgba(2, 255, 255, 0.3);
// [ ARC REACTOR VARIABLES ]
$arc-radius: 133px;
$arc-container-size: 195%;
$arc-spacing: 93%;
// [ Wrapper ]--
.arc-reactor-wrapper {
display: flex;
flex-direction: column;
align-items: center;
padding: 1rem 0;
}
// arc thickness per level
$arc-thickness-1: 1px;
$arc-thickness-2: 2px;
$arc-thickness-3: 3px;
$arc-thickness-4: 4px;
.state-label {
margin-top: 0.5rem;
font-size: 0.9rem;
color: #52fefe;
text-transform: uppercase;
letter-spacing: 2px;
opacity: 0.8;
transition: opacity 0.3s ease;
}
// marks (lines) settings
$mark-width: 2.5px;
$mark-height: 11px;
// [ Base container ]--
// [ DEFAULT THEME - CYAN ]
.reactor-container {
--arc-color: 2, 254, 255; // RGB values for easy rgba()
--arc-glow: #52fefe;
--arc-glow-rgb: 82, 254, 254;
--arc-core-border: #1b4e5f;
--arc-core-bg: #073c4b;
width: 300px;
height: 320px;
height: 300px;
margin: auto;
position: relative;
border-radius: 50%;
transition: scale 1s ease, opacity 0.5s ease;
scale: 0.9;
transition: transform 0.5s ease, opacity 0.5s ease, filter 0.5s ease;
transform: scale(0.95);
opacity: 0.9;
top: 10px;
ul {
list-style: none;
@ -112,18 +91,113 @@
}
}
// [ COLOR THEMES ]
.reactor-container.arc-cyan {
--arc-color: 2, 254, 255;
--arc-glow: #52fefe;
--arc-glow-rgb: 82, 254, 254;
--arc-core-border: #1b4e5f;
--arc-core-bg: #073c4b;
}
.reactor-container.arc-red {
--arc-color: 255, 50, 50;
--arc-glow: #ff5050;
--arc-glow-rgb: 255, 80, 80;
--arc-core-border: #5f1b1b;
--arc-core-bg: #4b0707;
}
.reactor-container.arc-orange {
--arc-color: 255, 150, 50;
--arc-glow: #ff9632;
--arc-glow-rgb: 255, 150, 50;
--arc-core-border: #5f3c1b;
--arc-core-bg: #4b2a07;
}
.reactor-container.arc-yellow {
--arc-color: 255, 230, 50;
--arc-glow: #ffe632;
--arc-glow-rgb: 255, 230, 50;
--arc-core-border: #5f5a1b;
--arc-core-bg: #4b4507;
}
.reactor-container.arc-green {
--arc-color: 50, 255, 100;
--arc-glow: #32ff64;
--arc-glow-rgb: 50, 255, 100;
--arc-core-border: #1b5f2a;
--arc-core-bg: #074b15;
}
.reactor-container.arc-blue {
--arc-color: 50, 150, 255;
--arc-glow: #3296ff;
--arc-glow-rgb: 50, 150, 255;
--arc-core-border: #1b3c5f;
--arc-core-bg: #072a4b;
}
.reactor-container.arc-purple {
--arc-color: 180, 100, 255;
--arc-glow: #b464ff;
--arc-glow-rgb: 180, 100, 255;
--arc-core-border: #3c1b5f;
--arc-core-bg: #28074b;
}
.reactor-container.arc-pink {
--arc-color: 255, 100, 200;
--arc-glow: #ff64c8;
--arc-glow-rgb: 255, 100, 200;
--arc-core-border: #5f1b4a;
--arc-core-bg: #4b0735;
}
.reactor-container.arc-white {
--arc-color: 255, 255, 255;
--arc-glow: #ffffff;
--arc-glow-rgb: 255, 255, 255;
--arc-core-border: #4a4a4a;
--arc-core-bg: #2a2a2a;
}
// [ BACKGROUND GLOW ]
.reactor-container::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 1000px;
height: 1000px;
border-radius: 50%;
background: radial-gradient(
circle,
rgba(var(--arc-color), 0.20) 0%,
rgba(var(--arc-color), 0.15) 30%,
rgba(var(--arc-color), 0.10) 40%,
rgba(var(--arc-color), 0.04) 50%,
transparent 70%
);
z-index: -1;
pointer-events: none;
transition: opacity 0.5s ease, transform 0.5s ease;
}
// [ CORE ELEMENTS - using CSS vars ]
.reactor-container-inner {
height: 238px;
width: 238px;
background-color: #161a1b;
box-shadow: 0px 0px 50px 15px $colour3, inset 0px 0px 50px 15px $colour3;
box-shadow: 0px 0px 50px 15px rgba(var(--arc-color), 0.3),
inset 0px 0px 50px 15px rgba(var(--arc-color), 0.3);
transition: box-shadow 0.5s ease;
}
// [ Utility classes ]--
.circle {
border-radius: 50%;
}
.circle { border-radius: 50%; }
.abs-center {
position: absolute;
@ -134,30 +208,32 @@
margin: auto;
}
// [ Core elements ]--
.core-inner {
width: 70px;
height: 70px;
border: 5px solid #1b4e5f;
border: 5px solid var(--arc-core-border);
background-color: #ffffff;
box-shadow: 0px 0px 7px 5px #52fefe, 0px 0px 10px 10px #52fefe inset;
box-shadow: 0px 0px 7px 5px var(--arc-glow),
0px 0px 10px 10px var(--arc-glow) inset;
transition: box-shadow 0.5s ease;
}
.core-outer {
width: 120px;
height: 120px;
border: 1px solid #52fefe;
border: 1px solid var(--arc-glow);
background-color: #ffffff;
box-shadow: 0px 0px 2px 1px #52fefe, 0px 0px 10px 5px #52fefe inset;
box-shadow: 0px 0px 2px 1px var(--arc-glow),
0px 0px 10px 5px var(--arc-glow) inset;
transition: box-shadow 0.5s ease;
}
.core-wrapper {
width: 180px;
height: 180px;
background-color: #073c4b;
box-shadow: 0px 0px 5px 4px #52fefe, 0px 0px 6px 2px #52fefe inset;
background-color: var(--arc-core-bg);
box-shadow: 0px 0px 5px 4px var(--arc-glow),
0px 0px 6px 2px var(--arc-glow) inset;
transition: box-shadow 0.5s ease;
}
@ -165,17 +241,16 @@
width: 220px;
height: 220px;
background-color: #ffffff;
box-shadow: 0px 0px 5px 1px #52fefe, 0px 0px 5px 4px #52fefe inset;
box-shadow: 0px 0px 5px 1px var(--arc-glow),
0px 0px 5px 4px var(--arc-glow) inset;
transition: box-shadow 0.5s ease;
}
// [ Coil animation ]--
.coil-container {
position: relative;
width: 100%;
height: 100%;
animation: 10s infinite linear reactor-anim;
transition: animation-duration 0.5s ease;
animation: reactor-anim 10s infinite linear;
}
.coil {
@ -185,8 +260,8 @@
top: calc(50% - 110px);
left: calc(50% - 15px);
transform-origin: 15px 110px;
background-color: #073c4b;
box-shadow: 0px 0px 5px #52fefe inset;
background-color: var(--arc-core-bg);
box-shadow: 0px 0px 5px var(--arc-glow) inset;
}
@for $i from 1 through 8 {
@ -200,41 +275,112 @@
to { transform: rotate(360deg); }
}
// [ Arc element ]--
.e7 {
position: relative;
position: absolute;
z-index: 1;
width: 160%;
height: 160%;
left: -32.5%;
top: -32.5%;
right: 0;
bottom: 0;
margin: auto;
border: $size3 solid transparent;
width: $arc-container-size;
height: $arc-container-size;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
border: 6px solid transparent;
background: transparent;
border-radius: 50%;
transform: rotateZ(0deg);
transition: box-shadow 3s ease, opacity 0.5s ease;
transition: opacity 0.5s ease;
text-align: center;
opacity: 0.3;
opacity: 0.5;
}
.semi_arc_3 {
content: "";
$offset: calc((100% - #{$arc-spacing}) / 2);
position: absolute;
width: 94%;
height: 94%;
left: 3%;
top: 3%;
border: 5px solid #02feff;
width: $arc-spacing;
height: $arc-spacing;
left: $offset;
top: $offset;
border-style: solid;
border-color: transparent;
border-radius: 50%;
box-sizing: border-box;
animation: rotate 4s linear infinite;
text-align: center;
animation: rotate 6s linear infinite;
overflow: hidden;
}
// [ MAIN ARCS ]
.e5_1 {
border-width: $arc-thickness-1;
border-top-color: rgba(var(--arc-color), 0.25);
border-right-color: transparent;
border-bottom-color: transparent;
border-left-color: rgba(var(--arc-color), 0.25);
animation: rotate 8s linear infinite;
}
.e5_2 {
border-width: $arc-thickness-2;
border-top-color: transparent;
border-right-color: rgba(var(--arc-color), 0.4);
border-bottom-color: rgba(var(--arc-color), 0.4);
border-left-color: transparent;
animation: rotate_anti 6s linear infinite;
}
.e5_3 {
border-width: $arc-thickness-3;
border-top-color: rgba(var(--arc-color), 0.6);
border-right-color: transparent;
border-bottom-color: transparent;
border-left-color: transparent;
animation: rotate 4s linear infinite;
}
.e5_4 {
border-width: $arc-thickness-4;
border-top-color: transparent;
border-right-color: transparent;
border-bottom-color: rgba(var(--arc-color), 0.8);
border-left-color: transparent;
animation: rotate_anti 5s linear infinite;
}
// [ GHOST ARCS ]
.e5_1_ghost {
border-width: $arc-thickness-1;
border-top-color: rgba(var(--arc-color), 0.08);
border-right-color: rgba(var(--arc-color), 0.08);
border-bottom-color: transparent;
border-left-color: transparent;
animation: rotate_anti 12s linear infinite;
}
.e5_2_ghost {
border-width: $arc-thickness-2;
border-top-color: rgba(var(--arc-color), 0.12);
border-right-color: transparent;
border-bottom-color: transparent;
border-left-color: rgba(var(--arc-color), 0.12);
animation: rotate 9s linear infinite;
}
.e5_3_ghost {
border-width: $arc-thickness-3;
border-top-color: transparent;
border-right-color: rgba(var(--arc-color), 0.18);
border-bottom-color: rgba(var(--arc-color), 0.18);
border-left-color: transparent;
animation: rotate_anti 6s linear infinite;
}
.e5_4_ghost {
border-width: $arc-thickness-4;
border-top-color: rgba(var(--arc-color), 0.24);
border-right-color: transparent;
border-bottom-color: transparent;
border-left-color: rgba(var(--arc-color), 0.24);
animation: rotate 7.5s linear infinite;
}
@keyframes rotate {
0% { transform: rotateZ(0deg); }
100% { transform: rotateZ(360deg); }
@ -245,167 +391,246 @@
100% { transform: rotateZ(-360deg); }
}
// [ Marks ]--
// [ MARKS ]
.marks {
li {
width: 11px;
height: 11px;
background: $cshadow;
position: absolute;
margin-left: 117.5px;
margin-top: 113.5px;
animation: colour_ease2 3s infinite ease-in-out;
}
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
.marks li {
width: $mark-width;
height: $mark-height;
background: rgba(var(--arc-color), 0.8);
position: absolute;
left: 50%;
top: 50%;
margin-left: calc(-#{$mark-width} / 2);
margin-top: calc(-#{$mark-height} / 2);
animation: colour_ease2 3s infinite ease-in-out;
}
@keyframes colour_ease2 {
0% { background: $marks-color-1; }
50% { background: $marks-color-2; }
100% { background: $marks-color-1; }
0% { background: rgba(var(--arc-color), 1); }
50% { background: rgba(var(--arc-color), 0.3); }
100% { background: rgba(var(--arc-color), 1); }
}
@for $i from 1 through 60 {
.marks li:nth-child(#{$i}) {
transform: rotate(#{$i * 6}deg) translateY($arc-radius);
transform: rotate(#{$i * 6}deg) translateY(-$arc-radius);
}
}
// [ DISCONNECTED state ]--
// [ DISCONNECTED ]
.reactor-container.disconnected {
transform: scale(0.8);
transform: scale(0.85);
opacity: 0.4;
filter: grayscale(0.8) brightness(0.6);
.coil-container {
animation-duration: 20s;
filter: grayscale(0.7) brightness(0.6);
.coil-container { animation-duration: 20s; }
.e7 { opacity: 0.3; }
.e5_1 {
border-top-color: rgba(var(--arc-color), 0.1);
border-left-color: rgba(var(--arc-color), 0.1);
animation: rotate 35s linear infinite;
}
.e5_2 {
border-right-color: rgba(var(--arc-color), 0.15);
border-bottom-color: rgba(var(--arc-color), 0.15);
animation: rotate_anti 30s linear infinite;
}
.e5_3 {
border-top-color: rgba(var(--arc-color), 0.2);
animation: rotate 25s linear infinite;
}
.e5_4 {
border-bottom-color: rgba(var(--arc-color), 0.25);
animation: rotate_anti 28s linear infinite;
}
.state-label {
opacity: 0.5;
.e5_1_ghost {
border-top-color: rgba(var(--arc-color), 0.03);
border-right-color: rgba(var(--arc-color), 0.03);
animation: rotate_anti 50s linear infinite;
}
.e5_2_ghost {
border-top-color: rgba(var(--arc-color), 0.05);
border-left-color: rgba(var(--arc-color), 0.05);
animation: rotate 45s linear infinite;
}
.e5_3_ghost {
border-right-color: rgba(var(--arc-color), 0.06);
border-bottom-color: rgba(var(--arc-color), 0.06);
animation: rotate_anti 40s linear infinite;
}
.e5_4_ghost {
border-top-color: rgba(var(--arc-color), 0.08);
border-left-color: rgba(var(--arc-color), 0.08);
animation: rotate 42s linear infinite;
}
}
// [ IDLE state ]--
.reactor-container.disconnected::before {
opacity: 0.3;
transform: translate(-50%, -50%) scale(0.7);
}
// [ IDLE ]
.reactor-container.idle {
transform: scale(0.9);
transform: scale(0.95);
opacity: 0.9;
.coil-container {
animation-duration: 10s;
.coil-container { animation-duration: 10s; }
.e7 { opacity: 0.6; }
.e5_1 {
border-top-color: rgba(var(--arc-color), 0.2);
border-left-color: rgba(var(--arc-color), 0.2);
animation: rotate 9s linear infinite;
}
.e5_2 {
border-right-color: rgba(var(--arc-color), 0.35);
border-bottom-color: rgba(var(--arc-color), 0.35);
animation: rotate_anti 15s linear infinite;
}
.e5_3 {
border-top-color: rgba(var(--arc-color), 0.5);
animation: rotate 12s linear infinite;
}
.e5_4 {
border-bottom-color: rgba(var(--arc-color), 0.65);
animation: rotate_anti 14s linear infinite;
}
.reactor-container-inner {
box-shadow: 0px 0px 50px 15px $colour3, inset 0px 0px 50px 15px $colour3;
.e5_1_ghost {
border-top-color: rgba(var(--arc-color), 0.06);
border-right-color: rgba(var(--arc-color), 0.06);
animation: rotate_anti 14s linear infinite;
}
.e5_2_ghost {
border-top-color: rgba(var(--arc-color), 0.1);
border-left-color: rgba(var(--arc-color), 0.1);
animation: rotate 22s linear infinite;
}
.e5_3_ghost {
border-right-color: rgba(var(--arc-color), 0.15);
border-bottom-color: rgba(var(--arc-color), 0.15);
animation: rotate_anti 18s linear infinite;
}
.e5_4_ghost {
border-top-color: rgba(var(--arc-color), 0.2);
border-left-color: rgba(var(--arc-color), 0.2);
animation: rotate 21s linear infinite;
}
}
// [ ACTIVE state (listening/processing) ]--
.reactor-container.idle::before {
opacity: 0.7;
transform: translate(-50%, -50%) scale(0.9);
}
// [ ACTIVE ]
.reactor-container.active {
transform: scale(1);
transform: scale(1.05);
opacity: 1;
.coil-container {
animation-duration: 3s;
.coil-container { animation-duration: 3s; }
.e7 { opacity: 1; }
.e5_1 {
border-top-color: rgba(var(--arc-color), 0.3);
border-left-color: rgba(var(--arc-color), 0.3);
animation: rotate 4s linear infinite;
}
.e5_2 {
border-right-color: rgba(var(--arc-color), 0.5);
border-bottom-color: rgba(var(--arc-color), 0.5);
animation: rotate_anti 3s linear infinite;
}
.e5_3 {
border-top-color: rgba(var(--arc-color), 0.7);
animation: rotate 2s linear infinite;
}
.e5_4 {
border-bottom-color: rgba(var(--arc-color), 0.9);
animation: rotate_anti 2.5s linear infinite;
}
.e5_1_ghost {
border-top-color: rgba(var(--arc-color), 0.1);
border-right-color: rgba(var(--arc-color), 0.1);
animation: rotate_anti 6s linear infinite;
}
.e5_2_ghost {
border-top-color: rgba(var(--arc-color), 0.15);
border-left-color: rgba(var(--arc-color), 0.15);
animation: rotate 4.5s linear infinite;
}
.e5_3_ghost {
border-right-color: rgba(var(--arc-color), 0.2);
border-bottom-color: rgba(var(--arc-color), 0.2);
animation: rotate_anti 3s linear infinite;
}
.e5_4_ghost {
border-top-color: rgba(var(--arc-color), 0.28);
border-left-color: rgba(var(--arc-color), 0.28);
animation: rotate 3.75s linear infinite;
}
.reactor-container-inner {
box-shadow: 0px 0px 70px 25px $colour3, inset 0px 0px 70px 25px $colour3;
box-shadow: 0px 0px 70px 25px rgba(var(--arc-color), 0.3),
inset 0px 0px 70px 25px rgba(var(--arc-color), 0.3);
}
.core-inner {
box-shadow: 0px 0px 15px 10px #52fefe, 0px 0px 20px 15px #52fefe inset;
box-shadow: 0px 0px 15px 10px var(--arc-glow),
0px 0px 20px 15px var(--arc-glow) inset;
}
.core-outer {
box-shadow: 0px 0px 5px 3px #52fefe, 0px 0px 15px 10px #52fefe inset;
box-shadow: 0px 0px 5px 3px var(--arc-glow),
0px 0px 15px 10px var(--arc-glow) inset;
}
.core-wrapper {
box-shadow: 0px 0px 10px 8px #52fefe, 0px 0px 10px 5px #52fefe inset;
box-shadow: 0px 0px 10px 8px var(--arc-glow),
0px 0px 10px 5px var(--arc-glow) inset;
}
.tunnel {
box-shadow: 0px 0px 10px 3px #52fefe, 0px 0px 10px 8px #52fefe inset;
box-shadow: 0px 0px 10px 3px var(--arc-glow),
0px 0px 10px 8px var(--arc-glow) inset;
}
.e7 {
opacity: 0.6;
}
.e5_1 { animation: rotate 3s linear infinite; }
.e5_2 { animation: rotate_anti 2s linear infinite; }
.e5_3 { animation: rotate 2s linear infinite; }
.e5_4 { animation: rotate_anti 2s linear infinite; }
.marks li {
animation: colour_ease2_active 1s infinite ease-in-out;
}
}
@keyframes colour_ease2_active {
0% { background: $marks-color-1; box-shadow: 0 0 5px $marks-color-1; }
50% { background: $marks-color-2; box-shadow: none; }
100% { background: $marks-color-1; box-shadow: 0 0 5px $marks-color-1; }
.reactor-container.active::before {
opacity: 1;
transform: translate(-50%, -50%) scale(1.1);
animation: bg-pulse 3s ease-in-out infinite;
}
// [ Pulse animation for listening ]--
@keyframes listening-pulse {
@keyframes bg-pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
transform: translate(-50%, -50%) scale(1.1);
}
50% {
transform: scale(1.03);
opacity: 0.85;
transform: translate(-50%, -50%) scale(1.05);
}
}
// [ State Label ]
.state-label {
margin-top: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: #52fefe;
box-shadow: 0 0 8px #52fefe;
}
.label-text {
font-size: 0.9rem;
color: rgba(82, 254, 254, 0.8);
text-transform: uppercase;
letter-spacing: 3px;
font-weight: 400;
font-family: "Roboto Condensed", sans-serif; // "Trebuchet MS", Arial, Helvetica, sans-serif
}
.reactor-container.active + .state-label {
.status-dot {
animation: dot-pulse 0.8s ease-in-out infinite;
}
.label-text {
color: #52fefe;
}
}
.reactor-container.disconnected + .state-label {
.status-dot {
background: #445555;
box-shadow: none;
}
.label-text {
color: #445555;
}
}
@keyframes dot-pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.5); opacity: 0.7; }
@keyframes colour_ease2_active {
0% { background: rgba(var(--arc-color), 1); box-shadow: 0 0 5px rgba(var(--arc-color), 1); }
50% { background: rgba(var(--arc-color), 0.3); box-shadow: none; }
100% { background: rgba(var(--arc-color), 1); box-shadow: 0 0 5px rgba(var(--arc-color), 1); }
}
</style>

View file

@ -1,4 +1,8 @@
<script lang="ts">
import { translations, translate } from "@/stores"
$: t = (key: string) => translate($translations, key)
let searchQuery = ""
</script>
@ -8,12 +12,11 @@
bind:value={searchQuery}
type="text"
name="q"
placeholder="Введите команду или скажите «Джарвис» ..."
placeholder={t('search-placeholder')}
autocomplete="off"
minlength="3"
maxlength="30"
/>
<button type="submit" aria-label="Search"></button>
<small>Enter</small>
</form>
</div>
</div>

View file

@ -1,346 +1,130 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte"
import { invoke } from "@tauri-apps/api/core"
import { capitalizeFirstLetter } from "@/functions"
import { onMount } from "svelte"
import {
isJarvisRunning,
jarvisRamUsage,
jarvisCpuUsage,
ipcConnected,
translations,
translate
} from "@/stores"
import {
Text,
} from "@svelteuidev/core"
$: t = (key: string) => translate($translations, key)
let jarvisStats = { running: false, ram_mb: 0, cpu_usage: 0 }
let microphoneLabel = ""
let wakeWordEngine = ""
let microphoneName = "Загрузка..."
let wakeWordEngine = "Rustpotter"
let sttEngine = "Vosk"
// let ramUsage = "-"
let statsUpdateInterval: number | null = null
async function updateStats() {
try {
jarvisStats = await invoke<{running: boolean, ram_mb: number, cpu_usage: number}>("get_jarvis_app_stats")
//const usage = await invoke<number>("get_current_ram_usage")
//ramUsage = usage.toFixed(2)
} catch (err) {
console.error("failed to get ram usage:", err)
}
}
let vadInfo = "Snip + ChatGPT"
onMount(async () => {
// start polling ram usage
updateStats()
statsUpdateInterval = setInterval(updateStats, 5000) as unknown as number
microphoneName = t('stats-loading')
try {
// load microphone info
const micIndex = Number(await invoke<string>("db_read", { key: "selected_microphone" }))
microphoneLabel = await invoke<string>("pv_get_audio_device_name", { idx: micIndex })
const micIndex = await invoke<string>("db_read", { key: "selected_microphone" })
if (micIndex && micIndex !== "-1") {
const devices = await invoke<string[]>("pv_get_audio_devices")
const idx = parseInt(micIndex)
if (devices[idx]) {
microphoneName = devices[idx]
}
} else {
microphoneName = t('stats-system-default')
}
// load wake word engine
const engine = await invoke<string>("db_read", { key: "selected_wake_word_engine" })
wakeWordEngine = capitalizeFirstLetter(engine)
wakeWordEngine = await invoke<string>("db_read", { key: "selected_wake_word_engine" }) || "Rustpotter"
sttEngine = await invoke<string>("db_read", { key: "selected_stt_engine" }) || "Vosk"
vadInfo = await invoke<string>("db_read", { key: "vad" }) || "Vosk"
} catch (err) {
console.error("failed to load stats:", err)
console.error("Failed to load stats:", err)
microphoneName = t('stats-not-selected')
}
})
onDestroy(() => {
if (statsUpdateInterval) {
clearInterval(statsUpdateInterval)
}
})
function truncate(str: string, max: number): string {
return str.length > max ? str.slice(0, max) + "..." : str
}
</script>
<div class="statistics">
<div class="online">
<div class="pulse"><div class="wave"></div></div>
<div class="info">
<span class="num">Микрофон</span>
<small title={microphoneLabel}>{microphoneLabel}</small>
<div class="stats-bar">
<div class="stat-item">
<span class="stat-dot" class:active={$isJarvisRunning} style="--color: #22c55e;"></span>
<div class="stat-content">
<span class="stat-label">{t('stats-microphone')}</span>
<span class="stat-value" title="{microphoneName}">{truncate(microphoneName, 18)}</span>
</div>
</div>
<div class="files">
<div class="pulse"><div class="wave"></div></div>
<div class="info">
<span class="num">Нейросети</span>
<small>{wakeWordEngine} + {sttEngine}</small>
<div class="stat-item">
<span class="stat-dot" class:active={$ipcConnected} style="--color: #f97316;"></span>
<div class="stat-content">
<span class="stat-label">{t('stats-neural-networks')}</span>
<span class="stat-value"><span title="Wake Word Engine">{wakeWordEngine}</span> + <span title="Speech To Text">{sttEngine}</span></span>
<span class="stat-value-sub">{#if vadInfo != "None"}VAD: {vadInfo}{/if}</span>
</div>
</div>
<div class="downloads hint--bottom" aria-label="Общее количество скачиваний по всему проекту">
<div class="pulse"><div class="wave"></div></div>
<div class="info">
<span class="num">Ресурсы</span>
{#if jarvisStats.running}
<small>RAM: {jarvisStats.ram_mb} MB</small>
<!--<Text>CPU: {jarvisStats.cpu_usage.toFixed(1)}%</Text>-->
{:else}
<Text color="gray">-</Text>
{/if}
<div class="stat-item">
<span class="stat-dot" class:active={$ipcConnected} style="--color: #3b82f6;"></span>
<div class="stat-content">
<span class="stat-label">{t('stats-resources')}</span>
<span class="stat-value">{#if jarvisRamUsage }RAM {$jarvisRamUsage}mb{:else}...{/if}</span>
</div>
</div>
</div>
<style lang="scss">
.statistics {
position: relative;
z-index: 3;
padding: 0 10px;
height: 100px;
.stats-bar {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 1.1rem 1.5rem;
background: transparent;
}
& > div {
height: 70px;
}
.stat-item {
display: flex;
align-items: flex-start;
gap: 0.6rem;
}
.info {
z-index: 10;
}
.stat-dot {
width: 10px;
height: 10px;
border-radius: 50%;
margin-top: 0.5rem;
background: rgba(70, 70, 70, 0.6);
transition: all 0.3s ease;
// [ Online/Microphone stat ]--
& > .online {
position: relative;
width: 40%;
$base-color: rgba(0, 191, 8, 1);
$mid-color: rgba(0, 191, 8, 0.4);
$end-color: rgba(0, 191, 8, 0);
& > .pulse::before {
background-color: $base-color;
}
& > .pulse::after {
background-color: $base-color;
animation: online-cdot linear 3s infinite forwards;
}
& > .pulse .wave {
background-color: $mid-color;
animation: online-radarWave cubic-bezier(0, 0.54, 0.53, 1) 3s 0s infinite;
}
& > .pulse .wave::after {
background-color: $mid-color;
animation: online-radarWave cubic-bezier(0, 0.54, 0.53, 1) 3s 0.1s infinite;
}
& > .info {
position: absolute;
top: 26px;
left: 26px;
& > span.num {
font-size: 18px;
font-weight: bold;
color: #00bf08;
}
& > small {
display: block;
color: #535a60;
font-size: 12px;
position: relative;
top: 0;
width: 130px;
max-height: 40px;
overflow: hidden;
line-height: 1.5em;
}
}
@keyframes online-cdot {
0% { opacity: 0.3; background: $base-color; }
50% { opacity: 0.5; }
100% { opacity: 1; background: $end-color; }
}
@keyframes online-radarWave {
0% { opacity: 0.1; transform: scale(0); }
5% { background: $mid-color; opacity: 1; }
100% { transform: scale(1.2); background: $end-color; }
}
}
// [ Files/Neural networks stat ]--
& > .files {
position: relative;
width: 35%;
$base-color: rgba(255, 129, 48, 1);
$mid-color: rgba(255, 129, 48, 0.4);
$end-color: rgba(255, 129, 48, 0);
& > .pulse::before {
background-color: $base-color;
}
& > .pulse::after {
background-color: $base-color;
animation: files-cdot linear 5s infinite forwards;
}
& > .pulse .wave {
background-color: $mid-color;
animation: files-radarWave cubic-bezier(0, 0.54, 0.53, 1) 5s 0s infinite;
}
& > .pulse .wave::after {
background-color: $mid-color;
animation: files-radarWave cubic-bezier(0, 0.54, 0.53, 1) 5s 0.1s infinite;
}
& > .info {
position: absolute;
top: 26px;
left: 26px;
& > span.num {
font-size: 18px;
font-weight: bold;
color: #ff8130;
}
& > small {
display: block;
color: #535a60;
font-size: 12px;
position: relative;
top: 0;
}
}
@keyframes files-cdot {
0% { opacity: 0.3; background: $base-color; }
50% { opacity: 0.5; }
100% { opacity: 1; background: $end-color; }
}
@keyframes files-radarWave {
0% { opacity: 0.1; transform: scale(0); }
5% { background: $mid-color; transform: scale(0.2); opacity: 1; }
100% { transform: scale(0.8); background: $end-color; }
}
}
// [ Downloads/Resources stat ]--
& > .downloads {
position: relative;
$base-color: rgba(11, 66, 166, 1);
$mid-color: rgba(32, 150, 243, 0.4);
$end-color: rgba(32, 150, 243, 0);
& > .pulse::before {
background: rgba(32, 150, 243, 1);
}
& > .pulse::after {
background: rgba(32, 150, 243, 1);
animation: downloads-cdot linear 7s infinite forwards;
animation-delay: 1s;
}
& > .pulse .wave {
background-color: $mid-color;
animation: downloads-radarWave cubic-bezier(0, 0.54, 0.53, 1) 7s 0s infinite;
animation-delay: 1s;
}
& > .pulse .wave::after {
background-color: $mid-color;
animation: downloads-radarWave cubic-bezier(0, 0.54, 0.53, 1) 7s 0.1s infinite;
animation-delay: 1s;
}
& > .info {
position: absolute;
top: 26px;
left: 26px;
& > span.num {
font-size: 18px;
font-weight: bold;
color: #1b78a6;
}
& > small {
display: block;
color: #535a60;
font-size: 12px;
position: relative;
top: 0;
}
}
@keyframes downloads-cdot {
0% { opacity: 0.3; background: $base-color; }
50% { opacity: 0.5; }
100% { opacity: 1; background: $end-color; }
}
@keyframes downloads-radarWave {
0% { opacity: 0.1; transform: scale(0); }
5% { background: $mid-color; opacity: 1; }
100% { transform: scale(0.7); background: $end-color; }
}
}
// [ Shared pulse styles ]--
.pulse {
position: relative;
height: 100px;
width: 100px;
margin: 0;
left: -43px;
top: 0px;
z-index: 5;
}
.pulse::before {
content: "";
position: absolute;
width: 11px;
height: 11px;
border-radius: 50%;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
opacity: 0.5;
}
.pulse::after {
content: "";
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.pulse .wave {
position: absolute;
left: 7%;
top: 7%;
width: 86%;
height: 86%;
border-radius: 50%;
opacity: 0;
}
.pulse .wave::after {
content: "";
position: absolute;
left: 7%;
top: 7%;
width: 86%;
height: 86%;
border-radius: 50%;
opacity: 0;
&.active {
background: var(--color);
box-shadow: 0 0 10px var(--color);
}
}
</style>
.stat-content {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.stat-label {
font-size: 0.8rem;
font-weight: 600;
color: #ffffff;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-value {
font-size: 0.7rem;
color: rgba(255, 255, 255, 0.42);
line-height: 1.35;
font-style: italic;
}
.stat-value-sub {
font-size: 0.65rem;
color: rgba(255, 255, 255, 0.28);
}
</style>

View file

@ -0,0 +1,411 @@
<script lang="ts">
import { jarvisState } from "@/stores"
// map state to class
$: stateClass = getStateClass($jarvisState)
function getStateClass(state: string): string {
switch (state) {
case "listening":
case "processing":
return "active"
case "idle":
return "idle"
case "disconnected":
default:
return "disconnected"
}
}
</script>
<!-- Based on: https://github.com/rembertdesigns/Iron-Man-Arc-Reactor-Pure-CSS -->
<!-- and https://codepen.io/FlyingEmu/pen/DZNqEj -->
<div class="arc-reactor-wrapper">
<div id="arc-reactor" class="reactor-container {stateClass}">
<div class="reactor-container-inner circle abs-center">
<ul class="marks">
{#each Array(60) as _, i}
<li></li>
{/each}
</ul>
<div class="e7">
<div class="semi_arc_3 e5_1">
<div class="semi_arc_3 e5_2">
<div class="semi_arc_3 e5_3">
<div class="semi_arc_3 e5_4"></div>
</div>
</div>
</div>
</div>
</div>
<div class="tunnel circle abs-center"></div>
<div class="core-wrapper circle abs-center"></div>
<div class="core-outer circle abs-center"></div>
<div class="core-inner circle abs-center"></div>
<div class="coil-container">
{#each Array(8) as _, i}
<div class="coil coil-{i + 1}"></div>
{/each}
</div>
</div>
<div class="state-label">
<span class="status-dot"></span>
<span class="label-text">
{#if $jarvisState === "disconnected"}
Отключен
{:else if $jarvisState === "idle"}
Ожидание
{:else if $jarvisState === "listening"}
Слушаю
{:else if $jarvisState === "processing"}
Обработка
{/if}
</span>
</div>
</div>
<style lang="scss" global>
// [ Variables ]--
$arc-radius: 130px;
$size3: 6px;
$cshadow: rgba(2, 254, 255, 0.8);
$marks-color-1: rgba(2, 254, 255, 1);
$marks-color-2: rgba(2, 254, 255, 0.3);
$colour1: rgba(2, 255, 255, 0.15);
$colour3: rgba(2, 255, 255, 0.3);
// [ Wrapper ]--
.arc-reactor-wrapper {
display: flex;
flex-direction: column;
align-items: center;
padding: 1rem 0;
}
.state-label {
margin-top: 0.5rem;
font-size: 0.9rem;
color: #52fefe;
text-transform: uppercase;
letter-spacing: 2px;
opacity: 0.8;
transition: opacity 0.3s ease;
}
// [ Base container ]--
.reactor-container {
width: 300px;
height: 320px;
margin: auto;
position: relative;
border-radius: 50%;
transition: scale 1s ease, opacity 0.5s ease;
scale: 0.9;
opacity: 0.9;
ul {
list-style: none;
margin: 0;
padding: 0;
}
}
.reactor-container-inner {
height: 238px;
width: 238px;
background-color: #161a1b;
box-shadow: 0px 0px 50px 15px $colour3, inset 0px 0px 50px 15px $colour3;
transition: box-shadow 0.5s ease;
}
// [ Utility classes ]--
.circle {
border-radius: 50%;
}
.abs-center {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
// [ Core elements ]--
.core-inner {
width: 70px;
height: 70px;
border: 5px solid #1b4e5f;
background-color: #ffffff;
box-shadow: 0px 0px 7px 5px #52fefe, 0px 0px 10px 10px #52fefe inset;
transition: box-shadow 0.5s ease;
}
.core-outer {
width: 120px;
height: 120px;
border: 1px solid #52fefe;
background-color: #ffffff;
box-shadow: 0px 0px 2px 1px #52fefe, 0px 0px 10px 5px #52fefe inset;
transition: box-shadow 0.5s ease;
}
.core-wrapper {
width: 180px;
height: 180px;
background-color: #073c4b;
box-shadow: 0px 0px 5px 4px #52fefe, 0px 0px 6px 2px #52fefe inset;
transition: box-shadow 0.5s ease;
}
.tunnel {
width: 220px;
height: 220px;
background-color: #ffffff;
box-shadow: 0px 0px 5px 1px #52fefe, 0px 0px 5px 4px #52fefe inset;
transition: box-shadow 0.5s ease;
}
// [ Coil animation ]--
.coil-container {
position: relative;
width: 100%;
height: 100%;
animation: 10s infinite linear reactor-anim;
transition: animation-duration 0.5s ease;
}
.coil {
position: absolute;
width: 30px;
height: 20px;
top: calc(50% - 110px);
left: calc(50% - 15px);
transform-origin: 15px 110px;
background-color: #073c4b;
box-shadow: 0px 0px 5px #52fefe inset;
}
@for $i from 1 through 8 {
.coil-#{$i} {
transform: rotate(#{($i - 1) * 45}deg);
}
}
@keyframes reactor-anim {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
// [ Arc element ]--
.e7 {
position: relative;
z-index: 1;
width: 160%;
height: 160%;
left: -32.5%;
top: -32.5%;
right: 0;
bottom: 0;
margin: auto;
border: $size3 solid transparent;
background: transparent;
border-radius: 50%;
transform: rotateZ(0deg);
transition: box-shadow 3s ease, opacity 0.5s ease;
text-align: center;
opacity: 0.3;
}
.semi_arc_3 {
content: "";
position: absolute;
width: 94%;
height: 94%;
left: 3%;
top: 3%;
border: 5px solid #02feff;
border-radius: 50%;
box-sizing: border-box;
animation: rotate 4s linear infinite;
text-align: center;
overflow: hidden;
}
@keyframes rotate {
0% { transform: rotateZ(0deg); }
100% { transform: rotateZ(360deg); }
}
@keyframes rotate_anti {
0% { transform: rotateZ(0deg); }
100% { transform: rotateZ(-360deg); }
}
// [ Marks ]--
.marks {
li {
width: 11px;
height: 11px;
background: $cshadow;
position: absolute;
margin-left: 117.5px;
margin-top: 113.5px;
animation: colour_ease2 3s infinite ease-in-out;
}
}
@keyframes colour_ease2 {
0% { background: $marks-color-1; }
50% { background: $marks-color-2; }
100% { background: $marks-color-1; }
}
@for $i from 1 through 60 {
.marks li:nth-child(#{$i}) {
transform: rotate(#{$i * 6}deg) translateY($arc-radius);
}
}
// [ DISCONNECTED state ]--
.reactor-container.disconnected {
transform: scale(0.8);
opacity: 0.4;
filter: grayscale(0.8) brightness(0.6);
.coil-container {
animation-duration: 20s;
}
.state-label {
opacity: 0.5;
}
}
// [ IDLE state ]--
.reactor-container.idle {
transform: scale(0.9);
opacity: 0.9;
.coil-container {
animation-duration: 10s;
}
.reactor-container-inner {
box-shadow: 0px 0px 50px 15px $colour3, inset 0px 0px 50px 15px $colour3;
}
}
// [ ACTIVE state (listening/processing) ]--
.reactor-container.active {
transform: scale(1);
opacity: 1;
.coil-container {
animation-duration: 3s;
}
.reactor-container-inner {
box-shadow: 0px 0px 70px 25px $colour3, inset 0px 0px 70px 25px $colour3;
}
.core-inner {
box-shadow: 0px 0px 15px 10px #52fefe, 0px 0px 20px 15px #52fefe inset;
}
.core-outer {
box-shadow: 0px 0px 5px 3px #52fefe, 0px 0px 15px 10px #52fefe inset;
}
.core-wrapper {
box-shadow: 0px 0px 10px 8px #52fefe, 0px 0px 10px 5px #52fefe inset;
}
.tunnel {
box-shadow: 0px 0px 10px 3px #52fefe, 0px 0px 10px 8px #52fefe inset;
}
.e7 {
opacity: 0.6;
}
.e5_1 { animation: rotate 3s linear infinite; }
.e5_2 { animation: rotate_anti 2s linear infinite; }
.e5_3 { animation: rotate 2s linear infinite; }
.e5_4 { animation: rotate_anti 2s linear infinite; }
.marks li {
animation: colour_ease2_active 1s infinite ease-in-out;
}
}
@keyframes colour_ease2_active {
0% { background: $marks-color-1; box-shadow: 0 0 5px $marks-color-1; }
50% { background: $marks-color-2; box-shadow: none; }
100% { background: $marks-color-1; box-shadow: 0 0 5px $marks-color-1; }
}
// [ Pulse animation for listening ]--
@keyframes listening-pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.03);
}
}
// [ State Label ]
.state-label {
margin-top: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: #52fefe;
box-shadow: 0 0 8px #52fefe;
}
.label-text {
font-size: 0.9rem;
color: rgba(82, 254, 254, 0.8);
text-transform: uppercase;
letter-spacing: 3px;
font-weight: 400;
font-family: "Roboto Condensed", sans-serif; // "Trebuchet MS", Arial, Helvetica, sans-serif
}
.reactor-container.active + .state-label {
.status-dot {
animation: dot-pulse 0.8s ease-in-out infinite;
}
.label-text {
color: #52fefe;
}
}
.reactor-container.disconnected + .state-label {
.status-dot {
background: #445555;
box-shadow: none;
}
.label-text {
color: #445555;
}
}
@keyframes dot-pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.5); opacity: 0.7; }
}
</style>

View file

@ -1,3 +1,37 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', 'Inter', -apple-system, sans-serif;
background-color: #0a1214;
color: #ffffff;
overflow: hidden;
background-color: darken($color: #090C10, $amount: 0.8)!important;
// background-image: url(../media/bg.png)!important;
background-repeat: no-repeat;
background-size: contain;
// background-attachment: fixed;
background-attachment: none;
}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
::-webkit-scrollbar-thumb {
background: rgba(82, 254, 254, 0.3);
border-radius: 3px;
}
// ### FONTS
$prim-font: "Roboto", sans-serif;
$sec-font: "Roboto Condensed", sans-serif;
@ -46,10 +80,9 @@ body {
justify-content: space-between;
.logo {
margin-top: 12px;
& > a > img {
width: 57px;
width: 55px;
display: inline-block;
transition: 0.5s opacity ease-in;
@ -60,8 +93,6 @@ body {
& > div {
display: inline-block;
margin-left: 13px;
margin-top: 8px;
vertical-align: top;
}
@ -95,6 +126,147 @@ body {
}
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
background: transparent;
}
.header-left {
display: flex;
align-items: center;
}
.logo {
display: flex;
align-items: center;
gap: 0.85rem;
}
.logo-image {
width: 54px;
height: 54px;
border-radius: 12px;
object-fit: cover;
}
.logo-text {
display: flex;
flex-direction: column;
}
.logo-title {
font-size: 1.5rem;
font-weight: 700;
color: #ffffff;
letter-spacing: -1px;
margin-top: 10px;
display: block;
& > #jarvis-logo {
display: block;
width: 100px;
height: 20px;
background: red;
background: url(../media/jarvis.png);
background-size: 80px;
background-repeat: no-repeat;
transition: all 0.3s ease;
&:hover {
background: url(../media/jarvis-hover.png);
background-size: 80px;
background-repeat: no-repeat;
border: none!important;
text-decoration: none;
}
}
}
.logo-version {
font-size: 0.7rem;
color: #aaa;
text-transform: uppercase;
font-style: italic;
letter-spacing: 1px;
font-weight: 500;
letter-spacing: 1px;
.v-badge {
color: #8AC832;
opacity: 0.9;
letter-spacing: 0;
font-weight: 600;
}
}
.header-right {
display: flex;
align-items: center;
gap: 0.6rem;
}
.header-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.55rem 0.85rem;
background: transparent;
border: none;
border-radius: 6px;
color: #ffffff;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 1.2px;
cursor: pointer;
font-weight: 600;
font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
opacity: 0.85;
&:hover {
opacity: 1;
background: rgba(35, 50, 55, 0.95);
border-color: rgba(255, 255, 255, 0.12);
}
}
.btn-icon {
opacity: 0.75;
}
.btn-badge {
padding: 0.25rem 0.3rem;
border-radius: 2px;
font-size: 0.7rem;
font-weight: 700;
color: #ffffff;
letter-spacing: 0;
text-shadow: 0px 0px 13.92px rgba(55, 55, 55, 0.54);
&.purple {
background-image: -moz-linear-gradient( -84deg, rgb(153,41,234) 0%, rgb(88,8,251) 100%);
background-image: -webkit-linear-gradient( -84deg, rgb(153,41,234) 0%, rgb(88,8,251) 100%);
background-image: -ms-linear-gradient( -84deg, rgb(153,41,234) 0%, rgb(88,8,251) 100%);
box-shadow: 0px 12px 14.56px 1.44px rgba(55, 55, 55, 0.15);
}
&.blue {
background: #3b82f6;
}
&.gray {
background: #333;
}
}
.beta-badge {
color: #8AC832;
opacity: 0.9;
font-size: 13px;
}
.top-menu {
vertical-align: top;
margin-top: 18px;
@ -165,33 +337,67 @@ body {
}
}
.h-divider {
position: relative;
z-index: 1;
}
// ### SEARCH BAR
.search {
display: block;
margin: 20px 0;
margin: 0;
text-align: center;
position: relative;
z-index: 1;
& > .h-divider {
margin: 10px 0!important;
background-position: left!important;
}
& > #search-form {
margin: 0;
position: relative;
}
& > form {
position: relative;
& > input {
width: 380px;
width: 97%;
height: 38px;
margin: 0 auto;
box-shadow: inset 0 0 5px 1px rgba(0, 0, 0, 0.6);
border: 1px solid rgba(6, 6, 6, 0.99);
background-color: #0f1012;
outline: none;
color: #d1d1d1;
font-family: $sec-font;
font-size: 17px;
font-weight: 600;
font-weight: 400;
line-height: 70.58px;
padding-left: 20px;
padding-left: 15px;
padding-right: 45px;
border-style: solid;
border-width: 3px;
border-color: rgba(6, 6, 6, 0.5);
background-color: rgba(25, 26, 28, 0.7);
opacity: 0.99;
box-shadow: inset 0px 0px 18.8px 1.2px rgba(0, 0, 0, 0.6);
&::placeholder {
color: #676767;
font-weight: 400;
font-weight: 300;
font-style: italic;
font-family: "Roboto Condensed", "Trebuchet MS", Arial, Helvetica, sans-serif;
opacity: 0.7;
font-size: 16px;
font-family: "Roboto Condensed";
color: rgb(103, 103, 103);
font-style: italic;
line-height: 4.437;
text-align: left;
}
&:focus + button + small {
@ -237,17 +443,19 @@ body {
font-family: $sec-font;
font-size: 14px;
font-weight: bold;
line-height: 1.7em;
top: 8px;
right: 75px;
line-height: 1.5em;
top: 9px;
right: 15px;
background-color: #d1d1d1;
color: #080c0f;
color: #161B1F;
padding: 0 7px;
padding-top: 2px;
border-radius: 4px;
padding-top: 0;
border-radius: 2px;
opacity: 0;
transition: opacity 0.3s;
cursor: default;
border: none;
text-transform: uppercase;
}
}
@ -258,6 +466,109 @@ body {
}
}
// ### MAIN
.search-section {
padding: 0.5rem 1.5rem;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
padding: 1rem;
}
.reactor-section {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
position: relative;
}
.reactor-wrapper {
transition: opacity 0.5s ease, filter 0.5s ease;
&.dimmed {
opacity: 0.15;
filter: grayscale(0.5);
}
}
.offline-badge {
display: flex;
align-items: center;
gap: 0.75rem;
position: absolute;
top: 52%;
left: 50%;
transform: translate(-50%, -50%);
margin-top: -3rem;
& > small {
display: block;
position: absolute;
top: 27px;
text-align: center;
width: 100%;
opacity: 0.7;
}
}
.offline-icon {
font-size: 1.3rem;
color: #fbbf24;
}
.offline-text {
color: #fbbf24;
font-size: 0.9rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 3px;
white-space: nowrap;
}
.start-button {
background: transparent;
border: 0 solid rgba(82, 254, 254, 0.4);
color: #52fefe;
padding: 0.75rem 2.5rem;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 2px;
cursor: pointer;
transition: all 0.3s ease;
border-radius: 4px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin-top: 2rem;
background: rgba(82, 254, 254, 0.1);
border-color: rgba(82, 254, 254, 0.7);
box-shadow: 0 0 20px rgba(82, 254, 254, 0.15);
&:hover:not(:disabled) {
background: rgba(82, 254, 254, 0.2);
border-color: rgba(82, 254, 254, 0.9);
box-shadow: 0 0 20px rgba(82, 254, 254, 0.25);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
// ### RESPONSIVE
@media (max-width: 1364px) {
#content > .inner > section.materials > header > h1 {

55
frontend/src/lib/i18n.ts Normal file
View file

@ -0,0 +1,55 @@
import { writable, derived } from "svelte/store"
import { invoke } from "@tauri-apps/api/core"
// stores
export const translations = writable<Record<string, string>>({})
export const currentLanguage = writable<string>("ru")
// simple helper function (not a store)
export function translate(translations: Record<string, string>, key: string, fallback?: string): string {
return translations[key] || fallback || key
}
// load translations from backend
export async function loadTranslations() {
try {
const [trans, lang] = await Promise.all([
invoke<Record<string, string>>("get_translations"),
invoke<string>("get_current_language")
])
translations.set(trans)
currentLanguage.set(lang)
} catch (err) {
console.error("Failed to load translations:", err)
}
}
// change language
export async function setLanguage(lang: string) {
try {
const newTranslations = await invoke<Record<string, string>>("set_language", { lang })
translations.set(newTranslations)
currentLanguage.set(lang)
} catch (err) {
console.error("Failed to set language:", err)
}
}
export async function loadLanguage() {
try {
const lang = await invoke<string>("db_read", { key: "language" })
if (lang) {
currentLanguage.set(lang)
}
} catch (err) {
console.error("Failed to load language:", err)
}
}
export async function getSupportedLanguages(): Promise<string[]> {
try {
return await invoke<string[]>("get_supported_languages")
} catch {
return ["ru", "en", "ua"]
}
}

View file

@ -4,7 +4,9 @@
import HDivider from "@/components/elements/HDivider.svelte"
import Footer from "@/components/Footer.svelte"
import { appInfo } from "@/stores"
import { appInfo, translations, translate } from "@/stores"
$: t = (key: string) => translate($translations, key)
let tgLink = ""
appInfo.subscribe(info => {
@ -15,13 +17,13 @@
<Space h="xl" />
<Notification
title="[404] Этот раздел еще находится в разработке!"
title={t('commands-wip-title')}
icon={InfoCircled}
color="blue"
withCloseButton={false}
>
Тут будет список команд + полноценный редактор команд.<br />
Следите за обновлениями в <a href={tgLink} target="_blank">нашем телеграм канале</a>!
{t('commands-wip-desc')}<br />
{t('commands-wip-follow')} <a href={tgLink} target="_blank">{t('commands-wip-channel')}</a>!
</Notification>
<div class="placeholder-image">
@ -36,4 +38,4 @@
text-align: center;
margin-top: 25px;
}
</style>
</style>

View file

@ -1,46 +1,41 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte"
import { invoke } from "@tauri-apps/api/core"
import { Notification, Space, Button } from "@svelteuidev/core"
import { InfoCircled } from "radix-icons-svelte"
import SearchBar from "@/components/elements/SearchBar.svelte"
import ArcReactor from "@/components/elements/ArcReactor.svelte"
import HDivider from "@/components/elements/HDivider.svelte"
import Stats from "@/components/elements/Stats.svelte"
import Footer from "@/components/Footer.svelte"
import {
isJarvisRunning,
updateJarvisStats,
jarvisState,
ipcConnected,
enableIpc,
disableIpc
disableIpc,
translate,
translations
} from "@/stores"
$: t = (key: string) => translate($translations, key)
let processRunning = false
let launching = false
// when process state changes, enable/disable IPC
isJarvisRunning.subscribe((value) => {
processRunning = value
if (value) {
// process is running, enable IPC connection
enableIpc()
} else {
// process stopped, disable IPC (stops reconnect attempts)
disableIpc()
}
})
onMount(() => {
document.body.classList.add("assist-page")
updateJarvisStats()
})
onDestroy(() => {
document.body.classList.remove("assist-page")
disableIpc()
})
@ -48,8 +43,6 @@
launching = true
try {
await invoke("run_jarvis_app")
// wait for startup
setTimeout(async () => {
await updateJarvisStats()
launching = false
@ -61,46 +54,35 @@
}
</script>
<HDivider />
<div class="app-container assist-page">
{#if !processRunning}
<Notification
title="Внимание!"
icon={InfoCircled}
color="cyan"
withCloseButton={false}
>
В данный момент ассистент не запущен.<br />
Но вы всё еще можете изменять его настройки.<br />
<br />
<div class="search search-section">
<HDivider />
<SearchBar />
</div>
<Button
color="lime"
radius="md"
size="sm"
uppercase
ripple
fullSize
on:click={runAssistant}
disabled={launching}
>
{launching ? "Запуск..." : "Запустить"}
</Button>
</Notification>
{:else}
<ArcReactor />
<div class="reactor-section">
<div class="reactor-wrapper" class:dimmed={!processRunning}>
<ArcReactor />
</div>
{#if !processRunning}
<div class="offline-badge">
<span class="offline-icon"></span>
<span class="offline-text">{t('assistant-not-running')}</span>
<small>{t('assistant-offline-hint')}</small>
</div>
<button
class="start-button"
on:click={runAssistant}
disabled={launching}
>
{launching ? t('btn-starting') : t('btn-start')}
</button>
{/if}
</div>
{#if !$ipcConnected}
<Notification
title="Подключение..."
color="yellow"
withCloseButton={false}
>
Устанавливается связь с ассистентом...
</Notification>
{/if}
{/if}
<HDivider noMargin />
<Stats />
<Footer />
<HDivider noMargin />
<Stats />
<Footer />
</div>

View file

@ -5,7 +5,7 @@
import { setTimeout } from "worker-timers"
import { showInExplorer } from "@/functions"
import { appInfo, assistantVoice } from "@/stores"
import { appInfo, assistantVoice, translations, translate } from "@/stores"
import HDivider from "@/components/elements/HDivider.svelte"
import Footer from "@/components/Footer.svelte"
@ -33,6 +33,8 @@
CrossCircled
} from "radix-icons-svelte"
$: t = (key: string) => translate($translations, key)
// ### STATE
interface MicrophoneOption {
label: string
@ -164,13 +166,13 @@
<Space h="xl" />
<Notification
title="БЕТА версия!"
title={t('settings-beta-title')}
icon={QuestionMarkCircled}
color="blue"
withCloseButton={false}
>
Часть функций может работать некорректно.<br />
Сообщайте обо всех найденных багах в <a href={feedbackLink} target="_blank">наш телеграм бот</a>.
{t('settings-beta-desc')}<br />
{t('settings-beta-feedback')} <a href={feedbackLink} target="_blank">{t('settings-beta-bot')}</a>.
<Space h="sm" />
<Button
color="gray"
@ -179,7 +181,7 @@
uppercase
on:click={() => showInExplorer(logFilePath)}
>
Открыть папку с логами
{t('settings-open-logs')}
</Button>
</Notification>
@ -187,7 +189,7 @@
{#if settingsSaved}
<Notification
title="Настройки сохранены!"
title={t('notification-saved')}
icon={Check}
color="teal"
on:close={() => { settingsSaved = false }}
@ -196,8 +198,7 @@
{/if}
<Tabs class="form" color="#8AC832" position="left">
<!-- general tab -->
<Tabs.Tab label="Общее" icon={Gear}>
<Tabs.Tab label={t('settings-general')} icon={Gear}>
<Space h="sm" />
<NativeSelect
data={[
@ -206,61 +207,58 @@
{ label: "Jarvis (от Хауди)", value: "jarvis-howdy" },
{ label: "Jarvis OG (из фильмов)", value: "jarvis-og" }
]}
label="Голос ассистента"
description="Не все команды работают со всеми звуковыми пакетами."
label={t('settings-voice')}
description={t('settings-voice-desc')}
variant="filled"
bind:value={voiceVal}
/>
</Tabs.Tab>
<!-- devices tab -->
<Tabs.Tab label="Устройства" icon={Mix}>
<Tabs.Tab label={t('settings-devices')} icon={Mix}>
<Space h="sm" />
<NativeSelect
data={availableMicrophones}
label="Выберите микрофон"
description="Его будет слушать ассистент."
label={t('settings-microphone')}
description={t('settings-microphone-desc')}
variant="filled"
bind:value={selectedMicrophone}
/>
</Tabs.Tab>
<!-- neural networks tab -->
<Tabs.Tab label="Нейросети" icon={Cube}>
<Tabs.Tab label={t('settings-neural-networks')} icon={Cube}>
<Space h="sm" />
<NativeSelect
data={[
{ label: "Rustpotter", value: "Rustpotter" },
{ label: "Vosk (медленный)", value: "Vosk" },
{ label: "Picovoice Porcupine (требует API ключ)", value: "Picovoice" }
{ label: "Vosk", value: "Vosk" },
{ label: "Picovoice Porcupine", value: "Picovoice" }
]}
label="Распознавание активационной фразы (Wake Word)"
description="Выберите, какая нейросеть будет отвечать за распознавание активационной фразы."
label={t('settings-wake-word-engine')}
description={t('settings-wake-word-desc')}
variant="filled"
bind:value={selectedWakeWordEngine}
/>
{#if selectedWakeWordEngine === "picovoice"}
<Space h="sm" />
<Alert title="Внимание!" color="#868E96" variant="outline">
<Alert title={t('settings-attention')} color="#868E96" variant="outline">
<Notification
title="Эта нейросеть работает не у всех!"
title={t('settings-picovoice-warning')}
icon={CrossCircled}
color="orange"
withCloseButton={false}
>
Мы ждем официального патча от разработчиков.
{t('settings-picovoice-waiting')}
</Notification>
<Space h="sm" />
<Text size="sm" color="gray">
Введите сюда свой ключ Picovoice.<br />
Он выдается бесплатно при регистрации в
{t('settings-picovoice-key-desc')}
<a href="https://console.picovoice.ai/" target="_blank">Picovoice Console</a>.
</Text>
<Space h="sm" />
<Input
icon={Code}
placeholder="Ключ Picovoice"
placeholder={t('settings-picovoice-key')}
variant="filled"
autocomplete="off"
bind:value={apiKeyPicovoice}
@ -272,11 +270,11 @@
{#key availableVoskModels}
<NativeSelect
data={[
{ label: "Авто-определение", value: "" },
{ label: t('settings-auto-detect'), value: "" },
...availableVoskModels
]}
label="Модель распознавания речи (Vosk)"
description="Выберите модель Vosk для распознавания речи."
label={t('settings-vosk-model')}
description={t('settings-vosk-model-desc')}
variant="filled"
bind:value={selectedVoskModel}
/>
@ -284,9 +282,9 @@
{#if availableVoskModels.length === 0}
<Space h="sm" />
<Alert title="Модели не найдены" color="orange" variant="outline">
<Alert title={t('settings-models-not-found')} color="orange" variant="outline">
<Text size="sm" color="gray">
Поместите модели Vosk в папку resources/vosk
{t('settings-models-hint')}
</Text>
</Alert>
{/if}
@ -297,8 +295,8 @@
{ label: "Intent Classifier", value: "IntentClassifier" },
{ label: "Rasa", value: "Rasa" }
]}
label="Распознавание команд (Intent Recognition)"
description="Выберите, какая нейросеть будет отвечать за распознавание команд."
label={t('settings-intent-engine')}
description={t('settings-intent-engine-desc')}
variant="filled"
bind:value={selectedIntentRecognitionEngine}
/>
@ -307,11 +305,11 @@
<NativeSelect
data={[
{ label: "Отключено", value: "None" },
{ label: t('settings-disabled'), value: "None" },
{ label: "Nnnoiseless", value: "Nnnoiseless" }
]}
label="Шумоподавление"
description="Уменьшает фоновый шум. Может ухудшить распознавание в некоторых случаях."
label={t('settings-noise-suppression')}
description={t('settings-noise-suppression-desc')}
variant="filled"
bind:value={selectedNoiseSuppression}
/>
@ -320,40 +318,39 @@
<NativeSelect
data={[
{ label: "Отключено", value: "None" },
{ label: "Energy (простой)", value: "Energy" },
{ label: "Nnnoiseless (нейросеть)", value: "Nnnoiseless" }
{ label: t('settings-disabled'), value: "None" },
{ label: "Energy", value: "Energy" },
{ label: "Nnnoiseless", value: "Nnnoiseless" }
]}
label="Определение голосой активности (VAD)"
description="Пропускает тишину, экономит ресурсы CPU."
label={t('settings-vad')}
description={t('settings-vad-desc')}
variant="filled"
bind:value={selectedVad}
/>
<Space h="md" />
<InputWrapper label="Нормализация громкости">
<InputWrapper label={t('settings-gain-normalizer')}>
<Text size="sm" color="gray">
Автоматически регулирует уровень громкости.
{t('settings-gain-normalizer-desc')}
</Text>
<Space h="xs" />
<Switch
label={gainNormalizerEnabled ? "Включено" : "Выключено"}
label={gainNormalizerEnabled ? t('settings-enabled') : t('settings-disabled')}
bind:checked={gainNormalizerEnabled}
/>
</InputWrapper>
<Space h="xl" />
<InputWrapper label="Ключ OpenAI">
<InputWrapper label={t('settings-openai-key')}>
<Text size="sm" color="gray">
В данный момент ChatGPT <u>не поддерживается</u>.
Он будет добавлен в ближайших обновлениях.
{t('settings-openai-not-supported')}
</Text>
<Space h="sm" />
<Input
icon={Code}
placeholder="Ключ OpenAI"
placeholder={t('settings-openai-key')}
variant="filled"
autocomplete="off"
bind:value={apiKeyOpenai}
@ -375,7 +372,7 @@
on:click={saveSettings}
disabled={saveButtonDisabled}
>
Сохранить
{t('settings-save')}
</Button>
<Space h="sm" />
@ -388,7 +385,7 @@
fullSize
on:click={() => $goto("/")}
>
Назад
{t('settings-back')}
</Button>
<HDivider />

View file

@ -17,6 +17,17 @@ export {
reloadCommands
} from "./lib/ipc"
// re-export i18n
export {
translations,
currentLanguage,
translate,
loadTranslations,
setLanguage,
loadLanguage,
getSupportedLanguages
} from "./lib/i18n"
// ### RUNNING STATE
export const isJarvisRunning = writable(false)
export const jarvisRamUsage = writable(0)
@ -30,6 +41,8 @@ export const appInfo = writable({
tgOfficialLink: "",
feedbackLink: "",
repositoryLink: "",
boostySupportLink: "",
patreonSupportLink: "",
logFilePath: ""
})
@ -45,10 +58,12 @@ export async function loadVoiceSetting() {
export async function loadAppInfo() {
try {
const [tg, feedback, repo, logPath] = await Promise.all([
const [tg, feedback, repo, boosty, patreon, logPath] = await Promise.all([
invoke<string>("get_tg_official_link"),
invoke<string>("get_feedback_link"),
invoke<string>("get_repository_link"),
invoke<string>("get_boosty_link"),
invoke<string>("get_patreon_link"),
invoke<string>("get_log_file_path")
])
@ -56,6 +71,8 @@ export async function loadAppInfo() {
tgOfficialLink: tg,
feedbackLink: feedback,
repositoryLink: repo,
boostySupportLink: boosty,
patreonSupportLink: patreon,
logFilePath: logPath
})
} catch (err) {