]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/blob
14ea68099c6fcc92705b4061b775655838de3e71
[pkg/ggml/sources/llama.cpp] /
1 <script lang="ts">
2 import {
3 ChatMessageActionIcons,
4 ChatMessageAgenticContent,
5 ChatMessageAssistantModel,
6 ChatMessageAssistantProcessingInfo,
7 ChatMessageAssistantRawOutput,
8 ChatMessageAssistantStatistics,
9 ChatMessageEditForm
10 } from '$lib/components/app';
11 import { getMessageEditContext } from '$lib/contexts';
12 import { MessageRole } from '$lib/enums';
13 import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
14 import { chatStore, modelsStore, serverStore, settingsStore } from '$lib/stores';
15 import { modelLoadProgressText } from '$lib/utils';
16 import { hasAgenticContent } from '$lib/utils';
17
18 interface Props {
19 class?: string;
20 deletionInfo: {
21 totalCount: number;
22 userMessages: number;
23 assistantMessages: number;
24 messageTypes: string[];
25 } | null;
26 isLastAssistantMessage?: boolean;
27 message: DatabaseMessage;
28 toolMessages?: DatabaseMessage[];
29 onCopy: () => void;
30 onConfirmDelete: () => void;
31 onContinue?: () => void;
32 onDelete: () => void;
33 onEdit?: () => void;
34 onForkConversation?: (options: { name: string; includeAttachments: boolean }) => void;
35 onNavigateToSibling?: (siblingId: string) => void;
36 onRegenerate: (modelOverride?: string) => void;
37 onShowDeleteDialogChange: (show: boolean) => void;
38 showDeleteDialog: boolean;
39 siblingInfo?: ChatMessageSiblingInfo | null;
40 textareaElement?: HTMLTextAreaElement;
41 }
42
43 let {
44 class: className = '',
45 deletionInfo,
46 isLastAssistantMessage = false,
47 message,
48 onConfirmDelete,
49 onContinue,
50 onCopy,
51 onDelete,
52 onEdit,
53 onForkConversation,
54 onNavigateToSibling,
55 onRegenerate,
56 onShowDeleteDialogChange,
57 showDeleteDialog,
58 siblingInfo = null,
59 textareaElement = $bindable(),
60 toolMessages = []
61 }: Props = $props();
62
63 // Get edit context
64 const editCtx = getMessageEditContext();
65
66 const isAgentic = $derived(hasAgenticContent(message, toolMessages));
67 const processingState = useProcessingState();
68
69 let currentConfig = $derived(settingsStore.config);
70 let isRouter = $derived(serverStore.isRouterMode);
71
72 let showRawOutput = $state(false);
73
74 let displayedModel = $derived(message.model ?? null);
75
76 let isCurrentlyLoading = $derived(chatStore.isLoading);
77 let isStreaming = $derived(chatStore.isStreaming());
78 let hasNoContent = $derived(!message?.content?.trim());
79 let isActivelyProcessing = $derived(isCurrentlyLoading || isStreaming);
80
81 // during a router auto-load the message has no model yet: target the model frozen in the
82 // persisted stream state (survives a reload), then fall back to the dropdown selection
83 let loadTargetModel = $derived(
84 message.model ?? chatStore.getResumeModel(message.convId) ?? modelsStore.selectedModelName
85 );
86 let modelLoadProgress = $derived(
87 isRouter && loadTargetModel ? modelsStore.getLoadProgress(loadTargetModel) : null
88 );
89 let modelLoadingText = $derived(modelLoadProgressText(modelLoadProgress));
90
91 let showProcessingInfoTop = $derived(
92 message?.role === MessageRole.ASSISTANT &&
93 isActivelyProcessing &&
94 hasNoContent &&
95 !isAgentic &&
96 isLastAssistantMessage
97 );
98
99 let showProcessingInfoBottom = $derived(
100 message?.role === MessageRole.ASSISTANT &&
101 isActivelyProcessing &&
102 (!hasNoContent || isAgentic) &&
103 isLastAssistantMessage
104 );
105
106 let assistantEl: HTMLDivElement | undefined = $state();
107 let lastUserMessageHeight = $state(0);
108 let assistantMarginTop = $state(0);
109
110 $effect(() => {
111 if (!assistantEl) return;
112
113 assistantMarginTop = Math.round(parseFloat(getComputedStyle(assistantEl).marginTop));
114
115 const chatMessageEl = assistantEl.closest('.chat-message');
116 const previousChatMessage = chatMessageEl?.previousElementSibling;
117 const userMessageEl = previousChatMessage?.querySelector(
118 '.chat-message-user'
119 ) as HTMLElement | null;
120
121 if (!userMessageEl) {
122 lastUserMessageHeight = 0;
123
124 return;
125 }
126
127 const updateHeight = () => {
128 const rect = userMessageEl.getBoundingClientRect();
129 const marginTop = Math.round(parseFloat(getComputedStyle(userMessageEl).marginTop));
130
131 lastUserMessageHeight = Math.round(rect.height + marginTop);
132 };
133
134 updateHeight();
135
136 const resizeObserver = new ResizeObserver(updateHeight);
137
138 resizeObserver.observe(userMessageEl);
139
140 return () => {
141 resizeObserver.disconnect();
142 };
143 });
144
145 $effect(() => {
146 if (showProcessingInfoTop || showProcessingInfoBottom) {
147 processingState.startMonitoring();
148 }
149 });
150 </script>
151
152 <div
153 bind:this={assistantEl}
154 class="chat-message-assistant text-md group w-full leading-7.5 {className}"
155 style:--last-user-message-height={lastUserMessageHeight > 0
156 ? `${lastUserMessageHeight}px`
157 : undefined}
158 style:--assistant-margin-top={assistantMarginTop > 0 ? `${assistantMarginTop}px` : undefined}
159 role="group"
160 aria-label="Assistant message with actions"
161 >
162 {#if showProcessingInfoTop}
163 <ChatMessageAssistantProcessingInfo {modelLoadingText} {processingState} position="top" />
164 {/if}
165
166 {#if editCtx.isEditing}
167 <ChatMessageEditForm />
168 {:else}
169 {#if showRawOutput}
170 <ChatMessageAssistantRawOutput {message} {toolMessages} />
171 {:else}
172 <ChatMessageAgenticContent
173 {message}
174 {toolMessages}
175 isStreaming={chatStore.isStreaming()}
176 {isLastAssistantMessage}
177 />
178 {/if}
179 {/if}
180
181 {#if showProcessingInfoBottom}
182 <ChatMessageAssistantProcessingInfo {modelLoadingText} {processingState} position="bottom" />
183 {/if}
184
185 {#if displayedModel}
186 <div class="info my-6 grid gap-4 tabular-nums">
187 <div class="inline-flex flex-wrap items-start gap-2 text-xs text-muted-foreground">
188 <ChatMessageAssistantModel
189 {displayedModel}
190 isLoading={chatStore.isLoading}
191 {isRouter}
192 {onRegenerate}
193 />
194
195 <ChatMessageAssistantStatistics
196 {message}
197 isLoading={chatStore.isLoading}
198 {processingState}
199 showMessageStats={currentConfig.showMessageStats}
200 />
201 </div>
202 </div>
203 {/if}
204
205 {#if message.timestamp && !editCtx.isEditing}
206 <ChatMessageActionIcons
207 role={MessageRole.ASSISTANT}
208 justify="start"
209 actionsPosition="left"
210 {siblingInfo}
211 {showDeleteDialog}
212 {deletionInfo}
213 {onCopy}
214 {onEdit}
215 {onRegenerate}
216 onContinue={currentConfig.enableContinueGeneration ? onContinue : undefined}
217 {onForkConversation}
218 {onDelete}
219 {onConfirmDelete}
220 {onNavigateToSibling}
221 {onShowDeleteDialogChange}
222 showRawOutputSwitch={currentConfig.showRawOutputSwitch}
223 rawOutputEnabled={showRawOutput}
224 onRawOutputToggle={(enabled) => (showRawOutput = enabled)}
225 />
226 {/if}
227 </div>
228
229 <style>
230 :global(.chat-message):last-child .chat-message-assistant {
231 --assistant-min-height-offset: calc(
232 var(--last-user-message-height, 19rem) + var(--chat-form-height, 6rem) +
233 var(--chat-form-bottom-position, 0.5rem) + var(--chat-form-padding-top, 6rem) +
234 var(--assistant-margin-top, 3rem)
235 );
236 min-height: calc(100dvh - var(--assistant-min-height-offset));
237
238 @media (width > 768px) {
239 --assistant-min-height-offset: calc(
240 var(--last-user-message-height, 18rem) + var(--chat-form-height, 6rem) +
241 var(--chat-form-bottom-position, 1rem) + var(--chat-form-padding-top, 6rem) +
242 var(--assistant-margin-top, 3rem)
243 );
244 }
245 }
246 </style>