From 5d87573a3bdf3d4bdb09c00cd51aae9921c7017e Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Mon, 28 Sep 2026 15:15:25 +0200 Subject: [PATCH] ui : add the discover models components The Discover surface: the curated-catalog list with its search and skeletons, the details pane with its readme, metadata, Hub stats and download options, and the standalone download progress bar that replaces the plain one. The quant download button sizes its select with a new xs trigger, so the select gains that size and the muted box look. The store stays incomplete when a repo fails, so the next mount retries it. Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash --- .../discover/DownloadProgressBar.svelte | 32 -- .../app/models/discover/ModelsDiscover.svelte | 138 +++++++++ .../ModelsDiscoverChatTemplateDialog.svelte | 67 +++++ .../ModelsDiscoverDetails.svelte | 96 ++++++ ...odelsDiscoverDetailsDownloadOptions.svelte | 173 +++++++++++ ...tailsDownloadOptionsDownloadCommand.svelte | 278 +++++++++++++++++ ...sDownloadOptionsQuantDownloadButton.svelte | 244 +++++++++++++++ ...lsDiscoverDetailsDownloadOptionsRow.svelte | 60 ++++ .../download-options.utils.ts | 28 ++ .../index.ts | 40 +++ .../ModelsDiscoverDetailsHeader.svelte | 94 ++++++ .../ModelsDiscoverDetailsHfHubStats.svelte | 31 ++ .../ModelsDiscoverDetailsMetadata.svelte | 80 +++++ .../ModelsDiscoverDetailsMetadataItem.svelte | 17 ++ .../ModelsDiscoverDetailsReadme.svelte | 17 ++ .../ModelsDiscoverDetailsSkeleton.svelte | 93 ++++++ .../discover/ModelsDiscoverDetails/index.ts | 72 +++++ .../ModelsDiscoverDownloadProgressBar.svelte | 4 +- .../ModelsDiscoverList.svelte | 41 +++ .../ModelsDiscoverListItem.svelte | 129 ++++++++ .../ModelsDiscoverListItemSkeleton.svelte | 39 +++ .../ModelsDiscoverListSearch.svelte | 26 ++ .../discover/ModelsDiscoverList/index.ts | 38 +++ .../components/app/models/discover/index.ts | 39 +++ .../ui/select/select-content.svelte | 4 +- .../ui/select/select-trigger.svelte | 14 +- tools/ui/src/lib/constants/index.ts | 1 + .../models-discover-download.constants.ts | 54 ++++ .../constants/models-discover.constants.ts | 6 + tools/ui/src/lib/enums/index.ts | 8 +- tools/ui/src/lib/enums/model.enums.ts | 10 + tools/ui/src/lib/stores/index.ts | 3 + .../stores/models-discover/index.svelte.ts | 282 ++++++++++++++++++ tools/ui/src/lib/types/index.ts | 8 + tools/ui/src/lib/types/models-discover.d.ts | 30 ++ 35 files changed, 2255 insertions(+), 41 deletions(-) delete mode 100644 tools/ui/src/lib/components/app/models/discover/DownloadProgressBar.svelte create mode 100644 tools/ui/src/lib/components/app/models/discover/ModelsDiscover.svelte create mode 100644 tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverChatTemplateDialog.svelte create mode 100644 tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetails.svelte create mode 100644 tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/ModelsDiscoverDetailsDownloadOptions.svelte create mode 100644 tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/ModelsDiscoverDetailsDownloadOptionsDownloadCommand.svelte create mode 100644 tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/ModelsDiscoverDetailsDownloadOptionsQuantDownloadButton.svelte create mode 100644 tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/ModelsDiscoverDetailsDownloadOptionsRow.svelte create mode 100644 tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/download-options.utils.ts create mode 100644 tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/index.ts create mode 100644 tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsHeader.svelte create mode 100644 tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsHfHubStats.svelte create mode 100644 tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsMetadata.svelte create mode 100644 tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsMetadataItem.svelte create mode 100644 tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsReadme.svelte create mode 100644 tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsSkeleton.svelte create mode 100644 tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/index.ts create mode 100644 tools/ui/src/lib/components/app/models/discover/ModelsDiscoverList/ModelsDiscoverList.svelte create mode 100644 tools/ui/src/lib/components/app/models/discover/ModelsDiscoverList/ModelsDiscoverListItem.svelte create mode 100644 tools/ui/src/lib/components/app/models/discover/ModelsDiscoverList/ModelsDiscoverListItemSkeleton.svelte create mode 100644 tools/ui/src/lib/components/app/models/discover/ModelsDiscoverList/ModelsDiscoverListSearch.svelte create mode 100644 tools/ui/src/lib/components/app/models/discover/ModelsDiscoverList/index.ts create mode 100644 tools/ui/src/lib/components/app/models/discover/index.ts create mode 100644 tools/ui/src/lib/constants/models-discover-download.constants.ts create mode 100644 tools/ui/src/lib/stores/models-discover/index.svelte.ts create mode 100644 tools/ui/src/lib/types/models-discover.d.ts diff --git a/tools/ui/src/lib/components/app/models/discover/DownloadProgressBar.svelte b/tools/ui/src/lib/components/app/models/discover/DownloadProgressBar.svelte deleted file mode 100644 index 6f4973c12e..0000000000 --- a/tools/ui/src/lib/components/app/models/discover/DownloadProgressBar.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - -{#if overlay} -
-
-
-{:else} -
-
-
-{/if} diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscover.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscover.svelte new file mode 100644 index 0000000000..01b4e90e0a --- /dev/null +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscover.svelte @@ -0,0 +1,138 @@ + + + + +
+ {#if selectedId} + + {/if} +
diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverChatTemplateDialog.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverChatTemplateDialog.svelte new file mode 100644 index 0000000000..45eb209a2a --- /dev/null +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverChatTemplateDialog.svelte @@ -0,0 +1,67 @@ + + + + + + + Chat template + + + + + + + + +
{chatTemplate}
+
+
diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetails.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetails.svelte new file mode 100644 index 0000000000..cecdda738c --- /dev/null +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetails.svelte @@ -0,0 +1,96 @@ + + +{#if loading} + +{:else if error} +
+

{error}

+
+{:else if details} +
+ + + + + +
+{/if} diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/ModelsDiscoverDetailsDownloadOptions.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/ModelsDiscoverDetailsDownloadOptions.svelte new file mode 100644 index 0000000000..75ecef320e --- /dev/null +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/ModelsDiscoverDetailsDownloadOptions.svelte @@ -0,0 +1,173 @@ + + +{#if bitDepthRows.length} +
+ +
+ {#each rows as row (row.bitDepth)} + + {/each} +
+ + +
+ +
+
+{/if} + + (confirmOpen = false)} + open={confirmOpen} + repoWithTag={pending.repoWithTag} +/> diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/ModelsDiscoverDetailsDownloadOptionsDownloadCommand.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/ModelsDiscoverDetailsDownloadOptionsDownloadCommand.svelte new file mode 100644 index 0000000000..e380505f38 --- /dev/null +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/ModelsDiscoverDetailsDownloadOptionsDownloadCommand.svelte @@ -0,0 +1,278 @@ + + + + +
+ +
+ {SERVE_COMMAND.BIN} + + {SERVE_COMMAND.SUBCOMMAND} + + {SERVE_COMMAND.MODEL_FLAG} + + + {modelId}{mainQuant ? MODEL_ID.QUANTIZATION_SEPARATOR : ''} + + + + {#if baseOption} + v && (basePick = v)} type="single" value={baseOption.path}> + + {baseOption.label} + + + + {#each mainOptions as option (option.path)} + + {option.label} + + {/each} + + + {/if} + + + {#if draftOptions.length && !withDraft} + + {/if} + + + {#if draftOption} + + {SERVE_COMMAND.DRAFT_FLAG} + + + {modelId}{draftQuant ? MODEL_ID.QUANTIZATION_SEPARATOR : ''} + + + v && (draftPick = v)} + type="single" + value={draftOption.path} + > + + {draftOption.label} + + + + {#each typeDraftOptions as option (option.path)} + + {option.label} + + {/each} + + + + {#if draftType} + {SERVE_COMMAND.SPEC_TYPE_FLAG} + + + {#if specTypes.length > 1} + v && (draftTypePick = v as ModelSidecar)} + type="single" + value={draftType} + > + + {SPEC_TYPE[draftType]} + + + + {#each specTypes as type (type)} + + {SPEC_TYPE[type]} + + {/each} + + + {:else} + {SPEC_TYPE[draftType]} + {/if} + {/if} + + + + {/if} +
+ + +
diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/ModelsDiscoverDetailsDownloadOptionsQuantDownloadButton.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/ModelsDiscoverDetailsDownloadOptionsQuantDownloadButton.svelte new file mode 100644 index 0000000000..2908ea97a3 --- /dev/null +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/ModelsDiscoverDetailsDownloadOptionsQuantDownloadButton.svelte @@ -0,0 +1,244 @@ + + +{#snippet chipBody(dividerClass: string)} + + {#if meta?.sidecar} + + {meta.sidecar} + + {/if} + + {label} + + +{/snippet} + + +{#snippet tooltipTrigger(tip: string, button: Snippet<[Record]>)} + + + {#snippet child({ props })} + {@render button(props)} + {/snippet} + + + +

{tip}

+
+
+{/snippet} + +{#snippet deleteChip(props: Record)} + + +{/snippet} + +{#snippet pauseResumeChip(props: Record)} + +{/snippet} + +{#snippet cancelChip(props: Record)} + + +{/snippet} + +{#snippet downloadChip(props: Record)} + + +{/snippet} + +{#if entry.isDownloaded} + {@render tooltipTrigger(tooltipText, deleteChip)} +{:else if entry.isDownloading || entry.isPaused} + +
+ {@render tooltipTrigger(tooltipText, pauseResumeChip)} + + {@render tooltipTrigger('Cancel downloading', cancelChip)} + + {#if percent !== null} + + {/if} +
+{:else} + {@render tooltipTrigger(tooltipText, downloadChip)} +{/if} diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/ModelsDiscoverDetailsDownloadOptionsRow.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/ModelsDiscoverDetailsDownloadOptionsRow.svelte new file mode 100644 index 0000000000..11bfe45d42 --- /dev/null +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/ModelsDiscoverDetailsDownloadOptionsRow.svelte @@ -0,0 +1,60 @@ + + +
+
+ {#if bitDepth === OTHER_BIT_DEPTH} + {OTHER_BIT_DEPTH_LABEL} + {:else} + {bitDepth}{BIT_DEPTH_LABEL_SUFFIX} + {/if} + + {#if mainMemGb} + + needs at least {mainMemGb}{GIGABYTE_LABEL}{draftMemGb + ? ` + ${draftMemGb}${GIGABYTE_LABEL}` + : ''} memory + + {/if} +
+ +
+ {#each files as file (file.path)} + + {/each} +
+
diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/download-options.utils.ts b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/download-options.utils.ts new file mode 100644 index 0000000000..4dd8accb42 --- /dev/null +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/download-options.utils.ts @@ -0,0 +1,28 @@ +import { DRAFT_FILE_LABEL, MODEL_ID, PATH_SEPARATOR } from '$lib/constants'; +import { ModelSelectableFileKind } from '$lib/enums'; +import { HuggingFaceService } from '$lib/services'; +import { isAuxSidecar, isDraftSidecar } from '$lib/utils'; + +/** Kind of a file path: the main weights, a draft sidecar, or an aux sidecar (mmproj). */ +export function classify(path: string): ModelSelectableFileKind { + const sidecar = HuggingFaceService.extractQuantMeta(path)?.sidecar; + + if (!sidecar) return ModelSelectableFileKind.MAIN; + + return isAuxSidecar(sidecar) ? ModelSelectableFileKind.AUX : ModelSelectableFileKind.DRAFT; +} + +/** Display label of a file: its quant, else the file name without the extension. */ +export function labelFor(path: string): string { + const meta = HuggingFaceService.extractQuantMeta(path); + + if (meta?.quant) return meta.quant; + + // Quantless sidecar files: the chip badge already carries the sidecar type, + // so the label only marks draft files; aux sidecars badge alone. + if (meta?.sidecar) return isDraftSidecar(meta.sidecar) ? DRAFT_FILE_LABEL : ''; + + const basename = path.split(PATH_SEPARATOR).pop() ?? path; + + return basename.replace(MODEL_ID.WEIGHT_EXTENSION_REGEX, ''); +} diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/index.ts b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/index.ts new file mode 100644 index 0000000000..b5c0ebef67 --- /dev/null +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/index.ts @@ -0,0 +1,40 @@ +/** + * + * MODELS DISCOVER - DETAILS - DOWNLOAD OPTIONS + * + * The download area of the detail pane: GGUF files grouped by bit depth, each an + * independent download action chip, plus the standalone terminal command preview. + * + */ + +/** + * **ModelsDiscoverDetailsDownloadOptions** - GGUF download options + * + * Groups GGUF files by bit depth and renders one independent download + * action chip per file, plus the standalone terminal command preview. + */ +export { default as ModelsDiscoverDetailsDownloadOptions } from './ModelsDiscoverDetailsDownloadOptions.svelte'; + +/** + * **ModelsDiscoverDetailsDownloadOptionsRow** - One bit-depth group of quants + * + * A single bit-depth row inside ModelsDiscoverDetailsDownloadOptions: the + * depth label with its memory hint and the quant chips of that depth. + */ +export { default as ModelsDiscoverDetailsDownloadOptionsRow } from './ModelsDiscoverDetailsDownloadOptionsRow.svelte'; + +/** + * **ModelsDiscoverDetailsDownloadOptionsQuantDownloadButton** - One quant chip + * + * A single GGUF file as an independent action chip: download / retry when + * idle, pause / resume / cancel while in flight, delete when downloaded. + */ +export { default as ModelsDiscoverDetailsDownloadOptionsQuantDownloadButton } from './ModelsDiscoverDetailsDownloadOptionsQuantDownloadButton.svelte'; + +/** + * **ModelsDiscoverDetailsDownloadOptionsDownloadCommand** - Terminal command + * + * The `llama serve -hf ...` command box with inline quant selects and a copy + * button; owns its picks, nothing two-way binds them to the quant chips. + */ +export { default as ModelsDiscoverDetailsDownloadOptionsDownloadCommand } from './ModelsDiscoverDetailsDownloadOptionsDownloadCommand.svelte'; diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsHeader.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsHeader.svelte new file mode 100644 index 0000000000..a216283436 --- /dev/null +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsHeader.svelte @@ -0,0 +1,94 @@ + + +
+
+
+ + +
+
+

{details.id ?? modelId}

+ + +
+ + {#if baseModels.length} +
+ {baseModels.join(', ')} + + + + +
+ {/if} +
+
+ + + + + View on Hugging Face + + + +
+ + + + +
diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsHfHubStats.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsHfHubStats.svelte new file mode 100644 index 0000000000..b848a9bfeb --- /dev/null +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsHfHubStats.svelte @@ -0,0 +1,31 @@ + + +
+ {#if typeof details.downloads === 'number'} + + + {HuggingFaceService.formatDownloads(details.downloads)} + + {/if} + + {#if typeof details.likes === 'number'} + + + {HuggingFaceService.formatLikes(details.likes)} + + {/if} + + {#if details.lastModified} + Updated {HuggingFaceService.formatRelativeTime(details.lastModified)} + {/if} +
diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsMetadata.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsMetadata.svelte new file mode 100644 index 0000000000..9f0184ac9f --- /dev/null +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsMetadata.svelte @@ -0,0 +1,80 @@ + + +{#if description} +

{description}

+{/if} + + +
+ {#if gguf?.total} + + {/if} + + {#if gguf?.context_length} + + {/if} + + {#if gguf?.architecture} + + {/if} + + {#if gguf?.chat_template} + + {/if} + + {#if licenseTag} + + {/if} + + {#if details.gated === true} + + gated + + {/if} +
+ +{#if gguf?.chat_template} + +{/if} diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsMetadataItem.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsMetadataItem.svelte new file mode 100644 index 0000000000..90c3c4c74f --- /dev/null +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsMetadataItem.svelte @@ -0,0 +1,17 @@ + + + + + {label} + + {value} + diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsReadme.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsReadme.svelte new file mode 100644 index 0000000000..fbf043b797 --- /dev/null +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsReadme.svelte @@ -0,0 +1,17 @@ + + +{#if readme} +
+ +
+{/if} diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsSkeleton.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsSkeleton.svelte new file mode 100644 index 0000000000..030d90fa58 --- /dev/null +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsSkeleton.svelte @@ -0,0 +1,93 @@ + + + +
+ +
+
+
+ + +
+ + + +
+
+ + +
+ + +
+ + + + + +
+ + +
+
+ + + + + + + +
+
+
+ + +
+
+ {#each [0, 1, 2] as _, index (index)} +
+
+ + + +
+ +
+ + + +
+
+ {/each} +
+ +
+ + +
+ + + + + +
+ + +
+
+ + +
+ + + + + + + +
+
diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/index.ts b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/index.ts new file mode 100644 index 0000000000..2420bd5ef2 --- /dev/null +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/index.ts @@ -0,0 +1,72 @@ +/** + * + * MODELS DISCOVER - DETAILS + * + * The right-hand detail pane of the discover view: header (avatar, name, stats, metadata + * chips, capability badges), the download options area and the model-card README. + * + */ + +/** + * **ModelsDiscoverDetails** - Model detail view + * + * Detail pane for the selected model. Loads its own data (details + GGUF file + * list) from HuggingFaceService based on the `modelId` route param. + */ +export { default as ModelsDiscoverDetails } from './ModelsDiscoverDetails.svelte'; + +/** + * **ModelsDiscoverDetailsHeader** - Detail view header + * + * Shows the model avatar (base org + quant org corner badge), name, base model + * info, stats, metadata chips and capability badges. + */ +export { default as ModelsDiscoverDetailsHeader } from './ModelsDiscoverDetailsHeader.svelte'; + +/** + * **ModelsDiscoverDetailsHfHubStats** - HuggingFace Hub stats row + * + * Downloads, likes and last-updated for the viewed model, formatted the way + * the Hub shows them. + */ +export { default as ModelsDiscoverDetailsHfHubStats } from './ModelsDiscoverDetailsHfHubStats.svelte'; + +/** + * **ModelsDiscoverDetailsSkeleton** - Detail view loading skeleton + * + * Static placeholder matching the detail layout: header with avatar and name, + * metadata chips, the download options box and readme text lines. + */ +export { default as ModelsDiscoverDetailsSkeleton } from './ModelsDiscoverDetailsSkeleton.svelte'; + +/** + * **ModelsDiscoverDetailsReadme** - Detail view README + * + * Renders the model card README as markdown. + */ +export { default as ModelsDiscoverDetailsReadme } from './ModelsDiscoverDetailsReadme.svelte'; + +/** + * **ModelsDiscoverDetailsMetadata** - Detail view metadata row + * + * The detail view's metadata chips (model size, context, architecture, license, + * chat template). + */ +export { default as ModelsDiscoverDetailsMetadata } from './ModelsDiscoverDetailsMetadata.svelte'; + +/** + * **ModelsDiscoverDetailsMetadataItem** - Single metadata chip + * + * One label | value chip of the detail view's metadata row (model size, + * context, architecture, license). + */ +export { default as ModelsDiscoverDetailsMetadataItem } from './ModelsDiscoverDetailsMetadataItem.svelte'; + +/** + * **ModelsDiscoverChatTemplateDialog** - Chat template viewer + * + * Shows the model's chat template in a scrollable dialog with a copy button. + */ +export { default as ModelsDiscoverChatTemplateDialog } from './ModelsDiscoverChatTemplateDialog.svelte'; + +export * from './ModelsDiscoverDetailsDownloadOptions'; diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDownloadProgressBar.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDownloadProgressBar.svelte index c3a89c0ba4..6f4973c12e 100644 --- a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDownloadProgressBar.svelte +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDownloadProgressBar.svelte @@ -18,14 +18,14 @@ {#if overlay}
{:else}
diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverList/ModelsDiscoverList.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverList/ModelsDiscoverList.svelte new file mode 100644 index 0000000000..5ebff7230d --- /dev/null +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverList/ModelsDiscoverList.svelte @@ -0,0 +1,41 @@ + + +
    + {#if loading} + {#each Array(loadingSkeletonRowsCount) as _, index (index)} + + {/each} + {:else} + {#each models as model (model.id)} + + {/each} + {/if} +
diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverList/ModelsDiscoverListItem.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverList/ModelsDiscoverListItem.svelte new file mode 100644 index 0000000000..3a496fd22f --- /dev/null +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverList/ModelsDiscoverListItem.svelte @@ -0,0 +1,129 @@ + + +
  • + +
  • diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverList/ModelsDiscoverListItemSkeleton.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverList/ModelsDiscoverListItemSkeleton.svelte new file mode 100644 index 0000000000..851abc5238 --- /dev/null +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverList/ModelsDiscoverListItemSkeleton.svelte @@ -0,0 +1,39 @@ + + + +
  • +
    + + +
    + + +
    + {#each badgeWidths as width, i (i)} + + {/each} +
    +
    +
    +
  • diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverList/ModelsDiscoverListSearch.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverList/ModelsDiscoverListSearch.svelte new file mode 100644 index 0000000000..dce949a0f6 --- /dev/null +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverList/ModelsDiscoverListSearch.svelte @@ -0,0 +1,26 @@ + + +
    + handleInput(v)} {placeholder} size="sm" /> +
    diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverList/index.ts b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverList/index.ts new file mode 100644 index 0000000000..d888343a15 --- /dev/null +++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverList/index.ts @@ -0,0 +1,38 @@ +/** + * + * MODELS DISCOVER - LIST + * + * The discover sidebar column: a debounced search field above a navigable list of + * HuggingFace GGUF models, with skeleton rows while loading. + * + */ + +/** + * **ModelsDiscoverList** - Sidebar model list + * + * Renders the discover model list as a navigable column. Each row links to the + * model's detail and highlights the active one. + */ +export { default as ModelsDiscoverList } from './ModelsDiscoverList.svelte'; + +/** + * **ModelsDiscoverListSearch** - Sidebar search input + * + * Debounced search field for the model list. + */ +export { default as ModelsDiscoverListSearch } from './ModelsDiscoverListSearch.svelte'; + +/** + * **ModelsDiscoverListItem** - Single sidebar row + * + * One model entry in the discover sidebar list, selectable via `onSelect`. + */ +export { default as ModelsDiscoverListItem } from './ModelsDiscoverListItem.svelte'; + +/** + * **ModelsDiscoverListItemSkeleton** - Skeleton sidebar row + * + * Pulsing placeholder matching a ModelsDiscoverListItem row, shown while the + * list is loading. + */ +export { default as ModelsDiscoverListItemSkeleton } from './ModelsDiscoverListItemSkeleton.svelte'; diff --git a/tools/ui/src/lib/components/app/models/discover/index.ts b/tools/ui/src/lib/components/app/models/discover/index.ts new file mode 100644 index 0000000000..5926b75153 --- /dev/null +++ b/tools/ui/src/lib/components/app/models/discover/index.ts @@ -0,0 +1,39 @@ +/** + * + * MODELS DISCOVER + * + * Components for the Models Discover view: a sidebar search + list of + * HuggingFace GGUF models and a detail view for the selected model, used as the + * body of the discovery dialog. The list and detail trees live in their own + * subfolders; this barrel re-exports them alongside the shared leaves. + * + */ + +/** + * **ModelsDiscover** - Models discover explorer + * + * The complete discovery layout: a sidebar search + model list on the left and a + * detail view for the selected model on the right. Used as the body of the + * discovery dialog. + */ +export { default as ModelsDiscover } from './ModelsDiscover.svelte'; + +/** + * **ModelsDiscoverAvatar** - Org avatar for a model row + * + * Shows the org's avatar image, falling back to a monogram on a stable hue + * derived from the org name when the image fails to load. Shared by the list, + * the detail header and the model selector rows. + */ +export { default as ModelsDiscoverAvatar } from './ModelsDiscoverAvatar.svelte'; + +/** + * **ModelsDiscoverDownloadProgressBar** - Thin download progress bar + * + * Normalizes bytes to a 0..100% bar; can pin to the bottom edge as an overlay. + * Shared by the quant chips and the model selector's download rows. + */ +export { default as ModelsDiscoverDownloadProgressBar } from './ModelsDiscoverDownloadProgressBar.svelte'; + +export * from './ModelsDiscoverList'; +export * from './ModelsDiscoverDetails'; diff --git a/tools/ui/src/lib/components/ui/select/select-content.svelte b/tools/ui/src/lib/components/ui/select/select-content.svelte index f8792f8a9d..a8263819a1 100644 --- a/tools/ui/src/lib/components/ui/select/select-content.svelte +++ b/tools/ui/src/lib/components/ui/select/select-content.svelte @@ -88,7 +88,7 @@ {@render children?.()} diff --git a/tools/ui/src/lib/components/ui/select/select-trigger.svelte b/tools/ui/src/lib/components/ui/select/select-trigger.svelte index 9f642819ec..de3abcdf21 100644 --- a/tools/ui/src/lib/components/ui/select/select-trigger.svelte +++ b/tools/ui/src/lib/components/ui/select/select-trigger.svelte @@ -11,20 +11,26 @@ variant = 'default', ...restProps }: WithoutChild & { - size?: 'sm' | 'default'; + size?: 'xs' | 'sm' | 'default'; variant?: 'default' | 'plain'; } = $props(); + // Super small trigger: fits its selected value, for dense inline use. + const xsClasses = + "group flex h-6 w-fit items-center justify-between gap-1 rounded-md border border-border/30 bg-muted/60 px-2 py-0 text-xs whitespace-nowrap outline-none select-none transition-colors hover:bg-muted/80 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground dark:border-border/20 dark:bg-muted/75 dark:hover:bg-muted [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='text-'])]:text-muted-foreground"; + const baseClasses = $derived( variant === 'plain' ? "group inline-flex w-full items-center justify-end gap-2 whitespace-nowrap px-0 py-0 text-sm font-medium text-muted-foreground transition-colors focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3 [&_svg:not([class*='text-'])]:text-muted-foreground" - : "flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground" + : size === 'xs' + ? xsClasses + : "flex w-fit items-center justify-between gap-2 rounded-md border border-border/30 bg-muted/60 px-3 py-2 text-sm whitespace-nowrap shadow-sm transition-colors outline-none select-none hover:bg-muted/80 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:border-border/20 dark:bg-muted/75 dark:hover:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 [&_svg:not([class*='text-'])]:text-muted-foreground" ); const chevronClasses = $derived( - variant === 'plain' + variant === 'plain' || size === 'xs' ? 'size-3 opacity-60 transition-transform group-data-[state=open]:-rotate-180' - : 'size-4 opacity-50' + : 'size-3.5 opacity-60 transition-transform group-data-[state=open]:-rotate-180' ); diff --git a/tools/ui/src/lib/constants/index.ts b/tools/ui/src/lib/constants/index.ts index 04d5ab779d..6412511736 100644 --- a/tools/ui/src/lib/constants/index.ts +++ b/tools/ui/src/lib/constants/index.ts @@ -47,6 +47,7 @@ export * from './path-display.constants'; export * from './model-id.constants'; export * from './model-loading.constants'; export * from './models-discover.constants'; +export * from './models-discover-download.constants'; export * from './model-compatibility.constants'; export * from './huggingface.constants'; export * from './precision.constants'; diff --git a/tools/ui/src/lib/constants/models-discover-download.constants.ts b/tools/ui/src/lib/constants/models-discover-download.constants.ts new file mode 100644 index 0000000000..e38dd1efbe --- /dev/null +++ b/tools/ui/src/lib/constants/models-discover-download.constants.ts @@ -0,0 +1,54 @@ +/** + * Models Discover - download options constants. + * + * Shared values backing the download-options area of the details pane: the + * serve `--spec-type` mapping, the bit-depth buckets, and the standalone + * command builder. Kept here (not component-local) so they sit with the rest + * of the discover domain constants. + */ + +import type { ModelSidecar } from '$lib/constants'; +import { ModelAuxSidecar, ModelDraftSidecar } from '$lib/enums'; + +/** + * `--spec-type` value for each draft sidecar; aux sidecars (mmproj, imatrix) + * carry none and stay empty. + */ +export const SPEC_TYPE: Record = { + [ModelAuxSidecar.IMATRIX]: '', + [ModelAuxSidecar.MMPROJ]: '', + [ModelDraftSidecar.DFLASH]: 'draft-dflash', + [ModelDraftSidecar.DSPARK]: 'draft-dspark', + [ModelDraftSidecar.EAGLE3]: 'draft-eagle3', + [ModelDraftSidecar.MTP]: 'draft-mtp' +}; + +/** Bit-depth bucket for files that carry no quant token; rendered as "Other". */ +export const OTHER_BIT_DEPTH = 99; + +/** Label for the OTHER_BIT_DEPTH bucket. */ +export const OTHER_BIT_DEPTH_LABEL = 'Other'; + +/** Suffix appended after a bit-depth number to label a row, e.g. `4-bit`. */ +export const BIT_DEPTH_LABEL_SUFFIX = '-bit'; + +/** + * Fixed tokens of the standalone `llama serve` command preview, so the command + * builder and its rendered spans share one source of the CLI spelling. + */ +export const SERVE_COMMAND = { + BIN: 'llama', + /** Draft model repo:tag argument (download sidecar / draft weights). */ + DRAFT_FLAG: '-hfd', + /** Main model repo:tag argument. */ + MODEL_FLAG: '-hf', + /** Speculative-decoding type argument for the draft. */ + SPEC_TYPE_FLAG: '--spec-type', + SUBCOMMAND: 'serve' +} as const; + +/** Bit depth preferred for the command's default base quant. */ +export const DEFAULT_BASE_BIT_DEPTH = 4; + +/** Label shown for a draft-sidecar chip whose quant is unknown. */ +export const DRAFT_FILE_LABEL = 'draft'; diff --git a/tools/ui/src/lib/constants/models-discover.constants.ts b/tools/ui/src/lib/constants/models-discover.constants.ts index f2a6a9463c..b0a0abc568 100644 --- a/tools/ui/src/lib/constants/models-discover.constants.ts +++ b/tools/ui/src/lib/constants/models-discover.constants.ts @@ -6,3 +6,9 @@ /** llama.app model catalog used as the default model list. Online-only source; the discover feature requires an internet connection anyway. */ export const MODELS_DISCOVER_CATALOG_URL = 'https://llama.app/v1/catalog.json'; + +/** + * Catalog repos fetched in parallel per batch; small on purpose so the HF API + * is not hit with the whole catalog at once. + */ +export const MODELS_DISCOVER_CATALOG_BATCH = 4; diff --git a/tools/ui/src/lib/enums/index.ts b/tools/ui/src/lib/enums/index.ts index 583187d964..6b529b66e4 100644 --- a/tools/ui/src/lib/enums/index.ts +++ b/tools/ui/src/lib/enums/index.ts @@ -69,7 +69,13 @@ export { JsonSchemaType } from './mcp.enums'; -export { ModelAuxSidecar, ModelCapability, ModelDraftSidecar, ModelModality } from './model.enums'; +export { + ModelAuxSidecar, + ModelCapability, + ModelDraftSidecar, + ModelModality, + ModelSelectableFileKind +} from './model.enums'; export { ModelDownloadConfirmAction, ModelDownloadStopRequest } from './model.enums'; diff --git a/tools/ui/src/lib/enums/model.enums.ts b/tools/ui/src/lib/enums/model.enums.ts index f2a0cc3552..0b54ec1631 100644 --- a/tools/ui/src/lib/enums/model.enums.ts +++ b/tools/ui/src/lib/enums/model.enums.ts @@ -36,6 +36,16 @@ export enum ModelAuxSidecar { MMPROJ = 'mmproj' } +/** + * Role of a selectable GGUF in the download options: the main weights, a + * speculative-decoding draft sidecar, or an auxiliary sidecar (mmproj). + */ +export enum ModelSelectableFileKind { + AUX = 'aux', + DRAFT = 'draft', + MAIN = 'main' +} + /** * Why an in-flight download is being stopped, so the terminal `download_failed` * feed event can be attributed: a user pause (resumable) or a user cancel diff --git a/tools/ui/src/lib/stores/index.ts b/tools/ui/src/lib/stores/index.ts index b571699072..fcab8a8064 100644 --- a/tools/ui/src/lib/stores/index.ts +++ b/tools/ui/src/lib/stores/index.ts @@ -40,6 +40,9 @@ export { mcpStore } from './mcp/index.svelte'; // MODELS export { modelsStore } from './models/index.svelte'; +// MODELS DISCOVER (HuggingFace browse state for the discover dialog) +export { modelsDiscoverStore } from './models-discover/index.svelte'; + // SERVER export { serverStore } from './server.svelte'; diff --git a/tools/ui/src/lib/stores/models-discover/index.svelte.ts b/tools/ui/src/lib/stores/models-discover/index.svelte.ts new file mode 100644 index 0000000000..28cc66f00d --- /dev/null +++ b/tools/ui/src/lib/stores/models-discover/index.svelte.ts @@ -0,0 +1,282 @@ +/** + * modelsDiscoverStore - Models Discover browse state + * + * Owns the HuggingFace GGUF model list shown in the discover sidebar + * (the Discover tab of DialogManageModels). By default the list is the curated catalog set; + * search replaces it with matches across all of HuggingFace. Both paths fetch + * the same fields, so a row renders the same badges and sizes either way. + */ + +import { MODELS_DISCOVER_CATALOG_BATCH } from '$lib/constants'; +import { HuggingFaceService } from '$lib/services'; +import type { + HfCatalogBuild, + HfCatalogEntry, + HfModelInfo, + HfModelSibling +} from '$lib/types/huggingface'; +import { isAuxSidecar } from '$lib/utils'; +import { SvelteMap } from 'svelte/reactivity'; + +/** Min/max GGUF file size (bytes) across the quants of one repo. */ +export interface ModelsDiscoverSizeRange { + max: number; + min: number; +} + +class ModelsDiscoverStore { + error = $state(null); + models = $state([]); + /** First model in the list - discover auto-opens this one. */ + firstModel = $derived(this.models[0] ?? null); + + loading = $state(false); + /** True while a search is in flight; only the newest request owns this flag. */ + searching = $state(false); + + private catalog: HfCatalogEntry[] = []; + /** Repo id -> size range, for catalog rows and lazily measured search rows. */ + private catalogSizeRanges = new SvelteMap(); + private defaultModels: HfModelInfo[] = []; + private fetched = false; + private searchRequestId = 0; + /** In-flight `sizeRange()` lookups, keyed by repo id. */ + private sizeRangePending = new Map>(); + + /** + * Cached size range for a repo, without measuring: the synchronous part of + * `sizeRange()`, for rendering a row before its measurement resolves. + */ + cachedSizeRangeFor(modelId: string): ModelsDiscoverSizeRange | undefined { + return this.catalogSizeRanges.get(modelId); + } + + /** + * 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, one repo per catalog + * size in display order (newest family first). Every repo is fetched by ID + * with its file tree, so the rows carry chat-template capabilities, context + * length and a real size range - the same data a search result gets. + * No-op when already loaded or in flight. + */ + async fetch(): Promise { + if (this.loading || this.fetched) return; + + this.loading = true; + this.error = null; + + try { + const catalog = await HuggingFaceService.getCatalog(); + + this.catalog = catalog; + + const builds = this.catalogBuilds(); + const failedBuilds: HfCatalogBuild[] = []; + + this.defaultModels = []; + + // fetch in small batches: not every repo hits the HF API at once, + // and the rows land as they arrive instead of all at the end + for (let i = 0; i < builds.length; i += MODELS_DISCOVER_CATALOG_BATCH) { + const batch = await Promise.all( + builds.slice(i, i + MODELS_DISCOVER_CATALOG_BATCH).map(async (build) => { + const [info, tree] = await Promise.all([ + HuggingFaceService.getDetails(build.repo), + HuggingFaceService.getTree(build.repo) + ]); + + return { build, info, tree }; + }) + ); + + for (const { build, info, tree } of batch) { + if (!info) { + failedBuilds.push(build); + + continue; + } + + this.catalogSizeRanges.set(build.repo, this.sizeRangeFor(build, tree)); + + // the catalog repo id, not the HF response `id`, drives selection + this.defaultModels.push({ ...info, id: build.repo, modelId: build.repo } as HfModelInfo); + } + + // show what has landed, unless a search owns the list right now + if (!this.searching) { + this.models = [...this.defaultModels]; + } + } + + // stay incomplete when some repos failed, so the next mount retries + // them; succeeded repos are served from the service cache for free + this.fetched = failedBuilds.length === 0; + } catch (err) { + this.error = err instanceof Error ? err.message : 'Failed to fetch models'; + } finally { + this.loading = false; + } + } + + /** Replace the list with search results; an empty query restores the default list. */ + async search(query: string): Promise { + const trimmed = query.trim(); + const requestId = ++this.searchRequestId; + + if (!trimmed) { + this.searching = false; + this.models = this.defaultModels; + this.error = null; + + return; + } + + this.searching = true; + + try { + const results = await HuggingFaceService.searchByQuery(trimmed, { limit: 50 }); + + if (requestId !== this.searchRequestId) return; + + this.models = results; + this.error = null; + } catch (err) { + if (requestId !== this.searchRequestId) return; + + this.error = err instanceof Error ? err.message : 'Search failed'; + } finally { + if (requestId === this.searchRequestId) this.searching = false; + } + } + + /** + * Size range for a repo not measured yet - a search result, or a catalog + * repo whose tree came back empty. Fetches the file tree once per repo and + * caches it, so remounting a row (scrolling, searching back) is free. + */ + sizeRange(modelId: string): Promise { + const cached = this.catalogSizeRanges.get(modelId); + + if (cached) return Promise.resolve(cached); + + const pending = this.sizeRangePending.get(modelId); + + if (pending) return pending; + + const request = (async () => { + const tree = await HuggingFaceService.getTree(modelId); + const range = this.sizeRangeOfMainQuants(tree); + + if (range) this.catalogSizeRanges.set(modelId, range); + + return range; + })() + .catch(() => undefined) + .finally(() => this.sizeRangePending.delete(modelId)); + + this.sizeRangePending.set(modelId, request); + + return request; + } + + /** + * Bytes of every quant the catalog lists under this repo, parsed from the + * `size` strings when a build carries no `sizeBytes`. Never empty: an + * unparsable entry contributes the build's own size. + */ + private buildSizeBytes(build: HfCatalogBuild): number[] { + const sizes = this.catalog + .flatMap((entry) => entry.sizes) + .flatMap((size) => size.builds.filter((b) => b.repo === build.repo)) + .map((b) => b.sizeBytes ?? HuggingFaceService.parseSizeBytes(b.size)) + .filter((bytes): bytes is number => Boolean(bytes) && bytes > 0); + + return sizes.length > 0 ? sizes : [build.sizeBytes ?? 0]; + } + + /** + * One build per catalog size, newest family first (by release date). + * Prefers the official ggml-org repo, falling back to the first build so + * families published only by other orgs (mistralai, unsloth) still show up. + * Returns an empty array when the catalog is empty. + */ + private catalogBuilds(): HfCatalogBuild[] { + return [...this.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/')) ?? size.builds[0]; + + return build ? [build] : []; + }) + ); + } + + /** Byte sizes of every non-sidecar quant file in a tree, shards collapsed. */ + private quantSizesOf(tree: HfModelSibling[]): number[] { + return HuggingFaceService.collapseGgufShards( + HuggingFaceService.filterByExtension(tree, '.gguf') + ) + .filter((f) => { + const { quant, sidecar } = HuggingFaceService.extractQuantMeta(f.path) ?? {}; + + return Boolean(quant) && (sidecar === null || sidecar === undefined); + }) + .map((f) => f.size ?? 0) + .filter((size) => size > 0); + } + + /** + * Size range of one catalog build: every quant in the repo's file tree, plus + * the draft sidecars those files carry (mtp, dflash, ...) so the downloaded + * model fits within the range. Falls back to the catalog `size` / `sizeBytes` + * strings when the tree yielded nothing (partial fetch, sharded-only repo). + */ + private sizeRangeFor(build: HfCatalogBuild, tree: HfModelSibling[]): ModelsDiscoverSizeRange { + const quantSizes = this.quantSizesOf(tree); + + if (quantSizes.length === 0) { + const listed = this.buildSizeBytes(build); + + return { max: Math.max(...listed), min: Math.min(...listed) }; + } + + const draftSizes = tree + .filter((f) => { + const sidecar = HuggingFaceService.extractQuantMeta(f.path)?.sidecar; + + return sidecar !== null && sidecar !== undefined && !isAuxSidecar(sidecar); + }) + .map((f) => f.size ?? 0) + .filter((size) => size > 0); + const extra = draftSizes.length > 0 ? Math.max(...draftSizes) : 0; + + return { + max: Math.max(...quantSizes) + extra, + min: Math.min(...quantSizes) + (draftSizes.length > 0 ? Math.min(...draftSizes) : 0) + }; + } + + /** + * Size range across the main-model quants of a file tree, draft sidecars + * excluded (they only widen the range when a row advertises them). + */ + private sizeRangeOfMainQuants(tree: HfModelSibling[]): ModelsDiscoverSizeRange | undefined { + const sizes = this.quantSizesOf(tree); + + if (sizes.length === 0) return undefined; + + return { max: Math.max(...sizes), min: Math.min(...sizes) }; + } +} + +export const modelsDiscoverStore = new ModelsDiscoverStore(); diff --git a/tools/ui/src/lib/types/index.ts b/tools/ui/src/lib/types/index.ts index 453af9bb80..7f35a13633 100644 --- a/tools/ui/src/lib/types/index.ts +++ b/tools/ui/src/lib/types/index.ts @@ -111,6 +111,14 @@ export type { ModalityCapabilities } from './models'; +// Models discover types +export type { + ModelBitDepthRow, + ModelDownloadEntryState, + ModelQuantOption, + ModelSelectableFile +} from './models-discover'; + // Settings types export type { SettingsConfigValue, diff --git a/tools/ui/src/lib/types/models-discover.d.ts b/tools/ui/src/lib/types/models-discover.d.ts new file mode 100644 index 0000000000..f9812fea85 --- /dev/null +++ b/tools/ui/src/lib/types/models-discover.d.ts @@ -0,0 +1,30 @@ +import type { ModelSelectableFileKind } from '$lib/enums'; +import type { HfModelSibling } from '$lib/types/huggingface'; + +/** + * Option of a quant `