]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/blob
3d5dec3b6ac956e8cb3387d85178699088b07389
[pkg/ggml/sources/llama.cpp] /
1 <script lang="ts">
2 import { Card } from '$lib/components/ui/card';
3 import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
4 import { mcpStore } from '$lib/stores/mcp.svelte';
5 import { SvelteMap } from 'svelte/reactivity';
6 import { McpPromptVariant } from '$lib/enums';
7 import { TruncatedText } from '$lib/components/app/misc';
8 import * as Tooltip from '$lib/components/ui/tooltip';
9
10 interface ContentPart {
11 text: string;
12 argKey: string | null;
13 }
14
15 interface Props {
16 class?: string;
17 prompt: DatabaseMessageExtraMcpPrompt;
18 variant?: McpPromptVariant;
19 isLoading?: boolean;
20 loadError?: string;
21 }
22
23 let {
24 class: className = '',
25 prompt,
26 variant = McpPromptVariant.MESSAGE,
27 isLoading = false,
28 loadError
29 }: Props = $props();
30
31 let hoveredArgKey = $state<string | null>(null);
32 let argumentEntries = $derived(Object.entries(prompt.arguments ?? {}));
33 let hasArguments = $derived(prompt.arguments && Object.keys(prompt.arguments).length > 0);
34 let hasContent = $derived(prompt.content && prompt.content.trim().length > 0);
35
36 let contentParts = $derived.by((): ContentPart[] => {
37 if (!prompt.content || !hasArguments) {
38 return [{ text: prompt.content || '', argKey: null }];
39 }
40
41 const parts: ContentPart[] = [];
42 let remaining = prompt.content;
43
44 const valueToKey = new SvelteMap<string, string>();
45 for (const [key, value] of argumentEntries) {
46 if (value && value.trim()) {
47 valueToKey.set(value, key);
48 }
49 }
50
51 const sortedValues = [...valueToKey.keys()].sort((a, b) => b.length - a.length);
52
53 while (remaining.length > 0) {
54 let earliestMatch: { index: number; value: string; key: string } | null = null;
55
56 for (const value of sortedValues) {
57 const index = remaining.indexOf(value);
58 if (index !== -1 && (earliestMatch === null || index < earliestMatch.index)) {
59 earliestMatch = { index, value, key: valueToKey.get(value)! };
60 }
61 }
62
63 if (earliestMatch) {
64 if (earliestMatch.index > 0) {
65 parts.push({ text: remaining.slice(0, earliestMatch.index), argKey: null });
66 }
67
68 parts.push({ text: earliestMatch.value, argKey: earliestMatch.key });
69 remaining = remaining.slice(earliestMatch.index + earliestMatch.value.length);
70 } else {
71 parts.push({ text: remaining, argKey: null });
72
73 break;
74 }
75 }
76
77 return parts;
78 });
79
80 let showArgBadges = $derived(hasArguments && !isLoading && !loadError);
81 let isAttachment = $derived(variant === McpPromptVariant.ATTACHMENT);
82 let textSizeClass = $derived(isAttachment ? 'text-xs' : 'text-md');
83 let paddingClass = $derived(isAttachment ? 'px-3 py-2' : 'px-3.75 py-2.5');
84 let maxHeightStyle = $derived(
85 isAttachment ? 'max-height: 6rem;' : 'max-height: var(--max-message-height);'
86 );
87
88 const serverFavicon = $derived(mcpStore.getServerFavicon(prompt.serverName));
89 const serverDisplayName = $derived(mcpStore.getServerDisplayName(prompt.serverName));
90 </script>
91
92 <div class="flex flex-col gap-2 {className}">
93 <div class="flex items-center justify-between gap-2">
94 <div class="inline-flex flex-wrap items-center gap-1.25 text-xs text-muted-foreground">
95 <Tooltip.Root>
96 <Tooltip.Trigger>
97 {#if serverFavicon}
98 <img
99 src={serverFavicon}
100 alt=""
101 class="h-3.5 w-3.5 shrink-0 rounded-sm"
102 onerror={(e) => {
103 (e.currentTarget as HTMLImageElement).style.display = 'none';
104 }}
105 />
106 {/if}
107 </Tooltip.Trigger>
108
109 <Tooltip.Content>
110 <span>{serverDisplayName}</span>
111 </Tooltip.Content>
112 </Tooltip.Root>
113
114 <TruncatedText text={prompt.name} />
115 </div>
116
117 {#if showArgBadges}
118 <div class="flex flex-wrap justify-end gap-1">
119 {#each argumentEntries as [key, value] (key)}
120 <Tooltip.Root>
121 <Tooltip.Trigger>
122 <!-- svelte-ignore a11y_no_static_element_interactions -->
123 <span
124 class="rounded-sm bg-purple-200/60 px-1.5 py-0.5 text-[10px] leading-none text-purple-700 transition-opacity dark:bg-purple-800/40 dark:text-purple-300 {hoveredArgKey &&
125 hoveredArgKey !== key
126 ? 'opacity-30'
127 : ''}"
128 onmouseenter={() => (hoveredArgKey = key)}
129 onmouseleave={() => (hoveredArgKey = null)}
130 >
131 {key}
132 </span>
133 </Tooltip.Trigger>
134
135 <Tooltip.Content>
136 <span class="max-w-xs break-all">{value}</span>
137 </Tooltip.Content>
138 </Tooltip.Root>
139 {/each}
140 </div>
141 {/if}
142 </div>
143
144 {#if loadError}
145 <Card
146 class="relative overflow-hidden rounded-[1.125rem] border border-destructive/50 bg-destructive/10 backdrop-blur-md"
147 >
148 <div
149 class="overflow-y-auto {paddingClass}"
150 style="{maxHeightStyle} overflow-wrap: anywhere; word-break: break-word;"
151 >
152 <span class="{textSizeClass} text-destructive">{loadError}</span>
153 </div>
154 </Card>
155 {:else if isLoading}
156 <Card
157 class="relative overflow-hidden rounded-[1.125rem] border border-purple-200 bg-purple-500/10 px-1 py-2 backdrop-blur-md dark:border-purple-800 dark:bg-purple-500/20"
158 >
159 <div
160 class="overflow-y-auto {paddingClass}"
161 style="{maxHeightStyle} overflow-wrap: anywhere; word-break: break-word;"
162 >
163 <div class="space-y-2">
164 <div class="h-3 w-3/4 animate-pulse rounded bg-foreground/20"></div>
165
166 <div class="h-3 w-full animate-pulse rounded bg-foreground/20"></div>
167
168 <div class="h-3 w-5/6 animate-pulse rounded bg-foreground/20"></div>
169 </div>
170 </div>
171 </Card>
172 {:else if hasContent}
173 <Card
174 class="relative overflow-hidden rounded-[1.125rem] border border-purple-200 bg-purple-500/10 py-0 text-foreground backdrop-blur-md dark:border-purple-800 dark:bg-purple-500/20"
175 >
176 <div
177 class="overflow-y-auto {paddingClass}"
178 style="{maxHeightStyle} overflow-wrap: anywhere; word-break: break-word;"
179 >
180 <span class="{textSizeClass} whitespace-pre-wrap">
181 <!-- This formatting is needed to keep the text in proper shape -->
182 <!-- svelte-ignore a11y_no_static_element_interactions -->
183 {#each contentParts as part, i (i)}{#if part.argKey}<span
184 class="rounded-sm bg-purple-300/50 px-0.5 text-purple-900 transition-opacity dark:bg-purple-700/50 dark:text-purple-100 {hoveredArgKey &&
185 hoveredArgKey !== part.argKey
186 ? 'opacity-30'
187 : ''}"
188 onmouseenter={() => (hoveredArgKey = part.argKey)}
189 onmouseleave={() => (hoveredArgKey = null)}>{part.text}</span
190 >{:else}<span class="transition-opacity {hoveredArgKey ? 'opacity-30' : ''}"
191 >{part.text}</span
192 >{/if}{/each}</span
193 >
194 </div>
195 </Card>
196 {/if}
197 </div>