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