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