mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-14 18:02:52 +02:00
ui : shared model display primitives
Extract ModelCapabilityIcons (canonical Tools/Reasoning/Vision/Video/Audio order) out of ModelId and reuse it there, add the shared DialogConfirmDownload for destructive download actions, the discover org avatar with dark-mode inversion and the thin download progress bar, and rework ModelId badges to take thinking/tool-use support directly. Assisted-by: pi:GLM-5.3-Flash
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import DialogConfirmation from '$lib/components/app/dialogs/DialogConfirmation.svelte';
|
||||
import { ModelDownloadConfirmAction } from '$lib/enums';
|
||||
import { modelsStore } from '$lib/stores';
|
||||
|
||||
interface Props {
|
||||
/** Action being confirmed; drives the wording. */
|
||||
action: ModelDownloadConfirmAction;
|
||||
/** `<repo>:<tag>` the action targets. */
|
||||
repoWithTag: string;
|
||||
onClose: () => void;
|
||||
/** Overrides the default store removal; defaults to removing the entry. */
|
||||
onConfirm?: (repoWithTag: string) => void;
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
let { action, onClose, onConfirm, open = true, repoWithTag }: Props = $props();
|
||||
|
||||
// Both actions resolve through the same store removal (cancelDownload drops a
|
||||
// running download's partial files or a cached model's files); only the copy
|
||||
// differs. One component so the discover chips and the selector rows word the
|
||||
// destructive confirmations identically.
|
||||
const COPY = {
|
||||
[ModelDownloadConfirmAction.CANCEL]: {
|
||||
cancelText: 'Keep downloading',
|
||||
confirmText: 'Cancel download',
|
||||
description: (name: string) =>
|
||||
`This stops the download of ${name} and removes the partial files. Pause it instead to keep the progress.`,
|
||||
title: 'Cancel download'
|
||||
},
|
||||
[ModelDownloadConfirmAction.DELETE]: {
|
||||
cancelText: 'Keep model',
|
||||
confirmText: 'Delete',
|
||||
description: (name: string) =>
|
||||
`This permanently removes ${name} from disk. You can download it again later.`,
|
||||
title: 'Delete model'
|
||||
}
|
||||
} as const;
|
||||
|
||||
let copy = $derived(COPY[action]);
|
||||
let displayName = $derived(modelsStore.toDisplayName(repoWithTag));
|
||||
|
||||
function confirm() {
|
||||
if (onConfirm) onConfirm(repoWithTag);
|
||||
else void modelsStore.status.cancelDownload(repoWithTag);
|
||||
|
||||
onClose();
|
||||
}
|
||||
</script>
|
||||
|
||||
<DialogConfirmation
|
||||
cancelText={copy.cancelText}
|
||||
confirmText={copy.confirmText}
|
||||
description={copy.description(displayName)}
|
||||
onCancel={onClose}
|
||||
onConfirm={confirm}
|
||||
{open}
|
||||
title={copy.title}
|
||||
variant="destructive"
|
||||
/>
|
||||
@@ -108,6 +108,17 @@ export { default as DialogExportSettings } from './DialogExportSettings.svelte';
|
||||
*/
|
||||
export { default as DialogConfirmation } from './DialogConfirmation.svelte';
|
||||
|
||||
/**
|
||||
* **DialogConfirmDownload** - Confirm a destructive download action
|
||||
*
|
||||
* Shared confirmation for stopping/cancelling an in-flight download or deleting
|
||||
* a downloaded model, used by the discover quant chips and the model selector's
|
||||
* download rows so both word the action identically. Owns the copy and the
|
||||
* default store removal; render one instance per surface keyed by the acted-on
|
||||
* repo:tag.
|
||||
*/
|
||||
export { default as DialogConfirmDownload } from './DialogConfirmDownload.svelte';
|
||||
|
||||
/**
|
||||
* **DialogConversationRename** - Rename a conversation
|
||||
*
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
import { Image, Lightbulb, Mic, Video, Wrench } from '@lucide/svelte';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import type { ModelModalities } from '$lib/types/models';
|
||||
|
||||
interface Props {
|
||||
modalities?: ModelModalities;
|
||||
supportsThinking?: boolean;
|
||||
supportsToolUse?: boolean;
|
||||
hideCapabilities?: boolean;
|
||||
hideModalities?: boolean;
|
||||
iconSize?: string;
|
||||
gapClass?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
gapClass = 'gap-1.25',
|
||||
hideCapabilities = false,
|
||||
hideModalities = false,
|
||||
iconSize = 'h-3 w-3',
|
||||
modalities,
|
||||
supportsThinking = false,
|
||||
supportsToolUse = false
|
||||
}: Props = $props();
|
||||
|
||||
let hasModalityIcons = $derived(modalities?.vision || modalities?.video || modalities?.audio);
|
||||
</script>
|
||||
|
||||
<span class="inline-flex items-center {gapClass}">
|
||||
{#if supportsToolUse && !hideCapabilities}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Wrench class="{iconSize} text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>Tool use</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
|
||||
{#if supportsThinking && !hideCapabilities}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Lightbulb class="{iconSize} text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>Reasoning</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
|
||||
{#if hasModalityIcons && !hideModalities}
|
||||
<span class="inline-flex items-center text-muted-foreground">
|
||||
{#if modalities?.vision}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Image class={iconSize} />
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>Vision</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
|
||||
{#if modalities?.video}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Video class={iconSize} />
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>Video</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
|
||||
{#if modalities?.audio}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Mic class={iconSize} />
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>Audio</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
@@ -1,45 +1,66 @@
|
||||
<script lang="ts">
|
||||
import ModelCapabilityIcons from './ModelCapabilityIcons.svelte';
|
||||
import { Database, ScrollText } from '@lucide/svelte';
|
||||
import { TruncatedText } from '$lib/components/app';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import {
|
||||
CAPABILITY_FLAG_KEYS,
|
||||
CAPABILITY_ICONS,
|
||||
CAPABILITY_LABELS,
|
||||
MODALITY_FLAG_KEYS,
|
||||
MODALITY_ICONS,
|
||||
MODALITY_LABELS
|
||||
} from '$lib/constants';
|
||||
import { ModelCapability, ModelModality } from '$lib/enums';
|
||||
import { type ModelSidecar } from '$lib/constants';
|
||||
import { HuggingFaceService } from '$lib/services';
|
||||
import { ModelsService } from '$lib/services/models.service';
|
||||
import { settingsStore } from '$lib/stores';
|
||||
import type { ModelCapabilities, ModelModalities } from '$lib/types/models';
|
||||
import type { ModelModalities } from '$lib/types/models';
|
||||
import { isAuxSidecar } from '$lib/utils';
|
||||
import { formatParameters } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
modelId: string;
|
||||
hideOrgName?: boolean;
|
||||
hideName?: boolean;
|
||||
hideModalities?: boolean;
|
||||
hideCapabilities?: boolean;
|
||||
hideParameters?: boolean;
|
||||
showRaw?: boolean;
|
||||
showRawTooltip?: boolean;
|
||||
hideQuantization?: boolean;
|
||||
hideTags?: boolean;
|
||||
aliases?: string[];
|
||||
tags?: string[];
|
||||
/** Render the capability/modality/context icons on a second row. */
|
||||
iconsOnNewLine?: boolean;
|
||||
modalities?: ModelModalities;
|
||||
capabilities?: ModelCapabilities;
|
||||
supportsThinking?: boolean;
|
||||
supportsToolUse?: boolean;
|
||||
/** Context length in tokens; renders a context icon when set. */
|
||||
contextLength?: number;
|
||||
/** Min/max GGUF file size (main + draft) across quants; renders a range when set. */
|
||||
sizeRange?: { min: number; max: number } | null;
|
||||
draftSidecars?: ModelSidecar[];
|
||||
/** Allow badges to wrap onto new lines instead of truncating. */
|
||||
wrap?: boolean;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
aliases,
|
||||
capabilities,
|
||||
class: className = '',
|
||||
contextLength,
|
||||
draftSidecars = [],
|
||||
hideCapabilities = false,
|
||||
hideModalities = false,
|
||||
hideName = false,
|
||||
hideOrgName = false,
|
||||
hideParameters = false,
|
||||
hideQuantization,
|
||||
hideTags,
|
||||
iconsOnNewLine = false,
|
||||
modalities,
|
||||
modelId,
|
||||
showRaw = undefined,
|
||||
showRawTooltip = false,
|
||||
sizeRange,
|
||||
supportsThinking = false,
|
||||
supportsToolUse = false,
|
||||
tags,
|
||||
wrap = false,
|
||||
...rest
|
||||
}: Props = $props();
|
||||
|
||||
@@ -47,6 +68,11 @@
|
||||
'inline-flex w-fit shrink-0 items-center justify-center whitespace-nowrap rounded-md border border-border/50 px-1 py-0 text-[10px] font-mono bg-foreground/15 dark:bg-foreground/10 text-foreground [a&]:hover:bg-foreground/25';
|
||||
const tagBadgeClass =
|
||||
'inline-flex w-fit shrink-0 items-center justify-center whitespace-nowrap rounded-md border border-border/50 px-1 py-0 text-[10px] font-mono text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground';
|
||||
const variantBadgeClass =
|
||||
'inline-flex w-fit shrink-0 items-center justify-center whitespace-nowrap rounded-md bg-primary px-1.5 py-0 text-[10px] font-mono font-semibold uppercase tracking-wide text-primary-foreground';
|
||||
|
||||
/** Alias badges beyond this many collapse into a single `+x more` badge. */
|
||||
const MAX_ALIAS_BADGES = 2;
|
||||
|
||||
let parsed = $derived(ModelsService.parseModelId(modelId));
|
||||
let resolvedShowRaw = $derived(
|
||||
@@ -59,104 +85,147 @@
|
||||
|
||||
let uniqueAliases = $derived([...new Set(aliases ?? [])]);
|
||||
let uniqueTags = $derived([...new Set([...(parsed.tags ?? []), ...(tags ?? [])])]);
|
||||
|
||||
const allModalities = [ModelModality.VISION, ModelModality.VIDEO, ModelModality.AUDIO] as const;
|
||||
const allCapabilities: ModelCapability[] = [ModelCapability.REASONING];
|
||||
|
||||
let activeModalities = $derived(
|
||||
allModalities.filter((modality) => modalities?.[MODALITY_FLAG_KEYS[modality]])
|
||||
);
|
||||
let activeCapabilities = $derived(
|
||||
allCapabilities.filter((capability) => capabilities?.[CAPABILITY_FLAG_KEYS[capability]])
|
||||
);
|
||||
let uniqueDraftSidecars = $derived([...new Set(draftSidecars)].filter((s) => !isAuxSidecar(s)));
|
||||
|
||||
let primaryAlias = $derived(uniqueAliases.length === 1 ? uniqueAliases[0] : null);
|
||||
let displayName = $derived(primaryAlias ?? parsed.modelName ?? modelId);
|
||||
|
||||
let hasBadges = $derived(
|
||||
parsed.sidecar ||
|
||||
(parsed.params && !hideParameters) ||
|
||||
(parsed.quantization && !resolvedHideQuantization) ||
|
||||
primaryAlias ||
|
||||
uniqueAliases.length > 1 ||
|
||||
(uniqueTags.length > 0 && !resolvedHideTags)
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if resolvedShowRaw}
|
||||
<TruncatedText class="font-medium {className}" showTooltip={false} text={modelId} {...rest} />
|
||||
{:else}
|
||||
{#snippet nameAndBadges()}
|
||||
<span class="min-w-0 truncate font-medium">
|
||||
{#if !hideOrgName && parsed.orgName}{parsed.orgName}/{/if}{displayName}
|
||||
</span>
|
||||
|
||||
<span class="inline-flex items-center gap-1">
|
||||
{#if parsed.params}
|
||||
<span class={badgeClass}>
|
||||
{parsed.params}{parsed.activatedParams ? `-${parsed.activatedParams}` : ''}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if parsed.quantization && !resolvedHideQuantization}
|
||||
<span class={badgeClass}>
|
||||
{parsed.quantization}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if primaryAlias}
|
||||
{#if primaryAlias !== parsed.modelName}
|
||||
<span class={badgeClass}>{parsed.modelName ?? modelId}</span>
|
||||
{/if}
|
||||
{:else if uniqueAliases.length > 1}
|
||||
{#each uniqueAliases as alias (alias)}
|
||||
<span class={badgeClass}>{alias}</span>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if uniqueTags.length > 0 && !resolvedHideTags}
|
||||
{#each uniqueTags as tag (tag)}
|
||||
<span class={tagBadgeClass}>{tag}</span>
|
||||
{/each}
|
||||
{/if}
|
||||
</span>
|
||||
{/snippet}
|
||||
|
||||
<span class="flex min-w-0 items-center gap-1.5 {className}" {...rest}>
|
||||
{#if showRawTooltip}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger class="flex min-w-0 items-center gap-1.5">
|
||||
{@render nameAndBadges()}
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>{modelId}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{:else}
|
||||
{@render nameAndBadges()}
|
||||
{#if !hideName}
|
||||
<span class="min-w-0 truncate font-medium">
|
||||
{#if !hideOrgName && parsed.orgName}{parsed.orgName}/{/if}{displayName}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if activeCapabilities.length > 0 || activeModalities.length > 0}
|
||||
<span class="inline-flex items-center gap-1.25 text-muted-foreground">
|
||||
{#each activeCapabilities as capability (capability)}
|
||||
{@const CapabilityIcon = CAPABILITY_ICONS[capability]}
|
||||
{#if hasBadges}
|
||||
<span class="inline-flex items-center gap-1 {wrap ? 'flex-wrap' : ''}">
|
||||
{#if parsed.sidecar}
|
||||
<span class={variantBadgeClass} title={`${parsed.sidecar.toUpperCase()} draft model`}>
|
||||
{parsed.sidecar}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<CapabilityIcon class="h-3 w-3 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
{#if parsed.params && !hideParameters}
|
||||
<span class={badgeClass}>
|
||||
{parsed.params}{parsed.activatedParams ? `-${parsed.activatedParams}` : ''}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>{CAPABILITY_LABELS[capability]}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{#each uniqueDraftSidecars as sidecar (sidecar)}
|
||||
<span class={variantBadgeClass} title={`${sidecar.toUpperCase()} draft model available`}>
|
||||
{sidecar}
|
||||
</span>
|
||||
{/each}
|
||||
|
||||
{#each activeModalities as modality (modality)}
|
||||
{@const ModalityIcon = MODALITY_ICONS[modality]}
|
||||
{#if parsed.quantization && !resolvedHideQuantization}
|
||||
<span class={badgeClass}>
|
||||
{parsed.quantization}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<ModalityIcon class="h-3 w-3 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
{#if primaryAlias}
|
||||
{#if primaryAlias !== parsed.modelName}
|
||||
<span class="{badgeClass} max-w-32 truncate" title={parsed.modelName ?? modelId}>
|
||||
{parsed.modelName ?? modelId}
|
||||
</span>
|
||||
{/if}
|
||||
{:else if uniqueAliases.length > 1}
|
||||
{#each uniqueAliases.slice(0, MAX_ALIAS_BADGES) as alias (alias)}
|
||||
<span class="{badgeClass} max-w-32 truncate" title={alias}>
|
||||
{alias}
|
||||
</span>
|
||||
{/each}
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>{MODALITY_LABELS[modality]}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/each}
|
||||
{#if uniqueAliases.length > MAX_ALIAS_BADGES}
|
||||
<span class={badgeClass} title={uniqueAliases.slice(MAX_ALIAS_BADGES).join(', ')}>
|
||||
+{uniqueAliases.length - MAX_ALIAS_BADGES} more
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if uniqueTags.length > 0 && !resolvedHideTags}
|
||||
{#each uniqueTags as tag (tag)}
|
||||
<span class={tagBadgeClass}>{tag}</span>
|
||||
{/each}
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<span
|
||||
class="flex min-w-0 items-center gap-1.5 {wrap ? 'flex-wrap' : ''} {iconsOnNewLine
|
||||
? 'flex-col items-start'
|
||||
: ''} {className}"
|
||||
{...rest}
|
||||
>
|
||||
<span class="flex min-w-0 items-center gap-1.5 {wrap ? 'flex-wrap' : ''}">
|
||||
{#if showRawTooltip}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger class="flex min-w-0 items-center gap-1.5">
|
||||
{@render nameAndBadges()}
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>{modelId}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{:else}
|
||||
{@render nameAndBadges()}
|
||||
{/if}
|
||||
|
||||
{#if !iconsOnNewLine}
|
||||
<ModelCapabilityIcons
|
||||
{hideCapabilities}
|
||||
{hideModalities}
|
||||
{modalities}
|
||||
{supportsThinking}
|
||||
{supportsToolUse}
|
||||
/>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
{#if iconsOnNewLine || contextLength || sizeRange}
|
||||
<span class="inline-flex items-center gap-1.5">
|
||||
{#if iconsOnNewLine}
|
||||
<ModelCapabilityIcons
|
||||
{hideCapabilities}
|
||||
{hideModalities}
|
||||
{modalities}
|
||||
{supportsThinking}
|
||||
{supportsToolUse}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#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}
|
||||
|
||||
{#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
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
@@ -86,12 +86,13 @@
|
||||
>
|
||||
<ModelId
|
||||
aliases={option.aliases}
|
||||
{capabilities}
|
||||
class="flex-1"
|
||||
{hideOrgName}
|
||||
{modalities}
|
||||
modelId={option.model}
|
||||
showRawTooltip
|
||||
supportsThinking={capabilities.reasoning}
|
||||
supportsToolUse={capabilities.tools}
|
||||
tags={option.tags}
|
||||
/>
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
downloadedBytes: number;
|
||||
totalBytes: number;
|
||||
overlay?: boolean;
|
||||
}
|
||||
|
||||
let { downloadedBytes, overlay = false, totalBytes }: Props = $props();
|
||||
|
||||
let fraction = $derived.by(() => {
|
||||
if (totalBytes <= 0) return 0;
|
||||
|
||||
return Math.min(Math.max(downloadedBytes / totalBytes, 0), 1);
|
||||
});
|
||||
let percent = $derived(Math.round(fraction * 100));
|
||||
</script>
|
||||
|
||||
{#if overlay}
|
||||
<div class="pointer-events-none absolute inset-x-0 bottom-0 h-0.5 overflow-hidden rounded-b-sm">
|
||||
<div
|
||||
class="h-full animate-pulse bg-primary transition-[width] duration-200 ease-out"
|
||||
style="width: {percent}%"
|
||||
></div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="h-1 w-full overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
class="h-full animate-pulse bg-primary transition-[width] duration-200 ease-out"
|
||||
style="width: {percent}%"
|
||||
></div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,105 @@
|
||||
<script lang="ts">
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { DARK_INVERT_AVATAR_ORGS } from '$lib/constants';
|
||||
import { HuggingFaceService } from '$lib/services';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
org: string;
|
||||
quantOrg?: string;
|
||||
size?: string;
|
||||
baseImageClass?: string;
|
||||
quantImageClass?: string;
|
||||
quantPositionClass?: string;
|
||||
quantSize?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
baseImageClass = '',
|
||||
class: className = '',
|
||||
org,
|
||||
quantImageClass = 'h-full w-full',
|
||||
quantOrg,
|
||||
quantPositionClass = '-bottom-0.75 -right-0.75',
|
||||
quantSize = 'h-4.25 w-4.25',
|
||||
size = 'h-9 w-9'
|
||||
}: Props = $props();
|
||||
|
||||
let avatarError = $state(false);
|
||||
let quantError = $state(false);
|
||||
|
||||
let invertAvatar = $derived(DARK_INVERT_AVATAR_ORGS.includes(org));
|
||||
let invertQuant = $derived(DARK_INVERT_AVATAR_ORGS.includes(quantOrg ?? ''));
|
||||
|
||||
// Monogram fallback: org initial on a hue derived from its name, so each org
|
||||
// gets a stable distinct color.
|
||||
let hue = $derived.by(() => {
|
||||
let h = 0;
|
||||
|
||||
for (let i = 0; i < org.length; i++) h = (h * 31 + org.charCodeAt(i)) >>> 0;
|
||||
|
||||
return h % 360;
|
||||
});
|
||||
|
||||
let quantHue = $derived.by(() => {
|
||||
const name = quantOrg ?? '';
|
||||
|
||||
let h = 0;
|
||||
|
||||
for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0;
|
||||
|
||||
return h % 360;
|
||||
});
|
||||
</script>
|
||||
|
||||
<span class="relative mt-0.5 inline-flex shrink-0 {className}">
|
||||
{#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%)"
|
||||
>
|
||||
{org.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
{:else}
|
||||
<div class="rounded-md">
|
||||
<img
|
||||
alt=""
|
||||
class="{size} rounded-md {invertAvatar ? 'dark:invert' : ''} {baseImageClass}"
|
||||
loading="lazy"
|
||||
onerror={() => (avatarError = true)}
|
||||
src={HuggingFaceService.getAvatarUrl(org)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if quantOrg && quantOrg !== org}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger
|
||||
class="absolute {quantPositionClass} {quantSize} overflow-hidden rounded-full border border-background bg-muted "
|
||||
>
|
||||
{#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%)"
|
||||
>
|
||||
{quantOrg.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
{:else}
|
||||
<img
|
||||
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>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
</span>
|
||||
@@ -109,3 +109,12 @@ export { default as ModelBadge } from './ModelBadge.svelte';
|
||||
* Respects the user's `showRawModelNames` setting.
|
||||
*/
|
||||
export { default as ModelId } from './ModelId.svelte';
|
||||
|
||||
/**
|
||||
* **ModelCapabilityIcons** - Capability and modality icon row
|
||||
*
|
||||
* The shared tool-use / reasoning / vision / video / audio icon cluster with
|
||||
* tooltips, used by ModelId and the discover details header so the order and
|
||||
* styling stay consistent across every model-id surface.
|
||||
*/
|
||||
export { default as ModelCapabilityIcons } from './ModelCapabilityIcons.svelte';
|
||||
|
||||
@@ -44,6 +44,9 @@ export const STATS_UNITS = {
|
||||
|
||||
export const DEFAULT_MOBILE_BREAKPOINT = 768;
|
||||
|
||||
/** Orgs whose avatar is dark and needs inverting in dark mode. */
|
||||
export const DARK_INVERT_AVATAR_ORGS = ['openai'];
|
||||
|
||||
/** Icon used for the model selector and the `/model` slash command. */
|
||||
export const MODEL_SELECTOR_ICON = Package;
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ export {
|
||||
ModelSelectableFileKind
|
||||
} from './model.enums';
|
||||
|
||||
export { ModelDownloadStopRequest } from './model.enums';
|
||||
export { ModelDownloadConfirmAction, ModelDownloadStopRequest } from './model.enums';
|
||||
|
||||
export { ServerRole, ServerModelStatus, ServerModelsSseEventType } from './server.enums';
|
||||
|
||||
|
||||
@@ -55,3 +55,13 @@ export enum ModelDownloadStopRequest {
|
||||
CANCEL = 'cancel',
|
||||
PAUSE = 'pause'
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructive download action the user is asked to confirm: stop and discard an
|
||||
* in-flight download, or delete an already-downloaded model from disk. Both
|
||||
* resolve through the same store removal call, differing only in the copy.
|
||||
*/
|
||||
export enum ModelDownloadConfirmAction {
|
||||
CANCEL = 'cancel',
|
||||
DELETE = 'delete'
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ export function formatFileSize(bytes: number | unknown): string {
|
||||
/**
|
||||
* Format parameter count to human-readable format (B, M, K)
|
||||
*
|
||||
* Billions render as whole numbers (`176.94e9` -> `177B`): the decimals are
|
||||
* noise at badge sizes and id-parsed counts are whole anyway (`Qwen3-8B`).
|
||||
*
|
||||
* @param params - Parameter count
|
||||
* @returns Human-readable parameter count
|
||||
*/
|
||||
@@ -35,7 +38,7 @@ export function formatParameters(params: number | unknown): string {
|
||||
if (typeof params !== 'number') return 'Unknown';
|
||||
|
||||
if (params >= 1e9) {
|
||||
return `${(params / 1e9).toFixed(2)}B`;
|
||||
return `${Math.round(params / 1e9)}B`;
|
||||
}
|
||||
|
||||
if (params >= 1e6) {
|
||||
|
||||
Reference in New Issue
Block a user