]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/blob
f35d816de92928037e7241a3b1d273631d149317
[pkg/ggml/sources/llama.cpp] /
1 <script lang="ts">
2 import { conversationsStore } from '$lib/stores/conversations.svelte';
3 import { mcpStore } from '$lib/stores/mcp.svelte';
4 import { debounce, uuid } from '$lib/utils';
5 import { KeyboardKey } from '$lib/enums';
6 import type { MCPPromptInfo, GetPromptResult, MCPServerSettingsEntry } from '$lib/types';
7 import { SvelteMap } from 'svelte/reactivity';
8 import {
9 ChatFormPickerPopover,
10 ChatFormPickerList,
11 ChatFormPickerListItem,
12 ChatFormPickerItemHeader,
13 ChatFormPickerListItemSkeleton,
14 ChatFormPromptPickerArgumentForm
15 } from '$lib/components/app/chat';
16 import Badge from '$lib/components/ui/badge/badge.svelte';
17
18 interface Props {
19 class?: string;
20 isOpen?: boolean;
21 searchQuery?: string;
22 onClose?: () => void;
23 onPromptLoadStart?: (
24 placeholderId: string,
25 promptInfo: MCPPromptInfo,
26 args?: Record<string, string>
27 ) => void;
28 onPromptLoadComplete?: (placeholderId: string, result: GetPromptResult) => void;
29 onPromptLoadError?: (placeholderId: string, error: string) => void;
30 }
31
32 let {
33 class: className = '',
34 isOpen = false,
35 searchQuery = '',
36 onClose,
37 onPromptLoadStart,
38 onPromptLoadComplete,
39 onPromptLoadError
40 }: Props = $props();
41
42 let prompts = $state<MCPPromptInfo[]>([]);
43 let isLoading = $state(false);
44 let selectedPrompt = $state<MCPPromptInfo | null>(null);
45 let promptArgs = $state<Record<string, string>>({});
46 let selectedIndex = $state(0);
47 let internalSearchQuery = $state('');
48 let promptError = $state<string | null>(null);
49 let selectedIndexBeforeArgumentForm = $state<number | null>(null);
50
51 let suggestions = $state<Record<string, string[]>>({});
52 let loadingSuggestions = $state<Record<string, boolean>>({});
53 let activeAutocomplete = $state<string | null>(null);
54 let autocompleteIndex = $state(0);
55
56 let serverSettingsMap = $derived.by(() => {
57 const servers = mcpStore.getServers();
58 const map = new SvelteMap<string, MCPServerSettingsEntry>();
59
60 for (const server of servers) {
61 map.set(server.id, server);
62 }
63
64 return map;
65 });
66
67 $effect(() => {
68 if (isOpen) {
69 loadPrompts();
70 selectedIndex = 0;
71 } else {
72 selectedPrompt = null;
73 promptArgs = {};
74 promptError = null;
75 }
76 });
77
78 $effect(() => {
79 if (filteredPrompts.length > 0 && selectedIndex >= filteredPrompts.length) {
80 selectedIndex = 0;
81 }
82 });
83
84 async function loadPrompts() {
85 isLoading = true;
86
87 try {
88 const perChatOverrides = conversationsStore.getAllMcpServerOverrides();
89
90 const initialized = await mcpStore.ensureInitialized(perChatOverrides);
91
92 if (!initialized) {
93 prompts = [];
94
95 return;
96 }
97
98 prompts = await mcpStore.getAllPrompts();
99 } catch (error) {
100 console.error('[ChatFormPickerMcpPrompts] Failed to load prompts:', error);
101 prompts = [];
102 } finally {
103 isLoading = false;
104 }
105 }
106
107 function handlePromptClick(prompt: MCPPromptInfo) {
108 const args = prompt.arguments ?? [];
109
110 if (args.length > 0) {
111 selectedIndexBeforeArgumentForm = selectedIndex;
112 selectedPrompt = prompt;
113 promptArgs = {};
114 promptError = null;
115
116 requestAnimationFrame(() => {
117 const firstInput = document.querySelector(`#arg-${args[0].name}`) as HTMLInputElement;
118 if (firstInput) {
119 firstInput.focus();
120 }
121 });
122 } else {
123 executePrompt(prompt, {});
124 }
125 }
126
127 async function executePrompt(prompt: MCPPromptInfo, args: Record<string, string>) {
128 promptError = null;
129
130 const placeholderId = uuid();
131
132 const nonEmptyArgs = Object.fromEntries(
133 Object.entries(args).filter(([, value]) => value.trim() !== '')
134 );
135 const argsToPass = Object.keys(nonEmptyArgs).length > 0 ? nonEmptyArgs : undefined;
136
137 onPromptLoadStart?.(placeholderId, prompt, argsToPass);
138 onClose?.();
139
140 try {
141 const result = await mcpStore.getPrompt(prompt.serverName, prompt.name, args);
142 onPromptLoadComplete?.(placeholderId, result);
143 } catch (error) {
144 const errorMessage =
145 error instanceof Error ? error.message : 'Unknown error executing prompt';
146 onPromptLoadError?.(placeholderId, errorMessage);
147 }
148 }
149
150 function handleArgumentSubmit(event: SubmitEvent) {
151 event.preventDefault();
152
153 if (selectedPrompt) {
154 executePrompt(selectedPrompt, promptArgs);
155 }
156 }
157
158 const fetchCompletions = debounce(async (argName: string, value: string) => {
159 if (!selectedPrompt || value.length < 1) {
160 suggestions[argName] = [];
161
162 return;
163 }
164
165 if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
166 console.log('[ChatFormPickerMcpPrompts] Fetching completions for:', {
167 serverName: selectedPrompt.serverName,
168 promptName: selectedPrompt.name,
169 argName,
170 value
171 });
172 }
173
174 loadingSuggestions[argName] = true;
175
176 try {
177 const result = await mcpStore.getPromptCompletions(
178 selectedPrompt.serverName,
179 selectedPrompt.name,
180 argName,
181 value
182 );
183
184 if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
185 console.log('[ChatFormPickerMcpPrompts] Autocomplete result:', {
186 argName,
187 value,
188 result,
189 suggestionsCount: result?.values.length ?? 0
190 });
191 }
192
193 if (result && result.values.length > 0) {
194 // Filter out empty strings from suggestions
195 const filteredValues = result.values.filter((v) => v.trim() !== '');
196
197 if (filteredValues.length > 0) {
198 suggestions[argName] = filteredValues;
199 activeAutocomplete = argName;
200 autocompleteIndex = 0;
201 } else {
202 suggestions[argName] = [];
203 }
204 } else {
205 suggestions[argName] = [];
206 }
207 } catch (error) {
208 console.error('[ChatFormPickerMcpPrompts] Failed to fetch completions:', error);
209 suggestions[argName] = [];
210 } finally {
211 loadingSuggestions[argName] = false;
212 }
213 }, 200);
214
215 function handleArgInput(argName: string, value: string) {
216 promptArgs[argName] = value;
217 fetchCompletions(argName, value);
218 }
219
220 function selectSuggestion(argName: string, value: string) {
221 promptArgs[argName] = value;
222 suggestions[argName] = [];
223 activeAutocomplete = null;
224 }
225
226 function handleArgKeydown(event: KeyboardEvent, argName: string) {
227 const argSuggestions = suggestions[argName] ?? [];
228
229 // Handle Escape - return to prompt selection list
230 if (event.key === KeyboardKey.ESCAPE) {
231 event.preventDefault();
232 event.stopPropagation();
233 handleCancelArgumentForm();
234 return;
235 }
236
237 if (argSuggestions.length === 0 || activeAutocomplete !== argName) return;
238
239 if (event.key === KeyboardKey.ARROW_DOWN) {
240 event.preventDefault();
241 autocompleteIndex = Math.min(autocompleteIndex + 1, argSuggestions.length - 1);
242 } else if (event.key === KeyboardKey.ARROW_UP) {
243 event.preventDefault();
244 autocompleteIndex = Math.max(autocompleteIndex - 1, 0);
245 } else if (event.key === KeyboardKey.ENTER && argSuggestions[autocompleteIndex]) {
246 event.preventDefault();
247 event.stopPropagation();
248 selectSuggestion(argName, argSuggestions[autocompleteIndex]);
249 }
250 }
251
252 function handleArgBlur(argName: string) {
253 // Delay to allow click on suggestion
254 setTimeout(() => {
255 if (activeAutocomplete === argName) {
256 suggestions[argName] = [];
257 activeAutocomplete = null;
258 }
259 }, 150);
260 }
261
262 function handleArgFocus(argName: string) {
263 if ((suggestions[argName]?.length ?? 0) > 0) {
264 activeAutocomplete = argName;
265 }
266 }
267
268 function handleCancelArgumentForm() {
269 // Restore the previously selected prompt index
270 if (selectedIndexBeforeArgumentForm !== null) {
271 selectedIndex = selectedIndexBeforeArgumentForm;
272 selectedIndexBeforeArgumentForm = null;
273 }
274 selectedPrompt = null;
275 promptArgs = {};
276 promptError = null;
277 }
278
279 export function handleKeydown(event: KeyboardEvent): boolean {
280 if (!isOpen) return false;
281
282 if (event.key === KeyboardKey.ESCAPE) {
283 event.preventDefault();
284 if (selectedPrompt) {
285 // Return to prompt selection list, keeping the selected prompt active
286 handleCancelArgumentForm();
287 } else {
288 onClose?.();
289 }
290
291 return true;
292 }
293
294 if (event.key === KeyboardKey.ARROW_DOWN) {
295 event.preventDefault();
296 if (filteredPrompts.length > 0) {
297 selectedIndex = (selectedIndex + 1) % filteredPrompts.length;
298 }
299
300 return true;
301 }
302
303 if (event.key === KeyboardKey.ARROW_UP) {
304 event.preventDefault();
305 if (filteredPrompts.length > 0) {
306 selectedIndex = selectedIndex === 0 ? filteredPrompts.length - 1 : selectedIndex - 1;
307 }
308
309 return true;
310 }
311
312 if (event.key === KeyboardKey.ENTER && !selectedPrompt) {
313 event.preventDefault();
314 if (filteredPrompts[selectedIndex]) {
315 handlePromptClick(filteredPrompts[selectedIndex]);
316 }
317
318 return true;
319 }
320
321 return false;
322 }
323
324 let filteredPrompts = $derived.by(() => {
325 const sortedServers = mcpStore.getServers();
326 const serverOrderMap = new Map(sortedServers.map((server, index) => [server.id, index]));
327
328 const sortedPrompts = [...prompts].sort((a, b) => {
329 const orderA = serverOrderMap.get(a.serverName) ?? Number.MAX_SAFE_INTEGER;
330 const orderB = serverOrderMap.get(b.serverName) ?? Number.MAX_SAFE_INTEGER;
331 return orderA - orderB;
332 });
333
334 const query = (searchQuery || internalSearchQuery).toLowerCase();
335 if (!query) return sortedPrompts;
336
337 return sortedPrompts.filter(
338 (prompt) =>
339 prompt.name.toLowerCase().includes(query) ||
340 prompt.title?.toLowerCase().includes(query) ||
341 prompt.description?.toLowerCase().includes(query)
342 );
343 });
344
345 let showSearchInput = $derived(prompts.length > 3);
346 </script>
347
348 <ChatFormPickerPopover
349 bind:isOpen
350 class={className}
351 srLabel="Open prompt picker"
352 {onClose}
353 onKeydown={handleKeydown}
354 >
355 {#if selectedPrompt}
356 {@const prompt = selectedPrompt}
357 {@const server = serverSettingsMap.get(prompt.serverName)}
358 {@const serverLabel = server ? mcpStore.getServerLabel(server) : prompt.serverName}
359
360 <div class="p-4">
361 <ChatFormPickerItemHeader
362 {server}
363 {serverLabel}
364 title={prompt.title || prompt.name}
365 description={prompt.description}
366 >
367 {#snippet titleExtra()}
368 {#if prompt.arguments?.length}
369 <Badge variant="secondary">
370 {prompt.arguments.length} arg{prompt.arguments.length > 1 ? 's' : ''}
371 </Badge>
372 {/if}
373 {/snippet}
374 </ChatFormPickerItemHeader>
375
376 <ChatFormPromptPickerArgumentForm
377 prompt={selectedPrompt}
378 {promptArgs}
379 {suggestions}
380 {loadingSuggestions}
381 {activeAutocomplete}
382 {autocompleteIndex}
383 {promptError}
384 onArgInput={handleArgInput}
385 onArgKeydown={handleArgKeydown}
386 onArgBlur={handleArgBlur}
387 onArgFocus={handleArgFocus}
388 onSelectSuggestion={selectSuggestion}
389 onSubmit={handleArgumentSubmit}
390 onCancel={handleCancelArgumentForm}
391 />
392 </div>
393 {:else}
394 <ChatFormPickerList
395 items={filteredPrompts}
396 {isLoading}
397 {selectedIndex}
398 bind:searchQuery={internalSearchQuery}
399 {showSearchInput}
400 searchPlaceholder="Search prompts..."
401 emptyMessage="No MCP prompts available"
402 itemKey={(prompt) => prompt.serverName + ':' + prompt.name}
403 >
404 {#snippet item(prompt, index, isSelected)}
405 {@const server = serverSettingsMap.get(prompt.serverName)}
406 {@const serverLabel = server ? mcpStore.getServerLabel(server) : prompt.serverName}
407
408 <ChatFormPickerListItem
409 dataIndex={index}
410 {isSelected}
411 onclick={() => handlePromptClick(prompt)}
412 >
413 <ChatFormPickerItemHeader
414 {server}
415 {serverLabel}
416 title={prompt.title || prompt.name}
417 description={prompt.description}
418 >
419 {#snippet titleExtra()}
420 {#if prompt.arguments?.length}
421 <Badge variant="secondary">
422 {prompt.arguments.length} arg{prompt.arguments.length > 1 ? 's' : ''}
423 </Badge>
424 {/if}
425 {/snippet}
426 </ChatFormPickerItemHeader>
427 </ChatFormPickerListItem>
428 {/snippet}
429
430 {#snippet skeleton()}
431 <ChatFormPickerListItemSkeleton titleWidth="w-32" showBadge />
432 {/snippet}
433 </ChatFormPickerList>
434 {/if}
435 </ChatFormPickerPopover>