Multilingual support via Fluent + Frontend improvements + Rewrite of ArcReactor
108
Cargo.lock
generated
@@ -1768,6 +1768,51 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fluent"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8137a6d5a2c50d6b0ebfcb9aaa91a28154e0a70605f112d30cb0cd4a78670477"
|
||||
dependencies = [
|
||||
"fluent-bundle",
|
||||
"unic-langid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fluent-bundle"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4"
|
||||
dependencies = [
|
||||
"fluent-langneg",
|
||||
"fluent-syntax",
|
||||
"intl-memoizer",
|
||||
"intl_pluralrules",
|
||||
"rustc-hash",
|
||||
"self_cell",
|
||||
"smallvec",
|
||||
"unic-langid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fluent-langneg"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0"
|
||||
dependencies = [
|
||||
"unic-langid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fluent-syntax"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@@ -2968,6 +3013,25 @@ dependencies = [
|
||||
"syn 2.0.113",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "intl-memoizer"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f"
|
||||
dependencies = [
|
||||
"type-map",
|
||||
"unic-langid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "intl_pluralrules"
|
||||
version = "7.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972"
|
||||
dependencies = [
|
||||
"unic-langid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.11.0"
|
||||
@@ -3059,6 +3123,8 @@ dependencies = [
|
||||
name = "jarvis-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"fluent",
|
||||
"fluent-bundle",
|
||||
"futures-util",
|
||||
"hound",
|
||||
"intent-classifier",
|
||||
@@ -3080,6 +3146,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"toml 0.9.10+spec-1.1.0",
|
||||
"unic-langid",
|
||||
"vosk",
|
||||
]
|
||||
|
||||
@@ -5338,6 +5405,12 @@ dependencies = [
|
||||
"realfft",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -5526,6 +5599,12 @@ dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "self_cell"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
@@ -6826,6 +6905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"serde_core",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
@@ -7125,6 +7205,15 @@ dependencies = [
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "type-map"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90"
|
||||
dependencies = [
|
||||
"rustc-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.3"
|
||||
@@ -7190,6 +7279,24 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
|
||||
|
||||
[[package]]
|
||||
name = "unic-langid"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05"
|
||||
dependencies = [
|
||||
"unic-langid-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-langid-impl"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658"
|
||||
dependencies = [
|
||||
"tinystr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-ucd-ident"
|
||||
version = "0.9.0"
|
||||
@@ -8509,6 +8616,7 @@ version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"yoke 0.8.1",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
|
||||
@@ -36,4 +36,7 @@ sha2 = "0.10"
|
||||
nnnoiseless = "0.5"
|
||||
sysinfo = "0.37.2"
|
||||
tokio-tungstenite = "0.28.0"
|
||||
futures-util = "0.3"
|
||||
futures-util = "0.3"
|
||||
fluent = "0.17.0"
|
||||
fluent-bundle = "0.16.0"
|
||||
unic-langid = "0.9"
|
||||
@@ -6,6 +6,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use jarvis_core::{
|
||||
audio, audio_processing, commands, config, db, listener, recorder, stt, intent,
|
||||
ipc::{self, IpcAction},
|
||||
i18n,
|
||||
APP_CONFIG_DIR, APP_LOG_DIR, COMMANDS_LIST, DB,
|
||||
};
|
||||
|
||||
@@ -40,6 +41,9 @@ fn main() -> Result<(), String> {
|
||||
DB.set(Arc::new(RwLock::new(db::init_settings())))
|
||||
.expect("DB already initialized");
|
||||
|
||||
// init i18n
|
||||
i18n::init(&DB.get().unwrap().read().language);
|
||||
|
||||
// initialize tray
|
||||
// @TODO. macOS currently not supported for tray functionality,
|
||||
// due to the separate thread in which tray processing works,
|
||||
|
||||
@@ -10,7 +10,7 @@ use image;
|
||||
#[cfg(target_os="windows")]
|
||||
use winit::platform::windows::EventLoopBuilderExtWindows;
|
||||
|
||||
use jarvis_core::config;
|
||||
use jarvis_core::{config, i18n};
|
||||
|
||||
const TRAY_ICON_BYTES: &[u8] = include_bytes!("../../../resources/icons/32x32.png");
|
||||
|
||||
@@ -21,22 +21,22 @@ pub fn init_blocking() {
|
||||
let icon = load_icon_from_bytes(TRAY_ICON_BYTES);
|
||||
|
||||
// form tray menu
|
||||
let tray_menu = Menu::with_items(&[
|
||||
&MenuItem::new("Перезапуск", true, None),
|
||||
&MenuItem::new("Настройки", true, None),
|
||||
&MenuItem::new("Выход", true, None),
|
||||
])
|
||||
.unwrap();
|
||||
// let tray_menu = Menu::with_items(&[
|
||||
// &MenuItem::new("Перезапуск", true, None),
|
||||
// &MenuItem::new("Настройки", true, None),
|
||||
// &MenuItem::new("Выход", true, None),
|
||||
// ])
|
||||
// .unwrap();
|
||||
|
||||
let tray_menu = Menu::with_items(&[
|
||||
&MenuItem::with_id("restart", "Перезапуск", true, None),
|
||||
&MenuItem::with_id("settings", "Настройки", true, None),
|
||||
&MenuItem::with_id("exit", "Выход", true, None),
|
||||
&MenuItem::with_id("restart", i18n::t("tray-restart"), true, None),
|
||||
&MenuItem::with_id("settings", i18n::t("tray-settings"), true, None),
|
||||
&MenuItem::with_id("exit", i18n::t("tray-exit"), true, None),
|
||||
]).unwrap();
|
||||
|
||||
let _tray_icon = TrayIconBuilder::new()
|
||||
.with_menu(Box::new(tray_menu))
|
||||
.with_tooltip(config::TRAY_TOOLTIP)
|
||||
.with_tooltip(i18n::t("tray-tooltip"))
|
||||
.with_icon(icon)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
@@ -26,6 +26,9 @@ sha2.workspace = true
|
||||
nnnoiseless = { workspace = true, optional = true }
|
||||
tokio-tungstenite = { workspace = true, optional = true }
|
||||
futures-util = { workspace = true, optional = true }
|
||||
fluent.workspace = true
|
||||
fluent-bundle.workspace = true
|
||||
unic-langid.workspace = true
|
||||
|
||||
# pv_recorder = { workspace = true, optional = true }
|
||||
vosk = { version = "0.3.1", optional = true }
|
||||
|
||||
@@ -80,6 +80,8 @@ pub const AUTHOR_NAME: Option<&str> = option_env!("CARGO_PKG_AUTHORS");
|
||||
pub const REPOSITORY_LINK: Option<&str> = option_env!("CARGO_PKG_REPOSITORY");
|
||||
pub const TG_OFFICIAL_LINK: Option<&str> = Some("https://t.me/howdyho_official");
|
||||
pub const FEEDBACK_LINK: Option<&str> = Some("https://t.me/jarvis_feedback_bot");
|
||||
pub const SUPPORT_BOOSTY_LINK: Option<&str> = Some("https://boosty.to/howdyho");
|
||||
pub const SUPPORT_PATREON_LINK: Option<&str> = Some("https://www.patreon.com/user?u=22843414");
|
||||
|
||||
/*
|
||||
Tray.
|
||||
|
||||
@@ -22,6 +22,8 @@ pub struct Settings {
|
||||
pub vad: VadBackend,
|
||||
pub gain_normalizer: bool,
|
||||
|
||||
pub language: String,
|
||||
|
||||
pub api_keys: ApiKeys,
|
||||
}
|
||||
|
||||
@@ -41,6 +43,8 @@ impl Default for Settings {
|
||||
vad: config::DEFAULT_VAD,
|
||||
gain_normalizer: config::DEFAULT_GAIN_NORMALIZER,
|
||||
|
||||
language: String::from("ru"),
|
||||
|
||||
api_keys: ApiKeys {
|
||||
picovoice: String::from(""),
|
||||
openai: String::from(""),
|
||||
|
||||
176
crates/jarvis-core/src/i18n.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
use fluent_bundle::{FluentBundle, FluentResource, FluentArgs, FluentValue};
|
||||
use fluent_bundle::concurrent::FluentBundle as ConcurrentFluentBundle;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::HashMap;
|
||||
use unic_langid::LanguageIdentifier;
|
||||
|
||||
// locale files embedded at compile time
|
||||
const LOCALE_RU: &str = include_str!("i18n/locales/ru.ftl");
|
||||
const LOCALE_EN: &str = include_str!("i18n/locales/en.ftl");
|
||||
const LOCALE_UA: &str = include_str!("i18n/locales/ua.ftl");
|
||||
|
||||
pub const SUPPORTED_LANGUAGES: &[&str] = &["ru", "en", "ua"];
|
||||
pub const DEFAULT_LANGUAGE: &str = "ru";
|
||||
|
||||
// use concurrent bundle (thread-safe)
|
||||
type Bundle = ConcurrentFluentBundle<FluentResource>;
|
||||
|
||||
static BUNDLES: OnceCell<HashMap<String, Bundle>> = OnceCell::new();
|
||||
static CURRENT_LANG: OnceCell<RwLock<String>> = OnceCell::new();
|
||||
|
||||
// Initialize i18n system
|
||||
pub fn init(lang: &str) {
|
||||
let bundles = create_bundles();
|
||||
BUNDLES.set(bundles).ok();
|
||||
|
||||
let lang = if SUPPORTED_LANGUAGES.contains(&lang) { lang } else { DEFAULT_LANGUAGE };
|
||||
CURRENT_LANG.set(RwLock::new(lang.to_string())).ok();
|
||||
|
||||
info!("i18n initialized with language: {}", lang);
|
||||
}
|
||||
|
||||
fn create_bundles() -> HashMap<String, Bundle> {
|
||||
let mut bundles = HashMap::new();
|
||||
|
||||
bundles.insert("ru".to_string(), create_bundle("ru", LOCALE_RU));
|
||||
bundles.insert("en".to_string(), create_bundle("en", LOCALE_EN));
|
||||
bundles.insert("ua".to_string(), create_bundle("ua", LOCALE_UA));
|
||||
|
||||
bundles
|
||||
}
|
||||
|
||||
fn create_bundle(lang: &str, source: &str) -> Bundle {
|
||||
let langid: LanguageIdentifier = lang.parse().expect("Invalid language identifier");
|
||||
let mut bundle = ConcurrentFluentBundle::new_concurrent(vec![langid]);
|
||||
|
||||
let resource = FluentResource::try_new(source.to_string())
|
||||
.expect("Failed to parse FTL resource");
|
||||
|
||||
bundle.add_resource(resource).expect("Failed to add resource");
|
||||
bundle
|
||||
}
|
||||
|
||||
// Set current language
|
||||
pub fn set_language(lang: &str) {
|
||||
if let Some(current) = CURRENT_LANG.get() {
|
||||
let lang = if SUPPORTED_LANGUAGES.contains(&lang) { lang } else { DEFAULT_LANGUAGE };
|
||||
*current.write() = lang.to_string();
|
||||
info!("Language changed to: {}", lang);
|
||||
}
|
||||
}
|
||||
|
||||
// Get current language
|
||||
pub fn get_language() -> String {
|
||||
CURRENT_LANG.get()
|
||||
.map(|l| l.read().clone())
|
||||
.unwrap_or_else(|| DEFAULT_LANGUAGE.to_string())
|
||||
}
|
||||
|
||||
// Translate a key
|
||||
pub fn t(key: &str) -> String {
|
||||
t_with_args(key, None)
|
||||
}
|
||||
|
||||
// Translate a key with arguments
|
||||
pub fn t_with_args(key: &str, args: Option<&FluentArgs>) -> String {
|
||||
let lang = get_language();
|
||||
|
||||
let bundles = match BUNDLES.get() {
|
||||
Some(b) => b,
|
||||
None => return key.to_string(),
|
||||
};
|
||||
|
||||
let bundle = match bundles.get(&lang) {
|
||||
Some(b) => b,
|
||||
None => bundles.get(DEFAULT_LANGUAGE).unwrap(),
|
||||
};
|
||||
|
||||
let msg = match bundle.get_message(key) {
|
||||
Some(m) => m,
|
||||
None => return key.to_string(),
|
||||
};
|
||||
|
||||
let pattern = match msg.value() {
|
||||
Some(p) => p,
|
||||
None => return key.to_string(),
|
||||
};
|
||||
|
||||
let mut errors = vec![];
|
||||
let result = bundle.format_pattern(pattern, args, &mut errors);
|
||||
|
||||
if !errors.is_empty() {
|
||||
warn!("i18n errors for key '{}': {:?}", key, errors);
|
||||
}
|
||||
|
||||
result.to_string()
|
||||
}
|
||||
|
||||
// Translate with a single argument
|
||||
pub fn t_arg(key: &str, arg_name: &str, arg_value: &str) -> String {
|
||||
let mut args = FluentArgs::new();
|
||||
args.set(arg_name, FluentValue::from(arg_value));
|
||||
t_with_args(key, Some(&args))
|
||||
}
|
||||
|
||||
// Translate with numeric argument
|
||||
pub fn t_count(key: &str, count: i64) -> String {
|
||||
let mut args = FluentArgs::new();
|
||||
args.set("count", FluentValue::from(count));
|
||||
t_with_args(key, Some(&args))
|
||||
}
|
||||
|
||||
// Get all translations for current language (for frontend)
|
||||
pub fn get_all_translations() -> HashMap<String, String> {
|
||||
let lang = get_language();
|
||||
get_translations_for(&lang)
|
||||
}
|
||||
|
||||
/// Get all translations for a specific language
|
||||
pub fn get_translations_for(lang: &str) -> HashMap<String, String> {
|
||||
let mut result = HashMap::new();
|
||||
|
||||
let bundles = match BUNDLES.get() {
|
||||
Some(b) => b,
|
||||
None => return result,
|
||||
};
|
||||
|
||||
let bundle = match bundles.get(lang) {
|
||||
Some(b) => b,
|
||||
None => match bundles.get(DEFAULT_LANGUAGE) {
|
||||
Some(b) => b,
|
||||
None => return result,
|
||||
},
|
||||
};
|
||||
|
||||
// get source for this language and extract all keys
|
||||
let source = match lang {
|
||||
"ru" => LOCALE_RU,
|
||||
"en" => LOCALE_EN,
|
||||
"ua" => LOCALE_UA,
|
||||
_ => LOCALE_RU,
|
||||
};
|
||||
|
||||
// parse keys from FTL source (lines that have "=" and don't start with "#" or "-")
|
||||
for line in source.lines() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') || line.starts_with('-') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(key) = line.split('=').next() {
|
||||
let key = key.trim();
|
||||
if !key.is_empty() && !key.contains(' ') {
|
||||
if let Some(msg) = bundle.get_message(key) {
|
||||
if let Some(pattern) = msg.value() {
|
||||
let mut errors = vec![];
|
||||
let value = bundle.format_pattern(pattern, None, &mut errors);
|
||||
result.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
119
crates/jarvis-core/src/i18n/locales/en.ftl
Normal file
@@ -0,0 +1,119 @@
|
||||
# ### APP INFO
|
||||
app-name = JARVIS
|
||||
app-description = Voice Assistant
|
||||
|
||||
# ### TRAY MENU
|
||||
tray-restart = Restart
|
||||
tray-settings = Settings
|
||||
tray-exit = Exit
|
||||
tray-tooltip = JARVIS - Voice Assistant
|
||||
|
||||
# ### HEADER
|
||||
header-commands = COMMANDS
|
||||
header-settings = SETTINGS
|
||||
|
||||
# ### SEARCH
|
||||
search-placeholder = Enter a command manually or say «Jarvis» ...
|
||||
|
||||
# ### MAIN PAGE
|
||||
assistant-not-running = ASSISTANT NOT RUNNING
|
||||
assistant-offline-hint = You can configure it without starting.
|
||||
btn-start = START
|
||||
btn-starting = STARTING...
|
||||
|
||||
# ### STATUS
|
||||
status-disconnected = Disconnected
|
||||
status-standby = Standby
|
||||
status-listening = Listening...
|
||||
status-processing = Processing...
|
||||
|
||||
# ### STATS
|
||||
stats-microphone = MICROPHONE
|
||||
stats-neural-networks = NEURAL NETWORKS
|
||||
stats-resources = RESOURCES
|
||||
stats-system-default = System Default
|
||||
stats-not-selected = Not selected
|
||||
stats-loading = Loading...
|
||||
|
||||
# ### FOOTER
|
||||
footer-author = Project author
|
||||
footer-telegram = Our Telegram channel
|
||||
footer-github = Github repository
|
||||
footer-support = Support the project on
|
||||
|
||||
# ### SETTINGS
|
||||
settings-title = Settings
|
||||
settings-general = General
|
||||
settings-devices = Devices
|
||||
settings-neural-networks = Neural Networks
|
||||
settings-audio = Audio
|
||||
settings-recognition = Recognition
|
||||
settings-about = About
|
||||
settings-language = Language
|
||||
settings-microphone = Microphone
|
||||
settings-microphone-desc = The assistant will listen to this microphone.
|
||||
settings-mic-default = Default (System)
|
||||
settings-voice = Assistant voice
|
||||
settings-voice-desc = Not all commands work with all sound packs.
|
||||
settings-wake-word-engine = Wake word engine
|
||||
settings-wake-word-desc = Choose the engine for wake word recognition.
|
||||
settings-stt-engine = Speech recognition
|
||||
settings-intent-engine = Intent recognition
|
||||
settings-intent-engine-desc = Select neural network for command recognition.
|
||||
settings-noise-suppression = Noise suppression
|
||||
settings-noise-suppression-desc = Reduces background noise.
|
||||
settings-vad = Voice detection (VAD)
|
||||
settings-vad-desc = Skips silence, saves CPU resources.
|
||||
settings-gain-normalizer = Gain normalizer
|
||||
settings-gain-normalizer-desc = Automatically adjusts volume level.
|
||||
settings-api-keys = API Keys
|
||||
settings-save = Save
|
||||
settings-cancel = Cancel
|
||||
settings-back = Back
|
||||
settings-enabled = Enabled
|
||||
settings-disabled = Disabled
|
||||
|
||||
# settings - beta notice
|
||||
settings-beta-title = BETA version!
|
||||
settings-beta-desc = Some features may not work correctly.
|
||||
settings-beta-feedback = Report all bugs to
|
||||
settings-beta-bot = our Telegram bot
|
||||
settings-open-logs = Open logs folder
|
||||
|
||||
# settings - picovoice
|
||||
settings-attention = Attention!
|
||||
settings-picovoice-warning = This neural network doesn't work for everyone!
|
||||
settings-picovoice-waiting = We are waiting for an official patch from the developers.
|
||||
settings-picovoice-key-desc = Enter your Picovoice key here. It is issued for free upon registration at
|
||||
settings-picovoice-key = Picovoice Key
|
||||
|
||||
# settings - vosk
|
||||
settings-auto-detect = Auto-detect
|
||||
settings-vosk-model = Speech recognition model (Vosk)
|
||||
settings-vosk-model-desc = Select Vosk model for speech recognition.
|
||||
settings-models-not-found = Models not found
|
||||
settings-models-hint = Place Vosk models in resources/vosk folder
|
||||
|
||||
# settings - openai
|
||||
settings-openai-key = OpenAI Key
|
||||
settings-openai-not-supported = ChatGPT is not currently supported. It will be added in future updates.
|
||||
|
||||
# ### COMMANDS PAGE
|
||||
commands-title = Commands
|
||||
commands-search = Search commands...
|
||||
commands-count = { $count } commands
|
||||
commands-wip-title = [404] This section is under development!
|
||||
commands-wip-desc = Here will be a list of commands + full-featured command editor.
|
||||
commands-wip-follow = Follow updates in
|
||||
commands-wip-channel = our Telegram channel
|
||||
|
||||
# ### ERRORS
|
||||
error-generic = An error occurred
|
||||
error-connection = Connection error
|
||||
error-not-found = Not found
|
||||
|
||||
# ### NOTIFICATIONS
|
||||
notification-saved = Settings saved!
|
||||
notification-error = Error
|
||||
notification-assistant-started = Assistant started
|
||||
notification-assistant-stopped = Assistant stopped
|
||||
119
crates/jarvis-core/src/i18n/locales/ru.ftl
Normal file
@@ -0,0 +1,119 @@
|
||||
# ### APP INFO
|
||||
app-name = JARVIS
|
||||
app-description = Голосовой ассистент
|
||||
|
||||
# ### TRAY MENU
|
||||
tray-restart = Перезапустить
|
||||
tray-settings = Настройки
|
||||
tray-exit = Выход
|
||||
tray-tooltip = JARVIS - Голосовой ассистент
|
||||
|
||||
# ### HEADER
|
||||
header-commands = КОМАНДЫ
|
||||
header-settings = НАСТРОЙКИ
|
||||
|
||||
# ### SEARCH
|
||||
search-placeholder = Введите команду вручную или произнесите «Джарвис» ...
|
||||
|
||||
# ### MAIN PAGE
|
||||
assistant-not-running = АССИСТЕНТ НЕ ЗАПУЩЕН
|
||||
assistant-offline-hint = Настроить его можно не запуская.
|
||||
btn-start = ЗАПУСТИТЬ
|
||||
btn-starting = ЗАПУСК...
|
||||
|
||||
# ### STATUS
|
||||
status-disconnected = Отключен
|
||||
status-standby = Ожидание
|
||||
status-listening = Слушаю...
|
||||
status-processing = Обработка...
|
||||
|
||||
# ### STATS
|
||||
stats-microphone = МИКРОФОН
|
||||
stats-neural-networks = НЕЙРОСЕТИ
|
||||
stats-resources = РЕСУРСЫ
|
||||
stats-system-default = Системный
|
||||
stats-not-selected = Не выбран
|
||||
stats-loading = Загрузка...
|
||||
|
||||
# ### FOOTER
|
||||
footer-author = Автор проекта
|
||||
footer-telegram = Наш телеграм канал
|
||||
footer-github = Github репозиторий проекта
|
||||
footer-support = Поддержать проект на
|
||||
|
||||
# ### SETTINGS
|
||||
settings-title = Настройки
|
||||
settings-general = Основные
|
||||
settings-devices = Устройства
|
||||
settings-neural-networks = Нейросети
|
||||
settings-audio = Аудио
|
||||
settings-recognition = Распознавание
|
||||
settings-about = О программе
|
||||
settings-language = Язык
|
||||
settings-microphone = Микрофон
|
||||
settings-microphone-desc = Его будет слушать ассистент.
|
||||
settings-mic-default = По умолчанию (Система)
|
||||
settings-voice = Голос ассистента
|
||||
settings-voice-desc = Не все команды работают со всеми звуковыми пакетами.
|
||||
settings-wake-word-engine = Движок активации
|
||||
settings-wake-word-desc = Выберите нейросеть для распознавания активационной фразы.
|
||||
settings-stt-engine = Распознавание речи
|
||||
settings-intent-engine = Определение намерения
|
||||
settings-intent-engine-desc = Выберите нейросеть для распознавания команд.
|
||||
settings-noise-suppression = Шумоподавление
|
||||
settings-noise-suppression-desc = Уменьшает фоновый шум.
|
||||
settings-vad = Определение голоса (VAD)
|
||||
settings-vad-desc = Пропускает тишину, экономит ресурсы CPU.
|
||||
settings-gain-normalizer = Нормализация громкости
|
||||
settings-gain-normalizer-desc = Автоматически регулирует уровень громкости.
|
||||
settings-api-keys = API Ключи
|
||||
settings-save = Сохранить
|
||||
settings-cancel = Отмена
|
||||
settings-back = Назад
|
||||
settings-enabled = Включено
|
||||
settings-disabled = Отключено
|
||||
|
||||
# settings - beta notice
|
||||
settings-beta-title = БЕТА версия!
|
||||
settings-beta-desc = Часть функций может работать некорректно.
|
||||
settings-beta-feedback = Сообщайте обо всех найденных багах в
|
||||
settings-beta-bot = наш телеграм бот
|
||||
settings-open-logs = Открыть папку с логами
|
||||
|
||||
# settings - picovoice
|
||||
settings-attention = Внимание!
|
||||
settings-picovoice-warning = Эта нейросеть работает не у всех!
|
||||
settings-picovoice-waiting = Мы ждем официального патча от разработчиков.
|
||||
settings-picovoice-key-desc = Введите сюда свой ключ Picovoice. Он выдается бесплатно при регистрации в
|
||||
settings-picovoice-key = Ключ Picovoice
|
||||
|
||||
# settings - vosk
|
||||
settings-auto-detect = Авто-определение
|
||||
settings-vosk-model = Модель распознавания речи (Vosk)
|
||||
settings-vosk-model-desc = Выберите модель Vosk для распознавания речи.
|
||||
settings-models-not-found = Модели не найдены
|
||||
settings-models-hint = Поместите модели Vosk в папку resources/vosk
|
||||
|
||||
# settings - openai
|
||||
settings-openai-key = Ключ OpenAI
|
||||
settings-openai-not-supported = В данный момент ChatGPT не поддерживается. Он будет добавлен в ближайших обновлениях.
|
||||
|
||||
# ### COMMANDS PAGE
|
||||
commands-title = Команды
|
||||
commands-search = Поиск команд...
|
||||
commands-count = { $count } команд
|
||||
commands-wip-title = [404] Этот раздел еще находится в разработке!
|
||||
commands-wip-desc = Тут будет список команд + полноценный редактор команд.
|
||||
commands-wip-follow = Следите за обновлениями в
|
||||
commands-wip-channel = нашем телеграм канале
|
||||
|
||||
# ### ERRORS
|
||||
error-generic = Произошла ошибка
|
||||
error-connection = Ошибка подключения
|
||||
error-not-found = Не найдено
|
||||
|
||||
# ### NOTIFICATIONS
|
||||
notification-saved = Настройки сохранены!
|
||||
notification-error = Ошибка
|
||||
notification-assistant-started = Ассистент запущен
|
||||
notification-assistant-stopped = Ассистент остановлен
|
||||
119
crates/jarvis-core/src/i18n/locales/ua.ftl
Normal file
@@ -0,0 +1,119 @@
|
||||
# ### APP INFO
|
||||
app-name = JARVIS
|
||||
app-description = Голосовий асистент
|
||||
|
||||
# ### TRAY MENU
|
||||
tray-restart = Перезапустити
|
||||
tray-settings = Налаштування
|
||||
tray-exit = Вихід
|
||||
tray-tooltip = JARVIS - Голосовий асистент
|
||||
|
||||
# ### HEADER
|
||||
header-commands = КОМАНДИ
|
||||
header-settings = НАЛАШТУВАННЯ
|
||||
|
||||
# ### SEARCH
|
||||
search-placeholder = Введіть команду вручну або скажіть «Джарвіс» ...
|
||||
|
||||
# ### MAIN PAGE
|
||||
assistant-not-running = АСИСТЕНТ НЕ ЗАПУЩЕНО
|
||||
assistant-offline-hint = Налаштувати його можна не запускаючи.
|
||||
btn-start = ЗАПУСТИТИ
|
||||
btn-starting = ЗАПУСК...
|
||||
|
||||
# ### STATUS
|
||||
status-disconnected = Відключено
|
||||
status-standby = Очікування
|
||||
status-listening = Слухаю...
|
||||
status-processing = Обробка...
|
||||
|
||||
# ### STATS
|
||||
stats-microphone = МІКРОФОН
|
||||
stats-neural-networks = НЕЙРОМЕРЕЖІ
|
||||
stats-resources = РЕСУРСИ
|
||||
stats-system-default = Системний
|
||||
stats-not-selected = Не вибрано
|
||||
stats-loading = Завантаження...
|
||||
|
||||
# ### FOOTER
|
||||
footer-author = Автор проєкту
|
||||
footer-telegram = Наш телеграм канал
|
||||
footer-github = Github репозиторій проєкту
|
||||
footer-support = Підтримати проєкт на
|
||||
|
||||
# ### SETTINGS
|
||||
settings-title = Налаштування
|
||||
settings-general = Основні
|
||||
settings-devices = Пристрої
|
||||
settings-neural-networks = Нейромережі
|
||||
settings-audio = Аудіо
|
||||
settings-recognition = Розпізнавання
|
||||
settings-about = Про програму
|
||||
settings-language = Мова
|
||||
settings-microphone = Мікрофон
|
||||
settings-microphone-desc = Його буде слухати асистент.
|
||||
settings-mic-default = За замовчуванням (Система)
|
||||
settings-voice = Голос асистента
|
||||
settings-voice-desc = Не всі команди працюють з усіма звуковими пакетами.
|
||||
settings-wake-word-engine = Рушій активації
|
||||
settings-wake-word-desc = Виберіть нейромережу для розпізнавання активаційної фрази.
|
||||
settings-stt-engine = Розпізнавання мовлення
|
||||
settings-intent-engine = Визначення наміру
|
||||
settings-intent-engine-desc = Виберіть нейромережу для розпізнавання команд.
|
||||
settings-noise-suppression = Шумозаглушення
|
||||
settings-noise-suppression-desc = Зменшує фоновий шум.
|
||||
settings-vad = Визначення голосу (VAD)
|
||||
settings-vad-desc = Пропускає тишу, економить ресурси CPU.
|
||||
settings-gain-normalizer = Нормалізація гучності
|
||||
settings-gain-normalizer-desc = Автоматично регулює рівень гучності.
|
||||
settings-api-keys = API Ключі
|
||||
settings-save = Зберегти
|
||||
settings-cancel = Скасувати
|
||||
settings-back = Назад
|
||||
settings-enabled = Увімкнено
|
||||
settings-disabled = Вимкнено
|
||||
|
||||
# settings - beta notice
|
||||
settings-beta-title = БЕТА версія!
|
||||
settings-beta-desc = Частина функцій може працювати некоректно.
|
||||
settings-beta-feedback = Повідомляйте про всі знайдені баги в
|
||||
settings-beta-bot = наш телеграм бот
|
||||
settings-open-logs = Відкрити папку з логами
|
||||
|
||||
# settings - picovoice
|
||||
settings-attention = Увага!
|
||||
settings-picovoice-warning = Ця нейромережа працює не у всіх!
|
||||
settings-picovoice-waiting = Ми чекаємо офіційного патча від розробників.
|
||||
settings-picovoice-key-desc = Введіть сюди свій ключ Picovoice. Він видається безкоштовно при реєстрації в
|
||||
settings-picovoice-key = Ключ Picovoice
|
||||
|
||||
# settings - vosk
|
||||
settings-auto-detect = Авто-визначення
|
||||
settings-vosk-model = Модель розпізнавання мовлення (Vosk)
|
||||
settings-vosk-model-desc = Виберіть модель Vosk для розпізнавання мовлення.
|
||||
settings-models-not-found = Моделі не знайдено
|
||||
settings-models-hint = Помістіть моделі Vosk в папку resources/vosk
|
||||
|
||||
# settings - openai
|
||||
settings-openai-key = Ключ OpenAI
|
||||
settings-openai-not-supported = Наразі ChatGPT не підтримується. Він буде доданий у наступних оновленнях.
|
||||
|
||||
# ### COMMANDS PAGE
|
||||
commands-title = Команди
|
||||
commands-search = Пошук команд...
|
||||
commands-count = { $count } команд
|
||||
commands-wip-title = [404] Цей розділ ще в розробці!
|
||||
commands-wip-desc = Тут буде список команд + повноцінний редактор команд.
|
||||
commands-wip-follow = Слідкуйте за оновленнями в
|
||||
commands-wip-channel = нашому телеграм каналі
|
||||
|
||||
# ### ERRORS
|
||||
error-generic = Сталася помилка
|
||||
error-connection = Помилка підключення
|
||||
error-not-found = Не знайдено
|
||||
|
||||
# ### NOTIFICATIONS
|
||||
notification-saved = Налаштування збережено!
|
||||
notification-error = Помилка
|
||||
notification-assistant-started = Асистент запущено
|
||||
notification-assistant-stopped = Асистент зупинено
|
||||
@@ -11,6 +11,7 @@ pub mod audio;
|
||||
pub mod commands;
|
||||
pub mod config;
|
||||
pub mod db;
|
||||
pub mod i18n;
|
||||
|
||||
#[cfg(feature = "jarvis_app")]
|
||||
pub mod listener;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use jarvis_core::{config, db, APP_CONFIG_DIR, APP_LOG_DIR, DB};
|
||||
use jarvis_core::{config, db, i18n, APP_CONFIG_DIR, APP_LOG_DIR, DB};
|
||||
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
@@ -26,6 +26,11 @@ fn main() {
|
||||
|
||||
// init db
|
||||
let settings = db::init_settings();
|
||||
|
||||
// init i18n
|
||||
i18n::init(&settings.language);
|
||||
|
||||
// set db
|
||||
DB.set(Arc::new(RwLock::new(settings)))
|
||||
.expect("DB already initialized");
|
||||
let db_arc = DB.get().unwrap().clone();
|
||||
@@ -67,6 +72,13 @@ fn main() {
|
||||
|
||||
// vosk
|
||||
tauri_commands::list_vosk_models,
|
||||
|
||||
// i18n
|
||||
tauri_commands::get_translations,
|
||||
tauri_commands::translate,
|
||||
tauri_commands::get_current_language,
|
||||
tauri_commands::set_language,
|
||||
tauri_commands::get_supported_languages,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -25,4 +25,8 @@ pub use sys::*;
|
||||
|
||||
// import STT commands
|
||||
mod stt;
|
||||
pub use stt::*;
|
||||
pub use stt::*;
|
||||
|
||||
// import i18n commands
|
||||
mod i18n;
|
||||
pub use i18n::*;
|
||||
@@ -15,6 +15,7 @@ pub fn db_read(state: tauri::State<'_, AppState>, key: &str) -> String {
|
||||
"noise_suppression" => format!("{:?}", settings.noise_suppression),
|
||||
"vad" => format!("{:?}", settings.vad),
|
||||
"gain_normalizer" => settings.gain_normalizer.to_string(),
|
||||
"language" => settings.language.to_string(),
|
||||
"api_key__picovoice" => settings.api_keys.picovoice.clone(),
|
||||
"api_key__openai" => settings.api_keys.openai.clone(),
|
||||
_ => String::new(),
|
||||
@@ -78,6 +79,9 @@ pub fn db_write(state: tauri::State<'_, AppState>, key: &str, val: &str) -> bool
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
"language" => {
|
||||
settings.language = val.to_string();
|
||||
}
|
||||
"api_key__picovoice" => {
|
||||
settings.api_keys.picovoice = val.to_string();
|
||||
}
|
||||
|
||||
@@ -38,6 +38,24 @@ pub fn get_tg_official_link() -> String {
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_boosty_link() -> String {
|
||||
if let Some(ver) = config::SUPPORT_BOOSTY_LINK {
|
||||
ver.to_string()
|
||||
} else {
|
||||
String::from("error")
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_patreon_link() -> String {
|
||||
if let Some(ver) = config::SUPPORT_PATREON_LINK {
|
||||
ver.to_string()
|
||||
} else {
|
||||
String::from("error")
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_feedback_link() -> String {
|
||||
if let Some(res) = config::FEEDBACK_LINK {
|
||||
|
||||
49
crates/jarvis-gui/src/tauri_commands/i18n.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use jarvis_core::i18n;
|
||||
use std::collections::HashMap;
|
||||
use crate::AppState;
|
||||
|
||||
// Get all translations for frontend
|
||||
#[tauri::command]
|
||||
pub fn get_translations() -> HashMap<String, String> {
|
||||
i18n::get_all_translations()
|
||||
}
|
||||
|
||||
// Get single translation
|
||||
#[tauri::command]
|
||||
pub fn translate(key: &str) -> String {
|
||||
i18n::t(key)
|
||||
}
|
||||
|
||||
// Get current language
|
||||
#[tauri::command]
|
||||
pub fn get_current_language() -> String {
|
||||
i18n::get_language()
|
||||
}
|
||||
|
||||
// Set language and get new translations
|
||||
#[tauri::command]
|
||||
pub fn set_language(state: tauri::State<'_, AppState>, lang: &str) -> HashMap<String, String> {
|
||||
// update i18n
|
||||
i18n::set_language(lang);
|
||||
|
||||
// also save to db
|
||||
{
|
||||
let mut settings = state.db.write();
|
||||
settings.language = lang.to_string();
|
||||
}
|
||||
|
||||
// save to disk
|
||||
let snapshot = state.db.read().clone();
|
||||
if let Err(e) = jarvis_core::db::save_settings(&snapshot) {
|
||||
log::error!("Failed to save settings: {}", e);
|
||||
}
|
||||
|
||||
// return new translations
|
||||
i18n::get_all_translations()
|
||||
}
|
||||
|
||||
// Get supported languages
|
||||
#[tauri::command]
|
||||
pub fn get_supported_languages() -> Vec<&'static str> {
|
||||
i18n::SUPPORTED_LANGUAGES.to_vec()
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
"resizable": false,
|
||||
"title": "Jarvis Voice Assistant",
|
||||
"width": 550,
|
||||
"height": 700
|
||||
"height": 800
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
BIN
frontend/public/media/128x128.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
frontend/public/media/bg.png
Normal file
|
After Width: | Height: | Size: 396 KiB |
BIN
frontend/public/media/flags/EN.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
frontend/public/media/flags/RU.png
Normal file
|
After Width: | Height: | Size: 792 B |
BIN
frontend/public/media/flags/UA.png
Normal file
|
After Width: | Height: | Size: 506 B |
BIN
frontend/public/media/icons/boosty.webp
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
frontend/public/media/icons/patreon.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
frontend/public/media/icons/telegram.webp
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
frontend/public/media/jarvis-hover.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
frontend/public/media/jarvis.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
@@ -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(() => {
|
||||
|
||||
@@ -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" />
|
||||
Наш телеграм
|
||||
{#if $currentLanguage === "ru" || $currentLanguage === "ua"}
|
||||
<a href={tgLink} target="_blank" class="telegram-link">
|
||||
<img src="/media/icons/telegram.webp" alt="Telegram" width="18px" />
|
||||
<span>{t('footer-telegram')}</span>
|
||||
</a>
|
||||
канал.
|
||||
|
||||
|
||||
{/if}
|
||||
<a href={repoLink} target="_blank">
|
||||
<img src="/media/icons/github-logo.png" alt="GitHub" width="18px" />
|
||||
Github репозиторий
|
||||
<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;
|
||||
|
||||
@@ -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"> </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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
411
frontend/src/components/elements/__ArcReactor.svelte
Normal 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>
|
||||
@@ -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
@@ -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"]
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 />
|
||||
|
||||
@@ -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) {
|
||||
|
||||