]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/commitdiff
Add SLEEPING status to the WebUI model selector (#20949)
authorPascal <redacted>
Wed, 25 Mar 2026 10:02:32 +0000 (11:02 +0100)
committerGitHub <redacted>
Wed, 25 Mar 2026 10:02:32 +0000 (11:02 +0100)
* webui: handle sleeping model status, fix favourite -> favorite

* Update tools/server/webui/src/lib/components/app/models/ModelsSelectorOption.svelte

Co-authored-by: Aleksander Grygier <redacted>
* Update tools/server/webui/src/lib/components/app/models/ModelsSelectorOption.svelte

Co-authored-by: Aleksander Grygier <redacted>
* webui: fix optional event parameter in sleeping model onclick

* typo

* webui: restore orange sleeping indicator dot with hover unload

* chore: update webui build output

* webui: move stopPropagation into ActionIcon onclick, remove svelte-ignore

* chore: update webui build output

* webui: fix favourite -> favorite (UK -> US spelling) everywhere

Address review feedback from WhyNotHugo

* chore: update webui build output

---------

Co-authored-by: Aleksander Grygier <redacted>
tools/server/public/index.html.gz
tools/server/webui/src/lib/components/app/models/ModelsSelector.svelte
tools/server/webui/src/lib/components/app/models/ModelsSelectorList.svelte
tools/server/webui/src/lib/components/app/models/ModelsSelectorOption.svelte
tools/server/webui/src/lib/components/app/models/ModelsSelectorSheet.svelte
tools/server/webui/src/lib/components/app/models/index.ts
tools/server/webui/src/lib/components/app/models/utils.ts
tools/server/webui/src/lib/constants/localstorage-keys.ts
tools/server/webui/src/lib/enums/server.ts
tools/server/webui/src/lib/stores/models.svelte.ts
tools/server/webui/src/lib/types/api.d.ts

index 0144abda450d4a34a2f2d9204e5590715bd3790d..e2ef75aebef1022bfc31fc0e30fc9ef365b1715a 100644 (file)
Binary files a/tools/server/public/index.html.gz and b/tools/server/public/index.html.gz differ
index bf489443fa8e1add2801fd4f0419cd31033e98c7..0ed6e5c1ddb3a811613531da754990d27be28e8e 100644 (file)
@@ -77,7 +77,7 @@
        let filteredOptions = $derived(filterModelOptions(options, searchTerm));
 
        let groupedFilteredOptions = $derived(
-               groupModelOptions(filteredOptions, modelsStore.favouriteModelIds, (m) =>
+               groupModelOptions(filteredOptions, modelsStore.favoriteModelIds, (m) =>
                        modelsStore.isModelLoaded(m)
                )
        );
                                                                {@const { option, flatIndex } = item}
                                                                {@const isSelected = currentModel === option.model || activeId === option.id}
                                                                {@const isHighlighted = flatIndex === highlightedIndex}
-                                                               {@const isFav = modelsStore.favouriteModelIds.has(option.model)}
+                                                               {@const isFav = modelsStore.favoriteModelIds.has(option.model)}
 
                                                                <ModelsSelectorOption
                                                                        {option}
index 86d798670ca0bb8c30e9fd7b393e7031ed95596e..5c36adc14639511ed6fd1bad55f4f26716e5c235 100644 (file)
@@ -30,7 +30,7 @@
 {#snippet defaultOption(item: ModelItem, showOrgName: boolean)}
        {@const { option } = item}
        {@const isSelected = currentModel === option.model || activeId === option.id}
-       {@const isFav = modelsStore.favouriteModelIds.has(option.model)}
+       {@const isFav = modelsStore.favoriteModelIds.has(option.model)}
 
        <ModelsSelectorOption
                {option}
@@ -52,9 +52,9 @@
        {/each}
 {/if}
 
-{#if groups.favourites.length > 0}
-       <p class={sectionHeaderClass}>Favourite models</p>
-       {#each groups.favourites as item (`fav-${item.option.id}`)}
+{#if groups.favorites.length > 0}
+       <p class={sectionHeaderClass}>Favorite models</p>
+       {#each groups.favorites as item (`fav-${item.option.id}`)}
                {@render render(item, true)}
        {/each}
 {/if}
index 8f44bb8de103edbacf009ae10e0e3ebab26dbb3c..3236130a9d6a8595b511ac8031136f1c5d87f361 100644 (file)
        });
        let isOperationInProgress = $derived(modelsStore.isModelOperationInProgress(option.model));
        let isFailed = $derived(serverStatus === ServerModelStatus.FAILED);
-       let isLoaded = $derived(serverStatus === ServerModelStatus.LOADED && !isOperationInProgress);
+       let isSleeping = $derived(serverStatus === ServerModelStatus.SLEEPING);
+       let isLoaded = $derived(
+               (serverStatus === ServerModelStatus.LOADED || isSleeping) && !isOperationInProgress
+       );
        let isLoading = $derived(serverStatus === ServerModelStatus.LOADING || isOperationInProgress);
 </script>
 
                                <ActionIcon
                                        iconSize="h-2.5 w-2.5"
                                        icon={HeartOff}
-                                       tooltip="Remove from favourites"
+                                       tooltip="Remove from favorites"
                                        class="h-3 w-3 hover:text-foreground"
-                                       onclick={() => modelsStore.toggleFavourite(option.model)}
+                                       onclick={() => modelsStore.toggleFavorite(option.model)}
                                />
                        {:else}
                                <ActionIcon
                                        iconSize="h-2.5 w-2.5"
                                        icon={Heart}
-                                       tooltip="Add to favourites"
+                                       tooltip="Add to favorites"
                                        class="h-3 w-3 hover:text-foreground"
-                                       onclick={() => modelsStore.toggleFavourite(option.model)}
+                                       onclick={() => modelsStore.toggleFavorite(option.model)}
                                />
                        {/if}
 
                                        />
                                </div>
                        </div>
+               {:else if isSleeping}
+                       <div class="flex w-4 items-center justify-center">
+                               <span class="h-2 w-2 rounded-full bg-orange-400 group-hover:hidden"></span>
+
+                               <div class="hidden group-hover:flex">
+                                       <ActionIcon
+                                               iconSize="h-2.5 w-2.5"
+                                               icon={PowerOff}
+                                               tooltip="Unload model"
+                                               class="h-3 w-3 text-red-500 hover:text-red-600"
+                                               onclick={(e) => {
+                                                       e?.stopPropagation();
+                                                       modelsStore.unloadModel(option.model);
+                                               }}
+                                       />
+                               </div>
+                       </div>
                {:else if isLoaded}
                        <div class="flex w-4 items-center justify-center">
                                <span class="h-2 w-2 rounded-full bg-green-500 group-hover:hidden"></span>
index 26f2b72d2b22e48464f87392b4c3b1aa556cb8f2..fe88c979f92c24f4352780604bdcca5ad32c64cf 100644 (file)
@@ -76,7 +76,7 @@
        let filteredOptions = $derived(filterModelOptions(options, searchTerm));
 
        let groupedFilteredOptions = $derived(
-               groupModelOptions(filteredOptions, modelsStore.favouriteModelIds, (m) =>
+               groupModelOptions(filteredOptions, modelsStore.favoriteModelIds, (m) =>
                        modelsStore.isModelLoaded(m)
                )
        );
index 6a873450539550bd6777021191c3788a9604e7d7..b4bcdf4308ee2fccef71c535982a2a4cdabed37e 100644 (file)
@@ -47,7 +47,7 @@ export { default as ModelsSelector } from './ModelsSelector.svelte';
 /**
  * **ModelsSelectorList** - Grouped model options list
  *
- * Renders grouped model options (loaded, favourites, available) with section
+ * Renders grouped model options (loaded, favorites, available) with section
  * headers and org subgroups. Shared between ModelsSelector and ModelsSelectorSheet
  * to avoid template duplication.
  *
@@ -59,7 +59,7 @@ export { default as ModelsSelectorList } from './ModelsSelectorList.svelte';
 /**
  * **ModelsSelectorOption** - Single model option row
  *
- * Renders a single model option with selection state, favourite toggle,
+ * Renders a single model option with selection state, favorite toggle,
  * load/unload actions, status indicators, and an info button.
  * Used inside ModelsSelectorList or directly in custom render snippets.
  */
index b3616ede8e58f81302079bada0da246299f9ac00..ae1f511e9f667c2ee09b666cd3574c5118126978 100644 (file)
@@ -13,7 +13,7 @@ export interface OrgGroup {
 
 export interface GroupedModelOptions {
        loaded: ModelItem[];
-       favourites: ModelItem[];
+       favorites: ModelItem[];
        available: OrgGroup[];
 }
 
@@ -32,7 +32,7 @@ export function filterModelOptions(options: ModelOption[], searchTerm: string):
 
 export function groupModelOptions(
        filteredOptions: ModelOption[],
-       favouriteIds: Set<string>,
+       favoriteIds: Set<string>,
        isModelLoaded: (model: string) => boolean
 ): GroupedModelOptions {
        // Loaded models
@@ -43,24 +43,24 @@ export function groupModelOptions(
                }
        }
 
-       // Favourites (excluding loaded)
+       // Favorites (excluding loaded)
        const loadedModelIds = new Set(loaded.map((item) => item.option.model));
-       const favourites: ModelItem[] = [];
+       const favorites: ModelItem[] = [];
        for (let i = 0; i < filteredOptions.length; i++) {
                if (
-                       favouriteIds.has(filteredOptions[i].model) &&
+                       favoriteIds.has(filteredOptions[i].model) &&
                        !loadedModelIds.has(filteredOptions[i].model)
                ) {
-                       favourites.push({ option: filteredOptions[i], flatIndex: i });
+                       favorites.push({ option: filteredOptions[i], flatIndex: i });
                }
        }
 
-       // Available models grouped by org (excluding loaded and favourites)
+       // Available models grouped by org (excluding loaded and favorites)
        const available: OrgGroup[] = [];
        const orgGroups = new SvelteMap<string, ModelItem[]>();
        for (let i = 0; i < filteredOptions.length; i++) {
                const option = filteredOptions[i];
-               if (loadedModelIds.has(option.model) || favouriteIds.has(option.model)) continue;
+               if (loadedModelIds.has(option.model) || favoriteIds.has(option.model)) continue;
 
                const key = option.parsedId?.orgName ?? '';
                if (!orgGroups.has(key)) orgGroups.set(key, []);
@@ -71,5 +71,5 @@ export function groupModelOptions(
                available.push({ orgName: orgName || null, items });
        }
 
-       return { loaded, favourites, available };
+       return { loaded, favorites, available };
 }
index dc4d69b4eca9d6ef3e57a7bfbfd190321b586ca0..2a2b629fd6ee8c7b8f620e59d8d2e6c90fb784ec 100644 (file)
@@ -1,4 +1,4 @@
 export const CONFIG_LOCALSTORAGE_KEY = 'LlamaCppWebui.config';
 export const USER_OVERRIDES_LOCALSTORAGE_KEY = 'LlamaCppWebui.userOverrides';
-export const FAVOURITE_MODELS_LOCALSTORAGE_KEY = 'LlamaCppWebui.favouriteModels';
+export const FAVORITE_MODELS_LOCALSTORAGE_KEY = 'LlamaCppWebui.favoriteModels';
 export const MCP_DEFAULT_ENABLED_LOCALSTORAGE_KEY = 'LlamaCppWebui.mcpDefaultEnabled';
index 7f30eab2cf6cca2f1edf33acb2592122be899940..c9d599c52bfa7ad04ff459222b79ed8a506a8041 100644 (file)
@@ -16,5 +16,6 @@ export enum ServerModelStatus {
        UNLOADED = 'unloaded',
        LOADING = 'loading',
        LOADED = 'loaded',
+       SLEEPING = 'sleeping',
        FAILED = 'failed'
 }
index 50c32034a62d5fb6f79c32f59c158947d2f58364..d7c885844fc8373681f3a7a5b7d0affaa4b8fb51 100644 (file)
@@ -7,7 +7,7 @@ import { TTLCache } from '$lib/utils';
 import {
        MODEL_PROPS_CACHE_TTL_MS,
        MODEL_PROPS_CACHE_MAX_ENTRIES,
-       FAVOURITE_MODELS_LOCALSTORAGE_KEY
+       FAVORITE_MODELS_LOCALSTORAGE_KEY
 } from '$lib/constants';
 
 /**
@@ -57,7 +57,7 @@ class ModelsStore {
        private modelUsage = $state<Map<string, SvelteSet<string>>>(new Map());
        private modelLoadingStates = new SvelteMap<string, boolean>();
 
-       favouriteModelIds = $state<Set<string>>(this.loadFavouritesFromStorage());
+       favoriteModelIds = $state<Set<string>>(this.loadFavoritesFromStorage());
 
        /**
         * Model-specific props cache with TTL
@@ -90,7 +90,11 @@ class ModelsStore {
 
        get loadedModelIds(): string[] {
                return this.routerModels
-                       .filter((m) => m.status.value === ServerModelStatus.LOADED)
+                       .filter(
+                               (m) =>
+                                       m.status.value === ServerModelStatus.LOADED ||
+                                       m.status.value === ServerModelStatus.SLEEPING
+                       )
                        .map((m) => m.id);
        }
 
@@ -215,7 +219,11 @@ class ModelsStore {
 
        isModelLoaded(modelId: string): boolean {
                const model = this.routerModels.find((m) => m.id === modelId);
-               return model?.status.value === ServerModelStatus.LOADED || false;
+               return (
+                       model?.status.value === ServerModelStatus.LOADED ||
+                       model?.status.value === ServerModelStatus.SLEEPING ||
+                       false
+               );
        }
 
        isModelOperationInProgress(modelId: string): boolean {
@@ -621,17 +629,17 @@ class ModelsStore {
        /**
         *
         *
-        * Favourites
+        * Favorites
         *
         *
         */
 
-       isFavourite(modelId: string): boolean {
-               return this.favouriteModelIds.has(modelId);
+       isFavorite(modelId: string): boolean {
+               return this.favoriteModelIds.has(modelId);
        }
 
-       toggleFavourite(modelId: string): void {
-               const next = new SvelteSet(this.favouriteModelIds);
+       toggleFavorite(modelId: string): void {
+               const next = new SvelteSet(this.favoriteModelIds);
 
                if (next.has(modelId)) {
                        next.delete(modelId);
@@ -639,22 +647,22 @@ class ModelsStore {
                        next.add(modelId);
                }
 
-               this.favouriteModelIds = next;
+               this.favoriteModelIds = next;
 
                try {
-                       localStorage.setItem(FAVOURITE_MODELS_LOCALSTORAGE_KEY, JSON.stringify([...next]));
+                       localStorage.setItem(FAVORITE_MODELS_LOCALSTORAGE_KEY, JSON.stringify([...next]));
                } catch {
-                       toast.error('Failed to save favourite models to local storage');
+                       toast.error('Failed to save favorite models to local storage');
                }
        }
 
-       private loadFavouritesFromStorage(): Set<string> {
+       private loadFavoritesFromStorage(): Set<string> {
                try {
-                       const raw = localStorage.getItem(FAVOURITE_MODELS_LOCALSTORAGE_KEY);
+                       const raw = localStorage.getItem(FAVORITE_MODELS_LOCALSTORAGE_KEY);
 
                        return raw ? new Set(JSON.parse(raw) as string[]) : new Set();
                } catch {
-                       toast.error('Failed to load favourite models from local storage');
+                       toast.error('Failed to load favorite models from local storage');
 
                        return new Set();
                }
@@ -713,4 +721,4 @@ export const loadingModelIds = () => modelsStore.loadingModelIds;
 export const propsCacheVersion = () => modelsStore.propsCacheVersion;
 export const singleModelName = () => modelsStore.singleModelName;
 export const selectedModelContextSize = () => modelsStore.selectedModelContextSize;
-export const favouriteModelIds = () => modelsStore.favouriteModelIds;
+export const favoriteModelIds = () => modelsStore.favoriteModelIds;
index 7cbd6db97b8a93aa1d86f8db28fd1792e2ac33ff..f7f876c875dbb75dbe1f3541de9bbff409ed7447 100644 (file)
@@ -54,7 +54,7 @@ export interface ApiChatMessageData {
  * Model status object from /models endpoint
  */
 export interface ApiModelStatus {
-       /** Status value: loaded, unloaded, loading, failed */
+       /** Status value: loaded, unloaded, loading, sleeping, failed */
        value: ServerModelStatus;
        /** Command line arguments used when loading (only for loaded models) */
        args?: string[];