]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/blob
5c2a945d317113c230d23fef16f5b2ff321439d8
[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 { mcpResourceStore } from '$lib/stores/mcp-resources.svelte';
5 import { KeyboardKey } from '$lib/enums';
6 import type { MCPResourceInfo, MCPServerSettingsEntry } from '$lib/types';
7 import { SvelteMap } from 'svelte/reactivity';
8 import { FolderOpen } from '@lucide/svelte';
9 import { Button } from '$lib/components/ui/button';
10 import {
11 ChatFormPickerPopover,
12 ChatFormPickerList,
13 ChatFormPickerListItem,
14 ChatFormPickerItemHeader,
15 ChatFormPickerListItemSkeleton
16 } from '$lib/components/app/chat';
17
18 interface Props {
19 class?: string;
20 isOpen?: boolean;
21 searchQuery?: string;
22 onClose?: () => void;
23 onResourceSelect?: (resource: MCPResourceInfo) => void;
24 onBrowse?: () => void;
25 }
26
27 let {
28 class: className = '',
29 isOpen = false,
30 searchQuery = '',
31 onClose,
32 onResourceSelect,
33 onBrowse
34 }: Props = $props();
35
36 let resources = $state<MCPResourceInfo[]>([]);
37 let isLoading = $state(false);
38 let selectedIndex = $state(0);
39 let internalSearchQuery = $state('');
40
41 let serverSettingsMap = $derived.by(() => {
42 const servers = mcpStore.getServers();
43 const map = new SvelteMap<string, MCPServerSettingsEntry>();
44
45 for (const server of servers) {
46 map.set(server.id, server);
47 }
48
49 return map;
50 });
51
52 $effect(() => {
53 if (isOpen) {
54 loadResources();
55 selectedIndex = 0;
56 }
57 });
58
59 $effect(() => {
60 if (filteredResources.length > 0 && selectedIndex >= filteredResources.length) {
61 selectedIndex = 0;
62 }
63 });
64
65 async function loadResources() {
66 isLoading = true;
67
68 try {
69 const perChatOverrides = conversationsStore.getAllMcpServerOverrides();
70
71 const initialized = await mcpStore.ensureInitialized(perChatOverrides);
72
73 if (!initialized) {
74 resources = [];
75
76 return;
77 }
78
79 await mcpStore.fetchAllResources();
80 resources = mcpResourceStore.getAllResourceInfos();
81 } catch (error) {
82 console.error('[ChatFormResourcePicker] Failed to load resources:', error);
83 resources = [];
84 } finally {
85 isLoading = false;
86 }
87 }
88
89 function handleResourceClick(resource: MCPResourceInfo) {
90 mcpStore.attachResource(resource.uri);
91 onResourceSelect?.(resource);
92 onClose?.();
93 }
94
95 function isResourceAttached(uri: string): boolean {
96 return mcpResourceStore.isAttached(uri);
97 }
98
99 export function handleKeydown(event: KeyboardEvent): boolean {
100 if (!isOpen) return false;
101
102 if (event.key === KeyboardKey.ESCAPE) {
103 event.preventDefault();
104 onClose?.();
105
106 return true;
107 }
108
109 if (event.key === KeyboardKey.ARROW_DOWN) {
110 event.preventDefault();
111
112 if (filteredResources.length > 0) {
113 selectedIndex = (selectedIndex + 1) % filteredResources.length;
114 }
115
116 return true;
117 }
118
119 if (event.key === KeyboardKey.ARROW_UP) {
120 event.preventDefault();
121 if (filteredResources.length > 0) {
122 selectedIndex = selectedIndex === 0 ? filteredResources.length - 1 : selectedIndex - 1;
123 }
124
125 return true;
126 }
127
128 if (event.key === KeyboardKey.ENTER) {
129 event.preventDefault();
130 if (filteredResources[selectedIndex]) {
131 handleResourceClick(filteredResources[selectedIndex]);
132 }
133
134 return true;
135 }
136
137 return false;
138 }
139
140 let filteredResources = $derived.by(() => {
141 const sortedServers = mcpStore.getServersSorted();
142 const serverOrderMap = new Map(sortedServers.map((server, index) => [server.id, index]));
143
144 const sortedResources = [...resources].sort((a, b) => {
145 const orderA = serverOrderMap.get(a.serverName) ?? Number.MAX_SAFE_INTEGER;
146 const orderB = serverOrderMap.get(b.serverName) ?? Number.MAX_SAFE_INTEGER;
147 return orderA - orderB;
148 });
149
150 const query = (searchQuery || internalSearchQuery).toLowerCase();
151 if (!query) return sortedResources;
152
153 return sortedResources.filter(
154 (resource) =>
155 resource.name.toLowerCase().includes(query) ||
156 resource.title?.toLowerCase().includes(query) ||
157 resource.description?.toLowerCase().includes(query) ||
158 resource.uri.toLowerCase().includes(query)
159 );
160 });
161
162 let showSearchInput = $derived(resources.length > 3);
163 </script>
164
165 <ChatFormPickerPopover
166 bind:isOpen
167 class={className}
168 srLabel="Open resource picker"
169 {onClose}
170 onKeydown={handleKeydown}
171 >
172 <ChatFormPickerList
173 items={filteredResources}
174 {isLoading}
175 {selectedIndex}
176 bind:searchQuery={internalSearchQuery}
177 {showSearchInput}
178 searchPlaceholder="Search resources..."
179 emptyMessage="No MCP resources available"
180 itemKey={(resource) => resource.serverName + ':' + resource.uri}
181 >
182 {#snippet item(resource, index, isSelected)}
183 {@const server = serverSettingsMap.get(resource.serverName)}
184 {@const serverLabel = server ? mcpStore.getServerLabel(server) : resource.serverName}
185
186 <ChatFormPickerListItem
187 dataIndex={index}
188 {isSelected}
189 onClick={() => handleResourceClick(resource)}
190 >
191 <ChatFormPickerItemHeader
192 {server}
193 {serverLabel}
194 title={resource.title || resource.name}
195 description={resource.description}
196 >
197 {#snippet titleExtra()}
198 {#if isResourceAttached(resource.uri)}
199 <span
200 class="inline-flex items-center rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium text-primary"
201 >
202 attached
203 </span>
204 {/if}
205 {/snippet}
206
207 {#snippet subtitle()}
208 <p class="mt-0.5 truncate text-xs text-muted-foreground/60">
209 {resource.uri}
210 </p>
211 {/snippet}
212 </ChatFormPickerItemHeader>
213 </ChatFormPickerListItem>
214 {/snippet}
215
216 {#snippet skeleton()}
217 <ChatFormPickerListItemSkeleton />
218 {/snippet}
219
220 {#snippet footer()}
221 {#if onBrowse && resources.length > 3}
222 <Button
223 class="fixed right-3 bottom-3"
224 type="button"
225 onclick={onBrowse}
226 variant="secondary"
227 size="sm"
228 >
229 <FolderOpen class="h-3 w-3" />
230
231 Browse all
232 </Button>
233 {/if}
234 {/snippet}
235 </ChatFormPickerList>
236 </ChatFormPickerPopover>