]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/blob
021e0e453947798c9aedc8a21d05745822a5c5da
[pkg/ggml/sources/llama.cpp] /
1 <script lang="ts">
2 import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
3 import type { Snippet } from 'svelte';
4 import * as Tooltip from '$lib/components/ui/tooltip';
5 import * as Sheet from '$lib/components/ui/sheet';
6 import * as Collapsible from '$lib/components/ui/collapsible';
7 import { File, MessageSquare, Zap, FolderOpen } from '@lucide/svelte';
8 import { Switch } from '$lib/components/ui/switch';
9 import { Checkbox } from '$lib/components/ui/checkbox';
10 import { TOOLTIP_DELAY_DURATION } from '$lib/constants';
11 import { ATTACHMENT_FILE_ITEMS } from '$lib/constants/attachment-menu';
12 import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
13 import { useToolsPanel } from '$lib/hooks/use-tools-panel.svelte';
14 import { useReasoningMenu } from '$lib/hooks/use-reasoning-menu.svelte';
15 import { conversationsStore } from '$lib/stores/conversations.svelte';
16 import { mcpStore } from '$lib/stores/mcp.svelte';
17 import { McpLogo } from '$lib/components/app';
18 import {
19 PencilRuler,
20 ChevronDown,
21 ChevronRight,
22 Lightbulb,
23 LightbulbOff,
24 Check
25 } from '@lucide/svelte';
26 import { HealthCheckStatus } from '$lib/enums';
27 import { AttachmentAction } from '$lib/enums/attachment.enums';
28
29 interface Props {
30 class?: string;
31 disabled?: boolean;
32 hasAudioModality?: boolean;
33 hasVideoModality?: boolean;
34 hasVisionModality?: boolean;
35 hasMcpPromptsSupport?: boolean;
36 hasMcpResourcesSupport?: boolean;
37 onFileUpload?: () => void;
38 onSystemPromptClick?: () => void;
39 onMcpPromptClick?: () => void;
40 onMcpResourcesClick?: () => void;
41 trigger: Snippet<[{ disabled: boolean; onclick?: () => void }]>;
42 }
43
44 let {
45 class: className = '',
46 disabled = false,
47 hasAudioModality = false,
48 hasVisionModality = false,
49 hasVideoModality = false,
50 hasMcpPromptsSupport = false,
51 hasMcpResourcesSupport = false,
52 onFileUpload,
53 onSystemPromptClick,
54 onMcpPromptClick,
55 onMcpResourcesClick,
56 trigger
57 }: Props = $props();
58
59 let sheetOpen = $state(false);
60 let reasoningExpanded = $state(false);
61 let filesExpanded = $state(true);
62 let toolsExpanded = $state(false);
63 let mcpExpanded = $state(false);
64
65 const attachmentMenu = useAttachmentMenu(
66 () => ({
67 hasVisionModality,
68 hasAudioModality,
69 hasVideoModality,
70 hasMcpPromptsSupport,
71 hasMcpResourcesSupport
72 }),
73 () => ({ onFileUpload, onSystemPromptClick, onMcpPromptClick, onMcpResourcesClick }),
74 () => {
75 sheetOpen = false;
76 }
77 );
78
79 const toolsPanel = useToolsPanel();
80 const reasoning = useReasoningMenu();
81
82 const sheetItemClass =
83 'flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-left text-sm transition-colors hover:bg-accent active:bg-accent disabled:cursor-not-allowed disabled:opacity-50';
84
85 const sheetItemRowClass =
86 'flex w-full items-center justify-between gap-2 rounded-md px-3 py-2 text-left text-sm transition-colors hover:bg-accent';
87
88 let mcpServers = $derived(mcpStore.getServers());
89 </script>
90
91 <div class="flex items-center gap-1 {className}">
92 <Sheet.Root bind:open={sheetOpen}>
93 {@render trigger({ disabled, onclick: () => (sheetOpen = true) })}
94
95 <Sheet.Content side="bottom" class="max-h-[85vh] gap-0 overflow-y-auto">
96 <Sheet.Header>
97 <Sheet.Title>Add to chat</Sheet.Title>
98
99 <Sheet.Description class="sr-only">
100 Add files, system prompt or configure MCP servers
101 </Sheet.Description>
102 </Sheet.Header>
103
104 <div class="flex flex-col gap-1 px-1.5 pb-2">
105 {#if reasoning.modelSupportsThinking}
106 <Collapsible.Root
107 open={reasoningExpanded}
108 onOpenChange={(open) => (reasoningExpanded = open)}
109 >
110 <Collapsible.Trigger class={sheetItemClass}>
111 {#if reasoningExpanded}
112 <ChevronDown class="{ICON_CLASS_DEFAULT} shrink-0" />
113 {:else}
114 <ChevronRight class="{ICON_CLASS_DEFAULT} shrink-0" />
115 {/if}
116
117 {#if reasoning.thinkingEnabled}
118 <Lightbulb class="{ICON_CLASS_DEFAULT} shrink-0 text-amber-400" />
119 {:else if reasoning.isOff}
120 <LightbulbOff class="{ICON_CLASS_DEFAULT} shrink-0 text-muted-foreground" />
121 {:else}
122 <Lightbulb class="{ICON_CLASS_DEFAULT} shrink-0 text-muted-foreground" />
123 {/if}
124
125 <span class="flex-1">Reasoning</span>
126
127 <span class="text-xs capitalize text-muted-foreground">
128 {reasoning.currentEffort}
129 </span>
130 </Collapsible.Trigger>
131
132 <Collapsible.Content>
133 <div class="flex flex-col gap-0.5 pl-4">
134 {#each reasoning.levels as level (level.value)}
135 {@const tokenLabel = reasoning.tokenLabel(level)}
136 <button
137 type="button"
138 class={sheetItemRowClass}
139 class:bg-accent={reasoning.isSelected(level)}
140 onclick={() => reasoning.select(level)}
141 >
142 <div class="flex min-w-0 items-center gap-3">
143 {#if reasoning.isSelected(level)}
144 <Check class="{ICON_CLASS_DEFAULT} shrink-0 text-foreground" />
145 {:else}
146 <div class="{ICON_CLASS_DEFAULT} shrink-0"></div>
147 {/if}
148
149 <span class="text-sm">{level.label}</span>
150 </div>
151
152 {#if tokenLabel}
153 <span class="shrink-0 text-[11px] text-muted-foreground opacity-60">
154 {tokenLabel}
155 </span>
156 {/if}
157 </button>
158 {/each}
159 </div>
160 </Collapsible.Content>
161 </Collapsible.Root>
162 {/if}
163
164 <Collapsible.Root open={filesExpanded} onOpenChange={(open) => (filesExpanded = open)}>
165 <Collapsible.Trigger class={sheetItemClass}>
166 {#if filesExpanded}
167 <ChevronDown class="{ICON_CLASS_DEFAULT} shrink-0" />
168 {:else}
169 <ChevronRight class="{ICON_CLASS_DEFAULT} shrink-0" />
170 {/if}
171
172 <File class="{ICON_CLASS_DEFAULT} shrink-0" />
173
174 <span class="flex-1">Add files</span>
175 </Collapsible.Trigger>
176
177 <Collapsible.Content>
178 <div class="flex flex-col gap-0.5 pl-4">
179 {#each ATTACHMENT_FILE_ITEMS as item (item.id)}
180 {@const enabled = attachmentMenu.isItemEnabled(item.enabledWhen)}
181 {#if enabled}
182 <button
183 type="button"
184 class={sheetItemClass}
185 onclick={() => attachmentMenu.callbacks[item.action]()}
186 >
187 <item.icon class="{ICON_CLASS_DEFAULT} shrink-0" />
188
189 <span>{item.label}</span>
190 </button>
191 {:else if item.disabledTooltip}
192 <Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
193 <Tooltip.Trigger>
194 <button type="button" class={sheetItemClass} disabled>
195 <item.icon class="{ICON_CLASS_DEFAULT} shrink-0" />
196
197 <span>{item.label}</span>
198 </button>
199 </Tooltip.Trigger>
200
201 <Tooltip.Content side="right">
202 <p>{item.disabledTooltip}</p>
203 </Tooltip.Content>
204 </Tooltip.Root>
205 {/if}
206 {/each}
207 </div>
208 </Collapsible.Content>
209 </Collapsible.Root>
210
211 <Collapsible.Root open={mcpExpanded} onOpenChange={(open) => (mcpExpanded = open)}>
212 <Collapsible.Trigger class={sheetItemClass}>
213 {#if mcpExpanded}
214 <ChevronDown class="{ICON_CLASS_DEFAULT} shrink-0" />
215 {:else}
216 <ChevronRight class="{ICON_CLASS_DEFAULT} shrink-0" />
217 {/if}
218
219 <McpLogo class="inline {ICON_CLASS_DEFAULT} shrink-0" />
220
221 <span class="flex-1">MCP Servers</span>
222
223 <span class="text-xs text-muted-foreground">
224 {mcpServers.length} server{mcpServers.length !== 1 ? 's' : ''}
225 </span>
226 </Collapsible.Trigger>
227
228 <Collapsible.Content>
229 <div class="flex flex-col gap-0.5 pl-4">
230 {#each mcpServers as server (server.id)}
231 {@const healthState = mcpStore.getHealthCheckState(server.id)}
232 {@const hasError = healthState.status === HealthCheckStatus.ERROR}
233 {@const displayName = mcpStore.getServerLabel(server)}
234 {@const faviconUrl = mcpStore.getServerFavicon(server.id)}
235 {@const isEnabled = conversationsStore.isMcpServerEnabledForChat(server.id)}
236
237 <button
238 type="button"
239 class={sheetItemRowClass}
240 onclick={() => !hasError && conversationsStore.toggleMcpServerForChat(server.id)}
241 disabled={hasError}
242 >
243 <div class="flex min-w-0 flex-1 items-center gap-2">
244 {#if faviconUrl}
245 <img
246 src={faviconUrl}
247 alt=""
248 class="{ICON_CLASS_DEFAULT} shrink-0 rounded-sm"
249 onerror={(e) => {
250 (e.currentTarget as HTMLImageElement).style.display = 'none';
251 }}
252 />
253 {/if}
254
255 <span class="min-w-0 truncate text-sm">{displayName}</span>
256 </div>
257
258 {#if hasError}
259 <span
260 class="shrink-0 rounded bg-destructive/15 px-1.5 py-0.5 text-xs text-destructive"
261 >
262 Error
263 </span>
264 {:else}
265 <Switch
266 checked={isEnabled}
267 onCheckedChange={() => conversationsStore.toggleMcpServerForChat(server.id)}
268 />
269 {/if}
270 </button>
271 {/each}
272
273 {#if mcpServers.length === 0}
274 <div class="px-3 py-2 text-center text-sm text-muted-foreground">
275 No MCP servers configured
276 </div>
277 {/if}
278 </div>
279 </Collapsible.Content>
280 </Collapsible.Root>
281
282 {#if toolsPanel.totalToolCount > 0}
283 <Collapsible.Root open={toolsExpanded} onOpenChange={(open) => (toolsExpanded = open)}>
284 <Collapsible.Trigger class={sheetItemClass}>
285 {#if toolsExpanded}
286 <ChevronDown class="{ICON_CLASS_DEFAULT} shrink-0" />
287 {:else}
288 <ChevronRight class="{ICON_CLASS_DEFAULT} shrink-0" />
289 {/if}
290
291 <PencilRuler class="inline {ICON_CLASS_DEFAULT} shrink-0" />
292
293 <span class="flex-1">Tools</span>
294
295 <span class="text-xs text-muted-foreground">
296 {toolsPanel.totalToolCount} tool{toolsPanel.totalToolCount !== 1 ? 's' : ''}
297 </span>
298 </Collapsible.Trigger>
299
300 <Collapsible.Content>
301 <div class="flex flex-col gap-0.5 pl-4">
302 {#each toolsPanel.activeGroups as group (group.label)}
303 {@const checked = toolsPanel.isGroupChecked(group)}
304 {@const enabledCount = toolsPanel.getEnabledToolCount(group)}
305 {@const favicon = toolsPanel.getFavicon(group)}
306
307 <button
308 type="button"
309 class={sheetItemRowClass}
310 onclick={() => toolsPanel.toggleGroupByLabel(group.label)}
311 >
312 {#if favicon}
313 <img
314 src={favicon}
315 alt=""
316 class="{ICON_CLASS_DEFAULT} shrink-0 rounded-sm"
317 onerror={(e) => {
318 (e.currentTarget as HTMLImageElement).style.display = 'none';
319 }}
320 />
321 {/if}
322
323 <span class="min-w-0 flex-1 truncate text-sm font-medium">{group.label}</span>
324
325 <span class="shrink-0 text-xs text-muted-foreground">
326 {enabledCount}/{group.tools.length}
327 </span>
328
329 <Checkbox
330 {checked}
331 class="{ICON_CLASS_DEFAULT} shrink-0"
332 onclick={(e) => e.stopPropagation()}
333 onCheckedChange={() => toolsPanel.toggleGroupByLabel(group.label)}
334 />
335 </button>
336 {/each}
337 </div>
338 </Collapsible.Content>
339 </Collapsible.Root>
340 {/if}
341
342 <button
343 type="button"
344 class={sheetItemClass}
345 onclick={() => attachmentMenu.callbacks[AttachmentAction.SYSTEM_PROMPT_CLICK]()}
346 >
347 <MessageSquare class="{ICON_CLASS_DEFAULT} shrink-0" />
348
349 <span>System Message</span>
350 </button>
351
352 {#if hasMcpPromptsSupport}
353 <button
354 type="button"
355 class={sheetItemClass}
356 onclick={() => attachmentMenu.callbacks[AttachmentAction.MCP_PROMPT_CLICK]()}
357 >
358 <Zap class="{ICON_CLASS_DEFAULT} shrink-0" />
359
360 <span>MCP Prompt</span>
361 </button>
362 {/if}
363
364 {#if hasMcpResourcesSupport}
365 <button
366 type="button"
367 class={sheetItemClass}
368 onclick={() => attachmentMenu.callbacks[AttachmentAction.MCP_RESOURCES_CLICK]()}
369 >
370 <FolderOpen class="{ICON_CLASS_DEFAULT} shrink-0" />
371
372 <span>MCP Resources</span>
373 </button>
374 {/if}
375 </div>
376 </Sheet.Content>
377 </Sheet.Root>
378 </div>