]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/commitdiff
ui: loading bar below the model picker (#24931)
authorPascal <redacted>
Wed, 24 Jun 2026 08:50:44 +0000 (10:50 +0200)
committerGitHub <redacted>
Wed, 24 Jun 2026 08:50:44 +0000 (10:50 +0200)
* 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

tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageAssistant/ChatMessageAssistant.svelte
tools/ui/src/lib/components/app/models/ModelLoadHighlight.svelte [new file with mode: 0644]
tools/ui/src/lib/components/app/models/ModelsSelectorDropdown.svelte
tools/ui/src/lib/components/app/models/ModelsSelectorOption.svelte
tools/ui/src/lib/components/app/models/ModelsSelectorSheet.svelte

index a1d02d6ece7d6d037eaccd29fb1e1c6d85fbe213..14f6c5ca027b24f79d217bcb1f76cfd11199ffb6 100644 (file)
 
        let displayedModel = $derived(message.model ?? null);
 
+       // model being switched to while it loads, so the selector bar tracks it
+       let pendingModel = $state<string | null>(null);
+
        let isCurrentlyLoading = $derived(isLoading());
        let isStreaming = $derived(isChatStreaming());
        let hasNoContent = $derived(!message?.content?.trim());
                        >
                                {#if isRouter}
                                        <ModelsSelectorDropdown
-                                               currentModel={displayedModel}
+                                               currentModel={pendingModel ?? displayedModel}
                                                disabled={isLoading()}
                                                onModelChange={async (modelId: string, modelName: string) => {
                                                        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 (file)
index 0000000..fa9a021
--- /dev/null
@@ -0,0 +1,11 @@
+<script lang="ts">
+       let { percent }: { percent: number } = $props();
+</script>
+
+<!-- thin determinate load bar pinned to the bottom edge, pulsing while it fills -->
+<div class="pointer-events-none absolute inset-x-0 bottom-0 h-0.5 overflow-hidden rounded-b-sm">
+       <div
+               class="h-full animate-pulse bg-primary transition-[width] duration-200 ease-out"
+               style="width: {percent}%"
+       ></div>
+</div>
index 40006a4c935908802f847263570cbc6862a09b41..720963a8db9dc45205a2270ae19e4fe1f8de6dc8 100644 (file)
@@ -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 {
                {/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}
                        <DropdownMenu.Root bind:open={isOpen} onOpenChange={ms.handleOpenChange}>
                                                        <DropdownMenu.Trigger
                                                                {...props}
                                                                class={[
-                                                                       `inline-grid cursor-pointer grid-cols-[1fr_auto_1fr] items-center gap-1.5 rounded-sm bg-background px-1.5 py-1 text-xs shadow-sm transition hover:bg-muted-foreground/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60 dark:bg-muted-foreground/15 dark:text-secondary-foreground`,
+                                                                       `relative inline-grid cursor-pointer grid-cols-[1fr_auto_1fr] items-center gap-1.5 rounded-sm bg-background px-1.5 py-1 text-xs shadow-sm transition hover:bg-muted-foreground/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60 dark:bg-muted-foreground/15 dark:text-secondary-foreground`,
                                                                        !ms.isCurrentModelInCache
                                                                                ? 'bg-red-400/10 !text-red-400 hover:bg-red-400/20 hover:text-red-400'
                                                                                : forceForegroundText
                                                                {:else}
                                                                        <ChevronDown class="h-3 w-3.5 shrink-0" />
                                                                {/if}
+
+                                                               {#if triggerLoading}
+                                                                       <ModelLoadHighlight percent={triggerLoadPercent} />
+                                                               {/if}
                                                        </DropdownMenu.Trigger>
                                                {/snippet}
                                        </Tooltip.Trigger>
index f2a024d31d031e54e83524904fe06acbe207fbee..981111e20157d600249826653219a16e54d10f0a 100644 (file)
@@ -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';
                </div>
 
                {#if isLoading}
-                       <div class="flex w-4 [@media(pointer:coarse)]:w-5 items-center justify-center">
+                       <div class="flex w-4 items-center justify-center [@media(pointer:coarse)]:w-5">
                                <Loader2 class="h-4 w-4 animate-spin text-muted-foreground" />
                        </div>
                {:else if isFailed}
-                       <div class="flex w-4 [@media(pointer:coarse)]:w-auto items-center justify-center">
+                       <div class="flex w-4 items-center justify-center [@media(pointer:coarse)]:w-auto">
                                <CircleAlert
                                        class="h-3.5 w-3.5 text-red-500 group-hover:hidden [@media(pointer:coarse)]:hidden"
                                />
                                </div>
                        </div>
                {:else if isSleeping}
-                       <div class="flex w-4 [@media(pointer:coarse)]:w-auto items-center justify-center">
+                       <div class="flex w-4 items-center justify-center [@media(pointer:coarse)]:w-auto">
                                <span
                                        class="h-2 w-2 rounded-full bg-orange-400 group-hover:hidden [@media(pointer:coarse)]:hidden"
                                ></span>
                                </div>
                        </div>
                {:else if isLoaded}
-                       <div class="flex w-4 [@media(pointer:coarse)]:w-auto items-center justify-center">
+                       <div class="flex w-4 items-center justify-center [@media(pointer:coarse)]:w-auto">
                                <span
                                        class="h-2 w-2 rounded-full bg-green-500 group-hover:hidden [@media(pointer:coarse)]:hidden"
                                ></span>
                                </div>
                        </div>
                {:else}
-                       <div class="flex w-4 [@media(pointer:coarse)]:w-auto items-center justify-center">
+                       <div class="flex w-4 items-center justify-center [@media(pointer:coarse)]:w-auto">
                                <span
                                        class="h-2 w-2 rounded-full bg-muted-foreground/50 group-hover:hidden [@media(pointer:coarse)]:hidden"
                                ></span>
        </div>
 
        {#if isLoading}
-               <div
-                       class="pointer-events-none absolute inset-x-0 bottom-0 h-0.5 overflow-hidden rounded-b-sm bg-muted"
-               >
-                       <div
-                               class="h-full bg-primary transition-[width] duration-200 ease-out"
-                               style="width: {loadPercent}%"
-                       ></div>
-               </div>
+               <ModelLoadHighlight percent={loadPercent} />
        {/if}
 </div>
index e5920a2a0f497522db7e6be9b664e711eda065c9..a9e9ea1c8d5d52c8ff032b11b859675dbf89d5d4 100644 (file)
@@ -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;
                <p class="text-xs text-muted-foreground">No models available.</p>
        {: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}
                        <button
                                type="button"
                                class={[
-                                       `inline-flex cursor-pointer items-center gap-1.5 rounded-sm bg-background px-1.5 py-1 max-sm:px-3 max-sm:py-2 text-xs max-sm:text-sm shadow-sm transition hover:bg-muted-foreground/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60 dark:bg-muted-foreground/15 dark:text-secondary-foreground`,
+                                       `relative inline-flex cursor-pointer items-center gap-1.5 rounded-sm bg-background px-1.5 py-1 text-xs shadow-sm transition hover:bg-muted-foreground/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60 max-sm:px-3 max-sm:py-2 max-sm:text-sm dark:bg-muted-foreground/15 dark:text-secondary-foreground`,
                                        !ms.isCurrentModelInCache
                                                ? 'bg-red-400/10 !text-red-400 hover:bg-red-400/20 hover:text-red-400'
                                                : forceForegroundText
                                {:else}
                                        <ChevronDown class="h-3 w-3.5 shrink-0" />
                                {/if}
+
+                               {#if triggerLoading}
+                                       <ModelLoadHighlight percent={triggerLoadPercent} />
+                               {/if}
                        </button>
 
                        <Sheet.Root bind:open={sheetOpen} onOpenChange={handleSheetOpenChange}>