]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/blob
81b55513d32d2e915a94b34e8f1588f9e40f87e9
[pkg/ggml/sources/llama.cpp] /
1 <script lang="ts">
2 import { page } from '$app/state';
3 import { Plus, MessageSquare, Settings, Zap, FolderOpen } from '@lucide/svelte';
4 import { Button } from '$lib/components/ui/button';
5 import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
6 import * as Tooltip from '$lib/components/ui/tooltip';
7 import { Switch } from '$lib/components/ui/switch';
8 import { FILE_TYPE_ICONS, TOOLTIP_DELAY_DURATION } from '$lib/constants';
9 import { McpLogo, DropdownMenuSearchable } from '$lib/components/app';
10 import { conversationsStore } from '$lib/stores/conversations.svelte';
11 import { mcpStore } from '$lib/stores/mcp.svelte';
12
13 import { HealthCheckStatus } from '$lib/enums';
14 import type { MCPServerSettingsEntry } from '$lib/types';
15
16 interface Props {
17 class?: string;
18 disabled?: boolean;
19 hasAudioModality?: boolean;
20 hasVisionModality?: boolean;
21 hasMcpPromptsSupport?: boolean;
22 hasMcpResourcesSupport?: boolean;
23 onFileUpload?: () => void;
24 onSystemPromptClick?: () => void;
25 onMcpPromptClick?: () => void;
26 onMcpSettingsClick?: () => void;
27 onMcpResourcesClick?: () => void;
28 }
29
30 let {
31 class: className = '',
32 disabled = false,
33 hasAudioModality = false,
34 hasVisionModality = false,
35 hasMcpPromptsSupport = false,
36 hasMcpResourcesSupport = false,
37 onFileUpload,
38 onSystemPromptClick,
39 onMcpPromptClick,
40 onMcpSettingsClick,
41 onMcpResourcesClick
42 }: Props = $props();
43
44 let isNewChat = $derived(!page.params.id);
45
46 let systemMessageTooltip = $derived(
47 isNewChat
48 ? 'Add custom system message for a new conversation'
49 : 'Inject custom system message at the beginning of the conversation'
50 );
51
52 let dropdownOpen = $state(false);
53
54 let mcpServers = $derived(mcpStore.getServersSorted().filter((s) => s.enabled));
55 let hasMcpServers = $derived(mcpServers.length > 0);
56 let mcpSearchQuery = $state('');
57 let filteredMcpServers = $derived.by(() => {
58 const query = mcpSearchQuery.toLowerCase().trim();
59 if (!query) return mcpServers;
60 return mcpServers.filter((s) => {
61 const name = getServerLabel(s).toLowerCase();
62 const url = s.url.toLowerCase();
63 return name.includes(query) || url.includes(query);
64 });
65 });
66
67 function getServerLabel(server: MCPServerSettingsEntry): string {
68 return mcpStore.getServerLabel(server);
69 }
70
71 function isServerEnabledForChat(serverId: string): boolean {
72 return conversationsStore.isMcpServerEnabledForChat(serverId);
73 }
74
75 async function toggleServerForChat(serverId: string) {
76 await conversationsStore.toggleMcpServerForChat(serverId);
77 }
78
79 function handleMcpSubMenuOpen(open: boolean) {
80 if (open) {
81 mcpSearchQuery = '';
82 mcpStore.runHealthChecksForServers(mcpServers);
83 }
84 }
85
86 function handleMcpPromptClick() {
87 dropdownOpen = false;
88 onMcpPromptClick?.();
89 }
90
91 function handleMcpSettingsClick() {
92 dropdownOpen = false;
93 onMcpSettingsClick?.();
94 }
95
96 function handleMcpResourcesClick() {
97 dropdownOpen = false;
98 onMcpResourcesClick?.();
99 }
100
101 const fileUploadTooltipText = 'Add files, system prompt or MCP Servers';
102 </script>
103
104 <div class="flex items-center gap-1 {className}">
105 <DropdownMenu.Root bind:open={dropdownOpen}>
106 <DropdownMenu.Trigger name="Attach files" {disabled}>
107 <Tooltip.Root>
108 <Tooltip.Trigger class="w-full">
109 <Button
110 class="file-upload-button h-8 w-8 rounded-full p-0"
111 {disabled}
112 variant="secondary"
113 type="button"
114 >
115 <span class="sr-only">{fileUploadTooltipText}</span>
116
117 <Plus class="h-4 w-4" />
118 </Button>
119 </Tooltip.Trigger>
120
121 <Tooltip.Content>
122 <p>{fileUploadTooltipText}</p>
123 </Tooltip.Content>
124 </Tooltip.Root>
125 </DropdownMenu.Trigger>
126
127 <DropdownMenu.Content align="start" class="w-48">
128 {#if hasVisionModality}
129 <DropdownMenu.Item
130 class="images-button flex cursor-pointer items-center gap-2"
131 onclick={() => onFileUpload?.()}
132 >
133 <FILE_TYPE_ICONS.image class="h-4 w-4" />
134
135 <span>Images</span>
136 </DropdownMenu.Item>
137 {:else}
138 <Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
139 <Tooltip.Trigger class="w-full">
140 <DropdownMenu.Item
141 class="images-button flex cursor-pointer items-center gap-2"
142 disabled
143 >
144 <FILE_TYPE_ICONS.image class="h-4 w-4" />
145
146 <span>Images</span>
147 </DropdownMenu.Item>
148 </Tooltip.Trigger>
149
150 <Tooltip.Content side="right">
151 <p>Images require vision models to be processed</p>
152 </Tooltip.Content>
153 </Tooltip.Root>
154 {/if}
155
156 {#if hasAudioModality}
157 <DropdownMenu.Item
158 class="audio-button flex cursor-pointer items-center gap-2"
159 onclick={() => onFileUpload?.()}
160 >
161 <FILE_TYPE_ICONS.audio class="h-4 w-4" />
162
163 <span>Audio Files</span>
164 </DropdownMenu.Item>
165 {:else}
166 <Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
167 <Tooltip.Trigger class="w-full">
168 <DropdownMenu.Item class="audio-button flex cursor-pointer items-center gap-2" disabled>
169 <FILE_TYPE_ICONS.audio class="h-4 w-4" />
170
171 <span>Audio Files</span>
172 </DropdownMenu.Item>
173 </Tooltip.Trigger>
174
175 <Tooltip.Content side="right">
176 <p>Audio files require audio models to be processed</p>
177 </Tooltip.Content>
178 </Tooltip.Root>
179 {/if}
180
181 <DropdownMenu.Item
182 class="flex cursor-pointer items-center gap-2"
183 onclick={() => onFileUpload?.()}
184 >
185 <FILE_TYPE_ICONS.text class="h-4 w-4" />
186
187 <span>Text Files</span>
188 </DropdownMenu.Item>
189
190 {#if hasVisionModality}
191 <DropdownMenu.Item
192 class="flex cursor-pointer items-center gap-2"
193 onclick={() => onFileUpload?.()}
194 >
195 <FILE_TYPE_ICONS.pdf class="h-4 w-4" />
196
197 <span>PDF Files</span>
198 </DropdownMenu.Item>
199 {:else}
200 <Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
201 <Tooltip.Trigger class="w-full">
202 <DropdownMenu.Item
203 class="flex cursor-pointer items-center gap-2"
204 onclick={() => onFileUpload?.()}
205 >
206 <FILE_TYPE_ICONS.pdf class="h-4 w-4" />
207
208 <span>PDF Files</span>
209 </DropdownMenu.Item>
210 </Tooltip.Trigger>
211
212 <Tooltip.Content side="right">
213 <p>PDFs will be converted to text. Image-based PDFs may not work properly.</p>
214 </Tooltip.Content>
215 </Tooltip.Root>
216 {/if}
217
218 <Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
219 <Tooltip.Trigger class="w-full">
220 <DropdownMenu.Item
221 class="flex cursor-pointer items-center gap-2"
222 onclick={() => onSystemPromptClick?.()}
223 >
224 <MessageSquare class="h-4 w-4" />
225
226 <span>System Message</span>
227 </DropdownMenu.Item>
228 </Tooltip.Trigger>
229
230 <Tooltip.Content side="right">
231 <p>{systemMessageTooltip}</p>
232 </Tooltip.Content>
233 </Tooltip.Root>
234
235 <DropdownMenu.Separator />
236
237 <DropdownMenu.Sub onOpenChange={handleMcpSubMenuOpen}>
238 <DropdownMenu.SubTrigger class="flex cursor-pointer items-center gap-2">
239 <McpLogo class="h-4 w-4" />
240
241 <span>MCP Servers</span>
242 </DropdownMenu.SubTrigger>
243
244 <DropdownMenu.SubContent class="w-72 pt-0">
245 <DropdownMenuSearchable
246 placeholder="Search servers..."
247 bind:searchValue={mcpSearchQuery}
248 emptyMessage={hasMcpServers ? 'No servers found' : 'No MCP servers configured'}
249 isEmpty={filteredMcpServers.length === 0}
250 >
251 <div class="max-h-64 overflow-y-auto">
252 {#each filteredMcpServers as server (server.id)}
253 {@const healthState = mcpStore.getHealthCheckState(server.id)}
254 {@const hasError = healthState.status === HealthCheckStatus.ERROR}
255 {@const isEnabledForChat = isServerEnabledForChat(server.id)}
256
257 <button
258 type="button"
259 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"
260 onclick={() => !hasError && toggleServerForChat(server.id)}
261 disabled={hasError}
262 >
263 <div class="flex min-w-0 flex-1 items-center gap-2">
264 {#if mcpStore.getServerFavicon(server.id)}
265 <img
266 src={mcpStore.getServerFavicon(server.id)}
267 alt=""
268 class="h-4 w-4 shrink-0 rounded-sm"
269 onerror={(e) => {
270 (e.currentTarget as HTMLImageElement).style.display = 'none';
271 }}
272 />
273 {/if}
274
275 <span class="truncate text-sm">{getServerLabel(server)}</span>
276
277 {#if hasError}
278 <span
279 class="shrink-0 rounded bg-destructive/15 px-1.5 py-0.5 text-xs text-destructive"
280 >
281 Error
282 </span>
283 {/if}
284 </div>
285
286 <Switch
287 checked={isEnabledForChat}
288 disabled={hasError}
289 onclick={(e: MouseEvent) => e.stopPropagation()}
290 onCheckedChange={() => toggleServerForChat(server.id)}
291 />
292 </button>
293 {/each}
294 </div>
295
296 {#snippet footer()}
297 <DropdownMenu.Item
298 class="flex cursor-pointer items-center gap-2"
299 onclick={handleMcpSettingsClick}
300 >
301 <Settings class="h-4 w-4" />
302
303 <span>Manage MCP Servers</span>
304 </DropdownMenu.Item>
305 {/snippet}
306 </DropdownMenuSearchable>
307 </DropdownMenu.SubContent>
308 </DropdownMenu.Sub>
309
310 {#if hasMcpPromptsSupport}
311 <DropdownMenu.Item
312 class="flex cursor-pointer items-center gap-2"
313 onclick={handleMcpPromptClick}
314 >
315 <Zap class="h-4 w-4" />
316
317 <span>MCP Prompt</span>
318 </DropdownMenu.Item>
319 {/if}
320
321 {#if hasMcpResourcesSupport}
322 <DropdownMenu.Item
323 class="flex cursor-pointer items-center gap-2"
324 onclick={handleMcpResourcesClick}
325 >
326 <FolderOpen class="h-4 w-4" />
327
328 <span>MCP Resources</span>
329 </DropdownMenu.Item>
330 {/if}
331 </DropdownMenu.Content>
332 </DropdownMenu.Root>
333 </div>