]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/blob
ad42f84698409e128793b7d9406f0aea0a5cf806
[pkg/ggml/sources/llama.cpp] /
1 <script lang="ts">
2 import { FolderOpen, ChevronDown, ChevronRight, Loader2, Braces } from '@lucide/svelte';
3 import { Checkbox } from '$lib/components/ui/checkbox';
4 import * as Collapsible from '$lib/components/ui/collapsible';
5 import { mcpStore } from '$lib/stores/mcp.svelte';
6 import type { MCPResourceInfo, MCPResourceTemplateInfo, MCPServerResources } from '$lib/types';
7 import { SvelteSet } from 'svelte/reactivity';
8 import {
9 type ResourceTreeNode,
10 buildResourceTree,
11 countTreeResources,
12 sortTreeChildren
13 } from './mcp-resources-browser';
14 import { getDisplayName, getResourceIcon } from '$lib/utils';
15
16 interface Props {
17 serverName: string;
18 serverRes: MCPServerResources;
19 isExpanded: boolean;
20 selectedUris: Set<string>;
21 selectedTemplateUri?: string | null;
22 expandedFolders: SvelteSet<string>;
23 onToggleServer: () => void;
24 onToggleFolder: (folderId: string) => void;
25 onSelect?: (resource: MCPResourceInfo, shiftKey?: boolean) => void;
26 onToggle?: (resource: MCPResourceInfo, checked: boolean) => void;
27 onTemplateSelect?: (template: MCPResourceTemplateInfo) => void;
28 searchQuery?: string;
29 }
30
31 let {
32 serverName,
33 serverRes,
34 isExpanded,
35 selectedUris,
36 selectedTemplateUri,
37 expandedFolders,
38 onToggleServer,
39 onToggleFolder,
40 onSelect,
41 onToggle,
42 onTemplateSelect,
43 searchQuery = ''
44 }: Props = $props();
45
46 const hasResources = $derived(serverRes.resources.length > 0);
47 const hasTemplates = $derived(serverRes.templates.length > 0);
48 const hasContent = $derived(hasResources || hasTemplates);
49 const displayName = $derived(mcpStore.getServerDisplayName(serverName));
50 const favicon = $derived(mcpStore.getServerFavicon(serverName));
51 const resourceTree = $derived(buildResourceTree(serverRes.resources, serverName, searchQuery));
52
53 const templateInfos = $derived<MCPResourceTemplateInfo[]>(
54 serverRes.templates.map((t) => ({
55 uriTemplate: t.uriTemplate,
56 name: t.name,
57 title: t.title,
58 description: t.description,
59 mimeType: t.mimeType,
60 serverName,
61 annotations: t.annotations,
62 icons: t.icons
63 }))
64 );
65
66 function handleResourceClick(resource: MCPResourceInfo, event: MouseEvent) {
67 onSelect?.(resource, event.shiftKey);
68 }
69
70 function handleCheckboxChange(resource: MCPResourceInfo, checked: boolean) {
71 onToggle?.(resource, checked);
72 }
73
74 function isResourceSelected(resource: MCPResourceInfo): boolean {
75 return selectedUris.has(resource.uri);
76 }
77 </script>
78
79 {#snippet renderTreeNode(node: ResourceTreeNode, depth: number, parentPath: string)}
80 {@const isFolder = !node.resource && node.children.size > 0}
81 {@const folderId = `${serverName}:${parentPath}/${node.name}`}
82 {@const isFolderExpanded = expandedFolders.has(folderId)}
83
84 {#if isFolder}
85 {@const folderCount = countTreeResources(node)}
86 <Collapsible.Root open={isFolderExpanded} onOpenChange={() => onToggleFolder(folderId)}>
87 <Collapsible.Trigger
88 class="flex w-full items-center gap-2 rounded px-2 py-1 text-sm hover:bg-muted/50"
89 >
90 {#if isFolderExpanded}
91 <ChevronDown class="h-3 w-3" />
92 {:else}
93 <ChevronRight class="h-3 w-3" />
94 {/if}
95
96 <FolderOpen class="h-3.5 w-3.5 text-muted-foreground" />
97
98 <span class="font-medium">{node.name}</span>
99
100 <span class="text-xs text-muted-foreground">({folderCount})</span>
101 </Collapsible.Trigger>
102
103 <Collapsible.Content>
104 <div class="ml-4 flex flex-col gap-0.5 border-l border-border/50 pl-2">
105 {#each sortTreeChildren( [...node.children.values()] ) as child (child.resource?.uri || `${serverName}:${parentPath}/${node.name}/${child.name}`)}
106 {@render renderTreeNode(child, depth + 1, `${parentPath}/${node.name}`)}
107 {/each}
108 </div>
109 </Collapsible.Content>
110 </Collapsible.Root>
111 {:else if node.resource}
112 {@const resource = node.resource}
113 {@const ResourceIcon = getResourceIcon(resource.mimeType, resource.uri)}
114 {@const isSelected = isResourceSelected(resource)}
115 {@const resourceDisplayName = resource.title || getDisplayName(node.name)}
116
117 <div class="group flex w-full items-center gap-2">
118 {#if onToggle}
119 <Checkbox
120 checked={isSelected}
121 onCheckedChange={(checked: boolean | 'indeterminate') =>
122 handleCheckboxChange(resource, checked === true)}
123 class="h-4 w-4"
124 />
125 {/if}
126
127 <button
128 class={[
129 'flex flex-1 items-center gap-2 rounded px-2 py-1 text-left text-sm transition-colors',
130 'hover:bg-muted/50',
131 isSelected && 'bg-muted'
132 ]}
133 onclick={(e: MouseEvent) => handleResourceClick(resource, e)}
134 title={resourceDisplayName}
135 >
136 <ResourceIcon class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
137
138 <span class="min-w-0 flex-1 truncate text-left">
139 {resourceDisplayName}
140 </span>
141 </button>
142 </div>
143 {/if}
144 {/snippet}
145
146 <Collapsible.Root open={isExpanded} onOpenChange={onToggleServer}>
147 <Collapsible.Trigger
148 class="flex w-full items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-muted/50"
149 >
150 {#if isExpanded}
151 <ChevronDown class="h-3.5 w-3.5" />
152 {:else}
153 <ChevronRight class="h-3.5 w-3.5" />
154 {/if}
155
156 <span class="inline-flex flex-col items-start text-left">
157 <span class="inline-flex items-center justify-start gap-1.5 font-medium">
158 {#if favicon}
159 <img
160 src={favicon}
161 alt=""
162 class="h-4 w-4 shrink-0 rounded-sm"
163 onerror={(e) => {
164 (e.currentTarget as HTMLImageElement).style.display = 'none';
165 }}
166 />
167 {/if}
168
169 {displayName}
170 </span>
171
172 <span class="text-xs text-muted-foreground">
173 ({serverRes.resources.length} resource{serverRes.resources.length !== 1
174 ? 's'
175 : ''}{#if hasTemplates}, {serverRes.templates.length} template{serverRes.templates
176 .length !== 1
177 ? 's'
178 : ''}{/if})
179 </span>
180 </span>
181
182 {#if serverRes.loading}
183 <Loader2 class="ml-auto h-3 w-3 animate-spin text-muted-foreground" />
184 {/if}
185 </Collapsible.Trigger>
186
187 <Collapsible.Content>
188 <div class="ml-4 flex flex-col gap-0.5 border-l border-border/50 pl-2">
189 {#if serverRes.error}
190 <div class="py-1 text-xs text-red-500">
191 Error: {serverRes.error}
192 </div>
193 {:else if !hasContent}
194 <div class="py-1 text-xs text-muted-foreground">No resources</div>
195 {:else}
196 {#if hasResources}
197 {#each sortTreeChildren( [...resourceTree.children.values()] ) as child (child.resource?.uri || `${serverName}:${child.name}`)}
198 {@render renderTreeNode(child, 1, '')}
199 {/each}
200 {/if}
201
202 {#if hasTemplates && onTemplateSelect}
203 {#if hasResources}
204 <div class="my-1 border-t border-border/30"></div>
205 {/if}
206
207 <div
208 class="py-0.5 text-[11px] font-medium tracking-wide text-muted-foreground/70 uppercase"
209 >
210 Templates
211 </div>
212
213 {#each templateInfos as template (template.uriTemplate)}
214 <button
215 class={[
216 'flex w-full items-center gap-2 rounded px-2 py-1 text-left text-sm transition-colors',
217 'hover:bg-muted/50',
218 selectedTemplateUri === template.uriTemplate && 'bg-muted'
219 ]}
220 onclick={() => onTemplateSelect(template)}
221 title={template.uriTemplate}
222 >
223 <Braces class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
224
225 <span class="min-w-0 flex-1 truncate text-left">
226 {template.title || template.name}
227 </span>
228 </button>
229 {/each}
230 {/if}
231 {/if}
232 </div>
233 </Collapsible.Content>
234 </Collapsible.Root>