]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/blob
852bf3fb66b38f2545c75e9dbd9632b37838ae39
[pkg/ggml/sources/llama.cpp] /
1 <script lang="ts">
2 import { Plus, Settings } from '@lucide/svelte';
3 import { goto } from '$app/navigation';
4 import { DropdownMenuSearchable, McpLogo, McpServerIdentity } from '$lib/components/app';
5 import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
6 import { Switch } from '$lib/components/ui/switch';
7 import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
8 import { ROUTES } from '$lib/constants/routes';
9 import { HealthCheckStatus } from '$lib/enums';
10 import { conversationsStore } from '$lib/stores/conversations.svelte';
11 import { mcpStore } from '$lib/stores/mcp.svelte';
12 import type { MCPServerSettingsEntry } from '$lib/types';
13
14 interface Props {
15 onMcpSettingsClick?: () => void;
16 }
17
18 let { onMcpSettingsClick }: Props = $props();
19
20 let mcpSearchQuery = $state('');
21 // Every configured server is listed; `enabled` is an on/off state,
22 // not a visibility filter, so a disabled server stays toggleable.
23 let mcpServers = $derived(mcpStore.getServers());
24 let hasMcpServers = $derived(mcpServers.length > 0);
25 let filteredMcpServers = $derived.by(() => {
26 const query = mcpSearchQuery.toLowerCase().trim();
27
28 if (!query) return mcpServers;
29
30 return mcpServers.filter((s) => {
31 const name = getServerLabel(s).toLowerCase();
32 const url = s.url.toLowerCase();
33
34 return name.includes(query) || url.includes(query);
35 });
36 });
37
38 function getServerLabel(server: MCPServerSettingsEntry): string {
39 return mcpStore.getServerLabel(server);
40 }
41
42 function isServerEnabledForChat(serverId: string): boolean {
43 return conversationsStore.isMcpServerEnabledForChat(serverId);
44 }
45
46 async function toggleServerForChat(serverId: string) {
47 await conversationsStore.toggleMcpServerForChat(serverId);
48 }
49
50 function handleMcpSubMenuOpen(open: boolean) {
51 if (open) {
52 mcpSearchQuery = '';
53 mcpStore.runHealthChecksForServers(mcpServers);
54 }
55 }
56
57 function handleMcpSettingsClick() {
58 onMcpSettingsClick?.();
59
60 goto(`${hasMcpServers ? '' : '?add'}${ROUTES.MCP_SERVERS}`);
61 }
62 </script>
63
64 <DropdownMenu.Root>
65 <DropdownMenu.Sub onOpenChange={handleMcpSubMenuOpen}>
66 <DropdownMenu.SubTrigger class="flex cursor-pointer items-center gap-2">
67 <McpLogo class={ICON_CLASS_DEFAULT} />
68
69 <span>MCP Servers</span>
70 </DropdownMenu.SubTrigger>
71
72 <DropdownMenu.SubContent class="w-72 pt-0">
73 {#if hasMcpServers}
74 <DropdownMenuSearchable
75 placeholder="Search servers..."
76 bind:searchValue={mcpSearchQuery}
77 emptyMessage="No servers found"
78 isEmpty={filteredMcpServers.length === 0}
79 >
80 <div class="max-h-64 overflow-y-auto">
81 {#each filteredMcpServers as server (server.id)}
82 {@const healthState = mcpStore.getHealthCheckState(server.id)}
83 {@const hasError = healthState.status === HealthCheckStatus.ERROR}
84 {@const isEnabledForChat = isServerEnabledForChat(server.id)}
85 {@const displayName = getServerLabel(server)}
86 {@const faviconUrl = mcpStore.getServerFavicon(server.id)}
87
88 <button
89 type="button"
90 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"
91 onclick={() => !hasError && toggleServerForChat(server.id)}
92 disabled={hasError}
93 >
94 <div class="flex min-w-0 flex-1 items-center gap-2">
95 <div class="min-w-0 flex-1">
96 <McpServerIdentity
97 {displayName}
98 {faviconUrl}
99 iconClass={ICON_CLASS_DEFAULT}
100 iconRounded="rounded-sm"
101 showVersion={false}
102 nameClass="text-sm"
103 />
104 </div>
105
106 {#if hasError}
107 <span
108 class="shrink-0 rounded bg-destructive/15 px-1.5 py-0.5 text-xs text-destructive"
109 >
110 Error
111 </span>
112 {/if}
113 </div>
114
115 <Switch
116 checked={isEnabledForChat}
117 disabled={hasError}
118 onclick={(e) => e.stopPropagation()}
119 onCheckedChange={() => toggleServerForChat(server.id)}
120 />
121 </button>
122 {/each}
123 </div>
124
125 {#snippet footer()}
126 <DropdownMenu.Item
127 class="flex cursor-pointer items-center gap-2"
128 onclick={handleMcpSettingsClick}
129 >
130 <Settings class={ICON_CLASS_DEFAULT} />
131
132 <span>Manage MCP Servers</span>
133 </DropdownMenu.Item>
134 {/snippet}
135 </DropdownMenuSearchable>
136 {:else}
137 <div class="px-2 py-3 text-center text-sm text-muted-foreground">
138 No MCP servers configured
139 </div>
140
141 <DropdownMenu.Separator />
142
143 <DropdownMenu.Item
144 class="flex cursor-pointer items-center gap-2"
145 onclick={handleMcpSettingsClick}
146 >
147 <Plus class={ICON_CLASS_DEFAULT} />
148
149 <span>Add MCP Servers</span>
150 </DropdownMenu.Item>
151 {/if}
152 </DropdownMenu.SubContent>
153 </DropdownMenu.Sub>
154 </DropdownMenu.Root>