mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-06 13:01:08 +02:00
feat: WIP
This commit is contained in:
@@ -1,9 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { SearchInput } from '$lib/components/app';
|
||||
import { ModelsDiscoverDetails, ModelsDiscoverList } from '$lib/components/app/models/discover';
|
||||
import { ModelsDiscover } from '$lib/components/app/models/discover';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { modelsHubStore } from '$lib/stores';
|
||||
import { untrack } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
open?: boolean;
|
||||
@@ -12,46 +9,10 @@
|
||||
|
||||
let { onOpenChange, open = $bindable(false) }: Props = $props();
|
||||
|
||||
let selectedId = $state<string | null>(null);
|
||||
let searchQuery = $state('');
|
||||
let searchTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function handleOpenChange(value: boolean) {
|
||||
open = value;
|
||||
onOpenChange?.(value);
|
||||
}
|
||||
|
||||
// Load the sidebar list when the dialog opens, and reset browsing state.
|
||||
// Store writes are untracked so this effect only depends on `open`.
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
untrack(() => {
|
||||
void modelsHubStore.fetch();
|
||||
void modelsHubStore.search('');
|
||||
});
|
||||
searchQuery = '';
|
||||
selectedId = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-select the first model only when the dialog opens (selection reset).
|
||||
$effect(() => {
|
||||
const first = modelsHubStore.firstModel;
|
||||
|
||||
if (!selectedId && first) {
|
||||
selectedId = first.id;
|
||||
}
|
||||
});
|
||||
|
||||
function handleSearchInput(value: string) {
|
||||
searchQuery = value;
|
||||
|
||||
if (searchTimeout) clearTimeout(searchTimeout);
|
||||
|
||||
searchTimeout = setTimeout(() => {
|
||||
void modelsHubStore.search(value);
|
||||
}, 300);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root {open} onOpenChange={handleOpenChange}>
|
||||
@@ -59,40 +20,6 @@
|
||||
class="grid gap-0 p-0 md:h-[calc(100vh-4rem)]! md:max-h-240! md:w-[calc(100vw-4rem)]! md:max-w-360!"
|
||||
style="grid-template-columns: auto 1fr;"
|
||||
>
|
||||
<aside
|
||||
class="w-108 shrink-0 self-start border-r border-border/40 bg-background overflow-y-auto md:p-4 h-full space-y-1"
|
||||
>
|
||||
<div class="p-2 sticky top-0 z-99">
|
||||
<SearchInput
|
||||
class=""
|
||||
bind:value={searchQuery}
|
||||
placeholder="Search models..."
|
||||
onInput={handleSearchInput}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{#if modelsHubStore.loading}
|
||||
<p class="p-4 text-sm text-muted-foreground">Loading models...</p>
|
||||
{:else if modelsHubStore.error}
|
||||
<p class="p-4 text-sm text-destructive">{modelsHubStore.error}</p>
|
||||
{:else if modelsHubStore.models.length === 0}
|
||||
<p class="p-4 text-sm text-muted-foreground">No models found</p>
|
||||
{:else}
|
||||
<ModelsDiscoverList
|
||||
models={modelsHubStore.models}
|
||||
activeId={selectedId}
|
||||
showBaseModelAvatar
|
||||
onSelect={(id) => (selectedId = id)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="overflow-y-auto">
|
||||
{#if selectedId}
|
||||
<ModelsDiscoverDetails modelId={selectedId} />
|
||||
{/if}
|
||||
</main>
|
||||
<ModelsDiscover />
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import { SearchInput } from '$lib/components/app';
|
||||
import { ModelsDiscoverDetails, ModelsDiscoverList } from '$lib/components/app/models/discover';
|
||||
import { modelsHubStore } from '$lib/stores';
|
||||
|
||||
let selectedId = $state<string | null>(null);
|
||||
let searchQuery = $state('');
|
||||
let searchTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// Load the sidebar list on mount (the component is mounted when the dialog opens).
|
||||
$effect(() => {
|
||||
void modelsHubStore.fetch();
|
||||
void modelsHubStore.search('');
|
||||
});
|
||||
|
||||
// Auto-select the first model.
|
||||
$effect(() => {
|
||||
const first = modelsHubStore.firstModel;
|
||||
|
||||
if (!selectedId && first) {
|
||||
selectedId = first.id;
|
||||
}
|
||||
});
|
||||
|
||||
function handleSearchInput(value: string) {
|
||||
searchQuery = value;
|
||||
|
||||
if (searchTimeout) clearTimeout(searchTimeout);
|
||||
|
||||
searchTimeout = setTimeout(() => {
|
||||
void modelsHubStore.search(value);
|
||||
}, 300);
|
||||
}
|
||||
</script>
|
||||
|
||||
<aside
|
||||
class="w-108 shrink-0 self-start border-r border-border/40 bg-background overflow-y-auto md:p-4 h-full space-y-1"
|
||||
>
|
||||
<div class="p-2 sticky top-0 z-99">
|
||||
<SearchInput
|
||||
class=""
|
||||
bind:value={searchQuery}
|
||||
placeholder="Search models..."
|
||||
onInput={handleSearchInput}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{#if modelsHubStore.loading}
|
||||
<p class="p-4 text-sm text-muted-foreground">Loading models...</p>
|
||||
{:else if modelsHubStore.error}
|
||||
<p class="p-4 text-sm text-destructive">{modelsHubStore.error}</p>
|
||||
{:else if modelsHubStore.models.length === 0}
|
||||
<p class="p-4 text-sm text-muted-foreground">No models found</p>
|
||||
{:else}
|
||||
<ModelsDiscoverList
|
||||
models={modelsHubStore.models}
|
||||
activeId={selectedId}
|
||||
showBaseModelAvatar
|
||||
onSelect={(id) => (selectedId = id)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="overflow-y-auto">
|
||||
{#if selectedId}
|
||||
<ModelsDiscoverDetails modelId={selectedId} />
|
||||
{/if}
|
||||
</main>
|
||||
+11
-12
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import DialogModelDownload from './DialogModelDownload.svelte';
|
||||
import DownloadProgressBar from './DownloadProgressBar.svelte';
|
||||
import { Check, Cpu, MessageSquareCode, TriangleAlert, X } from '@lucide/svelte';
|
||||
import { Check, Cpu, Download, MessageSquareCode, Monitor, TriangleAlert, X } from '@lucide/svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import type { GgufVariantTagInput } from '$lib/services';
|
||||
@@ -65,13 +65,11 @@
|
||||
</script>
|
||||
|
||||
{#if bitDepthRows.length}
|
||||
<section class="space-y-3">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<h2
|
||||
class="flex items-center gap-1.5 text-xs font-semibold tracking-wide text-muted-foreground uppercase"
|
||||
>
|
||||
<MessageSquareCode class="h-3.5 w-3.5" />
|
||||
Download options
|
||||
<section class="rounded-xl border">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 px-4 pt-3 pb-1">
|
||||
<h2 class="flex items-center gap-1.5 text-sm font-medium text-muted-foreground">
|
||||
<Download class="h-4 w-4" />
|
||||
Downloadable options
|
||||
</h2>
|
||||
|
||||
<span
|
||||
@@ -84,17 +82,17 @@
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="divide-y px-4 pb-1">
|
||||
{#each bitDepthRows as row (row.bitDepth)}
|
||||
<div class="grid grid-cols-[5rem_1fr] items-start gap-3">
|
||||
<div class="pt-1 text-xs font-semibold tabular-nums text-muted-foreground">
|
||||
<div class="grid grid-cols-[5rem_1fr] items-start gap-3 py-3">
|
||||
<div class="pt-1 text-sm tabular-nums text-muted-foreground">
|
||||
{#if row.bitDepth === 99}
|
||||
Other
|
||||
{:else}
|
||||
{row.bitDepth}-bit
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<div class="flex flex-wrap justify-end gap-1.5">
|
||||
{#each row.files as file (file.path)}
|
||||
{@const meta = HuggingFaceService.extractQuantMeta(file.path)}
|
||||
{@const basename = file.path.split('/').pop() ?? file.path}
|
||||
@@ -180,6 +178,7 @@
|
||||
<span class="font-medium {isDownloaded ? '' : 'text-muted-foreground/80'}"
|
||||
>{label}</span
|
||||
>
|
||||
<span class="-my-1 w-px self-stretch bg-border"></span>
|
||||
<span class={isDownloaded ? '' : 'text-muted-foreground/80'}>
|
||||
{#if isDownloading && progress && progress.totalBytes > 0}
|
||||
{Math.round((progress.downloadedBytes / progress.totalBytes) * 100)}%
|
||||
|
||||
+33
-63
@@ -2,20 +2,12 @@
|
||||
import ModelsDiscoverAvatar from './ModelsDiscoverAvatar.svelte';
|
||||
import ModelsDiscoverChatTemplateDialog from './ModelsDiscoverChatTemplateDialog.svelte';
|
||||
import ModelsDiscoverDetailsName from './ModelsDiscoverDetailsName.svelte';
|
||||
import {
|
||||
Download,
|
||||
ExternalLink,
|
||||
Heart,
|
||||
Image,
|
||||
Lightbulb,
|
||||
MessageSquareCode,
|
||||
Wrench
|
||||
} from '@lucide/svelte';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { Download, ExternalLink, Heart, MessageSquareCode } from '@lucide/svelte';
|
||||
import { HuggingFaceService } from '$lib/services';
|
||||
import { modelsHubStore } from '$lib/stores';
|
||||
import type { HfModelDetailInfo, HfModelGguf } from '$lib/types/huggingface';
|
||||
import { formatParameters } from '$lib/utils';
|
||||
import { ICON_CLASS_DEFAULT, ICON_CLASS_SM } from '$lib/constants';
|
||||
|
||||
interface Props {
|
||||
modelId: string;
|
||||
@@ -50,16 +42,26 @@
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<ModelsDiscoverAvatar org={avatarOrg} {quantOrg} />
|
||||
<ModelsDiscoverDetailsName modelId={details.id ?? modelId} {baseModels} />
|
||||
<ModelsDiscoverDetailsName
|
||||
modelId={details.id ?? modelId}
|
||||
{baseModels}
|
||||
{hasVision}
|
||||
{hasTools}
|
||||
{hasReasoning}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href={HuggingFaceService.getModelUrl(modelId)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex shrink-0 items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-muted"
|
||||
>
|
||||
<ExternalLink class="h-3.5 w-3.5" />
|
||||
View on HF
|
||||
<img src="/recommended-mcp/huggingface.ico" alt="" class="h-3.5 w-3.5" />
|
||||
|
||||
View on Hugging Face
|
||||
|
||||
<ExternalLink class={ICON_CLASS_SM} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -85,38 +87,42 @@
|
||||
<p class="text-sm text-muted-foreground">{description}</p>
|
||||
{/if}
|
||||
|
||||
<!-- Metadata chips -->
|
||||
<!-- Metadata chips: label | value pairs, matching the HF model page style -->
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
{#if gguf?.total}
|
||||
<span class="rounded bg-secondary px-2 py-0.5 text-xs font-medium text-secondary-foreground">
|
||||
{formatParameters(gguf.total)} params
|
||||
</span>
|
||||
{/if}
|
||||
{#if gguf?.architecture}
|
||||
<span
|
||||
class="rounded bg-secondary px-2 py-0.5 text-xs font-medium text-secondary-foreground capitalize"
|
||||
>
|
||||
{gguf.architecture.replace(/_/g, ' ')}
|
||||
<span class="inline-flex items-center divide-x divide-border rounded-md border text-xs">
|
||||
<span class="px-2.5 py-1 text-muted-foreground">Model size</span>
|
||||
<span class="px-2.5 py-1 font-medium">{formatParameters(gguf.total)} params</span>
|
||||
</span>
|
||||
{/if}
|
||||
{#if gguf?.context_length}
|
||||
<span class="rounded bg-secondary px-2 py-0.5 text-xs font-medium text-secondary-foreground">
|
||||
{gguf.context_length.toLocaleString()} ctx
|
||||
<span class="inline-flex items-center divide-x divide-border rounded-md border text-xs">
|
||||
<span class="px-2.5 py-1 text-muted-foreground">Context</span>
|
||||
<span class="px-2.5 py-1 font-medium">{gguf.context_length.toLocaleString()}</span>
|
||||
</span>
|
||||
{/if}
|
||||
{#if gguf?.architecture}
|
||||
<span class="inline-flex items-center divide-x divide-border rounded-md border text-xs">
|
||||
<span class="px-2.5 py-1 text-muted-foreground">Architecture</span>
|
||||
<span class="px-2.5 py-1 font-medium">{gguf.architecture}</span>
|
||||
</span>
|
||||
{/if}
|
||||
{#if gguf?.chat_template}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (chatTemplateOpen = true)}
|
||||
class="inline-flex items-center gap-1 rounded bg-secondary px-2 py-0.5 text-xs font-medium text-secondary-foreground transition-colors hover:bg-secondary/70"
|
||||
class="inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-medium transition-colors hover:bg-muted"
|
||||
>
|
||||
<MessageSquareCode class="h-3 w-3" />
|
||||
Chat template
|
||||
</button>
|
||||
{/if}
|
||||
{#if licenseTag}
|
||||
<span class="inline-flex items-center divide-x divide-border rounded-md border text-xs">
|
||||
<span class="px-2.5 py-1 text-muted-foreground">License</span>
|
||||
<span class="px-2.5 py-1 font-medium">{licenseTag}</span>
|
||||
</span>
|
||||
<span class="rounded bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground">
|
||||
{licenseTag}
|
||||
</span>
|
||||
{/if}
|
||||
{#if details.gated === true}
|
||||
@@ -127,42 +133,6 @@
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Capability / modality icons, matching the list item's icon style -->
|
||||
{#if hasVision || hasTools || hasReasoning}
|
||||
<div class="flex flex-wrap items-center gap-2.5 text-muted-foreground">
|
||||
{#if hasVision}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Image class="h-4 w-4" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Vision</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
{#if hasTools}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Wrench class="h-4 w-4" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Tool use</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
{#if hasReasoning}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Lightbulb class="h-4 w-4" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Reasoning</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
{#if gguf?.chat_template}
|
||||
|
||||
@@ -1,29 +1,59 @@
|
||||
<script lang="ts">
|
||||
import { Copy, ExternalLink } from '@lucide/svelte';
|
||||
import { ExternalLink, Image, Lightbulb, Wrench } from '@lucide/svelte';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { HuggingFaceService } from '$lib/services';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
/** Full HuggingFace model id (quant org + name), e.g. `ggml-org/Qwen3.8-27B-GGUF`. */
|
||||
modelId: string;
|
||||
/** Base model ids, shown as small text under the quant name. */
|
||||
baseModels: string[];
|
||||
hasVision: boolean;
|
||||
hasTools: boolean;
|
||||
hasReasoning: boolean;
|
||||
}
|
||||
|
||||
let { baseModels, modelId }: Props = $props();
|
||||
let { baseModels, hasReasoning, hasTools, hasVision, modelId }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<h1 class="truncate text-lg font-semibold">{modelId}</h1>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => copyToClipboard(modelId)}
|
||||
class="shrink-0 text-muted-foreground transition-colors hover:text-foreground"
|
||||
aria-label="Copy model id"
|
||||
>
|
||||
<Copy class="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
{#if hasVision || hasTools || hasReasoning}
|
||||
<div class="flex shrink-0 items-center gap-2.5 text-muted-foreground">
|
||||
{#if hasVision}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Image class="h-4 w-4" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Vision</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
{#if hasTools}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Wrench class="h-4 w-4" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Tool use</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
{#if hasReasoning}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Lightbulb class="h-4 w-4" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Reasoning</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if baseModels.length}
|
||||
|
||||
@@ -7,6 +7,15 @@
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* **ModelsDiscover** - Models hub 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';
|
||||
|
||||
/**
|
||||
* **ModelsDiscoverList** - Sidebar model list
|
||||
*
|
||||
@@ -57,8 +66,9 @@ export { default as ModelsDiscoverDetailsHeader } from './ModelsDiscoverDetailsH
|
||||
/**
|
||||
* **ModelsDiscoverDetailsName** - Model name block
|
||||
*
|
||||
* Shows the quant model name with a copy button, and the base model name with a
|
||||
* smaller external-link icon on the line below.
|
||||
* Shows the quant model name with capability icons (vision, tool use,
|
||||
* reasoning) beside it, and the base model name with a smaller external-link
|
||||
* icon on the line below.
|
||||
*/
|
||||
export { default as ModelsDiscoverDetailsName } from './ModelsDiscoverDetailsName.svelte';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user