diff --git a/app/src/components/ServerSettings/ModelManagement.tsx b/app/src/components/ServerSettings/ModelManagement.tsx index 71c8b68b..a5fafdbf 100644 --- a/app/src/components/ServerSettings/ModelManagement.tsx +++ b/app/src/components/ServerSettings/ModelManagement.tsx @@ -157,16 +157,30 @@ export function ModelManagement() { }); const handleCancel = (modelName: string) => { - // Immediately hide the error and suppress downloading state in UI + // Snapshot previous state for rollback + const prevDismissed = dismissedErrors; + const prevLocalErrors = localErrors; + const prevDownloadingModel = downloadingModel; + const prevDownloadingDisplayName = downloadingDisplayName; + + // Optimistically hide the error and suppress downloading state in UI setDismissedErrors((prev) => new Set(prev).add(modelName)); setLocalErrors((prev) => { const next = new Map(prev); next.delete(modelName); return next; }); - // Also clear local downloading state if this was our current download if (downloadingModel === modelName) { setDownloadingModel(null); setDownloadingDisplayName(null); } - // Fire-and-forget the backend cancel, then refetch to sync - cancelMutation.mutate(modelName); + + cancelMutation.mutate(modelName, { + onError: () => { + // Rollback optimistic updates on failure + setDismissedErrors(prevDismissed); + setLocalErrors(prevLocalErrors); + setDownloadingModel(prevDownloadingModel); + setDownloadingDisplayName(prevDownloadingDisplayName); + toast({ title: 'Cancel failed', description: 'Could not cancel the download task.', variant: 'destructive' }); + }, + }); }; const clearAllMutation = useMutation({ @@ -259,7 +273,7 @@ export function ModelManagement() { }} onCancel={() => handleCancel(model.model_name)} isDownloading={downloadingModel === model.model_name} - isCancelling={cancelMutation.isPending} + isCancelling={cancelMutation.isPending && cancelMutation.variables === model.model_name} isDismissed={dismissedErrors.has(model.model_name)} erroredDownload={erroredDownloads.get(model.model_name)} formatSize={formatSize} @@ -291,7 +305,7 @@ export function ModelManagement() { }} onCancel={() => handleCancel(model.model_name)} isDownloading={downloadingModel === model.model_name} - isCancelling={cancelMutation.isPending} + isCancelling={cancelMutation.isPending && cancelMutation.variables === model.model_name} isDismissed={dismissedErrors.has(model.model_name)} erroredDownload={erroredDownloads.get(model.model_name)} formatSize={formatSize} @@ -310,9 +324,9 @@ export function ModelManagement() { className="flex items-center gap-2 hover:text-foreground transition-colors" > {consoleOpen ? ( - - ) : ( + ) : ( + )} Problems diff --git a/backend/main.py b/backend/main.py index 0893e626..3f58aa8b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -932,7 +932,11 @@ async def transcribe_audio( # Check if Whisper model is downloaded (uses default size "base") model_size = whisper_model.model_size - model_name = f"openai/whisper-{model_size}" + # Map model sizes to HF repo IDs (whisper-large needs -v3 suffix) + whisper_hf_repos = { + "large": "openai/whisper-large-v3", + } + model_name = whisper_hf_repos.get(model_size, f"openai/whisper-{model_size}") # Check if model is cached from huggingface_hub import constants as hf_constants @@ -1595,11 +1599,15 @@ async def cancel_model_download(request: models.ModelDownloadRequest): removed = task_manager.cancel_download(request.model_name) # Also clear progress state so the model doesn't show as downloading + progress_removed = False with progress_manager._lock: if request.model_name in progress_manager._progress: del progress_manager._progress[request.model_name] + progress_removed = True - return {"message": f"Download task for {request.model_name} cancelled"} + if removed or progress_removed: + return {"message": f"Download task for {request.model_name} cancelled"} + return {"message": f"No active task found for {request.model_name}"} @app.post("/tasks/clear") @@ -1613,8 +1621,8 @@ async def clear_all_tasks(): with progress_manager._lock: progress_manager._progress.clear() - progress_manager._last_notify_time.clear() - progress_manager._last_notify_progress.clear() + progress_manager._last_notify_time.clear() + progress_manager._last_notify_progress.clear() return {"message": "All task state cleared"} @@ -1771,6 +1779,7 @@ async def get_active_tasks(): model_name=model_name, status=progress.get("status", "downloading"), started_at=started_at, + error=progress.get("error"), )) # Get active generations diff --git a/backend/utils/tasks.py b/backend/utils/tasks.py index 98ead512..da13011a 100644 --- a/backend/utils/tasks.py +++ b/backend/utils/tasks.py @@ -74,10 +74,7 @@ class TaskManager: def cancel_download(self, model_name: str) -> bool: """Cancel/dismiss a download task (removes it from active list).""" - if model_name in self._active_downloads: - del self._active_downloads[model_name] - return True - return False + return self._active_downloads.pop(model_name, None) is not None def is_download_active(self, model_name: str) -> bool: """Check if a download is active."""