From c800a27faa49924a204dc758d113d5c014edb855 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Fri, 20 Mar 2026 14:23:04 +0100 Subject: [PATCH] feat: Improvements --- .../ChatFormActionAttachmentsDropdown.svelte | 200 ++++++------------ .../ChatFormActionAttachmentsSheet.svelte | 13 -- .../ChatFormActions/ChatFormActions.svelte | 24 ++- .../app/mcp/McpServersSelector.svelte | 8 +- .../components/app/mcp/McpServersSheet.svelte | 179 ++++++++++++++++ .../webui/src/lib/components/app/mcp/index.ts | 15 ++ .../lib/components/app/models/ModelId.svelte | 10 +- .../app/models/ModelsSelector.svelte | 16 +- .../app/models/ModelsSelectorList.svelte | 10 +- .../app/models/ModelsSelectorOption.svelte | 6 +- .../app/models/ModelsSelectorSheet.svelte | 14 +- 11 files changed, 312 insertions(+), 183 deletions(-) create mode 100644 tools/server/webui/src/lib/components/app/mcp/McpServersSheet.svelte diff --git a/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAttachmentsDropdown.svelte b/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAttachmentsDropdown.svelte index db0bd18fa0..d0acd19436 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAttachmentsDropdown.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAttachmentsDropdown.svelte @@ -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(); - 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(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 @@ - {#if totalToolCount === 0} + {#if totalToolCount === 0 && groups.length === 0}
{#if toolsStore.loading} @@ -311,14 +280,13 @@ {/if}
{:else} -
- {enabledToolCount}/{totalToolCount} tools enabled -
- -
+
{#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)} -
+ +
{ + if (groupDisabled) hoveredGroup = group.label; + }} + onmouseleave={() => { + if (hoveredGroup === group.label) hoveredGroup = null; + }} + > {#if isExpanded} @@ -361,12 +340,23 @@ - toolsStore.toggleGroup(group)} - class="mr-2 h-4 w-4 shrink-0" - /> + {#if groupDisabled && hoveredGroup === group.label && group.serverId} + e.stopPropagation()} + onCheckedChange={() => + group.serverId && toggleServerForChat(group.serverId)} + class="mr-2 shrink-0" + /> + {:else} + toolsStore.toggleGroup(group)} + class="mr-2 h-4 w-4 shrink-0 {groupDisabled ? 'opacity-40' : ''}" + /> + {/if}
@@ -374,12 +364,19 @@ {#each group.tools as tool (tool.function.name)}
+ + {/if} - - - - - MCP Servers - - - - -
- {#each filteredMcpServers as server (server.id)} - {@const healthState = mcpStore.getHealthCheckState(server.id)} - {@const hasError = healthState.status === HealthCheckStatus.ERROR} - {@const isEnabledForChat = isServerEnabledForChat(server.id)} - - - {/each} -
- - {#snippet footer()} - - - - Manage MCP Servers - - {/snippet} -
-
-
- {#if hasMcpPromptsSupport} 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 @@ System Message - - {#if hasMcpPromptsSupport}
+ +
+ {#if isMobile.current} + (showChatSettingsDialogWithMcpSection = true)} + /> + {:else} + (showChatSettingsDialogWithMcpSection = true)} /> {/if} - (showChatSettingsDialogWithMcpSection = true)} - /> -
- -
{#if isMobile.current} -{#if hasMcpServers && hasEnabledMcpServers && mcpFavicons.length > 0} +{#if hasMcpServers} { if (!open) { @@ -84,11 +84,13 @@ > diff --git a/tools/server/webui/src/lib/components/app/mcp/McpServersSheet.svelte b/tools/server/webui/src/lib/components/app/mcp/McpServersSheet.svelte new file mode 100644 index 0000000000..8ff51b7c29 --- /dev/null +++ b/tools/server/webui/src/lib/components/app/mcp/McpServersSheet.svelte @@ -0,0 +1,179 @@ + + +{#if hasMcpServers} + + + + + + MCP Servers + + + Toggle MCP servers for the current conversation + + + +
+
+ +
+ +
+ {#if filteredMcpServers.length === 0} +

No servers found.

+ {/if} + + {#each filteredMcpServers as server (server.id)} + {@const healthState = mcpStore.getHealthCheckState(server.id)} + {@const hasError = healthState.status === HealthCheckStatus.ERROR} + {@const isEnabledForChat = isServerEnabledForChat(server.id)} + + + {/each} +
+ +
+ +
+
+
+
+{/if} diff --git a/tools/server/webui/src/lib/components/app/mcp/index.ts b/tools/server/webui/src/lib/components/app/mcp/index.ts index 6ab262a8c1..861b605aed 100644 --- a/tools/server/webui/src/lib/components/app/mcp/index.ts +++ b/tools/server/webui/src/lib/components/app/mcp/index.ts @@ -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 + * showMcpSettings = true} + * /> + * ``` + */ +export { default as McpServersSheet } from './McpServersSheet.svelte'; + /** * **McpCapabilitiesBadges** - Server capabilities display * diff --git a/tools/server/webui/src/lib/components/app/models/ModelId.svelte b/tools/server/webui/src/lib/components/app/models/ModelId.svelte index 5fda493429..108a7d9e39 100644 --- a/tools/server/webui/src/lib/components/app/models/ModelId.svelte +++ b/tools/server/webui/src/lib/components/app/models/ModelId.svelte @@ -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} - {#if showOrgName && parsed.orgName && !(aliases && aliases.length > 0)}{parsed.orgName}/{/if}{displayName} + {#if !hideOrgName && parsed.orgName && !(aliases && aliases.length > 0)}{parsed.orgName}/{/if}{displayName} {#if parsed.params} @@ -49,7 +51,7 @@ {/if} - {#if parsed.quantization} + {#if parsed.quantization && !hideQuantization} {parsed.quantization} diff --git a/tools/server/webui/src/lib/components/app/models/ModelsSelector.svelte b/tools/server/webui/src/lib/components/app/models/ModelsSelector.svelte index bf489443fa..94f945dfdd 100644 --- a/tools/server/webui/src/lib/components/app/models/ModelsSelector.svelte +++ b/tools/server/webui/src/lib/components/app/models/ModelsSelector.svelte @@ -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)" > - + {:else}

No models available.

@@ -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} > @@ -298,7 +298,7 @@ {#if selectedOption} - + @@ -339,7 +339,7 @@ aria-disabled="true" disabled > - + (not available) @@ -349,7 +349,7 @@

No models found.

{/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} - + diff --git a/tools/server/webui/src/lib/components/app/models/ModelsSelectorList.svelte b/tools/server/webui/src/lib/components/app/models/ModelsSelectorList.svelte index 86d798670c..baa5c2de4b 100644 --- a/tools/server/webui/src/lib/components/app/models/ModelsSelectorList.svelte +++ b/tools/server/webui/src/lib/components/app/models/ModelsSelectorList.svelte @@ -27,7 +27,7 @@ let render = $derived(renderOption ?? defaultOption); -{#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}

Loaded models

{#each groups.loaded as item (`loaded-${item.option.id}`)} - {@render render(item, true)} + {@render render(item, false)} {/each} {/if} {#if groups.favourites.length > 0}

Favourite models

{#each groups.favourites as item (`fav-${item.option.id}`)} - {@render render(item, true)} + {@render render(item, false)} {/each} {/if} @@ -66,7 +66,7 @@

{group.orgName}

{/if} {#each group.items as item (item.option.id)} - {@render render(item, false)} + {@render render(item, true)} {/each} {/each} {/if} diff --git a/tools/server/webui/src/lib/components/app/models/ModelsSelectorOption.svelte b/tools/server/webui/src/lib/components/app/models/ModelsSelectorOption.svelte index 8f44bb8de1..56757e3e1d 100644 --- a/tools/server/webui/src/lib/components/app/models/ModelsSelectorOption.svelte +++ b/tools/server/webui/src/lib/components/app/models/ModelsSelectorOption.svelte @@ -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 @@ > handleOpenChange(true)} > - + {#if !selectedOption} + Select model + {:else} + + {/if} {#if updating || isLoadingModel}