diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 68b5a4a..3c29ee4 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -25,20 +25,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.4", - "once_cell", - "serde", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -380,12 +366,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.21.7" @@ -433,7 +413,7 @@ dependencies = [ "bitflags 2.10.0", "cexpr", "clang-sys", - "itertools 0.12.1", + "itertools", "lazy_static", "lazycell", "log", @@ -697,15 +677,6 @@ dependencies = [ "toml 0.9.8", ] -[[package]] -name = "castaway" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - [[package]] name = "cbindgen" version = "0.29.2" @@ -912,21 +883,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "compact_str" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "serde", - "static_assertions", -] - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -936,19 +892,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "console" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" -dependencies = [ - "encode_unicode", - "libc", - "once_cell", - "unicode-width", - "windows-sys 0.61.2", -] - [[package]] name = "convert_case" version = "0.4.0" @@ -1119,25 +1062,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -1267,15 +1191,6 @@ dependencies = [ "syn 2.0.108", ] -[[package]] -name = "dary_heap" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06d2e3287df1c007e74221c49ca10a95d557349e54b3a75dc2fb14712c751f04" -dependencies = [ - "serde", -] - [[package]] name = "dasp_sample" version = "0.11.0" @@ -1526,12 +1441,6 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - [[package]] name = "encoding_rs" version = "0.8.35" @@ -1675,15 +1584,6 @@ version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" -[[package]] -name = "esaxx-rs" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" -dependencies = [ - "cc", -] - [[package]] name = "event-listener" version = "5.4.1" @@ -2449,7 +2349,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash 0.7.8", + "ahash", ] [[package]] @@ -2850,19 +2750,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "indicatif" -version = "0.18.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" -dependencies = [ - "console", - "portable-atomic", - "unicode-width", - "unit-prefix", - "web-time", -] - [[package]] name = "infer" version = "0.19.0" @@ -2953,15 +2840,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.15" @@ -3219,22 +3097,6 @@ dependencies = [ "core-foundation-sys", ] -[[package]] -name = "macro_rules_attribute" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520" -dependencies = [ - "macro_rules_attribute-proc_macro", - "paste", -] - -[[package]] -name = "macro_rules_attribute-proc_macro" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" - [[package]] name = "malloc_buf" version = "0.0.6" @@ -3370,28 +3232,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "monostate" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3341a273f6c9d5bef1908f17b7267bbab0e95c9bf69a0d4dcf8e9e1b2c76ef67" -dependencies = [ - "monostate-impl", - "serde", - "serde_core", -] - -[[package]] -name = "monostate-impl" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.108", -] - [[package]] name = "moxcms" version = "0.7.9" @@ -3968,28 +3808,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "onig" -version = "6.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" -dependencies = [ - "bitflags 2.10.0", - "libc", - "once_cell", - "onig_sys", -] - -[[package]] -name = "onig_sys" -version = "69.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" -dependencies = [ - "cc", - "pkg-config", -] - [[package]] name = "open" version = "5.3.2" @@ -4819,37 +4637,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" -[[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-cond" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964d0cf57a3e7a06e8183d14a8b527195c706b7983549cd5462d5aa3747438f" -dependencies = [ - "either", - "itertools 0.14.0", - "rayon", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - [[package]] name = "rdev" version = "0.5.0-2" @@ -5751,18 +5538,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "spm_precompiled" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326" -dependencies = [ - "base64 0.13.1", - "nom 7.1.3", - "serde", - "unicode-segmentation", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -6789,40 +6564,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "tokenizers" -version = "0.22.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b238e22d44a15349529690fb07bd645cf58149a1b1e44d6cb5bd1641ff1a6223" -dependencies = [ - "ahash 0.8.12", - "aho-corasick", - "compact_str", - "dary_heap", - "derive_builder", - "esaxx-rs", - "getrandom 0.3.4", - "indicatif", - "itertools 0.14.0", - "log", - "macro_rules_attribute", - "monostate", - "onig", - "paste", - "rand 0.9.2", - "rayon", - "rayon-cond", - "regex", - "regex-syntax", - "serde", - "serde_json", - "spm_precompiled", - "thiserror 2.0.17", - "unicode-normalization-alignments", - "unicode-segmentation", - "unicode_categories", -] - [[package]] name = "tokio" version = "1.48.0" @@ -7056,9 +6797,9 @@ dependencies = [ [[package]] name = "transcribe-rs" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d8fb085c53485dfaa3f0e377c2c8f5e4affb991ddf7cb04fe32e49fc3e81bc4" +checksum = "2a15e55d3bd4f74a474b7ff2c87ab416130223db490102c5ad32c6e2227686b0" dependencies = [ "derive_builder", "env_logger", @@ -7071,7 +6812,6 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.17", - "tokenizers", "whisper-rs", ] @@ -7195,39 +6935,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" -[[package]] -name = "unicode-normalization-alignments" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f613e4fa046e69818dd287fdc4bc78175ff20331479dab6e1b0f98d57062de" -dependencies = [ - "smallvec 1.15.1", -] - [[package]] name = "unicode-segmentation" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - -[[package]] -name = "unit-prefix" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" - [[package]] name = "untrusted" version = "0.9.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 625e90d..b2e0450 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -67,7 +67,7 @@ chrono = "0.4" rusqlite = { version = "0.37", features = ["bundled"] } tar = "0.4.44" flate2 = "1.0" -transcribe-rs = { version = "0.2", features = ["whisper", "parakeet", "moonshine"] } +transcribe-rs = { version = "0.2.2", features = ["whisper", "parakeet", "moonshine"] } ferrous-opencc = "0.2.3" specta = "=2.0.0-rc.22" specta-typescript = "0.0.9" diff --git a/src/components/settings/LanguageSelector.tsx b/src/components/settings/LanguageSelector.tsx index a8cf3fe..c011e4d 100644 --- a/src/components/settings/LanguageSelector.tsx +++ b/src/components/settings/LanguageSelector.tsx @@ -1,10 +1,8 @@ import React, { useState, useRef, useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { listen } from "@tauri-apps/api/event"; import { SettingContainer } from "../ui/SettingContainer"; import { ResetButton } from "../ui/ResetButton"; import { useSettings } from "../../hooks/useSettings"; -import { useModels } from "../../hooks/useModels"; import { LANGUAGES } from "../../lib/constants/languages"; interface LanguageSelectorProps { @@ -12,22 +10,18 @@ interface LanguageSelectorProps { grouped?: boolean; } -const unsupportedModels = ["parakeet-tdt-0.6b-v2", "parakeet-tdt-0.6b-v3"]; - export const LanguageSelector: React.FC = ({ descriptionMode = "tooltip", grouped = false, }) => { const { t } = useTranslation(); const { getSetting, updateSetting, resetSetting, isUpdating } = useSettings(); - const { currentModel, loadCurrentModel } = useModels(); const [isOpen, setIsOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const dropdownRef = useRef(null); const searchInputRef = useRef(null); const selectedLanguage = getSetting("selected_language") || "auto"; - const isUnsupported = unsupportedModels.includes(currentModel); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -46,17 +40,6 @@ export const LanguageSelector: React.FC = ({ }; }, []); - // Listen for model state changes to update UI reactively - useEffect(() => { - const modelStateUnlisten = listen("model-state-changed", () => { - loadCurrentModel(); - }); - - return () => { - modelStateUnlisten.then((fn) => fn()); - }; - }, [loadCurrentModel]); - useEffect(() => { if (isOpen && searchInputRef.current) { searchInputRef.current.focus(); @@ -71,10 +54,9 @@ export const LanguageSelector: React.FC = ({ [searchQuery], ); - const selectedLanguageName = isUnsupported - ? t("settings.general.language.auto") - : LANGUAGES.find((lang) => lang.value === selectedLanguage)?.label || - t("settings.general.language.auto"); + const selectedLanguageName = + LANGUAGES.find((lang) => lang.value === selectedLanguage)?.label || + t("settings.general.language.auto"); const handleLanguageSelect = async (languageCode: string) => { await updateSetting("selected_language", languageCode); @@ -87,7 +69,7 @@ export const LanguageSelector: React.FC = ({ }; const handleToggle = () => { - if (isUpdating("selected_language") || isUnsupported) return; + if (isUpdating("selected_language")) return; setIsOpen(!isOpen); }; @@ -108,26 +90,21 @@ export const LanguageSelector: React.FC = ({ return (
- {isOpen && !isUpdating("selected_language") && !isUnsupported && ( + {isOpen && !isUpdating("selected_language") && (
{/* Search input */}
@@ -191,7 +168,7 @@ export const LanguageSelector: React.FC = ({
{isUpdating("selected_language") && ( diff --git a/src/components/settings/TranslateToEnglish.tsx b/src/components/settings/TranslateToEnglish.tsx index b236cf0..5280dd2 100644 --- a/src/components/settings/TranslateToEnglish.tsx +++ b/src/components/settings/TranslateToEnglish.tsx @@ -1,66 +1,27 @@ -import React, { useEffect, useMemo } from "react"; +import React from "react"; import { useTranslation } from "react-i18next"; -import { listen } from "@tauri-apps/api/event"; import { ToggleSwitch } from "../ui/ToggleSwitch"; import { useSettings } from "../../hooks/useSettings"; -import { useModels } from "../../hooks/useModels"; interface TranslateToEnglishProps { descriptionMode?: "inline" | "tooltip"; grouped?: boolean; } -const unsupportedTranslationModels = [ - "parakeet-tdt-0.6b-v2", - "parakeet-tdt-0.6b-v3", - "turbo", -]; - export const TranslateToEnglish: React.FC = React.memo( ({ descriptionMode = "tooltip", grouped = false }) => { const { t } = useTranslation(); const { getSetting, updateSetting, isUpdating } = useSettings(); - const { currentModel, loadCurrentModel, models } = useModels(); const translateToEnglish = getSetting("translate_to_english") || false; - const isDisabledTranslation = - unsupportedTranslationModels.includes(currentModel); - - const description = useMemo(() => { - if (isDisabledTranslation) { - const currentModelDisplayName = models.find( - (model) => model.id === currentModel, - )?.name; - return t( - "settings.advanced.translateToEnglish.descriptionUnsupported", - { - model: currentModelDisplayName, - }, - ); - } - - return t("settings.advanced.translateToEnglish.description"); - }, [t, models, currentModel, isDisabledTranslation]); - - // Listen for model state changes to update UI reactively - useEffect(() => { - const modelStateUnlisten = listen("model-state-changed", () => { - loadCurrentModel(); - }); - - return () => { - modelStateUnlisten.then((fn) => fn()); - }; - }, [loadCurrentModel]); return ( updateSetting("translate_to_english", enabled)} isUpdating={isUpdating("translate_to_english")} - disabled={isDisabledTranslation} label={t("settings.advanced.translateToEnglish.label")} - description={description} + description={t("settings.advanced.translateToEnglish.description")} descriptionMode={descriptionMode} grouped={grouped} /> diff --git a/src/components/settings/advanced/AdvancedSettings.tsx b/src/components/settings/advanced/AdvancedSettings.tsx index 7f3bd3c..779bb97 100644 --- a/src/components/settings/advanced/AdvancedSettings.tsx +++ b/src/components/settings/advanced/AdvancedSettings.tsx @@ -9,9 +9,14 @@ import { StartHidden } from "../StartHidden"; import { AutostartToggle } from "../AutostartToggle"; import { PasteMethodSetting } from "../PasteMethod"; import { ClipboardHandlingSetting } from "../ClipboardHandling"; +import { useModelStore } from "../../../stores/modelStore"; export const AdvancedSettings: React.FC = () => { const { t } = useTranslation(); + const { currentModel, getModelInfo } = useModelStore(); + const currentModelInfo = getModelInfo(currentModel); + const showTranslateToEnglish = + currentModelInfo?.engine_type === "Whisper" && currentModel !== "turbo"; return (
@@ -20,7 +25,9 @@ export const AdvancedSettings: React.FC = () => { - + {showTranslateToEnglish && ( + + )} diff --git a/src/components/settings/general/GeneralSettings.tsx b/src/components/settings/general/GeneralSettings.tsx index b3a0ad6..dd511c0 100644 --- a/src/components/settings/general/GeneralSettings.tsx +++ b/src/components/settings/general/GeneralSettings.tsx @@ -8,16 +8,22 @@ import { OutputDeviceSelector } from "../OutputDeviceSelector"; import { PushToTalk } from "../PushToTalk"; import { AudioFeedback } from "../AudioFeedback"; import { useSettings } from "../../../hooks/useSettings"; +import { useModelStore } from "../../../stores/modelStore"; import { VolumeSlider } from "../VolumeSlider"; export const GeneralSettings: React.FC = () => { const { t } = useTranslation(); const { audioFeedbackEnabled } = useSettings(); + const { currentModel, getModelInfo } = useModelStore(); + const currentModelInfo = getModelInfo(currentModel); + const showLanguageSelector = currentModelInfo?.engine_type === "Whisper"; return (
- + {showLanguageSelector && ( + + )} diff --git a/src/hooks/useModels.ts b/src/hooks/useModels.ts deleted file mode 100644 index 2e804a1..0000000 --- a/src/hooks/useModels.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { useEffect, useState } from "react"; -import { listen } from "@tauri-apps/api/event"; -import { commands, type ModelInfo } from "@/bindings"; - -interface DownloadProgress { - model_id: string; - downloaded: number; - total: number; - percentage: number; -} - -export const useModels = () => { - const [models, setModels] = useState([]); - const [currentModel, setCurrentModel] = useState(""); - const [downloadingModels, setDownloadingModels] = useState>( - new Set(), - ); - const [extractingModels, setExtractingModels] = useState>( - new Set(), - ); - const [downloadProgress, setDownloadProgress] = useState< - Map - >(new Map()); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [hasAnyModels, setHasAnyModels] = useState(false); - const [isFirstRun, setIsFirstRun] = useState(false); - - const loadModels = async () => { - try { - const result = await commands.getAvailableModels(); - if (result.status === "ok") { - setModels(result.data); - setError(null); - } else { - setError(`Failed to load models: ${result.error}`); - } - } catch (err) { - setError(`Failed to load models: ${err}`); - } finally { - setLoading(false); - } - }; - - const loadCurrentModel = async () => { - try { - const result = await commands.getCurrentModel(); - if (result.status === "ok") { - setCurrentModel(result.data); - } - } catch (err) { - console.error("Failed to load current model:", err); - } - }; - - const checkFirstRun = async () => { - try { - const result = await commands.hasAnyModelsAvailable(); - if (result.status === "ok") { - const hasModels = result.data; - setHasAnyModels(hasModels); - setIsFirstRun(!hasModels); - return !hasModels; - } - return false; - } catch (err) { - console.error("Failed to check model availability:", err); - return false; - } - }; - - const selectModel = async (modelId: string) => { - try { - setError(null); - const result = await commands.setActiveModel(modelId); - if (result.status === "ok") { - setCurrentModel(modelId); - setIsFirstRun(false); - setHasAnyModels(true); - return true; - } else { - setError(`Failed to switch to model: ${result.error}`); - return false; - } - } catch (err) { - setError(`Failed to switch to model: ${err}`); - return false; - } - }; - - const downloadModel = async (modelId: string) => { - try { - setError(null); - setDownloadingModels((prev) => new Set(prev.add(modelId))); - const result = await commands.downloadModel(modelId); - if (result.status === "ok") { - return true; - } else { - setError(`Failed to download model: ${result.error}`); - setDownloadingModels((prev) => { - const next = new Set(prev); - next.delete(modelId); - return next; - }); - return false; - } - } catch (err) { - setError(`Failed to download model: ${err}`); - setDownloadingModels((prev) => { - const next = new Set(prev); - next.delete(modelId); - return next; - }); - return false; - } - }; - - const deleteModel = async (modelId: string) => { - try { - setError(null); - const result = await commands.deleteModel(modelId); - if (result.status === "ok") { - await loadModels(); // Refresh the list - return true; - } else { - setError(`Failed to delete model: ${result.error}`); - return false; - } - } catch (err) { - setError(`Failed to delete model: ${err}`); - return false; - } - }; - - const getModelInfo = (modelId: string): ModelInfo | undefined => { - return models.find((model) => model.id === modelId); - }; - - const isModelDownloading = (modelId: string): boolean => { - return downloadingModels.has(modelId); - }; - - const isModelExtracting = (modelId: string): boolean => { - return extractingModels.has(modelId); - }; - - const getDownloadProgress = ( - modelId: string, - ): DownloadProgress | undefined => { - return downloadProgress.get(modelId); - }; - - useEffect(() => { - loadModels(); - loadCurrentModel(); - checkFirstRun(); - - // Listen for download progress - const progressUnlisten = listen( - "model-download-progress", - (event) => { - setDownloadProgress( - (prev) => new Map(prev.set(event.payload.model_id, event.payload)), - ); - }, - ); - - // Listen for download completion - const completeUnlisten = listen( - "model-download-complete", - (event) => { - const modelId = event.payload; - setDownloadingModels((prev) => { - const next = new Set(prev); - next.delete(modelId); - return next; - }); - setDownloadProgress((prev) => { - const next = new Map(prev); - next.delete(modelId); - return next; - }); - // Refresh models list to update download status - loadModels(); - }, - ); - - // Listen for extraction events - const extractionStartedUnlisten = listen( - "model-extraction-started", - (event) => { - const modelId = event.payload; - setExtractingModels((prev) => new Set(prev.add(modelId))); - }, - ); - - const extractionCompletedUnlisten = listen( - "model-extraction-completed", - (event) => { - const modelId = event.payload; - setExtractingModels((prev) => { - const next = new Set(prev); - next.delete(modelId); - return next; - }); - // Refresh models list to update download status - loadModels(); - }, - ); - - const extractionFailedUnlisten = listen<{ - model_id: string; - error: string; - }>("model-extraction-failed", (event) => { - const modelId = event.payload.model_id; - setExtractingModels((prev) => { - const next = new Set(prev); - next.delete(modelId); - return next; - }); - setError(`Failed to extract model: ${event.payload.error}`); - }); - - return () => { - progressUnlisten.then((fn) => fn()); - completeUnlisten.then((fn) => fn()); - extractionStartedUnlisten.then((fn) => fn()); - extractionCompletedUnlisten.then((fn) => fn()); - extractionFailedUnlisten.then((fn) => fn()); - }; - }, []); - - return { - models, - currentModel, - loading, - error, - downloadingModels, - extractingModels, - downloadProgress, - hasAnyModels, - isFirstRun, - loadModels, - loadCurrentModel, - checkFirstRun, - selectModel, - downloadModel, - deleteModel, - getModelInfo, - isModelDownloading, - isModelExtracting, - getDownloadProgress, - }; -}; diff --git a/src/main.tsx b/src/main.tsx index 6364313..c31c1e9 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,6 +5,10 @@ import App from "./App"; // Initialize i18n import "./i18n"; +// Initialize model store (loads models and sets up event listeners) +import { useModelStore } from "./stores/modelStore"; +useModelStore.getState().initialize(); + ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( diff --git a/src/stores/modelStore.ts b/src/stores/modelStore.ts new file mode 100644 index 0000000..b452771 --- /dev/null +++ b/src/stores/modelStore.ts @@ -0,0 +1,254 @@ +import { create } from "zustand"; +import { subscribeWithSelector } from "zustand/middleware"; +import { listen } from "@tauri-apps/api/event"; +import { commands, type ModelInfo } from "@/bindings"; + +interface DownloadProgress { + model_id: string; + downloaded: number; + total: number; + percentage: number; +} + +interface ModelStore { + models: ModelInfo[]; + currentModel: string; + downloadingModels: Set; + extractingModels: Set; + downloadProgress: Map; + loading: boolean; + error: string | null; + hasAnyModels: boolean; + isFirstRun: boolean; + + // Actions + initialize: () => Promise; + loadModels: () => Promise; + loadCurrentModel: () => Promise; + checkFirstRun: () => Promise; + selectModel: (modelId: string) => Promise; + downloadModel: (modelId: string) => Promise; + deleteModel: (modelId: string) => Promise; + + // Getters + getModelInfo: (modelId: string) => ModelInfo | undefined; + isModelDownloading: (modelId: string) => boolean; + isModelExtracting: (modelId: string) => boolean; + getDownloadProgress: (modelId: string) => DownloadProgress | undefined; + + // Internal setters + setModels: (models: ModelInfo[]) => void; + setCurrentModel: (model: string) => void; + setLoading: (loading: boolean) => void; + setError: (error: string | null) => void; + setHasAnyModels: (hasAny: boolean) => void; + setIsFirstRun: (isFirst: boolean) => void; + addDownloadingModel: (modelId: string) => void; + removeDownloadingModel: (modelId: string) => void; + addExtractingModel: (modelId: string) => void; + removeExtractingModel: (modelId: string) => void; + setDownloadProgress: (modelId: string, progress: DownloadProgress) => void; + removeDownloadProgress: (modelId: string) => void; +} + +export const useModelStore = create()( + subscribeWithSelector((set, get) => ({ + models: [], + currentModel: "", + downloadingModels: new Set(), + extractingModels: new Set(), + downloadProgress: new Map(), + loading: true, + error: null, + hasAnyModels: false, + isFirstRun: false, + + // Internal setters + setModels: (models) => set({ models }), + setCurrentModel: (currentModel) => set({ currentModel }), + setLoading: (loading) => set({ loading }), + setError: (error) => set({ error }), + setHasAnyModels: (hasAnyModels) => set({ hasAnyModels }), + setIsFirstRun: (isFirstRun) => set({ isFirstRun }), + addDownloadingModel: (modelId) => + set((state) => ({ + downloadingModels: new Set(state.downloadingModels).add(modelId), + })), + removeDownloadingModel: (modelId) => + set((state) => { + const next = new Set(state.downloadingModels); + next.delete(modelId); + return { downloadingModels: next }; + }), + addExtractingModel: (modelId) => + set((state) => ({ + extractingModels: new Set(state.extractingModels).add(modelId), + })), + removeExtractingModel: (modelId) => + set((state) => { + const next = new Set(state.extractingModels); + next.delete(modelId); + return { extractingModels: next }; + }), + setDownloadProgress: (modelId, progress) => + set((state) => ({ + downloadProgress: new Map(state.downloadProgress).set( + modelId, + progress, + ), + })), + removeDownloadProgress: (modelId) => + set((state) => { + const next = new Map(state.downloadProgress); + next.delete(modelId); + return { downloadProgress: next }; + }), + + // Getters + getModelInfo: (modelId) => + get().models.find((model) => model.id === modelId), + isModelDownloading: (modelId) => get().downloadingModels.has(modelId), + isModelExtracting: (modelId) => get().extractingModels.has(modelId), + getDownloadProgress: (modelId) => get().downloadProgress.get(modelId), + + // Actions + loadModels: async () => { + try { + const result = await commands.getAvailableModels(); + if (result.status === "ok") { + set({ models: result.data, error: null }); + } else { + set({ error: `Failed to load models: ${result.error}` }); + } + } catch (err) { + set({ error: `Failed to load models: ${err}` }); + } finally { + set({ loading: false }); + } + }, + + loadCurrentModel: async () => { + try { + const result = await commands.getCurrentModel(); + if (result.status === "ok") { + set({ currentModel: result.data }); + } + } catch (err) { + console.error("Failed to load current model:", err); + } + }, + + checkFirstRun: async () => { + try { + const result = await commands.hasAnyModelsAvailable(); + if (result.status === "ok") { + const hasModels = result.data; + set({ hasAnyModels: hasModels, isFirstRun: !hasModels }); + return !hasModels; + } + return false; + } catch (err) { + console.error("Failed to check model availability:", err); + return false; + } + }, + + selectModel: async (modelId) => { + try { + set({ error: null }); + const result = await commands.setActiveModel(modelId); + if (result.status === "ok") { + set({ currentModel: modelId, isFirstRun: false, hasAnyModels: true }); + return true; + } else { + set({ error: `Failed to switch to model: ${result.error}` }); + return false; + } + } catch (err) { + set({ error: `Failed to switch to model: ${err}` }); + return false; + } + }, + + downloadModel: async (modelId) => { + const { addDownloadingModel, removeDownloadingModel } = get(); + try { + set({ error: null }); + addDownloadingModel(modelId); + const result = await commands.downloadModel(modelId); + if (result.status === "ok") { + return true; + } else { + set({ error: `Failed to download model: ${result.error}` }); + removeDownloadingModel(modelId); + return false; + } + } catch (err) { + set({ error: `Failed to download model: ${err}` }); + removeDownloadingModel(modelId); + return false; + } + }, + + deleteModel: async (modelId) => { + const { loadModels } = get(); + try { + set({ error: null }); + const result = await commands.deleteModel(modelId); + if (result.status === "ok") { + await loadModels(); + return true; + } else { + set({ error: `Failed to delete model: ${result.error}` }); + return false; + } + } catch (err) { + set({ error: `Failed to delete model: ${err}` }); + return false; + } + }, + + initialize: async () => { + const { loadModels, loadCurrentModel, checkFirstRun } = get(); + await Promise.all([loadModels(), loadCurrentModel(), checkFirstRun()]); + }, + })), +); + +// Set up event listeners at module load +listen("model-state-changed", () => { + useModelStore.getState().loadCurrentModel(); +}); + +listen("model-download-progress", (event) => { + useModelStore + .getState() + .setDownloadProgress(event.payload.model_id, event.payload); +}); + +listen("model-download-complete", (event) => { + const modelId = event.payload; + const state = useModelStore.getState(); + state.removeDownloadingModel(modelId); + state.removeDownloadProgress(modelId); + state.loadModels(); +}); + +listen("model-extraction-started", (event) => { + useModelStore.getState().addExtractingModel(event.payload); +}); + +listen("model-extraction-completed", (event) => { + const state = useModelStore.getState(); + state.removeExtractingModel(event.payload); + state.loadModels(); +}); + +listen<{ model_id: string; error: string }>( + "model-extraction-failed", + (event) => { + const state = useModelStore.getState(); + state.removeExtractingModel(event.payload.model_id); + state.setError(`Failed to extract model: ${event.payload.error}`); + }, +);