ui : apply ESLint formatting to models discover components

Assisted-by: Claude
This commit is contained in:
Aleksander Grygier
2026-08-25 15:27:59 +02:00
parent 393c8ca1d8
commit edd8b98a02
30 changed files with 348 additions and 225 deletions
@@ -20,6 +20,7 @@
{:else if isLoading}
<div class="flex items-center gap-2 border-t border-border/50 pt-2 text-xs text-muted-foreground">
<LoaderCircle class="h-3.5 w-3.5 animate-spin" />
<span>Loading model...</span>
</div>
{/if}
@@ -47,6 +47,7 @@
{#if showSpinner}
<span class="text-foreground/80 text-sm font-medium">Current time</span>
<LoaderCircle class="text-muted-foreground/70 h-3 w-3 animate-spin" />
{:else if dateMeta.errorMessage}
<span class="text-foreground/80 text-sm font-medium">Current time&nbsp;</span>
@@ -55,6 +55,7 @@
{#if showSpinner}
<span class="text-foreground/80 text-sm font-medium">Runtime info</span>
<LoaderCircle class="text-muted-foreground/70 h-3 w-3 animate-spin" />
{:else if infoMeta.errorMessage}
<span class="text-foreground/80 text-sm font-medium">Runtime info&nbsp;</span>
@@ -168,6 +168,7 @@
{:else if showSpinner}
<div class="text-muted-foreground/70 flex items-center gap-2 py-1 text-xs italic">
<LoaderCircle class="h-3 w-3 animate-spin" />
<span>Searching...</span>
</div>
{:else}
@@ -13,6 +13,7 @@
role="status"
>
<LoaderCircle class="h-3.5 w-3.5 animate-spin" />
<span>Reconnecting to the stream...</span>
</div>
{/if}
@@ -15,7 +15,7 @@
}
</script>
<Dialog.Root {open} onOpenChange={handleOpenChange}>
<Dialog.Root onOpenChange={handleOpenChange} {open}>
<Dialog.Content
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;"
@@ -169,6 +169,7 @@
<Tooltip.Trigger>
<Wrench class="h-3 w-3 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<p>Tool use</p>
</Tooltip.Content>
@@ -180,6 +181,7 @@
<Tooltip.Trigger>
<Lightbulb class="h-3 w-3 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<p>Reasoning</p>
</Tooltip.Content>
@@ -193,6 +195,7 @@
<Tooltip.Trigger>
<Image class="h-3 w-3 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<p>Vision</p>
</Tooltip.Content>
@@ -204,6 +207,7 @@
<Tooltip.Trigger>
<Video class="h-3 w-3 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<p>Video</p>
</Tooltip.Content>
@@ -215,6 +219,7 @@
<Tooltip.Trigger>
<Mic class="h-3 w-3 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<p>Audio</p>
</Tooltip.Content>
@@ -228,6 +233,7 @@
{#if contextLength}
<span class="inline-flex items-center gap-1 text-muted-foreground">
<ScrollText class="h-3 w-3" />
<span class="text-xs">{formatParameters(contextLength)}</span>
</span>
{/if}
@@ -235,6 +241,7 @@
{#if sizeRange}
<span class="inline-flex items-center gap-1 text-muted-foreground">
<Database class="h-3 w-3" />
<span class="text-xs"
>{HuggingFaceService.formatSizeRange(sizeRange.min, sizeRange.max)}</span
>
@@ -168,17 +168,19 @@
});
</script>
<AlertDialog.Root {open} onOpenChange={handleOpenChange}>
<AlertDialog.Content onkeydown={handleKeydown} class="max-w-md">
<AlertDialog.Root onOpenChange={handleOpenChange} {open}>
<AlertDialog.Content class="max-w-md" onkeydown={handleKeydown}>
<AlertDialog.Header>
<AlertDialog.Title class="flex items-center gap-2">
<Download class="h-5 w-5 text-primary" />
{#if phase === 'pending'}
Download this model?
{:else}
Downloading {tagDisplay}
{/if}
</AlertDialog.Title>
<AlertDialog.Description>
{#if phase === 'pending'}
llama-server will download this file (and related sidecar weights such as multimodal
@@ -194,6 +196,7 @@
role="status"
>
<TriangleAlert class="mt-0.5 h-4 w-4 shrink-0" />
<span>
A previous attempt for this tag failed and left partial files on disk. The server will
reject a fresh download until those files are removed. The Retry button below deletes
@@ -206,10 +209,10 @@
{#if canDelete}
<div class="flex justify-end">
<button
type="button"
onclick={() => (showDeleteConfirm = true)}
class="inline-flex items-center gap-1.5 rounded-md border border-destructive/40 px-2 py-1 text-xs font-medium text-destructive transition-colors hover:bg-destructive/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-destructive/50"
aria-label="Delete model from cache"
class="inline-flex items-center gap-1.5 rounded-md border border-destructive/40 px-2 py-1 text-xs font-medium text-destructive transition-colors hover:bg-destructive/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-destructive/50"
onclick={() => (showDeleteConfirm = true)}
type="button"
>
<Trash2 class="h-3.5 w-3.5" />
Delete from cache
@@ -220,21 +223,27 @@
<div class="space-y-3 rounded-md border bg-muted/40 p-3 text-xs">
<div class="flex flex-col gap-1">
<span class="text-muted-foreground">Request</span>
<code class="break-all font-mono"
>POST /models&nbsp;&middot;&nbsp;{`{ model: "${hfRepoWithTag}" }`}</code
>
</div>
<div class="flex flex-col gap-1">
<span class="text-muted-foreground">File</span>
<code class="break-all font-mono">{filePath}</code>
</div>
<div class="flex flex-wrap items-center gap-2">
<span class="rounded bg-primary/15 px-2 py-0.5 font-mono font-semibold text-primary">
{tagDisplay}
</span>
{#if formattedSize}
<span class="text-muted-foreground">{formattedSize}</span>
{/if}
{#if variant}
<span
class="rounded bg-primary px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-primary-foreground"
@@ -256,8 +265,10 @@
Preparing download
{/if}
</span>
<span class="font-mono tabular-nums">{progressPercent}%</span>
</div>
<DownloadProgressBar
downloadedBytes={progress?.downloadedBytes ?? 0}
totalBytes={progress?.totalBytes ?? 0}
@@ -269,6 +280,7 @@
{#if lastError}
<p class="text-xs text-destructive">{lastError}</p>
{/if}
{#if lastCancelError}
<p class="text-xs text-destructive">{lastCancelError}</p>
{/if}
@@ -288,6 +300,7 @@
{#if phase === 'finished'}Close{:else}Cancel{/if}
</AlertDialog.Cancel>
{/if}
{#if phase === 'pending'}
<AlertDialog.Action disabled={inFlight} onclick={trigger}>
<Download class="mr-1.5 h-4 w-4" />
@@ -305,12 +318,12 @@
<DialogConfirmation
bind:open={showDeleteConfirm}
title="Delete model"
description={`Remove "${hfRepoWithTag}" from your cache? Any cached files will be deleted from disk.`}
confirmText="Delete"
cancelText="Cancel"
variant="destructive"
confirmText="Delete"
description={`Remove "${hfRepoWithTag}" from your cache? Any cached files will be deleted from disk.`}
icon={Trash2}
onConfirm={handleConfirmDelete}
onCancel={() => (showDeleteConfirm = false)}
onConfirm={handleConfirmDelete}
title="Delete model"
variant="destructive"
/>
@@ -38,10 +38,10 @@
>
<div class="p-2 sticky top-0 z-99">
<SearchInput
class=""
bind:value={searchQuery}
placeholder="Search models..."
class=""
onInput={handleSearchInput}
placeholder="Search models..."
/>
</div>
@@ -54,10 +54,10 @@
<p class="p-4 text-sm text-muted-foreground">No models found</p>
{:else}
<ModelsDiscoverList
models={modelsHubStore.models}
activeId={selectedId}
showBaseModelAvatar
models={modelsHubStore.models}
onSelect={(id) => (selectedId = id)}
showBaseModelAvatar
/>
{/if}
</div>
@@ -57,20 +57,20 @@
<span class="relative mt-0.5 inline-flex shrink-0">
{#if avatarError}
<span
aria-hidden="true"
class="flex {size} 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}
<div class="rounded-md">
<img
src={HuggingFaceService.getAvatarUrl(org)}
onerror={() => (avatarError = true)}
class="{size} rounded-md {invertAvatar ? 'dark:invert' : ''} {baseImageClass}"
alt=""
class="{size} rounded-md {invertAvatar ? 'dark:invert' : ''} {baseImageClass}"
loading="lazy"
onerror={() => (avatarError = true)}
src={HuggingFaceService.getAvatarUrl(org)}
/>
</div>
{/if}
@@ -82,22 +82,23 @@
>
{#if quantError}
<span
aria-hidden="true"
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="{quantImageClass} rounded-full {invertQuant ? 'dark:invert' : ''}"
alt=""
class="{quantImageClass} rounded-full {invertQuant ? 'dark:invert' : ''}"
loading="lazy"
onerror={() => (quantError = true)}
src={HuggingFaceService.getAvatarUrl(quantOrg)}
/>
{/if}
</Tooltip.Trigger>
<Tooltip.Content>
<p>{quantOrg}</p>
</Tooltip.Content>
@@ -17,16 +17,17 @@
}
</script>
<Dialog.Root {open} onOpenChange={handleOpenChange}>
<Dialog.Root onOpenChange={handleOpenChange} {open}>
<Dialog.Content
class="flex max-h-[calc(100vh-4rem)] flex-col gap-0 p-0 md:w-[calc(100vw-4rem)]! md:max-w-4xl!"
>
<Dialog.Header class="flex-row items-center justify-between border-b border-border/40 p-4">
<Dialog.Title class="text-sm font-semibold">Chat template</Dialog.Title>
<button
type="button"
onclick={() => copyToClipboard(chatTemplate)}
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-medium transition-colors hover:bg-muted"
onclick={() => copyToClipboard(chatTemplate)}
type="button"
>
<Copy class="h-3.5 w-3.5" />
Copy
@@ -124,24 +124,24 @@
{:else if details}
<div class="space-y-6 p-6">
<ModelsDiscoverDetailsHeader
{modelId}
{baseModels}
{details}
{gguf}
{baseModels}
{licenseTag}
{hasVision}
{hasTools}
{hasReasoning}
{hasTools}
{hasVision}
{licenseTag}
{modelId}
/>
<ModelsDiscoverDetailsDownloadOptions
{modelId}
{files}
{bitDepthRows}
{files}
{modelId}
nativeCtxTokens={gguf?.context_length ?? 0}
/>
<TerminalCommands {modelId} {draftVariants} />
<TerminalCommands {draftVariants} {modelId} />
<ModelsDiscoverDetailsReadme {readme} />
</div>
@@ -1,7 +1,7 @@
<script lang="ts">
import DialogModelDownload from './DialogModelDownload.svelte';
import DownloadProgressBar from './DownloadProgressBar.svelte';
import { Check, Cpu, Download, MessageSquareCode, Monitor, TriangleAlert, X } from '@lucide/svelte';
import { Check, Cpu, Download, TriangleAlert, X } from '@lucide/svelte';
import { browser } from '$app/environment';
import * as Tooltip from '$lib/components/ui/tooltip';
import type { GgufVariantTagInput } from '$lib/services';
@@ -82,6 +82,7 @@
{/if}
</span>
</div>
<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 py-3">
@@ -92,6 +93,7 @@
{row.bitDepth}-bit
{/if}
</div>
<div class="flex flex-wrap justify-end gap-1.5">
{#each row.files as file (file.path)}
{@const meta = HuggingFaceService.extractQuantMeta(file.path)}
@@ -124,7 +126,12 @@
: `Download ${file.path}`}
<Tooltip.Root>
<Tooltip.Trigger
type="button"
aria-disabled={isUnavailable}
class={buttonClass({
isDownloaded,
isFailed,
isUnavailable
})}
onclick={() => {
if (isUnavailable) return;
@@ -135,12 +142,7 @@
variant: meta?.variant ?? null
};
}}
aria-disabled={isUnavailable}
class={buttonClass({
isDownloaded,
isFailed,
isUnavailable
})}
type="button"
>
{#if isAvailable}
<span
@@ -161,6 +163,7 @@
<X class="h-2.5 w-2.5 text-white" />
</span>
{/if}
{#if isFailed && !isDownloading && !isDownloaded}
<span
class="rounded bg-destructive px-1 py-0.5 text-[10px] font-semibold tracking-wide text-destructive-foreground uppercase"
@@ -168,6 +171,7 @@
Failed
</span>
{/if}
{#if meta?.variant}
<span
class="rounded bg-primary px-1 py-0.5 text-[10px] font-semibold tracking-wide text-primary-foreground uppercase"
@@ -175,10 +179,13 @@
{meta.variant}
</span>
{/if}
<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)}%
@@ -186,10 +193,11 @@
{HuggingFaceService.formatFileSize(file.size ?? 0)}
{/if}
</span>
{#if isDownloading && progress}
<DownloadProgressBar
overlay
downloadedBytes={progress.downloadedBytes}
overlay
totalBytes={progress.totalBytes}
/>
{/if}
@@ -215,14 +223,14 @@
if (!v) pendingDownload = null;
}
}
repoId={modelId}
filePath={pendingDownload.filePath}
quant={pendingDownload.quant}
variant={pendingDownload.variant}
formattedSize={pendingDownload.sizeBytes != null
? HuggingFaceService.formatFileSize(pendingDownload.sizeBytes)
: undefined}
onConfirm={() => (pendingDownload = null)}
onCancel={() => (pendingDownload = null)}
onConfirm={() => (pendingDownload = null)}
quant={pendingDownload.quant}
repoId={modelId}
variant={pendingDownload.variant}
/>
{/if}
@@ -3,11 +3,11 @@
import ModelsDiscoverChatTemplateDialog from './ModelsDiscoverChatTemplateDialog.svelte';
import ModelsDiscoverDetailsName from './ModelsDiscoverDetailsName.svelte';
import { Download, ExternalLink, Heart, MessageSquareCode } from '@lucide/svelte';
import { ICON_CLASS_SM } from '$lib/constants';
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;
@@ -42,22 +42,23 @@
<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}
{hasVision}
{hasTools}
{hasReasoning}
{hasTools}
{hasVision}
modelId={details.id ?? modelId}
/>
</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"
href={HuggingFaceService.getModelUrl(modelId)}
rel="noopener noreferrer"
target="_blank"
>
<img src="/recommended-mcp/huggingface.ico" alt="" class="h-3.5 w-3.5" />
<img alt="" class="h-3.5 w-3.5" src="/recommended-mcp/huggingface.ico" />
View on Hugging Face
@@ -72,12 +73,14 @@
{HuggingFaceService.formatDownloads(details.downloads)}
</span>
{/if}
{#if typeof details.likes === 'number'}
<span class="inline-flex items-center gap-1.5">
<Heart class="h-3.5 w-3.5" />
{HuggingFaceService.formatLikes(details.likes)}
</span>
{/if}
{#if details.lastModified}
<span>Updated {HuggingFaceService.formatRelativeTime(details.lastModified)}</span>
{/if}
@@ -92,39 +95,49 @@
{#if gguf?.total}
<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="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.5 rounded-md border px-2.5 py-1 text-xs font-medium transition-colors hover:bg-muted"
onclick={() => (chatTemplateOpen = true)}
type="button"
>
<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">
</span>
{/if}
{#if details.gated === true}
<span
class="rounded bg-yellow-500/10 px-2 py-0.5 text-xs font-medium text-yellow-600 dark:text-yellow-400"
@@ -27,26 +27,31 @@
<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>
@@ -59,12 +64,13 @@
{#if baseModels.length}
<div class="flex items-center gap-1">
<span class="truncate text-xs text-muted-foreground">{baseModels.join(', ')}</span>
<a
href={HuggingFaceService.getModelUrl(baseModels[0])}
target="_blank"
rel="noopener noreferrer"
class="shrink-0 text-muted-foreground transition-colors hover:text-foreground"
aria-label="View base model on HuggingFace"
class="shrink-0 text-muted-foreground transition-colors hover:text-foreground"
href={HuggingFaceService.getModelUrl(baseModels[0])}
rel="noopener noreferrer"
target="_blank"
>
<ExternalLink class="h-3 w-3" />
</a>
@@ -11,6 +11,7 @@
{#if readme}
<section class="space-y-2 bg-muted/50 p-3 rounded-xl">
<h2 class="text-xs font-semibold tracking-wide text-muted-foreground uppercase">README</h2>
<MarkdownContent content={readme} allowHtml class="prose-sm max-w-none" />
<MarkdownContent allowHtml class="prose-sm max-w-none" content={readme} />
</section>
{/if}
@@ -134,17 +134,17 @@
<span class="min-w-0 flex-1">
<ModelId
modelId={model.id}
class="min-w-0"
{contextLength}
{draftVariants}
hideOrgName
iconsOnNewLine
{modalities}
modelId={model.id}
params={paramsFallback}
{sizeRange}
{supportsThinking}
{supportsToolUse}
{contextLength}
{sizeRange}
{draftVariants}
params={paramsFallback}
iconsOnNewLine
wrap
class="min-w-0"
/>
</span>
@@ -29,14 +29,15 @@
<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'}"
onclick={() => onSelect?.(model.id)}
type="button"
>
<ModelsDiscoverAvatar org={avatarOrg} quantOrg={showBaseModelAvatar ? org : undefined} />
<ModelsDiscoverInfo {model} />
</button>
</li>
@@ -1,6 +1,6 @@
<script lang="ts">
import type { HfModelInfo } from '$lib/types/huggingface';
import ModelsDiscoverItem from './ModelsDiscoverItem.svelte';
import type { HfModelInfo } from '$lib/types/huggingface';
interface Props {
models: HfModelInfo[];
@@ -10,11 +10,11 @@
onSelect?: (modelId: string) => void;
}
let { models, activeId = null, showBaseModelAvatar = false, onSelect }: Props = $props();
let { activeId = null, models, onSelect, showBaseModelAvatar = false }: Props = $props();
</script>
<ul class="space-y-0.5 p-2">
{#each models as model (model.id)}
<ModelsDiscoverItem {model} active={model.id === activeId} {showBaseModelAvatar} {onSelect} />
<ModelsDiscoverItem active={model.id === activeId} {model} {onSelect} {showBaseModelAvatar} />
{/each}
</ul>
@@ -59,6 +59,7 @@
style="border-bottom: 1px solid color-mix(in oklch, var(--border) 30%, transparent);"
>
<box.icon class="h-3.5 w-3.5 text-muted-foreground/60" />
<span class="text-xs font-medium text-foreground/80">{box.title}</span>
</div>
@@ -68,11 +69,12 @@
class="group flex items-center justify-between gap-2 rounded px-2 py-1 font-mono text-xs"
>
<span class="truncate text-foreground/90">{cmd}</span>
<button
type="button"
onclick={() => handleCopy(i, cmd)}
class="shrink-0 text-muted-foreground/60 transition-colors hover:text-foreground"
aria-label="Copy command"
class="shrink-0 text-muted-foreground/60 transition-colors hover:text-foreground"
onclick={() => handleCopy(i, cmd)}
type="button"
>
{#if copiedIndex === i}
<Check class="h-3.5 w-3.5 text-green-500" />
@@ -228,10 +228,10 @@
<span class="flex min-w-0 items-center gap-1">
{#if selectedOption}
<ModelId
modelId={selectedOption.model}
class="min-w-0 overflow-hidden"
hideQuantization
hideOrgName={!showOrgNameInTrigger}
hideQuantization
modelId={selectedOption.model}
/>
{:else}
<span class="min-w-0 font-medium">Select model</span>
@@ -273,10 +273,10 @@
{#if selectedOption}
<ModelId
modelId={selectedOption.model}
class="min-w-0 flex-1 overflow-hidden"
hideOrgName={!showOrgNameInTrigger}
hideQuantization
modelId={selectedOption.model}
/>
{:else}
<span class="min-w-0 flex-1 truncate text-muted-foreground">No model</span>
@@ -285,25 +285,25 @@
<DropdownMenu.SubContent class="max-w-[calc(100vw-2rem)] md:max-w-108 pt-0">
<DropdownMenuSearchable
searchValue={ms.searchTerm}
onSearchChange={(v) => ms.setSearchTerm(v)}
placeholder="Search models..."
onSearchKeyDown={handleSearchKeyDown}
emptyMessage="No models found."
isEmpty={ms.filteredOptions.length === 0 && ms.isCurrentModelInCache}
onSearchChange={(v) => ms.setSearchTerm(v)}
onSearchKeyDown={handleSearchKeyDown}
placeholder="Search models..."
searchValue={ms.searchTerm}
>
<div class="models-list">
{#if !ms.isCurrentModelInCache && currentModel}
<!-- Show unavailable model as first option (disabled) -->
<button
type="button"
class="flex w-full cursor-not-allowed items-center bg-red-400/10 p-2 text-left text-sm text-red-400"
role="option"
aria-selected="true"
aria-disabled="true"
aria-selected="true"
class="flex w-full cursor-not-allowed items-center bg-red-400/10 p-2 text-left text-sm text-red-400"
disabled
role="option"
type="button"
>
<ModelId modelId={currentModel} class="flex-1" hideQuantization />
<ModelId class="flex-1" hideQuantization modelId={currentModel} />
<span class="ml-2 text-xs whitespace-nowrap opacity-70">(not available)</span>
</button>
@@ -320,32 +320,32 @@
{@const isFav = ms.isFavorite(option.model)}
<ModelsSelectorOption
{option}
{isSelected}
{isHighlighted}
{isFav}
hideOrgName={false}
{compact}
onSelect={ms.handleSelect}
hideOrgName={false}
{isFav}
{isHighlighted}
{isSelected}
onInfoClick={ms.handleInfoClick}
onMouseEnter={() => (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}
<ModelsSelectorList
groups={ms.groupedFilteredOptions}
{currentModel}
activeId={ms.activeId}
sectionHeaderClass="my-1.5 px-2 py-2 text-[13px] font-semibold text-muted-foreground/70 select-none"
onSelect={ms.handleSelect}
{currentModel}
groups={ms.groupedFilteredOptions}
onInfoClick={ms.handleInfoClick}
onSelect={ms.handleSelect}
renderOption={modelOption}
sectionHeaderClass="my-1.5 px-2 py-2 text-[13px] font-semibold text-muted-foreground/70 select-none"
/>
</div>
</DropdownMenuSearchable>
@@ -384,18 +384,18 @@
: 'text-foreground',
isOpen && 'text-foreground'
]}
style="max-width: min(calc(100cqw - 6.5rem), 32rem)"
onclick={() => ms.handleOpenChange(true)}
disabled={disabled || ms.updating}
onclick={() => ms.handleOpenChange(true)}
style="max-width: min(calc(100cqw - 6.5rem), 32rem)"
>
<MODEL_SELECTOR_ICON class="h-3.5 w-3.5 shrink-0" />
{#if selectedOption}
<ModelId
modelId={selectedOption.model}
class="min-w-0 overflow-hidden"
hideOrgName={!showOrgNameInTrigger}
hideQuantization
modelId={selectedOption.model}
/>
{/if}
@@ -422,9 +422,9 @@
{#if ms.showModelDialog}
<DialogModelInformation
open={ms.showModelDialog}
onOpenChange={(v) => ms.setShowModelDialog(v)}
modelId={ms.infoModelId}
onOpenChange={(v) => ms.setShowModelDialog(v)}
open={ms.showModelDialog}
/>
{/if}
@@ -31,21 +31,22 @@
{@const isFav = modelsStore.favoriteModelIds.has(option.model)}
<ModelsSelectorOption
{option}
{isSelected}
isHighlighted={false}
{isFav}
{hideOrgName}
{compact}
{onSelect}
{hideOrgName}
{isFav}
isHighlighted={false}
{isSelected}
{onInfoClick}
onMouseEnter={() => {}}
onKeyDown={() => {}}
onMouseEnter={() => {}}
{onSelect}
{option}
/>
{/snippet}
{#if groups.loaded.length > 0}
<p class={sectionHeaderClass}>Loaded models</p>
{#each groups.loaded as item (`loaded-${item.option.id}`)}
{@render render(item, false)}
{/each}
@@ -53,6 +54,7 @@
{#if groups.favorites.length > 0}
<p class={sectionHeaderClass}>Favorite models</p>
{#each groups.favorites as item (`fav-${item.option.id}`)}
{@render render(item, true)}
{/each}
@@ -60,6 +62,7 @@
{#if groups.available.length > 0}
<p class={sectionHeaderClass}>Available models</p>
{#each groups.available as item (`avail-${item.option.id}`)}
{@render render(item, false)}
{/each}
@@ -68,6 +68,7 @@
</script>
<div
aria-selected={isSelected || isHighlighted}
class={[
'group relative flex w-full items-center gap-2 rounded-sm p-2 text-left text-sm transition focus:outline-none',
'cursor-pointer',
@@ -78,36 +79,35 @@
'focus:bg-accent',
isLoaded ? 'text-popover-foreground' : 'text-muted-foreground'
]}
role="option"
aria-selected={isSelected || isHighlighted}
title={loadTitle}
tabindex="0"
onclick={() => onSelect(option.id)}
onmouseenter={onMouseEnter}
onkeydown={onKeyDown}
onmouseenter={onMouseEnter}
role="option"
tabindex="0"
title={loadTitle}
>
{#if !compact}
<ModelsDiscoverAvatar
org={baseOrg}
{quantOrg}
size="size-5"
quantImageClass="size-3.25"
{quantOrg}
quantPositionClass="-right-1.25 -bottom-1.25"
size="size-5"
/>
{/if}
<ModelId
modelId={option.model}
{hideOrgName}
hideName={compact}
hideModalities={compact}
hideParameters={compact}
aliases={option.aliases}
tags={option.tags}
{modalities}
{supportsThinking}
showRawTooltip
class="flex-1"
hideModalities={compact}
hideName={compact}
{hideOrgName}
hideParameters={compact}
{modalities}
modelId={option.model}
showRawTooltip
{supportsThinking}
tags={option.tags}
/>
<div class="flex shrink-0 items-center gap-1">
@@ -119,30 +119,30 @@
>
{#if isFav}
<ActionIcon
iconSize="h-2.5 w-2.5"
icon={HeartOff}
tooltip="Remove from favorites"
class="h-3 w-3 hover:text-foreground"
icon={HeartOff}
iconSize="h-2.5 w-2.5"
onclick={() => modelsStore.toggleFavorite(option.model)}
tooltip="Remove from favorites"
/>
{:else}
<ActionIcon
iconSize="h-2.5 w-2.5"
icon={Heart}
tooltip="Add to favorites"
class="h-3 w-3 hover:text-foreground"
icon={Heart}
iconSize="h-2.5 w-2.5"
onclick={() => modelsStore.toggleFavorite(option.model)}
tooltip="Add to favorites"
/>
{/if}
<!-- info button: only shown when model is loaded and callback is provided -->
{#if isLoaded && onInfoClick}
<ActionIcon
iconSize="h-2.5 w-2.5"
icon={Info}
tooltip="Model information"
class="h-3 w-3 hover:text-foreground"
icon={Info}
iconSize="h-2.5 w-2.5"
onclick={() => onInfoClick(option.model)}
tooltip="Model information"
/>
{/if}
</div>
@@ -159,12 +159,12 @@
<div class="hidden group-hover:flex [@media(pointer:coarse)]:flex">
<ActionIcon
iconSize="h-2.5 w-2.5"
icon={RotateCw}
tooltip="Retry loading model"
class="h-3 w-3 text-red-500 hover:text-foreground"
icon={RotateCw}
iconSize="h-2.5 w-2.5"
onclick={() => modelsStore.status.load(option.model)}
stopPropagationOnClick
tooltip="Retry loading model"
/>
</div>
</div>
@@ -176,14 +176,14 @@
<div class="hidden group-hover:flex [@media(pointer:coarse)]:flex">
<ActionIcon
iconSize="h-2.5 w-2.5"
icon={PowerOff}
tooltip="Unload model"
class="h-3 w-3 text-red-500 hover:text-red-600 [@media(pointer:coarse)]:text-amber-500 [@media(pointer:coarse)]:hover:text-amber-600"
icon={PowerOff}
iconSize="h-2.5 w-2.5"
onclick={(e) => {
e?.stopPropagation();
modelsStore.status.unload(option.model);
}}
tooltip="Unload model"
/>
</div>
</div>
@@ -195,12 +195,12 @@
<div class="hidden group-hover:flex [@media(pointer:coarse)]:flex">
<ActionIcon
iconSize="h-2.5 w-2.5"
icon={PowerOff}
tooltip="Unload model"
class="h-3 w-3 text-red-500 hover:text-red-600 [@media(pointer:coarse)]:text-green-500 [@media(pointer:coarse)]:hover:text-green-600"
icon={PowerOff}
iconSize="h-2.5 w-2.5"
onclick={() => modelsStore.status.unload(option.model)}
stopPropagationOnClick
tooltip="Unload model"
/>
</div>
</div>
@@ -212,12 +212,12 @@
<div class="hidden group-hover:flex [@media(pointer:coarse)]:flex">
<ActionIcon
iconSize="h-2.5 w-2.5"
icon={Power}
tooltip="Load model"
class="h-3 w-3 [@media(pointer:coarse)]:text-muted-foreground"
icon={Power}
iconSize="h-2.5 w-2.5"
onclick={() => modelsStore.status.load(option.model)}
stopPropagationOnClick
tooltip="Load model"
/>
</div>
</div>
@@ -79,7 +79,6 @@
{#if ms.isRouter}
<button
type="button"
class={[
`relative inline-flex cursor-pointer items-center gap-1.5 rounded-sm bg-background px-1.5 py-1 text-xs shadow-sm transition hover:bg-muted-foreground/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60 max-sm:px-3 max-sm:py-2 max-sm:text-sm dark:bg-muted-foreground/15 dark:text-secondary-foreground`,
!ms.isCurrentModelInCache
@@ -91,9 +90,10 @@
: 'text-foreground',
sheetOpen && 'text-foreground'
]}
style="max-width: min(calc(100cqw - 9rem), 20rem)"
disabled={disabled || ms.updating}
onclick={() => ms.handleOpenChange(true)}
style="max-width: min(calc(100cqw - 9rem), 20rem)"
type="button"
>
<Package class="h-3.5 w-3.5 shrink-0" />
@@ -102,10 +102,10 @@
{:else}
<ModelId
class="text-xs"
modelId={selectedOption?.model || ''}
hideOrgName
hideQuantization
hideTags
hideOrgName
modelId={selectedOption?.model || ''}
/>
{/if}
@@ -121,7 +121,7 @@
</button>
<Sheet.Root bind:open={sheetOpen} onOpenChange={handleSheetOpenChange}>
<Sheet.Content side="bottom" class="max-h-[85vh] gap-1">
<Sheet.Content class="max-h-[85vh] gap-1" side="bottom">
<Sheet.Header>
<Sheet.Title>Select Model</Sheet.Title>
@@ -133,24 +133,26 @@
<div class="flex flex-col gap-1 pb-4">
<div class="mb-3 px-4">
<SearchInput
onInput={(v) => ms.setSearchTerm(v)}
placeholder="Search models..."
value={ms.searchTerm}
onInput={(v) => ms.setSearchTerm(v)}
/>
</div>
<div class="max-h-[60vh] overflow-y-auto px-2">
{#if !ms.isCurrentModelInCache && currentModel}
<button
type="button"
class="flex w-full cursor-not-allowed items-center rounded-md bg-red-400/10 px-3 py-2.5 text-left text-sm text-red-400"
disabled
type="button"
>
<span class="min-w-0 flex-1 truncate">
{selectedOption?.name || currentModel}
</span>
<span class="ml-2 text-xs whitespace-nowrap opacity-70">(not available)</span>
</button>
<div class="my-1 h-px bg-border"></div>
{/if}
@@ -159,12 +161,12 @@
{/if}
<ModelsSelectorList
groups={ms.groupedFilteredOptions}
{currentModel}
activeId={ms.activeId}
sectionHeaderClass="px-2 py-2 text-xs font-semibold text-muted-foreground/60 select-none"
onSelect={ms.handleSelect}
{currentModel}
groups={ms.groupedFilteredOptions}
onInfoClick={ms.handleInfoClick}
onSelect={ms.handleSelect}
sectionHeaderClass="px-2 py-2 text-xs font-semibold text-muted-foreground/60 select-none"
/>
</div>
</div>
@@ -182,13 +184,13 @@
? 'text-foreground'
: 'text-foreground'
]}
style="max-width: min(calc(100cqw - 6.5rem), 32rem)"
onclick={() => ms.handleOpenChange(true)}
disabled={disabled || ms.updating}
onclick={() => ms.handleOpenChange(true)}
style="max-width: min(calc(100cqw - 6.5rem), 32rem)"
>
<Package class="h-3.5 w-3.5 shrink-0" />
<ModelId modelId={selectedOption?.model || ''} class="font-medium" hideQuantization />
<ModelId class="font-medium" hideQuantization modelId={selectedOption?.model || ''} />
{#if ms.updating}
<LoaderCircle class="h-3 w-3.5 shrink-0 animate-spin" />
@@ -200,8 +202,8 @@
{#if ms.showModelDialog}
<DialogModelInformation
open={ms.showModelDialog}
onOpenChange={(v) => ms.setShowModelDialog(v)}
modelId={ms.infoModelId}
onOpenChange={(v) => ms.setShowModelDialog(v)}
open={ms.showModelDialog}
/>
{/if}
+27 -19
View File
@@ -74,13 +74,13 @@
<tr class="border-t transition-colors hover:bg-muted/40">
<td class="px-3 py-2 {indent ? 'pl-8' : ''}">
<ModelId
modelId={option.model}
aliases={option.aliases}
tags={option.tags}
modalities={option.modalities}
supportsThinking={modelsStore.props.checkModelSupportsThinking(option.model)}
{hideQuantization}
modalities={option.modalities}
modelId={option.model}
showRawTooltip
supportsThinking={modelsStore.props.checkModelSupportsThinking(option.model)}
tags={option.tags}
/>
</td>
@@ -88,21 +88,27 @@
<span class="inline-flex items-center gap-1.5 text-xs">
{#if isLoading}
<LoaderCircle class="h-3.5 w-3.5 animate-spin text-muted-foreground" />
<span>Loading</span>
{:else if isDownloading}
<LoaderCircle class="h-3.5 w-3.5 animate-spin text-muted-foreground" />
<span>Downloading</span>
{:else if isLoaded}
<span class="h-2 w-2 rounded-full bg-green-500"></span>
<span>{statusValue === ServerModelStatus.SLEEPING ? 'Sleeping' : 'Loaded'}</span>
{:else if isFailed}
<span class="h-2 w-2 rounded-full bg-red-500"></span>
<span>Failed</span>
{:else if statusValue === ServerModelStatus.DOWNLOADED}
<span class="h-2 w-2 rounded-full bg-muted-foreground/50"></span>
<span>Downloaded</span>
{:else}
<span class="h-2 w-2 rounded-full bg-muted-foreground/50"></span>
<span>Unloaded</span>
{/if}
</span>
@@ -125,57 +131,57 @@
{#if isLoading}
<ActionIcon
icon={X}
tooltip="Cancel load"
onclick={() => modelsStore.status.cancelLoad(option.model)}
tooltip="Cancel load"
/>
{:else if isDownloading}
<ActionIcon
icon={X}
tooltip="Cancel download"
onclick={() => modelsStore.status.cancelDownload(option.model)}
tooltip="Cancel download"
/>
{:else if isLoaded}
<ActionIcon
icon={PowerOff}
tooltip="Unload model"
onclick={() => modelsStore.status.unload(option.model)}
tooltip="Unload model"
/>
{:else}
<ActionIcon
icon={Power}
tooltip="Load model"
onclick={() => modelsStore.status.load(option.model)}
tooltip="Load model"
/>
{/if}
{#if canRemove && !isDownloading}
<ActionIcon
icon={Trash2}
tooltip="Delete model"
onclick={() => modelsStore.status.cancelDownload(option.model)}
tooltip="Delete model"
/>
{/if}
{/if}
<ActionIcon
icon={isFav ? HeartOff : Heart}
tooltip={isFav ? 'Remove from favorites' : 'Add to favorites'}
onclick={() => modelsStore.toggleFavorite(option.model)}
tooltip={isFav ? 'Remove from favorites' : 'Add to favorites'}
/>
<ActionIcon
icon={Copy}
tooltip="Copy model id"
onclick={() => copyToClipboard(option.model)}
tooltip="Copy model id"
/>
{#if hfLink}
<a
href={hfLink}
target="_blank"
rel="noreferrer"
class="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted-foreground/10 hover:text-foreground"
aria-label="View on HuggingFace"
class="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted-foreground/10 hover:text-foreground"
href={hfLink}
rel="noreferrer"
target="_blank"
title="View on HuggingFace"
>
<ExternalLink class="h-3.5 w-3.5" />
@@ -196,7 +202,7 @@
</p>
</div>
<Button size="sm" onclick={() => (modelsHubOpen = true)}>
<Button onclick={() => (modelsHubOpen = true)} size="sm">
<Plus class="h-4 w-4" />
Add more models
@@ -215,7 +221,7 @@
<div class="flex flex-col items-center justify-center gap-3 py-16 text-center">
<p class="text-sm text-muted-foreground">No models installed.</p>
<Button size="sm" variant="outline" onclick={() => (modelsHubOpen = true)}>
<Button onclick={() => (modelsHubOpen = true)} size="sm" variant="outline">
<Plus class="h-4 w-4" />
Add more models
@@ -227,7 +233,9 @@
<thead class="bg-muted/40 text-left text-xs text-muted-foreground">
<tr>
<th class="px-3 py-2 font-medium">Model</th>
<th class="px-3 py-2 font-medium">Status</th>
<th class="px-3 py-2 text-right font-medium">Actions</th>
</tr>
</thead>
@@ -235,14 +243,14 @@
<tbody>
{#each tree as parent (parent.parentId)}
<tr class="border-t bg-muted/20">
<td colspan="3" class="px-3 py-1.5 text-xs font-semibold text-muted-foreground">
<td class="px-3 py-1.5 text-xs font-semibold text-muted-foreground" colspan="3">
{parent.parentId}
</td>
</tr>
{#each parent.quantOrgs as org (org.repoId)}
<tr class="border-t bg-muted/10">
<td colspan="3" class="px-6 py-1.5 text-xs font-medium text-muted-foreground">
<td class="px-6 py-1.5 text-xs font-medium text-muted-foreground" colspan="3">
{org.repoId}
</td>
</tr>
@@ -22,7 +22,7 @@
}
</script>
<nav class={className} aria-label="Breadcrumb">
<nav aria-label="Breadcrumb" class={className}>
<ol class="flex list-none items-center gap-1.5 p-0 text-sm">
{#each items as item, i (item.label)}
<li class="flex items-center gap-1.5">
@@ -32,8 +32,8 @@
{#if item.href && i < items.length - 1}
<a
href={item.href}
class="text-muted-foreground transition-colors hover:text-foreground"
href={item.href}
onclick={(e) => {
e.preventDefault();
navigateTo(item.href);
@@ -109,13 +109,14 @@
}
</script>
<AlertDialog.Root {open} onOpenChange={handleOpenChange}>
<AlertDialog.Content onkeydown={handleKeydown} class="max-w-md">
<AlertDialog.Root onOpenChange={handleOpenChange} {open}>
<AlertDialog.Content class="max-w-md" onkeydown={handleKeydown}>
<AlertDialog.Header>
<AlertDialog.Title class="flex items-center gap-2">
<Download class="h-5 w-5 text-primary" />
Download this model?
</AlertDialog.Title>
<AlertDialog.Description>
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
@@ -128,6 +129,7 @@
role="status"
>
<TriangleAlert class="mt-0.5 h-4 w-4 shrink-0" />
<span>
A previous attempt for this tag failed and left partial files on disk. The server will
reject a fresh download until those files are removed. The Retry button below deletes
@@ -140,21 +142,27 @@
<div class="space-y-3 rounded-md border bg-muted/40 p-3 text-xs">
<div class="flex flex-col gap-1">
<span class="text-muted-foreground">Request</span>
<code class="break-all font-mono"
>POST /models&nbsp;&middot;&nbsp;{`{ model: "${hfRepoWithTag}" }`}</code
>
</div>
<div class="flex flex-col gap-1">
<span class="text-muted-foreground">File</span>
<code class="break-all font-mono">{filePath}</code>
</div>
<div class="flex flex-wrap items-center gap-2">
<span class="rounded bg-primary/15 px-2 py-0.5 font-mono font-semibold text-primary">
{tagDisplay}
</span>
{#if formattedSize}
<span class="text-muted-foreground">{formattedSize}</span>
{/if}
{#if variant}
<span
class="rounded bg-primary px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-primary-foreground"
@@ -173,6 +181,7 @@
<AlertDialog.Cancel disabled={phase === 'starting'} onclick={onCancel}>
Cancel
</AlertDialog.Cancel>
<AlertDialog.Action disabled={phase === 'starting'} onclick={trigger}>
{#if phase === 'starting'}
<LoaderCircle class="mr-1.5 h-4 w-4 animate-spin" />
@@ -72,17 +72,21 @@
Downloading
{/if}
</span>
<span class="font-mono text-xs tabular-nums text-muted-foreground">{percent}%</span>
</div>
<p class="truncate text-xs text-muted-foreground">{displayName}</p>
<DownloadProgressBar
downloadedBytes={progress?.downloadedBytes ?? 0}
totalBytes={progress?.totalBytes ?? 0}
/>
<button
type="button"
onclick={openDetails}
class="inline-flex items-center justify-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
onclick={openDetails}
type="button"
>
Open details
<ArrowUpRight class="h-3.5 w-3.5" />
@@ -119,16 +119,16 @@
class="flex shrink-0 items-center gap-2 border-b border-border/40 bg-background/90 px-4 py-3 backdrop-blur"
>
<div class="lg:hidden">
<ActionIcon icon={ArrowLeft} tooltip="Back to models" onclick={handleBack} />
<ActionIcon icon={ArrowLeft} onclick={handleBack} tooltip="Back to models" />
</div>
<h1 class="min-w-0 flex-1 truncate font-semibold">{modelId}</h1>
<button
type="button"
onclick={handleCopy}
class="inline-flex shrink-0 items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-medium text-muted-foreground transition-colors hover:border-border hover:text-foreground"
aria-label="Copy model id"
class="inline-flex shrink-0 items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-medium text-muted-foreground transition-colors hover:border-border hover:text-foreground"
onclick={handleCopy}
type="button"
>
{#if copied}
<Check class="h-3.5 w-3.5 text-primary" />
@@ -139,20 +139,21 @@
</button>
<a
href={`https://huggingface.co/${modelId}`}
target="_blank"
rel="noopener noreferrer"
class="inline-flex shrink-0 items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90"
href={`https://huggingface.co/${modelId}`}
rel="noopener noreferrer"
target="_blank"
>
<ExternalLink size={14} />
<span class="hidden sm:inline">View on HF</span>
</a>
</header>
<div class="min-h-0 flex-1 overflow-y-auto">
<Breadcrumb
items={[{ href: ROUTES.MANAGE_MODELS, label: 'Models' }, { label: modelId }]}
class="px-4 pt-4 md:px-6"
items={[{ href: ROUTES.MANAGE_MODELS, label: 'Models' }, { label: modelId }]}
/>
{#if error}
@@ -179,11 +180,13 @@
{author}
</span>
{/if}
{#if modelInfo.pipeline_tag}
<span class="rounded bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
{HuggingFaceService.pipelineTagLabel(modelInfo.pipeline_tag)}
</span>
{/if}
{#if modelInfo.library_name}
<span
class="rounded bg-secondary px-2 py-0.5 text-xs font-medium text-secondary-foreground"
@@ -191,6 +194,7 @@
{modelInfo.library_name}
</span>
{/if}
{#if modelInfo.gated === true}
<span
class="rounded bg-yellow-500/10 px-2 py-0.5 text-xs font-medium text-yellow-600 dark:text-yellow-400"
@@ -198,6 +202,7 @@
gated
</span>
{/if}
{#if licenseTag}
<span class="rounded bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground">
{licenseTag}
@@ -213,6 +218,7 @@
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground">
About
</h2>
<p class="text-sm whitespace-pre-line text-foreground/90">{description}</p>
</section>
{/if}
@@ -222,26 +228,32 @@
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground">
GGUF Specs
</h2>
<dl class="grid grid-cols-2 gap-x-4 gap-y-3 md:grid-cols-3">
{#if gguf.architecture}
<div>
<dt class="text-xs text-muted-foreground">Architecture</dt>
<dd class="text-sm font-medium capitalize">
{gguf.architecture.replace(/_/g, ' ')}
</dd>
</div>
{/if}
{#if gguf.total}
<div>
<dt class="text-xs text-muted-foreground">Total params</dt>
<dd class="text-sm font-medium tabular-nums">
{HuggingFaceService.formatFileSize(gguf.total).replace(' B', '')}B
</dd>
</div>
{/if}
{#if gguf.context_length}
<div>
<dt class="text-xs text-muted-foreground">Context length</dt>
<dd class="text-sm font-medium tabular-nums">
{gguf.context_length.toLocaleString()}
</dd>
@@ -257,6 +269,7 @@
<h2 class="mb-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Stats
</h2>
<dl class="space-y-2.5 text-sm">
{#if typeof modelInfo.downloads === 'number'}
<div class="flex items-center justify-between">
@@ -264,33 +277,40 @@
<Download size={13} />
Downloads
</dt>
<dd class="font-medium tabular-nums">
{HuggingFaceService.formatDownloads(modelInfo.downloads)}
</dd>
</div>
{/if}
{#if typeof modelInfo.likes === 'number'}
<div class="flex items-center justify-between">
<dt class="flex items-center gap-1.5 text-muted-foreground">
<Heart size={13} />
Likes
</dt>
<dd class="font-medium tabular-nums">
{HuggingFaceService.formatLikes(modelInfo.likes)}
</dd>
</div>
{/if}
{#if details?.lastModified}
<div class="flex items-center justify-between">
<dt class="text-muted-foreground">Last modified</dt>
<dd class="font-medium">
{HuggingFaceService.formatRelativeTime(details.lastModified)}
</dd>
</div>
{/if}
{#if gguf?.totalFileSize}
<div class="flex items-center justify-between">
<dt class="text-muted-foreground">Total size</dt>
<dd class="font-medium tabular-nums">
{HuggingFaceService.formatFileSize(gguf.totalFileSize)}
</dd>
@@ -302,7 +322,8 @@
{#if ggufFiles.length}
<section class="rounded-lg border bg-card p-4">
<header class="mb-3 flex items-center gap-1.5">
<Cpu size={13} class="text-muted-foreground" />
<Cpu class="text-muted-foreground" size={13} />
<h2 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Available files
</h2>
@@ -318,6 +339,7 @@
{row.bitDepth}-bit
{/if}
</div>
<div class="flex flex-wrap gap-1.5">
{#each row.files as file (file.path)}
{@const meta = HuggingFaceService.extractQuantMeta(file.path)}
@@ -349,7 +371,10 @@
? 'failed'
: 'idle'}
<button
type="button"
class:bg-muted={isFullyDownloaded && !isDownloading && !isFailed}
class:border-destructive={isFailed && !isDownloading}
class:border-foreground={isFullyDownloaded && !isDownloading && !isFailed}
class="relative inline-flex cursor-pointer items-center gap-1 overflow-hidden rounded-md border bg-background px-2 py-1 text-left font-mono text-xs transition-colors hover:border-primary/60 hover:bg-primary/5"
onclick={() =>
(pendingDownload = {
filePath: file.path,
@@ -357,10 +382,6 @@
sizeBytes: file.size ?? null,
variant: meta?.variant ?? null
})}
class="relative inline-flex cursor-pointer items-center gap-1 overflow-hidden rounded-md border bg-background px-2 py-1 text-left font-mono text-xs transition-colors hover:border-primary/60 hover:bg-primary/5"
class:border-foreground={isFullyDownloaded && !isDownloading && !isFailed}
class:bg-muted={isFullyDownloaded && !isDownloading && !isFailed}
class:border-destructive={isFailed && !isDownloading}
title={chipState === 'downloading'
? `In progress: ${file.path}. Click to view cancel options.`
: chipState === 'downloaded'
@@ -368,10 +389,12 @@
: chipState === 'failed'
? `Last attempt failed: ${file.path}. Click to delete partial files and retry.`
: `Download ${file.path}`}
type="button"
>
{#if isFullyDownloaded && !isDownloading}
<Check class="h-3 w-3 text-foreground/70" />
{/if}
{#if isFailed && !isDownloading && !isFullyDownloaded}
<span
class="rounded bg-destructive px-1 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-destructive-foreground"
@@ -379,6 +402,7 @@
Failed
</span>
{/if}
{#if meta?.variant && meta.variantForm === 'prefix'}
<span
class="rounded bg-primary px-1 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-primary-foreground"
@@ -386,7 +410,9 @@
{meta.variant}
</span>
{/if}
<span class="font-medium">{label}</span>
{#if meta?.variant && meta.variantForm === 'suffix'}
<span
class="rounded bg-primary px-1 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-primary-foreground"
@@ -394,6 +420,7 @@
{meta.variant}
</span>
{/if}
<span class="text-muted-foreground">
{#if isDownloading && downloadProgress && downloadProgress.totalBytes > 0}
{Math.round(
@@ -404,10 +431,11 @@
{HuggingFaceService.formatFileSize(file.size ?? 0)}
{/if}
</span>
{#if isDownloading && downloadProgress}
<DownloadProgressBar
overlay
downloadedBytes={downloadProgress.downloadedBytes}
overlay
totalBytes={downloadProgress.totalBytes}
/>
{/if}
@@ -433,14 +461,14 @@
if (!v) pendingDownload = null;
}
}
repoId={modelId}
filePath={pendingDownload.filePath}
quant={pendingDownload.quant}
variant={pendingDownload.variant}
formattedSize={pendingDownload.sizeBytes !== null
? HuggingFaceService.formatFileSize(pendingDownload.sizeBytes)
: ''}
onConfirm={() => (pendingDownload = null)}
onCancel={() => (pendingDownload = null)}
onConfirm={() => (pendingDownload = null)}
quant={pendingDownload.quant}
repoId={modelId}
variant={pendingDownload.variant}
/>
{/if}
@@ -150,6 +150,7 @@
<div class="shrink-0 space-y-3 border-b border-border/40 bg-background/80 p-3 backdrop-blur">
<div class="flex items-center justify-between gap-2">
<h1 class="text-base font-semibold">Models</h1>
{#if !loading}
<span class="text-xs text-muted-foreground">{filteredModels.length}</span>
{/if}
@@ -158,26 +159,27 @@
{#if !isSearching}
<div class="flex rounded-md border bg-muted/40 p-0.5 text-xs font-medium" role="tablist">
<button
type="button"
role="tab"
aria-selected={feed === 'trending'}
onclick={() => (feed = 'trending')}
class="flex flex-1 cursor-pointer items-center justify-center gap-1.5 rounded px-2 py-1 transition-colors {feed ===
'trending'
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'}"
onclick={() => (feed = 'trending')}
role="tab"
type="button"
>
Trending
</button>
<button
type="button"
role="tab"
aria-selected={feed === 'recommended'}
onclick={() => (feed = 'recommended')}
class="flex flex-1 cursor-pointer items-center justify-center gap-1.5 rounded px-2 py-1 transition-colors {feed ===
'recommended'
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'}"
onclick={() => (feed = 'recommended')}
role="tab"
type="button"
>
<Sparkles class="h-3 w-3 text-primary" />
ggml-org
@@ -187,13 +189,14 @@
<SearchInput
bind:value={searchQuery}
placeholder="Search models..."
onInput={handleSearchInput}
placeholder="Search models..."
/>
{#if !isSearching}
<label class="flex items-center gap-2 text-xs text-muted-foreground">
<span>Sort</span>
<select
bind:value={sortBy}
class="flex-1 rounded-md border bg-background px-2 py-1 text-xs font-medium text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
@@ -210,25 +213,26 @@
{#if !isSearching && availableFilters.length > 0}
<div class="flex flex-wrap items-center gap-1">
<button
type="button"
onclick={() => (activeFilter = null)}
class="inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-medium transition-colors {activeFilter ===
null
? 'border-primary bg-primary/10 text-primary'
: 'border-border bg-background text-muted-foreground hover:border-primary/40 hover:text-foreground'}"
onclick={() => (activeFilter = null)}
type="button"
>
All
</button>
{#each availableFilters as f (f.tag)}
{@const isActive = activeFilter === f.tag}
<button
type="button"
onclick={() => (activeFilter = isActive ? null : f.tag)}
class="inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-medium transition-colors {isActive
? 'border-primary bg-primary/10 text-primary'
: 'border-border bg-background text-muted-foreground hover:border-primary/40 hover:text-foreground'}"
onclick={() => (activeFilter = isActive ? null : f.tag)}
type="button"
>
<IconFromName name={HuggingFaceService.pipelineTagIcon(f.tag)} class="h-3 w-3" />
<IconFromName class="h-3 w-3" name={HuggingFaceService.pipelineTagIcon(f.tag)} />
{HuggingFaceService.pipelineTagLabel(f.tag)}
<span class="text-muted-foreground">{f.count}</span>
</button>
@@ -244,11 +248,13 @@
<p class="text-xs text-destructive">{error}</p>
</div>
{/if}
{#if searchError}
<div class="mx-1 rounded-lg border border-destructive/50 bg-destructive/5 p-4 text-center">
<p class="text-xs text-destructive">{searchError}</p>
</div>
{/if}
{#if loading || searchLoading}
<div class="flex items-center justify-center py-16">
<p class="text-xs text-muted-foreground">
@@ -270,38 +276,42 @@
{#each filteredModels as model (model.id)}
{@const isActive = model.id === selectedModelId}
<button
type="button"
onclick={() => openModelDetails(model)}
class="flex w-full cursor-pointer items-start gap-2.5 rounded-lg p-2.5 text-left transition-colors {isActive
? 'bg-primary/10 hover:bg-primary/15'
: 'hover:bg-muted/60'}"
onclick={() => openModelDetails(model)}
type="button"
>
<div
class="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary"
>
<IconFromName
name={HuggingFaceService.pipelineTagIcon(model.pipeline_tag)}
class="h-4 w-4"
name={HuggingFaceService.pipelineTagIcon(model.pipeline_tag)}
/>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center justify-between gap-2">
<span class="truncate text-sm font-medium">{model.id}</span>
<span class="shrink-0 text-[10px] text-muted-foreground">
{HuggingFaceService.formatRelativeTime(model.createdAt)}
</span>
</div>
{#if model.pipeline_tag}
<p class="mt-0.5 truncate text-xs text-muted-foreground">
{HuggingFaceService.pipelineTagLabel(model.pipeline_tag)}
</p>
{/if}
<div class="mt-1 flex items-center gap-3 text-xs text-muted-foreground">
<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)}
@@ -322,7 +332,7 @@
: 'hidden lg:flex'} h-full flex-1 lg:border-l lg:border-border/40"
>
{#if selectedModelId}
<ModelDetail modelId={selectedModelId} class="h-full" />
<ModelDetail class="h-full" modelId={selectedModelId} />
{:else}
<div
class="flex h-full items-center justify-center px-8 text-center text-sm text-muted-foreground"
@@ -331,6 +341,7 @@
<div class="flex h-14 w-14 items-center justify-center rounded-2xl bg-muted/60">
<Download class="h-6 w-6 text-muted-foreground/60" />
</div>
<p class="max-w-xs">
Select a model from the list to see its details and download options.
</p>