From bd03fbcb99551f39783d85fd1e68af7eb6d3a6c1 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Mon, 28 Sep 2026 15:06:17 +0200 Subject: [PATCH] ui : add the shared model row and list primitives The manager and later surfaces share one set of row parts: the collapsible section and grouped list containers, the avatar with its org and quant badges, the capability and context columns with their Hub fallbacks, the load control, the shared row action set, and the download row with its progress bar. The toggle and toggle-group controls and the tertiary button variant arrive with them. Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash --- tools/ui/package-lock.json | 8 +- tools/ui/package.json | 2 +- .../components/app/forms/SearchInput.svelte | 5 +- .../app/misc/CollapsibleRegion.svelte | 87 +++++++++ .../app/misc/CollapsibleSection.svelte | 116 ++++++++++++ .../components/app/misc/GroupedList.svelte | 167 ++++++++++++++++++ tools/ui/src/lib/components/app/misc/index.ts | 9 + .../components/app/models/ModelAvatar.svelte | 111 ++++++++++++ .../components/app/models/ModelBadge.svelte | 4 +- .../app/models/ModelCapabilities.svelte | 81 +++++++++ .../app/models/ModelCapabilityIcons.svelte | 2 +- .../components/app/models/ModelContext.svelte | 78 ++++++++ .../lib/components/app/models/ModelId.svelte | 33 ++-- .../app/models/ModelLoadControl.svelte | 108 +++++++++++ .../app/models/ModelRowActions.svelte | 71 ++++++++ .../app/models/ModelsSection.svelte | 100 +++++++++++ .../ModelsSelectorDownloadItem.svelte | 135 ++++++++++++++ .../discover/ModelsDiscoverAvatar.svelte | 2 +- .../ModelsDiscoverDownloadProgressBar.svelte | 32 ++++ .../ui/src/lib/components/app/models/index.ts | 13 ++ .../lib/components/ui/button/button.svelte | 4 +- .../lib/components/ui/toggle-group/index.ts | 10 ++ .../ui/toggle-group/toggle-group-item.svelte | 35 ++++ .../ui/toggle-group/toggle-group.svelte | 75 ++++++++ .../ui/src/lib/components/ui/toggle/index.ts | 13 ++ .../lib/components/ui/toggle/toggle.svelte | 54 ++++++ tools/ui/src/lib/constants/icons.constants.ts | 9 +- tools/ui/src/lib/constants/index.ts | 2 + .../models-discover-download.constants.ts | 54 ++++++ .../src/lib/services/huggingface.service.ts | 164 +++++++++++++---- tools/ui/src/lib/types/huggingface.d.ts | 7 + tools/ui/src/lib/types/models.d.ts | 17 ++ tools/ui/src/lib/utils/index.ts | 3 +- tools/ui/src/lib/utils/model-families.ts | 41 +++++ 34 files changed, 1587 insertions(+), 65 deletions(-) create mode 100644 tools/ui/src/lib/components/app/misc/CollapsibleRegion.svelte create mode 100644 tools/ui/src/lib/components/app/misc/CollapsibleSection.svelte create mode 100644 tools/ui/src/lib/components/app/misc/GroupedList.svelte create mode 100644 tools/ui/src/lib/components/app/models/ModelAvatar.svelte create mode 100644 tools/ui/src/lib/components/app/models/ModelCapabilities.svelte create mode 100644 tools/ui/src/lib/components/app/models/ModelContext.svelte create mode 100644 tools/ui/src/lib/components/app/models/ModelLoadControl.svelte create mode 100644 tools/ui/src/lib/components/app/models/ModelRowActions.svelte create mode 100644 tools/ui/src/lib/components/app/models/ModelsSection.svelte create mode 100644 tools/ui/src/lib/components/app/models/ModelsSelector/ModelsSelectorDownloadItem.svelte create mode 100644 tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDownloadProgressBar.svelte create mode 100644 tools/ui/src/lib/components/ui/toggle-group/index.ts create mode 100644 tools/ui/src/lib/components/ui/toggle-group/toggle-group-item.svelte create mode 100644 tools/ui/src/lib/components/ui/toggle-group/toggle-group.svelte create mode 100644 tools/ui/src/lib/components/ui/toggle/index.ts create mode 100644 tools/ui/src/lib/components/ui/toggle/toggle.svelte create mode 100644 tools/ui/src/lib/constants/models-discover-download.constants.ts create mode 100644 tools/ui/src/lib/utils/model-families.ts diff --git a/tools/ui/package-lock.json b/tools/ui/package-lock.json index 86d599e671..c38fce566d 100644 --- a/tools/ui/package-lock.json +++ b/tools/ui/package-lock.json @@ -12,7 +12,7 @@ "@eslint/compat": "1.4.1", "@eslint/js": "9.39.2", "@internationalized/date": "3.12.2", - "@lucide/svelte": "1.25.0", + "@lucide/svelte": "1.46.0", "@modelcontextprotocol/sdk": "1.30.0", "@playwright/test": "1.56.1", "@storybook/addon-a11y": "10.5.6", @@ -3225,9 +3225,9 @@ } }, "node_modules/@lucide/svelte": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-1.25.0.tgz", - "integrity": "sha512-v9m+dD68jxVnqkU3K59mG/RSRFlPGzmKCGSyMfnXcaGv9jODDQMyQkcp1CGvk3Y/cUj9v7f8rw1n//K0B53xGQ==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-1.46.0.tgz", + "integrity": "sha512-Ru8jDjoCVLJ7J7pPRUdY+odSBV2BI6rj68JTmT+ENq/ZwYOPGPAiKVvKz+XLyAoK4HAwlRTzZHpsMvrHfsetqg==", "dev": true, "license": "ISC", "peerDependencies": { diff --git a/tools/ui/package.json b/tools/ui/package.json index 4017604741..5e7c05bf39 100644 --- a/tools/ui/package.json +++ b/tools/ui/package.json @@ -32,7 +32,7 @@ "@eslint/compat": "1.4.1", "@eslint/js": "9.39.2", "@internationalized/date": "3.12.2", - "@lucide/svelte": "1.25.0", + "@lucide/svelte": "1.46.0", "@modelcontextprotocol/sdk": "1.30.0", "@playwright/test": "1.56.1", "@storybook/addon-a11y": "10.5.6", diff --git a/tools/ui/src/lib/components/app/forms/SearchInput.svelte b/tools/ui/src/lib/components/app/forms/SearchInput.svelte index 99d261208f..674d2f6be2 100644 --- a/tools/ui/src/lib/components/app/forms/SearchInput.svelte +++ b/tools/ui/src/lib/components/app/forms/SearchInput.svelte @@ -11,6 +11,8 @@ onClose?: () => void; onKeyDown?: (event: KeyboardEvent) => void; class?: string; + /** Input height, matching the small size of the other controls. */ + size?: 'sm' | 'default'; id?: string; ref?: HTMLInputElement | null; isCancelAlwaysVisible?: boolean; @@ -26,6 +28,7 @@ onKeyDown, placeholder = 'Search...', ref = $bindable(null), + size = 'default', value = $bindable('') }: Props = $props(); @@ -58,7 +61,7 @@ bind:ref bind:value {autofocus} - class="pl-9 {showClearButton ? 'pr-9' : ''}" + class="pl-9 {showClearButton ? 'pr-9' : ''} {size === 'sm' ? 'h-8' : ''}" {id} oninput={handleInput} onkeydown={onKeyDown} diff --git a/tools/ui/src/lib/components/app/misc/CollapsibleRegion.svelte b/tools/ui/src/lib/components/app/misc/CollapsibleRegion.svelte new file mode 100644 index 0000000000..4a7a2ffbcc --- /dev/null +++ b/tools/ui/src/lib/components/app/misc/CollapsibleRegion.svelte @@ -0,0 +1,87 @@ + + +
+ {#if contentMounted} +
+ {@render children()} +
+ {/if} +
+ + diff --git a/tools/ui/src/lib/components/app/misc/CollapsibleSection.svelte b/tools/ui/src/lib/components/app/misc/CollapsibleSection.svelte new file mode 100644 index 0000000000..68e4e9b7cb --- /dev/null +++ b/tools/ui/src/lib/components/app/misc/CollapsibleSection.svelte @@ -0,0 +1,116 @@ + + +{#snippet chevron()} + + {#if open} + + {:else} + + {/if} + +{/snippet} + +{#snippet triggerButton()} + {#if inMenu} + + + {#snippet child({ props })} + + + {/snippet} + + {:else} + + {/if} +{/snippet} + +{#snippet region()} + + {#snippet body()} + {@render children()} + {/snippet} + + {@render body()} +{/snippet} + +{#if triggerPosition === 'top'} + {@render triggerButton()} + + {@render region()} +{:else} + {@render region()} + + {@render triggerButton()} +{/if} diff --git a/tools/ui/src/lib/components/app/misc/GroupedList.svelte b/tools/ui/src/lib/components/app/misc/GroupedList.svelte new file mode 100644 index 0000000000..5c8927a107 --- /dev/null +++ b/tools/ui/src/lib/components/app/misc/GroupedList.svelte @@ -0,0 +1,167 @@ + + + + +{#if groups} + {#each windowed as entry (entry.key)} + {@const expanded = !collapsed.has(entry.key)} + + {#if group} +
+ {@render group({ + depth: 0, + expanded, + group: entry.group, + key: entry.key, + toggle: () => toggleGroup(entry.key) + })} +
+ {/if} + + + {#each entry.rows as row (keyOf(row))} + {@render item({ depth: 1, entry: row })} + {/each} + + {#if entry.hidden > 0 && more} + {@render more({ + count: Math.min(groupWindow, entry.hidden), + onMore: () => growGroup(entry.key), + unit: 'entries' + })} + {/if} + + {/each} + + {#if windowed.length < (groups?.length ?? 0) && more} + {@render more({ + count: Math.min(sectionWindow, (groups?.length ?? 0) - windowed.length), + onMore: () => (sectionSteps += 1), + unit: 'families' + })} + {/if} +{:else} + {#each flatRows as row (keyOf(row))} + {@render item({ depth: 0, entry: row })} + {/each} + + {#if flatRows.length < items.length && more} + {@render more({ + count: Math.min(sectionWindow, items.length - flatRows.length), + onMore: () => (sectionSteps += 1), + unit: 'entries' + })} + {/if} +{/if} diff --git a/tools/ui/src/lib/components/app/misc/index.ts b/tools/ui/src/lib/components/app/misc/index.ts index a10410ef92..e695f242e4 100644 --- a/tools/ui/src/lib/components/app/misc/index.ts +++ b/tools/ui/src/lib/components/app/misc/index.ts @@ -59,3 +59,12 @@ export { default as CodeBlockActions } from './CodeBlockActions.svelte'; * `class` and `style` props and inherits color via `currentColor`. */ export { default as Logo } from './Logo.svelte'; +/** + * **CollapsibleSection** - Section with a trigger that expands in place + * + * The trigger can sit above or below the content, and the chevron can stay out + * of sight while the section is open. Collapsed content unmounts after the + * transition, so hidden rows never reach menu keyboard navigation. + */ +export { default as CollapsibleSection } from './CollapsibleSection.svelte'; +export { default as GroupedList, type GroupedListGroup } from './GroupedList.svelte'; diff --git a/tools/ui/src/lib/components/app/models/ModelAvatar.svelte b/tools/ui/src/lib/components/app/models/ModelAvatar.svelte new file mode 100644 index 0000000000..6838f43070 --- /dev/null +++ b/tools/ui/src/lib/components/app/models/ModelAvatar.svelte @@ -0,0 +1,111 @@ + + +{#if orgName} + + + +{:else} + {@render fallback?.()} +{/if} diff --git a/tools/ui/src/lib/components/app/models/ModelBadge.svelte b/tools/ui/src/lib/components/app/models/ModelBadge.svelte index d5b723ff90..53cdcc26a8 100644 --- a/tools/ui/src/lib/components/app/models/ModelBadge.svelte +++ b/tools/ui/src/lib/components/app/models/ModelBadge.svelte @@ -1,8 +1,8 @@ + + + + diff --git a/tools/ui/src/lib/components/app/models/ModelCapabilityIcons.svelte b/tools/ui/src/lib/components/app/models/ModelCapabilityIcons.svelte index c9505a95eb..e98631bd1f 100644 --- a/tools/ui/src/lib/components/app/models/ModelCapabilityIcons.svelte +++ b/tools/ui/src/lib/components/app/models/ModelCapabilityIcons.svelte @@ -52,7 +52,7 @@ {/if} {#if hasModalityIcons && !hideModalities} - + {#if modalities?.vision} diff --git a/tools/ui/src/lib/components/app/models/ModelContext.svelte b/tools/ui/src/lib/components/app/models/ModelContext.svelte new file mode 100644 index 0000000000..74b61447c9 --- /dev/null +++ b/tools/ui/src/lib/components/app/models/ModelContext.svelte @@ -0,0 +1,78 @@ + + + + {values.length ? `${values.map((value) => formatParameters(value)).join(' / ')} tokens` : '—'} + diff --git a/tools/ui/src/lib/components/app/models/ModelId.svelte b/tools/ui/src/lib/components/app/models/ModelId.svelte index d346cffa18..49feb789d4 100644 --- a/tools/ui/src/lib/components/app/models/ModelId.svelte +++ b/tools/ui/src/lib/components/app/models/ModelId.svelte @@ -4,6 +4,7 @@ import { TruncatedText } from '$lib/components/app'; import * as Tooltip from '$lib/components/ui/tooltip'; import { type ModelSidecar } from '$lib/constants'; + import { MODEL_BADGE_CLASS, MODEL_VARIANT_BADGE_CLASS } from '$lib/constants'; import { HuggingFaceService } from '$lib/services'; import { ModelsService } from '$lib/services/models.service'; import { settingsStore } from '$lib/stores'; @@ -67,12 +68,10 @@ ...rest }: Props = $props(); - const badgeClass = - 'inline-flex w-fit shrink-0 items-center justify-center whitespace-nowrap rounded-md border border-border/50 px-1 py-0 text-[10px] font-mono bg-foreground/15 dark:bg-foreground/10 text-foreground [a&]:hover:bg-foreground/25'; + const badgeClass = MODEL_BADGE_CLASS; const tagBadgeClass = 'inline-flex w-fit shrink-0 items-center justify-center whitespace-nowrap rounded-md border border-border/50 px-1 py-0 text-[10px] font-mono text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground'; - const variantBadgeClass = - 'inline-flex w-fit shrink-0 items-center justify-center whitespace-nowrap rounded-md bg-primary px-1.5 py-0 text-[10px] font-mono font-semibold uppercase tracking-wide text-primary-foreground'; + const variantBadgeClass = MODEL_VARIANT_BADGE_CLASS; /** Alias badges beyond this many collapse into a single `+x more` badge. */ const MAX_ALIAS_BADGES = 2; @@ -114,7 +113,11 @@ {/if} {#if hasBadges} - + + {#if parsed.sidecar} {parsed.sidecar} @@ -127,18 +130,18 @@ {/if} - {#each uniqueDraftSidecars as sidecar (sidecar)} - - {sidecar} - - {/each} - {#if parsed.quantization && !resolvedHideQuantization} {parsed.quantization} {/if} + {#each uniqueDraftSidecars as sidecar (sidecar)} + + {sidecar} + + {/each} + {#if primaryAlias} {#if primaryAlias !== parsed.modelName} @@ -168,10 +171,12 @@ {/if} {/snippet} + diff --git a/tools/ui/src/lib/components/app/models/ModelLoadControl.svelte b/tools/ui/src/lib/components/app/models/ModelLoadControl.svelte new file mode 100644 index 0000000000..f95a2f61a4 --- /dev/null +++ b/tools/ui/src/lib/components/app/models/ModelLoadControl.svelte @@ -0,0 +1,108 @@ + + +
+ {#if isLoading} + + {:else} + + {#if showAction && revealOnHover} + {#if isFailed} + + {:else} + + {/if} + {:else} + {#if isFailed} + + {:else} + + {/if} + {/if} + + +
+ {#if isFailed} + modelsStore.status.load(option.model)} + stopPropagationOnClick + tooltip="Retry loading model" + tooltipAsTitle + /> + {:else if isLoaded || isSleeping} + modelsStore.status.unload(option.model)} + stopPropagationOnClick + tooltip="Unload model" + tooltipAsTitle + /> + {:else} + modelsStore.status.load(option.model)} + stopPropagationOnClick + tooltip="Load model" + tooltipAsTitle + /> + {/if} +
+ {/if} +
diff --git a/tools/ui/src/lib/components/app/models/ModelRowActions.svelte b/tools/ui/src/lib/components/app/models/ModelRowActions.svelte new file mode 100644 index 0000000000..6218f824a5 --- /dev/null +++ b/tools/ui/src/lib/components/app/models/ModelRowActions.svelte @@ -0,0 +1,71 @@ + + +
event.stopPropagation()} + onkeydown={(event) => event.stopPropagation()} + role="presentation" +> + uiStore.openModelsManager(option.id)} + tooltip="Manage model" + tooltipAsTitle + /> + + {#if isFav} + + + modelsStore.toggleFavorite(option.model)} + tooltip="Remove from favorites" + tooltipAsTitle + /> + + + + + {:else} + modelsStore.toggleFavorite(option.model)} + tooltip="Add to favorites" + tooltipAsTitle + /> + {/if} +
diff --git a/tools/ui/src/lib/components/app/models/ModelsSection.svelte b/tools/ui/src/lib/components/app/models/ModelsSection.svelte new file mode 100644 index 0000000000..2427adf1d9 --- /dev/null +++ b/tools/ui/src/lib/components/app/models/ModelsSection.svelte @@ -0,0 +1,100 @@ + + + modelsStore.setGroupOpen(persistKey, open) : null} + {revealChevronOnHover} + {triggerClass} + {triggerStyle} +> + {#snippet trigger()} + {#if onBack} + + {/if} + + {#if icon} + {@render icon()} + {/if} + + {label} + + {#if loading} + + {:else if error} + + {/if} + + {#if count !== undefined} + {count} + {/if} + {/snippet} + + {@render children()} + diff --git a/tools/ui/src/lib/components/app/models/ModelsSelector/ModelsSelectorDownloadItem.svelte b/tools/ui/src/lib/components/app/models/ModelsSelector/ModelsSelectorDownloadItem.svelte new file mode 100644 index 0000000000..6ee03f2d79 --- /dev/null +++ b/tools/ui/src/lib/components/app/models/ModelsSelector/ModelsSelectorDownloadItem.svelte @@ -0,0 +1,135 @@ + + + +
+ + + + + + + {#if entry.progress && entry.progress.totalBytes > 0} + + {/if} +
diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverAvatar.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverAvatar.svelte index 8523f0685b..5543720982 100644 --- a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverAvatar.svelte +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverAvatar.svelte @@ -59,7 +59,7 @@ }); - + {#if orgAvatarFailed}