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