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