mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-17 20:31:47 +02:00
ui : merge download options and terminal commands into one panel
The quant and draft sidecar buttons become a multiple toggle group (selecting which files to download, not download triggers), and below the panel the terminal llama serve command updates live to reflect the selection, with a download CTA that fires the downloads for the selected entries (main + optional draft sidecar). Adds the shadcn-svelte toggle-group component. Assisted-by: pi
This commit is contained in:
Generated
+8
-5
@@ -71,7 +71,7 @@
|
||||
"svelte-check": "4.6.0",
|
||||
"svelte-sonner": "1.1.1",
|
||||
"tailwind-merge": "3.6.0",
|
||||
"tailwind-variants": "3.2.2",
|
||||
"tailwind-variants": "^3.3.1",
|
||||
"tailwindcss": "4.3.0",
|
||||
"tw-animate-css": "1.4.0",
|
||||
"typescript": "5.9.3",
|
||||
@@ -16048,13 +16048,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-variants": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz",
|
||||
"integrity": "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==",
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.3.1.tgz",
|
||||
"integrity": "sha512-4pAvwUtM4HKBiRZftncAbpn6V9Hhwoa5Fl7O2u5zbp7Z5Cvu+/o/6+176WY3WCEES209543quG8zFIcXCsc5Jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.x",
|
||||
"node": ">=16.9.x",
|
||||
"pnpm": ">=7.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -16064,6 +16064,9 @@
|
||||
"peerDependenciesMeta": {
|
||||
"tailwind-merge": {
|
||||
"optional": true
|
||||
},
|
||||
"tailwindcss": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
"svelte-check": "4.6.0",
|
||||
"svelte-sonner": "1.1.1",
|
||||
"tailwind-merge": "3.6.0",
|
||||
"tailwind-variants": "3.2.2",
|
||||
"tailwind-variants": "^3.3.1",
|
||||
"tailwindcss": "4.3.0",
|
||||
"tw-animate-css": "1.4.0",
|
||||
"typescript": "5.9.3",
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<script lang="ts">
|
||||
import ModelsDiscoverModelDetailsCommands from './ModelsDiscoverModelDetailsCommands.svelte';
|
||||
import ModelsDiscoverModelDetailsDownloadOptions from './ModelsDiscoverModelDetailsDownloadOptions.svelte';
|
||||
import ModelsDiscoverDetailsHeader from './ModelsDiscoverModelDetailsHeader.svelte';
|
||||
import ModelsDiscoverDetailsReadme from './ModelsDiscoverModelDetailsReadme.svelte';
|
||||
import { isAuxSidecar, type ModelSidecar } from '$lib/constants';
|
||||
import ModelsDiscoverModelDetailsHeader from './ModelsDiscoverModelDetailsHeader.svelte';
|
||||
import ModelsDiscoverModelDetailsReadme from './ModelsDiscoverModelDetailsReadme.svelte';
|
||||
import { isAuxSidecar } from '$lib/constants';
|
||||
import { HuggingFaceService } from '$lib/services';
|
||||
import type { HfModelDetailInfo, HfModelSibling } from '$lib/types/huggingface';
|
||||
import { detectThinkingSupport, detectToolUseSupport } from '$lib/utils';
|
||||
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
|
||||
interface Props {
|
||||
/** Full HuggingFace model id, e.g. `ggml-org/gemma-3-4b-it-GGUF`. */
|
||||
@@ -49,29 +48,8 @@
|
||||
|
||||
// Draft sidecars (mtp, dflash, dspark, eagle3) present in the repo, e.g.
|
||||
// speculative-decoding drafts. mmproj is excluded: it is vision.
|
||||
let draftSidecars = $derived.by<ModelSidecar[]>(() => {
|
||||
const set = new SvelteSet<ModelSidecar>();
|
||||
|
||||
for (const file of files) {
|
||||
const sidecar = HuggingFaceService.extractQuantMeta(file.path)?.sidecar;
|
||||
|
||||
if (sidecar && !isAuxSidecar(sidecar)) set.add(sidecar);
|
||||
}
|
||||
|
||||
return [...set];
|
||||
});
|
||||
|
||||
type BitDepthRow = { bitDepth: number; files: HfModelSibling[] };
|
||||
let quants = $derived(
|
||||
Array.from(
|
||||
new Set(
|
||||
files
|
||||
.map((f) => HuggingFaceService.extractQuantMeta(f.path)?.quant)
|
||||
.filter((q): q is string => Boolean(q))
|
||||
.map((q) => q.toUpperCase())
|
||||
)
|
||||
)
|
||||
);
|
||||
let bitDepthRows = $derived.by<BitDepthRow[]>(() => {
|
||||
const rows = new SvelteMap<number, HfModelSibling[]>();
|
||||
|
||||
@@ -105,7 +83,7 @@
|
||||
</div>
|
||||
{:else if details}
|
||||
<div class="space-y-6 p-6">
|
||||
<ModelsDiscoverDetailsHeader
|
||||
<ModelsDiscoverModelDetailsHeader
|
||||
{baseModels}
|
||||
{details}
|
||||
{gguf}
|
||||
@@ -118,8 +96,6 @@
|
||||
|
||||
<ModelsDiscoverModelDetailsDownloadOptions {bitDepthRows} {modelId} />
|
||||
|
||||
<ModelsDiscoverModelDetailsCommands {modelId} {quants} sidecars={draftSidecars} />
|
||||
|
||||
<ModelsDiscoverDetailsReadme {readme} />
|
||||
<ModelsDiscoverModelDetailsReadme {readme} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
-124
@@ -1,124 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Check, Copy, Server, SquareTerminal } from '@lucide/svelte';
|
||||
import { isAuxSidecar, type ModelSidecar } from '$lib/constants';
|
||||
import { ModelAuxSidecar, ModelDraftSidecar } from '$lib/enums';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
/** Full HuggingFace repo id, e.g. `ggml-org/gemma-3-4b-it-GGUF`. */
|
||||
modelId: string;
|
||||
/** Available quantization tags, e.g. `Q4_K_M`, `Q8_0`. */
|
||||
quants: string[];
|
||||
/** Draft sidecars present in the repo (mtp, dflash, dspark, eagle3). */
|
||||
sidecars?: ModelSidecar[];
|
||||
}
|
||||
|
||||
let { modelId, quants, sidecars = [] }: Props = $props();
|
||||
|
||||
let pickedQuant = $state<string | null>(null);
|
||||
let selectedQuant = $derived(pickedQuant ?? quants[0] ?? null);
|
||||
let selectedSidecar = $state<ModelSidecar | null>(null);
|
||||
|
||||
// llama.cpp --spec-type value for each draft sidecar.
|
||||
const SPEC_TYPE: Record<ModelSidecar, string> = {
|
||||
[ModelAuxSidecar.MMPROJ]: '',
|
||||
[ModelDraftSidecar.DFLASH]: 'draft-dflash',
|
||||
[ModelDraftSidecar.DSPARK]: 'draft-dspark',
|
||||
[ModelDraftSidecar.EAGLE3]: 'eagle3',
|
||||
[ModelDraftSidecar.MTP]: 'draft-mtp'
|
||||
};
|
||||
|
||||
let copiedIndex = $state<number | null>(null);
|
||||
|
||||
async function handleCopy(index: number, text: string) {
|
||||
await copyToClipboard(text);
|
||||
copiedIndex = index;
|
||||
setTimeout(() => (copiedIndex = null), 1500);
|
||||
}
|
||||
|
||||
// One box per binary (serve / cli). The command embeds inline selectors for
|
||||
// the quant and the draft sidecar type.
|
||||
let boxes = $derived.by(() => {
|
||||
const quant = selectedQuant ?? quants[0];
|
||||
|
||||
if (!quant) return [];
|
||||
|
||||
const draft = selectedSidecar && !isAuxSidecar(selectedSidecar) ? selectedSidecar : null;
|
||||
const specType = draft ? `--spec-type ${SPEC_TYPE[draft]}` : '';
|
||||
const build = (bin: string) => {
|
||||
const parts = ['llama', bin, '-hfd', modelId];
|
||||
|
||||
if (specType) parts.push(specType);
|
||||
|
||||
return parts.join(' ');
|
||||
};
|
||||
|
||||
return [
|
||||
{ command: build('serve'), icon: Server, title: 'Serve' },
|
||||
{ command: build('cli'), icon: SquareTerminal, title: 'CLI' }
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<span>Quant</span>
|
||||
|
||||
<select
|
||||
bind:value={selectedQuant}
|
||||
class="rounded border bg-background px-1.5 py-0.5 font-mono text-xs"
|
||||
>
|
||||
{#each quants as quant (quant)}
|
||||
<option value={quant}>{quant}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
{#if sidecars.length}
|
||||
<span>Draft</span>
|
||||
|
||||
<select
|
||||
bind:value={selectedSidecar}
|
||||
class="rounded border bg-background px-1.5 py-0.5 font-mono text-xs"
|
||||
>
|
||||
<option value={null}>none</option>
|
||||
|
||||
{#each sidecars as sidecar (sidecar)}
|
||||
<option value={sidecar}>{sidecar}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#each boxes as box, i (box.title)}
|
||||
<div
|
||||
class="overflow-hidden rounded-md"
|
||||
style="background: var(--code-background); border: 1px solid color-mix(in oklch, var(--border) 30%, transparent);"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-2 px-3 py-2"
|
||||
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>
|
||||
|
||||
<div class="flex items-center justify-between gap-2 p-2">
|
||||
<span class="truncate font-mono text-xs text-foreground/90">{box.command}</span>
|
||||
|
||||
<button
|
||||
aria-label="Copy command"
|
||||
class="shrink-0 text-muted-foreground/60 transition-colors hover:text-foreground"
|
||||
onclick={() => handleCopy(i, box.command)}
|
||||
type="button"
|
||||
>
|
||||
{#if copiedIndex === i}
|
||||
<Check class="h-3.5 w-3.5 text-green-500" />
|
||||
{:else}
|
||||
<Copy class="h-3.5 w-3.5" />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
+207
-77
@@ -1,14 +1,14 @@
|
||||
<script lang="ts">
|
||||
import DialogModelDownload from './DialogModelDownload.svelte';
|
||||
import DownloadProgressBar from './DownloadProgressBar.svelte';
|
||||
import { Download } from '@lucide/svelte';
|
||||
import { Check, Copy, Download } from '@lucide/svelte';
|
||||
import { ToggleGroup, ToggleGroupItem } from '$lib/components/ui/toggle-group';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { isAuxSidecar, type ModelSidecar } from '$lib/constants';
|
||||
import { ModelAuxSidecar, ModelDraftSidecar } from '$lib/enums';
|
||||
import { HuggingFaceService, ModelsService } from '$lib/services';
|
||||
import { modelsStore } from '$lib/stores';
|
||||
import type { ModelDownloadProgress } from '$lib/types';
|
||||
import type { HfModelSibling } from '$lib/types/huggingface';
|
||||
import { estimateModelMemoryBytes } from '$lib/utils';
|
||||
import { copyToClipboard, estimateModelMemoryBytes } from '$lib/utils';
|
||||
|
||||
/** Download state of a single repo entry, injected by the integration layer. */
|
||||
export interface DownloadEntryState {
|
||||
@@ -20,9 +20,8 @@
|
||||
|
||||
type BitDepthRow = { bitDepth: number; files: HfModelSibling[] };
|
||||
|
||||
interface PendingDownload {
|
||||
interface SelectedDownload {
|
||||
filePath: string;
|
||||
sizeBytes: number | null;
|
||||
quant: string | null;
|
||||
sidecar: ModelSidecar | null;
|
||||
}
|
||||
@@ -42,7 +41,39 @@
|
||||
|
||||
let { bitDepthRows, getDownloadState, modelId }: Props = $props();
|
||||
|
||||
let pendingDownload: PendingDownload | null = $state(null);
|
||||
let selectedPaths = $state<string[]>([]);
|
||||
|
||||
/** Kind of a file path: the main weights, a draft sidecar, or an aux sidecar (mmproj). */
|
||||
function classify(path: string): 'main' | 'draft' | 'aux' {
|
||||
const sidecar = HuggingFaceService.extractQuantMeta(path)?.sidecar;
|
||||
|
||||
if (!sidecar) return 'main';
|
||||
|
||||
return isAuxSidecar(sidecar) ? 'aux' : 'draft';
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrained selection: at most one base model and one draft sidecar.
|
||||
* Aux sidecars (mmproj) are unconstrained.
|
||||
*/
|
||||
function handleSelection(next: string[]) {
|
||||
const added = next.find((p) => !selectedPaths.includes(p));
|
||||
|
||||
if (!added) {
|
||||
selectedPaths = next;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const kind = classify(added);
|
||||
const base = kind === 'aux' ? selectedPaths : selectedPaths.filter((p) => classify(p) !== kind);
|
||||
|
||||
selectedPaths = [...base, added];
|
||||
}
|
||||
let copied = $state(false);
|
||||
|
||||
/** All repo files in one lookup by path. */
|
||||
let fileByPath = $derived(new Map(bitDepthRows.flatMap((r) => r.files).map((f) => [f.path, f])));
|
||||
|
||||
function stateFor(repoWithTag: string, filePath: string, isSidecar: boolean): DownloadEntryState {
|
||||
if (getDownloadState) return getDownloadState(repoWithTag, filePath, isSidecar);
|
||||
@@ -57,6 +88,47 @@
|
||||
};
|
||||
}
|
||||
|
||||
/** Selection ordered main-quant-first, so the command reads naturally. */
|
||||
let selected: SelectedDownload[] = $derived.by(() => {
|
||||
const mains: SelectedDownload[] = [];
|
||||
const sidecars: SelectedDownload[] = [];
|
||||
|
||||
for (const path of selectedPaths) {
|
||||
const file = fileByPath.get(path);
|
||||
|
||||
if (!file) continue;
|
||||
|
||||
const meta = HuggingFaceService.extractQuantMeta(file.path);
|
||||
const entry = {
|
||||
filePath: file.path,
|
||||
quant: meta?.quant ?? null,
|
||||
sidecar: meta?.sidecar ?? null
|
||||
};
|
||||
|
||||
if (entry.sidecar && !isAuxSidecar(entry.sidecar)) sidecars.push(entry);
|
||||
else mains.push(entry);
|
||||
}
|
||||
|
||||
return [...mains, ...sidecars];
|
||||
});
|
||||
|
||||
/** First selected main quant, drives the `-hf <repo>:<quant>` tag. */
|
||||
let primaryQuant = $derived(selected.find((s) => !s.sidecar)?.quant ?? null);
|
||||
|
||||
/** First selected draft sidecar, drives the `--spec-type` flag. */
|
||||
let draft = $derived(
|
||||
selected.find((s) => s.sidecar && !isAuxSidecar(s.sidecar))?.sidecar ?? null
|
||||
);
|
||||
|
||||
// llama.cpp --spec-type value for each draft sidecar.
|
||||
const SPEC_TYPE: Record<ModelSidecar, string> = {
|
||||
[ModelAuxSidecar.MMPROJ]: '',
|
||||
[ModelDraftSidecar.DFLASH]: 'draft-dflash',
|
||||
[ModelDraftSidecar.DSPARK]: 'draft-dspark',
|
||||
[ModelDraftSidecar.EAGLE3]: 'eagle3',
|
||||
[ModelDraftSidecar.MTP]: 'draft-mtp'
|
||||
};
|
||||
|
||||
function buttonClass(parts: { isDownloaded: boolean; isFailed: boolean }): string {
|
||||
const classes = [
|
||||
'relative inline-flex items-center gap-1 overflow-hidden rounded-md border bg-muted px-2 py-1 text-left font-mono text-xs transition-colors'
|
||||
@@ -70,20 +142,62 @@
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
async function copyCommand() {
|
||||
await copyToClipboard(serveCommand);
|
||||
copied = true;
|
||||
setTimeout(() => (copied = false), 1500);
|
||||
}
|
||||
|
||||
/** Fire downloads for every selected entry. */
|
||||
function downloadSelected() {
|
||||
for (const sel of selected) {
|
||||
const tag = ModelsService.buildDownloadTag(modelId, sel.quant, sel.sidecar);
|
||||
|
||||
if (modelsStore.status.hasFailedDownload(tag)) {
|
||||
void modelsStore.status
|
||||
.cancelDownload(tag)
|
||||
.then(() => modelsStore.status.downloadModel(tag, sel.filePath));
|
||||
} else {
|
||||
void modelsStore.status.downloadModel(tag, sel.filePath);
|
||||
}
|
||||
}
|
||||
|
||||
selectedPaths = [];
|
||||
}
|
||||
|
||||
/** The llama serve command for the current selection. */
|
||||
let serveCommand = $derived.by(() => {
|
||||
const quantTag = primaryQuant ? `${modelId}:${primaryQuant}` : modelId;
|
||||
const parts = ['llama', 'serve', '-hf', quantTag];
|
||||
|
||||
if (draft) parts.push('-hfd', modelId, '--spec-type', SPEC_TYPE[draft]);
|
||||
|
||||
return parts.join(' ');
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if bitDepthRows.length}
|
||||
<section class="rounded-xl border">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 px-4 pt-3 pb-1">
|
||||
<section class="space-y-3 rounded-xl border p-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<h2 class="flex items-center gap-1.5 text-sm font-medium text-muted-foreground">
|
||||
<Download class="h-4 w-4" />
|
||||
Downloadable options
|
||||
</h2>
|
||||
|
||||
{#if selectedPaths.length}
|
||||
<span class="text-xs text-muted-foreground">{selected.length} selected</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="divide-y px-4 pb-1">
|
||||
<ToggleGroup
|
||||
class="flex flex-col"
|
||||
onValueChange={handleSelection}
|
||||
type="multiple"
|
||||
value={selectedPaths}
|
||||
>
|
||||
{#each bitDepthRows as row (row.bitDepth)}
|
||||
<div class="grid grid-cols-[5rem_1fr] items-start gap-3 py-3">
|
||||
<div class="grid grid-cols-[5rem_1fr] items-start gap-3 py-2">
|
||||
<div class="pt-1 text-sm tabular-nums text-muted-foreground">
|
||||
{#if row.bitDepth === 99}
|
||||
Other
|
||||
@@ -113,58 +227,55 @@
|
||||
: isDownloaded
|
||||
? `Already downloaded: ${file.path}`
|
||||
: isFailed
|
||||
? `Last attempt failed: ${file.path}. Click to retry.`
|
||||
? `Last attempt failed: ${file.path}`
|
||||
: `Download ${file.path} (requires ~${memoryGb} GB of memory)`}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger
|
||||
class={buttonClass({ isDownloaded, isFailed })}
|
||||
onclick={() => {
|
||||
pendingDownload = {
|
||||
filePath: file.path,
|
||||
quant: meta?.quant ?? null,
|
||||
sidecar: meta?.sidecar ?? null,
|
||||
sizeBytes: file.size ?? null
|
||||
};
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if isFailed && !isDownloading && !isDownloaded}
|
||||
<span
|
||||
class="rounded bg-destructive px-1 py-0.5 text-[10px] font-semibold tracking-wide text-destructive-foreground uppercase"
|
||||
>
|
||||
Failed
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if meta?.sidecar && !isAuxSidecar(meta.sidecar)}
|
||||
<span
|
||||
class="rounded bg-primary px-1 py-0.5 text-[10px] font-semibold tracking-wide text-primary-foreground uppercase"
|
||||
>
|
||||
{meta.sidecar}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<span class="font-medium {isDownloaded ? '' : 'text-muted-foreground/80'}"
|
||||
>{label}</span
|
||||
<Tooltip.Trigger>
|
||||
<ToggleGroupItem
|
||||
aria-label={tooltipText}
|
||||
class="relative inline-flex h-auto items-center gap-1 overflow-hidden rounded-md border bg-muted px-2 py-1 text-left font-mono text-xs transition-colors data-[state=on]:border-primary data-[state=on]:bg-primary/10 {buttonClass(
|
||||
{ isDownloaded, isFailed }
|
||||
)}"
|
||||
value={file.path}
|
||||
>
|
||||
|
||||
<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)}%
|
||||
{:else}
|
||||
{HuggingFaceService.formatFileSize(file.size ?? 0)}
|
||||
{#if isFailed && !isDownloading && !isDownloaded}
|
||||
<span
|
||||
class="rounded bg-destructive px-1 py-0.5 text-[10px] font-semibold tracking-wide text-destructive-foreground uppercase"
|
||||
>
|
||||
Failed
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
{#if isDownloading && progress}
|
||||
<DownloadProgressBar
|
||||
downloadedBytes={progress.downloadedBytes}
|
||||
overlay
|
||||
totalBytes={progress.totalBytes}
|
||||
/>
|
||||
{/if}
|
||||
{#if meta?.sidecar && !isAuxSidecar(meta.sidecar)}
|
||||
<span
|
||||
class="rounded bg-primary px-1 py-0.5 text-[10px] font-semibold tracking-wide text-primary-foreground uppercase"
|
||||
>
|
||||
{meta.sidecar}
|
||||
</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)}%
|
||||
{:else}
|
||||
{HuggingFaceService.formatFileSize(file.size ?? 0)}
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
{#if isDownloading && progress}
|
||||
<DownloadProgressBar
|
||||
downloadedBytes={progress.downloadedBytes}
|
||||
overlay
|
||||
totalBytes={progress.totalBytes}
|
||||
/>
|
||||
{/if}
|
||||
</ToggleGroupItem>
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
@@ -175,25 +286,44 @@
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</ToggleGroup>
|
||||
|
||||
<!-- Terminal command + download CTA for the current selection -->
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 rounded-md px-3 py-2"
|
||||
style="background: var(--code-background); border: 1px solid color-mix(in oklch, var(--border) 30%, transparent);"
|
||||
>
|
||||
<span class="truncate font-mono text-xs text-foreground/90">{serveCommand}</span>
|
||||
|
||||
<button
|
||||
aria-label="Copy command"
|
||||
class="shrink-0 text-muted-foreground/60 transition-colors hover:text-foreground"
|
||||
onclick={copyCommand}
|
||||
type="button"
|
||||
>
|
||||
{#if copied}
|
||||
<Check class="h-3.5 w-3.5 text-green-500" />
|
||||
{:else}
|
||||
<Copy class="h-3.5 w-3.5" />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="inline-flex w-full cursor-pointer items-center justify-center gap-2 rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
disabled={selected.length === 0}
|
||||
onclick={downloadSelected}
|
||||
type="button"
|
||||
>
|
||||
<Download class="h-4 w-4" />
|
||||
|
||||
{#if primaryQuant && draft}
|
||||
Download {primaryQuant} + {draft}
|
||||
{:else}
|
||||
Download
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if pendingDownload}
|
||||
<DialogModelDownload
|
||||
bind:open={
|
||||
() => pendingDownload !== null,
|
||||
(v) => {
|
||||
if (!v) pendingDownload = null;
|
||||
}
|
||||
}
|
||||
filePath={pendingDownload.filePath}
|
||||
formattedSize={pendingDownload.sizeBytes != null
|
||||
? HuggingFaceService.formatFileSize(pendingDownload.sizeBytes)
|
||||
: undefined}
|
||||
onClose={() => (pendingDownload = null)}
|
||||
quant={pendingDownload.quant}
|
||||
repoId={modelId}
|
||||
sidecar={pendingDownload.sidecar}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -84,13 +84,6 @@ export { default as ModelsDiscoverChatTemplateDialog } from './ModelsDiscoverCha
|
||||
*/
|
||||
export { default as ModelsDiscoverModelDetailsReadme } from './ModelsDiscoverModelDetailsReadme.svelte';
|
||||
|
||||
/**
|
||||
* **TerminalCommands** - Terminal command block
|
||||
*
|
||||
* Shows the `llama serve` / `llama cli` commands for a model with copy buttons.
|
||||
*/
|
||||
export { default as ModelsDiscoverModelDetailsCommands } from './ModelsDiscoverModelDetailsCommands.svelte';
|
||||
|
||||
/**
|
||||
* **DialogModelDownload** - Download confirmation / progress dialog
|
||||
*
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import Root from './toggle-group.svelte';
|
||||
import Item from './toggle-group-item.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Item,
|
||||
//
|
||||
Root as ToggleGroup,
|
||||
Item as ToggleGroupItem
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { getToggleGroupCtx } from './toggle-group.svelte';
|
||||
import { type ToggleVariants, toggleVariants } from '$lib/components/ui/toggle/index.js';
|
||||
import { cn } from '$lib/components/ui/utils.js';
|
||||
import { ToggleGroup as ToggleGroupPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
ref = $bindable(null),
|
||||
size,
|
||||
value = $bindable(),
|
||||
variant,
|
||||
...restProps
|
||||
}: ToggleGroupPrimitive.ItemProps & ToggleVariants = $props();
|
||||
|
||||
const ctx = getToggleGroupCtx();
|
||||
</script>
|
||||
|
||||
<ToggleGroupPrimitive.Item
|
||||
bind:ref
|
||||
class={cn(
|
||||
'group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-lg group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-lg group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-lg group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-lg shrink-0 focus:z-10 focus-visible:z-10 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t',
|
||||
toggleVariants({
|
||||
size: ctx.size || size,
|
||||
variant: ctx.variant || variant
|
||||
}),
|
||||
className
|
||||
)}
|
||||
data-size={ctx.size || size}
|
||||
data-slot="toggle-group-item"
|
||||
data-spacing={ctx.spacing}
|
||||
data-variant={ctx.variant || variant}
|
||||
{value}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script lang="ts" module>
|
||||
import { toggleVariants } from '$lib/components/ui/toggle/index.js';
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import type { VariantProps } from 'tailwind-variants';
|
||||
|
||||
type ToggleVariants = VariantProps<typeof toggleVariants>;
|
||||
|
||||
interface ToggleGroupContext extends ToggleVariants {
|
||||
spacing?: number;
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
}
|
||||
|
||||
export function setToggleGroupCtx(props: ToggleGroupContext) {
|
||||
setContext('toggleGroup', props);
|
||||
}
|
||||
|
||||
export function getToggleGroupCtx() {
|
||||
return getContext<Required<ToggleGroupContext>>('toggleGroup');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/components/ui/utils.js';
|
||||
import { ToggleGroup as ToggleGroupPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
orientation = 'horizontal',
|
||||
ref = $bindable(null),
|
||||
size = 'default',
|
||||
spacing = 0,
|
||||
value = $bindable(),
|
||||
variant = 'default',
|
||||
...restProps
|
||||
}: ToggleGroupPrimitive.RootProps &
|
||||
ToggleVariants & {
|
||||
spacing?: number;
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
} = $props();
|
||||
|
||||
setToggleGroupCtx({
|
||||
get orientation() {
|
||||
return orientation;
|
||||
},
|
||||
get size() {
|
||||
return size;
|
||||
},
|
||||
get spacing() {
|
||||
return spacing;
|
||||
},
|
||||
get variant() {
|
||||
return variant;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!--
|
||||
Discriminated Unions + Destructing (required for bindable) do not
|
||||
get along, so we shut typescript up by casting `value` to `never`.
|
||||
-->
|
||||
<ToggleGroupPrimitive.Root
|
||||
bind:ref
|
||||
bind:value={value as never}
|
||||
class={cn(
|
||||
'rounded-lg data-[size=sm]:rounded-[min(var(--radius-md),10px)] group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-vertical:flex-col data-vertical:items-stretch',
|
||||
className
|
||||
)}
|
||||
data-size={size}
|
||||
data-slot="toggle-group"
|
||||
data-spacing={spacing}
|
||||
data-variant={variant}
|
||||
{orientation}
|
||||
style={`--gap: ${spacing}`}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,13 @@
|
||||
import Root from './toggle.svelte';
|
||||
export {
|
||||
toggleVariants,
|
||||
type ToggleSize,
|
||||
type ToggleVariant,
|
||||
type ToggleVariants
|
||||
} from './toggle.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Toggle
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
<script lang="ts" module>
|
||||
import { tv, type VariantProps } from 'tailwind-variants';
|
||||
|
||||
export const toggleVariants = tv({
|
||||
base: "gap-1 rounded-lg text-sm font-medium transition-all hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 group/toggle inline-flex items-center justify-center whitespace-nowrap outline-none hover:bg-muted focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
defaultVariants: {
|
||||
size: 'default',
|
||||
variant: 'default'
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
default:
|
||||
'h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
||||
lg: 'h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
||||
sm: "h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5"
|
||||
},
|
||||
variant: {
|
||||
default: 'bg-transparent',
|
||||
outline: 'border border-input bg-transparent hover:bg-muted'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export type ToggleVariant = VariantProps<typeof toggleVariants>['variant'];
|
||||
export type ToggleSize = VariantProps<typeof toggleVariants>['size'];
|
||||
export type ToggleVariants = VariantProps<typeof toggleVariants>;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/components/ui/utils.js';
|
||||
import { Toggle as TogglePrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
pressed = $bindable(false),
|
||||
ref = $bindable(null),
|
||||
size = 'default',
|
||||
variant = 'default',
|
||||
...restProps
|
||||
}: TogglePrimitive.RootProps & {
|
||||
variant?: ToggleVariant;
|
||||
size?: ToggleSize;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<TogglePrimitive.Root
|
||||
bind:pressed
|
||||
bind:ref
|
||||
class={cn(toggleVariants({ size, variant }), className)}
|
||||
data-slot="toggle"
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -3,11 +3,9 @@
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import ModelsDiscoverChatTemplateDialog from '$lib/components/app/models/discover/ModelsDiscoverChatTemplateDialog.svelte';
|
||||
import ModelsDiscoverModelDetails from '$lib/components/app/models/discover/ModelsDiscoverModelDetails.svelte';
|
||||
import ModelsDiscoverModelDetailsCommands from '$lib/components/app/models/discover/ModelsDiscoverModelDetailsCommands.svelte';
|
||||
import ModelsDiscoverModelDetailsDownloadOptions from '$lib/components/app/models/discover/ModelsDiscoverModelDetailsDownloadOptions.svelte';
|
||||
import ModelsDiscoverModelDetailsHeader from '$lib/components/app/models/discover/ModelsDiscoverModelDetailsHeader.svelte';
|
||||
import ModelsDiscoverModelDetailsReadme from '$lib/components/app/models/discover/ModelsDiscoverModelDetailsReadme.svelte';
|
||||
import { ModelDraftSidecar } from '$lib/enums';
|
||||
import type { HfModelSibling } from '$lib/types';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
@@ -72,25 +70,6 @@
|
||||
</div>
|
||||
</Story>
|
||||
|
||||
<Story name="Terminal commands (no sidecars)">
|
||||
<div class="w-200 p-4">
|
||||
<ModelsDiscoverModelDetailsCommands
|
||||
modelId="ggml-org/Qwen3.8-27B-GGUF"
|
||||
quants={['Q4_K_M', 'Q8_0']}
|
||||
/>
|
||||
</div>
|
||||
</Story>
|
||||
|
||||
<Story name="Terminal commands (MTP sidecar)">
|
||||
<div class="w-200 p-4">
|
||||
<ModelsDiscoverModelDetailsCommands
|
||||
modelId="ggml-org/gemma-4-12b-it-GGUF"
|
||||
quants={['Q4_K_M', 'Q8_0']}
|
||||
sidecars={[ModelDraftSidecar.MTP]}
|
||||
/>
|
||||
</div>
|
||||
</Story>
|
||||
|
||||
<Story name="Readme">
|
||||
<div class="w-160 p-4">
|
||||
<ModelsDiscoverModelDetailsReadme {readme} />
|
||||
|
||||
Reference in New Issue
Block a user