From c6d80496abe7fb5d3a122332025334f04b5a989a Mon Sep 17 00:00:00 2001 From: silentoplayz Date: Sat, 23 Aug 2025 15:08:07 -0400 Subject: [PATCH] feat: improve ollama model management experience This commit introduces several improvements to the Ollama model management modal: - Adds a cancel button to the model pulling operation, using the existing 'x' button pattern. - Adds a cancel button to the "Update All" models operation, allowing the user to cancel the update for the currently processing model. - Cleans up toast notifications when updating all models. A single toast is now shown at the beginning and a summary toast at the end, preventing notification spam. - Refactors the `ManageOllama.svelte` component to support these new cancellation features. - Adds tooltips to all buttons in the modal to improve clarity. - Disables buttons when their corresponding input fields are empty to prevent accidental clicks. --- .../Models/Manage/ManageOllama.svelte | 374 +++++++++++------- 1 file changed, 225 insertions(+), 149 deletions(-) diff --git a/src/lib/components/admin/Settings/Models/Manage/ManageOllama.svelte b/src/lib/components/admin/Settings/Models/Manage/ManageOllama.svelte index fbfcf93677..29682a7a37 100644 --- a/src/lib/components/admin/Settings/Models/Manage/ManageOllama.svelte +++ b/src/lib/components/admin/Settings/Models/Manage/ManageOllama.svelte @@ -36,6 +36,8 @@ let updateModelId = null; let updateProgress = null; + let updateModelsControllers = {}; + let updateCancelled = false; let showExperimentalOllama = false; const MAX_PARALLEL_DOWNLOADS = 3; @@ -65,17 +67,31 @@ let deleteModelTag = ''; const updateModelsHandler = async () => { + updateCancelled = false; + toast.info('Checking for model updates...'); + for (const model of ollamaModels) { + if (updateCancelled) { + break; + } + console.debug(model); updateModelId = model.id; const [res, controller] = await pullModel(localStorage.token, model.id, urlIdx).catch( (error) => { - toast.error(`${error}`); - return null; + if (error.name !== 'AbortError') { + toast.error(`${error}`); + } + return [null, null]; } ); + updateModelsControllers = { + ...updateModelsControllers, + [model.id]: controller + }; + if (res) { const reader = res.body .pipeThrough(new TextDecoderStream()) @@ -108,19 +124,28 @@ } else { updateProgress = 100; } - } else { - toast.success(data.status); } } } } } catch (err) { - console.error(err); + if (err.name !== 'AbortError') { + console.error(err); + } + break; } } } + + delete updateModelsControllers[model.id]; + updateModelsControllers = { ...updateModelsControllers }; } + if (updateCancelled) { + toast.info('Model update cancelled'); + } else { + toast.success('All models are up to date'); + } updateModelId = null; updateProgress = null; }; @@ -143,10 +168,13 @@ return; } + modelTransferring = true; const [res, controller] = await pullModel(localStorage.token, sanitizedModelTag, urlIdx).catch( (error) => { - toast.error(`${error}`); - return null; + if (error.name !== 'AbortError') { + toast.error(`${error}`); + } + return [null, null]; } ); @@ -202,8 +230,6 @@ } }); } else { - toast.success(data.status); - MODEL_DOWNLOAD_POOL.set({ ...$MODEL_DOWNLOAD_POOL, [sanitizedModelTag]: { @@ -216,19 +242,23 @@ } } } catch (err) { - console.error(err); - if (typeof err !== 'string') { - err = err.message; - } + if (err.name !== 'AbortError') { + console.error(err); + if (typeof err !== 'string') { + err = err.message; + } - toast.error(`${err}`); - // opts.callback({ success: false, error, modelName: opts.modelName }); + toast.error(`${err}`); + // opts.callback({ success: false, error, modelName: opts.modelName }); + } else { + break; + } } } console.log($MODEL_DOWNLOAD_POOL[sanitizedModelTag]); - if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag].done) { + if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag]?.done) { toast.success( $i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, { modelName: sanitizedModelTag @@ -425,6 +455,14 @@ ); }; + const cancelUpdateModelHandler = async (model: string) => { + const controller = updateModelsControllers[model]; + if (controller) { + controller.abort(); + updateCancelled = true; + } + }; + const cancelModelPullHandler = async (model: string) => { const { reader, abortController } = $MODEL_DOWNLOAD_POOL[model]; if (abortController) { @@ -437,7 +475,7 @@ ...$MODEL_DOWNLOAD_POOL }); await deleteModel(localStorage.token, model); - toast.success($i18n.t('{{model}} download has been canceled', { model: model })); + toast.success(`${model} download has been canceled`); } }; @@ -605,59 +643,61 @@ bind:value={modelTag} /> - + {/if} + +
@@ -670,8 +710,35 @@
{#if updateModelId} -
- Updating "{updateModelId}" {updateProgress ? `(${updateProgress}%)` : ''} +
+
Updating "{updateModelId}" {updateProgress ? `(${updateProgress}%)` : ''}
+ + + +
{/if} @@ -754,25 +821,28 @@ {/each}
- + + + + + @@ -799,27 +869,31 @@
- + + + + + +
@@ -936,57 +1010,59 @@ {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')} - + {/if} + + {/if}