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