From 003bf23402d7339b6e390b67ae2b654922c2f758 Mon Sep 17 00:00:00 2001 From: Vlad Gerasimov Date: Fri, 8 Aug 2025 17:50:31 +0400 Subject: [PATCH 01/15] feat(transcription): add word correction with phonetic matching --- src-tauri/Cargo.lock | 23 ++++- src-tauri/Cargo.toml | 2 + src-tauri/src/lib.rs | 1 + src-tauri/src/managers/transcription.rs | 87 ++++++++++++++++- src-tauri/src/settings.rs | 3 + src-tauri/src/shortcut.rs | 8 ++ src-tauri/tauri.conf.json | 4 +- src/App.tsx | 6 +- src/components/settings/CorrectWords.tsx | 113 +++++++++++++++++++++++ src/components/settings/Settings.tsx | 2 + src/components/settings/index.ts | 1 + src/hooks/useSettings.ts | 4 + src/lib/types.ts | 1 + 13 files changed, 248 insertions(+), 7 deletions(-) create mode 100644 src/components/settings/CorrectWords.tsx diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 9203b8f..6ccf061 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1105,7 +1105,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.0", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2001,6 +2001,7 @@ dependencies = [ "futures-util", "hound", "log", + "natural", "once_cell", "ort-sys", "rdev", @@ -2010,6 +2011,7 @@ dependencies = [ "rustfft", "serde", "serde_json", + "strsim", "tauri", "tauri-build", "tauri-plugin-autostart", @@ -2962,6 +2964,15 @@ dependencies = [ "tempfile", ] +[[package]] +name = "natural" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65e6b44f8ddc659cde3555e0140d3441ad26cb03a1410774af1f9a19097c1867" +dependencies = [ + "rust-stemmers", +] + [[package]] name = "ndarray" version = "0.16.1" @@ -4365,6 +4376,16 @@ dependencies = [ "realfft", ] +[[package]] +name = "rust-stemmers" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" +dependencies = [ + "serde", + "serde_derive", +] + [[package]] name = "rustc-demangle" version = "0.1.24" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9b31d84..89f25ac 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -57,6 +57,8 @@ reqwest = { version = "0.11", features = ["json", "stream"] } futures-util = "0.3" tauri-plugin-fs = "2" rustfft = "6.4.0" +strsim = "0.11.0" +natural = "0.5.0" [dependencies.ort-sys] version = "=2.0.0-rc.9" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e220fac..9bc0702 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -187,6 +187,7 @@ pub fn run() { shortcut::change_selected_language_setting, shortcut::change_overlay_position_setting, shortcut::change_debug_mode_setting, + shortcut::update_correct_words, shortcut::suspend_binding, shortcut::resume_binding, trigger_update_check, diff --git a/src-tauri/src/managers/transcription.rs b/src-tauri/src/managers/transcription.rs index bfd3e79..13fc4c2 100644 --- a/src-tauri/src/managers/transcription.rs +++ b/src-tauri/src/managers/transcription.rs @@ -1,8 +1,10 @@ use crate::managers::model::ModelManager; use crate::settings::get_settings; use anyhow::Result; +use natural::phonetics::soundex; use serde::Serialize; use std::sync::{Arc, Mutex}; +use strsim::levenshtein; use tauri::{App, AppHandle, Emitter, Manager}; use whisper_rs::install_whisper_log_trampoline; use whisper_rs::{ @@ -25,6 +27,82 @@ pub struct TranscriptionManager { current_model_id: Mutex>, } +fn correct_words(text: &str, correct_words: &[String]) -> String { + if correct_words.is_empty() { + return text.to_string(); + } + + let words: Vec<&str> = text.split_whitespace().collect(); + let mut corrected_words = Vec::new(); + + for word in words { + let cleaned_word = word.trim_matches(|c: char| !c.is_alphabetic()).to_lowercase(); + + if cleaned_word.is_empty() { + corrected_words.push(word.to_string()); + continue; + } + + let mut best_match: Option<&String> = None; + let mut best_score = f64::MAX; + + for correct_word in correct_words { + let correct_word_lower = correct_word.to_lowercase(); + + // Calculate Levenshtein distance (normalized by length) + let levenshtein_dist = levenshtein(&cleaned_word, &correct_word_lower); + let max_len = cleaned_word.len().max(correct_word_lower.len()) as f64; + let levenshtein_score = if max_len > 0.0 { levenshtein_dist as f64 / max_len } else { 1.0 }; + + // Calculate phonetic similarity using Soundex + let phonetic_match = soundex(&cleaned_word, &correct_word_lower); + + // Combine scores: favor phonetic matches, but also consider string similarity + let combined_score = if phonetic_match { + levenshtein_score * 0.3 // Give significant boost to phonetic matches + } else { + levenshtein_score + }; + + // Accept if the score is good enough (threshold: 0.4 for good matches) + if combined_score < 0.4 && combined_score < best_score { + best_match = Some(correct_word); + best_score = combined_score; + } + } + + if let Some(replacement) = best_match { + // Preserve the original case pattern as much as possible + let corrected = if word.chars().all(|c| c.is_uppercase()) { + replacement.to_uppercase() + } else if word.chars().next().map_or(false, |c| c.is_uppercase()) { + let mut chars: Vec = replacement.chars().collect(); + if let Some(first_char) = chars.get_mut(0) { + *first_char = first_char.to_uppercase().next().unwrap_or(*first_char); + } + chars.into_iter().collect() + } else { + replacement.clone() + }; + + // Preserve punctuation from original word + let original_suffix: String = word.chars().rev() + .take_while(|c| !c.is_alphabetic()) + .collect::() + .chars().rev().collect(); + let original_prefix: String = word.chars() + .take_while(|c| !c.is_alphabetic()) + .collect(); + + corrected_words.push(format!("{}{}{}", original_prefix, corrected, original_suffix)); + } else { + corrected_words.push(word.to_string()); + } + } + + corrected_words.join(" ") +} + impl TranscriptionManager { pub fn new(app: &App, model_manager: Arc) -> Result { let app_handle = app.app_handle().clone(); @@ -207,6 +285,13 @@ impl TranscriptionManager { result.push_str(&segment); } + // Apply word correction if correct words are configured + let corrected_result = if !settings.correct_words.is_empty() { + correct_words(&result, &settings.correct_words) + } else { + result + }; + let et = std::time::Instant::now(); let translation_note = if settings.translate_to_english { " (translated)" @@ -215,6 +300,6 @@ impl TranscriptionManager { }; println!("\ntook {}ms{}", (et - st).as_millis(), translation_note); - Ok(result.trim().to_string()) + Ok(corrected_result.trim().to_string()) } } diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 99d1520..39863d3 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -42,6 +42,8 @@ pub struct AppSettings { pub overlay_position: OverlayPosition, #[serde(default = "default_debug_mode")] pub debug_mode: bool, + #[serde(default)] + pub correct_words: Vec, } fn default_model() -> String { @@ -104,6 +106,7 @@ pub fn get_default_settings() -> AppSettings { selected_language: "auto".to_string(), overlay_position: OverlayPosition::Bottom, debug_mode: false, + correct_words: Vec::new(), } } diff --git a/src-tauri/src/shortcut.rs b/src-tauri/src/shortcut.rs index 189f33d..30eca98 100644 --- a/src-tauri/src/shortcut.rs +++ b/src-tauri/src/shortcut.rs @@ -163,6 +163,14 @@ pub fn change_debug_mode_setting(app: AppHandle, enabled: bool) -> Result<(), St Ok(()) } +#[tauri::command] +pub fn update_correct_words(app: AppHandle, words: Vec) -> Result<(), String> { + let mut settings = settings::get_settings(&app); + settings.correct_words = words; + settings::write_settings(&app, settings); + Ok(()) +} + /// Determine whether a shortcut string contains at least one non-modifier key. /// We allow single non-modifier keys (e.g. "f5" or "space") but disallow /// modifier-only combos (e.g. "ctrl" or "ctrl+shift"). diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index fceaad4..8867490 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -15,9 +15,9 @@ { "title": "Handy", "width": 540, - "height": 700, + "height": 750, "minWidth": 540, - "minHeight": 700, + "minHeight": 750, "resizable": true, "maximizable": false } diff --git a/src/App.tsx b/src/App.tsx index fbd1a45..73fe4c3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,12 @@ import { invoke } from "@tauri-apps/api/core"; import { useEffect, useState } from "react"; +import { Toaster } from "sonner"; import "./App.css"; import AccessibilityPermissions from "./components/AccessibilityPermissions"; import Footer from "./components/footer"; import HandyTextLogo from "./components/icons/HandyTextLogo"; import Onboarding from "./components/onboarding"; import { Settings } from "./components/settings/Settings"; -import { Toaster } from "sonner"; function App() { const [showOnboarding, setShowOnboarding] = useState(null); @@ -35,7 +35,7 @@ function App() { return (
- +
@@ -46,7 +46,7 @@ function App() {
- +
diff --git a/src/components/settings/CorrectWords.tsx b/src/components/settings/CorrectWords.tsx new file mode 100644 index 0000000..f090824 --- /dev/null +++ b/src/components/settings/CorrectWords.tsx @@ -0,0 +1,113 @@ +import React, { useState } from "react"; +import { SettingContainer } from "../ui/SettingContainer"; +import { useSettings } from "../../hooks/useSettings"; + +interface CorrectWordsProps { + descriptionMode?: "inline" | "tooltip"; + grouped?: boolean; +} + +export const CorrectWords: React.FC = ({ + descriptionMode = "tooltip", + grouped = false, +}) => { + const { getSetting, updateSetting, isUpdating } = useSettings(); + const [newWord, setNewWord] = useState(""); + + const correctWords = getSetting("correct_words") || []; + + const handleAddWord = () => { + const trimmedWord = newWord.trim(); + // Don't allow spaces in words + if (trimmedWord && !trimmedWord.includes(' ') && !correctWords.includes(trimmedWord)) { + updateSetting("correct_words", [...correctWords, trimmedWord]); + setNewWord(""); + } + }; + + const handleRemoveWord = (wordToRemove: string) => { + updateSetting( + "correct_words", + correctWords.filter((word) => word !== wordToRemove) + ); + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + handleAddWord(); + } + }; + + return ( +
+ {/* Title, info button, input and add button all in one line */} +
+

Custom Words

+ {descriptionMode === "tooltip" && ( +
+ + + +
+ )} + setNewWord(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="Add a word" + className="flex-1 px-2 py-1 text-sm border border-mid-gray/30 rounded focus:outline-none focus:ring-1 focus:ring-logo-primary focus:border-transparent bg-background" + disabled={isUpdating("correct_words")} + /> + +
+ + {/* Words list - flex wrap layout */} + {correctWords.length > 0 && ( +
+ {correctWords.map((word) => ( + + ))} +
+ )} +
+ ); +}; \ No newline at end of file diff --git a/src/components/settings/Settings.tsx b/src/components/settings/Settings.tsx index 53c3ab9..fa530f6 100644 --- a/src/components/settings/Settings.tsx +++ b/src/components/settings/Settings.tsx @@ -8,6 +8,7 @@ import { ShowOverlay } from "./ShowOverlay"; import { HandyShortcut } from "./HandyShortcut"; import { TranslateToEnglish } from "./TranslateToEnglish"; import { LanguageSelector } from "./LanguageSelector"; +import { CorrectWords } from "./CorrectWords"; import { SettingsGroup } from "../ui/SettingsGroup"; import { DebugSettings } from "./debug"; import { useSettings } from "../../hooks/useSettings"; @@ -54,6 +55,7 @@ export const Settings: React.FC = () => { + diff --git a/src/components/settings/index.ts b/src/components/settings/index.ts index e8ffd89..cd7dd78 100644 --- a/src/components/settings/index.ts +++ b/src/components/settings/index.ts @@ -7,3 +7,4 @@ export { AudioFeedback } from "./AudioFeedback"; export { ShowOverlay } from "./ShowOverlay"; export { HandyShortcut } from "./HandyShortcut"; export { TranslateToEnglish } from "./TranslateToEnglish"; +export { CorrectWords } from "./CorrectWords"; diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 17588bc..5ad26b4 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -205,6 +205,9 @@ export const useSettings = (): UseSettingsReturn => { case "debug_mode": await invoke("change_debug_mode_setting", { enabled: value }); break; + case "correct_words": + await invoke("update_correct_words", { words: value }); + break; case "bindings": // Handle bindings separately - they use their own invoke methods break; @@ -251,6 +254,7 @@ export const useSettings = (): UseSettingsReturn => { selected_language: "auto", overlay_position: "bottom", debug_mode: false, + correct_words: [], }; const defaultValue = defaults[key]; diff --git a/src/lib/types.ts b/src/lib/types.ts index dfec7b3..7979b04 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -34,6 +34,7 @@ export const SettingsSchema = z.object({ selected_language: z.string(), overlay_position: OverlayPositionSchema, debug_mode: z.boolean(), + correct_words: z.array(z.string()).optional().default([]), }); export const BindingResponseSchema = z.object({ From ca00ddeb53002ca40bcad513d578a3e402360cc3 Mon Sep 17 00:00:00 2001 From: Vlad Gerasimov Date: Fri, 8 Aug 2025 17:51:57 +0400 Subject: [PATCH 02/15] fix(ui): reduce window height and tighten layout spacing --- src-tauri/tauri.conf.json | 4 ++-- src/App.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 8867490..6a95c9d 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -15,9 +15,9 @@ { "title": "Handy", "width": 540, - "height": 750, + "height": 720, "minWidth": 540, - "minHeight": 750, + "minHeight": 720, "resizable": true, "maximizable": false } diff --git a/src/App.tsx b/src/App.tsx index 73fe4c3..e62631c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -34,7 +34,7 @@ function App() { if (showOnboarding) { return (
-
+
@@ -45,7 +45,7 @@ function App() { return (
-
+
From 6404ce74878fac2e64a7781509cd2cc938085301 Mon Sep 17 00:00:00 2001 From: Vlad Gerasimov Date: Fri, 8 Aug 2025 19:25:21 +0400 Subject: [PATCH 03/15] refactor(transcription): optimize word correction performance --- src-tauri/src/managers/transcription.rs | 39 ++++++++++++++++-------- src/components/settings/CorrectWords.tsx | 1 - 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src-tauri/src/managers/transcription.rs b/src-tauri/src/managers/transcription.rs index 13fc4c2..9f70501 100644 --- a/src-tauri/src/managers/transcription.rs +++ b/src-tauri/src/managers/transcription.rs @@ -32,6 +32,11 @@ fn correct_words(text: &str, correct_words: &[String]) -> String { return text.to_string(); } + // Pre-compute lowercase versions to avoid repeated allocations + let correct_words_lower: Vec = correct_words.iter() + .map(|w| w.to_lowercase()) + .collect(); + let words: Vec<&str> = text.split_whitespace().collect(); let mut corrected_words = Vec::new(); @@ -43,19 +48,29 @@ fn correct_words(text: &str, correct_words: &[String]) -> String { continue; } + // Skip extremely long words to avoid performance issues + if cleaned_word.len() > 50 { + corrected_words.push(word.to_string()); + continue; + } + let mut best_match: Option<&String> = None; let mut best_score = f64::MAX; - for correct_word in correct_words { - let correct_word_lower = correct_word.to_lowercase(); + for (i, correct_word_lower) in correct_words_lower.iter().enumerate() { + // Skip if lengths are too different (optimization) + let len_diff = (cleaned_word.len() as i32 - correct_word_lower.len() as i32).abs(); + if len_diff > 5 { + continue; + } // Calculate Levenshtein distance (normalized by length) - let levenshtein_dist = levenshtein(&cleaned_word, &correct_word_lower); + let levenshtein_dist = levenshtein(&cleaned_word, correct_word_lower); let max_len = cleaned_word.len().max(correct_word_lower.len()) as f64; let levenshtein_score = if max_len > 0.0 { levenshtein_dist as f64 / max_len } else { 1.0 }; // Calculate phonetic similarity using Soundex - let phonetic_match = soundex(&cleaned_word, &correct_word_lower); + let phonetic_match = soundex(&cleaned_word, correct_word_lower); // Combine scores: favor phonetic matches, but also consider string similarity let combined_score = if phonetic_match { @@ -66,7 +81,7 @@ fn correct_words(text: &str, correct_words: &[String]) -> String { // Accept if the score is good enough (threshold: 0.4 for good matches) if combined_score < 0.4 && combined_score < best_score { - best_match = Some(correct_word); + best_match = Some(&correct_words[i]); best_score = combined_score; } } @@ -85,14 +100,12 @@ fn correct_words(text: &str, correct_words: &[String]) -> String { replacement.clone() }; - // Preserve punctuation from original word - let original_suffix: String = word.chars().rev() - .take_while(|c| !c.is_alphabetic()) - .collect::() - .chars().rev().collect(); - let original_prefix: String = word.chars() - .take_while(|c| !c.is_alphabetic()) - .collect(); + // Preserve punctuation from original word - optimized version + let prefix_end = word.chars().take_while(|c| !c.is_alphabetic()).count(); + let suffix_start = word.char_indices().rev().take_while(|(_, c)| !c.is_alphabetic()).count(); + + let original_prefix = if prefix_end > 0 { &word[..prefix_end] } else { "" }; + let original_suffix = if suffix_start > 0 { &word[word.len() - suffix_start..] } else { "" }; corrected_words.push(format!("{}{}{}", original_prefix, corrected, original_suffix)); } else { diff --git a/src/components/settings/CorrectWords.tsx b/src/components/settings/CorrectWords.tsx index f090824..e0115c3 100644 --- a/src/components/settings/CorrectWords.tsx +++ b/src/components/settings/CorrectWords.tsx @@ -1,5 +1,4 @@ import React, { useState } from "react"; -import { SettingContainer } from "../ui/SettingContainer"; import { useSettings } from "../../hooks/useSettings"; interface CorrectWordsProps { From f219bd14b681929fd19b31596baffb616014ec28 Mon Sep 17 00:00:00 2001 From: Vlad Gerasimov Date: Sat, 9 Aug 2025 07:33:55 +0400 Subject: [PATCH 04/15] refactor(settings): wrap CorrectWords with SettingContainer --- src/components/settings/CorrectWords.tsx | 92 +++++++++--------------- 1 file changed, 32 insertions(+), 60 deletions(-) diff --git a/src/components/settings/CorrectWords.tsx b/src/components/settings/CorrectWords.tsx index e0115c3..0d87d2c 100644 --- a/src/components/settings/CorrectWords.tsx +++ b/src/components/settings/CorrectWords.tsx @@ -1,5 +1,6 @@ import React, { useState } from "react"; import { useSettings } from "../../hooks/useSettings"; +import { SettingContainer } from "../ui/SettingContainer"; interface CorrectWordsProps { descriptionMode?: "inline" | "tooltip"; @@ -12,12 +13,10 @@ export const CorrectWords: React.FC = ({ }) => { const { getSetting, updateSetting, isUpdating } = useSettings(); const [newWord, setNewWord] = useState(""); - const correctWords = getSetting("correct_words") || []; const handleAddWord = () => { const trimmedWord = newWord.trim(); - // Don't allow spaces in words if (trimmedWord && !trimmedWord.includes(' ') && !correctWords.includes(trimmedWord)) { updateSetting("correct_words", [...correctWords, trimmedWord]); setNewWord(""); @@ -25,10 +24,7 @@ export const CorrectWords: React.FC = ({ }; const handleRemoveWord = (wordToRemove: string) => { - updateSetting( - "correct_words", - correctWords.filter((word) => word !== wordToRemove) - ); + updateSetting("correct_words", correctWords.filter((word) => word !== wordToRemove)); }; const handleKeyPress = (e: React.KeyboardEvent) => { @@ -39,48 +35,34 @@ export const CorrectWords: React.FC = ({ }; return ( -
- {/* Title, info button, input and add button all in one line */} -
-

Custom Words

- {descriptionMode === "tooltip" && ( -
- - - -
- )} - setNewWord(e.target.value)} - onKeyPress={handleKeyPress} - placeholder="Add a word" - className="flex-1 px-2 py-1 text-sm border border-mid-gray/30 rounded focus:outline-none focus:ring-1 focus:ring-logo-primary focus:border-transparent bg-background" - disabled={isUpdating("correct_words")} - /> - -
- - {/* Words list - flex wrap layout */} + <> + +
+ setNewWord(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="Add a word" + className="px-2 py-1 text-sm border border-mid-gray/30 rounded focus:outline-none focus:ring-1 focus:ring-logo-primary focus:border-transparent bg-background" + disabled={isUpdating("correct_words")} + /> + +
+
{correctWords.length > 0 && ( -
+
{correctWords.map((word) => ( ))}
)} -
+ ); }; \ No newline at end of file From 6f16fdf2f45b9742df2d53961dabbcdfc2a55053 Mon Sep 17 00:00:00 2001 From: Vlad Gerasimov Date: Sat, 9 Aug 2025 07:52:28 +0400 Subject: [PATCH 05/15] refactor(settings): wrap components in React.memo and improve ResetIcon --- src/components/icons/ResetIcon.tsx | 13 +--- .../settings/AlwaysOnMicrophone.tsx | 4 +- src/components/settings/AudioFeedback.tsx | 4 +- src/components/settings/CorrectWords.tsx | 11 +-- src/components/settings/HandyShortcut.tsx | 17 ++--- src/components/settings/LanguageSelector.tsx | 73 ++++--------------- .../settings/MicrophoneSelector.tsx | 40 ++-------- .../settings/OutputDeviceSelector.tsx | 22 ++---- src/components/settings/PushToTalk.tsx | 4 +- src/components/settings/ShowOverlay.tsx | 4 +- .../settings/TranslateToEnglish.tsx | 4 +- src/components/ui/ResetButton.tsx | 22 ++++++ src/lib/constants/languages.ts | 59 +++++++++++++++ 13 files changed, 133 insertions(+), 144 deletions(-) create mode 100644 src/components/ui/ResetButton.tsx create mode 100644 src/lib/constants/languages.ts diff --git a/src/components/icons/ResetIcon.tsx b/src/components/icons/ResetIcon.tsx index 4136118..09f2e46 100644 --- a/src/components/icons/ResetIcon.tsx +++ b/src/components/icons/ResetIcon.tsx @@ -1,16 +1,7 @@ -import React from "react"; -const ResetIcon = ({ className = "" }: { className?: string }) => { +const ResetIcon = () => { return ( - - - + ); }; diff --git a/src/components/settings/AlwaysOnMicrophone.tsx b/src/components/settings/AlwaysOnMicrophone.tsx index c4c40c1..48fdae0 100644 --- a/src/components/settings/AlwaysOnMicrophone.tsx +++ b/src/components/settings/AlwaysOnMicrophone.tsx @@ -7,7 +7,7 @@ interface AlwaysOnMicrophoneProps { grouped?: boolean; } -export const AlwaysOnMicrophone: React.FC = ({ +export const AlwaysOnMicrophone: React.FC = React.memo(({ descriptionMode = "tooltip", grouped = false, }) => { @@ -26,4 +26,4 @@ export const AlwaysOnMicrophone: React.FC = ({ grouped={grouped} /> ); -}; +}); diff --git a/src/components/settings/AudioFeedback.tsx b/src/components/settings/AudioFeedback.tsx index dfe73e5..36524c1 100644 --- a/src/components/settings/AudioFeedback.tsx +++ b/src/components/settings/AudioFeedback.tsx @@ -7,7 +7,7 @@ interface AudioFeedbackProps { grouped?: boolean; } -export const AudioFeedback: React.FC = ({ +export const AudioFeedback: React.FC = React.memo(({ descriptionMode = "tooltip", grouped = false, }) => { @@ -26,4 +26,4 @@ export const AudioFeedback: React.FC = ({ grouped={grouped} /> ); -}; +}); diff --git a/src/components/settings/CorrectWords.tsx b/src/components/settings/CorrectWords.tsx index 0d87d2c..1bba17d 100644 --- a/src/components/settings/CorrectWords.tsx +++ b/src/components/settings/CorrectWords.tsx @@ -7,7 +7,7 @@ interface CorrectWordsProps { grouped?: boolean; } -export const CorrectWords: React.FC = ({ +export const CorrectWords: React.FC = React.memo(({ descriptionMode = "tooltip", grouped = false, }) => { @@ -17,8 +17,9 @@ export const CorrectWords: React.FC = ({ const handleAddWord = () => { const trimmedWord = newWord.trim(); - if (trimmedWord && !trimmedWord.includes(' ') && !correctWords.includes(trimmedWord)) { - updateSetting("correct_words", [...correctWords, trimmedWord]); + const sanitizedWord = trimmedWord.replace(/[<>"'&]/g, ''); + if (sanitizedWord && !sanitizedWord.includes(' ') && sanitizedWord.length <= 50 && !correctWords.includes(sanitizedWord)) { + updateSetting("correct_words", [...correctWords, sanitizedWord]); setNewWord(""); } }; @@ -54,7 +55,7 @@ export const CorrectWords: React.FC = ({ />
)} - + />
); })()} diff --git a/src/components/settings/LanguageSelector.tsx b/src/components/settings/LanguageSelector.tsx index c29db8c..c0ec705 100644 --- a/src/components/settings/LanguageSelector.tsx +++ b/src/components/settings/LanguageSelector.tsx @@ -1,55 +1,14 @@ -import React, { useState, useRef, useEffect } from "react"; +import React, { useState, useRef, useEffect, useMemo } from "react"; import { SettingContainer } from "../ui/SettingContainer"; -import ResetIcon from "../icons/ResetIcon"; +import { ResetButton } from "../ui/ResetButton"; import { useSettings } from "../../hooks/useSettings"; +import { LANGUAGES } from "../../lib/constants/languages"; interface LanguageSelectorProps { descriptionMode?: "inline" | "tooltip"; grouped?: boolean; } -const LANGUAGES = [ - { code: "auto", name: "Auto" }, - { code: "en", name: "English" }, - { code: "zh", name: "Chinese" }, - { code: "hi", name: "Hindi" }, - { code: "es", name: "Spanish" }, - { code: "fr", name: "French" }, - { code: "ar", name: "Arabic" }, - { code: "pt", name: "Portuguese" }, - { code: "ru", name: "Russian" }, - { code: "ja", name: "Japanese" }, - { code: "de", name: "German" }, - { code: "ko", name: "Korean" }, - { code: "it", name: "Italian" }, - { code: "tr", name: "Turkish" }, - { code: "vi", name: "Vietnamese" }, - { code: "pl", name: "Polish" }, - { code: "nl", name: "Dutch" }, - { code: "uk", name: "Ukrainian" }, - { code: "fa", name: "Persian" }, - { code: "th", name: "Thai" }, - { code: "ro", name: "Romanian" }, - { code: "el", name: "Greek" }, - { code: "cs", name: "Czech" }, - { code: "sv", name: "Swedish" }, - { code: "hu", name: "Hungarian" }, - { code: "he", name: "Hebrew" }, - { code: "da", name: "Danish" }, - { code: "fi", name: "Finnish" }, - { code: "no", name: "Norwegian" }, - { code: "sk", name: "Slovak" }, - { code: "bg", name: "Bulgarian" }, - { code: "hr", name: "Croatian" }, - { code: "lt", name: "Lithuanian" }, - { code: "sl", name: "Slovenian" }, - { code: "lv", name: "Latvian" }, - { code: "et", name: "Estonian" }, - { code: "sr", name: "Serbian" }, - { code: "is", name: "Icelandic" }, - { code: "id", name: "Indonesian" }, -]; - export const LanguageSelector: React.FC = ({ descriptionMode = "tooltip", grouped = false, @@ -85,12 +44,15 @@ export const LanguageSelector: React.FC = ({ } }, [isOpen]); - const filteredLanguages = LANGUAGES.filter((language) => - language.name.toLowerCase().includes(searchQuery.toLowerCase()), + const filteredLanguages = useMemo( + () => LANGUAGES.filter((language) => + language.label.toLowerCase().includes(searchQuery.toLowerCase()), + ), + [searchQuery] ); const selectedLanguageName = - LANGUAGES.find((lang) => lang.code === selectedLanguage)?.name || "Auto"; + LANGUAGES.find((lang) => lang.value === selectedLanguage)?.label || "Auto"; const handleLanguageSelect = async (languageCode: string) => { await updateSetting("selected_language", languageCode); @@ -114,7 +76,7 @@ export const LanguageSelector: React.FC = ({ const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === "Enter" && filteredLanguages.length > 0) { // Select first filtered language on Enter - handleLanguageSelect(filteredLanguages[0].code); + handleLanguageSelect(filteredLanguages[0].value); } else if (event.key === "Escape") { setIsOpen(false); setSearchQuery(""); @@ -181,17 +143,17 @@ export const LanguageSelector: React.FC = ({ ) : ( filteredLanguages.map((language) => ( )) @@ -200,13 +162,10 @@ export const LanguageSelector: React.FC = ({
)}
- + />
{isUpdating("selected_language") && (
diff --git a/src/components/settings/MicrophoneSelector.tsx b/src/components/settings/MicrophoneSelector.tsx index a958719..dc9bccf 100644 --- a/src/components/settings/MicrophoneSelector.tsx +++ b/src/components/settings/MicrophoneSelector.tsx @@ -1,33 +1,15 @@ import React from "react"; import { Dropdown } from "../ui/Dropdown"; import { SettingContainer } from "../ui/SettingContainer"; -import ResetIcon from "../icons/ResetIcon"; +import { ResetButton } from "../ui/ResetButton"; import { useSettings } from "../../hooks/useSettings"; -// Simple refresh icon component -const RefreshIcon: React.FC<{ className?: string }> = ({ className = "" }) => ( - - - -); - interface MicrophoneSelectorProps { descriptionMode?: "inline" | "tooltip"; grouped?: boolean; } -export const MicrophoneSelector: React.FC = ({ +export const MicrophoneSelector: React.FC = React.memo(({ descriptionMode = "tooltip", grouped = false, }) => { @@ -45,14 +27,10 @@ export const MicrophoneSelector: React.FC = ({ const handleMicrophoneSelect = async (deviceName: string) => { await updateSetting("selected_microphone", deviceName); - console.log( - `Microphone changed to: ${audioDevices.find((d) => d.name === deviceName)?.name}`, - ); }; const handleReset = async () => { await resetSetting("selected_microphone"); - console.log("Microphone reset to default"); }; const microphoneOptions = audioDevices.map(device => ({ @@ -76,19 +54,11 @@ export const MicrophoneSelector: React.FC = ({ disabled={isUpdating("selected_microphone") || isLoading} onRefresh={refreshAudioDevices} /> - + />
- {isUpdating("selected_microphone") && ( -
-
-
- )} ); -}; +}); diff --git a/src/components/settings/OutputDeviceSelector.tsx b/src/components/settings/OutputDeviceSelector.tsx index f3b3776..156805e 100644 --- a/src/components/settings/OutputDeviceSelector.tsx +++ b/src/components/settings/OutputDeviceSelector.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Dropdown } from "../ui/Dropdown"; import { SettingContainer } from "../ui/SettingContainer"; -import ResetIcon from "../icons/ResetIcon"; +import { ResetButton } from "../ui/ResetButton"; import { useSettings } from "../../hooks/useSettings"; interface OutputDeviceSelectorProps { @@ -9,7 +9,7 @@ interface OutputDeviceSelectorProps { grouped?: boolean; } -export const OutputDeviceSelector: React.FC = ({ +export const OutputDeviceSelector: React.FC = React.memo(({ descriptionMode = "tooltip", grouped = false, }) => { @@ -28,14 +28,10 @@ export const OutputDeviceSelector: React.FC = ({ const handleOutputDeviceSelect = async (deviceName: string) => { await updateSetting("selected_output_device", deviceName); - console.log( - `Output device changed to: ${outputDevices.find((d) => d.name === deviceName)?.name}`, - ); }; const handleReset = async () => { await resetSetting("selected_output_device"); - console.log("Output device reset to default"); }; const outputDeviceOptions = outputDevices.map(device => ({ @@ -59,19 +55,11 @@ export const OutputDeviceSelector: React.FC = ({ disabled={isUpdating("selected_output_device") || isLoading} onRefresh={refreshOutputDevices} /> - + />
- {isUpdating("selected_output_device") && ( -
-
-
- )} ); -}; +}); diff --git a/src/components/settings/PushToTalk.tsx b/src/components/settings/PushToTalk.tsx index 848e504..01ed019 100644 --- a/src/components/settings/PushToTalk.tsx +++ b/src/components/settings/PushToTalk.tsx @@ -7,7 +7,7 @@ interface PushToTalkProps { grouped?: boolean; } -export const PushToTalk: React.FC = ({ +export const PushToTalk: React.FC = React.memo(({ descriptionMode = "tooltip", grouped = false, }) => { @@ -26,4 +26,4 @@ export const PushToTalk: React.FC = ({ grouped={grouped} /> ); -}; +}); diff --git a/src/components/settings/ShowOverlay.tsx b/src/components/settings/ShowOverlay.tsx index e177e6d..d606e74 100644 --- a/src/components/settings/ShowOverlay.tsx +++ b/src/components/settings/ShowOverlay.tsx @@ -15,7 +15,7 @@ const overlayOptions = [ { value: "top", label: "Top" }, ]; -export const ShowOverlay: React.FC = ({ +export const ShowOverlay: React.FC = React.memo(({ descriptionMode = "tooltip", grouped = false, }) => { @@ -41,4 +41,4 @@ export const ShowOverlay: React.FC = ({ /> ); -}; +}); diff --git a/src/components/settings/TranslateToEnglish.tsx b/src/components/settings/TranslateToEnglish.tsx index 92917c3..00f4599 100644 --- a/src/components/settings/TranslateToEnglish.tsx +++ b/src/components/settings/TranslateToEnglish.tsx @@ -7,7 +7,7 @@ interface TranslateToEnglishProps { grouped?: boolean; } -export const TranslateToEnglish: React.FC = ({ +export const TranslateToEnglish: React.FC = React.memo(({ descriptionMode = "tooltip", grouped = false, }) => { @@ -26,4 +26,4 @@ export const TranslateToEnglish: React.FC = ({ grouped={grouped} /> ); -}; +}); diff --git a/src/components/ui/ResetButton.tsx b/src/components/ui/ResetButton.tsx new file mode 100644 index 0000000..eb98b4e --- /dev/null +++ b/src/components/ui/ResetButton.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import ResetIcon from "../icons/ResetIcon"; + +interface ResetButtonProps { + onClick: () => void; + disabled?: boolean; + className?: string; +} + +export const ResetButton: React.FC = React.memo(({ + onClick, + disabled = false, + className = "" +}) => ( + +)); \ No newline at end of file diff --git a/src/lib/constants/languages.ts b/src/lib/constants/languages.ts new file mode 100644 index 0000000..8e661fd --- /dev/null +++ b/src/lib/constants/languages.ts @@ -0,0 +1,59 @@ +export interface Language { + value: string; + label: string; +} + +export const LANGUAGES: Language[] = [ + { value: "auto", label: "Auto Detect" }, + { value: "en", label: "English" }, + { value: "zh", label: "Chinese" }, + { value: "de", label: "German" }, + { value: "es", label: "Spanish" }, + { value: "ru", label: "Russian" }, + { value: "ko", label: "Korean" }, + { value: "fr", label: "French" }, + { value: "ja", label: "Japanese" }, + { value: "pt", label: "Portuguese" }, + { value: "tr", label: "Turkish" }, + { value: "pl", label: "Polish" }, + { value: "ca", label: "Catalan" }, + { value: "nl", label: "Dutch" }, + { value: "ar", label: "Arabic" }, + { value: "sv", label: "Swedish" }, + { value: "it", label: "Italian" }, + { value: "id", label: "Indonesian" }, + { value: "hi", label: "Hindi" }, + { value: "fi", label: "Finnish" }, + { value: "vi", label: "Vietnamese" }, + { value: "he", label: "Hebrew" }, + { value: "uk", label: "Ukrainian" }, + { value: "el", label: "Greek" }, + { value: "ms", label: "Malay" }, + { value: "cs", label: "Czech" }, + { value: "ro", label: "Romanian" }, + { value: "da", label: "Danish" }, + { value: "hu", label: "Hungarian" }, + { value: "ta", label: "Tamil" }, + { value: "no", label: "Norwegian" }, + { value: "th", label: "Thai" }, + { value: "ur", label: "Urdu" }, + { value: "hr", label: "Croatian" }, + { value: "bg", label: "Bulgarian" }, + { value: "lt", label: "Lithuanian" }, + { value: "la", label: "Latin" }, + { value: "mi", label: "Maori" }, + { value: "ml", label: "Malayalam" }, + { value: "cy", label: "Welsh" }, + { value: "sk", label: "Slovak" }, + { value: "te", label: "Telugu" }, + { value: "fa", label: "Persian" }, + { value: "lv", label: "Latvian" }, + { value: "bn", label: "Bengali" }, + { value: "sr", label: "Serbian" }, + { value: "az", label: "Azerbaijani" }, + { value: "sl", label: "Slovenian" }, + { value: "kn", label: "Kannada" }, + { value: "et", label: "Estonian" }, + { value: "mk", label: "Macedonian" }, + { value: "br", label: "Breton" }, +]; \ No newline at end of file From 1f96da9381ed93fdf3cfb5911a334b77eda3dda6 Mon Sep 17 00:00:00 2001 From: Vlad Gerasimov Date: Sat, 9 Aug 2025 08:08:49 +0400 Subject: [PATCH 06/15] style(ui): unify components with shared Input/Button --- src/App.css | 2 +- src/components/settings/CorrectWords.tsx | 24 +++++++----- .../settings/MicrophoneSelector.tsx | 13 ++++--- .../settings/OutputDeviceSelector.tsx | 13 ++++--- src/components/ui/Button.tsx | 38 +++++++++++++++++++ src/components/ui/Dropdown.tsx | 4 +- src/components/ui/Input.tsx | 25 ++++++++++++ src/components/ui/SettingContainer.tsx | 2 +- 8 files changed, 98 insertions(+), 23 deletions(-) create mode 100644 src/components/ui/Button.tsx create mode 100644 src/components/ui/Input.tsx diff --git a/src/App.css b/src/App.css index a026572..c0a659d 100644 --- a/src/App.css +++ b/src/App.css @@ -12,7 +12,7 @@ :root { /* Typography */ - font-size: 16px; + font-size: 15px; line-height: 24px; font-weight: 400; diff --git a/src/components/settings/CorrectWords.tsx b/src/components/settings/CorrectWords.tsx index 1bba17d..451f2d7 100644 --- a/src/components/settings/CorrectWords.tsx +++ b/src/components/settings/CorrectWords.tsx @@ -1,5 +1,7 @@ import React, { useState } from "react"; import { useSettings } from "../../hooks/useSettings"; +import { Input } from "../ui/Input"; +import { Button } from "../ui/Button"; import { SettingContainer } from "../ui/SettingContainer"; interface CorrectWordsProps { @@ -44,39 +46,43 @@ export const CorrectWords: React.FC = React.memo(({ grouped={grouped} >
- setNewWord(e.target.value)} onKeyPress={handleKeyPress} placeholder="Add a word" - className="px-2 py-1 text-sm border border-mid-gray/30 rounded focus:outline-none focus:ring-1 focus:ring-logo-primary focus:border-transparent bg-background" + variant="compact" disabled={isUpdating("correct_words")} /> - +
{correctWords.length > 0 && (
{correctWords.map((word) => ( - + ))}
)} diff --git a/src/components/settings/MicrophoneSelector.tsx b/src/components/settings/MicrophoneSelector.tsx index dc9bccf..2b80342 100644 --- a/src/components/settings/MicrophoneSelector.tsx +++ b/src/components/settings/MicrophoneSelector.tsx @@ -33,10 +33,13 @@ export const MicrophoneSelector: React.FC = React.memo( await resetSetting("selected_microphone"); }; - const microphoneOptions = audioDevices.map(device => ({ - value: device.name, - label: device.name - })); + const microphoneOptions = [ + { value: "Default", label: "Default" }, + ...audioDevices.map(device => ({ + value: device.name, + label: device.name + })) + ]; return ( = React.memo( options={microphoneOptions} selectedValue={selectedMicrophone} onSelect={handleMicrophoneSelect} - placeholder={isLoading ? "Loading..." : "Select microphone..."} + placeholder={isLoading ? "Loading..." : ""} disabled={isUpdating("selected_microphone") || isLoading} onRefresh={refreshAudioDevices} /> diff --git a/src/components/settings/OutputDeviceSelector.tsx b/src/components/settings/OutputDeviceSelector.tsx index 156805e..a13fc64 100644 --- a/src/components/settings/OutputDeviceSelector.tsx +++ b/src/components/settings/OutputDeviceSelector.tsx @@ -34,10 +34,13 @@ export const OutputDeviceSelector: React.FC = React.m await resetSetting("selected_output_device"); }; - const outputDeviceOptions = outputDevices.map(device => ({ - value: device.name, - label: device.name - })); + const outputDeviceOptions = [ + { value: "Default", label: "Default" }, + ...outputDevices.map(device => ({ + value: device.name, + label: device.name + })) + ]; return ( = React.m options={outputDeviceOptions} selectedValue={selectedOutputDevice} onSelect={handleOutputDeviceSelect} - placeholder={isLoading ? "Loading..." : "Select output device..."} + placeholder={isLoading ? "Loading..." : ""} disabled={isUpdating("selected_output_device") || isLoading} onRefresh={refreshOutputDevices} /> diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx new file mode 100644 index 0000000..0e50734 --- /dev/null +++ b/src/components/ui/Button.tsx @@ -0,0 +1,38 @@ +import React from "react"; + +interface ButtonProps extends React.ButtonHTMLAttributes { + variant?: "primary" | "secondary" | "danger" | "ghost"; + size?: "sm" | "md" | "lg"; +} + +export const Button: React.FC = ({ + children, + className = "", + variant = "primary", + size = "md", + ...props +}) => { + const baseClasses = "font-medium rounded focus:outline-none transition-colors disabled:opacity-50 disabled:cursor-not-allowed"; + + const variantClasses = { + primary: "text-white bg-background-ui hover:bg-background-ui/90 focus:ring-1 focus:ring-background-ui", + secondary: "bg-mid-gray/10 hover:bg-background-ui/30 focus:outline-none", + danger: "text-white bg-red-600 hover:bg-red-700 focus:ring-1 focus:ring-red-500", + ghost: "text-current hover:bg-mid-gray/10 focus:bg-mid-gray/20" + }; + + const sizeClasses = { + sm: "px-2 py-1 text-xs", + md: "px-3 py-1 text-sm", + lg: "px-4 py-2 text-base" + }; + + return ( + + ); +}; \ No newline at end of file diff --git a/src/components/ui/Dropdown.tsx b/src/components/ui/Dropdown.tsx index 5ffb010..8e1cf5c 100644 --- a/src/components/ui/Dropdown.tsx +++ b/src/components/ui/Dropdown.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from "react"; +import React, { useEffect, useRef, useState } from "react"; export interface DropdownOption { value: string; @@ -95,7 +95,7 @@ export const Dropdown: React.FC = ({ type="button" className={`w-full px-2 py-1 text-sm text-left hover:bg-logo-primary/10 transition-colors duration-150 ${ selectedValue === option.value - ? "bg-logo-primary/20 text-logo-primary font-semibold" + ? "bg-logo-primary/20 font-semibold" : "" }`} onClick={() => handleSelect(option.value)} diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx new file mode 100644 index 0000000..282de34 --- /dev/null +++ b/src/components/ui/Input.tsx @@ -0,0 +1,25 @@ +import React from "react"; + +interface InputProps extends React.InputHTMLAttributes { + variant?: "default" | "compact"; +} + +export const Input: React.FC = ({ + className = "", + variant = "default", + ...props +}) => { + const baseClasses = "px-2 py-1 text-sm font-semibold bg-mid-gray/10 border border-mid-gray/80 rounded text-left flex items-center justify-between transition-all duration-150 hover:bg-logo-primary/10 cursor-pointer hover:border-logo-primary focus:outline-none focus:bg-logo-primary/20 focus:border-logo-primary"; + + const variantClasses = { + default: "px-3 py-2", + compact: "px-2 py-1" + }; + + return ( + + ); +}; \ No newline at end of file diff --git a/src/components/ui/SettingContainer.tsx b/src/components/ui/SettingContainer.tsx index 01d06d5..a50f598 100644 --- a/src/components/ui/SettingContainer.tsx +++ b/src/components/ui/SettingContainer.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from "react"; +import React, { useEffect, useRef, useState } from "react"; interface SettingContainerProps { title: string; From 46d28a06166e091a73ff5440ea5f5f0557a2d70d Mon Sep 17 00:00:00 2001 From: Vlad Gerasimov Date: Sat, 9 Aug 2025 08:10:18 +0400 Subject: [PATCH 07/15] fix(settings): remove hardcoded Default option and add placeholder text --- src/components/settings/MicrophoneSelector.tsx | 13 +++++-------- src/components/settings/OutputDeviceSelector.tsx | 13 +++++-------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/components/settings/MicrophoneSelector.tsx b/src/components/settings/MicrophoneSelector.tsx index 2b80342..dc9bccf 100644 --- a/src/components/settings/MicrophoneSelector.tsx +++ b/src/components/settings/MicrophoneSelector.tsx @@ -33,13 +33,10 @@ export const MicrophoneSelector: React.FC = React.memo( await resetSetting("selected_microphone"); }; - const microphoneOptions = [ - { value: "Default", label: "Default" }, - ...audioDevices.map(device => ({ - value: device.name, - label: device.name - })) - ]; + const microphoneOptions = audioDevices.map(device => ({ + value: device.name, + label: device.name + })); return ( = React.memo( options={microphoneOptions} selectedValue={selectedMicrophone} onSelect={handleMicrophoneSelect} - placeholder={isLoading ? "Loading..." : ""} + placeholder={isLoading ? "Loading..." : "Select microphone..."} disabled={isUpdating("selected_microphone") || isLoading} onRefresh={refreshAudioDevices} /> diff --git a/src/components/settings/OutputDeviceSelector.tsx b/src/components/settings/OutputDeviceSelector.tsx index a13fc64..156805e 100644 --- a/src/components/settings/OutputDeviceSelector.tsx +++ b/src/components/settings/OutputDeviceSelector.tsx @@ -34,13 +34,10 @@ export const OutputDeviceSelector: React.FC = React.m await resetSetting("selected_output_device"); }; - const outputDeviceOptions = [ - { value: "Default", label: "Default" }, - ...outputDevices.map(device => ({ - value: device.name, - label: device.name - })) - ]; + const outputDeviceOptions = outputDevices.map(device => ({ + value: device.name, + label: device.name + })); return ( = React.m options={outputDeviceOptions} selectedValue={selectedOutputDevice} onSelect={handleOutputDeviceSelect} - placeholder={isLoading ? "Loading..." : ""} + placeholder={isLoading ? "Loading..." : "Select output device..."} disabled={isUpdating("selected_output_device") || isLoading} onRefresh={refreshOutputDevices} /> From 24e0319256800f8a4d25aebe658ca4922ef5f472 Mon Sep 17 00:00:00 2001 From: Vlad Gerasimov Date: Sat, 9 Aug 2025 08:21:43 +0400 Subject: [PATCH 08/15] fix(settings): handle default device labels and empty device lists --- src/components/icons/ResetIcon.tsx | 2 +- src/components/settings/MicrophoneSelector.tsx | 7 ++++--- src/components/settings/OutputDeviceSelector.tsx | 7 +++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/icons/ResetIcon.tsx b/src/components/icons/ResetIcon.tsx index 09f2e46..eb7db62 100644 --- a/src/components/icons/ResetIcon.tsx +++ b/src/components/icons/ResetIcon.tsx @@ -1,7 +1,7 @@ const ResetIcon = () => { return ( - + ); }; diff --git a/src/components/settings/MicrophoneSelector.tsx b/src/components/settings/MicrophoneSelector.tsx index dc9bccf..f37ab38 100644 --- a/src/components/settings/MicrophoneSelector.tsx +++ b/src/components/settings/MicrophoneSelector.tsx @@ -23,7 +23,8 @@ export const MicrophoneSelector: React.FC = React.memo( refreshAudioDevices, } = useSettings(); - const selectedMicrophone = getSetting("selected_microphone") || "Default"; + const selectedMicrophone = getSetting("selected_microphone") === "default" ? "Default" : getSetting("selected_microphone") || "Default"; + const handleMicrophoneSelect = async (deviceName: string) => { await updateSetting("selected_microphone", deviceName); @@ -50,8 +51,8 @@ export const MicrophoneSelector: React.FC = React.memo( options={microphoneOptions} selectedValue={selectedMicrophone} onSelect={handleMicrophoneSelect} - placeholder={isLoading ? "Loading..." : "Select microphone..."} - disabled={isUpdating("selected_microphone") || isLoading} + placeholder={isLoading || audioDevices.length === 0 ? "Loading..." : "Select microphone..."} + disabled={isUpdating("selected_microphone") || isLoading || audioDevices.length === 0} onRefresh={refreshAudioDevices} /> = React.m refreshOutputDevices, } = useSettings(); - const selectedOutputDevice = - getSetting("selected_output_device") || "Default"; + const selectedOutputDevice = getSetting("selected_output_device") === "default" ? "Default" : getSetting("selected_output_device") || "Default"; const handleOutputDeviceSelect = async (deviceName: string) => { await updateSetting("selected_output_device", deviceName); @@ -51,8 +50,8 @@ export const OutputDeviceSelector: React.FC = React.m options={outputDeviceOptions} selectedValue={selectedOutputDevice} onSelect={handleOutputDeviceSelect} - placeholder={isLoading ? "Loading..." : "Select output device..."} - disabled={isUpdating("selected_output_device") || isLoading} + placeholder={isLoading || outputDevices.length === 0 ? "Loading..." : "Select output device..."} + disabled={isUpdating("selected_output_device") || isLoading || outputDevices.length === 0} onRefresh={refreshOutputDevices} /> Date: Sat, 9 Aug 2025 09:16:35 +0400 Subject: [PATCH 09/15] during startup, if the dev server at localhost:1420 isn't running, you get a blank settings window --- src-tauri/tauri.conf.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 6a95c9d..977b950 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -19,7 +19,8 @@ "minWidth": 540, "minHeight": 720, "resizable": true, - "maximizable": false + "maximizable": false, + "visible": false } ], "security": { From bc9617beb2ebc1acfb05614294df4b9050b847da Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Mon, 11 Aug 2025 09:32:36 -0700 Subject: [PATCH 10/15] Use proper cursor for input --- src/components/ui/Input.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx index 282de34..e1b09cd 100644 --- a/src/components/ui/Input.tsx +++ b/src/components/ui/Input.tsx @@ -9,7 +9,7 @@ export const Input: React.FC = ({ variant = "default", ...props }) => { - const baseClasses = "px-2 py-1 text-sm font-semibold bg-mid-gray/10 border border-mid-gray/80 rounded text-left flex items-center justify-between transition-all duration-150 hover:bg-logo-primary/10 cursor-pointer hover:border-logo-primary focus:outline-none focus:bg-logo-primary/20 focus:border-logo-primary"; + const baseClasses = "px-2 py-1 text-sm font-semibold bg-mid-gray/10 border border-mid-gray/80 rounded text-left flex items-center justify-between transition-all duration-150 hover:bg-logo-primary/10 hover:border-logo-primary focus:outline-none focus:bg-logo-primary/20 focus:border-logo-primary"; const variantClasses = { default: "px-3 py-2", From a252655b2f7b95352710a99b23e911fb8e909b96 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Mon, 11 Aug 2025 15:09:39 -0700 Subject: [PATCH 11/15] reset icon color --- src/components/icons/ResetIcon.tsx | 33 ++++++++++++++++++++++++++++-- src/components/ui/ResetButton.tsx | 24 ++++++++++------------ 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/components/icons/ResetIcon.tsx b/src/components/icons/ResetIcon.tsx index eb7db62..c1dbf82 100644 --- a/src/components/icons/ResetIcon.tsx +++ b/src/components/icons/ResetIcon.tsx @@ -1,7 +1,36 @@ +import React from "react"; -const ResetIcon = () => { +interface ResetIconProps { + width?: number; + height?: number; + color?: string; + className?: string; +} + +const ResetIcon: React.FC = ({ + width = 20, + height = 20, + className = "", +}) => { return ( - + + + + + + ); }; diff --git a/src/components/ui/ResetButton.tsx b/src/components/ui/ResetButton.tsx index eb98b4e..4f8a7d3 100644 --- a/src/components/ui/ResetButton.tsx +++ b/src/components/ui/ResetButton.tsx @@ -7,16 +7,14 @@ interface ResetButtonProps { className?: string; } -export const ResetButton: React.FC = React.memo(({ - onClick, - disabled = false, - className = "" -}) => ( - -)); \ No newline at end of file +export const ResetButton: React.FC = React.memo( + ({ onClick, disabled = false, className = "" }) => ( + + ), +); From c2b3815f56d988f3e9d71a1e9ecb89203c9eae9a Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Mon, 11 Aug 2025 15:19:48 -0700 Subject: [PATCH 12/15] 'custom words' everywhere instead of 'correct words' --- src-tauri/src/lib.rs | 2 +- src-tauri/src/managers/transcription.rs | 77 +++++++++------ src-tauri/src/settings.rs | 4 +- src-tauri/src/shortcut.rs | 8 +- src/components/settings/CorrectWords.tsx | 91 ------------------ src/components/settings/CustomWords.tsx | 115 +++++++++++++++++++++++ src/components/settings/Settings.tsx | 4 +- src/components/settings/index.ts | 2 +- src/hooks/useSettings.ts | 10 +- src/lib/types.ts | 2 +- 10 files changed, 180 insertions(+), 135 deletions(-) delete mode 100644 src/components/settings/CorrectWords.tsx create mode 100644 src/components/settings/CustomWords.tsx diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9bc0702..a7761d4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -187,7 +187,7 @@ pub fn run() { shortcut::change_selected_language_setting, shortcut::change_overlay_position_setting, shortcut::change_debug_mode_setting, - shortcut::update_correct_words, + shortcut::update_custom_words, shortcut::suspend_binding, shortcut::resume_binding, trigger_update_check, diff --git a/src-tauri/src/managers/transcription.rs b/src-tauri/src/managers/transcription.rs index 9f70501..51ea000 100644 --- a/src-tauri/src/managers/transcription.rs +++ b/src-tauri/src/managers/transcription.rs @@ -27,22 +27,22 @@ pub struct TranscriptionManager { current_model_id: Mutex>, } -fn correct_words(text: &str, correct_words: &[String]) -> String { - if correct_words.is_empty() { +fn apply_custom_words(text: &str, custom_words: &[String]) -> String { + if custom_words.is_empty() { return text.to_string(); } // Pre-compute lowercase versions to avoid repeated allocations - let correct_words_lower: Vec = correct_words.iter() - .map(|w| w.to_lowercase()) - .collect(); + let custom_words_lower: Vec = custom_words.iter().map(|w| w.to_lowercase()).collect(); let words: Vec<&str> = text.split_whitespace().collect(); let mut corrected_words = Vec::new(); for word in words { - let cleaned_word = word.trim_matches(|c: char| !c.is_alphabetic()).to_lowercase(); - + let cleaned_word = word + .trim_matches(|c: char| !c.is_alphabetic()) + .to_lowercase(); + if cleaned_word.is_empty() { corrected_words.push(word.to_string()); continue; @@ -57,31 +57,35 @@ fn correct_words(text: &str, correct_words: &[String]) -> String { let mut best_match: Option<&String> = None; let mut best_score = f64::MAX; - for (i, correct_word_lower) in correct_words_lower.iter().enumerate() { + for (i, custom_word_lower) in custom_words_lower.iter().enumerate() { // Skip if lengths are too different (optimization) - let len_diff = (cleaned_word.len() as i32 - correct_word_lower.len() as i32).abs(); + let len_diff = (cleaned_word.len() as i32 - custom_word_lower.len() as i32).abs(); if len_diff > 5 { continue; } - + // Calculate Levenshtein distance (normalized by length) - let levenshtein_dist = levenshtein(&cleaned_word, correct_word_lower); - let max_len = cleaned_word.len().max(correct_word_lower.len()) as f64; - let levenshtein_score = if max_len > 0.0 { levenshtein_dist as f64 / max_len } else { 1.0 }; - + let levenshtein_dist = levenshtein(&cleaned_word, custom_word_lower); + let max_len = cleaned_word.len().max(custom_word_lower.len()) as f64; + let levenshtein_score = if max_len > 0.0 { + levenshtein_dist as f64 / max_len + } else { + 1.0 + }; + // Calculate phonetic similarity using Soundex - let phonetic_match = soundex(&cleaned_word, correct_word_lower); - + let phonetic_match = soundex(&cleaned_word, custom_word_lower); + // Combine scores: favor phonetic matches, but also consider string similarity let combined_score = if phonetic_match { - levenshtein_score * 0.3 // Give significant boost to phonetic matches + levenshtein_score * 0.3 // Give significant boost to phonetic matches } else { levenshtein_score }; - + // Accept if the score is good enough (threshold: 0.4 for good matches) if combined_score < 0.4 && combined_score < best_score { - best_match = Some(&correct_words[i]); + best_match = Some(&custom_words[i]); best_score = combined_score; } } @@ -99,15 +103,30 @@ fn correct_words(text: &str, correct_words: &[String]) -> String { } else { replacement.clone() }; - + // Preserve punctuation from original word - optimized version let prefix_end = word.chars().take_while(|c| !c.is_alphabetic()).count(); - let suffix_start = word.char_indices().rev().take_while(|(_, c)| !c.is_alphabetic()).count(); - - let original_prefix = if prefix_end > 0 { &word[..prefix_end] } else { "" }; - let original_suffix = if suffix_start > 0 { &word[word.len() - suffix_start..] } else { "" }; - - corrected_words.push(format!("{}{}{}", original_prefix, corrected, original_suffix)); + let suffix_start = word + .char_indices() + .rev() + .take_while(|(_, c)| !c.is_alphabetic()) + .count(); + + let original_prefix = if prefix_end > 0 { + &word[..prefix_end] + } else { + "" + }; + let original_suffix = if suffix_start > 0 { + &word[word.len() - suffix_start..] + } else { + "" + }; + + corrected_words.push(format!( + "{}{}{}", + original_prefix, corrected, original_suffix + )); } else { corrected_words.push(word.to_string()); } @@ -298,9 +317,9 @@ impl TranscriptionManager { result.push_str(&segment); } - // Apply word correction if correct words are configured - let corrected_result = if !settings.correct_words.is_empty() { - correct_words(&result, &settings.correct_words) + // Apply word correction if custom words are configured + let corrected_result = if !settings.custom_words.is_empty() { + apply_custom_words(&result, &settings.custom_words) } else { result }; diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 39863d3..6b08cf8 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -43,7 +43,7 @@ pub struct AppSettings { #[serde(default = "default_debug_mode")] pub debug_mode: bool, #[serde(default)] - pub correct_words: Vec, + pub custom_words: Vec, } fn default_model() -> String { @@ -106,7 +106,7 @@ pub fn get_default_settings() -> AppSettings { selected_language: "auto".to_string(), overlay_position: OverlayPosition::Bottom, debug_mode: false, - correct_words: Vec::new(), + custom_words: Vec::new(), } } diff --git a/src-tauri/src/shortcut.rs b/src-tauri/src/shortcut.rs index 30eca98..d1db091 100644 --- a/src-tauri/src/shortcut.rs +++ b/src-tauri/src/shortcut.rs @@ -148,10 +148,10 @@ pub fn change_overlay_position_setting(app: AppHandle, position: String) -> Resu }; settings.overlay_position = parsed; settings::write_settings(&app, settings); - + // Update overlay position without recreating window crate::utils::update_overlay_position(&app); - + Ok(()) } @@ -164,9 +164,9 @@ pub fn change_debug_mode_setting(app: AppHandle, enabled: bool) -> Result<(), St } #[tauri::command] -pub fn update_correct_words(app: AppHandle, words: Vec) -> Result<(), String> { +pub fn update_custom_words(app: AppHandle, words: Vec) -> Result<(), String> { let mut settings = settings::get_settings(&app); - settings.correct_words = words; + settings.custom_words = words; settings::write_settings(&app, settings); Ok(()) } diff --git a/src/components/settings/CorrectWords.tsx b/src/components/settings/CorrectWords.tsx deleted file mode 100644 index 451f2d7..0000000 --- a/src/components/settings/CorrectWords.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { useState } from "react"; -import { useSettings } from "../../hooks/useSettings"; -import { Input } from "../ui/Input"; -import { Button } from "../ui/Button"; -import { SettingContainer } from "../ui/SettingContainer"; - -interface CorrectWordsProps { - descriptionMode?: "inline" | "tooltip"; - grouped?: boolean; -} - -export const CorrectWords: React.FC = React.memo(({ - descriptionMode = "tooltip", - grouped = false, -}) => { - const { getSetting, updateSetting, isUpdating } = useSettings(); - const [newWord, setNewWord] = useState(""); - const correctWords = getSetting("correct_words") || []; - - const handleAddWord = () => { - const trimmedWord = newWord.trim(); - const sanitizedWord = trimmedWord.replace(/[<>"'&]/g, ''); - if (sanitizedWord && !sanitizedWord.includes(' ') && sanitizedWord.length <= 50 && !correctWords.includes(sanitizedWord)) { - updateSetting("correct_words", [...correctWords, sanitizedWord]); - setNewWord(""); - } - }; - - const handleRemoveWord = (wordToRemove: string) => { - updateSetting("correct_words", correctWords.filter((word) => word !== wordToRemove)); - }; - - const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - e.preventDefault(); - handleAddWord(); - } - }; - - return ( - <> - -
- setNewWord(e.target.value)} - onKeyPress={handleKeyPress} - placeholder="Add a word" - variant="compact" - disabled={isUpdating("correct_words")} - /> - -
-
- {correctWords.length > 0 && ( -
- {correctWords.map((word) => ( - - ))} -
- )} - - ); -}); \ No newline at end of file diff --git a/src/components/settings/CustomWords.tsx b/src/components/settings/CustomWords.tsx new file mode 100644 index 0000000..c5d2710 --- /dev/null +++ b/src/components/settings/CustomWords.tsx @@ -0,0 +1,115 @@ +import React, { useState } from "react"; +import { useSettings } from "../../hooks/useSettings"; +import { Input } from "../ui/Input"; +import { Button } from "../ui/Button"; +import { SettingContainer } from "../ui/SettingContainer"; + +interface CustomWordsProps { + descriptionMode?: "inline" | "tooltip"; + grouped?: boolean; +} + +export const CustomWords: React.FC = React.memo( + ({ descriptionMode = "tooltip", grouped = false }) => { + const { getSetting, updateSetting, isUpdating } = useSettings(); + const [newWord, setNewWord] = useState(""); + const customWords = getSetting("custom_words") || []; + + const handleAddWord = () => { + const trimmedWord = newWord.trim(); + const sanitizedWord = trimmedWord.replace(/[<>"'&]/g, ""); + if ( + sanitizedWord && + !sanitizedWord.includes(" ") && + sanitizedWord.length <= 50 && + !customWords.includes(sanitizedWord) + ) { + updateSetting("custom_words", [...customWords, sanitizedWord]); + setNewWord(""); + } + }; + + const handleRemoveWord = (wordToRemove: string) => { + updateSetting( + "custom_words", + customWords.filter((word) => word !== wordToRemove), + ); + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + handleAddWord(); + } + }; + + return ( + <> + +
+ setNewWord(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="Add a word" + variant="compact" + disabled={isUpdating("custom_words")} + /> + +
+
+ {customWords.length > 0 && ( +
+ {customWords.map((word) => ( + + ))} +
+ )} + + ); + }, +); diff --git a/src/components/settings/Settings.tsx b/src/components/settings/Settings.tsx index fa530f6..ca83a8d 100644 --- a/src/components/settings/Settings.tsx +++ b/src/components/settings/Settings.tsx @@ -8,7 +8,7 @@ import { ShowOverlay } from "./ShowOverlay"; import { HandyShortcut } from "./HandyShortcut"; import { TranslateToEnglish } from "./TranslateToEnglish"; import { LanguageSelector } from "./LanguageSelector"; -import { CorrectWords } from "./CorrectWords"; +import { CustomWords } from "./CustomWords"; import { SettingsGroup } from "../ui/SettingsGroup"; import { DebugSettings } from "./debug"; import { useSettings } from "../../hooks/useSettings"; @@ -55,7 +55,7 @@ export const Settings: React.FC = () => { - + diff --git a/src/components/settings/index.ts b/src/components/settings/index.ts index cd7dd78..4838851 100644 --- a/src/components/settings/index.ts +++ b/src/components/settings/index.ts @@ -7,4 +7,4 @@ export { AudioFeedback } from "./AudioFeedback"; export { ShowOverlay } from "./ShowOverlay"; export { HandyShortcut } from "./HandyShortcut"; export { TranslateToEnglish } from "./TranslateToEnglish"; -export { CorrectWords } from "./CorrectWords"; +export { CustomWords } from "./CustomWords"; diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 5ad26b4..7b64811 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -200,13 +200,15 @@ export const useSettings = (): UseSettingsReturn => { }); break; case "overlay_position": - await invoke("change_overlay_position_setting", { position: value }); + await invoke("change_overlay_position_setting", { + position: value, + }); break; case "debug_mode": await invoke("change_debug_mode_setting", { enabled: value }); break; - case "correct_words": - await invoke("update_correct_words", { words: value }); + case "custom_words": + await invoke("update_custom_words", { words: value }); break; case "bindings": // Handle bindings separately - they use their own invoke methods @@ -254,7 +256,7 @@ export const useSettings = (): UseSettingsReturn => { selected_language: "auto", overlay_position: "bottom", debug_mode: false, - correct_words: [], + custom_words: [], }; const defaultValue = defaults[key]; diff --git a/src/lib/types.ts b/src/lib/types.ts index 7979b04..cde2622 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -34,7 +34,7 @@ export const SettingsSchema = z.object({ selected_language: z.string(), overlay_position: OverlayPositionSchema, debug_mode: z.boolean(), - correct_words: z.array(z.string()).optional().default([]), + custom_words: z.array(z.string()).optional().default([]), }); export const BindingResponseSchema = z.object({ From 6be48caf82f29dbea34dc93826ef23aff45476db Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Mon, 11 Aug 2025 15:28:26 -0700 Subject: [PATCH 13/15] slightly tweak styling --- src/components/settings/CustomWords.tsx | 4 ++-- src/components/ui/Button.tsx | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/settings/CustomWords.tsx b/src/components/settings/CustomWords.tsx index c5d2710..0f059d3 100644 --- a/src/components/settings/CustomWords.tsx +++ b/src/components/settings/CustomWords.tsx @@ -54,10 +54,10 @@ export const CustomWords: React.FC = React.memo(
setNewWord(e.target.value)} - onKeyPress={handleKeyPress} + onKeyDown={handleKeyPress} placeholder="Add a word" variant="compact" disabled={isUpdating("custom_words")} diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 0e50734..13462ca 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -12,19 +12,22 @@ export const Button: React.FC = ({ size = "md", ...props }) => { - const baseClasses = "font-medium rounded focus:outline-none transition-colors disabled:opacity-50 disabled:cursor-not-allowed"; - + const baseClasses = + "font-medium rounded focus:outline-none transition-colors disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"; + const variantClasses = { - primary: "text-white bg-background-ui hover:bg-background-ui/90 focus:ring-1 focus:ring-background-ui", + primary: + "text-white bg-background-ui hover:bg-background-ui/90 focus:ring-1 focus:ring-background-ui", secondary: "bg-mid-gray/10 hover:bg-background-ui/30 focus:outline-none", - danger: "text-white bg-red-600 hover:bg-red-700 focus:ring-1 focus:ring-red-500", - ghost: "text-current hover:bg-mid-gray/10 focus:bg-mid-gray/20" + danger: + "text-white bg-red-600 hover:bg-red-700 focus:ring-1 focus:ring-red-500", + ghost: "text-current hover:bg-mid-gray/10 focus:bg-mid-gray/20", }; const sizeClasses = { sm: "px-2 py-1 text-xs", - md: "px-3 py-1 text-sm", - lg: "px-4 py-2 text-base" + md: "px-4 py-[5px] text-sm", + lg: "px-4 py-2 text-base", }; return ( @@ -35,4 +38,4 @@ export const Button: React.FC = ({ {children} ); -}; \ No newline at end of file +}; From 1b6f95918d06c3088dce4f6b97492cd4059c0e32 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Mon, 11 Aug 2025 15:30:34 -0700 Subject: [PATCH 14/15] tweak min height for a single row of custom words by default. --- src-tauri/tauri.conf.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 977b950..c3bb863 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -15,9 +15,9 @@ { "title": "Handy", "width": 540, - "height": 720, + "height": 730, "minWidth": 540, - "minHeight": 720, + "minHeight": 730, "resizable": true, "maximizable": false, "visible": false From 174e7d4bbbe8b80bc70b66ecb1657c8c0536ae24 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Mon, 11 Aug 2025 15:44:19 -0700 Subject: [PATCH 15/15] remove visible for now --- src-tauri/tauri.conf.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index c3bb863..97a22c4 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -19,8 +19,7 @@ "minWidth": 540, "minHeight": 730, "resizable": true, - "maximizable": false, - "visible": false + "maximizable": false } ], "security": {