3 ChatMessageAgenticContent,
4 ChatMessageActionIcons,
5 ChatMessageAssistantModel,
6 ChatMessageAssistantProcessingInfo,
7 ChatMessageAssistantRawOutput,
8 ChatMessageAssistantStatistics,
10 } from '$lib/components/app';
11 import { getMessageEditContext } from '$lib/contexts';
12 import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
13 import { 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';
20 import { hasAgenticContent } from '$lib/utils';
27 assistantMessages: number;
28 messageTypes: string[];
30 isLastAssistantMessage?: boolean;
31 message: DatabaseMessage;
32 toolMessages?: DatabaseMessage[];
34 onConfirmDelete: () => void;
35 onContinue?: () => 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;
48 class: className = '',
50 isLastAssistantMessage = false,
61 onShowDeleteDialogChange,
64 textareaElement = $bindable()
68 const editCtx = getMessageEditContext();
70 const isAgentic = $derived(hasAgenticContent(message, toolMessages));
71 const processingState = useProcessingState();
73 let currentConfig = $derived(config());
74 let isRouter = $derived(isRouterMode());
76 let showRawOutput = $state(false);
78 let displayedModel = $derived(message.model ?? null);
80 let isCurrentlyLoading = $derived(isLoading());
81 let isStreaming = $derived(isChatStreaming());
82 let hasNoContent = $derived(!message?.content?.trim());
83 let isActivelyProcessing = $derived(isCurrentlyLoading || isStreaming);
85 // during a router auto-load the message has no model yet, so target the selected one
86 let loadTargetModel = $derived(message.model ?? modelsStore.selectedModelName);
87 let modelLoadProgress = $derived(
88 isRouter && loadTargetModel ? modelsStore.getLoadProgress(loadTargetModel) : null
90 let modelLoadingText = $derived(modelLoadProgressText(modelLoadProgress));
92 let showProcessingInfoTop = $derived(
93 message?.role === MessageRole.ASSISTANT &&
94 isActivelyProcessing &&
97 isLastAssistantMessage
100 let showProcessingInfoBottom = $derived(
101 message?.role === MessageRole.ASSISTANT &&
102 isActivelyProcessing &&
103 (!hasNoContent || isAgentic) &&
104 isLastAssistantMessage
107 let assistantEl: HTMLDivElement | undefined = $state();
108 let lastUserMessageHeight = $state(0);
109 let assistantMarginTop = $state(0);
112 if (!assistantEl) return;
114 assistantMarginTop = Math.round(parseFloat(getComputedStyle(assistantEl).marginTop));
116 const chatMessageEl = assistantEl.closest('.chat-message');
117 const previousChatMessage = chatMessageEl?.previousElementSibling;
118 const userMessageEl = previousChatMessage?.querySelector(
120 ) as HTMLElement | null;
122 if (!userMessageEl) {
123 lastUserMessageHeight = 0;
127 const updateHeight = () => {
128 const rect = userMessageEl.getBoundingClientRect();
129 const marginTop = Math.round(parseFloat(getComputedStyle(userMessageEl).marginTop));
130 lastUserMessageHeight = Math.round(rect.height + marginTop);
135 const resizeObserver = new ResizeObserver(updateHeight);
136 resizeObserver.observe(userMessageEl);
139 resizeObserver.disconnect();
144 if (showProcessingInfoTop || showProcessingInfoBottom) {
145 processingState.startMonitoring();
151 bind:this={assistantEl}
152 class="chat-message-assistant text-md group w-full leading-7.5 {className}"
153 style:--last-user-message-height={lastUserMessageHeight > 0
154 ? `${lastUserMessageHeight}px`
156 style:--assistant-margin-top={assistantMarginTop > 0 ? `${assistantMarginTop}px` : undefined}
158 aria-label="Assistant message with actions"
160 {#if showProcessingInfoTop}
161 <ChatMessageAssistantProcessingInfo {modelLoadingText} {processingState} position="top" />
164 {#if editCtx.isEditing}
165 <ChatMessageEditForm />
168 <ChatMessageAssistantRawOutput {message} {toolMessages} />
170 <ChatMessageAgenticContent
173 isStreaming={isChatStreaming()}
174 {isLastAssistantMessage}
179 {#if showProcessingInfoBottom}
180 <ChatMessageAssistantProcessingInfo {modelLoadingText} {processingState} position="bottom" />
183 <div class="info my-6 grid gap-4 tabular-nums">
185 <div class="inline-flex flex-wrap items-start gap-2 text-xs text-muted-foreground">
186 <ChatMessageAssistantModel
188 isLoading={isLoading()}
193 <ChatMessageAssistantStatistics
195 isLoading={isLoading()}
197 showMessageStats={currentConfig.showMessageStats}
203 {#if message.timestamp && !editCtx.isEditing}
204 <ChatMessageActionIcons
205 role={MessageRole.ASSISTANT}
207 actionsPosition="left"
214 onContinue={currentConfig.enableContinueGeneration ? onContinue : undefined}
218 {onNavigateToSibling}
219 {onShowDeleteDialogChange}
220 showRawOutputSwitch={currentConfig.showRawOutputSwitch}
221 rawOutputEnabled={showRawOutput}
222 onRawOutputToggle={(enabled) => (showRawOutput = enabled)}
228 :global(.chat-message):last-child .chat-message-assistant {
229 --assistant-min-height-offset: calc(
230 var(--last-user-message-height, 19rem) + var(--chat-form-height, 6rem) +
231 var(--chat-form-bottom-position, 0.5rem) + var(--chat-form-padding-top, 6rem) +
232 var(--assistant-margin-top, 3rem)
234 min-height: calc(100dvh - var(--assistant-min-height-offset));
236 @media (width > 768px) {
237 --assistant-min-height-offset: calc(
238 var(--last-user-message-height, 18rem) + var(--chat-form-height, 6rem) +
239 var(--chat-form-bottom-position, 1rem) + var(--chat-form-padding-top, 6rem) +
240 var(--assistant-margin-top, 3rem)