ui : fold the model information dialog into the configuration tab

Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
Aleksander Grygier
2026-09-28 12:10:12 +02:00
parent b7dd5ba24f
commit 6112cafe4e
9 changed files with 103 additions and 445 deletions
@@ -1,369 +0,0 @@
<script lang="ts">
import { ActionIconCopyToClipboard, BadgesModality } from '$lib/components/app';
import * as Dialog from '$lib/components/ui/dialog';
import * as Table from '$lib/components/ui/table';
import { modelsStore, serverStore } from '$lib/stores';
import type { ApiLlamaCppServerProps } from '$lib/types';
import { formatFileSize, formatNumber, formatParameters } from '$lib/utils';
interface Props {
open?: boolean;
onOpenChange?: (open: boolean) => void;
// when set, fetch props from the child process (router mode)
modelId?: string | null;
}
let { modelId = null, onOpenChange, open = $bindable() }: Props = $props();
let isRouter = $derived(serverStore.isRouterMode);
// per-model props fetched from the child process
let routerModelProps = $state<ApiLlamaCppServerProps | null>(null);
let isLoadingRouterProps = $state(false);
// in router mode use per-model props, otherwise use global props
let serverProps = $derived(isRouter && modelId ? routerModelProps : serverStore.props);
let modelName = $derived(isRouter && modelId ? modelId : modelsStore.singleModelName);
let models = $derived(modelsStore.models);
let isLoadingModels = $derived(modelsStore.loading);
// in router mode, find the model option matching modelId
// in single mode, use the first model as before
let firstModel = $derived.by(() => {
if (isRouter && modelId) {
return models.find((m) => m.model === modelId) ?? null;
}
return models[0] ?? null;
});
// Get modalities from modelStore using the model ID from the first model
let modalities = $derived.by(() => {
if (!firstModel?.id) return [];
return modelsStore.props.getModelModalitiesArray(firstModel.id);
});
// Ensure models are fetched when dialog opens
$effect(() => {
if (open && models.length === 0) {
modelsStore.fetch();
}
});
// fetch per-model props from child process when dialog opens in router mode
$effect(() => {
if (open && isRouter && modelId) {
isLoadingRouterProps = true;
modelsStore.props
.fetchModelProps(modelId)
.then((props) => {
routerModelProps = props;
})
.catch(() => {
routerModelProps = null;
})
.finally(() => {
isLoadingRouterProps = false;
});
}
if (!open) {
routerModelProps = null;
}
});
</script>
<Dialog.Root bind:open {onOpenChange}>
<Dialog.Content
class="z-9999 max-md:h-[100dvh]! max-md:w-screen! max-md:max-w-none! md:w-[calc(100vw-4rem)]! md:max-w-[60rem]! md:max-h-[80dvh]!"
>
<!-- sticky header holds only the close button; the title scrolls with the body -->
<Dialog.Header />
<div class="min-w-0 space-y-6 md:py-4 -mt-4! md:mt-0 pb-4">
<div class="min-w-0 space-y-2">
<Dialog.Title>Model Information</Dialog.Title>
<Dialog.Description>Current model details and capabilities</Dialog.Description>
</div>
{#if isLoadingModels || isLoadingRouterProps}
<div class="flex items-center justify-center py-8">
<div class="text-sm text-muted-foreground">Loading model information...</div>
</div>
{:else if firstModel}
{@const modelMeta = firstModel.meta}
{#if serverProps}
<!-- Desktop: fixed-layout table, long values scroll inside their cell -->
<Table.Root class="hidden table-fixed md:table">
<Table.Header>
<Table.Row>
<Table.Head class="w-[10rem]">Model</Table.Head>
<Table.Head>
<div class="flex min-w-0 items-center gap-2">
<span class="min-w-0 flex-1 overflow-x-auto whitespace-nowrap">
{modelName}
</span>
<ActionIconCopyToClipboard
ariaLabel="Copy model name to clipboard"
canCopy={!!modelName}
text={modelName || ''}
/>
</div>
</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
<!-- Model Path -->
<Table.Row>
<Table.Cell class="h-10 align-middle font-medium">File Path</Table.Cell>
<Table.Cell class="h-10 align-middle font-mono text-xs">
<div class="flex min-w-0 items-center gap-2">
<span class="min-w-0 flex-1 overflow-x-auto whitespace-nowrap">
{serverProps.model_path}
</span>
<ActionIconCopyToClipboard
ariaLabel="Copy model path to clipboard"
text={serverProps.model_path}
/>
</div>
</Table.Cell>
</Table.Row>
<!-- Context Size -->
{#if serverProps?.default_generation_settings?.n_ctx}
<Table.Row>
<Table.Cell class="h-10 align-middle font-medium">Context Size</Table.Cell>
<Table.Cell
>{formatNumber(serverProps.default_generation_settings.n_ctx)} tokens</Table.Cell
>
</Table.Row>
{:else}
<Table.Row>
<Table.Cell class="h-10 align-middle font-medium text-red-500"
>Context Size</Table.Cell
>
<Table.Cell class="text-red-500">Not available</Table.Cell>
</Table.Row>
{/if}
<!-- Training Context -->
{#if modelMeta?.n_ctx_train}
<Table.Row>
<Table.Cell class="h-10 align-middle font-medium">Training Context</Table.Cell>
<Table.Cell>{formatNumber(modelMeta.n_ctx_train)} tokens</Table.Cell>
</Table.Row>
{/if}
<!-- Model Size -->
{#if modelMeta?.size}
<Table.Row>
<Table.Cell class="h-10 align-middle font-medium">Model Size</Table.Cell>
<Table.Cell>{formatFileSize(modelMeta.size)}</Table.Cell>
</Table.Row>
{/if}
<!-- Parameters -->
{#if modelMeta?.n_params}
<Table.Row>
<Table.Cell class="h-10 align-middle font-medium">Parameters</Table.Cell>
<Table.Cell>{formatParameters(modelMeta.n_params)}</Table.Cell>
</Table.Row>
{/if}
<!-- Embedding Size -->
{#if modelMeta?.n_embd}
<Table.Row>
<Table.Cell class="align-middle font-medium">Embedding Size</Table.Cell>
<Table.Cell>{formatNumber(modelMeta.n_embd)}</Table.Cell>
</Table.Row>
{/if}
<!-- Vocabulary Size -->
{#if modelMeta?.n_vocab}
<Table.Row>
<Table.Cell class="align-middle font-medium">Vocabulary Size</Table.Cell>
<Table.Cell>{formatNumber(modelMeta.n_vocab)} tokens</Table.Cell>
</Table.Row>
{/if}
<!-- Vocabulary Type -->
{#if modelMeta?.vocab_type}
<Table.Row>
<Table.Cell class="align-middle font-medium">Vocabulary Type</Table.Cell>
<Table.Cell class="align-middle capitalize">{modelMeta.vocab_type}</Table.Cell>
</Table.Row>
{/if}
<!-- Total Slots -->
<Table.Row>
<Table.Cell class="align-middle font-medium">Parallel Slots</Table.Cell>
<Table.Cell>{serverProps.total_slots}</Table.Cell>
</Table.Row>
<!-- Modalities -->
{#if modalities.length > 0}
<Table.Row>
<Table.Cell class="align-middle font-medium">Modalities</Table.Cell>
<Table.Cell>
<div class="flex flex-wrap gap-1">
<BadgesModality {modalities} />
</div>
</Table.Cell>
</Table.Row>
{/if}
<!-- Build Info -->
<Table.Row>
<Table.Cell class="align-middle font-medium">Build Info</Table.Cell>
<Table.Cell class="align-middle font-mono text-xs"
>{serverProps.build_info}</Table.Cell
>
</Table.Row>
<!-- Chat Template -->
{#if serverProps.chat_template}
<Table.Row>
<Table.Cell class="py-4" colspan={2}>
<div class="flex flex-col gap-2">
<span class="font-medium">Chat Template</span>
<div class="overflow-x-auto rounded-md bg-muted p-4">
<pre
class="font-mono text-xs whitespace-pre">{serverProps.chat_template}</pre>
</div>
</div>
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
<!-- Mobile: stacked layout; long values wrap instead of scrolling the page -->
<div class="flex min-w-0 flex-col gap-4 md:hidden">
<div class="min-w-0 space-y-1">
<div class="text-xs font-medium text-muted-foreground">Model</div>
<div class="flex min-w-0 items-start gap-2">
<span class="min-w-0 flex-1 break-all font-mono text-xs">{modelName}</span>
<ActionIconCopyToClipboard
ariaLabel="Copy model name to clipboard"
canCopy={!!modelName}
text={modelName || ''}
/>
</div>
</div>
<div class="min-w-0 space-y-1">
<div class="text-xs font-medium text-muted-foreground">File Path</div>
<div class="flex min-w-0 items-start gap-2">
<span class="min-w-0 flex-1 break-all font-mono text-xs"
>{serverProps.model_path}</span
>
<ActionIconCopyToClipboard
ariaLabel="Copy model path to clipboard"
text={serverProps.model_path}
/>
</div>
</div>
{#if serverProps?.default_generation_settings?.n_ctx}
{@render infoRow(
'Context Size',
`${formatNumber(serverProps.default_generation_settings.n_ctx)} tokens`
)}
{:else}
{@render infoRow('Context Size', 'Not available', 'text-red-500')}
{/if}
{#if modelMeta?.n_ctx_train}
{@render infoRow('Training Context', `${formatNumber(modelMeta.n_ctx_train)} tokens`)}
{/if}
{#if modelMeta?.size}
{@render infoRow('Model Size', formatFileSize(modelMeta.size))}
{/if}
{#if modelMeta?.n_params}
{@render infoRow('Parameters', formatParameters(modelMeta.n_params))}
{/if}
{#if modelMeta?.n_embd}
{@render infoRow('Embedding Size', formatNumber(modelMeta.n_embd))}
{/if}
{#if modelMeta?.n_vocab}
{@render infoRow('Vocabulary Size', `${formatNumber(modelMeta.n_vocab)} tokens`)}
{/if}
{#if modelMeta?.vocab_type}
{@render infoRow('Vocabulary Type', modelMeta.vocab_type, 'capitalize')}
{/if}
{@render infoRow('Parallel Slots', `${serverProps.total_slots}`)}
{#if modalities.length > 0}
<div class="min-w-0 space-y-1">
<div class="text-xs font-medium text-muted-foreground">Modalities</div>
<div class="flex flex-wrap gap-1">
<BadgesModality {modalities} />
</div>
</div>
{/if}
<div class="min-w-0 space-y-1">
<div class="text-xs font-medium text-muted-foreground">Build Info</div>
<span class="block break-all font-mono text-xs">{serverProps.build_info}</span>
</div>
{#if serverProps.chat_template}
<div class="min-w-0 space-y-2">
<div class="text-xs font-medium text-muted-foreground">Chat Template</div>
<div class="overflow-x-auto rounded-md bg-muted p-4">
<pre class="font-mono text-xs whitespace-pre">{serverProps.chat_template}</pre>
</div>
</div>
{/if}
</div>
{/if}
{:else if !isLoadingModels}
<div class="flex items-center justify-center py-8">
<div class="text-sm text-muted-foreground">No model information available</div>
</div>
{/if}
</div>
</Dialog.Content>
</Dialog.Root>
{#snippet infoRow(label: string, value: string, valueClass: string = '')}
<div class="flex items-center justify-between gap-3">
<span class="shrink-0 text-xs font-medium text-muted-foreground {valueClass}">{label}</span>
<span class="text-sm {valueClass}">{value}</span>
</div>
{/snippet}
@@ -454,7 +454,6 @@ export { default as DialogConversationSelection } from './DialogConversationSele
* <DialogModelInformation bind:open={showModelInfo} />
* ```
*/
export { default as DialogModelInformation } from './DialogModelInformation.svelte';
/**
* **DialogMcpResourcesBrowser** - MCP resources browser dialog
@@ -1,10 +1,15 @@
<script lang="ts">
import { isLocalOption, modelQuantLabel, modelSizeLabel, resolveModelSize } from '../utils';
import { CollapsibleSection } from '$lib/components/app';
import {
ActionIconCopyToClipboard,
BadgesModality,
CollapsibleSection
} from '$lib/components/app';
import { Badge } from '$lib/components/ui/badge';
import { modelsStore } from '$lib/stores';
import type { ApiLlamaCppServerProps } from '$lib/types/api';
import type { ModelOption } from '$lib/types/models';
import { formatParameters } from '$lib/utils/formatters';
import { formatFileSize, formatNumber, formatParameters } from '$lib/utils/formatters';
interface Props {
option: ModelOption;
@@ -36,46 +41,123 @@
};
});
let meta = $derived(option.meta);
let modalities = $derived(modelsStore.props.getModelModalitiesArray(option.id));
let isMissingContext = $derived(!serverProps);
let rows = $derived([
{ label: 'Model', value: option.model },
{ label: 'File Path', value: serverProps?.model_path ?? null },
{ isCopyable: true, isMono: true, label: 'File Path', value: serverProps?.model_path ?? null },
{
label: 'Context Size',
value: serverProps
? `${formatParameters(serverProps.default_generation_settings.n_ctx)} tokens`
? `${formatNumber(serverProps.default_generation_settings.n_ctx)} tokens`
: null
},
{
label: 'Training Context',
value: option.contextLength ? `${formatParameters(option.contextLength)} tokens` : null
value: meta?.n_ctx_train
? `${formatNumber(meta.n_ctx_train)} tokens`
: option.contextLength
? `${formatParameters(option.contextLength)} tokens`
: null
},
{ label: 'Model Size', value: resolvedSize ?? size },
{ label: 'Parameters', value: option.parsedId?.params ?? null },
{
label: 'Model Size',
value: meta?.size ? formatFileSize(meta.size) : (resolvedSize ?? size)
},
{
label: 'Parameters',
value: meta?.n_params ? formatParameters(meta.n_params) : (option.parsedId?.params ?? null)
},
{ label: 'Embedding Size', value: meta?.n_embd ? formatNumber(meta.n_embd) : null },
{
label: 'Vocabulary Size',
value: meta?.n_vocab ? `${formatNumber(meta.n_vocab)} tokens` : null
},
{ isCapitalized: true, label: 'Vocabulary Type', value: (meta?.vocab_type as string) ?? null },
{ isBadge: true, label: 'Quantization', value: quant },
{ isBadge: true, label: 'Architecture', value: (option.meta?.architecture as string) ?? null },
{ label: 'Format', value: isLocalOption(option) ? 'GGUF' : null },
{ label: 'Parallel Slots', value: serverProps ? String(serverProps.total_slots) : null }
] satisfies Array<{ isBadge?: boolean; label: string; value: string | null }>);
{ label: 'Parallel Slots', value: serverProps ? String(serverProps.total_slots) : null },
{ isMono: true, label: 'Build Info', value: serverProps?.build_info ?? null }
] satisfies Array<{
isBadge?: boolean;
isCapitalized?: boolean;
isCopyable?: boolean;
isMono?: boolean;
label: string;
value: string | null;
}>);
const sectionTrigger = 'flex w-full cursor-pointer items-center gap-2 py-2 text-left';
</script>
<div class="space-y-0">
<div class="flex items-center gap-3 border-b border-border/30 py-2.5">
<span class="text-sm text-muted-foreground">Model</span>
<span class="ml-auto flex min-w-0 items-center gap-2">
<span class="min-w-0 truncate font-mono text-xs">{option.model}</span>
<ActionIconCopyToClipboard
ariaLabel="Copy model name to clipboard"
canCopy
text={option.model}
/>
</span>
</div>
{#each rows as row (row.label)}
<div class="flex items-center gap-3 border-b border-border/30 py-2.5 last:border-b-0">
<span class="text-sm text-muted-foreground">{row.label}</span>
<span
class="text-sm text-muted-foreground {row.label === 'Context Size' && isMissingContext
? 'text-destructive'
: ''}">{row.label}</span
>
<span class="ml-auto min-w-0 truncate text-sm">
<span class="ml-auto flex min-w-0 items-center gap-2">
{#if row.value === null}
<span class="text-muted-foreground">—</span>
{:else if row.isBadge}
<Badge class="h-5 px-1.5 text-[10px]" variant="secondary">{row.value}</Badge>
<span
class={isMissingContext && row.label === 'Context Size'
? 'text-destructive'
: 'text-muted-foreground'}
>
{isMissingContext && row.label === 'Context Size' ? 'Not available' : '—'}
</span>
{:else}
{row.value}
<span
class={[
'min-w-0 truncate',
row.isBadge ? '' : 'text-sm',
row.isCapitalized ? 'capitalize' : '',
row.isMono ? 'font-mono text-xs' : ''
]}
>
{#if row.isBadge}
<Badge class="h-5 px-1.5 text-[10px]" variant="secondary">{row.value}</Badge>
{:else}
{row.value}
{/if}
</span>
{#if row.isCopyable}
<ActionIconCopyToClipboard
ariaLabel="Copy value to clipboard"
canCopy
text={row.value}
/>
{/if}
{/if}
</span>
</div>
{/each}
{#if modalities.length > 0}
<div class="flex items-center gap-3 border-b border-border/30 py-2.5 last:border-b-0">
<span class="text-sm text-muted-foreground">Modalities</span>
<span class="ml-auto flex flex-wrap gap-1"><BadgesModality {modalities} /></span>
</div>
{/if}
</div>
<div class="mt-4">
@@ -2,7 +2,6 @@
import ModelLoadHighlight from '../ModelLoadHighlight.svelte';
import { ChevronDown, Lightbulb, Loader2 } from '@lucide/svelte';
import {
DialogModelInformation,
DropdownMenuSearchable,
ModelId,
ModelsSelectorList,
@@ -334,7 +333,6 @@
{isFav}
{isHighlighted}
{isSelected}
onInfoClick={ms.handleInfoClick}
onKeyDown={(event) => {
if (event.key === KeyboardKey.ENTER || event.key === KeyboardKey.SPACE) {
event.preventDefault();
@@ -354,7 +352,6 @@
favorites={ms.favoriteItems}
groups={ms.groupedFilteredOptions}
loaded={ms.loadedItems}
onInfoClick={ms.handleInfoClick}
onProviderBack={ms.isProviderView ? ms.closeProvider : undefined}
onProviderOpen={ms.openProvider}
onSelect={ms.handleSelect}
@@ -434,14 +431,6 @@
{/if}
</div>
{#if ms.showModelDialog}
<DialogModelInformation
modelId={ms.infoModelId}
onOpenChange={(v) => ms.setShowModelDialog(v)}
open={ms.showModelDialog}
/>
{/if}
<DialogBackendForm
bind:open={showAddBackend}
onSaved={(backend) => void ms.showBackendModels(backend.id)}
@@ -20,7 +20,6 @@
activeId: string | null;
sectionHeaderClass?: string;
onSelect: (modelId: string) => void;
onInfoClick: (modelName: string) => void;
renderOption?: import('svelte').Snippet<[ModelItem, boolean]>;
/** Favorite models of every backend, listed in their own section. */
favorites?: ModelItem[];
@@ -40,7 +39,6 @@
favorites = [],
groups,
loaded = [],
onInfoClick,
onProviderBack,
onProviderOpen,
onSelect,
@@ -114,7 +112,6 @@
{isFav}
isHighlighted={false}
{isSelected}
{onInfoClick}
onKeyDown={() => {}}
onMouseEnter={() => {}}
{onSelect}
@@ -17,7 +17,6 @@
onSelect: (modelId: string) => void;
onMouseEnter: () => void;
onKeyDown: (e: KeyboardEvent) => void;
onInfoClick?: (modelName: string) => void;
/** Show the base model's org as the main avatar and the repo (quant) org as the corner badge; resolves the base org lazily via HF. */
showBaseModelAvatar?: boolean;
}
@@ -27,7 +26,6 @@
isFav,
isHighlighted,
isSelected,
onInfoClick,
onKeyDown,
onMouseEnter,
onSelect,
@@ -103,7 +101,7 @@
/>
<div class="flex shrink-0 items-center gap-1">
<ModelRowActions {isFav} {isLoaded} onInfo={onInfoClick} {option} />
<ModelRowActions {isFav} {isLoaded} {option} />
<ModelLoadControl
{canLoad}
@@ -2,7 +2,6 @@
import ModelLoadHighlight from '../ModelLoadHighlight.svelte';
import { ChevronDown, Lightbulb, Loader2, Package } from '@lucide/svelte';
import {
DialogModelInformation,
ModelId,
ModelsSelectorList,
ModelsSelectorReasoningPanel,
@@ -196,7 +195,6 @@
favorites={ms.favoriteItems}
groups={ms.groupedFilteredOptions}
loaded={ms.loadedItems}
onInfoClick={ms.handleInfoClick}
onProviderBack={ms.isProviderView ? ms.closeProvider : undefined}
onProviderOpen={ms.openProvider}
onSelect={ms.handleSelect}
@@ -244,14 +242,6 @@
{/if}
</div>
{#if ms.showModelDialog}
<DialogModelInformation
modelId={ms.infoModelId}
onOpenChange={(v) => ms.setShowModelDialog(v)}
open={ms.showModelDialog}
/>
{/if}
<DialogBackendForm
bind:open={showAddBackend}
onSaved={(backend) => void ms.showBackendModels(backend.id)}
@@ -10,7 +10,7 @@ import {
LOCAL_BACKEND_ID,
REMOTE_PROVIDER_MODEL_LIMIT
} from '$lib/constants';
import { backendsModelsStore, backendsStore, modelsStore, serverStore } from '$lib/stores';
import { backendsModelsStore, backendsStore, modelsStore, serverStore, uiStore } from '$lib/stores';
import type { ModelOption } from '$lib/types/models';
import { getBackend } from '$lib/utils/api-base';
import { getBackendCapabilities } from '$lib/utils/backend';
@@ -53,14 +53,10 @@ export interface UseModelsSelectorReturn {
readonly groupedFilteredOptions: ReturnType<typeof groupModelOptions>;
readonly isLoadingModel: boolean;
readonly searchTerm: string;
readonly showModelDialog: boolean;
readonly infoModelId: string | null;
closeProvider(): void;
openProvider(backendId: string): void;
setSearchTerm(value: string): void;
showBackendModels(backendId: string): Promise<void>;
setShowModelDialog(value: boolean): void;
handleInfoClick(modelName: string): void;
handleSelect(modelId: string): Promise<void>;
handleOpenChange(open: boolean): void;
isFavorite(model: string): boolean;
@@ -124,8 +120,6 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
let isLoadingModel = $state(false);
let searchTerm = $state('');
let showModelDialog = $state(false);
let infoModelId = $state<string | null>(null);
const visibleOptions = $derived(allOptions.filter((option) => !modelsStore.isHidden(option.id)));
const filteredOptions = $derived(filterModelOptions(options, searchTerm));
@@ -196,11 +190,6 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
);
const emptyMessage = $derived(searchTerm ? 'No models found.' : 'No models yet.');
function handleInfoClick(modelName: string) {
infoModelId = modelName;
showModelDialog = true;
}
onMount(() => {
modelsStore.fetch().catch((error) => {
console.error('Unable to load models:', error);
@@ -210,10 +199,10 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
function handleOpenChange(open: boolean) {
if (loading || updating) return;
// a single-model llama.cpp server with no other backend has no list to
// show, so the trigger opens the model info dialog instead
// a single-model llama.cpp server with no other backend has no list to show,
// so the trigger opens the manager, which holds the one model's configuration
if (!isMultiModel) {
showModelDialog = open;
if (open) uiStore.openModelsManager();
return;
}
@@ -362,16 +351,10 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
return groupedFilteredOptions;
},
handleInfoClick,
handleOpenChange,
handleSelect,
get infoModelId() {
return infoModelId;
},
get isCurrentModelInCache() {
return isCurrentModelInCache;
},
@@ -430,16 +413,8 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
searchTerm = value;
},
setShowModelDialog(value: boolean) {
showModelDialog = value;
},
showBackendModels,
get showModelDialog() {
return showModelDialog;
},
get updating() {
return updating;
}
@@ -126,7 +126,6 @@
currentModel={selectedModel}
favorites={favoriteModels}
groups={groupedOptions}
onInfoClick={(modelName) => console.log('Info clicked:', modelName)}
onSelect={handleSelect}
/>
</div>
@@ -142,7 +141,6 @@
loaded: [loadedModels[0]],
providers: []
}}
onInfoClick={(modelName) => console.log('Info clicked:', modelName)}
onSelect={handleSelect}
/>
</div>
@@ -159,7 +157,6 @@
loaded: [],
providers: []
}}
onInfoClick={(modelName) => console.log('Info clicked:', modelName)}
onSelect={handleSelect}
/>
</div>