feat: Improvements

This commit is contained in:
Aleksander Grygier 2026-03-20 14:23:04 +01:00
parent 155af69edf
commit c800a27faa
11 changed files with 312 additions and 183 deletions

View File

@ -3,7 +3,6 @@
import {
Plus,
MessageSquare,
Settings,
Zap,
FolderOpen,
PencilRuler,
@ -18,12 +17,10 @@
import * as Tooltip from '$lib/components/ui/tooltip';
import { Switch } from '$lib/components/ui/switch';
import { FILE_TYPE_ICONS, TOOLTIP_DELAY_DURATION } from '$lib/constants';
import { McpLogo, DropdownMenuSearchable } from '$lib/components/app';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { mcpStore } from '$lib/stores/mcp.svelte';
import { toolsStore, ToolSource } from '$lib/stores/tools.svelte';
import { toolsStore, ToolSource, type ToolGroup } from '$lib/stores/tools.svelte';
import { HealthCheckStatus } from '$lib/enums';
import { SvelteSet } from 'svelte/reactivity';
interface Props {
@ -36,7 +33,6 @@
onFileUpload?: () => void;
onSystemPromptClick?: () => void;
onMcpPromptClick?: () => void;
onMcpSettingsClick?: () => void;
onMcpResourcesClick?: () => void;
}
@ -50,7 +46,6 @@
onFileUpload,
onSystemPromptClick,
onMcpPromptClick,
onMcpSettingsClick,
onMcpResourcesClick
}: Props = $props();
@ -65,34 +60,25 @@
let dropdownOpen = $state(false);
let expandedGroups = new SvelteSet<string>();
let groups = $derived(
toolsStore.toolGroups.filter(
let groups = $derived(toolsStore.toolGroups);
let activeGroups = $derived(
groups.filter(
(g) =>
g.source !== ToolSource.MCP ||
!g.serverId ||
conversationsStore.isMcpServerEnabledForChat(g.serverId)
)
);
let totalToolCount = $derived(groups.reduce((n, g) => n + g.tools.length, 0));
let enabledToolCount = $derived(
groups.reduce(
(n, g) => n + g.tools.filter((t) => toolsStore.isToolEnabled(t.function.name)).length,
0
)
);
let mcpServers = $derived(mcpStore.getServersSorted().filter((s) => s.enabled));
let hasMcpServers = $derived(mcpServers.length > 0);
let mcpSearchQuery = $state('');
let filteredMcpServers = $derived.by(() => {
const query = mcpSearchQuery.toLowerCase().trim();
if (!query) return mcpServers;
let totalToolCount = $derived(activeGroups.reduce((n, g) => n + g.tools.length, 0));
return mcpServers.filter((s) => {
const name = mcpStore.getServerLabel(s).toLowerCase();
const url = s.url.toLowerCase();
return name.includes(query) || url.includes(query);
});
});
function isGroupDisabled(group: ToolGroup): boolean {
return (
group.source === ToolSource.MCP &&
!!group.serverId &&
!conversationsStore.isMcpServerEnabledForChat(group.serverId)
);
}
let hoveredGroup = $state<string | null>(null);
const fileUploadTooltipText = 'Add files, system prompt or MCP Servers';
@ -126,32 +112,15 @@
mcpStore.runHealthChecksForServers(mcpStore.getServersSorted().filter((s) => s.enabled));
}
}
function isServerEnabledForChat(serverId: string): boolean {
return conversationsStore.isMcpServerEnabledForChat(serverId);
}
async function toggleServerForChat(serverId: string) {
await conversationsStore.toggleMcpServerForChat(serverId);
}
function handleMcpSubMenuOpen(open: boolean) {
if (open) {
mcpSearchQuery = '';
mcpStore.runHealthChecksForServers(mcpServers);
}
}
function handleMcpPromptClick() {
dropdownOpen = false;
onMcpPromptClick?.();
}
function handleMcpSettingsClick() {
dropdownOpen = false;
onMcpSettingsClick?.();
}
function handleMcpResourcesClick() {
dropdownOpen = false;
onMcpResourcesClick?.();
@ -299,7 +268,7 @@
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent class="w-72 p-0">
{#if totalToolCount === 0}
{#if totalToolCount === 0 && groups.length === 0}
<div class="px-3 py-4 text-center text-sm text-muted-foreground">
{#if toolsStore.loading}
<Loader2 class="mx-auto mb-1 h-4 w-4 animate-spin" />
@ -311,14 +280,13 @@
{/if}
</div>
{:else}
<div class="px-3 py-2 text-xs font-medium text-muted-foreground">
{enabledToolCount}/{totalToolCount} tools enabled
</div>
<div class="max-h-80 overflow-y-auto px-1 pb-2">
<div class="max-h-80 overflow-y-auto p-2 pr-1">
{#each groups as group (group.label)}
{@const groupDisabled = isGroupDisabled(group)}
{@const isExpanded = expandedGroups.has(group.label)}
{@const { checked, indeterminate } = getGroupCheckedState(group)}
{@const { checked, indeterminate } = groupDisabled
? { checked: false, indeterminate: false }
: getGroupCheckedState(group)}
{@const favicon = getFavicon(group)}
<Collapsible.Root
@ -331,9 +299,20 @@
}
}}
>
<div class="flex items-center gap-1">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="flex items-center gap-1"
onmouseenter={() => {
if (groupDisabled) hoveredGroup = group.label;
}}
onmouseleave={() => {
if (hoveredGroup === group.label) hoveredGroup = null;
}}
>
<Collapsible.Trigger
class="flex min-w-0 flex-1 items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-muted/50"
class="flex min-w-0 flex-1 items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-muted/50 {groupDisabled
? 'opacity-40'
: ''}"
>
{#if isExpanded}
<ChevronDown class="h-3.5 w-3.5 shrink-0" />
@ -361,12 +340,23 @@
</span>
</Collapsible.Trigger>
<Checkbox
{checked}
{indeterminate}
onCheckedChange={() => toolsStore.toggleGroup(group)}
class="mr-2 h-4 w-4 shrink-0"
/>
{#if groupDisabled && hoveredGroup === group.label && group.serverId}
<Switch
checked={false}
onclick={(e: MouseEvent) => e.stopPropagation()}
onCheckedChange={() =>
group.serverId && toggleServerForChat(group.serverId)}
class="mr-2 shrink-0"
/>
{:else}
<Checkbox
{checked}
{indeterminate}
disabled={groupDisabled}
onCheckedChange={() => toolsStore.toggleGroup(group)}
class="mr-2 h-4 w-4 shrink-0 {groupDisabled ? 'opacity-40' : ''}"
/>
{/if}
</div>
<Collapsible.Content>
@ -374,12 +364,19 @@
{#each group.tools as tool (tool.function.name)}
<button
type="button"
class="flex w-full items-center gap-2 rounded px-2 py-1 text-left text-sm transition-colors hover:bg-muted/50"
onclick={() => toolsStore.toggleTool(tool.function.name)}
class="flex w-full items-center gap-2 rounded px-2 py-1 text-left text-sm transition-colors {groupDisabled
? 'pointer-events-none opacity-40'
: 'hover:bg-muted/50'}"
onclick={() =>
!groupDisabled && toolsStore.toggleTool(tool.function.name)}
>
<Checkbox
checked={toolsStore.isToolEnabled(tool.function.name)}
onCheckedChange={() => toolsStore.toggleTool(tool.function.name)}
checked={groupDisabled
? false
: toolsStore.isToolEnabled(tool.function.name)}
disabled={groupDisabled}
onCheckedChange={() =>
!groupDisabled && toolsStore.toggleTool(tool.function.name)}
class="h-4 w-4 shrink-0"
/>
@ -393,83 +390,14 @@
</Collapsible.Root>
{/each}
</div>
<!-- <div class="px-3 py-2 text-xs font-medium text-muted-foreground">
{enabledToolCount}/{totalToolCount} tools enabled
</div> -->
{/if}
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
<DropdownMenu.Sub onOpenChange={handleMcpSubMenuOpen}>
<DropdownMenu.SubTrigger class="flex cursor-pointer items-center gap-2">
<McpLogo class="h-4 w-4" />
<span>MCP Servers</span>
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent class="w-72 pt-0">
<DropdownMenuSearchable
placeholder="Search servers..."
bind:searchValue={mcpSearchQuery}
emptyMessage={hasMcpServers ? 'No servers found' : 'No MCP servers configured'}
isEmpty={filteredMcpServers.length === 0}
>
<div class="max-h-64 overflow-y-auto">
{#each filteredMcpServers as server (server.id)}
{@const healthState = mcpStore.getHealthCheckState(server.id)}
{@const hasError = healthState.status === HealthCheckStatus.ERROR}
{@const isEnabledForChat = isServerEnabledForChat(server.id)}
<button
type="button"
class="flex w-full items-center justify-between gap-2 rounded-sm px-2 py-2 text-left transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
onclick={() => !hasError && toggleServerForChat(server.id)}
disabled={hasError}
>
<div class="flex min-w-0 flex-1 items-center gap-2">
{#if mcpStore.getServerFavicon(server.id)}
<img
src={mcpStore.getServerFavicon(server.id)}
alt=""
class="h-4 w-4 shrink-0 rounded-sm"
onerror={(e) => {
(e.currentTarget as HTMLImageElement).style.display = 'none';
}}
/>
{/if}
<span class="truncate text-sm">{mcpStore.getServerLabel(server)}</span>
{#if hasError}
<span
class="shrink-0 rounded bg-destructive/15 px-1.5 py-0.5 text-xs text-destructive"
>
Error
</span>
{/if}
</div>
<Switch
checked={isEnabledForChat}
disabled={hasError}
onclick={(e: MouseEvent) => e.stopPropagation()}
onCheckedChange={() => toggleServerForChat(server.id)}
/>
</button>
{/each}
</div>
{#snippet footer()}
<DropdownMenu.Item
class="flex cursor-pointer items-center gap-2"
onclick={handleMcpSettingsClick}
>
<Settings class="h-4 w-4" />
<span>Manage MCP Servers</span>
</DropdownMenu.Item>
{/snippet}
</DropdownMenuSearchable>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
{#if hasMcpPromptsSupport}
<DropdownMenu.Item
class="flex cursor-pointer items-center gap-2"

View File

@ -3,7 +3,6 @@
import { Button } from '$lib/components/ui/button';
import * as Sheet from '$lib/components/ui/sheet';
import { FILE_TYPE_ICONS } from '$lib/constants';
import { McpLogo } from '$lib/components/app';
interface Props {
class?: string;
@ -15,7 +14,6 @@
onFileUpload?: () => void;
onSystemPromptClick?: () => void;
onMcpPromptClick?: () => void;
onMcpSettingsClick?: () => void;
onMcpResourcesClick?: () => void;
}
@ -29,7 +27,6 @@
onFileUpload,
onSystemPromptClick,
onMcpPromptClick,
onMcpSettingsClick,
onMcpResourcesClick
}: Props = $props();
@ -40,10 +37,6 @@
onMcpPromptClick?.();
}
function handleMcpSettingsClick() {
onMcpSettingsClick?.();
}
function handleMcpResourcesClick() {
sheetOpen = false;
onMcpResourcesClick?.();
@ -143,12 +136,6 @@
<span>System Message</span>
</button>
<button type="button" class={sheetItemClass} onclick={handleMcpSettingsClick}>
<McpLogo class="h-4 w-4 shrink-0" />
<span>MCP Servers</span>
</button>
{#if hasMcpPromptsSupport}
<button type="button" class={sheetItemClass} onclick={handleMcpPromptClick}>
<Zap class="h-4 w-4 shrink-0" />

View File

@ -7,6 +7,7 @@
ChatFormActionRecord,
ChatFormActionSubmit,
McpServersSelector,
McpServersSheet,
ModelsSelector,
ModelsSelectorSheet
} from '$lib/components/app';
@ -197,7 +198,6 @@
{onSystemPromptClick}
{onMcpPromptClick}
{onMcpResourcesClick}
onMcpSettingsClick={() => (showChatSettingsDialogWithMcpSection = true)}
/>
{:else}
<ChatFormActionAttachmentsDropdown
@ -210,17 +210,23 @@
{onSystemPromptClick}
{onMcpPromptClick}
{onMcpResourcesClick}
onMcpSettingsClick={() => (showChatSettingsDialogWithMcpSection = true)}
/>
{/if}
</div>
<div class="ml-auto flex items-center gap-2">
{#if isMobile.current}
<McpServersSheet
{disabled}
onSettingsClick={() => (showChatSettingsDialogWithMcpSection = true)}
/>
{:else}
<McpServersSelector
{disabled}
onSettingsClick={() => (showChatSettingsDialogWithMcpSection = true)}
/>
{/if}
<McpServersSelector
{disabled}
onSettingsClick={() => (showChatSettingsDialogWithMcpSection = true)}
/>
</div>
<div class="ml-auto flex items-center gap-1.5">
{#if isMobile.current}
<ModelsSelectorSheet
disabled={disabled || isOffline}

View File

@ -66,7 +66,7 @@
}
</script>
{#if hasMcpServers && hasEnabledMcpServers && mcpFavicons.length > 0}
{#if hasMcpServers}
<DropdownMenu.Root
onOpenChange={(open) => {
if (!open) {
@ -84,11 +84,13 @@
>
<button
type="button"
class="inline-flex cursor-pointer items-center rounded-sm py-1 disabled:cursor-not-allowed disabled:opacity-60"
class="inline-flex cursor-pointer items-center gap-1.5 rounded-sm py-1 disabled:cursor-not-allowed disabled:opacity-60"
{disabled}
aria-label="MCP Servers"
>
<McpActiveServersAvatars class={className} />
{#if hasEnabledMcpServers && mcpFavicons.length > 0}
<McpActiveServersAvatars class={className} />
{/if}
</button>
</DropdownMenu.Trigger>

View File

@ -0,0 +1,179 @@
<script lang="ts">
import { Settings } from '@lucide/svelte';
import * as Sheet from '$lib/components/ui/sheet';
import { Switch } from '$lib/components/ui/switch';
import { McpActiveServersAvatars, SearchInput } from '$lib/components/app';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { mcpStore } from '$lib/stores/mcp.svelte';
import { HealthCheckStatus } from '$lib/enums';
import type { MCPServerSettingsEntry } from '$lib/types';
interface Props {
class?: string;
disabled?: boolean;
onSettingsClick?: () => void;
}
let { class: className = '', disabled = false, onSettingsClick }: Props = $props();
let searchQuery = $state('');
let mcpServers = $derived(mcpStore.getServersSorted().filter((s) => s.enabled));
let hasMcpServers = $derived(mcpServers.length > 0);
let enabledMcpServersForChat = $derived(
mcpServers.filter((s) => conversationsStore.isMcpServerEnabledForChat(s.id) && s.url.trim())
);
let healthyEnabledMcpServers = $derived(
enabledMcpServersForChat.filter((s) => {
const healthState = mcpStore.getHealthCheckState(s.id);
return healthState.status !== HealthCheckStatus.ERROR;
})
);
let hasEnabledMcpServers = $derived(enabledMcpServersForChat.length > 0);
let mcpFavicons = $derived(
healthyEnabledMcpServers
.slice(0, 3)
.map((s) => ({ id: s.id, url: mcpStore.getServerFavicon(s.id) }))
.filter((f) => f.url !== null)
);
let filteredMcpServers = $derived.by(() => {
const query = searchQuery.toLowerCase().trim();
if (query) {
return mcpServers.filter((s) => {
const name = getServerLabel(s).toLowerCase();
const url = s.url.toLowerCase();
return name.includes(query) || url.includes(query);
});
}
return mcpServers;
});
let sheetOpen = $state(false);
function getServerLabel(server: MCPServerSettingsEntry): string {
return mcpStore.getServerLabel(server);
}
function handleOpenChange(open: boolean) {
if (open) {
sheetOpen = true;
searchQuery = '';
mcpStore.runHealthChecksForServers(mcpServers);
} else {
sheetOpen = false;
searchQuery = '';
}
}
function handleSheetOpenChange(open: boolean) {
if (!open) {
handleOpenChange(false);
}
}
export function open() {
handleOpenChange(true);
}
function isServerEnabledForChat(serverId: string): boolean {
return conversationsStore.isMcpServerEnabledForChat(serverId);
}
async function toggleServerForChat(serverId: string) {
await conversationsStore.toggleMcpServerForChat(serverId);
}
</script>
{#if hasMcpServers}
<button
type="button"
class="inline-flex cursor-pointer items-center gap-1.5 rounded-sm py-1 disabled:cursor-not-allowed disabled:opacity-60"
{disabled}
aria-label="MCP Servers"
onclick={() => handleOpenChange(true)}
>
{#if hasEnabledMcpServers && mcpFavicons.length > 0}
<McpActiveServersAvatars class={className} />
{/if}
</button>
<Sheet.Root bind:open={sheetOpen} onOpenChange={handleSheetOpenChange}>
<Sheet.Content side="bottom" class="max-h-[85vh] gap-1">
<Sheet.Header>
<Sheet.Title>MCP Servers</Sheet.Title>
<Sheet.Description class="sr-only">
Toggle MCP servers for the current conversation
</Sheet.Description>
</Sheet.Header>
<div class="flex flex-col gap-1 pb-4">
<div class="mb-3 px-4">
<SearchInput placeholder="Search servers..." bind:value={searchQuery} />
</div>
<div class="max-h-[60vh] overflow-y-auto px-2">
{#if filteredMcpServers.length === 0}
<p class="px-3 py-3 text-center text-sm text-muted-foreground">No servers found.</p>
{/if}
{#each filteredMcpServers as server (server.id)}
{@const healthState = mcpStore.getHealthCheckState(server.id)}
{@const hasError = healthState.status === HealthCheckStatus.ERROR}
{@const isEnabledForChat = isServerEnabledForChat(server.id)}
<button
type="button"
class="flex w-full items-center justify-between gap-2 rounded-md px-3 py-2.5 text-left transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
onclick={() => !hasError && toggleServerForChat(server.id)}
disabled={hasError}
>
<div class="flex min-w-0 flex-1 items-center gap-2">
{#if mcpStore.getServerFavicon(server.id)}
<img
src={mcpStore.getServerFavicon(server.id)}
alt=""
class="h-4 w-4 shrink-0 rounded-sm"
onerror={(e) => {
(e.currentTarget as HTMLImageElement).style.display = 'none';
}}
/>
{/if}
<span class="truncate text-sm">{getServerLabel(server)}</span>
{#if hasError}
<span
class="shrink-0 rounded bg-destructive/15 px-1.5 py-0.5 text-xs text-destructive"
>
Error
</span>
{/if}
</div>
<Switch
checked={isEnabledForChat}
disabled={hasError}
onclick={(e: MouseEvent) => e.stopPropagation()}
onCheckedChange={() => toggleServerForChat(server.id)}
/>
</button>
{/each}
</div>
<div class="mt-2 border-t px-2 pt-2">
<button
type="button"
class="flex w-full cursor-pointer items-center gap-2 rounded-md px-3 py-2.5 text-sm transition-colors hover:bg-accent"
onclick={() => {
handleOpenChange(false);
onSettingsClick?.();
}}
>
<Settings class="h-4 w-4" />
<span>Manage MCP Servers</span>
</button>
</div>
</div>
</Sheet.Content>
</Sheet.Root>
{/if}

View File

@ -96,6 +96,21 @@ export { default as McpActiveServersAvatars } from './McpActiveServersAvatars.sv
*/
export { default as McpServersSelector } from './McpServersSelector.svelte';
/**
* **McpServersSheet** - Mobile MCP server toggle sheet
*
* Bottom sheet variant of McpServersSelector for mobile devices.
* Uses Sheet UI instead of dropdown for better touch interaction.
*
* @example
* ```svelte
* <McpServersSheet
* onSettingsClick={() => showMcpSettings = true}
* />
* ```
*/
export { default as McpServersSheet } from './McpServersSheet.svelte';
/**
* **McpCapabilitiesBadges** - Server capabilities display
*

View File

@ -5,8 +5,9 @@
interface Props {
modelId: string;
showOrgName?: boolean;
hideOrgName?: boolean;
showRaw?: boolean;
hideQuantization?: boolean;
aliases?: string[];
tags?: string[];
class?: string;
@ -14,8 +15,9 @@
let {
modelId,
showOrgName = false,
hideOrgName = false,
showRaw = undefined,
hideQuantization = false,
aliases,
tags,
class: className = ''
@ -40,7 +42,7 @@
{:else}
<span class="flex min-w-0 flex-wrap items-center gap-1 {className}">
<span class="min-w-0 truncate font-medium">
{#if showOrgName && parsed.orgName && !(aliases && aliases.length > 0)}{parsed.orgName}/{/if}{displayName}
{#if !hideOrgName && parsed.orgName && !(aliases && aliases.length > 0)}{parsed.orgName}/{/if}{displayName}
</span>
{#if parsed.params}
@ -49,7 +51,7 @@
</span>
{/if}
{#if parsed.quantization}
{#if parsed.quantization && !hideQuantization}
<span class={badgeClass}>
{parsed.quantization}
</span>

View File

@ -256,11 +256,11 @@
'inline-flex items-center gap-1.5 rounded-sm bg-muted-foreground/10 px-1.5 py-1 text-xs text-muted-foreground',
className
)}
style="max-width: min(calc(100cqw - 9rem), 20rem)"
style="max-width: min(calc(100cqw - 10rem), 20rem)"
>
<Package class="h-3.5 w-3.5" />
<ModelId modelId={currentModel} class="min-w-0" showOrgName />
<ModelId modelId={currentModel} class="min-w-0" hideQuantization />
</span>
{:else}
<p class="text-xs text-muted-foreground">No models available.</p>
@ -290,7 +290,7 @@
: 'text-muted-foreground',
isOpen ? 'text-foreground' : ''
)}
style="max-width: min(calc(100cqw - 9rem), 20rem)"
style="max-width: min(calc(100cqw - 10rem), 20rem)"
disabled={disabled || updating}
>
<Package class="h-3.5 w-3.5" />
@ -298,7 +298,7 @@
{#if selectedOption}
<Tooltip.Root>
<Tooltip.Trigger class="min-w-0 overflow-hidden">
<ModelId modelId={selectedOption.model} class="min-w-0" showOrgName />
<ModelId modelId={selectedOption.model} class="min-w-0" hideQuantization />
</Tooltip.Trigger>
<Tooltip.Content>
@ -339,7 +339,7 @@
aria-disabled="true"
disabled
>
<ModelId modelId={currentModel} class="flex-1" showOrgName />
<ModelId modelId={currentModel} class="flex-1" hideQuantization />
<span class="ml-2 text-xs whitespace-nowrap opacity-70">(not available)</span>
</button>
@ -349,7 +349,7 @@
<p class="px-4 py-3 text-sm text-muted-foreground">No models found.</p>
{/if}
{#snippet modelOption(item: ModelItem, showOrgName: boolean)}
{#snippet modelOption(item: ModelItem, hideOrgName: boolean)}
{@const { option, flatIndex } = item}
{@const isSelected = currentModel === option.model || activeId === option.id}
{@const isHighlighted = flatIndex === highlightedIndex}
@ -360,7 +360,7 @@
{isSelected}
{isHighlighted}
{isFav}
{showOrgName}
{hideOrgName}
onSelect={handleSelect}
onInfoClick={handleInfoClick}
onMouseEnter={() => (highlightedIndex = flatIndex)}
@ -408,7 +408,7 @@
{#if selectedOption}
<Tooltip.Root>
<Tooltip.Trigger class="min-w-0 overflow-hidden">
<ModelId modelId={selectedOption.model} class="min-w-0" showOrgName />
<ModelId modelId={selectedOption.model} class="min-w-0" />
</Tooltip.Trigger>
<Tooltip.Content>

View File

@ -27,7 +27,7 @@
let render = $derived(renderOption ?? defaultOption);
</script>
{#snippet defaultOption(item: ModelItem, showOrgName: boolean)}
{#snippet defaultOption(item: ModelItem, hideOrgName: boolean)}
{@const { option } = item}
{@const isSelected = currentModel === option.model || activeId === option.id}
{@const isFav = modelsStore.favouriteModelIds.has(option.model)}
@ -37,7 +37,7 @@
{isSelected}
isHighlighted={false}
{isFav}
{showOrgName}
{hideOrgName}
{onSelect}
{onInfoClick}
onMouseEnter={() => {}}
@ -48,14 +48,14 @@
{#if groups.loaded.length > 0}
<p class={sectionHeaderClass}>Loaded models</p>
{#each groups.loaded as item (`loaded-${item.option.id}`)}
{@render render(item, true)}
{@render render(item, false)}
{/each}
{/if}
{#if groups.favourites.length > 0}
<p class={sectionHeaderClass}>Favourite models</p>
{#each groups.favourites as item (`fav-${item.option.id}`)}
{@render render(item, true)}
{@render render(item, false)}
{/each}
{/if}
@ -66,7 +66,7 @@
<p class={orgHeaderClass}>{group.orgName}</p>
{/if}
{#each group.items as item (item.option.id)}
{@render render(item, false)}
{@render render(item, true)}
{/each}
{/each}
{/if}

View File

@ -20,7 +20,7 @@
isSelected: boolean;
isHighlighted: boolean;
isFav: boolean;
showOrgName?: boolean;
hideOrgName?: boolean;
onSelect: (modelId: string) => void;
onMouseEnter: () => void;
onKeyDown: (e: KeyboardEvent) => void;
@ -32,7 +32,7 @@
isSelected,
isHighlighted,
isFav,
showOrgName = false,
hideOrgName = false,
onSelect,
onMouseEnter,
onKeyDown,
@ -68,7 +68,7 @@
>
<ModelId
modelId={option.model}
{showOrgName}
{hideOrgName}
aliases={option.aliases}
tags={option.tags}
class="flex-1"

View File

@ -14,6 +14,7 @@
import { isRouterMode } from '$lib/stores/server.svelte';
import {
DialogModelInformation,
ModelId,
ModelsSelectorList,
SearchInput,
TruncatedText
@ -227,13 +228,22 @@
: 'text-muted-foreground',
sheetOpen ? 'text-foreground' : ''
)}
style="max-width: min(calc(100cqw - 9rem), 20rem)"
style="max-width: min(calc(100cqw - 10.5rem), 20rem)"
disabled={disabled || updating}
onclick={() => handleOpenChange(true)}
>
<Package class="h-3.5 w-3.5" />
<TruncatedText text={selectedOption?.model || 'Select model'} class="min-w-0 font-medium" />
{#if !selectedOption}
<span class="min-w-0 font-medium">Select model</span>
{:else}
<ModelId
class="text-xs"
modelId={selectedOption?.model || ''}
hideQuantization
hideOrgName
/>
{/if}
{#if updating || isLoadingModel}
<Loader2 class="h-3 w-3.5 animate-spin" />