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