mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-08-25 22:21:03 +02:00
ui : apply ESLint formatting to models discover components
Assisted-by: Claude
This commit is contained in:
+1
@@ -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}
|
||||
|
||||
+1
@@ -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 </span>
|
||||
|
||||
+1
@@ -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 </span>
|
||||
|
||||
+1
@@ -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 · {`{ 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>
|
||||
|
||||
+4
-3
@@ -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>
|
||||
|
||||
+21
-13
@@ -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}
|
||||
|
||||
+23
-10
@@ -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}
|
||||
|
||||
@@ -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 · {`{ 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>
|
||||
|
||||
Reference in New Issue
Block a user