mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-18 16:55:05 +02:00
feat: WIP
This commit is contained in:
+9
-5
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Boxes } from '@lucide/svelte';
|
||||
import { SearchInput } from '$lib/components/app';
|
||||
import { ModelsHubList, ModelsHubModelDetails } from '$lib/components/app/models-hub';
|
||||
import { ModelsDiscoverList, ModelsDiscoverDetails } from '$lib/components/app/models/discover';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { modelsHubStore } from '$lib/stores';
|
||||
import { untrack } from 'svelte';
|
||||
@@ -59,11 +60,14 @@
|
||||
class="flex flex-col md:h-[calc(100vh-4rem)]! md:max-h-240! md:w-[calc(100vw-4rem)]! md:max-w-360!"
|
||||
>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Discover Models</Dialog.Title>
|
||||
<Dialog.Title class="flex items-center gap-2">
|
||||
<Boxes class="h-4 w-4" />
|
||||
Discover Models
|
||||
</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="flex min-h-0 flex-1">
|
||||
<aside class="flex w-72 shrink-0 flex-col border-r border-border/40">
|
||||
<aside class="flex w-88 shrink-0 flex-col border-r border-border/40">
|
||||
<div class="p-2">
|
||||
<SearchInput
|
||||
bind:value={searchQuery}
|
||||
@@ -80,7 +84,7 @@
|
||||
{:else if modelsHubStore.models.length === 0}
|
||||
<p class="p-4 text-sm text-muted-foreground">No models found</p>
|
||||
{:else}
|
||||
<ModelsHubList
|
||||
<ModelsDiscoverList
|
||||
models={modelsHubStore.models}
|
||||
activeId={selectedId}
|
||||
showBaseModelAvatar
|
||||
@@ -92,7 +96,7 @@
|
||||
|
||||
<main class="min-w-0 flex-1 overflow-y-auto">
|
||||
{#if selectedId}
|
||||
<ModelsHubModelDetails modelId={selectedId} />
|
||||
<ModelsDiscoverDetails modelId={selectedId} />
|
||||
{/if}
|
||||
</main>
|
||||
</div>
|
||||
@@ -27,14 +27,14 @@ export { default as DialogMcpServerAddNew } from './DialogMcpServerAddNew.svelte
|
||||
export { default as DialogMcpServers } from './DialogMcpServers.svelte';
|
||||
|
||||
/**
|
||||
* **DialogModelsHub** - Models Hub discovery dialog
|
||||
* **DialogModelsDiscover** - Models Hub discovery dialog
|
||||
*
|
||||
* Two-pane HuggingFace GGUF browser in a modal dialog: a sidebar model list
|
||||
* (ModelsHubList) and a detail view (ModelsHubModelDetails). Always opens the
|
||||
* (ModelsDiscoverList) and a detail view (ModelsDiscoverDetails). Always opens the
|
||||
* first model. Used for discovery and downloading; the `/models-hub` route is
|
||||
* reserved for model management.
|
||||
*/
|
||||
export { default as DialogModelsHub } from './DialogModelsHub.svelte';
|
||||
export { default as DialogModelsDiscover } from './DialogModelsDiscover.svelte';
|
||||
|
||||
/**
|
||||
* **DialogSettingsChat** - Chat settings shown in a modal dialog
|
||||
|
||||
@@ -8,6 +8,6 @@ export * from './mcp';
|
||||
export * from './misc';
|
||||
export * from './settings';
|
||||
export * from './models';
|
||||
export * from './models-hub';
|
||||
export * from './models/discover';
|
||||
export * from './navigation';
|
||||
export * from './server';
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Download, Eye, Heart } from '@lucide/svelte';
|
||||
import { HuggingFaceService } from '$lib/services';
|
||||
import type { HfModelInfo } from '$lib/types/huggingface';
|
||||
|
||||
interface Props {
|
||||
model: HfModelInfo;
|
||||
active?: boolean;
|
||||
/** Show the original (base) model's org avatar instead of the repo's org. */
|
||||
showBaseModelAvatar?: boolean;
|
||||
onSelect?: (modelId: string) => void;
|
||||
}
|
||||
|
||||
let { model, active = false, showBaseModelAvatar = false, onSelect }: Props = $props();
|
||||
|
||||
let org = $derived(model.id.split('/')[0] ?? model.id);
|
||||
let name = $derived(model.id.split('/')[1] ?? model.id);
|
||||
let params = $derived(HuggingFaceService.parseParamCount(model.id));
|
||||
let avatarError = $state(false);
|
||||
|
||||
// Org whose avatar is shown: the base model's org when showBaseModelAvatar
|
||||
// (e.g. the Qwen logo for ggml-org/Qwen3.8-27B-GGUF), else the repo's org.
|
||||
let avatarOrg = $derived.by(() => {
|
||||
if (!showBaseModelAvatar) return org;
|
||||
|
||||
const base = HuggingFaceService.getBaseModels(model)[0];
|
||||
|
||||
return base?.split('/')[0] || org;
|
||||
});
|
||||
// Vision = a multimodal pipeline tag or an mmproj projector in the repo.
|
||||
let hasVision = $derived(
|
||||
model.pipeline_tag === 'image-text-to-text' ||
|
||||
Boolean(model.siblings?.some((s) => s.rfilename.toLowerCase().includes('mmproj')))
|
||||
);
|
||||
|
||||
// Monogram fallback: avatar org initial on a hue derived from its name, so
|
||||
// each org gets a stable distinct color.
|
||||
let hue = $derived.by(() => {
|
||||
let h = 0;
|
||||
|
||||
for (let i = 0; i < avatarOrg.length; i++) h = (h * 31 + avatarOrg.charCodeAt(i)) >>> 0;
|
||||
|
||||
return h % 360;
|
||||
});
|
||||
</script>
|
||||
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => onSelect?.(model.id)}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
class="flex w-full cursor-pointer items-start gap-2.5 rounded-lg p-2.5 text-left transition-colors {active
|
||||
? 'bg-primary/10 hover:bg-primary/15'
|
||||
: 'hover:bg-muted/60'}"
|
||||
>
|
||||
{#if avatarError}
|
||||
<span
|
||||
class="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-sm font-semibold text-white"
|
||||
style="background-color: hsl({hue} 60% 45%)"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{avatarOrg.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
{:else}
|
||||
<img
|
||||
src={HuggingFaceService.getAvatarUrl(avatarOrg)}
|
||||
onerror={() => (avatarError = true)}
|
||||
class="mt-0.5 h-9 w-9 shrink-0 rounded-md bg-gray-200 p-0.5"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="flex items-center justify-between gap-2">
|
||||
<span class="truncate text-sm font-medium">{name}</span>
|
||||
{#if hasVision}
|
||||
<Eye class="h-3.5 w-3.5 shrink-0 text-muted-foreground" aria-label="Vision" />
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<span class="mt-0.5 block truncate text-xs text-muted-foreground">
|
||||
{org}
|
||||
{#if model.lastModified}
|
||||
· {HuggingFaceService.formatRelativeTime(model.lastModified)}
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<span class="mt-1 flex items-center gap-2.5 text-xs text-muted-foreground">
|
||||
{#if params}
|
||||
<span class="font-medium tabular-nums">{params}</span>
|
||||
{/if}
|
||||
<span class="flex items-center gap-1">
|
||||
<Download class="h-3 w-3" />
|
||||
{HuggingFaceService.formatDownloads(model.downloads)}
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<Heart class="h-3 w-3" />
|
||||
{HuggingFaceService.formatLikes(model.likes)}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Image, Lightbulb, Mic, Video } from '@lucide/svelte';
|
||||
import { type DraftVariant } from '$lib/constants';
|
||||
import { TruncatedText } from '$lib/components/app';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { ModelsService } from '$lib/services/models.service';
|
||||
@@ -17,12 +18,14 @@
|
||||
tags?: string[];
|
||||
modalities?: ModelModalities;
|
||||
supportsThinking?: boolean;
|
||||
draftVariants?: DraftVariant[];
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
aliases,
|
||||
class: className = '',
|
||||
draftVariants,
|
||||
hideOrgName = false,
|
||||
hideQuantization,
|
||||
hideTags,
|
||||
@@ -53,6 +56,7 @@
|
||||
|
||||
let uniqueAliases = $derived([...new Set(aliases ?? [])]);
|
||||
let uniqueTags = $derived([...new Set([...(parsed.tags ?? []), ...(tags ?? [])])]);
|
||||
let uniqueDraftVariants = $derived([...new Set(draftVariants ?? [])]);
|
||||
let hasModalityIcons = $derived(
|
||||
supportsThinking || modalities?.vision || modalities?.video || modalities?.audio
|
||||
);
|
||||
@@ -76,6 +80,12 @@
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#each uniqueDraftVariants as variant (variant)}
|
||||
<span class={variantBadgeClass} title={`${variant.toUpperCase()} draft model available`}>
|
||||
{variant}
|
||||
</span>
|
||||
{/each}
|
||||
|
||||
{#if parsed.params}
|
||||
<span class={badgeClass}>
|
||||
{parsed.params}{parsed.activatedParams ? `-${parsed.activatedParams}` : ''}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<script lang="ts">
|
||||
import { HuggingFaceService } from '$lib/services';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
|
||||
interface Props {
|
||||
/** Org whose avatar is shown (may differ from the repo's org for base models). */
|
||||
org: string;
|
||||
/** Repo's own org, shown as a small corner badge when provided. */
|
||||
quantOrg?: string;
|
||||
}
|
||||
|
||||
let { org, quantOrg }: Props = $props();
|
||||
|
||||
let avatarError = $state(false);
|
||||
let quantError = $state(false);
|
||||
|
||||
// Monogram fallback: org initial on a hue derived from its name, so each org
|
||||
// gets a stable distinct color.
|
||||
let hue = $derived.by(() => {
|
||||
let h = 0;
|
||||
|
||||
for (let i = 0; i < org.length; i++) h = (h * 31 + org.charCodeAt(i)) >>> 0;
|
||||
|
||||
return h % 360;
|
||||
});
|
||||
|
||||
let quantHue = $derived.by(() => {
|
||||
const name = quantOrg ?? '';
|
||||
let h = 0;
|
||||
|
||||
for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0;
|
||||
|
||||
return h % 360;
|
||||
});
|
||||
</script>
|
||||
|
||||
<span class="relative mt-0.5 inline-flex shrink-0">
|
||||
{#if avatarError}
|
||||
<span
|
||||
class="flex h-9 w-9 items-center justify-center rounded-md text-sm font-semibold text-white"
|
||||
style="background-color: hsl({hue} 60% 45%)"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{org.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
{:else}
|
||||
<img
|
||||
src={HuggingFaceService.getAvatarUrl(org)}
|
||||
onerror={() => (avatarError = true)}
|
||||
class="h-9 w-9 rounded-md bg-gray-200 p-0.5"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if quantOrg}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger
|
||||
class="absolute -bottom-0.75 -right-0.75 h-4.25 w-4.25 overflow-hidden rounded-full border border-background"
|
||||
>
|
||||
{#if quantError}
|
||||
<span
|
||||
class="flex h-full w-full items-center justify-center rounded-full text-[8px] font-semibold text-white"
|
||||
style="background-color: hsl({quantHue} 60% 45%)"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{quantOrg.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
{:else}
|
||||
<img
|
||||
src={HuggingFaceService.getAvatarUrl(quantOrg)}
|
||||
onerror={() => (quantError = true)}
|
||||
class="h-full w-full rounded-full bg-gray-200"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
/>
|
||||
{/if}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>{quantOrg}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
</span>
|
||||
@@ -0,0 +1,74 @@
|
||||
<script lang="ts">
|
||||
import { Download, Heart } from '@lucide/svelte';
|
||||
import { type DraftVariant } from '$lib/constants';
|
||||
import { HuggingFaceService } from '$lib/services';
|
||||
import { formatParameters } from '$lib/utils';
|
||||
import type { ModelModalities } from '$lib/types/models';
|
||||
import type { HfModelInfo } from '$lib/types/huggingface';
|
||||
import ModelId from '../ModelId.svelte';
|
||||
|
||||
interface Props {
|
||||
model: HfModelInfo;
|
||||
}
|
||||
|
||||
let { model }: Props = $props();
|
||||
|
||||
let contextLength = $derived(model.gguf?.context_length);
|
||||
|
||||
// Modalities derived from HF metadata: vision from an mmproj sidecar or a
|
||||
// multimodal pipeline tag, audio/video from their pipeline tags.
|
||||
let modalities = $derived.by<ModelModalities>(() => {
|
||||
const tag = model.pipeline_tag ?? '';
|
||||
|
||||
const vision =
|
||||
['image-text-to-text', 'image-to-text', 'text-to-image', 'image-to-video'].includes(tag) ||
|
||||
Boolean(model.siblings?.some((s) => s.rfilename.toLowerCase().includes('mmproj')));
|
||||
|
||||
const audio = [
|
||||
'audio-classification',
|
||||
'audio-to-audio',
|
||||
'automatic-speech-recognition',
|
||||
'text-to-speech',
|
||||
'voice-activity-detection'
|
||||
].includes(tag);
|
||||
|
||||
const video = ['text-to-video', 'image-to-video', 'video-to-video'].includes(tag);
|
||||
|
||||
return { vision, audio, video };
|
||||
});
|
||||
|
||||
// Draft sidecars (mtp, dflash, dspark, eagle3) present in the repo, e.g.
|
||||
// speculative-decoding drafts. mmproj is excluded: it is vision, already
|
||||
// conveyed by the modalities.
|
||||
let draftVariants = $derived.by<DraftVariant[]>(() => {
|
||||
const set = new Set<DraftVariant>();
|
||||
|
||||
for (const sibling of model.siblings ?? []) {
|
||||
const variant = HuggingFaceService.extractQuantMeta(sibling.rfilename)?.variant;
|
||||
|
||||
if (variant && variant !== 'mmproj') set.add(variant);
|
||||
}
|
||||
|
||||
return [...set];
|
||||
});
|
||||
</script>
|
||||
|
||||
<span class="min-w-0 flex-1">
|
||||
<ModelId modelId={model.id} hideOrgName {modalities} {draftVariants} class="min-w-0" />
|
||||
|
||||
<span class="mt-0.5 block truncate text-xs text-muted-foreground">
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<Download class="h-3 w-3" />
|
||||
{HuggingFaceService.formatDownloads(model.downloads)}
|
||||
</span>
|
||||
<span class="ml-2.5 inline-flex items-center gap-1">
|
||||
<Heart class="h-3 w-3" />
|
||||
{HuggingFaceService.formatLikes(model.likes)}
|
||||
</span>
|
||||
{#if contextLength}
|
||||
<span class="ml-2.5 inline-flex items-center gap-1">
|
||||
{formatParameters(contextLength)} ctx
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
</span>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import { HuggingFaceService } from '$lib/services';
|
||||
import type { HfModelInfo } from '$lib/types/huggingface';
|
||||
import ModelsDiscoverAvatar from './ModelsDiscoverAvatar.svelte';
|
||||
import ModelsDiscoverInfo from './ModelsDiscoverInfo.svelte';
|
||||
|
||||
interface Props {
|
||||
model: HfModelInfo;
|
||||
active?: boolean;
|
||||
/** Show the original (base) model's org avatar instead of the repo's org. */
|
||||
showBaseModelAvatar?: boolean;
|
||||
onSelect?: (modelId: string) => void;
|
||||
}
|
||||
|
||||
let { model, active = false, showBaseModelAvatar = false, onSelect }: Props = $props();
|
||||
|
||||
let org = $derived(model.id.split('/')[0] ?? model.id);
|
||||
|
||||
// Org whose avatar is shown: the base model's org when showBaseModelAvatar
|
||||
// (e.g. the Qwen logo for ggml-org/Qwen3.8-27B-GGUF), else the repo's org.
|
||||
let avatarOrg = $derived.by(() => {
|
||||
if (!showBaseModelAvatar) return org;
|
||||
|
||||
const base = HuggingFaceService.getBaseModels(model)[0];
|
||||
|
||||
return base?.split('/')[0] || org;
|
||||
});
|
||||
</script>
|
||||
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => onSelect?.(model.id)}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
class="flex w-full cursor-pointer items-start gap-2.5 rounded-lg p-2.5 text-left transition-colors {active
|
||||
? 'bg-primary/10 hover:bg-primary/15'
|
||||
: 'hover:bg-muted/60'}"
|
||||
>
|
||||
<ModelsDiscoverAvatar org={avatarOrg} quantOrg={showBaseModelAvatar ? org : undefined} />
|
||||
<ModelsDiscoverInfo {model} />
|
||||
</button>
|
||||
</li>
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { HfModelInfo } from '$lib/types/huggingface';
|
||||
import ModelsHubListItem from './ModelsHubListItem.svelte';
|
||||
import ModelsDiscoverItem from './ModelsDiscoverItem.svelte';
|
||||
|
||||
interface Props {
|
||||
models: HfModelInfo[];
|
||||
@@ -15,6 +15,6 @@
|
||||
|
||||
<ul class="space-y-0.5 p-2">
|
||||
{#each models as model (model.id)}
|
||||
<ModelsHubListItem {model} active={model.id === activeId} {showBaseModelAvatar} {onSelect} />
|
||||
<ModelsDiscoverItem {model} active={model.id === activeId} {showBaseModelAvatar} {onSelect} />
|
||||
{/each}
|
||||
</ul>
|
||||
+22
-6
@@ -8,27 +8,43 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* **ModelsHubList** - Sidebar model list
|
||||
* **ModelsDiscoverList** - Sidebar model list
|
||||
*
|
||||
* Renders the hub's model list as a navigable column. Each row links to the
|
||||
* model's detail route and highlights the active one.
|
||||
*/
|
||||
export { default as ModelsHubList } from './ModelsHubList.svelte';
|
||||
export { default as ModelsDiscoverList } from './ModelsDiscoverList.svelte';
|
||||
|
||||
/**
|
||||
* **ModelsHubListItem** - Single sidebar row
|
||||
* **ModelsDiscoverItem** - Single sidebar row
|
||||
*
|
||||
* One model entry in the sidebar list. Links to `/models-hub/[org]/[model]`.
|
||||
*/
|
||||
export { default as ModelsHubListItem } from './ModelsHubListItem.svelte';
|
||||
export { default as ModelsDiscoverItem } from './ModelsDiscoverItem.svelte';
|
||||
|
||||
/**
|
||||
* **ModelsHubModelDetails** - Model detail view
|
||||
* **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.
|
||||
*/
|
||||
export { default as ModelsDiscoverAvatar } from './ModelsDiscoverAvatar.svelte';
|
||||
|
||||
/**
|
||||
* **ModelsDiscoverInfo** - Model name + metadata for a row
|
||||
*
|
||||
* Renders the model name via ModelId and the remaining metadata (org, last
|
||||
* modified, params, downloads, likes, vision) below it.
|
||||
*/
|
||||
export { default as ModelsDiscoverInfo } from './ModelsDiscoverInfo.svelte';
|
||||
|
||||
/**
|
||||
* **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 ModelsHubModelDetails } from './ModelsHubModelDetails.svelte';
|
||||
export { default as ModelsDiscoverDetails } from './ModelsDiscoverDetails.svelte';
|
||||
|
||||
/**
|
||||
* **DialogModelDownload** - Download confirmation / progress dialog
|
||||
@@ -42,7 +42,7 @@
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export { default as ModelsSelectorDropdown } from './ModelsSelector/ModelsSelectorDropdown.svelte';
|
||||
export { default as ModelsSelectorDropdown } from './selector/ModelsSelectorDropdown.svelte';
|
||||
|
||||
/**
|
||||
* **ModelsSelectorList** - Grouped model options list
|
||||
@@ -54,7 +54,7 @@ export { default as ModelsSelectorDropdown } from './ModelsSelector/ModelsSelect
|
||||
* Accepts an optional `renderOption` snippet to customize how each option is
|
||||
* rendered (e.g., to add keyboard navigation or highlighting).
|
||||
*/
|
||||
export { default as ModelsSelectorList } from './ModelsSelector/ModelsSelectorList.svelte';
|
||||
export { default as ModelsSelectorList } from './selector/ModelsSelectorList.svelte';
|
||||
|
||||
/**
|
||||
* **ModelsSelectorOption** - Single model option row
|
||||
@@ -63,7 +63,7 @@ export { default as ModelsSelectorList } from './ModelsSelector/ModelsSelectorLi
|
||||
* load/unload actions, status indicators, and an info button.
|
||||
* Used inside ModelsSelectorList or directly in custom render snippets.
|
||||
*/
|
||||
export { default as ModelsSelectorOption } from './ModelsSelector/ModelsSelectorOption.svelte';
|
||||
export { default as ModelsSelectorOption } from './selector/ModelsSelectorOption.svelte';
|
||||
|
||||
/**
|
||||
* **ModelsSelectorSheet** - Mobile model selection sheet
|
||||
@@ -72,7 +72,7 @@ export { default as ModelsSelectorOption } from './ModelsSelector/ModelsSelector
|
||||
* on mobile devices. Same functionality as ModelsSelectorDropdown but uses Sheet UI
|
||||
* instead of DropdownMenu.
|
||||
*/
|
||||
export { default as ModelsSelectorSheet } from './ModelsSelector/ModelsSelectorSheet.svelte';
|
||||
export { default as ModelsSelectorSheet } from './selector/ModelsSelectorSheet.svelte';
|
||||
|
||||
/** * **ModelBadge** - Model name display badge
|
||||
*
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@
|
||||
import {
|
||||
ChatFormActionAddReasoningSubmenu,
|
||||
DialogModelInformation,
|
||||
DialogModelsHub,
|
||||
DialogModelsDiscover,
|
||||
DropdownMenuSearchable,
|
||||
ModelId,
|
||||
ModelsSelectorList,
|
||||
@@ -429,4 +429,4 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<DialogModelsHub bind:open={modelsHubOpen} />
|
||||
<DialogModelsDiscover bind:open={modelsHubOpen} />
|
||||
@@ -44,6 +44,7 @@ export * from './message-export.constants';
|
||||
export * from './path-display.constants';
|
||||
export * from './model-id.constants';
|
||||
export * from './model-loading.constants';
|
||||
export * from './models-discover.constants';
|
||||
export * from './precision.constants';
|
||||
export * from './pwa.constants';
|
||||
export * from './routes.constants';
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Curated default models shown in the Discover Models sidebar, in display order.
|
||||
*/
|
||||
export const CURATED_MODEL_IDS = [
|
||||
'ggml-org/Qwen3.8-27B-GGUF',
|
||||
'ggml-org/DeepSeek-V4-Flash-0731-GGUF',
|
||||
'ggml-org/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-GGUF',
|
||||
'ggml-org/Qwen3.6-35B-A3B-GGUF',
|
||||
'ggml-org/gemma-4-31B-it-GGUF',
|
||||
'ggml-org/gemma-4-26B-A4B-it-GGUF',
|
||||
'ggml-org/gemma-4-12B-it-GGUF',
|
||||
'ggml-org/Qwen3.5-0.8B-GGUF',
|
||||
'ggml-org/gemma-4-E2B-it-GGUF',
|
||||
'ggml-org/gemma-4-E4B-it-GGUF',
|
||||
'ggml-org/gpt-oss-120b-GGUF',
|
||||
'ggml-org/gpt-oss-20b-GGUF',
|
||||
];
|
||||
@@ -2,13 +2,14 @@
|
||||
* modelsHubStore - Model Hub browse state
|
||||
*
|
||||
* Owns the HuggingFace GGUF model list shown in the hub sidebar
|
||||
* (DialogModelsHub). The hub has no "nothing selected" screen: it always opens
|
||||
* (DialogModelsDiscover). The hub has no "nothing selected" screen: it always opens
|
||||
* a model, so `firstModel` drives the initial selection. By default the list
|
||||
* shows the official ggml-org GGUF models sorted by popularity (downloads);
|
||||
* shows a curated set of official ggml-org GGUF models in a fixed display order;
|
||||
* search replaces the list with matching models across all of HuggingFace.
|
||||
* Detail data is loaded by ModelsHubModelDetails, not here.
|
||||
* 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';
|
||||
|
||||
@@ -28,8 +29,8 @@ class ModelsHubStore {
|
||||
private searchRequestId = 0;
|
||||
|
||||
/**
|
||||
* Fetch the default list: official ggml-org GGUF models sorted by popularity
|
||||
* (downloads). No-op when already loaded or in flight.
|
||||
* Fetch the default list: the curated ggml-org models in display order.
|
||||
* No-op when already loaded or in flight.
|
||||
*/
|
||||
async fetch(): Promise<void> {
|
||||
if (this.loading || this.fetched) return;
|
||||
@@ -45,8 +46,24 @@ class ModelsHubStore {
|
||||
full: true
|
||||
});
|
||||
|
||||
// `models-moved` is a redirect placeholder, not a browsable model
|
||||
this.defaultModels = results.filter((m) => m.id !== 'ggml-org/models-moved');
|
||||
const byId = new Map(results.map((m) => [m.id, m]));
|
||||
|
||||
// Keep only the curated models, in the curated display order.
|
||||
const curated = CURATED_MODEL_IDS.flatMap((id) => {
|
||||
const model = byId.get(id);
|
||||
|
||||
return model ? [model] : [];
|
||||
});
|
||||
|
||||
// The list endpoint omits gguf metadata (context length, architecture),
|
||||
// so fetch it per model and merge it in.
|
||||
this.defaultModels = await Promise.all(
|
||||
curated.map(async (model) => {
|
||||
const details = await HuggingFaceService.getDetails(model.id);
|
||||
|
||||
return details?.gguf ? { ...model, gguf: details.gguf } : model;
|
||||
})
|
||||
);
|
||||
this.models = this.defaultModels;
|
||||
this.fetched = true;
|
||||
} catch (err) {
|
||||
|
||||
+2
@@ -67,6 +67,8 @@ export interface HfModelInfo {
|
||||
lastModified?: string;
|
||||
/** Repository file listing (present when full=true) */
|
||||
siblings?: HfModelSiblingRef[];
|
||||
/** GGUF metadata (context length, architecture, etc.) */
|
||||
gguf?: HfModelGguf;
|
||||
}
|
||||
|
||||
// Model Details (with full=true)
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
Trash2,
|
||||
X
|
||||
} from '@lucide/svelte';
|
||||
import { ActionIcon, DialogModelsHub, ModelId } from '$lib/components/app';
|
||||
import { ActionIcon, DialogModelsDiscover, ModelId } from '$lib/components/app';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { ServerModelStatus } from '$lib/enums';
|
||||
import { modelsStore, serverStore } from '$lib/stores';
|
||||
@@ -261,4 +261,4 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<DialogModelsHub bind:open={modelsHubOpen} />
|
||||
<DialogModelsDiscover bind:open={modelsHubOpen} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import ModelsSelectorList from '$lib/components/app/models/ModelsSelectorList.svelte';
|
||||
import ModelsSelectorOption from '$lib/components/app/models/ModelsSelectorOption.svelte';
|
||||
import ModelsSelectorList from '$lib/components/app/models/selector/ModelsSelectorList.svelte';
|
||||
import ModelsSelectorOption from '$lib/components/app/models/selector/ModelsSelectorOption.svelte';
|
||||
import type { GroupedModelOptions, ModelItem } from '$lib/components/app/models/utils';
|
||||
import { ServerModelStatus } from '$lib/enums';
|
||||
import { modelsStore } from '$lib/stores/models/index.svelte';
|
||||
|
||||
Reference in New Issue
Block a user