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