feat: Display and manage servers in ChatForm actions
This commit is contained in:
parent
74345d8785
commit
260375819d
|
|
@ -1,17 +1,20 @@
|
|||
<script lang="ts">
|
||||
import { Paperclip } from '@lucide/svelte';
|
||||
import { Paperclip, Plus } from '@lucide/svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { FILE_TYPE_ICONS } from '$lib/constants/icons';
|
||||
import { FileTypeCategory } from '$lib/enums';
|
||||
import McpLogo from '$lib/components/app/misc/McpLogo.svelte';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
disabled?: boolean;
|
||||
hasAudioModality?: boolean;
|
||||
hasVisionModality?: boolean;
|
||||
showMcpOption?: boolean;
|
||||
onFileUpload?: (fileType?: FileTypeCategory) => void;
|
||||
onMcpClick?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -19,14 +22,12 @@
|
|||
disabled = false,
|
||||
hasAudioModality = false,
|
||||
hasVisionModality = false,
|
||||
onFileUpload
|
||||
showMcpOption = false,
|
||||
onFileUpload,
|
||||
onMcpClick
|
||||
}: Props = $props();
|
||||
|
||||
const fileUploadTooltipText = $derived.by(() => {
|
||||
return !hasVisionModality
|
||||
? 'Text files and PDFs supported. Images, audio, and video require vision models.'
|
||||
: 'Attach files';
|
||||
});
|
||||
const fileUploadTooltipText = 'Add files or MCP servers';
|
||||
|
||||
function handleFileUpload(fileType?: FileTypeCategory) {
|
||||
onFileUpload?.(fileType);
|
||||
|
|
@ -43,9 +44,9 @@
|
|||
{disabled}
|
||||
type="button"
|
||||
>
|
||||
<span class="sr-only">Attach files</span>
|
||||
<span class="sr-only">{fileUploadTooltipText}</span>
|
||||
|
||||
<Paperclip class="h-4 w-4" />
|
||||
<Plus class="h-4 w-4" />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
|
||||
|
|
@ -123,6 +124,18 @@
|
|||
</Tooltip.Content>
|
||||
{/if}
|
||||
</Tooltip.Root>
|
||||
|
||||
{#if showMcpOption}
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
class="flex cursor-pointer items-center gap-2"
|
||||
onclick={() => onMcpClick?.()}
|
||||
>
|
||||
<McpLogo style="width: 1rem; height: 1rem;" />
|
||||
|
||||
<span>MCP Servers</span>
|
||||
</DropdownMenu.Item>
|
||||
{/if}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,33 @@
|
|||
<script lang="ts">
|
||||
import { Square } from '@lucide/svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { Square, Settings, ChevronDown } from '@lucide/svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { Switch } from '$lib/components/ui/switch';
|
||||
import { cn } from '$lib/components/ui/utils';
|
||||
import {
|
||||
ChatFormActionFileAttachments,
|
||||
ChatFormActionRecord,
|
||||
ChatFormActionSubmit,
|
||||
DialogMcpServersSettings,
|
||||
ModelsSelector
|
||||
} from '$lib/components/app';
|
||||
import McpLogo from '$lib/components/app/misc/McpLogo.svelte';
|
||||
import { FileTypeCategory } from '$lib/enums';
|
||||
import { getFileTypeCategory } from '$lib/utils';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { modelsStore, modelOptions, selectedModelId } from '$lib/stores/models.svelte';
|
||||
import { isRouterMode } from '$lib/stores/server.svelte';
|
||||
import { chatStore } from '$lib/stores/chat.svelte';
|
||||
import { activeMessages, usedModalities } from '$lib/stores/conversations.svelte';
|
||||
import { useModelChangeValidation } from '$lib/hooks/use-model-change-validation.svelte';
|
||||
import { parseMcpServerSettings } from '$lib/config/mcp';
|
||||
import type { MCPServerSettingsEntry } from '$lib/types/mcp';
|
||||
import {
|
||||
mcpGetHealthCheckState,
|
||||
mcpHasHealthCheck,
|
||||
mcpRunHealthCheck
|
||||
} from '$lib/stores/mcp.svelte';
|
||||
|
||||
interface Props {
|
||||
canSend?: boolean;
|
||||
|
|
@ -161,16 +174,173 @@
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
let showMcpDialog = $state(false);
|
||||
|
||||
// MCP servers state
|
||||
let mcpServers = $derived<MCPServerSettingsEntry[]>(
|
||||
parseMcpServerSettings(currentConfig.mcpServers)
|
||||
);
|
||||
let enabledMcpServers = $derived(mcpServers.filter((s) => s.enabled && s.url.trim()));
|
||||
// Filter out servers with health check errors
|
||||
let healthyEnabledMcpServers = $derived(
|
||||
enabledMcpServers.filter((s) => {
|
||||
const healthState = mcpGetHealthCheckState(s.id);
|
||||
return healthState.status !== 'error';
|
||||
})
|
||||
);
|
||||
let hasEnabledMcpServers = $derived(enabledMcpServers.length > 0);
|
||||
let hasMcpServers = $derived(mcpServers.length > 0);
|
||||
|
||||
// Count of extra servers beyond the 3 shown as favicons (excluding error servers)
|
||||
let extraServersCount = $derived(Math.max(0, healthyEnabledMcpServers.length - 3));
|
||||
|
||||
// Toggle server enabled state
|
||||
function toggleServer(serverId: string, enabled: boolean) {
|
||||
const servers = mcpServers.map((s) => (s.id === serverId ? { ...s, enabled } : s));
|
||||
settingsStore.updateConfig('mcpServers', JSON.stringify(servers));
|
||||
}
|
||||
|
||||
// Get display name for server
|
||||
function getServerDisplayName(server: MCPServerSettingsEntry): string {
|
||||
if (server.name) return server.name;
|
||||
try {
|
||||
const url = new URL(server.url);
|
||||
const host = url.hostname.replace(/^(www\.|mcp\.)/, '');
|
||||
const name = host.split('.')[0] || 'Unknown';
|
||||
return name.charAt(0).toUpperCase() + name.slice(1);
|
||||
} catch {
|
||||
return 'New Server';
|
||||
}
|
||||
}
|
||||
|
||||
// Get favicon URLs for enabled servers (max 3)
|
||||
function getFaviconUrl(server: MCPServerSettingsEntry): string | null {
|
||||
try {
|
||||
const url = new URL(server.url);
|
||||
const hostnameParts = url.hostname.split('.');
|
||||
const rootDomain =
|
||||
hostnameParts.length >= 2 ? hostnameParts.slice(-2).join('.') : url.hostname;
|
||||
return `https://www.google.com/s2/favicons?domain=${rootDomain}&sz=32`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
let mcpFavicons = $derived(
|
||||
healthyEnabledMcpServers
|
||||
.slice(0, 3)
|
||||
.map((s) => ({ id: s.id, url: getFaviconUrl(s) }))
|
||||
.filter((f) => f.url !== null)
|
||||
);
|
||||
|
||||
// Run health checks on mount if there are enabled servers
|
||||
onMount(() => {
|
||||
if (hasEnabledMcpServers) {
|
||||
for (const server of enabledMcpServers) {
|
||||
if (!mcpHasHealthCheck(server.id)) {
|
||||
mcpRunHealthCheck(server);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex w-full items-center gap-3 {className}" style="container-type: inline-size">
|
||||
<ChatFormActionFileAttachments
|
||||
class="mr-auto"
|
||||
{disabled}
|
||||
{hasAudioModality}
|
||||
{hasVisionModality}
|
||||
{onFileUpload}
|
||||
/>
|
||||
<div class="mr-auto flex items-center gap-1.5">
|
||||
<ChatFormActionFileAttachments
|
||||
{disabled}
|
||||
{hasAudioModality}
|
||||
{hasVisionModality}
|
||||
showMcpOption={!hasMcpServers}
|
||||
onMcpClick={() => (showMcpDialog = true)}
|
||||
{onFileUpload}
|
||||
/>
|
||||
|
||||
{#if hasMcpServers}
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger {disabled}>
|
||||
<button
|
||||
type="button"
|
||||
class={cn(
|
||||
'inline-flex cursor-pointer items-center rounded-sm bg-muted-foreground/10 px-1.5 py-1 text-xs transition hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60',
|
||||
hasEnabledMcpServers ? 'text-foreground' : 'text-muted-foreground'
|
||||
)}
|
||||
{disabled}
|
||||
aria-label="MCP Servers"
|
||||
>
|
||||
<McpLogo style="width: 0.875rem; height: 0.875rem;" />
|
||||
|
||||
<span class="mx-1.5 font-medium"> MCP </span>
|
||||
|
||||
{#if hasEnabledMcpServers && mcpFavicons.length > 0}
|
||||
<div class="flex -space-x-1">
|
||||
{#each mcpFavicons as favicon (favicon.id)}
|
||||
<img
|
||||
src={favicon.url}
|
||||
alt=""
|
||||
class="h-3.5 w-3.5 rounded-sm"
|
||||
onerror={(e) => {
|
||||
(e.currentTarget as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if hasEnabledMcpServers && extraServersCount > 0}
|
||||
<span class="ml-1 text-muted-foreground">+{extraServersCount}</span>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<ChevronDown class="h-3 w-3.5" />
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content align="start" class="w-64">
|
||||
<div class="max-h-64 overflow-y-auto">
|
||||
{#each mcpServers as server (server.id)}
|
||||
{@const healthState = mcpGetHealthCheckState(server.id)}
|
||||
{@const hasError = healthState.status === 'error'}
|
||||
<div class="flex items-center justify-between gap-2 px-2 py-1.5">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2">
|
||||
{#if getFaviconUrl(server)}
|
||||
<img
|
||||
src={getFaviconUrl(server)}
|
||||
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">{getServerDisplayName(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={server.enabled}
|
||||
onCheckedChange={(checked) => toggleServer(server.id, checked)}
|
||||
disabled={hasError}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
class="flex cursor-pointer items-center gap-2"
|
||||
onclick={() => (showMcpDialog = true)}
|
||||
>
|
||||
<Settings class="h-4 w-4" />
|
||||
<span>Manage MCP Servers</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<ModelsSelector
|
||||
{disabled}
|
||||
|
|
@ -202,3 +372,8 @@
|
|||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<DialogMcpServersSettings
|
||||
bind:open={showMcpDialog}
|
||||
onOpenChange={(open) => (showMcpDialog = open)}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Reference in New Issue