]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/blob
30605ba9f1e44d141231a4deffaeb821dba5455d
[pkg/ggml/sources/llama.cpp] /
1 <script lang="ts">
2 import { Check, X } from '@lucide/svelte';
3 import { ChatMessageActionIcons, MarkdownContent } from '$lib/components/app';
4 import { Button } from '$lib/components/ui/button';
5 import { Card } from '$lib/components/ui/card';
6 import { INPUT_CLASSES } from '$lib/constants';
7 import { getMessageEditContext } from '$lib/contexts';
8 import { KeyboardKey, MessageRole } from '$lib/enums';
9 import { config } from '$lib/stores/settings.svelte';
10 import { autoResizeTextarea, isIMEComposing } from '$lib/utils';
11
12 interface Props {
13 class?: string;
14 message: DatabaseMessage;
15 siblingInfo?: ChatMessageSiblingInfo | null;
16 showDeleteDialog: boolean;
17 deletionInfo: {
18 totalCount: number;
19 userMessages: number;
20 assistantMessages: number;
21 messageTypes: string[];
22 } | null;
23 onCopy: () => void;
24 onEdit: () => void;
25 onDelete: () => void;
26 onConfirmDelete: () => void;
27 onNavigateToSibling?: (siblingId: string) => void;
28 onShowDeleteDialogChange: (show: boolean) => void;
29 textareaElement?: HTMLTextAreaElement;
30 }
31
32 let {
33 class: className = '',
34 deletionInfo,
35 message,
36 onConfirmDelete,
37 onCopy,
38 onDelete,
39 onEdit,
40 onNavigateToSibling,
41 onShowDeleteDialogChange,
42 showDeleteDialog,
43 siblingInfo = null,
44 textareaElement = $bindable()
45 }: Props = $props();
46
47 const editCtx = getMessageEditContext();
48
49 function handleEditKeydown(event: KeyboardEvent) {
50 if (event.key === KeyboardKey.ENTER && !event.shiftKey && !isIMEComposing(event)) {
51 event.preventDefault();
52
53 editCtx.save();
54 } else if (event.key === KeyboardKey.ESCAPE) {
55 event.preventDefault();
56
57 editCtx.cancel();
58 }
59 }
60
61 let isMultiline = $state(false);
62 let messageElement: HTMLElement | undefined = $state();
63 let isExpanded = $state(false);
64 let contentHeight = $state(0);
65
66 const MAX_HEIGHT = 200; // pixels
67 const currentConfig = config();
68
69 let showExpandButton = $derived(contentHeight > MAX_HEIGHT);
70
71 $effect(() => {
72 if (!messageElement || !message.content.trim()) return;
73
74 if (message.content.includes('\n')) {
75 isMultiline = true;
76 }
77
78 const resizeObserver = new ResizeObserver((entries) => {
79 for (const entry of entries) {
80 const element = entry.target as HTMLElement;
81 const estimatedSingleLineHeight = 24;
82
83 isMultiline = element.offsetHeight > estimatedSingleLineHeight * 1.5;
84 contentHeight = element.scrollHeight;
85 }
86 });
87
88 resizeObserver.observe(messageElement);
89
90 return () => {
91 resizeObserver.disconnect();
92 };
93 });
94 $effect(() => {
95 if (editCtx.isEditing && textareaElement) {
96 autoResizeTextarea(textareaElement);
97 }
98 });
99
100 function toggleExpand() {
101 isExpanded = !isExpanded;
102 }
103 </script>
104
105 <div
106 aria-label="System message with actions"
107 class="group flex flex-col items-end gap-3 md:gap-2 {className}"
108 role="group"
109 >
110 {#if editCtx.isEditing}
111 <div class="w-full max-w-[80%]">
112 <textarea
113 style="max-height: var(--max-message-height);"
114 bind:this={textareaElement}
115 value={editCtx.editedContent}
116 class="min-h-[60px] w-full resize-none rounded-2xl px-3 py-2 text-sm {INPUT_CLASSES}"
117 onkeydown={handleEditKeydown}
118 oninput={(e) => {
119 autoResizeTextarea(e.currentTarget);
120 editCtx.setContent(e.currentTarget.value);
121 }}
122 placeholder="Edit system message..."
123 ></textarea>
124
125 <div class="mt-2 flex justify-end gap-2">
126 <Button class="h-8 px-3" onclick={editCtx.cancel} size="sm" variant="outline">
127 <X class="mr-1 h-3 w-3" />
128
129 Cancel
130 </Button>
131
132 <Button
133 class="h-8 px-3"
134 onclick={editCtx.save}
135 disabled={!editCtx.editedContent.trim()}
136 size="sm"
137 >
138 <Check class="mr-1 h-3 w-3" />
139
140 Save
141 </Button>
142 </div>
143 </div>
144 {:else}
145 {#if message.content.trim()}
146 <div class="relative max-w-[80%]">
147 <button
148 class="group/expand w-full text-left {!isExpanded && showExpandButton
149 ? 'cursor-pointer'
150 : 'cursor-auto'}"
151 onclick={showExpandButton && !isExpanded ? toggleExpand : undefined}
152 type="button"
153 >
154 <Card
155 class="overflow-y-auto rounded-[1.125rem] !border-2 !border-dashed !border-border/50 bg-muted px-3.75 py-1.5 data-[multiline]:py-2.5"
156 data-multiline={isMultiline ? '' : undefined}
157 style="border: 2px dashed hsl(var(--border)); max-height: var(--max-message-height); overflow-wrap: anywhere; word-break: break-word;"
158 >
159 <div
160 class="relative transition-all duration-300 {isExpanded
161 ? 'cursor-text select-text'
162 : 'select-none'}"
163 style={!isExpanded && showExpandButton
164 ? `max-height: ${MAX_HEIGHT}px;`
165 : 'max-height: none;'}
166 >
167 {#if currentConfig.renderUserContentAsMarkdown}
168 <div bind:this={messageElement} class={isExpanded ? 'cursor-text' : ''}>
169 <MarkdownContent class="markdown-system-content" content={message.content} />
170 </div>
171 {:else}
172 <span
173 bind:this={messageElement}
174 class="text-md whitespace-pre-wrap {isExpanded ? 'cursor-text' : ''}"
175 >
176 {message.content}
177 </span>
178 {/if}
179
180 {#if !isExpanded && showExpandButton}
181 <div
182 class="pointer-events-none absolute right-0 bottom-0 left-0 h-48 bg-gradient-to-t from-muted to-transparent"
183 ></div>
184
185 <div
186 class="pointer-events-none absolute right-0 bottom-4 left-0 flex justify-center opacity-0 transition-opacity group-hover/expand:opacity-100"
187 >
188 <Button
189 class="rounded-full px-4 py-1.5 text-xs shadow-md"
190 size="sm"
191 variant="outline"
192 >
193 Show full system message
194 </Button>
195 </div>
196 {/if}
197 </div>
198
199 {#if isExpanded && showExpandButton}
200 <div class="mb-2 flex justify-center">
201 <Button
202 class="rounded-full px-4 py-1.5 text-xs"
203 onclick={(e) => {
204 e.stopPropagation();
205 toggleExpand();
206 }}
207 size="sm"
208 variant="outline"
209 >
210 Collapse System Message
211 </Button>
212 </div>
213 {/if}
214 </Card>
215 </button>
216 </div>
217 {/if}
218
219 {#if message.timestamp}
220 <div class="max-w-[80%]">
221 <ChatMessageActionIcons
222 actionsPosition="right"
223 {deletionInfo}
224 justify="end"
225 {onConfirmDelete}
226 {onCopy}
227 {onDelete}
228 {onEdit}
229 {onNavigateToSibling}
230 {onShowDeleteDialogChange}
231 {siblingInfo}
232 {showDeleteDialog}
233 role={MessageRole.USER}
234 />
235 </div>
236 {/if}
237 {/if}
238 </div>