diff --git a/bun.lock b/bun.lock index d1aeb09..acc1a0f 100644 --- a/bun.lock +++ b/bun.lock @@ -18,6 +18,7 @@ "@tauri-apps/plugin-store": "~2.4.1", "@tauri-apps/plugin-updater": "~2.9.0", "i18next": "^25.7.2", + "immer": "^11.1.3", "lucide-react": "^0.542.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -493,6 +494,8 @@ "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "immer": ["immer@11.1.3", "", {}, "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q=="], + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], diff --git a/package.json b/package.json index 5aa071e..a5592e8 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "react-select": "^5.8.0", "tauri-plugin-macos-permissions-api": "2.3.0", "i18next": "^25.7.2", + "immer": "^11.1.3", "lucide-react": "^0.542.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/src/components/model-selector/DownloadProgressDisplay.tsx b/src/components/model-selector/DownloadProgressDisplay.tsx index b550b1f..de8c14e 100644 --- a/src/components/model-selector/DownloadProgressDisplay.tsx +++ b/src/components/model-selector/DownloadProgressDisplay.tsx @@ -16,8 +16,8 @@ interface DownloadStats { } interface DownloadProgressDisplayProps { - downloadProgress: Map; - downloadStats: Map; + downloadProgress: Record; + downloadStats: Record; className?: string; } @@ -26,14 +26,13 @@ const DownloadProgressDisplay: React.FC = ({ downloadStats, className = "", }) => { - if (downloadProgress.size === 0) { + const progressValues = Object.values(downloadProgress); + if (progressValues.length === 0) { return null; } - const progressData: ProgressData[] = Array.from( - downloadProgress.values(), - ).map((progress) => { - const stats = downloadStats.get(progress.model_id); + const progressData: ProgressData[] = progressValues.map((progress) => { + const stats = downloadStats[progress.model_id]; return { id: progress.model_id, percentage: progress.percentage, @@ -45,7 +44,7 @@ const DownloadProgressDisplay: React.FC = ({ ); diff --git a/src/components/model-selector/ModelDropdown.tsx b/src/components/model-selector/ModelDropdown.tsx index 10bb23c..8f22fe5 100644 --- a/src/components/model-selector/ModelDropdown.tsx +++ b/src/components/model-selector/ModelDropdown.tsx @@ -18,7 +18,7 @@ interface DownloadProgress { interface ModelDropdownProps { models: ModelInfo[]; currentModelId: string; - downloadProgress: Map; + downloadProgress: Record; onModelSelect: (modelId: string) => void; onModelDownload: (modelId: string) => void; onModelDelete: (modelId: string) => Promise; @@ -52,14 +52,14 @@ const ModelDropdown: React.FC = ({ }; const handleModelClick = (modelId: string) => { - if (downloadProgress.has(modelId)) { + if (modelId in downloadProgress) { return; // Don't allow interaction while downloading } onModelSelect(modelId); }; const handleDownloadClick = (modelId: string) => { - if (downloadProgress.has(modelId)) { + if (modelId in downloadProgress) { return; // Don't allow interaction while downloading } onModelDownload(modelId); @@ -158,8 +158,8 @@ const ModelDropdown: React.FC = ({ : t("modelSelector.downloadModels")} {downloadableModels.map((model) => { - const isDownloading = downloadProgress.has(model.id); - const progress = downloadProgress.get(model.id); + const isDownloading = model.id in downloadProgress; + const progress = downloadProgress[model.id]; return (
= ({ onError }) => { const [modelStatus, setModelStatus] = useState("unloaded"); const [modelError, setModelError] = useState(null); const [modelDownloadProgress, setModelDownloadProgress] = useState< - Map - >(new Map()); + Record + >({}); const [showModelDropdown, setShowModelDropdown] = useState(false); const [downloadStats, setDownloadStats] = useState< - Map - >(new Map()); - const [extractingModels, setExtractingModels] = useState>( - new Set(), - ); + Record + >({}); + const [extractingModels, setExtractingModels] = useState< + Record + >({}); const dropdownRef = useRef(null); @@ -97,53 +98,52 @@ const ModelSelector: React.FC = ({ onError }) => { "model-download-progress", (event) => { const progress = event.payload; - setModelDownloadProgress((prev) => { - const newMap = new Map(prev); - newMap.set(progress.model_id, progress); - return newMap; - }); + setModelDownloadProgress( + produce((downloadProgress) => { + downloadProgress[progress.model_id] = progress; + }), + ); setModelStatus("downloading"); // Update download stats for speed calculation const now = Date.now(); - setDownloadStats((prev) => { - const current = prev.get(progress.model_id); - const newStats = new Map(prev); + setDownloadStats( + produce((stats) => { + const current = stats[progress.model_id]; - if (!current) { - // First progress update - initialize - newStats.set(progress.model_id, { - startTime: now, - lastUpdate: now, - totalDownloaded: progress.downloaded, - speed: 0, - }); - } else { - // Calculate speed over last few seconds - const timeDiff = (now - current.lastUpdate) / 1000; // seconds - const bytesDiff = progress.downloaded - current.totalDownloaded; - - if (timeDiff > 0.5) { - // Update speed every 500ms - const currentSpeed = bytesDiff / (1024 * 1024) / timeDiff; // MB/s - // Smooth the speed with exponential moving average, but ensure positive values - const validCurrentSpeed = Math.max(0, currentSpeed); - const smoothedSpeed = - current.speed > 0 - ? current.speed * 0.8 + validCurrentSpeed * 0.2 - : validCurrentSpeed; - - newStats.set(progress.model_id, { - startTime: current.startTime, + if (!current) { + // First progress update - initialize + stats[progress.model_id] = { + startTime: now, lastUpdate: now, totalDownloaded: progress.downloaded, - speed: Math.max(0, smoothedSpeed), - }); - } - } + speed: 0, + }; + } else { + // Calculate speed over last few seconds + const timeDiff = (now - current.lastUpdate) / 1000; // seconds + const bytesDiff = progress.downloaded - current.totalDownloaded; - return newStats; - }); + if (timeDiff > 0.5) { + // Update speed every 500ms + const currentSpeed = bytesDiff / (1024 * 1024) / timeDiff; // MB/s + // Smooth the speed with exponential moving average, but ensure positive values + const validCurrentSpeed = Math.max(0, currentSpeed); + const smoothedSpeed = + current.speed > 0 + ? current.speed * 0.8 + validCurrentSpeed * 0.2 + : validCurrentSpeed; + + stats[progress.model_id] = { + startTime: current.startTime, + lastUpdate: now, + totalDownloaded: progress.downloaded, + speed: Math.max(0, smoothedSpeed), + }; + } + } + }), + ); }, ); @@ -152,16 +152,16 @@ const ModelSelector: React.FC = ({ onError }) => { "model-download-complete", (event) => { const modelId = event.payload; - setModelDownloadProgress((prev) => { - const newMap = new Map(prev); - newMap.delete(modelId); - return newMap; - }); - setDownloadStats((prev) => { - const newStats = new Map(prev); - newStats.delete(modelId); - return newStats; - }); + setModelDownloadProgress( + produce((progress) => { + delete progress[modelId]; + }), + ); + setDownloadStats( + produce((stats) => { + delete stats[modelId]; + }), + ); loadModels(); // Refresh models list // Auto-select the newly downloaded model (skip if recording in progress) @@ -181,7 +181,11 @@ const ModelSelector: React.FC = ({ onError }) => { "model-extraction-started", (event) => { const modelId = event.payload; - setExtractingModels((prev) => new Set(prev.add(modelId))); + setExtractingModels( + produce((extracting) => { + extracting[modelId] = true; + }), + ); setModelStatus("extracting"); }, ); @@ -190,11 +194,11 @@ const ModelSelector: React.FC = ({ onError }) => { "model-extraction-completed", (event) => { const modelId = event.payload; - setExtractingModels((prev) => { - const next = new Set(prev); - next.delete(modelId); - return next; - }); + setExtractingModels( + produce((extracting) => { + delete extracting[modelId]; + }), + ); loadModels(); // Refresh models list // Auto-select the newly extracted model (skip if recording in progress) @@ -214,11 +218,11 @@ const ModelSelector: React.FC = ({ onError }) => { error: string; }>("model-extraction-failed", (event) => { const modelId = event.payload.model_id; - setExtractingModels((prev) => { - const next = new Set(prev); - next.delete(modelId); - return next; - }); + setExtractingModels( + produce((extracting) => { + delete extracting[modelId]; + }), + ); setModelError(`Failed to extract model: ${event.payload.error}`); setModelStatus("error"); }); @@ -329,9 +333,10 @@ const ModelSelector: React.FC = ({ onError }) => { }; const getModelDisplayText = (): string => { - if (extractingModels.size > 0) { - if (extractingModels.size === 1) { - const [modelId] = Array.from(extractingModels); + const extractingKeys = Object.keys(extractingModels); + if (extractingKeys.length > 0) { + if (extractingKeys.length === 1) { + const modelId = extractingKeys[0]; const model = models.find((m) => m.id === modelId); const modelName = model ? getTranslatedModelName(model, t) @@ -339,14 +344,15 @@ const ModelSelector: React.FC = ({ onError }) => { return t("modelSelector.extracting", { modelName }); } else { return t("modelSelector.extractingMultiple", { - count: extractingModels.size, + count: extractingKeys.length, }); } } - if (modelDownloadProgress.size > 0) { - if (modelDownloadProgress.size === 1) { - const [progress] = Array.from(modelDownloadProgress.values()); + const progressValues = Object.values(modelDownloadProgress); + if (progressValues.length > 0) { + if (progressValues.length === 1) { + const progress = progressValues[0]; const percentage = Math.max( 0, Math.min(100, Math.round(progress.percentage)), @@ -354,7 +360,7 @@ const ModelSelector: React.FC = ({ onError }) => { return t("modelSelector.downloading", { percentage }); } else { return t("modelSelector.downloadingMultiple", { - count: modelDownloadProgress.size, + count: progressValues.length, }); } }