mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-29 09:27:33 -05:00
ui : group the selector models by family
Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
+10
-10
@@ -1,11 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { ModelsTableGroup } from './utils';
|
||||
import {
|
||||
formatLastUsed,
|
||||
groupModelFamilies,
|
||||
type ModelFamilyGroup,
|
||||
type ModelQuantGroup
|
||||
} from './utils';
|
||||
import { formatLastUsed, type ModelQuantGroup } from './utils';
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
@@ -37,6 +32,7 @@
|
||||
import type { ModelOption } from '$lib/types/models';
|
||||
import { getBackend } from '$lib/utils/api-base';
|
||||
import { getBackendCapabilities } from '$lib/utils/backend';
|
||||
import { groupModelFamilies, type ModelFamilyGroup } from '$lib/utils/model-families';
|
||||
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||
|
||||
interface Props {
|
||||
@@ -72,7 +68,11 @@
|
||||
groups.map((group) => {
|
||||
const flat = FLAT_SECTIONS.has(group.kind);
|
||||
|
||||
return { ...group, families: flat ? [] : groupModelFamilies(group.items), flat };
|
||||
return {
|
||||
...group,
|
||||
families: flat ? [] : groupModelFamilies(group.items, (entry) => entry.base.model),
|
||||
flat
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
|
||||
/** Cut a section down to the mounted window, counting models rather than groups. */
|
||||
function windowSection(group: (typeof sections)[number]): {
|
||||
families: ModelFamilyGroup[];
|
||||
families: ModelFamilyGroup<ModelQuantGroup>[];
|
||||
hidden: number;
|
||||
items: ModelQuantGroup[];
|
||||
unit: string;
|
||||
@@ -126,7 +126,7 @@
|
||||
return { families: [], hidden: group.items.length - items.length, items, unit: 'models' };
|
||||
}
|
||||
|
||||
const families: ModelFamilyGroup[] = [];
|
||||
const families: ModelFamilyGroup<ModelQuantGroup>[] = [];
|
||||
|
||||
let shown = 0;
|
||||
|
||||
@@ -413,7 +413,7 @@
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet familyRow(family: ModelFamilyGroup)}
|
||||
{#snippet familyRow(family: ModelFamilyGroup<ModelQuantGroup>)}
|
||||
{@const isExpanded = !collapsedFamilies.has(family.key)}
|
||||
{@const countLabel = `${family.entries.length} model${family.entries.length === 1 ? '' : 's'}`}
|
||||
{@const options = family.entries.flatMap((entry) => entry.quants)}
|
||||
|
||||
@@ -228,45 +228,6 @@ function groupKind(quants: ModelOption[]): ModelGroupKind {
|
||||
return isQuant ? 'quants' : 'variants';
|
||||
}
|
||||
|
||||
/**
|
||||
* Family a repo belongs to, from its name. The version is dropped, so `Qwen3.5`,
|
||||
* `Qwen3.8-27B` and `Qwen3.8-Flash-Next` all read as `Qwen`. A name that does not
|
||||
* start with letters keeps its first segment.
|
||||
*/
|
||||
export function modelFamilyKey(repo: string): string {
|
||||
const name = repo.split('/').pop() ?? repo;
|
||||
const letters = name.match(/^[A-Za-z]+/);
|
||||
|
||||
return letters ? letters[0] : (name.split(/[-_.]/)[0] ?? name);
|
||||
}
|
||||
|
||||
/** One family of the table, the repos it covers. */
|
||||
export interface ModelFamilyGroup {
|
||||
entries: ModelQuantGroup[];
|
||||
key: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/** Fold repos into families, so `Qwen3.8` collects its sizes and variants. */
|
||||
export function groupModelFamilies(entries: ModelQuantGroup[]): ModelFamilyGroup[] {
|
||||
const families = new SvelteMap<string, ModelFamilyGroup>();
|
||||
|
||||
for (const entry of entries) {
|
||||
const key = modelFamilyKey(entry.base.model);
|
||||
const family = families.get(key);
|
||||
|
||||
if (family) {
|
||||
family.entries.push(entry);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
families.set(key, { entries: [entry], key, label: key });
|
||||
}
|
||||
|
||||
return Array.from(families.values());
|
||||
}
|
||||
|
||||
/** Compact "last used" label: minutes, hours, then days. */
|
||||
export function formatLastUsed(timestamp?: number): string {
|
||||
if (!timestamp) return '—';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import ModelsSelectorDownloadItem from './ModelsSelectorDownloadItem.svelte';
|
||||
import { Heart, Power } from '@lucide/svelte';
|
||||
import { ModelsSelectorOption } from '$lib/components/app';
|
||||
import { ChevronDown, ChevronRight, Heart, Power } from '@lucide/svelte';
|
||||
import { ModelAvatar, ModelsSelectorOption } from '$lib/components/app';
|
||||
import { ModelsSection } from '$lib/components/app';
|
||||
import { DialogConfirmDownload } from '$lib/components/app/dialogs';
|
||||
import Logo from '$lib/components/app/misc/Logo.svelte';
|
||||
@@ -13,6 +13,8 @@
|
||||
import { MODEL_ROW_WINDOW } from '$lib/constants';
|
||||
import { ModelDownloadConfirmAction } from '$lib/enums';
|
||||
import { modelsStore } from '$lib/stores';
|
||||
import { groupModelFamilies, type ModelFamilyGroup } from '$lib/utils/model-families';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
|
||||
interface Props {
|
||||
groups: GroupedModelOptions;
|
||||
@@ -87,6 +89,21 @@
|
||||
let headerClass = $derived(`${sectionHeaderClass} sticky z-10 bg-popover`);
|
||||
const headerStyle = 'top: var(--dropdown-sticky-height, 0px)';
|
||||
|
||||
/** Families of each section, folded away by the user. */
|
||||
const collapsedFamilies = new SvelteSet<string>();
|
||||
|
||||
function familyKey(prefix: string, key: string): string {
|
||||
return `${prefix}-${key}`;
|
||||
}
|
||||
|
||||
function toggleFamily(key: string): void {
|
||||
if (collapsedFamilies.has(key)) {
|
||||
collapsedFamilies.delete(key);
|
||||
} else {
|
||||
collapsedFamilies.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
/** In-flight / paused downloads, tracked by the status feed. */
|
||||
let getDownloadEntries = $derived(modelsStore.status.getDownloadEntries());
|
||||
|
||||
@@ -102,6 +119,51 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet familyRow(family: ModelFamilyGroup<ModelItem>, prefix: string)}
|
||||
{@const key = familyKey(prefix, family.key)}
|
||||
{@const isExpanded = !collapsedFamilies.has(key)}
|
||||
|
||||
<button
|
||||
aria-expanded={isExpanded}
|
||||
class="flex w-full cursor-pointer items-center gap-3 rounded-sm p-2 text-left transition hover:bg-accent"
|
||||
onclick={() => toggleFamily(key)}
|
||||
type="button"
|
||||
>
|
||||
<ModelAvatar
|
||||
option={family.entries[0].option}
|
||||
showBaseModelAvatar
|
||||
showQuantBadge={false}
|
||||
size="size-7"
|
||||
/>
|
||||
|
||||
<span class="truncate text-sm font-medium">{family.label}</span>
|
||||
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{family.entries.length} model{family.entries.length === 1 ? '' : 's'}
|
||||
</span>
|
||||
|
||||
<span class="ml-auto flex shrink-0 items-center text-muted-foreground">
|
||||
{#if isExpanded}
|
||||
<ChevronDown class="h-3.5 w-3.5" />
|
||||
{:else}
|
||||
<ChevronRight class="h-3.5 w-3.5" />
|
||||
{/if}
|
||||
</span>
|
||||
</button>
|
||||
{/snippet}
|
||||
|
||||
{#snippet familyRows(items: ModelItem[], prefix: string)}
|
||||
{#each groupModelFamilies(items, (item) => item.option.model) as family (familyKey(prefix, family.key))}
|
||||
{@render familyRow(family, prefix)}
|
||||
|
||||
{#if !collapsedFamilies.has(familyKey(prefix, family.key))}
|
||||
{#each family.entries as item (`family-${prefix}-${item.option.id}`)}
|
||||
<div class="pl-4">{@render render(item, !showOrgName)}</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{/each}
|
||||
{/snippet}
|
||||
|
||||
{#snippet defaultOption(item: ModelItem, hideOrgName: boolean)}
|
||||
{@const { option } = item}
|
||||
{@const isSelected = currentModel === option.model || activeId === option.id}
|
||||
@@ -165,11 +227,10 @@
|
||||
{@render render(item, !showOrgName)}
|
||||
{/each}
|
||||
|
||||
{#each localGroups.available as group (group.orgName)}
|
||||
{#each group.items as item (item.option.id)}
|
||||
{@render render(item, !showOrgName)}
|
||||
{/each}
|
||||
{/each}
|
||||
{@render familyRows(
|
||||
localGroups.available.flatMap((group) => group.items),
|
||||
'local'
|
||||
)}
|
||||
|
||||
{#if hasMoreLocal}
|
||||
<div bind:this={sentinelEl} aria-hidden="true" class="h-px"></div>
|
||||
@@ -202,9 +263,7 @@
|
||||
sticky
|
||||
>
|
||||
{#if provider.items.length > 0}
|
||||
{#each provider.items as item (`${provider.backendId}-${item.option.id}`)}
|
||||
{@render render(item, !showOrgName)}
|
||||
{/each}
|
||||
{@render familyRows(provider.items, provider.backendId)}
|
||||
|
||||
{#if onProviderOpen && provider.matched > provider.items.length}
|
||||
<!-- same box as a model row, it opens the provider's full list -->
|
||||
|
||||
@@ -131,6 +131,7 @@ export { isValidModelName, normalizeModelName, orgOf } from './model-names';
|
||||
|
||||
// Backend-qualified model option ids
|
||||
export { backendIdFromModelId, qualifyModelId, rawModelId } from './model-option-id';
|
||||
export { groupModelFamilies, modelFamilyKey, type ModelFamilyGroup } from './model-families';
|
||||
|
||||
// Sidecar token utilities
|
||||
export { isAuxSidecar, isDraftSidecar, sidecarFromFileToken } from './sidecars';
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/** One family of a model list, and the entries it covers. */
|
||||
export interface ModelFamilyGroup<T> {
|
||||
entries: T[];
|
||||
key: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Family a repo belongs to, from its name. The version is dropped, so `Qwen3.5`,
|
||||
* `Qwen3.8-27B` and `Qwen3.8-Flash-Next` all read as `Qwen`. A name that does not
|
||||
* start with letters keeps its first segment.
|
||||
*/
|
||||
export function modelFamilyKey(repo: string): string {
|
||||
const name = repo.split('/').pop() ?? repo;
|
||||
const letters = name.match(/^[A-Za-z]+/);
|
||||
|
||||
return letters ? letters[0] : (name.split(/[-_.]/)[0] ?? name);
|
||||
}
|
||||
|
||||
/** Fold entries into families, so `Qwen` collects its sizes and variants. */
|
||||
export function groupModelFamilies<T>(
|
||||
entries: T[],
|
||||
modelOf: (entry: T) => string
|
||||
): ModelFamilyGroup<T>[] {
|
||||
const families = new Map<string, ModelFamilyGroup<T>>();
|
||||
|
||||
for (const entry of entries) {
|
||||
const key = modelFamilyKey(modelOf(entry));
|
||||
const family = families.get(key);
|
||||
|
||||
if (family) {
|
||||
family.entries.push(entry);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
families.set(key, { entries: [entry], key, label: key });
|
||||
}
|
||||
|
||||
return Array.from(families.values());
|
||||
}
|
||||
Reference in New Issue
Block a user