From 2296e9bda8ec505aca8ea2779de96286d2acd42e Mon Sep 17 00:00:00 2001 From: David Perov Date: Thu, 25 Jun 2026 13:17:48 +0300 Subject: [PATCH] feat: English UI/i18n, summary slide-over, model-mode UX (v0.3.0) - i18n: lightweight RU/EN system + interface-language picker; full UI translated, OS-detected default - summary: right slide-over modal with per-record summary history table (generate/regenerate/edit/copy/delete); replaces inline SummaryPanel; triggers disabled with hint when no text model configured - models: API & Local split into Recognition/Text sub-tabs; explicit select-mode commit row; TextModelCard restyled to match adapter cards; honest Configured badge - widget: click-to-expand notice bubble - bump version to 0.3.0 --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- .../binaries/talkis-llm-aarch64-apple-darwin | Bin 6030088 -> 6030088 bytes src-tauri/src/ai.rs | 58 +- src-tauri/src/commands/widget.rs | 27 +- src-tauri/src/lib.rs | 1 + src-tauri/src/llm_runtime.rs | 5 + src-tauri/tauri.conf.json | 2 +- src/components/Dropdown.tsx | 124 ++++ src/components/LocalLlmModels.tsx | 141 ++--- src/components/PermissionScreen.tsx | 79 +-- src/components/RowActionsMenu.tsx | 115 ++++ src/components/SlideOverModal.tsx | 109 ++++ src/components/SummaryModal.tsx | 300 ++++++++++ src/components/SummaryPanel.tsx | 242 -------- src/components/TitleBar.tsx | 9 +- src/components/TranscriptionStatsPanel.tsx | 26 +- src/components/UserPanel.tsx | 20 +- src/lib/callCapture.ts | 54 +- src/lib/fileTranscription.ts | 77 +-- src/lib/i18n/dict/common.ts | 11 + src/lib/i18n/dict/components.ts | 148 +++++ src/lib/i18n/dict/libMessages.ts | 185 ++++++ src/lib/i18n/dict/settingsGeneral.ts | 80 +++ src/lib/i18n/dict/settingsModels.ts | 323 +++++++++++ src/lib/i18n/dict/settingsRest.ts | 268 +++++++++ src/lib/i18n/dict/settingsTabsMisc.ts | 330 +++++++++++ src/lib/i18n/dict/summary.ts | 49 ++ src/lib/i18n/dict/widget.ts | 243 ++++++++ src/lib/i18n/index.tsx | 117 ++++ src/lib/i18n/messages.ts | 27 + src/lib/processingControl.ts | 5 +- src/lib/store.ts | 81 ++- src/lib/summarize.ts | 19 +- src/main.tsx | 5 +- src/windows/settings/SettingsApp.tsx | 36 +- .../settings/tabs/FileTranscriptionTab.tsx | 192 ++++--- src/windows/settings/tabs/MainTab.tsx | 214 +++---- .../settings/tabs/RealtimeInterpreterTab.tsx | 115 ++-- src/windows/settings/tabs/SettingsTab.tsx | 202 ++++--- src/windows/settings/tabs/SettingsTabs.tsx | 529 ++++++++++++------ src/windows/widget/Widget.tsx | 71 ++- src/windows/widget/WidgetNoticeOverlay.tsx | 56 +- src/windows/widget/hooks/useWidgetHotkey.ts | 12 +- .../widget/hooks/useWidgetRecording.ts | 33 +- src/windows/widget/main.tsx | 5 +- .../widget/services/transcriptionPipeline.ts | 37 +- 48 files changed, 3770 insertions(+), 1018 deletions(-) create mode 100644 src/components/Dropdown.tsx create mode 100644 src/components/RowActionsMenu.tsx create mode 100644 src/components/SlideOverModal.tsx create mode 100644 src/components/SummaryModal.tsx delete mode 100644 src/components/SummaryPanel.tsx create mode 100644 src/lib/i18n/dict/common.ts create mode 100644 src/lib/i18n/dict/components.ts create mode 100644 src/lib/i18n/dict/libMessages.ts create mode 100644 src/lib/i18n/dict/settingsGeneral.ts create mode 100644 src/lib/i18n/dict/settingsModels.ts create mode 100644 src/lib/i18n/dict/settingsRest.ts create mode 100644 src/lib/i18n/dict/settingsTabsMisc.ts create mode 100644 src/lib/i18n/dict/summary.ts create mode 100644 src/lib/i18n/dict/widget.ts create mode 100644 src/lib/i18n/index.tsx create mode 100644 src/lib/i18n/messages.ts diff --git a/package.json b/package.json index 055a1e6..9a0442f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "talkis", "private": true, - "version": "0.2.0", + "version": "0.3.0", "license": "AGPL-3.0-or-later", "type": "module", "packageManager": "bun@1.2.13", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 99eef7f..de11985 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4969,7 +4969,7 @@ dependencies = [ [[package]] name = "talkis" -version = "0.2.0" +version = "0.3.0" dependencies = [ "base64 0.22.1", "block2 0.6.2", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9eb8970..8fe0806 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "talkis" -version = "0.2.0" +version = "0.3.0" description = "Talkis - Voice to Text Desktop Widget" authors = ["trixter"] license = "AGPL-3.0-or-later" diff --git a/src-tauri/binaries/talkis-llm-aarch64-apple-darwin b/src-tauri/binaries/talkis-llm-aarch64-apple-darwin index eb17e2a2321bf1cbb8be75613d0c331d4cb7f20e..8d8efae228851bea8c512334ca22e9e4c0fd364b 100644 GIT binary patch delta 1737 zcmZ9MYfy|)9LBY!(yrQVHwmSZC~Zo)FIu{|B&GDSH6sQyN(K{UWZWKi4b9+W_>jw( z%rL{q^nnkIB1x%`E|jDp>7qy^-p=rzJ@1E|Ip_ELKj%67;XOwVbt;aCzrC=40+z7C zP*}qTwipIGD51h|*uw#8Xy6Dfba285IAbJSFbbnF2Ci_!Sh&Ljo)`x&jE6TSU?P0r zi%IZ<9{!k&00d$Rf)I?Un1<AOV#!;8n%IgDP3O z@+E5*|7tw>BM05D*hF?Ui;HW4)W2qDQ}Dcofi-pFL}4ulq{~xzy`GZ-$$cXOr{6LV z*udJwwLmf(IVq4W?_^+I69X3SW%0;nCh2)09~h`;5etG_I3Qh~$}t}~DG>E18L(<) z;9@Ilr}4JWOj6L;#=zEgvHist4oK}(-s|9`KyG%*z?Cirj9*zhji2adl7hWG3~0WK z?P=dQAhlDO*vsU*K5;twhYW=FaX^aGX}@2b6o`Aj4CsC{aO*d_JdO7cFiAn@9|rbW zBnW4k6Qu7|+)L$yj+82xj2S9|`<610Z^Z#APNy$eb5i`=ZDatp40sG#^S z*q)@6fjAWhq;@)OuxIjtT8sxc$bgrc15%t$s~kBgoYnGs(qjg)b!K-Td>iVV5~Qb! zJ{IL8Bv6p*oUp5P&rH4E;rsqaw!Tg2;d_c3qIYY@U-mp)cGtgWdu2w}75}BjdLp!W zSp!8rE}{CqiZeR`cSSv0bnBu^&g#;j&1(%UjwM}+^mFQ_Lhsc6;km(!-RgC!ZpYZG zYrTz^?l=YBQ~b0)ENr``iEGp!4RyQU))BTk=lI0J>Ve*Mqfgo!-R!Qp>XH?eZApi2 z_{6xjW%W9la^~6=P}x_ z*%nV z{1Je$2*fx9VLXBnf(g(e6caHClQ9KTF%8oZh8dWNa6}*yQJ95j%*GtVU@qoiJ{BMr zaaf2&Sd4fq!BXh43<(gBh~=e*no8BSwJS3xq?R>Q8nUhaZAkoWgFs4e8HfdFvseqH zxQdekIdDe?aF+qUdj`vR0$G3GaQOdu9cHmCJgi}0&z}m&NKyqGlQXm;`WMIu(2CUx6;<1fP((@vk7>qUMm0V$UOy<@irp3YApk$N)AYII$f;dr1-hp$^h&b@Uv&_G`?W~ zlO1ZYJ;gx=^lA=B?Q}ZKk;!Y$Vm#PM2DHu`km7V&J&2Qn$(7%eZVOPPvAFx+x8apr zlJr#3$HMF`fwFXur0p#`P8J3&b=;pBs2&;9tUFV^Vt7!LDyy`jwc*&@ym>v^eMQwv ztfyCmmmf72c9us3oS5CE@_(l|wos$rl4{&y)SRBI^*Aymeq!R-Q=vZACU;ZA_D^ZM zOjnG(j)`YW(ruChs&sFkHM=VFQ*S=G{Bik>y<0!%A9~v=&p(KA`lgIbyPdnarm40d zL+7_!x4OrqDmiXG8 break result, + _ = tokio::time::sleep(Duration::from_millis(400)) => { + if crate::download_cancel::is_cancel_requested(requested_model) { + if let Some(stop) = &local_progress_stop { + stop.store(true, Ordering::Relaxed); + } + if let (Some(kind), Some(port)) = + (managed_runtime_kind, port_from_url(&models_url)) + { + if let Err(err) = local_stt::stop_managed_runtime(kind, port) { + logger::log_error("STT_INSTALL", &err); + } + } + crate::download_cancel::clear(requested_model); + local_stt::emit_model_download_progress_message( + &app, + requested_model, + "cancelled", + 0, + None, + crate::download_cancel::CANCELLED_MESSAGE, + ); + logger::log_info( + "STT_INSTALL", + &format!( + "User cancelled managed STT model download: {}", + requested_model + ), + ); + return Err(crate::download_cancel::CANCELLED_MESSAGE.to_string()); + } + } + } + } + } else { + request.send().await + }; + + let response = send_result.map_err(|err| { if let Some(stop) = &local_progress_stop { stop.store(true, Ordering::Relaxed); } @@ -2186,6 +2239,9 @@ pub async fn install_stt_model( if let Some(stop) = &local_progress_stop { stop.store(true, Ordering::Relaxed); } + if cancellable_engine_download { + crate::download_cancel::clear(requested_model); + } let status = response.status(); if !status.is_success() { diff --git a/src-tauri/src/commands/widget.rs b/src-tauri/src/commands/widget.rs index e4d8109..e00bdf0 100644 --- a/src-tauri/src/commands/widget.rs +++ b/src-tauri/src/commands/widget.rs @@ -66,23 +66,29 @@ pub fn ensure_widget_notice_window(app: &AppHandle) -> Result Result<(), String> { let widget_position = widget_window.outer_position().map_err(|e| e.to_string())?; let widget_size = widget_window.outer_size().map_err(|e| e.to_string())?; let scale_factor = widget_window.scale_factor().map_err(|e| e.to_string())?; let notice_width = NOTICE_WIDTH * scale_factor; - let notice_height = NOTICE_HEIGHT * scale_factor; + let notice_height = height * scale_factor; let notice_gap = NOTICE_GAP * scale_factor; let x = widget_position.x as f64 + (widget_size.width as f64 - notice_width) / 2.0; + // Anchor the bubble's bottom edge just above the widget, so it grows upward. let y = widget_position.y as f64 - notice_gap - notice_height; notice_window .set_size(tauri::Size::Logical(tauri::LogicalSize { width: NOTICE_WIDTH, - height: NOTICE_HEIGHT, + height, })) .map_err(|e| e.to_string())?; @@ -271,7 +277,8 @@ pub async fn show_widget_notice( .ok_or_else(|| "Widget window not found".to_string())?; let notice_window = ensure_widget_notice_window(&app)?; - position_widget_notice_window(&widget_window, ¬ice_window)?; + // Always (re)show collapsed; the overlay expands on click. + position_widget_notice_window(&widget_window, ¬ice_window, NOTICE_HEIGHT)?; app.emit_to( NOTICE_WINDOW_LABEL, @@ -294,6 +301,20 @@ pub async fn hide_widget_notice(app: AppHandle) -> Result<(), String> { Ok(()) } +/// Resize the notice bubble to fit its full text on click (or back to the +/// collapsed height). Re-anchors above the widget so it grows upward. +#[tauri::command] +pub async fn expand_widget_notice(app: AppHandle, height: f64) -> Result<(), String> { + let widget_window = app + .get_webview_window("widget") + .ok_or_else(|| "Widget window not found".to_string())?; + let notice_window = app + .get_webview_window(NOTICE_WINDOW_LABEL) + .ok_or_else(|| "Notice window not found".to_string())?; + let clamped = height.clamp(NOTICE_HEIGHT, NOTICE_MAX_HEIGHT); + position_widget_notice_window(&widget_window, ¬ice_window, clamped) +} + #[tauri::command] pub async fn widget_resize( app: AppHandle, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4e3e7cb..3a4aad3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -146,6 +146,7 @@ pub fn run() { widget::activate_widget_for_hotkey, widget::show_widget_notice, widget::hide_widget_notice, + widget::expand_widget_notice, paste::remember_paste_target_window, paste::paste_text, ai::transcribe_and_clean, diff --git a/src-tauri/src/llm_runtime.rs b/src-tauri/src/llm_runtime.rs index 12cc262..0ea2e4f 100644 --- a/src-tauri/src/llm_runtime.rs +++ b/src-tauri/src/llm_runtime.rs @@ -22,6 +22,7 @@ struct LlmModelInfo { file_name: &'static str, url: &'static str, label: &'static str, + description: &'static str, size_label: &'static str, min_ram_gb: u32, } @@ -34,6 +35,7 @@ static LLM_CATALOG: &[LlmModelInfo] = &[ file_name: "Qwen2.5-3B-Instruct-Q4_K_M.gguf", url: "https://huggingface.co/bartowski/Qwen2.5-3B-Instruct-GGUF/resolve/main/Qwen2.5-3B-Instruct-Q4_K_M.gguf", label: "Qwen2.5 3B Instruct", + description: "Компактная текстовая модель для саммари на слабых машинах: быстрая, с хорошей поддержкой русского.", size_label: "2.0 ГБ", min_ram_gb: 8, }, @@ -42,6 +44,7 @@ static LLM_CATALOG: &[LlmModelInfo] = &[ file_name: "Qwen2.5-7B-Instruct-Q4_K_M.gguf", url: "https://huggingface.co/bartowski/Qwen2.5-7B-Instruct-GGUF/resolve/main/Qwen2.5-7B-Instruct-Q4_K_M.gguf", label: "Qwen2.5 7B Instruct", + description: "Более качественная текстовая модель для детальных саммари: точнее, но требовательнее к памяти.", size_label: "4.7 ГБ", min_ram_gb: 16, }, @@ -374,6 +377,7 @@ pub fn stop_runtime() { pub struct LocalLlmModel { id: String, label: String, + description: String, file_name: String, size_label: String, min_ram_gb: u32, @@ -387,6 +391,7 @@ pub fn list_local_llm_models(app: AppHandle) -> Vec { .map(|model| LocalLlmModel { id: model.id.to_string(), label: model.label.to_string(), + description: model.description.to_string(), file_name: model.file_name.to_string(), size_label: model.size_label.to_string(), min_ram_gb: model.min_ram_gb, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 76a45e4..58aa515 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -2,7 +2,7 @@ "$schema": "https://schema.tauri.app/config/2", "productName": "Talkis", "mainBinaryName": "Talkis", - "version": "0.2.0", + "version": "0.3.0", "identifier": "com.trixter.talkis", "build": { "beforeDevCommand": "bun run prepare:sidecars && bun run dev", diff --git a/src/components/Dropdown.tsx b/src/components/Dropdown.tsx new file mode 100644 index 0000000..5c0ac35 --- /dev/null +++ b/src/components/Dropdown.tsx @@ -0,0 +1,124 @@ +import { useEffect, useRef, useState } from "react"; +import { Check, ChevronDown } from "lucide-react"; + +export interface DropdownOption { + value: string; + label: string; +} + +/** + * Custom select styled like the dropdowns in Settings (a `.btn` trigger + a + * popover list with a check on the active item) — used instead of a native + *