3 ChatMessageActionIcons,
4 ChatMessageAgenticContent,
5 ChatMessageAssistantModel,
6 ChatMessageAssistantProcessingInfo,
7 ChatMessageAssistantRawOutput,
8 ChatMessageAssistantStatistics,
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';
26 assistantMessages: number;
27 messageTypes: string[];
29 isLastAssistantMessage?: boolean;
30 message: DatabaseMessage;
31 toolMessages?: DatabaseMessage[];
33 onConfirmDelete: () => void;
34 onContinue?: () => 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;
47 class: className = '',
49 isLastAssistantMessage = false,
59 onShowDeleteDialogChange,
62 textareaElement = $bindable(),
67 const editCtx = getMessageEditContext();
69 const isAgentic = $derived(hasAgenticContent(message, toolMessages));
70 const processingState = useProcessingState();
72 let currentConfig = $derived(config());
73 let isRouter = $derived(isRouterMode());
75 let showRawOutput = $state(false);
77 let displayedModel = $derived(message.model ?? null);
79 let isCurrentlyLoading = $derived(isLoading());
80 let isStreaming = $derived(isChatStreaming());
81 let hasNoContent = $derived(!message?.content?.trim());
82 let isActivelyProcessing = $derived(isCurrentlyLoading || isStreaming);
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
89 let modelLoadProgress = $derived(
90 isRouter && loadTargetModel ? modelsStore.getLoadProgress(loadTargetModel) : null
92 let modelLoadingText = $derived(modelLoadProgressText(modelLoadProgress));
94 let showProcessingInfoTop = $derived(
95 message?.role === MessageRole.ASSISTANT &&
96 isActivelyProcessing &&
99 isLastAssistantMessage
102 let showProcessingInfoBottom = $derived(
103 message?.role === MessageRole.ASSISTANT &&
104 isActivelyProcessing &&
105 (!hasNoContent || isAgentic) &&
106 isLastAssistantMessage
109 let assistantEl: HTMLDivElement | undefined = $state();
110 let lastUserMessageHeight = $state(0);
111 let assistantMarginTop = $state(0);
114 if (!assistantEl) return;
116 assistantMarginTop = Math.round(parseFloat(getComputedStyle(assistantEl).marginTop));
118 const chatMessageEl = assistantEl.closest('.chat-message');
119 const previousChatMessage = chatMessageEl?.previousElementSibling;
120 const userMessageEl = previousChatMessage?.querySelector(
122 ) as HTMLElement | null;
124 if (!userMessageEl) {
125 lastUserMessageHeight = 0;
130 const updateHeight = () => {
131 const rect = userMessageEl.getBoundingClientRect();
132 const marginTop = Math.round(parseFloat(getComputedStyle(userMessageEl).marginTop));
134 lastUserMessageHeight = Math.round(rect.height + marginTop);
139 const resizeObserver = new ResizeObserver(updateHeight);
141 resizeObserver.observe(userMessageEl);
144 resizeObserver.disconnect();
149 if (showProcessingInfoTop || showProcessingInfoBottom) {
150 processingState.startMonitoring();
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`
161 style:--assistant-margin-top={assistantMarginTop > 0 ? `${assistantMarginTop}px` : undefined}
163 aria-label="Assistant message with actions"
165 {#if showProcessingInfoTop}
166 <ChatMessageAssistantProcessingInfo {modelLoadingText} {processingState} position="top" />
169 {#if editCtx.isEditing}
170 <ChatMessageEditForm />
173 <ChatMessageAssistantRawOutput {message} {toolMessages} />
175 <ChatMessageAgenticContent
178 isStreaming={isChatStreaming()}
179 {isLastAssistantMessage}
184 {#if showProcessingInfoBottom}
185 <ChatMessageAssistantProcessingInfo {modelLoadingText} {processingState} position="bottom" />
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
193 isLoading={isLoading()}
198 <ChatMessageAssistantStatistics
200 isLoading={isLoading()}
202 showMessageStats={currentConfig.showMessageStats}
208 {#if message.timestamp && !editCtx.isEditing}
209 <ChatMessageActionIcons
210 role={MessageRole.ASSISTANT}
212 actionsPosition="left"
219 onContinue={currentConfig.enableContinueGeneration ? onContinue : undefined}
223 {onNavigateToSibling}
224 {onShowDeleteDialogChange}
225 showRawOutputSwitch={currentConfig.showRawOutputSwitch}
226 rawOutputEnabled={showRawOutput}
227 onRawOutputToggle={(enabled) => (showRawOutput = enabled)}
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)
239 min-height: calc(100dvh - var(--assistant-min-height-offset));
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)