fixes for 0.6.11: linking, ui (#579)

* static linking?

* move to 0.2.1

* transcribe-rs 0.2.2

* Update Cargo.lock

* remove settings which dont apply to models
This commit is contained in:
CJ Pais 2026-01-14 12:44:59 +08:00 committed by GitHub
parent 738c5a82d8
commit 56b0f44e45
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 289 additions and 621 deletions

295
src-tauri/Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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<LanguageSelectorProps> = ({
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<HTMLDivElement>(null);
const searchInputRef = useRef<HTMLInputElement>(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<LanguageSelectorProps> = ({
};
}, []);
// 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<LanguageSelectorProps> = ({
[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<LanguageSelectorProps> = ({
};
const handleToggle = () => {
if (isUpdating("selected_language") || isUnsupported) return;
if (isUpdating("selected_language")) return;
setIsOpen(!isOpen);
};
@ -108,26 +90,21 @@ export const LanguageSelector: React.FC<LanguageSelectorProps> = ({
return (
<SettingContainer
title={t("settings.general.language.title")}
description={
isUnsupported
? t("settings.general.language.descriptionUnsupported")
: t("settings.general.language.description")
}
description={t("settings.general.language.description")}
descriptionMode={descriptionMode}
grouped={grouped}
disabled={isUnsupported}
>
<div className="flex items-center space-x-1">
<div className="relative" ref={dropdownRef}>
<button
type="button"
className={`px-2 py-1 text-sm font-semibold bg-mid-gray/10 border border-mid-gray/80 rounded min-w-[200px] text-left flex items-center justify-between transition-all duration-150 ${
isUpdating("selected_language") || isUnsupported
isUpdating("selected_language")
? "opacity-50 cursor-not-allowed"
: "hover:bg-logo-primary/10 cursor-pointer hover:border-logo-primary"
}`}
onClick={handleToggle}
disabled={isUpdating("selected_language") || isUnsupported}
disabled={isUpdating("selected_language")}
>
<span className="truncate">{selectedLanguageName}</span>
<svg
@ -147,7 +124,7 @@ export const LanguageSelector: React.FC<LanguageSelectorProps> = ({
</svg>
</button>
{isOpen && !isUpdating("selected_language") && !isUnsupported && (
{isOpen && !isUpdating("selected_language") && (
<div className="absolute top-full left-0 right-0 mt-1 bg-background border border-mid-gray/80 rounded shadow-lg z-50 max-h-60 overflow-hidden">
{/* Search input */}
<div className="p-2 border-b border-mid-gray/80">
@ -191,7 +168,7 @@ export const LanguageSelector: React.FC<LanguageSelectorProps> = ({
</div>
<ResetButton
onClick={handleReset}
disabled={isUpdating("selected_language") || isUnsupported}
disabled={isUpdating("selected_language")}
/>
</div>
{isUpdating("selected_language") && (

View file

@ -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<TranslateToEnglishProps> = 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 (
<ToggleSwitch
checked={translateToEnglish}
onChange={(enabled) => 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}
/>

View file

@ -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 (
<div className="max-w-3xl w-full mx-auto space-y-6">
<SettingsGroup title={t("settings.advanced.title")}>
@ -20,7 +25,9 @@ export const AdvancedSettings: React.FC = () => {
<ShowOverlay descriptionMode="tooltip" grouped={true} />
<PasteMethodSetting descriptionMode="tooltip" grouped={true} />
<ClipboardHandlingSetting descriptionMode="tooltip" grouped={true} />
<TranslateToEnglish descriptionMode="tooltip" grouped={true} />
{showTranslateToEnglish && (
<TranslateToEnglish descriptionMode="tooltip" grouped={true} />
)}
<ModelUnloadTimeoutSetting descriptionMode="tooltip" grouped={true} />
<CustomWords descriptionMode="tooltip" grouped />
</SettingsGroup>

View file

@ -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 (
<div className="max-w-3xl w-full mx-auto space-y-6">
<SettingsGroup title={t("settings.general.title")}>
<HandyShortcut shortcutId="transcribe" grouped={true} />
<LanguageSelector descriptionMode="tooltip" grouped={true} />
{showLanguageSelector && (
<LanguageSelector descriptionMode="tooltip" grouped={true} />
)}
<PushToTalk descriptionMode="tooltip" grouped={true} />
</SettingsGroup>
<SettingsGroup title={t("settings.sound.title")}>

View file

@ -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<ModelInfo[]>([]);
const [currentModel, setCurrentModel] = useState<string>("");
const [downloadingModels, setDownloadingModels] = useState<Set<string>>(
new Set(),
);
const [extractingModels, setExtractingModels] = useState<Set<string>>(
new Set(),
);
const [downloadProgress, setDownloadProgress] = useState<
Map<string, DownloadProgress>
>(new Map());
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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<DownloadProgress>(
"model-download-progress",
(event) => {
setDownloadProgress(
(prev) => new Map(prev.set(event.payload.model_id, event.payload)),
);
},
);
// Listen for download completion
const completeUnlisten = listen<string>(
"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<string>(
"model-extraction-started",
(event) => {
const modelId = event.payload;
setExtractingModels((prev) => new Set(prev.add(modelId)));
},
);
const extractionCompletedUnlisten = listen<string>(
"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,
};
};

View file

@ -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(
<React.StrictMode>
<App />

254
src/stores/modelStore.ts Normal file
View file

@ -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<string>;
extractingModels: Set<string>;
downloadProgress: Map<string, DownloadProgress>;
loading: boolean;
error: string | null;
hasAnyModels: boolean;
isFirstRun: boolean;
// Actions
initialize: () => Promise<void>;
loadModels: () => Promise<void>;
loadCurrentModel: () => Promise<void>;
checkFirstRun: () => Promise<boolean>;
selectModel: (modelId: string) => Promise<boolean>;
downloadModel: (modelId: string) => Promise<boolean>;
deleteModel: (modelId: string) => Promise<boolean>;
// 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<ModelStore>()(
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<DownloadProgress>("model-download-progress", (event) => {
useModelStore
.getState()
.setDownloadProgress(event.payload.model_id, event.payload);
});
listen<string>("model-download-complete", (event) => {
const modelId = event.payload;
const state = useModelStore.getState();
state.removeDownloadingModel(modelId);
state.removeDownloadProgress(modelId);
state.loadModels();
});
listen<string>("model-extraction-started", (event) => {
useModelStore.getState().addExtractingModel(event.payload);
});
listen<string>("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}`);
},
);