feat: Improvements
This commit is contained in:
parent
155af69edf
commit
c800a27faa
|
|
@ -3,7 +3,6 @@
|
||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Settings,
|
|
||||||
Zap,
|
Zap,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
PencilRuler,
|
PencilRuler,
|
||||||
|
|
@ -18,12 +17,10 @@
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||||
import { Switch } from '$lib/components/ui/switch';
|
import { Switch } from '$lib/components/ui/switch';
|
||||||
import { FILE_TYPE_ICONS, TOOLTIP_DELAY_DURATION } from '$lib/constants';
|
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 { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||||
import { mcpStore } from '$lib/stores/mcp.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';
|
import { SvelteSet } from 'svelte/reactivity';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -36,7 +33,6 @@
|
||||||
onFileUpload?: () => void;
|
onFileUpload?: () => void;
|
||||||
onSystemPromptClick?: () => void;
|
onSystemPromptClick?: () => void;
|
||||||
onMcpPromptClick?: () => void;
|
onMcpPromptClick?: () => void;
|
||||||
onMcpSettingsClick?: () => void;
|
|
||||||
onMcpResourcesClick?: () => void;
|
onMcpResourcesClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,7 +46,6 @@
|
||||||
onFileUpload,
|
onFileUpload,
|
||||||
onSystemPromptClick,
|
onSystemPromptClick,
|
||||||
onMcpPromptClick,
|
onMcpPromptClick,
|
||||||
onMcpSettingsClick,
|
|
||||||
onMcpResourcesClick
|
onMcpResourcesClick
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
|
|
@ -65,34 +60,25 @@
|
||||||
let dropdownOpen = $state(false);
|
let dropdownOpen = $state(false);
|
||||||
|
|
||||||
let expandedGroups = new SvelteSet<string>();
|
let expandedGroups = new SvelteSet<string>();
|
||||||
let groups = $derived(
|
let groups = $derived(toolsStore.toolGroups);
|
||||||
toolsStore.toolGroups.filter(
|
let activeGroups = $derived(
|
||||||
|
groups.filter(
|
||||||
(g) =>
|
(g) =>
|
||||||
g.source !== ToolSource.MCP ||
|
g.source !== ToolSource.MCP ||
|
||||||
!g.serverId ||
|
!g.serverId ||
|
||||||
conversationsStore.isMcpServerEnabledForChat(g.serverId)
|
conversationsStore.isMcpServerEnabledForChat(g.serverId)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
let totalToolCount = $derived(groups.reduce((n, g) => n + g.tools.length, 0));
|
let totalToolCount = $derived(activeGroups.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;
|
|
||||||
|
|
||||||
return mcpServers.filter((s) => {
|
function isGroupDisabled(group: ToolGroup): boolean {
|
||||||
const name = mcpStore.getServerLabel(s).toLowerCase();
|
return (
|
||||||
const url = s.url.toLowerCase();
|
group.source === ToolSource.MCP &&
|
||||||
return name.includes(query) || url.includes(query);
|
!!group.serverId &&
|
||||||
});
|
!conversationsStore.isMcpServerEnabledForChat(group.serverId)
|
||||||
});
|
);
|
||||||
|
}
|
||||||
|
let hoveredGroup = $state<string | null>(null);
|
||||||
|
|
||||||
const fileUploadTooltipText = 'Add files, system prompt or MCP Servers';
|
const fileUploadTooltipText = 'Add files, system prompt or MCP Servers';
|
||||||
|
|
||||||
|
|
@ -126,32 +112,15 @@
|
||||||
mcpStore.runHealthChecksForServers(mcpStore.getServersSorted().filter((s) => s.enabled));
|
mcpStore.runHealthChecksForServers(mcpStore.getServersSorted().filter((s) => s.enabled));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isServerEnabledForChat(serverId: string): boolean {
|
|
||||||
return conversationsStore.isMcpServerEnabledForChat(serverId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toggleServerForChat(serverId: string) {
|
async function toggleServerForChat(serverId: string) {
|
||||||
await conversationsStore.toggleMcpServerForChat(serverId);
|
await conversationsStore.toggleMcpServerForChat(serverId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMcpSubMenuOpen(open: boolean) {
|
|
||||||
if (open) {
|
|
||||||
mcpSearchQuery = '';
|
|
||||||
mcpStore.runHealthChecksForServers(mcpServers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMcpPromptClick() {
|
function handleMcpPromptClick() {
|
||||||
dropdownOpen = false;
|
dropdownOpen = false;
|
||||||
onMcpPromptClick?.();
|
onMcpPromptClick?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMcpSettingsClick() {
|
|
||||||
dropdownOpen = false;
|
|
||||||
onMcpSettingsClick?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMcpResourcesClick() {
|
function handleMcpResourcesClick() {
|
||||||
dropdownOpen = false;
|
dropdownOpen = false;
|
||||||
onMcpResourcesClick?.();
|
onMcpResourcesClick?.();
|
||||||
|
|
@ -299,7 +268,7 @@
|
||||||
</DropdownMenu.SubTrigger>
|
</DropdownMenu.SubTrigger>
|
||||||
|
|
||||||
<DropdownMenu.SubContent class="w-72 p-0">
|
<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">
|
<div class="px-3 py-4 text-center text-sm text-muted-foreground">
|
||||||
{#if toolsStore.loading}
|
{#if toolsStore.loading}
|
||||||
<Loader2 class="mx-auto mb-1 h-4 w-4 animate-spin" />
|
<Loader2 class="mx-auto mb-1 h-4 w-4 animate-spin" />
|
||||||
|
|
@ -311,14 +280,13 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="px-3 py-2 text-xs font-medium text-muted-foreground">
|
<div class="max-h-80 overflow-y-auto p-2 pr-1">
|
||||||
{enabledToolCount}/{totalToolCount} tools enabled
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="max-h-80 overflow-y-auto px-1 pb-2">
|
|
||||||
{#each groups as group (group.label)}
|
{#each groups as group (group.label)}
|
||||||
|
{@const groupDisabled = isGroupDisabled(group)}
|
||||||
{@const isExpanded = expandedGroups.has(group.label)}
|
{@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)}
|
{@const favicon = getFavicon(group)}
|
||||||
|
|
||||||
<Collapsible.Root
|
<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
|
<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}
|
{#if isExpanded}
|
||||||
<ChevronDown class="h-3.5 w-3.5 shrink-0" />
|
<ChevronDown class="h-3.5 w-3.5 shrink-0" />
|
||||||
|
|
@ -361,12 +340,23 @@
|
||||||
</span>
|
</span>
|
||||||
</Collapsible.Trigger>
|
</Collapsible.Trigger>
|
||||||
|
|
||||||
<Checkbox
|
{#if groupDisabled && hoveredGroup === group.label && group.serverId}
|
||||||
{checked}
|
<Switch
|
||||||
{indeterminate}
|
checked={false}
|
||||||
onCheckedChange={() => toolsStore.toggleGroup(group)}
|
onclick={(e: MouseEvent) => e.stopPropagation()}
|
||||||
class="mr-2 h-4 w-4 shrink-0"
|
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>
|
</div>
|
||||||
|
|
||||||
<Collapsible.Content>
|
<Collapsible.Content>
|
||||||
|
|
@ -374,12 +364,19 @@
|
||||||
{#each group.tools as tool (tool.function.name)}
|
{#each group.tools as tool (tool.function.name)}
|
||||||
<button
|
<button
|
||||||
type="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"
|
class="flex w-full items-center gap-2 rounded px-2 py-1 text-left text-sm transition-colors {groupDisabled
|
||||||
onclick={() => toolsStore.toggleTool(tool.function.name)}
|
? 'pointer-events-none opacity-40'
|
||||||
|
: 'hover:bg-muted/50'}"
|
||||||
|
onclick={() =>
|
||||||
|
!groupDisabled && toolsStore.toggleTool(tool.function.name)}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={toolsStore.isToolEnabled(tool.function.name)}
|
checked={groupDisabled
|
||||||
onCheckedChange={() => toolsStore.toggleTool(tool.function.name)}
|
? false
|
||||||
|
: toolsStore.isToolEnabled(tool.function.name)}
|
||||||
|
disabled={groupDisabled}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
!groupDisabled && toolsStore.toggleTool(tool.function.name)}
|
||||||
class="h-4 w-4 shrink-0"
|
class="h-4 w-4 shrink-0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -393,83 +390,14 @@
|
||||||
</Collapsible.Root>
|
</Collapsible.Root>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="px-3 py-2 text-xs font-medium text-muted-foreground">
|
||||||
|
{enabledToolCount}/{totalToolCount} tools enabled
|
||||||
|
</div> -->
|
||||||
{/if}
|
{/if}
|
||||||
</DropdownMenu.SubContent>
|
</DropdownMenu.SubContent>
|
||||||
</DropdownMenu.Sub>
|
</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}
|
{#if hasMcpPromptsSupport}
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex cursor-pointer items-center gap-2"
|
class="flex cursor-pointer items-center gap-2"
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Sheet from '$lib/components/ui/sheet';
|
import * as Sheet from '$lib/components/ui/sheet';
|
||||||
import { FILE_TYPE_ICONS } from '$lib/constants';
|
import { FILE_TYPE_ICONS } from '$lib/constants';
|
||||||
import { McpLogo } from '$lib/components/app';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
|
|
@ -15,7 +14,6 @@
|
||||||
onFileUpload?: () => void;
|
onFileUpload?: () => void;
|
||||||
onSystemPromptClick?: () => void;
|
onSystemPromptClick?: () => void;
|
||||||
onMcpPromptClick?: () => void;
|
onMcpPromptClick?: () => void;
|
||||||
onMcpSettingsClick?: () => void;
|
|
||||||
onMcpResourcesClick?: () => void;
|
onMcpResourcesClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,7 +27,6 @@
|
||||||
onFileUpload,
|
onFileUpload,
|
||||||
onSystemPromptClick,
|
onSystemPromptClick,
|
||||||
onMcpPromptClick,
|
onMcpPromptClick,
|
||||||
onMcpSettingsClick,
|
|
||||||
onMcpResourcesClick
|
onMcpResourcesClick
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
|
|
@ -40,10 +37,6 @@
|
||||||
onMcpPromptClick?.();
|
onMcpPromptClick?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMcpSettingsClick() {
|
|
||||||
onMcpSettingsClick?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMcpResourcesClick() {
|
function handleMcpResourcesClick() {
|
||||||
sheetOpen = false;
|
sheetOpen = false;
|
||||||
onMcpResourcesClick?.();
|
onMcpResourcesClick?.();
|
||||||
|
|
@ -143,12 +136,6 @@
|
||||||
<span>System Message</span>
|
<span>System Message</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button type="button" class={sheetItemClass} onclick={handleMcpSettingsClick}>
|
|
||||||
<McpLogo class="h-4 w-4 shrink-0" />
|
|
||||||
|
|
||||||
<span>MCP Servers</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{#if hasMcpPromptsSupport}
|
{#if hasMcpPromptsSupport}
|
||||||
<button type="button" class={sheetItemClass} onclick={handleMcpPromptClick}>
|
<button type="button" class={sheetItemClass} onclick={handleMcpPromptClick}>
|
||||||
<Zap class="h-4 w-4 shrink-0" />
|
<Zap class="h-4 w-4 shrink-0" />
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
ChatFormActionRecord,
|
ChatFormActionRecord,
|
||||||
ChatFormActionSubmit,
|
ChatFormActionSubmit,
|
||||||
McpServersSelector,
|
McpServersSelector,
|
||||||
|
McpServersSheet,
|
||||||
ModelsSelector,
|
ModelsSelector,
|
||||||
ModelsSelectorSheet
|
ModelsSelectorSheet
|
||||||
} from '$lib/components/app';
|
} from '$lib/components/app';
|
||||||
|
|
@ -197,7 +198,6 @@
|
||||||
{onSystemPromptClick}
|
{onSystemPromptClick}
|
||||||
{onMcpPromptClick}
|
{onMcpPromptClick}
|
||||||
{onMcpResourcesClick}
|
{onMcpResourcesClick}
|
||||||
onMcpSettingsClick={() => (showChatSettingsDialogWithMcpSection = true)}
|
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<ChatFormActionAttachmentsDropdown
|
<ChatFormActionAttachmentsDropdown
|
||||||
|
|
@ -210,17 +210,23 @@
|
||||||
{onSystemPromptClick}
|
{onSystemPromptClick}
|
||||||
{onMcpPromptClick}
|
{onMcpPromptClick}
|
||||||
{onMcpResourcesClick}
|
{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}
|
{/if}
|
||||||
|
|
||||||
<McpServersSelector
|
|
||||||
{disabled}
|
|
||||||
onSettingsClick={() => (showChatSettingsDialogWithMcpSection = true)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ml-auto flex items-center gap-1.5">
|
|
||||||
{#if isMobile.current}
|
{#if isMobile.current}
|
||||||
<ModelsSelectorSheet
|
<ModelsSelectorSheet
|
||||||
disabled={disabled || isOffline}
|
disabled={disabled || isOffline}
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if hasMcpServers && hasEnabledMcpServers && mcpFavicons.length > 0}
|
{#if hasMcpServers}
|
||||||
<DropdownMenu.Root
|
<DropdownMenu.Root
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
|
|
@ -84,11 +84,13 @@
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="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}
|
{disabled}
|
||||||
aria-label="MCP Servers"
|
aria-label="MCP Servers"
|
||||||
>
|
>
|
||||||
<McpActiveServersAvatars class={className} />
|
{#if hasEnabledMcpServers && mcpFavicons.length > 0}
|
||||||
|
<McpActiveServersAvatars class={className} />
|
||||||
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -96,6 +96,21 @@ export { default as McpActiveServersAvatars } from './McpActiveServersAvatars.sv
|
||||||
*/
|
*/
|
||||||
export { default as McpServersSelector } from './McpServersSelector.svelte';
|
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
|
* **McpCapabilitiesBadges** - Server capabilities display
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,9 @@
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelId: string;
|
modelId: string;
|
||||||
showOrgName?: boolean;
|
hideOrgName?: boolean;
|
||||||
showRaw?: boolean;
|
showRaw?: boolean;
|
||||||
|
hideQuantization?: boolean;
|
||||||
aliases?: string[];
|
aliases?: string[];
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
class?: string;
|
class?: string;
|
||||||
|
|
@ -14,8 +15,9 @@
|
||||||
|
|
||||||
let {
|
let {
|
||||||
modelId,
|
modelId,
|
||||||
showOrgName = false,
|
hideOrgName = false,
|
||||||
showRaw = undefined,
|
showRaw = undefined,
|
||||||
|
hideQuantization = false,
|
||||||
aliases,
|
aliases,
|
||||||
tags,
|
tags,
|
||||||
class: className = ''
|
class: className = ''
|
||||||
|
|
@ -40,7 +42,7 @@
|
||||||
{:else}
|
{:else}
|
||||||
<span class="flex min-w-0 flex-wrap items-center gap-1 {className}">
|
<span class="flex min-w-0 flex-wrap items-center gap-1 {className}">
|
||||||
<span class="min-w-0 truncate font-medium">
|
<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>
|
</span>
|
||||||
|
|
||||||
{#if parsed.params}
|
{#if parsed.params}
|
||||||
|
|
@ -49,7 +51,7 @@
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if parsed.quantization}
|
{#if parsed.quantization && !hideQuantization}
|
||||||
<span class={badgeClass}>
|
<span class={badgeClass}>
|
||||||
{parsed.quantization}
|
{parsed.quantization}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -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',
|
'inline-flex items-center gap-1.5 rounded-sm bg-muted-foreground/10 px-1.5 py-1 text-xs text-muted-foreground',
|
||||||
className
|
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" />
|
<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>
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="text-xs text-muted-foreground">No models available.</p>
|
<p class="text-xs text-muted-foreground">No models available.</p>
|
||||||
|
|
@ -290,7 +290,7 @@
|
||||||
: 'text-muted-foreground',
|
: 'text-muted-foreground',
|
||||||
isOpen ? 'text-foreground' : ''
|
isOpen ? 'text-foreground' : ''
|
||||||
)}
|
)}
|
||||||
style="max-width: min(calc(100cqw - 9rem), 20rem)"
|
style="max-width: min(calc(100cqw - 10rem), 20rem)"
|
||||||
disabled={disabled || updating}
|
disabled={disabled || updating}
|
||||||
>
|
>
|
||||||
<Package class="h-3.5 w-3.5" />
|
<Package class="h-3.5 w-3.5" />
|
||||||
|
|
@ -298,7 +298,7 @@
|
||||||
{#if selectedOption}
|
{#if selectedOption}
|
||||||
<Tooltip.Root>
|
<Tooltip.Root>
|
||||||
<Tooltip.Trigger class="min-w-0 overflow-hidden">
|
<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.Trigger>
|
||||||
|
|
||||||
<Tooltip.Content>
|
<Tooltip.Content>
|
||||||
|
|
@ -339,7 +339,7 @@
|
||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
disabled
|
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>
|
<span class="ml-2 text-xs whitespace-nowrap opacity-70">(not available)</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -349,7 +349,7 @@
|
||||||
<p class="px-4 py-3 text-sm text-muted-foreground">No models found.</p>
|
<p class="px-4 py-3 text-sm text-muted-foreground">No models found.</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#snippet modelOption(item: ModelItem, showOrgName: boolean)}
|
{#snippet modelOption(item: ModelItem, hideOrgName: boolean)}
|
||||||
{@const { option, flatIndex } = item}
|
{@const { option, flatIndex } = item}
|
||||||
{@const isSelected = currentModel === option.model || activeId === option.id}
|
{@const isSelected = currentModel === option.model || activeId === option.id}
|
||||||
{@const isHighlighted = flatIndex === highlightedIndex}
|
{@const isHighlighted = flatIndex === highlightedIndex}
|
||||||
|
|
@ -360,7 +360,7 @@
|
||||||
{isSelected}
|
{isSelected}
|
||||||
{isHighlighted}
|
{isHighlighted}
|
||||||
{isFav}
|
{isFav}
|
||||||
{showOrgName}
|
{hideOrgName}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
onInfoClick={handleInfoClick}
|
onInfoClick={handleInfoClick}
|
||||||
onMouseEnter={() => (highlightedIndex = flatIndex)}
|
onMouseEnter={() => (highlightedIndex = flatIndex)}
|
||||||
|
|
@ -408,7 +408,7 @@
|
||||||
{#if selectedOption}
|
{#if selectedOption}
|
||||||
<Tooltip.Root>
|
<Tooltip.Root>
|
||||||
<Tooltip.Trigger class="min-w-0 overflow-hidden">
|
<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.Trigger>
|
||||||
|
|
||||||
<Tooltip.Content>
|
<Tooltip.Content>
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
let render = $derived(renderOption ?? defaultOption);
|
let render = $derived(renderOption ?? defaultOption);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet defaultOption(item: ModelItem, showOrgName: boolean)}
|
{#snippet defaultOption(item: ModelItem, hideOrgName: boolean)}
|
||||||
{@const { option } = item}
|
{@const { option } = item}
|
||||||
{@const isSelected = currentModel === option.model || activeId === option.id}
|
{@const isSelected = currentModel === option.model || activeId === option.id}
|
||||||
{@const isFav = modelsStore.favouriteModelIds.has(option.model)}
|
{@const isFav = modelsStore.favouriteModelIds.has(option.model)}
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
{isSelected}
|
{isSelected}
|
||||||
isHighlighted={false}
|
isHighlighted={false}
|
||||||
{isFav}
|
{isFav}
|
||||||
{showOrgName}
|
{hideOrgName}
|
||||||
{onSelect}
|
{onSelect}
|
||||||
{onInfoClick}
|
{onInfoClick}
|
||||||
onMouseEnter={() => {}}
|
onMouseEnter={() => {}}
|
||||||
|
|
@ -48,14 +48,14 @@
|
||||||
{#if groups.loaded.length > 0}
|
{#if groups.loaded.length > 0}
|
||||||
<p class={sectionHeaderClass}>Loaded models</p>
|
<p class={sectionHeaderClass}>Loaded models</p>
|
||||||
{#each groups.loaded as item (`loaded-${item.option.id}`)}
|
{#each groups.loaded as item (`loaded-${item.option.id}`)}
|
||||||
{@render render(item, true)}
|
{@render render(item, false)}
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if groups.favourites.length > 0}
|
{#if groups.favourites.length > 0}
|
||||||
<p class={sectionHeaderClass}>Favourite models</p>
|
<p class={sectionHeaderClass}>Favourite models</p>
|
||||||
{#each groups.favourites as item (`fav-${item.option.id}`)}
|
{#each groups.favourites as item (`fav-${item.option.id}`)}
|
||||||
{@render render(item, true)}
|
{@render render(item, false)}
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
@ -66,7 +66,7 @@
|
||||||
<p class={orgHeaderClass}>{group.orgName}</p>
|
<p class={orgHeaderClass}>{group.orgName}</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#each group.items as item (item.option.id)}
|
{#each group.items as item (item.option.id)}
|
||||||
{@render render(item, false)}
|
{@render render(item, true)}
|
||||||
{/each}
|
{/each}
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
isHighlighted: boolean;
|
isHighlighted: boolean;
|
||||||
isFav: boolean;
|
isFav: boolean;
|
||||||
showOrgName?: boolean;
|
hideOrgName?: boolean;
|
||||||
onSelect: (modelId: string) => void;
|
onSelect: (modelId: string) => void;
|
||||||
onMouseEnter: () => void;
|
onMouseEnter: () => void;
|
||||||
onKeyDown: (e: KeyboardEvent) => void;
|
onKeyDown: (e: KeyboardEvent) => void;
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
isSelected,
|
isSelected,
|
||||||
isHighlighted,
|
isHighlighted,
|
||||||
isFav,
|
isFav,
|
||||||
showOrgName = false,
|
hideOrgName = false,
|
||||||
onSelect,
|
onSelect,
|
||||||
onMouseEnter,
|
onMouseEnter,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
|
|
@ -68,7 +68,7 @@
|
||||||
>
|
>
|
||||||
<ModelId
|
<ModelId
|
||||||
modelId={option.model}
|
modelId={option.model}
|
||||||
{showOrgName}
|
{hideOrgName}
|
||||||
aliases={option.aliases}
|
aliases={option.aliases}
|
||||||
tags={option.tags}
|
tags={option.tags}
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
import { isRouterMode } from '$lib/stores/server.svelte';
|
import { isRouterMode } from '$lib/stores/server.svelte';
|
||||||
import {
|
import {
|
||||||
DialogModelInformation,
|
DialogModelInformation,
|
||||||
|
ModelId,
|
||||||
ModelsSelectorList,
|
ModelsSelectorList,
|
||||||
SearchInput,
|
SearchInput,
|
||||||
TruncatedText
|
TruncatedText
|
||||||
|
|
@ -227,13 +228,22 @@
|
||||||
: 'text-muted-foreground',
|
: 'text-muted-foreground',
|
||||||
sheetOpen ? 'text-foreground' : ''
|
sheetOpen ? 'text-foreground' : ''
|
||||||
)}
|
)}
|
||||||
style="max-width: min(calc(100cqw - 9rem), 20rem)"
|
style="max-width: min(calc(100cqw - 10.5rem), 20rem)"
|
||||||
disabled={disabled || updating}
|
disabled={disabled || updating}
|
||||||
onclick={() => handleOpenChange(true)}
|
onclick={() => handleOpenChange(true)}
|
||||||
>
|
>
|
||||||
<Package class="h-3.5 w-3.5" />
|
<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}
|
{#if updating || isLoadingModel}
|
||||||
<Loader2 class="h-3 w-3.5 animate-spin" />
|
<Loader2 class="h-3 w-3.5 animate-spin" />
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue