]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/blob
1125ae8ec987d71a833cdb1f1459ebb4fddb689a
[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 const initialized = await mcpStore.ensureInitialized(perChatOverrides);
71
72 if (!initialized) {
73 resources = [];
74
75 return;
76 }
77
78 await mcpStore.fetchAllResources();
79 resources = mcpResourceStore.getAllResourceInfos();
80 } catch (error) {
81 console.error('[ChatFormPickerMcpResources] Failed to load resources:', error);
82 resources = [];
83 } finally {
84 isLoading = false;
85 }
86 }
87
88 function handleResourceClick(resource: MCPResourceInfo) {
89 mcpStore.attachResource(resource.uri);
90
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
148 return orderA - orderB;
149 });
150
151 const query = (searchQuery || internalSearchQuery).toLowerCase();
152 if (!query) return sortedResources;
153
154 return sortedResources.filter(
155 (resource) =>
156 resource.name.toLowerCase().includes(query) ||
157 resource.title?.toLowerCase().includes(query) ||
158 resource.description?.toLowerCase().includes(query) ||
159 resource.uri.toLowerCase().includes(query)
160 );
161 });
162
163 let showSearchInput = $derived(resources.length > 3);
164 </script>
165
166 <ChatFormPickerPopover
167 bind:isOpen
168 class={className}
169 srLabel="Open resource picker"
170 {onClose}
171 onKeydown={handleKeydown}
172 >
173 <ChatFormPickerList
174 items={filteredResources}
175 {isLoading}
176 {selectedIndex}
177 bind:searchQuery={internalSearchQuery}
178 {showSearchInput}
179 searchPlaceholder="Search resources..."
180 emptyMessage="No MCP resources available"
181 itemKey={(resource) => resource.serverName + ':' + resource.uri}
182 >
183 {#snippet item(resource, index, isSelected)}
184 {@const server = serverSettingsMap.get(resource.serverName)}
185 {@const serverLabel = server ? mcpStore.getServerLabel(server) : resource.serverName}
186
187 <ChatFormPickerListItem
188 dataIndex={index}
189 {isSelected}
190 onclick={() => handleResourceClick(resource)}
191 >
192 <ChatFormPickerItemHeader
193 {server}
194 {serverLabel}
195 title={resource.title || resource.name}
196 description={resource.description}
197 >
198 {#snippet titleExtra()}
199 {#if isResourceAttached(resource.uri)}
200 <span
201 class="inline-flex items-center rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium text-primary"
202 >
203 attached
204 </span>
205 {/if}
206 {/snippet}
207
208 {#snippet subtitle()}
209 <p class="mt-0.5 truncate text-xs text-muted-foreground/60">
210 {resource.uri}
211 </p>
212 {/snippet}
213 </ChatFormPickerItemHeader>
214 </ChatFormPickerListItem>
215 {/snippet}
216
217 {#snippet skeleton()}
218 <ChatFormPickerListItemSkeleton />
219 {/snippet}
220
221 {#snippet footer()}
222 {#if onBrowse && resources.length > 3}
223 <Button
224 class="fixed right-3 bottom-3"
225 type="button"
226 onclick={onBrowse}
227 variant="secondary"
228 size="sm"
229 >
230 <FolderOpen class="h-3 w-3" />
231
232 Browse all
233 </Button>
234 {/if}
235 {/snippet}
236 </ChatFormPickerList>
237 </ChatFormPickerPopover>