From 21c61123a9173e06a7ef8cdc6a0a584befe10090 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Tue, 25 Aug 2026 09:30:23 +0200 Subject: [PATCH] feat: WIP --- .../ChatFormActionAddToolsSubmenu.svelte | 9 +- .../MarkdownContent/MarkdownContent.svelte | 34 ++- .../app/dialogs/DialogModelsDiscover.svelte | 72 +++--- .../lib/components/app/models/ModelId.svelte | 197 +++++++++------ .../discover/ModelsDiscoverAvatar.svelte | 46 ++-- .../discover/ModelsDiscoverDetails.svelte | 11 +- ...odelsDiscoverDetailsDownloadOptions.svelte | 24 +- .../ModelsDiscoverDetailsHeader.svelte | 50 ++-- .../ModelsDiscoverDetailsReadme.svelte | 6 +- .../models/discover/ModelsDiscoverInfo.svelte | 83 ++++-- .../models/discover/ModelsDiscoverItem.svelte | 6 +- .../models/discover/TerminalCommands.svelte | 13 +- .../selector/ModelsSelectorDropdown.svelte | 75 +++--- .../models/selector/ModelsSelectorList.svelte | 38 ++- .../selector/ModelsSelectorOption.svelte | 84 ++++--- .../selector/ModelsSelectorSheet.svelte | 35 ++- .../ui/src/lib/components/app/models/utils.ts | 23 +- .../src/lib/constants/model-id.constants.ts | 14 +- .../ui/src/lib/constants/routes.constants.ts | 4 +- .../lib/hooks/use-models-selector.svelte.ts | 39 ++- .../src/lib/services/huggingface.service.ts | 237 ++++++++++++------ .../src/lib/stores/models-hub/index.svelte.ts | 76 +++++- .../ui/src/lib/stores/models/status.svelte.ts | 66 ++--- tools/ui/src/lib/types/huggingface.d.ts | 30 +++ tools/ui/src/lib/types/model-manager.d.ts | 2 +- tools/ui/src/lib/types/models.d.ts | 2 + .../lib/utils/chat-template-tool-detector.ts | 28 +++ tools/ui/src/lib/utils/index.ts | 2 + tools/ui/src/lib/utils/model-manager.ts | 17 +- .../ui/src/routes/model-manager/+page.svelte | 7 +- .../models-hub/DialogModelDownload.svelte | 199 +++------------ .../temp/models-hub/DownloadToast.svelte | 90 +++++++ .../stories/ModelsSelector.stories.svelte | 25 +- 33 files changed, 974 insertions(+), 670 deletions(-) create mode 100644 tools/ui/src/lib/utils/chat-template-tool-detector.ts create mode 100644 tools/ui/src/routes/temp/models-hub/DownloadToast.svelte diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte index 81b56826b5..60bbb89e23 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte @@ -1,5 +1,12 @@
- +
- {#if details.cardData?.description} -

{details.cardData.description}

+ {#if description} +

{description}

{/if} @@ -82,7 +80,9 @@ {/if} {#if gguf?.architecture} - + {gguf.architecture.replace(/_/g, ' ')} {/if} @@ -97,7 +97,9 @@ {/if} {#if details.gated === true} - + gated {/if} @@ -107,19 +109,25 @@ {#if hasVision || hasTools || hasReasoning}
{#if hasVision} - + Vision {/if} {#if hasTools} - + Tool use {/if} {#if hasReasoning} - + Reasoning diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetailsReadme.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetailsReadme.svelte index 705843e5d3..961014d5ef 100644 --- a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetailsReadme.svelte +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetailsReadme.svelte @@ -9,10 +9,8 @@ {#if readme} -
+

README

-
- -
+
{/if} diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverInfo.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverInfo.svelte index 970ababfdf..afe21aec74 100644 --- a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverInfo.svelte +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverInfo.svelte @@ -1,11 +1,12 @@ @@ -64,24 +109,12 @@ hideOrgName {modalities} {supportsThinking} + {supportsToolUse} + {contextLength} + {sizeRange} {draftVariants} + iconsOnNewLine wrap class="min-w-0" /> - - - - - {HuggingFaceService.formatDownloads(model.downloads)} - - - - {HuggingFaceService.formatLikes(model.likes)} - - {#if contextLength} - - {formatParameters(contextLength)} ctx - - {/if} - diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverItem.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverItem.svelte index 4e4c0e2ea2..0ba61a7cee 100644 --- a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverItem.svelte +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverItem.svelte @@ -1,8 +1,8 @@ @@ -65,7 +64,9 @@
{#each box.commands as cmd, i (cmd)} -
+
{cmd} @@ -315,38 +313,39 @@

No models found.

{/if} - {#snippet modelOption(item: ModelItem, hideOrgName: boolean)} + {#snippet modelOption(item: ModelItem, _hideOrgName: boolean, compact = false)} {@const { option } = item} {@const isSelected = currentModel === option.model || ms.activeId === option.id} {@const isHighlighted = option.id === highlightedId} {@const isFav = ms.isFavorite(option.model)} (highlightedId = option.id)} onKeyDown={(event) => { if (event.key === KeyboardKey.ENTER || event.key === KeyboardKey.SPACE) { event.preventDefault(); void handleModelKeyAction(option.id, event.altKey); } }} - onMouseEnter={() => (highlightedId = option.id)} - onSelect={ms.handleSelect} - {option} /> {/snippet}
@@ -385,18 +384,18 @@ : 'text-foreground', isOpen && 'text-foreground' ]} - disabled={disabled || ms.updating} - onclick={() => ms.handleOpenChange(true)} style="max-width: min(calc(100cqw - 6.5rem), 32rem)" + onclick={() => ms.handleOpenChange(true)} + disabled={disabled || ms.updating} > {#if selectedOption} {/if} @@ -423,9 +422,9 @@ {#if ms.showModelDialog} ms.setShowModelDialog(v)} open={ms.showModelDialog} + onOpenChange={(v) => ms.setShowModelDialog(v)} + modelId={ms.infoModelId} /> {/if} diff --git a/tools/ui/src/lib/components/app/models/selector/ModelsSelectorList.svelte b/tools/ui/src/lib/components/app/models/selector/ModelsSelectorList.svelte index 191d8bccd0..c7badca513 100644 --- a/tools/ui/src/lib/components/app/models/selector/ModelsSelectorList.svelte +++ b/tools/ui/src/lib/components/app/models/selector/ModelsSelectorList.svelte @@ -8,10 +8,9 @@ currentModel: string | null; activeId: string | null; sectionHeaderClass?: string; - orgHeaderClass?: string; onSelect: (modelId: string) => void; onInfoClick: (modelName: string) => void; - renderOption?: import('svelte').Snippet<[ModelItem, boolean]>; + renderOption?: import('svelte').Snippet<[ModelItem, boolean, boolean?]>; } let { @@ -20,34 +19,33 @@ groups, onInfoClick, onSelect, - orgHeaderClass = 'px-2 py-2 text-[11px] font-semibold text-muted-foreground/50 select-none [&:not(:first-child)]:mt-1', renderOption, - sectionHeaderClass = 'my-1 px-2 py-2 text-[13px] font-semibold text-muted-foreground/70 select-none' + sectionHeaderClass = 'my-1 px-2 py-2 text-xs font-semibold text-muted-foreground/70 select-none' }: Props = $props(); let render = $derived(renderOption ?? defaultOption); -{#snippet defaultOption(item: ModelItem, hideOrgName: boolean)} +{#snippet defaultOption(item: ModelItem, hideOrgName: boolean, compact = false)} {@const { option } = item} {@const isSelected = currentModel === option.model || activeId === option.id} {@const isFav = modelsStore.favoriteModelIds.has(option.model)} {}} - onMouseEnter={() => {}} - {onSelect} {option} + {isSelected} + isHighlighted={false} + {isFav} + {hideOrgName} + {compact} + {onSelect} + {onInfoClick} + onMouseEnter={() => {}} + onKeyDown={() => {}} /> {/snippet} {#if groups.loaded.length > 0}

Loaded models

- {#each groups.loaded as item (`loaded-${item.option.id}`)} {@render render(item, false)} {/each} @@ -55,7 +53,6 @@ {#if groups.favorites.length > 0}

Favorite models

- {#each groups.favorites as item (`fav-${item.option.id}`)} {@render render(item, true)} {/each} @@ -63,14 +60,7 @@ {#if groups.available.length > 0}

Available models

- - {#each groups.available as group (group.orgName)} - {#if group.orgName} -

{group.orgName}

- {/if} - - {#each group.items as item (item.option.id)} - {@render render(item, true)} - {/each} + {#each groups.available as item (`avail-${item.option.id}`)} + {@render render(item, false)} {/each} {/if} diff --git a/tools/ui/src/lib/components/app/models/selector/ModelsSelectorOption.svelte b/tools/ui/src/lib/components/app/models/selector/ModelsSelectorOption.svelte index 5bc9b87f61..fd4c7a14db 100644 --- a/tools/ui/src/lib/components/app/models/selector/ModelsSelectorOption.svelte +++ b/tools/ui/src/lib/components/app/models/selector/ModelsSelectorOption.svelte @@ -10,7 +10,7 @@ PowerOff, RotateCw } from '@lucide/svelte'; - import { ActionIcon, ModelId } from '$lib/components/app'; + import { ActionIcon, ModelId, ModelsDiscoverAvatar } from '$lib/components/app'; import { ICON_CLASS_DEFAULT } from '$lib/constants'; import { ServerModelStatus } from '$lib/enums'; import { modelsStore } from '$lib/stores'; @@ -23,6 +23,8 @@ isHighlighted: boolean; isFav: boolean; hideOrgName?: boolean; + /** Show only the quant/param badges, hiding the avatar and org/model name. */ + compact?: boolean; onSelect: (modelId: string) => void; onMouseEnter: () => void; onKeyDown: (e: KeyboardEvent) => void; @@ -30,6 +32,7 @@ } let { + compact = false, hideOrgName = false, isFav, isHighlighted, @@ -60,10 +63,11 @@ let loadTitle = $derived(modelLoadProgressText(loadProgress)); let modalities = $derived(option.modalities); let supportsThinking = $derived(modelsStore.props.checkModelSupportsThinking(option.model)); + let quantOrg = $derived(option.parsedId?.orgName || option.model.split('/')[0] || option.model); + let baseOrg = $derived(option.baseModel?.org || quantOrg);
onSelect(option.id)} - onkeydown={onKeyDown} - onmouseenter={onMouseEnter} role="option" - tabindex="0" + aria-selected={isSelected || isHighlighted} title={loadTitle} + tabindex="0" + onclick={() => onSelect(option.id)} + onmouseenter={onMouseEnter} + onkeydown={onKeyDown} > + {#if !compact} + + {/if} +
@@ -101,30 +119,30 @@ > {#if isFav} modelsStore.toggleFavorite(option.model)} + icon={HeartOff} tooltip="Remove from favorites" + class="h-3 w-3 hover:text-foreground" + onclick={() => modelsStore.toggleFavorite(option.model)} /> {:else} modelsStore.toggleFavorite(option.model)} + icon={Heart} tooltip="Add to favorites" + class="h-3 w-3 hover:text-foreground" + onclick={() => modelsStore.toggleFavorite(option.model)} /> {/if} {#if isLoaded && onInfoClick} onInfoClick(option.model)} + icon={Info} tooltip="Model information" + class="h-3 w-3 hover:text-foreground" + onclick={() => onInfoClick(option.model)} /> {/if}
@@ -141,12 +159,12 @@
@@ -158,14 +176,14 @@
@@ -177,12 +195,12 @@
@@ -194,12 +212,12 @@
diff --git a/tools/ui/src/lib/components/app/models/selector/ModelsSelectorSheet.svelte b/tools/ui/src/lib/components/app/models/selector/ModelsSelectorSheet.svelte index 50c0f204d4..b8094608a2 100644 --- a/tools/ui/src/lib/components/app/models/selector/ModelsSelectorSheet.svelte +++ b/tools/ui/src/lib/components/app/models/selector/ModelsSelectorSheet.svelte @@ -79,6 +79,7 @@ {#if ms.isRouter} - + Select Model @@ -133,26 +133,24 @@
ms.setSearchTerm(v)} placeholder="Search models..." value={ms.searchTerm} + onInput={(v) => ms.setSearchTerm(v)} />
{#if !ms.isCurrentModelInCache && currentModel} -
{/if} @@ -161,13 +159,12 @@ {/if}
@@ -185,13 +182,13 @@ ? 'text-foreground' : 'text-foreground' ]} - disabled={disabled || ms.updating} - onclick={() => ms.handleOpenChange(true)} style="max-width: min(calc(100cqw - 6.5rem), 32rem)" + onclick={() => ms.handleOpenChange(true)} + disabled={disabled || ms.updating} > - + {#if ms.updating} @@ -203,8 +200,8 @@ {#if ms.showModelDialog} ms.setShowModelDialog(v)} open={ms.showModelDialog} + onOpenChange={(v) => ms.setShowModelDialog(v)} + modelId={ms.infoModelId} /> {/if} diff --git a/tools/ui/src/lib/components/app/models/utils.ts b/tools/ui/src/lib/components/app/models/utils.ts index 35bfb58a25..17764b3910 100644 --- a/tools/ui/src/lib/components/app/models/utils.ts +++ b/tools/ui/src/lib/components/app/models/utils.ts @@ -1,21 +1,15 @@ import { ModelModality } from '$lib/enums'; import type { ModelOption } from '$lib/types/models'; -import { SvelteMap } from 'svelte/reactivity'; export interface ModelItem { option: ModelOption; flatIndex: number; } -export interface OrgGroup { - orgName: string | null; - items: ModelItem[]; -} - export interface GroupedModelOptions { loaded: ModelItem[]; favorites: ModelItem[]; - available: OrgGroup[]; + available: ModelItem[]; } function matchesModality(option: ModelOption, term: string): boolean { @@ -77,24 +71,15 @@ export function groupModelOptions( } } - // Available models grouped by org (excluding loaded and favorites) - const available: OrgGroup[] = []; - const orgGroups = new SvelteMap(); + // Available models (excluding loaded and favorites) + const available: ModelItem[] = []; for (let i = 0; i < filteredOptions.length; i++) { const option = filteredOptions[i]; if (loadedModelIds.has(option.model) || favoriteIds.has(option.model)) continue; - const key = option.parsedId?.orgName ?? ''; - - if (!orgGroups.has(key)) orgGroups.set(key, []); - - orgGroups.get(key)!.push({ flatIndex: i, option }); - } - - for (const [orgName, items] of orgGroups) { - available.push({ items, orgName: orgName || null }); + available.push({ flatIndex: i, option }); } return { available, favorites, loaded }; diff --git a/tools/ui/src/lib/constants/model-id.constants.ts b/tools/ui/src/lib/constants/model-id.constants.ts index 80431c059d..ea0ed7f8fb 100644 --- a/tools/ui/src/lib/constants/model-id.constants.ts +++ b/tools/ui/src/lib/constants/model-id.constants.ts @@ -11,13 +11,6 @@ export const MODEL_ID = { /** Matches prefix for custom quantization types, e.g. `UD-Q8_K_XL`. */ CUSTOM_QUANTIZATION_PREFIX_RE: /^UD$/i, - /** - * Trailing `-` suffix marking a GGUF with an embedded draft in the - * same weight file (MTP) or a sidecar download entry, e.g. - * `Hy3-IQ1_M-mtp.gguf`, `Q4_K_M-dspark`. The captured prefix is the - * candidate model id; the caller decides whether it looks quantized. - */ - DRAFT_VARIANT_SUFFIX_RE: /^(.*)-(mtp|dflash|dspark|eagle3)$/i, /** * Sidecar prefix that wraps a model id with a draft/aux variant, e.g. * `mtp-.gguf`, `dflash-.gguf`, `dspark-.gguf`, @@ -25,6 +18,13 @@ export const MODEL_ID = { * token for typed lookup. */ DRAFT_VARIANT_PREFIX_RE: /^(mtp|dflash|dspark|eagle3|mmproj)-(.*)$/i, + /** + * Trailing `-` suffix marking a GGUF with an embedded draft in the + * same weight file (MTP) or a sidecar download entry, e.g. + * `Hy3-IQ1_M-mtp.gguf`, `Q4_K_M-dspark`. The captured prefix is the + * candidate model id; the caller decides whether it looks quantized. + */ + DRAFT_VARIANT_SUFFIX_RE: /^(.*)-(mtp|dflash|dspark|eagle3)$/i, /** Container format segments to exclude from tags (every model uses these). */ IGNORED_SEGMENTS: new Set(['GGUF', 'GGML']), diff --git a/tools/ui/src/lib/constants/routes.constants.ts b/tools/ui/src/lib/constants/routes.constants.ts index b2fc489738..72dae2b20c 100644 --- a/tools/ui/src/lib/constants/routes.constants.ts +++ b/tools/ui/src/lib/constants/routes.constants.ts @@ -15,10 +15,10 @@ export const ROUTES = { MANAGE_MODEL: '#/models-hub/[modelId]', /** Model hub - browse and download HuggingFace GGUF models. */ MANAGE_MODELS: '#/models-hub', - /** Model manager - installed models from /v1/models. */ - MODEL_MANAGER: '#/model-manager', /** MCP servers. */ MCP_SERVERS: '#/mcp-servers', + /** Model manager - installed models from /v1/models. */ + MODEL_MANAGER: '#/model-manager', /** Search — mobile-only full-page conversation search. */ SEARCH: '#/search', /** Root — start of the app. */ diff --git a/tools/ui/src/lib/hooks/use-models-selector.svelte.ts b/tools/ui/src/lib/hooks/use-models-selector.svelte.ts index 7d2770a261..9c261a5cb1 100644 --- a/tools/ui/src/lib/hooks/use-models-selector.svelte.ts +++ b/tools/ui/src/lib/hooks/use-models-selector.svelte.ts @@ -1,8 +1,10 @@ import { filterModelOptions, groupModelOptions } from '$lib/components/app/models/utils'; import { CHAT_INPUT_FOCUS_SELECTOR } from '$lib/constants'; +import { HuggingFaceService } from '$lib/services'; import { modelsStore, serverStore } from '$lib/stores'; import type { ModelOption } from '$lib/types/models'; import { onMount } from 'svelte'; +import { SvelteMap, SvelteSet } from 'svelte/reactivity'; export interface UseModelsSelectorOptions { currentModel: () => string | null; @@ -76,14 +78,47 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele let searchTerm = $state(''); let showModelDialog = $state(false); let infoModelId = $state(null); + let menuOpen = $state(false); + // Base model org per base repo (e.g. `Qwen` for `ggml-org/Qwen3.8-27B-GGUF`), + // resolved lazily from the HF card while the menu is open. + const baseOrgs = new SvelteMap(); const filteredOptions = $derived(filterModelOptions(options, searchTerm)); + // Augment each option with its base model; the org falls back to the quant + // org until the HF lookup resolves. + const optionsWithBaseModel = $derived( + filteredOptions.map((option) => { + const repo = option.model.split(':')[0]; + const org = baseOrgs.get(repo) ?? option.parsedId?.orgName ?? repo.split('/')[0] ?? ''; + const name = option.parsedId?.modelName ?? option.name ?? option.model; + + return { ...option, baseModel: { name, org } }; + }) + ); const groupedFilteredOptions = $derived( - groupModelOptions(filteredOptions, modelsStore.favoriteModelIds, (m) => + groupModelOptions(optionsWithBaseModel, modelsStore.favoriteModelIds, (m) => modelsStore.isModelLoaded(m) ) ); + // Fetch base model orgs for the visible repos while the menu is open. The + // service caches per repo, so repeated opens never re-hit the HF API. + $effect(() => { + if (!menuOpen) return; + + const repos = new SvelteSet(); + + for (const option of options) repos.add(option.model.split(':')[0]); + + for (const repo of repos) { + if (baseOrgs.has(repo)) continue; + + void HuggingFaceService.getBaseModel(repo).then((info) => { + baseOrgs.set(repo, info?.org ?? repo.split('/')[0] ?? ''); + }); + } + }); + function handleInfoClick(modelName: string) { infoModelId = modelName; showModelDialog = true; @@ -98,6 +133,8 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele function handleOpenChange(open: boolean) { if (loading || updating) return; + menuOpen = open; + if (isRouter) { searchTerm = ''; diff --git a/tools/ui/src/lib/services/huggingface.service.ts b/tools/ui/src/lib/services/huggingface.service.ts index f46969a7e7..bd149c857d 100644 --- a/tools/ui/src/lib/services/huggingface.service.ts +++ b/tools/ui/src/lib/services/huggingface.service.ts @@ -1,10 +1,12 @@ -import { type DraftVariant,MODEL_ID } from '$lib/constants'; +import { type DraftVariant, MODEL_ID } from '$lib/constants'; import type { + HfCatalogEntry, HfModelDetailInfo, HfModelInfo, HfModelSearchParams, HfModelSibling, - HfModelSort} from '$lib/types/huggingface'; + HfModelSort +} from '$lib/types/huggingface'; /** Variant flag in a GGUF filename (e.g. draft-mtp, diffusion-flash, multimodal projector). */ export type GgufVariant = DraftVariant; @@ -142,10 +144,21 @@ export class HuggingFaceService { private static readonly BASE_URL = 'https://huggingface.co/api/models'; + // Cached base model lookups keyed by repo id, so repeated selector opens + // never re-hit the HF API for the same repo. + private static baseModelCache = new Map(); + + private static baseModelPending = new Map< + string, + Promise<{ org: string; name: string } | null> + >(); + private static readonly DEFAULT_LIMIT = 50; private static readonly MAX_LIMIT = 100; + // GGUF Model Searching + /** * Map of quant token to its average bit-depth in bits-per-weight (bpw). */ @@ -242,7 +255,7 @@ export class HuggingFaceService { return { quant, variant, variantForm }; } - // GGUF Model Searching + // GGUF Model Browsing /** * Filter raw siblings by file extension and sort by size descending. @@ -268,8 +281,6 @@ export class HuggingFaceService { return downloads.toString(); } - // GGUF Model Browsing - /** * Format file size in bytes to human-readable string */ @@ -289,6 +300,19 @@ export class HuggingFaceService { return `${bytes} B`; } + /** + * Format a min-max size range with a single shared unit and no spaces + * around the dash, e.g. `19.0-28.6 GB`. + */ + static formatSizeRange(min: number, max: number): string { + const unit = max >= 1_000_000_000 ? 'GB' : max >= 1_000_000 ? 'MB' : max >= 1_000 ? 'KB' : 'B'; + const div = + unit === 'GB' ? 1_000_000_000 : unit === 'MB' ? 1_000_000 : unit === 'KB' ? 1_000 : 1; + const fmt = (n: number) => (div === 1 ? `${n}` : `${(n / div).toFixed(1)}`); + + return `${fmt(min)}-${fmt(max)} ${unit}`; + } + /** * Format likes count with K suffix if applicable */ @@ -322,6 +346,66 @@ export class HuggingFaceService { return `${Math.floor(diffDays / 365)} years ago`; } + // Model Details & Files + + /** + * Avatar URL for an author (org or user). 404s when the author does not + * exist, so callers should provide a fallback. + */ + static getAvatarUrl(author: string): string { + return `https://huggingface.co/api/avatars/${author}`; + } + + /** + * Resolve the original (non-GGUF) base model `{ org, name }` for a GGUF repo + * from its HF card (`cardData.base_model`). Returns null when the card has no + * base model. Results are cached per repo. + */ + static getBaseModel(repoId: string): Promise<{ org: string; name: string } | null> { + const cached = this.baseModelCache.get(repoId); + + if (cached !== undefined) return Promise.resolve(cached); + + const pending = this.baseModelPending.get(repoId); + + if (pending) return pending; + + const promise = (async () => { + const details = await this.getDetails(repoId); + const base = this.getBaseModels(details)[0]; + + if (!base) return null; + + const [org, ...rest] = base.split('/'); + + return { name: rest.join('/'), org }; + })(); + + this.baseModelPending.set(repoId, promise); + + promise + .then((result) => this.baseModelCache.set(repoId, result)) + .finally(() => this.baseModelPending.delete(repoId)); + + return promise; + } + + /** + * Extract the original (non-GGUF) base model ids for a repo, from + * `cardData.base_model` (string or list) and the `base_model:` tags. + */ + static getBaseModels(model: HfModelDetailInfo | null): string[] { + if (!model) return []; + + const cardBase = model.cardData?.base_model; + const fromCard: string[] = Array.isArray(cardBase) ? cardBase : cardBase ? [cardBase] : []; + const fromTags = (model.tags ?? []) + .map((t) => /^base_model:(?:quantized:)?(.+)$/.exec(t)?.[1]) + .filter((v): v is string => Boolean(v)); + + return Array.from(new Set([...fromCard, ...fromTags])); + } + /** * Look up the average bit-depth for a known GGUF quantization. * Returns `null` for unrecognized tokens. @@ -343,11 +427,29 @@ export class HuggingFaceService { }); } - // Model Details & Files - /** * Get detailed information about a specific GGUF model */ + /** + * Fetch the llama.app model catalog (https://llama.app/v1/catalog.json). + * Returns an empty array on failure so callers can fall back gracefully. + */ + static async getCatalog(): Promise { + const url = 'https://llama.app/v1/catalog.json'; + + try { + const response = await fetch(url); + + if (!response.ok) throw new Error(`Failed to fetch catalog: ${response.status}`); + + return (await response.json()) as HfCatalogEntry[]; + } catch (error) { + console.error('Error fetching catalog:', error); + + return []; + } + } + static async getDetails(modelId: string): Promise { // Do not encode the modelId, it contains slashes for author/name. // `full=true` includes cardData (description, base_model) and safetensors. @@ -370,6 +472,15 @@ export class HuggingFaceService { } } + /** + * Get model URL on Hugging Face Hub + */ + static getModelUrl(modelId: string): string { + return `https://huggingface.co/${modelId}`; + } + + // Utility Methods + /** * Get most liked GGUF models */ @@ -395,6 +506,28 @@ export class HuggingFaceService { return this.search({ limit, sort: 'downloads' }); } + /** + * Fetch the raw README.md for a repo, with the YAML frontmatter stripped. + */ + static async getReadme(modelId: string): Promise { + // Do not encode the modelId, it contains slashes for author/name + const url = `https://huggingface.co/${modelId}/raw/main/README.md`; + + try { + const response = await fetch(url); + + if (response.status === 404) return null; + + if (!response.ok) throw new Error(`Failed to fetch README: ${response.status}`); + + return HuggingFaceService.stripFrontmatter(await response.text()); + } catch (error) { + console.error(`Error fetching README for ${modelId}:`, error); + + return null; + } + } + /** * Get repository file tree to list available GGUF variants */ @@ -413,7 +546,6 @@ export class HuggingFaceService { return []; } } - /** * Get trending GGUF models */ @@ -423,44 +555,6 @@ export class HuggingFaceService { return this.search({ limit, sort: 'trendingScore' }); } - /** - * Fetch the raw README.md for a repo, with the YAML frontmatter stripped. - */ - static async getReadme(modelId: string): Promise { - // Do not encode the modelId, it contains slashes for author/name - const url = `https://huggingface.co/${modelId}/raw/main/README.md`; - - try { - const response = await fetch(url); - - if (response.status === 404) return null; - if (!response.ok) throw new Error(`Failed to fetch README: ${response.status}`); - - return HuggingFaceService.stripFrontmatter(await response.text()); - } catch (error) { - console.error(`Error fetching README for ${modelId}:`, error); - - return null; - } - } - - // Utility Methods - - /** - * Get model URL on Hugging Face Hub - */ - static getModelUrl(modelId: string): string { - return `https://huggingface.co/${modelId}`; - } - - /** - * Avatar URL for an author (org or user). 404s when the author does not - * exist, so callers should provide a fallback. - */ - static getAvatarUrl(author: string): string { - return `https://huggingface.co/api/avatars/${author}`; - } - /** * Parse a local HF cache file path * (`.../models----/snapshots//`) into its repo id and @@ -475,31 +569,20 @@ export class HuggingFaceService { if (parts.length < 2) return null; - return { repo: `${parts[0]}/${parts.slice(1).join('--')}`, file: match[2] }; + return { file: match[2], repo: `${parts[0]}/${parts.slice(1).join('--')}` }; } /** - * Extract the original (non-GGUF) base model ids for a repo, from - * `cardData.base_model` (string or list) and the `base_model:` tags. + * Best-effort parameter count parsed from a model id/name, e.g. `27B` from + * `Qwen3.8-27B-GGUF` or `300M` from `embeddinggemma-300M-GGUF`. Returns null + * when no size token is present. */ - static getBaseModels(model: HfModelDetailInfo | null): string[] { - if (!model) return []; + static parseParamCount(name: string): string | null { + const match = /(?:^|[^a-z0-9])(\d+(?:[._]\d+)?)\s*([bm])(?![a-z0-9])/i.exec(name); - const cardBase = model.cardData?.base_model; - const fromCard: string[] = Array.isArray(cardBase) ? cardBase : cardBase ? [cardBase] : []; + if (!match) return null; - const fromTags = (model.tags ?? []) - .map((t) => /^base_model:(?:quantized:)?(.+)$/.exec(t)?.[1]) - .filter((v): v is string => Boolean(v)); - - return Array.from(new Set([...fromCard, ...fromTags])); - } - - /** Strip a leading YAML frontmatter block (--- ... ---) from a markdown document. */ - private static stripFrontmatter(text: string): string { - const match = text.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/); - - return match ? text.slice(match[0].length) : text; + return `${match[1]}${match[2].toUpperCase()}`; } /** @@ -521,19 +604,6 @@ export class HuggingFaceService { return { isGated, isGguf, isSafetensors, license, tasks }; } - /** - * Best-effort parameter count parsed from a model id/name, e.g. `27B` from - * `Qwen3.8-27B-GGUF` or `300M` from `embeddinggemma-300M-GGUF`. Returns null - * when no size token is present. - */ - static parseParamCount(name: string): string | null { - const match = /(?:^|[^a-z0-9])(\d+(?:[._]\d+)?)\s*([bm])(?![a-z0-9])/i.exec(name); - - if (!match) return null; - - return `${match[1]}${match[2].toUpperCase()}`; - } - /** Resolve a pipeline_tag to a lucide icon name, or null when unknown. */ static pipelineTagIcon(tag: string | null | undefined): string | null { if (!tag) return null; @@ -575,8 +645,6 @@ export class HuggingFaceService { }); } - // Internal Methods - /** * Build API URL from search parameters */ @@ -596,6 +664,8 @@ export class HuggingFaceService { return url.toString(); } + // Internal Methods + /** * Delay helper for retry logic */ @@ -648,4 +718,11 @@ export class HuggingFaceService { throw error; } } + + /** Strip a leading YAML frontmatter block (--- ... ---) from a markdown document. */ + private static stripFrontmatter(text: string): string { + const match = text.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/); + + return match ? text.slice(match[0].length) : text; + } } diff --git a/tools/ui/src/lib/stores/models-hub/index.svelte.ts b/tools/ui/src/lib/stores/models-hub/index.svelte.ts index ec47a0a206..e769553377 100644 --- a/tools/ui/src/lib/stores/models-hub/index.svelte.ts +++ b/tools/ui/src/lib/stores/models-hub/index.svelte.ts @@ -9,27 +9,37 @@ * Detail data is loaded by ModelsDiscoverDetails, not here. */ -import { CURATED_MODEL_IDS } from '$lib/constants'; import { HuggingFaceService } from '$lib/services'; -import type { HfModelInfo } from '$lib/types/huggingface'; +import type { HfCatalogEntry, HfModelInfo } from '$lib/types/huggingface'; class ModelsHubStore { - models = $state([]); - loading = $state(false); error = $state(null); - + models = $state([]); /** First model in the list - the hub auto-opens this one. */ firstModel = $derived(this.models[0] ?? null); - private fetched = false; + loading = $state(false); + + private catalog: HfCatalogEntry[] = []; private defaultModels: HfModelInfo[] = []; + private fetched = false; private searchRequestId = 0; /** - * Fetch the default list: the curated ggml--org models in display order. - * Each curated model is fetched directly by ID, so the list is independent - * of download ranking (curated models may fall outside the top-50 search - * window). No-op when already loaded or in flight. + * Catalog family description for a repo id, or undefined when the repo is + * not part of the catalog (e.g. a search result outside the curated list). + */ + descriptionFor(modelId: string): string | undefined { + return this.catalog.find((entry) => + entry.sizes.some((size) => size.builds.some((build) => build.repo === modelId)) + )?.description; + } + + /** + * Fetch the default list from the llama.app catalog, flattened to a flat + * list of ggml-org repo ids in catalog order (one per size). Each repo is + * fetched directly by ID, so the list is independent of download ranking. + * No-op when already loaded or in flight. */ async fetch(): Promise { if (this.loading || this.fetched) return; @@ -38,10 +48,15 @@ class ModelsHubStore { this.error = null; try { + const catalog = await HuggingFaceService.getCatalog(); + + this.catalog = catalog; + const ids = this.catalogModelIds(catalog); + // getDetails returns full metadata (downloads, likes, lastModified, // siblings, tags, gguf) for a single model. this.defaultModels = ( - await Promise.all(CURATED_MODEL_IDS.map((id) => HuggingFaceService.getDetails(id))) + await Promise.all(ids.map((id) => HuggingFaceService.getDetails(id))) ).filter((m): m is HfModelInfo => m !== null); this.models = this.defaultModels; this.fetched = true; @@ -65,13 +80,14 @@ class ModelsHubStore { if (!trimmed) { this.models = this.defaultModels; this.error = null; + return; } const requestId = this.searchRequestId; try { - const results = await HuggingFaceService.searchByQuery(trimmed, { limit: 50, full: true }); + const results = await HuggingFaceService.searchByQuery(trimmed, { full: true, limit: 50 }); if (requestId === this.searchRequestId) { this.models = results; @@ -83,6 +99,42 @@ class ModelsHubStore { } } } + + /** + * Min/max GGUF file size (bytes) across the available quants for a repo, + * or undefined when the repo is not part of the catalog. + */ + sizeRangeFor(modelId: string): { min: number; max: number } | undefined { + for (const entry of this.catalog) { + for (const size of entry.sizes) { + const builds = size.builds.filter((b) => b.repo === modelId); + + if (builds.length === 0) continue; + + const bytes = builds.map((b) => b.sizeBytes); + + return { max: Math.max(...bytes), min: Math.min(...bytes) }; + } + } + + return undefined; + } + + /** + * Flatten the catalog to a flat list of ggml-org repo ids, newest family + * first (by release date). Returns an empty array when the catalog is empty. + */ + private catalogModelIds(catalog: HfCatalogEntry[]): string[] { + return [...catalog] + .sort((a, b) => b.released.localeCompare(a.released)) + .flatMap((entry) => + entry.sizes.flatMap((size) => { + const build = size.builds.find((b) => b.repo.startsWith('ggml-org/')); + + return build ? [build.repo] : []; + }) + ); + } } export const modelsHubStore = new ModelsHubStore(); diff --git a/tools/ui/src/lib/stores/models/status.svelte.ts b/tools/ui/src/lib/stores/models/status.svelte.ts index 6b4f5e5ab4..2cafe63540 100644 --- a/tools/ui/src/lib/stores/models/status.svelte.ts +++ b/tools/ui/src/lib/stores/models/status.svelte.ts @@ -34,11 +34,6 @@ export interface ModelStatusHost { } export class ModelStatusManager { - private downloadProgress = new SvelteMap(); - /** `:` strings whose most recent download attempt failed (download_failed). */ - private failedDownloads = new SvelteSet(); - private loadingStates = new SvelteMap(); - private loadProgress = new SvelteMap(); /** * Draft sidecar files pulled by registered models, as `/` keys. * Drafts are not separate /v1/models entries - the router pulls them as @@ -63,6 +58,11 @@ export class ModelStatusManager { return result; }); + private downloadProgress = new SvelteMap(); + /** `:` strings whose most recent download attempt failed (download_failed). */ + private failedDownloads = new SvelteSet(); + private loadingStates = new SvelteMap(); + private loadProgress = new SvelteMap(); // /models/sse feed state, the single source of truth for status and load progress private statusAbort: AbortController | null = null; private statusReaderActive = false; @@ -102,6 +102,26 @@ export class ModelStatusManager { } } + /** + * Cancel an in-flight load (ROUTER mode only). The server force-kills a + * LOADING model on unload; the feed reports the settled status, so no + * waiter is registered here. + */ + async cancelLoad(modelId: string): Promise { + if (!serverStore.isRouterMode) return; + + this.subscribe(); + + try { + await ModelsService.unload(modelId); + toast.info(`Load cancelled: ${this.host.toDisplayName(modelId)}`); + } catch (error) { + toast.error(`Failed to cancel load: ${this.host.toDisplayName(modelId)}`); + + throw error; + } + } + constructor(private host: ModelStatusHost) {} /** @@ -170,14 +190,6 @@ export class ModelStatusManager { return this.downloadProgress.has(repoWithTag); } - /** - * True when the given `:` is already a fully downloaded model - * registered with the server (i.e. it shows up in the /v1/models list). - */ - isModelDownloaded(repoWithTag: string): boolean { - return this.host.routerModels.some((m) => m.id === repoWithTag); - } - /** * True when the given draft sidecar file (repo-relative path) has been pulled * as the `--model-draft` of some registered model. @@ -186,6 +198,14 @@ export class ModelStatusManager { return this.downloadedDrafts.has(`${repoId}/${filePath}`); } + /** + * True when the given `:` is already a fully downloaded model + * registered with the server (i.e. it shows up in the /v1/models list). + */ + isModelDownloaded(repoWithTag: string): boolean { + return this.host.routerModels.some((m) => m.id === repoWithTag); + } + isOperationInProgress(modelId: string): boolean { return this.loadingStates.get(modelId) ?? false; } @@ -263,26 +283,6 @@ export class ModelStatusManager { } } - /** - * Cancel an in-flight load (ROUTER mode only). The server force-kills a - * LOADING model on unload; the feed reports the settled status, so no - * waiter is registered here. - */ - async cancelLoad(modelId: string): Promise { - if (!serverStore.isRouterMode) return; - - this.subscribe(); - - try { - await ModelsService.unload(modelId); - toast.info(`Load cancelled: ${this.host.toDisplayName(modelId)}`); - } catch (error) { - toast.error(`Failed to cancel load: ${this.host.toDisplayName(modelId)}`); - - throw error; - } - } - /** * Close the /models/sse feed and drop transient progress. */ diff --git a/tools/ui/src/lib/types/huggingface.d.ts b/tools/ui/src/lib/types/huggingface.d.ts index 5f30c4e35b..db8137aee1 100644 --- a/tools/ui/src/lib/types/huggingface.d.ts +++ b/tools/ui/src/lib/types/huggingface.d.ts @@ -189,3 +189,33 @@ export interface HfModelApiResponse { /** Total count (if available) */ total?: number; } + +// llama.app model catalog (https://llama.app/v1/catalog.json) + +/** A single GGUF build/repo within a catalog size. */ +export interface HfCatalogBuild { + quant: string; + size: string; + sizeBytes: number; + repo: string; +} + +/** A size variant (e.g. `GPT-OSS 20B`) within a catalog entry. */ +export interface HfCatalogSize { + name: string; + params: string; + builds: HfCatalogBuild[]; +} + +/** A single model family in the catalog. `featured` marks the staff picks. */ +export interface HfCatalogEntry { + name: string; + brand: string; + description: string; + details: string; + released: string; + license: string; + featured?: boolean; + maxMemGb?: number; + sizes: HfCatalogSize[]; +} diff --git a/tools/ui/src/lib/types/model-manager.d.ts b/tools/ui/src/lib/types/model-manager.d.ts index 3b129ec06b..95efc2a2a3 100644 --- a/tools/ui/src/lib/types/model-manager.d.ts +++ b/tools/ui/src/lib/types/model-manager.d.ts @@ -1,5 +1,5 @@ -import type { DraftVariant } from '$lib/constants'; import type { ModelOption } from './models'; +import type { DraftVariant } from '$lib/constants'; /** * A draft sidecar model attached to a quant (speculative decoding). diff --git a/tools/ui/src/lib/types/models.d.ts b/tools/ui/src/lib/types/models.d.ts index e36e9743ac..bc528b6d87 100644 --- a/tools/ui/src/lib/types/models.d.ts +++ b/tools/ui/src/lib/types/models.d.ts @@ -19,6 +19,8 @@ export interface ModelOption { parsedId?: ParsedModelId; aliases?: string[]; tags?: string[]; + /** Original (non-GGUF) base model, resolved from the HF card. */ + baseModel?: { org: string; name: string }; } /** diff --git a/tools/ui/src/lib/utils/chat-template-tool-detector.ts b/tools/ui/src/lib/utils/chat-template-tool-detector.ts new file mode 100644 index 0000000000..a90ee24c63 --- /dev/null +++ b/tools/ui/src/lib/utils/chat-template-tool-detector.ts @@ -0,0 +1,28 @@ +/** + * Detects whether a model's chat template supports tool calling. + * + * There is no server flag for tool support, so we infer it from the chat + * template. A template that accepts a `tools` array or emits tool-call tokens + * is treated as tool-capable. + */ + +/** Tool-call tokens emitted by the template for assistant tool calls. */ +const TOOL_CALL_TOKENS = [ + 'tool_call', + 'tool_calls', + 'function_call', + 'tool_use', + ' t.includes(token)); +} diff --git a/tools/ui/src/lib/utils/index.ts b/tools/ui/src/lib/utils/index.ts index 92c4c9da26..5b2b89c8fb 100644 --- a/tools/ui/src/lib/utils/index.ts +++ b/tools/ui/src/lib/utils/index.ts @@ -263,6 +263,8 @@ export { detectThinkingSupportWithReason } from './chat-template-thinking-detector'; +export { detectToolUseSupport } from './chat-template-tool-detector'; + // Agentic content utilities (structured section derivation) export { deriveAgenticSections, diff --git a/tools/ui/src/lib/utils/model-manager.ts b/tools/ui/src/lib/utils/model-manager.ts index 40f10a39be..c76285a3fa 100644 --- a/tools/ui/src/lib/utils/model-manager.ts +++ b/tools/ui/src/lib/utils/model-manager.ts @@ -1,9 +1,5 @@ import { HuggingFaceService } from '$lib/services'; -import type { - ModelManagerParent, - ModelManagerQuant, - ModelManagerQuantOrg -} from '$lib/types'; +import type { ModelManagerParent, ModelManagerQuant, ModelManagerQuantOrg } from '$lib/types'; import type { ModelOption } from '$lib/types/models'; /** Strip the `:quant` / `:quant-VARIANT` tag from a model id to get the repo id. */ @@ -50,7 +46,9 @@ export async function resolveBaseModel(repoId: string): Promise { resolved = first.trim(); } else { // tag fallback, e.g. `base_model:Qwen/Qwen3-8B` - const tag = details?.tags?.find((t) => t.startsWith('base_model:') && !t.startsWith('base_model:quantized:')); + const tag = details?.tags?.find( + (t) => t.startsWith('base_model:') && !t.startsWith('base_model:quantized:') + ); const fromTag = tag?.slice('base_model:'.length).trim(); if (fromTag) resolved = fromTag; @@ -90,17 +88,18 @@ export function buildModelManagerTree( } const key = quant ?? ''; + let entry = quantMap.get(key); if (!entry) { - entry = { quant, main: option, drafts: [], mmproj: null }; + entry = { drafts: [], main: option, mmproj: null, quant }; quantMap.set(key, entry); } if (variant === 'mmproj') { entry.mmproj = option; } else if (variant) { - entry.drafts.push({ variant, option }); + entry.drafts.push({ option, variant }); } else { entry.main = option; } @@ -119,7 +118,7 @@ export function buildModelManagerTree( const slashIdx = repoId.indexOf('/'); const orgName = slashIdx !== -1 ? repoId.slice(0, slashIdx) : repoId; - quantOrgs.push({ repoId, orgName, quants }); + quantOrgs.push({ orgName, quants, repoId }); } // Group quant orgs under parents. diff --git a/tools/ui/src/routes/model-manager/+page.svelte b/tools/ui/src/routes/model-manager/+page.svelte index 751afc3c8c..5b37952e44 100644 --- a/tools/ui/src/routes/model-manager/+page.svelte +++ b/tools/ui/src/routes/model-manager/+page.svelte @@ -22,7 +22,7 @@ import { SvelteMap } from 'svelte/reactivity'; let modelsHubOpen = $state(false); - let baseModels = $state(new SvelteMap()); + let baseModels = new SvelteMap(); let isRouter = $derived(serverStore.isRouterMode); @@ -61,7 +61,8 @@ {@const entry = getRouterEntry(option.model)} {@const status = entry?.status} {@const statusValue = status?.value} - {@const isLoaded = statusValue === ServerModelStatus.LOADED || statusValue === ServerModelStatus.SLEEPING} + {@const isLoaded = + statusValue === ServerModelStatus.LOADED || statusValue === ServerModelStatus.SLEEPING} {@const isLoading = statusValue === ServerModelStatus.LOADING} {@const isDownloading = statusValue === ServerModelStatus.DOWNLOADING} {@const isFailed = statusValue === ServerModelStatus.FAILED || status?.failed === true} @@ -78,7 +79,7 @@ tags={option.tags} modalities={option.modalities} supportsThinking={modelsStore.props.checkModelSupportsThinking(option.model)} - hideQuantization={hideQuantization} + {hideQuantization} showRawTooltip /> diff --git a/tools/ui/src/routes/temp/models-hub/DialogModelDownload.svelte b/tools/ui/src/routes/temp/models-hub/DialogModelDownload.svelte index fed5f88b1a..d84881a5e7 100644 --- a/tools/ui/src/routes/temp/models-hub/DialogModelDownload.svelte +++ b/tools/ui/src/routes/temp/models-hub/DialogModelDownload.svelte @@ -1,12 +1,12 @@ @@ -173,19 +114,12 @@ - {#if phase === 'pending'} - Download this model? - {:else} - Downloading {tagDisplay} - {/if} + Download this model? - {#if phase === 'pending'} - llama-server will download this file (and related sidecar weights such as multimodal - projectors or draft models) from Hugging Face into your local model cache. - {:else} - Download runs in the background; this dialog tracks live progress. - {/if} + llama-server will download this file (and related sidecar weights such as multimodal + projectors or draft models) from Hugging Face into your local model cache. The download runs + in the background and its progress is shown in a notification. {#if previousFailure && phase === 'pending'} @@ -203,20 +137,6 @@ {/if} - {#if canDelete} -
- -
- {/if} -
Request @@ -243,74 +163,25 @@ {/if}
- - {#if phase === 'downloading' || phase === 'finished'} -
-
- - {#if phase === 'finished'} - Complete - {:else if progress && progress.totalBytes > 0} - Downloading - {:else} - Preparing download - {/if} - - {progressPercent}% -
- -
- {/if}
{#if lastError}

{lastError}

{/if} - {#if lastCancelError} -

{lastCancelError}

- {/if} - {#if phase === 'downloading'} - - {#if cancelling} - - Cancelling... - {:else} - Cancel download - {/if} - - {:else} - - {#if phase === 'finished'}Close{:else}Cancel{/if} - - {/if} - {#if phase === 'pending'} - - - {previousFailure ? 'Retry download' : 'Download'} - - {:else if phase === 'starting'} - + + Cancel + + + {#if phase === 'starting'} Starting... - - {/if} + {:else} + + {previousFailure ? 'Retry download' : 'Download'} + {/if} +
- - (showDeleteConfirm = false)} -/> diff --git a/tools/ui/src/routes/temp/models-hub/DownloadToast.svelte b/tools/ui/src/routes/temp/models-hub/DownloadToast.svelte new file mode 100644 index 0000000000..1a89f46f37 --- /dev/null +++ b/tools/ui/src/routes/temp/models-hub/DownloadToast.svelte @@ -0,0 +1,90 @@ + + +
+
+ + {#if done && failed} + + Download failed + {:else if done} + + Download complete + {:else} + + Downloading + {/if} + + {percent}% +
+

{displayName}

+ + +
diff --git a/tools/ui/tests/stories/ModelsSelector.stories.svelte b/tools/ui/tests/stories/ModelsSelector.stories.svelte index 7ffe004614..3d024e4615 100644 --- a/tools/ui/tests/stories/ModelsSelector.stories.svelte +++ b/tools/ui/tests/stories/ModelsSelector.stories.svelte @@ -82,26 +82,11 @@ const groupedOptions: GroupedModelOptions = { available: [ - { - items: [availableModels[0]], - orgName: 'deepseek' - }, - { - items: [availableModels[1]], - orgName: 'google' - }, - { - items: [availableModels[2]], - orgName: 'microsoft' - }, - { - items: [availableModels[3]], - orgName: 'codellama' - }, - { - items: [availableModels[4]], - orgName: 'intel' - } + availableModels[0], + availableModels[1], + availableModels[2], + availableModels[3], + availableModels[4] ], favorites: favoriteModels, loaded: loadedModels