mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-30 01:47:39 -05:00
ui : keep the collapsed model sections and families in local storage
Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
@@ -17,6 +17,8 @@
|
||||
defaultOpen?: boolean;
|
||||
/** Controlled open state, for a caller that binds it. */
|
||||
open?: boolean;
|
||||
/** Called on every toggle, for a caller that persists the state. */
|
||||
ontoggle?: ((open: boolean) => void) | null;
|
||||
/** Extra classes for the chevron, to line it up with a row's own control. */
|
||||
chevronClass?: string;
|
||||
/** Hide the chevron while expanded, until the trigger is hovered. */
|
||||
@@ -32,6 +34,7 @@
|
||||
children,
|
||||
defaultOpen = true,
|
||||
inMenu = false,
|
||||
ontoggle = null,
|
||||
open = $bindable(defaultOpen),
|
||||
revealChevronOnHover = false,
|
||||
trigger,
|
||||
@@ -39,6 +42,11 @@
|
||||
triggerPosition = 'top',
|
||||
triggerStyle = ''
|
||||
}: Props = $props();
|
||||
|
||||
function toggle(): void {
|
||||
open = !open;
|
||||
ontoggle?.(open);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet chevron()}
|
||||
@@ -65,13 +73,7 @@
|
||||
>
|
||||
{#snippet child({ props })}
|
||||
<!-- No `class` here: a static attribute would override the spread props.class. -->
|
||||
<button
|
||||
{...props}
|
||||
aria-expanded={open}
|
||||
onclick={() => (open = !open)}
|
||||
style={triggerStyle}
|
||||
type="button"
|
||||
>
|
||||
<button {...props} aria-expanded={open} onclick={toggle} style={triggerStyle} type="button">
|
||||
{@render trigger()}
|
||||
|
||||
{@render chevron()}
|
||||
@@ -82,7 +84,7 @@
|
||||
<button
|
||||
aria-expanded={open}
|
||||
class="group {triggerClass}"
|
||||
onclick={() => (open = !open)}
|
||||
onclick={toggle}
|
||||
style={triggerStyle}
|
||||
type="button"
|
||||
>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
<script generics="G, E" lang="ts">
|
||||
import CollapsibleRegion from './CollapsibleRegion.svelte';
|
||||
import { modelsStore } from '$lib/stores';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||
|
||||
@@ -21,6 +22,8 @@
|
||||
groups?: GroupedListGroup<G, E>[] | null;
|
||||
/** Entries of one group shown before its show more row. 0 shows every entry. */
|
||||
groupWindow?: number;
|
||||
/** Namespace of the persisted open state, e.g. a section key. Omit to keep it in memory. */
|
||||
groupStateKey?: string;
|
||||
/** One row. Depth is 1 under a group, 0 in a flat list. */
|
||||
item: Snippet<[{ depth: number; entry: E }]>;
|
||||
/** Identity of a row, used for the keyed each. */
|
||||
@@ -42,6 +45,7 @@
|
||||
let {
|
||||
group,
|
||||
groups = null,
|
||||
groupStateKey,
|
||||
groupWindow = 0,
|
||||
item,
|
||||
items = [],
|
||||
@@ -53,8 +57,13 @@
|
||||
weightOf
|
||||
}: Props = $props();
|
||||
|
||||
// groups start open; this tracks the ones the user folded away
|
||||
const collapsed = new SvelteSet<string>();
|
||||
// groups start open; this tracks the ones the user folded away, and a list with
|
||||
// a state key opens with the ones the user had folded away before the reload
|
||||
// the initial namespace only
|
||||
// svelte-ignore state_referenced_locally
|
||||
const collapsed = new SvelteSet<string>(
|
||||
groupStateKey ? modelsStore.collapsedGroupsUnder(groupStateKey) : []
|
||||
);
|
||||
// windows grow one step at a time, per group and per list
|
||||
const groupSteps = new SvelteMap<string, number>();
|
||||
let sectionSteps = $state(0);
|
||||
@@ -62,11 +71,11 @@
|
||||
function toggleGroup(key: string): void {
|
||||
if (collapsed.has(key)) {
|
||||
collapsed.delete(key);
|
||||
|
||||
return;
|
||||
} else {
|
||||
collapsed.add(key);
|
||||
}
|
||||
|
||||
collapsed.add(key);
|
||||
if (groupStateKey) modelsStore.setGroupOpen(`${groupStateKey}-${key}`, !collapsed.has(key));
|
||||
}
|
||||
|
||||
function growGroup(key: string): void {
|
||||
|
||||
@@ -556,12 +556,14 @@
|
||||
defaultOpen={group.kind !== 'hidden'}
|
||||
icon={group.kind === 'provider' ? undefined : groupIcon}
|
||||
label={group.label}
|
||||
persistKey={group.key}
|
||||
revealChevronOnHover
|
||||
sticky
|
||||
stickyClass="sticky z-10 bg-muted/90 backdrop-blur-lg"
|
||||
>
|
||||
<GroupedList
|
||||
group={familyRow}
|
||||
groupStateKey={group.key}
|
||||
groupWindow={FAMILY_ROW_WINDOW}
|
||||
groups={group.flat ? null : familyGroups(group)}
|
||||
item={listItem}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ChevronLeft, CircleAlert, Loader2 } from '@lucide/svelte';
|
||||
import { CollapsibleSection } from '$lib/components/app';
|
||||
import { BackendIcon } from '$lib/components/app/backends';
|
||||
import { modelsStore } from '$lib/stores';
|
||||
import { getBackend } from '$lib/utils/api-base';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
@@ -21,6 +22,8 @@
|
||||
loading?: boolean;
|
||||
/** Renders the back control, for a drilled-in provider. */
|
||||
onBack?: () => void;
|
||||
/** Persists the open state under this id, e.g. a section key. */
|
||||
persistKey?: string;
|
||||
/** Start expanded; the manager collapses its hidden block. */
|
||||
defaultOpen?: boolean;
|
||||
revealChevronOnHover?: boolean;
|
||||
@@ -42,6 +45,7 @@
|
||||
label,
|
||||
loading = false,
|
||||
onBack,
|
||||
persistKey,
|
||||
revealChevronOnHover = false,
|
||||
sectionHeaderClass = 'm-0 px-2 py-2 text-[13px] font-semibold text-muted-foreground select-none',
|
||||
sticky = false,
|
||||
@@ -57,7 +61,8 @@
|
||||
|
||||
<CollapsibleSection
|
||||
{chevronClass}
|
||||
{defaultOpen}
|
||||
defaultOpen={persistKey ? modelsStore.isGroupOpen(persistKey, defaultOpen) : defaultOpen}
|
||||
ontoggle={persistKey ? (open: boolean) => modelsStore.setGroupOpen(persistKey, open) : null}
|
||||
{revealChevronOnHover}
|
||||
{triggerClass}
|
||||
{triggerStyle}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
type ModelItem,
|
||||
windowLocalGroups
|
||||
} from '$lib/components/app/navigation/utils';
|
||||
import { MODEL_ROW_WINDOW } from '$lib/constants';
|
||||
import { LOCAL_BACKEND_ID, MODEL_ROW_WINDOW } from '$lib/constants';
|
||||
import { ModelDownloadConfirmAction } from '$lib/enums';
|
||||
import { ModelGroupingMode } from '$lib/enums/settings.enums';
|
||||
import { modelsStore, settingsStore } from '$lib/stores';
|
||||
@@ -154,6 +154,7 @@
|
||||
{#if settingsStore.config.groupModelsByFamily}
|
||||
<GroupedList
|
||||
group={familyHeading}
|
||||
groupStateKey={prefix}
|
||||
groups={groupModelFamilies(items, (row) => row.option.model).map((family) => ({
|
||||
entries: family.entries,
|
||||
group: family,
|
||||
@@ -188,7 +189,13 @@
|
||||
{/snippet}
|
||||
|
||||
{#if loaded.length > 0}
|
||||
<ModelsSection count={loaded.length} label="Loaded models" revealChevronOnHover sticky>
|
||||
<ModelsSection
|
||||
count={loaded.length}
|
||||
label="Loaded models"
|
||||
persistKey="loaded"
|
||||
revealChevronOnHover
|
||||
sticky
|
||||
>
|
||||
{#snippet icon()}
|
||||
<Power class="h-3.5 w-3.5 shrink-0" />
|
||||
{/snippet}
|
||||
@@ -201,7 +208,7 @@
|
||||
|
||||
{#if favorites.length > 0}
|
||||
<!-- Favorites come first; the sections below skip them -->
|
||||
<ModelsSection label="Favorites" revealChevronOnHover sticky>
|
||||
<ModelsSection label="Favorites" persistKey="favorites" revealChevronOnHover sticky>
|
||||
{#snippet icon()}
|
||||
<Heart class="h-3.5 w-3.5 shrink-0" />
|
||||
{/snippet}
|
||||
@@ -243,7 +250,7 @@
|
||||
iconKind: 'boxes' | 'logo',
|
||||
key: string
|
||||
)}
|
||||
<ModelsSection {label} revealChevronOnHover sticky>
|
||||
<ModelsSection {label} persistKey={key} revealChevronOnHover sticky>
|
||||
{#snippet icon()}
|
||||
{#if iconKind === 'logo'}
|
||||
<Logo class="shrink-0" style="--size: 0.875rem" />
|
||||
@@ -261,7 +268,7 @@
|
||||
{/each}
|
||||
{:else}
|
||||
{#if groups.loaded.length > 0 || groups.available.length > 0}
|
||||
<ModelsSection label="Local models" revealChevronOnHover sticky>
|
||||
<ModelsSection label="Local models" persistKey={LOCAL_BACKEND_ID} revealChevronOnHover sticky>
|
||||
{#snippet icon()}
|
||||
<Logo class="shrink-0" style="--size: 0.875rem" />
|
||||
{/snippet}
|
||||
@@ -278,6 +285,7 @@
|
||||
label={provider.name}
|
||||
loading={provider.loading}
|
||||
onBack={onProviderBack}
|
||||
persistKey={provider.backendId}
|
||||
revealChevronOnHover
|
||||
sticky
|
||||
>
|
||||
|
||||
@@ -31,6 +31,9 @@ export const DISABLED_TOOL_KEYS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.disabled
|
||||
export const DISABLED_TOOL_CATEGORIES_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.disabledToolCategories`;
|
||||
export const FAVORITE_MODELS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.favoriteModels`;
|
||||
|
||||
/** Open state the user set for a model list section or one of its families, by id. */
|
||||
export const MODEL_GROUP_OPEN_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.modelGroupOpen`;
|
||||
|
||||
/** Model families pinned to the top of their provider section. */
|
||||
export const FAVORITE_MODEL_FAMILIES_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.favoriteModelFamilies`;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
FAVORITE_MODEL_FAMILIES_LOCALSTORAGE_KEY,
|
||||
FAVORITE_MODELS_LOCALSTORAGE_KEY,
|
||||
HIDDEN_MODELS_LOCALSTORAGE_KEY,
|
||||
MODEL_GROUP_OPEN_LOCALSTORAGE_KEY,
|
||||
MODEL_ROW_WINDOW,
|
||||
RECENT_MODEL_LIMIT,
|
||||
RECENT_MODEL_USAGE_LOCALSTORAGE_KEY,
|
||||
@@ -33,7 +34,7 @@ import { settingsStore } from '$lib/stores/settings/index.svelte';
|
||||
import { getBackendCapabilities, readModelContextLength } from '$lib/utils/backend';
|
||||
import { getConversationModel } from '$lib/utils/conversation-utils';
|
||||
import { backendIdFromModelId, qualifyModelId, rawModelId } from '$lib/utils/model-option-id';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
/** Selection kept from the last session, so a reload does not drop the picked model. */
|
||||
@@ -100,6 +101,24 @@ function loadFavoriteFamilies(): Set<string> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Open state the user set for a section or family of the model lists. */
|
||||
function loadGroupOpenState(): SvelteMap<string, boolean> {
|
||||
if (!browser) return new SvelteMap<string, boolean>();
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(MODEL_GROUP_OPEN_LOCALSTORAGE_KEY);
|
||||
const parsed = raw ? (JSON.parse(raw) as unknown) : null;
|
||||
const entries =
|
||||
parsed && typeof parsed === 'object' ? Object.entries(parsed as Record<string, unknown>) : [];
|
||||
|
||||
return new SvelteMap(
|
||||
entries.filter((entry): entry is [string, boolean] => typeof entry[1] === 'boolean')
|
||||
);
|
||||
} catch {
|
||||
return new SvelteMap<string, boolean>();
|
||||
}
|
||||
}
|
||||
|
||||
/** Recently used backend-qualified ids, most recent first. */
|
||||
function loadRecentModels(): string[] {
|
||||
if (!browser) return [];
|
||||
@@ -124,6 +143,7 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
error = $state<string | null>(null);
|
||||
favoriteFamilyIds = $state<Set<string>>(loadFavoriteFamilies());
|
||||
favoriteModelIds = $state<Set<string>>(this.loadFavoritesFromStorage());
|
||||
groupOpenState = $state<SvelteMap<string, boolean>>(loadGroupOpenState());
|
||||
hiddenModelIds = $state<Set<string>>(loadHiddenModels());
|
||||
loading = $state(false);
|
||||
/**
|
||||
@@ -280,6 +300,15 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
this.persistSelection();
|
||||
}
|
||||
|
||||
/** Family keys folded away under one section, for a list that restores them. */
|
||||
collapsedGroupsUnder(prefix: string): string[] {
|
||||
const head = `${prefix}-`;
|
||||
|
||||
return [...this.groupOpenState]
|
||||
.filter(([id, open]) => !open && id.startsWith(head))
|
||||
.map(([id]) => id.slice(head.length));
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-selects the first available model if none is selected.
|
||||
* Prioritizes:
|
||||
@@ -448,6 +477,10 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
return this.favoriteFamilyIds.has(key);
|
||||
}
|
||||
|
||||
isGroupOpen(id: string, fallbackOpen: boolean): boolean {
|
||||
return this.groupOpenState.get(id) ?? fallbackOpen;
|
||||
}
|
||||
|
||||
isHidden(modelId: string): boolean {
|
||||
return this.hiddenModelIds.has(modelId);
|
||||
}
|
||||
@@ -563,6 +596,22 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
}
|
||||
}
|
||||
|
||||
setGroupOpen(id: string, open: boolean): void {
|
||||
const next = new SvelteMap(this.groupOpenState);
|
||||
|
||||
next.set(id, open);
|
||||
this.groupOpenState = next;
|
||||
|
||||
try {
|
||||
localStorage.setItem(
|
||||
MODEL_GROUP_OPEN_LOCALSTORAGE_KEY,
|
||||
JSON.stringify(Object.fromEntries(next))
|
||||
);
|
||||
} catch {
|
||||
toast.error('Failed to save the model list state to local storage');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate a backend for the selector tabs. Everything comes from memory:
|
||||
* the model list and router rows are prefetched at startup and the local
|
||||
|
||||
Reference in New Issue
Block a user