Skip to content

Commit 062cca5

Browse files
Add SLEEPING status to the WebUI model selector (ggml-org#20949)
* webui: handle sleeping model status, fix favourite -> favorite * Update tools/server/webui/src/lib/components/app/models/ModelsSelectorOption.svelte Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com> * Update tools/server/webui/src/lib/components/app/models/ModelsSelectorOption.svelte Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com> * 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 <aleksander.grygier@gmail.com>
1 parent 406f4e3 commit 062cca5

11 files changed

Lines changed: 70 additions & 41 deletions

File tree

tools/server/public/index.html.gz

117 Bytes
Binary file not shown.

tools/server/webui/src/lib/components/app/models/ModelsSelector.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
let filteredOptions = $derived(filterModelOptions(options, searchTerm));
7878
7979
let groupedFilteredOptions = $derived(
80-
groupModelOptions(filteredOptions, modelsStore.favouriteModelIds, (m) =>
80+
groupModelOptions(filteredOptions, modelsStore.favoriteModelIds, (m) =>
8181
modelsStore.isModelLoaded(m)
8282
)
8383
);
@@ -353,7 +353,7 @@
353353
{@const { option, flatIndex } = item}
354354
{@const isSelected = currentModel === option.model || activeId === option.id}
355355
{@const isHighlighted = flatIndex === highlightedIndex}
356-
{@const isFav = modelsStore.favouriteModelIds.has(option.model)}
356+
{@const isFav = modelsStore.favoriteModelIds.has(option.model)}
357357

358358
<ModelsSelectorOption
359359
{option}

tools/server/webui/src/lib/components/app/models/ModelsSelectorList.svelte

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
{#snippet defaultOption(item: ModelItem, showOrgName: boolean)}
3131
{@const { option } = item}
3232
{@const isSelected = currentModel === option.model || activeId === option.id}
33-
{@const isFav = modelsStore.favouriteModelIds.has(option.model)}
33+
{@const isFav = modelsStore.favoriteModelIds.has(option.model)}
3434

3535
<ModelsSelectorOption
3636
{option}
@@ -52,9 +52,9 @@
5252
{/each}
5353
{/if}
5454

55-
{#if groups.favourites.length > 0}
56-
<p class={sectionHeaderClass}>Favourite models</p>
57-
{#each groups.favourites as item (`fav-${item.option.id}`)}
55+
{#if groups.favorites.length > 0}
56+
<p class={sectionHeaderClass}>Favorite models</p>
57+
{#each groups.favorites as item (`fav-${item.option.id}`)}
5858
{@render render(item, true)}
5959
{/each}
6060
{/if}

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

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@
4646
});
4747
let isOperationInProgress = $derived(modelsStore.isModelOperationInProgress(option.model));
4848
let isFailed = $derived(serverStatus === ServerModelStatus.FAILED);
49-
let isLoaded = $derived(serverStatus === ServerModelStatus.LOADED && !isOperationInProgress);
49+
let isSleeping = $derived(serverStatus === ServerModelStatus.SLEEPING);
50+
let isLoaded = $derived(
51+
(serverStatus === ServerModelStatus.LOADED || isSleeping) && !isOperationInProgress
52+
);
5053
let isLoading = $derived(serverStatus === ServerModelStatus.LOADING || isOperationInProgress);
5154
</script>
5255

@@ -85,17 +88,17 @@
8588
<ActionIcon
8689
iconSize="h-2.5 w-2.5"
8790
icon={HeartOff}
88-
tooltip="Remove from favourites"
91+
tooltip="Remove from favorites"
8992
class="h-3 w-3 hover:text-foreground"
90-
onclick={() => modelsStore.toggleFavourite(option.model)}
93+
onclick={() => modelsStore.toggleFavorite(option.model)}
9194
/>
9295
{:else}
9396
<ActionIcon
9497
iconSize="h-2.5 w-2.5"
9598
icon={Heart}
96-
tooltip="Add to favourites"
99+
tooltip="Add to favorites"
97100
class="h-3 w-3 hover:text-foreground"
98-
onclick={() => modelsStore.toggleFavourite(option.model)}
101+
onclick={() => modelsStore.toggleFavorite(option.model)}
99102
/>
100103
{/if}
101104

@@ -129,6 +132,23 @@
129132
/>
130133
</div>
131134
</div>
135+
{:else if isSleeping}
136+
<div class="flex w-4 items-center justify-center">
137+
<span class="h-2 w-2 rounded-full bg-orange-400 group-hover:hidden"></span>
138+
139+
<div class="hidden group-hover:flex">
140+
<ActionIcon
141+
iconSize="h-2.5 w-2.5"
142+
icon={PowerOff}
143+
tooltip="Unload model"
144+
class="h-3 w-3 text-red-500 hover:text-red-600"
145+
onclick={(e) => {
146+
e?.stopPropagation();
147+
modelsStore.unloadModel(option.model);
148+
}}
149+
/>
150+
</div>
151+
</div>
132152
{:else if isLoaded}
133153
<div class="flex w-4 items-center justify-center">
134154
<span class="h-2 w-2 rounded-full bg-green-500 group-hover:hidden"></span>

tools/server/webui/src/lib/components/app/models/ModelsSelectorSheet.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
let filteredOptions = $derived(filterModelOptions(options, searchTerm));
7777
7878
let groupedFilteredOptions = $derived(
79-
groupModelOptions(filteredOptions, modelsStore.favouriteModelIds, (m) =>
79+
groupModelOptions(filteredOptions, modelsStore.favoriteModelIds, (m) =>
8080
modelsStore.isModelLoaded(m)
8181
)
8282
);

tools/server/webui/src/lib/components/app/models/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export { default as ModelsSelector } from './ModelsSelector.svelte';
4747
/**
4848
* **ModelsSelectorList** - Grouped model options list
4949
*
50-
* Renders grouped model options (loaded, favourites, available) with section
50+
* Renders grouped model options (loaded, favorites, available) with section
5151
* headers and org subgroups. Shared between ModelsSelector and ModelsSelectorSheet
5252
* to avoid template duplication.
5353
*
@@ -59,7 +59,7 @@ export { default as ModelsSelectorList } from './ModelsSelectorList.svelte';
5959
/**
6060
* **ModelsSelectorOption** - Single model option row
6161
*
62-
* Renders a single model option with selection state, favourite toggle,
62+
* Renders a single model option with selection state, favorite toggle,
6363
* load/unload actions, status indicators, and an info button.
6464
* Used inside ModelsSelectorList or directly in custom render snippets.
6565
*/

tools/server/webui/src/lib/components/app/models/utils.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export interface OrgGroup {
1313

1414
export interface GroupedModelOptions {
1515
loaded: ModelItem[];
16-
favourites: ModelItem[];
16+
favorites: ModelItem[];
1717
available: OrgGroup[];
1818
}
1919

@@ -32,7 +32,7 @@ export function filterModelOptions(options: ModelOption[], searchTerm: string):
3232

3333
export function groupModelOptions(
3434
filteredOptions: ModelOption[],
35-
favouriteIds: Set<string>,
35+
favoriteIds: Set<string>,
3636
isModelLoaded: (model: string) => boolean
3737
): GroupedModelOptions {
3838
// Loaded models
@@ -43,24 +43,24 @@ export function groupModelOptions(
4343
}
4444
}
4545

46-
// Favourites (excluding loaded)
46+
// Favorites (excluding loaded)
4747
const loadedModelIds = new Set(loaded.map((item) => item.option.model));
48-
const favourites: ModelItem[] = [];
48+
const favorites: ModelItem[] = [];
4949
for (let i = 0; i < filteredOptions.length; i++) {
5050
if (
51-
favouriteIds.has(filteredOptions[i].model) &&
51+
favoriteIds.has(filteredOptions[i].model) &&
5252
!loadedModelIds.has(filteredOptions[i].model)
5353
) {
54-
favourites.push({ option: filteredOptions[i], flatIndex: i });
54+
favorites.push({ option: filteredOptions[i], flatIndex: i });
5555
}
5656
}
5757

58-
// Available models grouped by org (excluding loaded and favourites)
58+
// Available models grouped by org (excluding loaded and favorites)
5959
const available: OrgGroup[] = [];
6060
const orgGroups = new SvelteMap<string, ModelItem[]>();
6161
for (let i = 0; i < filteredOptions.length; i++) {
6262
const option = filteredOptions[i];
63-
if (loadedModelIds.has(option.model) || favouriteIds.has(option.model)) continue;
63+
if (loadedModelIds.has(option.model) || favoriteIds.has(option.model)) continue;
6464

6565
const key = option.parsedId?.orgName ?? '';
6666
if (!orgGroups.has(key)) orgGroups.set(key, []);
@@ -71,5 +71,5 @@ export function groupModelOptions(
7171
available.push({ orgName: orgName || null, items });
7272
}
7373

74-
return { loaded, favourites, available };
74+
return { loaded, favorites, available };
7575
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export const CONFIG_LOCALSTORAGE_KEY = 'LlamaCppWebui.config';
22
export const USER_OVERRIDES_LOCALSTORAGE_KEY = 'LlamaCppWebui.userOverrides';
3-
export const FAVOURITE_MODELS_LOCALSTORAGE_KEY = 'LlamaCppWebui.favouriteModels';
3+
export const FAVORITE_MODELS_LOCALSTORAGE_KEY = 'LlamaCppWebui.favoriteModels';
44
export const MCP_DEFAULT_ENABLED_LOCALSTORAGE_KEY = 'LlamaCppWebui.mcpDefaultEnabled';

tools/server/webui/src/lib/enums/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ export enum ServerModelStatus {
1616
UNLOADED = 'unloaded',
1717
LOADING = 'loading',
1818
LOADED = 'loaded',
19+
SLEEPING = 'sleeping',
1920
FAILED = 'failed'
2021
}

tools/server/webui/src/lib/stores/models.svelte.ts

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { TTLCache } from '$lib/utils';
77
import {
88
MODEL_PROPS_CACHE_TTL_MS,
99
MODEL_PROPS_CACHE_MAX_ENTRIES,
10-
FAVOURITE_MODELS_LOCALSTORAGE_KEY
10+
FAVORITE_MODELS_LOCALSTORAGE_KEY
1111
} from '$lib/constants';
1212

1313
/**
@@ -57,7 +57,7 @@ class ModelsStore {
5757
private modelUsage = $state<Map<string, SvelteSet<string>>>(new Map());
5858
private modelLoadingStates = new SvelteMap<string, boolean>();
5959

60-
favouriteModelIds = $state<Set<string>>(this.loadFavouritesFromStorage());
60+
favoriteModelIds = $state<Set<string>>(this.loadFavoritesFromStorage());
6161

6262
/**
6363
* Model-specific props cache with TTL
@@ -90,7 +90,11 @@ class ModelsStore {
9090

9191
get loadedModelIds(): string[] {
9292
return this.routerModels
93-
.filter((m) => m.status.value === ServerModelStatus.LOADED)
93+
.filter(
94+
(m) =>
95+
m.status.value === ServerModelStatus.LOADED ||
96+
m.status.value === ServerModelStatus.SLEEPING
97+
)
9498
.map((m) => m.id);
9599
}
96100

@@ -215,7 +219,11 @@ class ModelsStore {
215219

216220
isModelLoaded(modelId: string): boolean {
217221
const model = this.routerModels.find((m) => m.id === modelId);
218-
return model?.status.value === ServerModelStatus.LOADED || false;
222+
return (
223+
model?.status.value === ServerModelStatus.LOADED ||
224+
model?.status.value === ServerModelStatus.SLEEPING ||
225+
false
226+
);
219227
}
220228

221229
isModelOperationInProgress(modelId: string): boolean {
@@ -621,40 +629,40 @@ class ModelsStore {
621629
/**
622630
*
623631
*
624-
* Favourites
632+
* Favorites
625633
*
626634
*
627635
*/
628636

629-
isFavourite(modelId: string): boolean {
630-
return this.favouriteModelIds.has(modelId);
637+
isFavorite(modelId: string): boolean {
638+
return this.favoriteModelIds.has(modelId);
631639
}
632640

633-
toggleFavourite(modelId: string): void {
634-
const next = new SvelteSet(this.favouriteModelIds);
641+
toggleFavorite(modelId: string): void {
642+
const next = new SvelteSet(this.favoriteModelIds);
635643

636644
if (next.has(modelId)) {
637645
next.delete(modelId);
638646
} else {
639647
next.add(modelId);
640648
}
641649

642-
this.favouriteModelIds = next;
650+
this.favoriteModelIds = next;
643651

644652
try {
645-
localStorage.setItem(FAVOURITE_MODELS_LOCALSTORAGE_KEY, JSON.stringify([...next]));
653+
localStorage.setItem(FAVORITE_MODELS_LOCALSTORAGE_KEY, JSON.stringify([...next]));
646654
} catch {
647-
toast.error('Failed to save favourite models to local storage');
655+
toast.error('Failed to save favorite models to local storage');
648656
}
649657
}
650658

651-
private loadFavouritesFromStorage(): Set<string> {
659+
private loadFavoritesFromStorage(): Set<string> {
652660
try {
653-
const raw = localStorage.getItem(FAVOURITE_MODELS_LOCALSTORAGE_KEY);
661+
const raw = localStorage.getItem(FAVORITE_MODELS_LOCALSTORAGE_KEY);
654662

655663
return raw ? new Set(JSON.parse(raw) as string[]) : new Set();
656664
} catch {
657-
toast.error('Failed to load favourite models from local storage');
665+
toast.error('Failed to load favorite models from local storage');
658666

659667
return new Set();
660668
}
@@ -713,4 +721,4 @@ export const loadingModelIds = () => modelsStore.loadingModelIds;
713721
export const propsCacheVersion = () => modelsStore.propsCacheVersion;
714722
export const singleModelName = () => modelsStore.singleModelName;
715723
export const selectedModelContextSize = () => modelsStore.selectedModelContextSize;
716-
export const favouriteModelIds = () => modelsStore.favouriteModelIds;
724+
export const favoriteModelIds = () => modelsStore.favoriteModelIds;

0 commit comments

Comments
 (0)