feat: WIP

This commit is contained in:
Aleksander Grygier
2026-08-24 16:23:08 +02:00
parent a3d04fa1e8
commit 9a02b89cee
7 changed files with 46 additions and 58 deletions
@@ -57,26 +57,22 @@
<Dialog.Root {open} onOpenChange={handleOpenChange}>
<Dialog.Content
class="flex flex-col md:h-[calc(100vh-4rem)]! md:max-h-240! md:w-[calc(100vw-4rem)]! md:max-w-360!"
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;"
>
<Dialog.Header>
<Dialog.Title class="flex items-center gap-2">
<Boxes class="h-4 w-4" />
Discover Models
</Dialog.Title>
</Dialog.Header>
<div class="flex min-h-0 flex-1">
<aside class="flex w-88 shrink-0 flex-col border-r border-border/40">
<div class="p-2">
<!-- <div class="flex min-h-0 flex-1"> -->
<aside
class="sticky top-0 w-100 shrink-0 self-start border-r border-border/40 bg-background overflow-y-auto md:p-4 h-full space-y-1"
>
<div class="p-2 sticky top-0 z-99">
<SearchInput
class=""
bind:value={searchQuery}
placeholder="Search models..."
onInput={handleSearchInput}
/>
</div>
<div class="min-h-0 flex-1 overflow-y-auto">
<div>
{#if modelsHubStore.loading}
<p class="p-4 text-sm text-muted-foreground">Loading models...</p>
{:else if modelsHubStore.error}
@@ -94,11 +90,11 @@
</div>
</aside>
<main class="min-w-0 flex-1 overflow-y-auto">
<main>
{#if selectedId}
<ModelsDiscoverDetails modelId={selectedId} />
{/if}
</main>
</div>
<!-- </div> -->
</Dialog.Content>
</Dialog.Root>
@@ -19,6 +19,8 @@
modalities?: ModelModalities;
supportsThinking?: boolean;
draftVariants?: DraftVariant[];
/** Allow badges to wrap onto new lines instead of truncating. */
wrap?: boolean;
class?: string;
}
@@ -35,6 +37,7 @@
showRawTooltip = false,
supportsThinking = false,
tags,
wrap = false,
...rest
}: Props = $props();
@@ -73,7 +76,7 @@
{#if !hideOrgName && parsed.orgName}{parsed.orgName}/{/if}{displayName}
</span>
<span class="inline-flex items-center gap-1">
<span class="inline-flex items-center gap-1 {wrap ? 'flex-wrap' : ''}">
{#if parsed.variant}
<span class={variantBadgeClass} title={`${parsed.variant.toUpperCase()} draft model`}>
{parsed.variant}
@@ -116,7 +119,7 @@
</span>
{/snippet}
<span class="flex min-w-0 items-center gap-1.5 {className}" {...rest}>
<span class="flex min-w-0 items-center gap-1.5 {wrap ? 'flex-wrap' : ''} {className}" {...rest}>
{#if showRawTooltip}
<Tooltip.Root>
<Tooltip.Trigger class="flex min-w-0 items-center gap-1.5">
@@ -1,5 +1,6 @@
<script lang="ts">
import { HuggingFaceService } from '$lib/services';
import { DARK_INVERT_AVATAR_ORGS } from '$lib/constants';
import * as Tooltip from '$lib/components/ui/tooltip';
interface Props {
@@ -14,6 +15,9 @@
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(() => {
@@ -44,19 +48,21 @@
{org.charAt(0).toUpperCase()}
</span>
{:else}
<img
src={HuggingFaceService.getAvatarUrl(org)}
onerror={() => (avatarError = true)}
class="h-9 w-9 rounded-md bg-gray-200 p-0.5"
alt=""
loading="lazy"
/>
<div class="rounded-md">
<img
src={HuggingFaceService.getAvatarUrl(org)}
onerror={() => (avatarError = true)}
class="h-9 w-9 rounded-md {invertAvatar ? 'dark:invert' : ''}"
alt=""
loading="lazy"
/>
</div>
{/if}
{#if quantOrg}
{#if quantOrg && quantOrg !== org}
<Tooltip.Root>
<Tooltip.Trigger
class="absolute -bottom-0.75 -right-0.75 h-4.25 w-4.25 overflow-hidden rounded-full border border-background"
class="absolute -bottom-0.75 -right-0.75 h-4.25 w-4.25 overflow-hidden rounded-full border border-background bg-muted "
>
{#if quantError}
<span
@@ -70,7 +76,7 @@
<img
src={HuggingFaceService.getAvatarUrl(quantOrg)}
onerror={() => (quantError = true)}
class="h-full w-full rounded-full bg-gray-200"
class="h-full w-full rounded-full {invertQuant ? 'dark:invert' : ''}"
alt=""
loading="lazy"
/>
@@ -54,7 +54,7 @@
</script>
<span class="min-w-0 flex-1">
<ModelId modelId={model.id} hideOrgName {modalities} {draftVariants} class="min-w-0" />
<ModelId modelId={model.id} hideOrgName {modalities} {draftVariants} wrap class="min-w-0" />
<span class="mt-0.5 block truncate text-xs text-muted-foreground">
<span class="inline-flex items-center gap-1">
@@ -6,6 +6,7 @@ export const CURATED_MODEL_IDS = [
'ggml-org/DeepSeek-V4-Flash-0731-GGUF',
'ggml-org/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-GGUF',
'ggml-org/Qwen3.6-35B-A3B-GGUF',
'ggml-org/Laguna-S-2.1-GGUF',
'ggml-org/gemma-4-31B-it-GGUF',
'ggml-org/gemma-4-26B-A4B-it-GGUF',
'ggml-org/gemma-4-12B-it-GGUF',
@@ -13,5 +14,5 @@ export const CURATED_MODEL_IDS = [
'ggml-org/gemma-4-E2B-it-GGUF',
'ggml-org/gemma-4-E4B-it-GGUF',
'ggml-org/gpt-oss-120b-GGUF',
'ggml-org/gpt-oss-20b-GGUF',
'ggml-org/gpt-oss-20b-GGUF'
];
@@ -45,6 +45,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;
@@ -14,9 +14,6 @@ import { HuggingFaceService } from '$lib/services';
import type { HfModelInfo } from '$lib/types/huggingface';
class ModelsHubStore {
/** Org whose models the sidebar lists by default. */
private static readonly DEFAULT_AUTHOR = 'ggml-org';
models = $state<HfModelInfo[]>([]);
loading = $state(false);
error = $state<string | null>(null);
@@ -29,8 +26,10 @@ class ModelsHubStore {
private searchRequestId = 0;
/**
* Fetch the default list: the curated ggml-org models in display order.
* No-op when already loaded or in flight.
* Fetch the default list: the curated ggml--org models in display order.
* Each curated model is fetched directly by ID, so the list is independent
* of download ranking (curated models may fall outside the top-50 search
* window). No-op when already loaded or in flight.
*/
async fetch(): Promise<void> {
if (this.loading || this.fetched) return;
@@ -39,31 +38,11 @@ class ModelsHubStore {
this.error = null;
try {
const results = await HuggingFaceService.search({
author: ModelsHubStore.DEFAULT_AUTHOR,
sort: 'downloads',
limit: 50,
full: true
});
const byId = new Map(results.map((m) => [m.id, m]));
// Keep only the curated models, in the curated display order.
const curated = CURATED_MODEL_IDS.flatMap((id) => {
const model = byId.get(id);
return model ? [model] : [];
});
// The list endpoint omits gguf metadata (context length, architecture),
// so fetch it per model and merge it in.
this.defaultModels = await Promise.all(
curated.map(async (model) => {
const details = await HuggingFaceService.getDetails(model.id);
return details?.gguf ? { ...model, gguf: details.gguf } : model;
})
);
// getDetails returns full metadata (downloads, likes, lastModified,
// siblings, tags, gguf) for a single model.
this.defaultModels = (
await Promise.all(CURATED_MODEL_IDS.map((id) => HuggingFaceService.getDetails(id)))
).filter((m): m is HfModelInfo => m !== null);
this.models = this.defaultModels;
this.fetched = true;
} catch (err) {