From 00139b660b3a61e130aa76dcc7cb9e447da43898 Mon Sep 17 00:00:00 2001 From: Pascal Date: Wed, 24 Jun 2026 10:50:44 +0200 Subject: [PATCH] ui: loading bar below the model picker (#24931) * ui: show model load progress on the selector trigger Mirror the in-dropdown stage progress as a thin bar on the selector trigger, so the active model's load percent stays visible when the menu is closed. Same status gating and composite fraction as the dropdown row, so both bars track the selected model in sync. Suggested-by: Julien Chaumond <@julien-c> * ui: show model load progress bar on the in-conversation model selector * ui: tune model load indicator to a pulsing highlight (suggested by @ngxson) Also wire the indicator onto the mobile sheet trigger, which was missing it since mobile uses the sheet instead of the dropdown. * ui: thin (@allozaur) pulsating (@ngxson) model load bar --- .../ChatMessageAssistant.svelte | 13 +++++++++-- .../app/models/ModelLoadHighlight.svelte | 11 ++++++++++ .../app/models/ModelsSelectorDropdown.svelte | 22 +++++++++++++++++-- .../app/models/ModelsSelectorOption.svelte | 20 ++++++----------- .../app/models/ModelsSelectorSheet.svelte | 21 +++++++++++++++++- 5 files changed, 69 insertions(+), 18 deletions(-) create mode 100644 tools/ui/src/lib/components/app/models/ModelLoadHighlight.svelte diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageAssistant/ChatMessageAssistant.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageAssistant/ChatMessageAssistant.svelte index a1d02d6ece..14f6c5ca02 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageAssistant/ChatMessageAssistant.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageAssistant/ChatMessageAssistant.svelte @@ -180,6 +180,9 @@ let displayedModel = $derived(message.model ?? null); + // model being switched to while it loads, so the selector bar tracks it + let pendingModel = $state(null); + let isCurrentlyLoading = $derived(isLoading()); let isStreaming = $derived(isChatStreaming()); let hasNoContent = $derived(!message?.content?.trim()); @@ -318,13 +321,19 @@ > {#if isRouter} { const status = modelsStore.getModelStatus(modelId); if (status !== ServerModelStatus.LOADED) { - await modelsStore.loadModel(modelId); + pendingModel = modelId; + + try { + await modelsStore.loadModel(modelId); + } finally { + pendingModel = null; + } } onRegenerate(modelName); diff --git a/tools/ui/src/lib/components/app/models/ModelLoadHighlight.svelte b/tools/ui/src/lib/components/app/models/ModelLoadHighlight.svelte new file mode 100644 index 0000000000..fa9a02108a --- /dev/null +++ b/tools/ui/src/lib/components/app/models/ModelLoadHighlight.svelte @@ -0,0 +1,11 @@ + + + +
+
+
diff --git a/tools/ui/src/lib/components/app/models/ModelsSelectorDropdown.svelte b/tools/ui/src/lib/components/app/models/ModelsSelectorDropdown.svelte index 40006a4c93..720963a8db 100644 --- a/tools/ui/src/lib/components/app/models/ModelsSelectorDropdown.svelte +++ b/tools/ui/src/lib/components/app/models/ModelsSelectorDropdown.svelte @@ -2,8 +2,10 @@ import { ChevronDown, Loader2, Package } from '@lucide/svelte'; import * as DropdownMenu from '$lib/components/ui/dropdown-menu'; import * as Tooltip from '$lib/components/ui/tooltip'; - import { KeyboardKey } from '$lib/enums'; + import { KeyboardKey, ServerModelStatus } from '$lib/enums'; import { useModelsSelector } from '$lib/hooks/use-models-selector.svelte'; + import { modelsStore, routerModels } from '$lib/stores/models.svelte'; + import { modelLoadFraction } from '$lib/utils'; import { DialogModelInformation, DropdownMenuSearchable, @@ -11,6 +13,7 @@ ModelsSelectorList, ModelsSelectorOption } from '$lib/components/app'; + import ModelLoadHighlight from './ModelLoadHighlight.svelte'; import type { ModelItem } from './utils'; interface Props { @@ -113,6 +116,17 @@ {/if} {:else} {@const selectedOption = ms.getDisplayOption()} + {@const triggerModel = selectedOption?.model} + {@const triggerStatus = triggerModel + ? routerModels().find((m) => m.id === triggerModel)?.status?.value + : undefined} + {@const triggerLoading = + !!triggerModel && + (triggerStatus === ServerModelStatus.LOADING || + modelsStore.isModelOperationInProgress(triggerModel))} + {@const triggerLoadPercent = triggerLoading + ? Math.round(modelLoadFraction(modelsStore.getLoadProgress(triggerModel)) * 100) + : 0} {#if ms.isRouter} @@ -123,7 +137,7 @@ {/if} + + {#if triggerLoading} + + {/if} {/snippet} diff --git a/tools/ui/src/lib/components/app/models/ModelsSelectorOption.svelte b/tools/ui/src/lib/components/app/models/ModelsSelectorOption.svelte index f2a024d31d..981111e201 100644 --- a/tools/ui/src/lib/components/app/models/ModelsSelectorOption.svelte +++ b/tools/ui/src/lib/components/app/models/ModelsSelectorOption.svelte @@ -10,6 +10,7 @@ RotateCw } from '@lucide/svelte'; import { ActionIcon, ModelId } from '$lib/components/app'; + import ModelLoadHighlight from './ModelLoadHighlight.svelte'; import type { ModelOption } from '$lib/types/models'; import { ServerModelStatus } from '$lib/enums'; import { modelsStore, routerModels } from '$lib/stores/models.svelte'; @@ -119,11 +120,11 @@ {#if isLoading} -
+
{:else if isFailed} -
+
@@ -140,7 +141,7 @@
{:else if isSleeping} -
+
@@ -159,7 +160,7 @@
{:else if isLoaded} -
+
@@ -176,7 +177,7 @@
{:else} -
+
@@ -196,13 +197,6 @@
{#if isLoading} -
-
-
+ {/if}
diff --git a/tools/ui/src/lib/components/app/models/ModelsSelectorSheet.svelte b/tools/ui/src/lib/components/app/models/ModelsSelectorSheet.svelte index e5920a2a0f..a9e9ea1c8d 100644 --- a/tools/ui/src/lib/components/app/models/ModelsSelectorSheet.svelte +++ b/tools/ui/src/lib/components/app/models/ModelsSelectorSheet.svelte @@ -8,6 +8,10 @@ ModelsSelectorList, SearchInput } from '$lib/components/app'; + import ModelLoadHighlight from './ModelLoadHighlight.svelte'; + import { ServerModelStatus } from '$lib/enums'; + import { modelsStore, routerModels } from '$lib/stores/models.svelte'; + import { modelLoadFraction } from '$lib/utils'; interface Props { class?: string; @@ -61,12 +65,23 @@

No models available.

{:else} {@const selectedOption = ms.getDisplayOption()} + {@const triggerModel = selectedOption?.model} + {@const triggerStatus = triggerModel + ? routerModels().find((m) => m.id === triggerModel)?.status?.value + : undefined} + {@const triggerLoading = + !!triggerModel && + (triggerStatus === ServerModelStatus.LOADING || + modelsStore.isModelOperationInProgress(triggerModel))} + {@const triggerLoadPercent = triggerLoading + ? Math.round(modelLoadFraction(modelsStore.getLoadProgress(triggerModel)) * 100) + : 0} {#if ms.isRouter}