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