ui : extract shared ModelCapabilityIcons and reuse in details header

Pull the tool-use / reasoning / vision / video / audio icon row out of ModelId
into a shared ModelCapabilityIcons component and use it from the discover
details header, replacing the header's hand-rolled icons. This fixes the
divergent icon order and styling between the two surfaces and keeps every
model-id capability row consistent.

Assisted-by: llama-ui:Qwen3.8-Flash-Next
This commit is contained in:
Aleksander Grygier
2026-09-04 14:54:52 +02:00
parent aef40b5927
commit caa00695a1
4 changed files with 134 additions and 111 deletions
@@ -0,0 +1,96 @@
<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 {
/** Input modalities to render (vision / video / audio). */
modalities?: ModelModalities;
supportsThinking?: boolean;
supportsToolUse?: boolean;
/** Skip the reasoning (lightbulb) icon regardless of support. */
hideReasoning?: boolean;
/** Skip all modality icons regardless of support. */
hideModalities?: boolean;
/** Icon box size classes, e.g. `h-3 w-3` (default) or `h-4 w-4`. */
iconSize?: string;
/** Gap between the capability and modality icon groups. */
gapClass?: string;
}
let {
gapClass = 'gap-1.25',
hideModalities = false,
hideReasoning = false,
iconSize = 'h-3 w-3',
modalities,
supportsThinking = false,
supportsToolUse = false
}: Props = $props();
let hasModalityIcons = $derived(modalities?.vision || modalities?.video || modalities?.audio);
</script>
{#if supportsToolUse}
<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 && !hideReasoning}
<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 {gapClass} 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}
@@ -1,5 +1,6 @@
<script lang="ts">
import { Database, Image, Lightbulb, Mic, ScrollText, Video, Wrench } from '@lucide/svelte';
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 { isAuxSidecar, type ModelSidecar } from '$lib/constants';
@@ -81,7 +82,6 @@
let uniqueAliases = $derived([...new Set(aliases ?? [])]);
let uniqueTags = $derived([...new Set([...(parsed.tags ?? []), ...(tags ?? [])])]);
let uniqueDraftSidecars = $derived([...new Set(draftSidecars)].filter((s) => !isAuxSidecar(s)));
let hasModalityIcons = $derived(modalities?.vision || modalities?.video || modalities?.audio);
let primaryAlias = $derived(uniqueAliases.length === 1 ? uniqueAliases[0] : null);
let displayName = $derived(primaryAlias ?? parsed.modelName ?? modelId);
@@ -99,72 +99,6 @@
{#if resolvedShowRaw}
<TruncatedText class="font-medium {className}" showTooltip={false} text={modelId} {...rest} />
{:else}
{#snippet capabilityIcons()}
{#if supportsToolUse}
<Tooltip.Root>
<Tooltip.Trigger>
<Wrench class="h-3 w-3 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<p>Tool use</p>
</Tooltip.Content>
</Tooltip.Root>
{/if}
{#if supportsThinking && !hideReasoning}
<Tooltip.Root>
<Tooltip.Trigger>
<Lightbulb class="h-3 w-3 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<p>Reasoning</p>
</Tooltip.Content>
</Tooltip.Root>
{/if}
{#if hasModalityIcons && !hideModalities}
<span class="inline-flex items-center gap-1.25 text-muted-foreground">
{#if modalities?.vision}
<Tooltip.Root>
<Tooltip.Trigger>
<Image class="h-3 w-3 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<p>Vision</p>
</Tooltip.Content>
</Tooltip.Root>
{/if}
{#if modalities?.video}
<Tooltip.Root>
<Tooltip.Trigger>
<Video class="h-3 w-3 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<p>Video</p>
</Tooltip.Content>
</Tooltip.Root>
{/if}
{#if modalities?.audio}
<Tooltip.Root>
<Tooltip.Trigger>
<Mic class="h-3 w-3 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<p>Audio</p>
</Tooltip.Content>
</Tooltip.Root>
{/if}
</span>
{/if}
{/snippet}
{#snippet nameAndBadges()}
{#if !hideName}
<span class="min-w-0 truncate font-medium">
@@ -238,12 +172,28 @@
{@render nameAndBadges()}
{/if}
{#if !iconsOnNewLine}{@render capabilityIcons()}{/if}
{#if !iconsOnNewLine}
<ModelCapabilityIcons
{hideModalities}
{hideReasoning}
{modalities}
{supportsThinking}
{supportsToolUse}
/>
{/if}
</span>
{#if iconsOnNewLine || contextLength || sizeRange}
<span class="inline-flex items-center gap-1.5">
{#if iconsOnNewLine}{@render capabilityIcons()}{/if}
{#if iconsOnNewLine}
<ModelCapabilityIcons
{hideModalities}
{hideReasoning}
{modalities}
{supportsThinking}
{supportsToolUse}
/>
{/if}
{#if contextLength}
<span class="inline-flex items-center gap-1 text-muted-foreground">
@@ -1,8 +1,8 @@
<script lang="ts">
import ModelCapabilityIcons from '../../ModelCapabilityIcons.svelte';
import ModelsDiscoverAvatar from '../ModelsDiscoverAvatar.svelte';
import ModelsDiscoverDetailsMetadata from './ModelsDiscoverDetailsMetadata.svelte';
import { Download, ExternalLink, Heart, Image, Lightbulb, Wrench } from '@lucide/svelte';
import * as Tooltip from '$lib/components/ui/tooltip';
import { Download, ExternalLink, Heart } from '@lucide/svelte';
import { ICON_CLASS_SM } from '$lib/constants';
import { HuggingFaceService } from '$lib/services';
import type { HfModelDetailInfo, HfModelGguf } from '$lib/types/huggingface';
@@ -44,45 +44,13 @@
<div class="flex items-center gap-2">
<h1 class="truncate text-lg font-semibold">{details.id ?? modelId}</h1>
{#if hasVision || hasTools || hasReasoning}
<div class="flex shrink-0 items-center gap-2.5 text-muted-foreground">
{#if hasVision}
<Tooltip.Root>
<Tooltip.Trigger>
<Image class="h-4 w-4" />
</Tooltip.Trigger>
<Tooltip.Content>
<p>Vision</p>
</Tooltip.Content>
</Tooltip.Root>
{/if}
{#if hasTools}
<Tooltip.Root>
<Tooltip.Trigger>
<Wrench class="h-4 w-4" />
</Tooltip.Trigger>
<Tooltip.Content>
<p>Tool use</p>
</Tooltip.Content>
</Tooltip.Root>
{/if}
{#if hasReasoning}
<Tooltip.Root>
<Tooltip.Trigger>
<Lightbulb class="h-4 w-4" />
</Tooltip.Trigger>
<Tooltip.Content>
<p>Reasoning</p>
</Tooltip.Content>
</Tooltip.Root>
{/if}
</div>
{/if}
<ModelCapabilityIcons
gapClass="gap-2"
iconSize="h-4 w-4"
modalities={{ audio: false, video: false, vision: hasVision }}
supportsThinking={hasReasoning}
supportsToolUse={hasTools}
/>
</div>
{#if baseModels.length}
@@ -49,4 +49,13 @@ export { default as ModelBadge } from './ModelBadge.svelte';
*/
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';
export * from './ModelsSelector';