From aa4fb786a2b5629f59353cfa60747e30db2bd22d Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Wed, 28 Jan 2026 18:28:03 +0100 Subject: [PATCH] refactor: Refine Chat Message Processing State Display --- .../app/chat/ChatMessages/ChatMessage.svelte | 9 +++- .../ChatMessageAgenticContent.svelte | 4 +- .../ChatMessages/ChatMessageAssistant.svelte | 43 +++++++++++++++++-- .../app/chat/ChatMessages/ChatMessages.svelte | 23 ++++++++-- .../ChatScreenProcessingInfo.svelte | 2 +- .../lib/hooks/use-processing-state.svelte.ts | 6 +-- 6 files changed, 70 insertions(+), 17 deletions(-) diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessage.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessage.svelte index 0734e9f889..03803490ba 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessage.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessage.svelte @@ -17,10 +17,16 @@ interface Props { class?: string; message: DatabaseMessage; + isLastAssistantMessage?: boolean; siblingInfo?: ChatMessageSiblingInfo | null; } - let { class: className = '', message, siblingInfo = null }: Props = $props(); + let { + class: className = '', + message, + isLastAssistantMessage = false, + siblingInfo = null + }: Props = $props(); const chatActions = getChatActionsContext(); @@ -278,6 +284,7 @@ bind:textareaElement class={className} {deletionInfo} + {isLastAssistantMessage} {message} messageContent={message.content} onConfirmDelete={handleConfirmDelete} diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte index 3f39f8f80c..5978fe8e06 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte @@ -67,7 +67,7 @@ {:else if section.type === AgenticSectionType.TOOL_CALL_STREAMING} {@const streamingIcon = isStreaming ? Loader2 : AlertTriangle} {@const streamingIconClass = isStreaming ? 'h-4 w-4 animate-spin' : 'h-4 w-4 text-yellow-500'} - {@const streamingSubtitle = isStreaming ? 'streaming...' : 'incomplete'} + {@const streamingSubtitle = isStreaming ? '' : 'incomplete'} {:else if section.type === AgenticSectionType.REASONING_PENDING} {@const reasoningTitle = isStreaming ? 'Reasoning...' : 'Reasoning'} - {@const reasoningSubtitle = isStreaming ? 'streaming...' : 'incomplete'} + {@const reasoningSubtitle = isStreaming ? '' : 'incomplete'} void; @@ -51,6 +52,7 @@ let { class: className = '', deletionInfo, + isLastAssistantMessage = false, message, messageContent, onConfirmDelete, @@ -100,6 +102,25 @@ let displayedModel = $derived(message.model ?? null); + let isCurrentlyLoading = $derived(isLoading()); + let isStreaming = $derived(isChatStreaming()); + let hasNoContent = $derived(!message?.content?.trim()); + let isActivelyProcessing = $derived(isCurrentlyLoading || isStreaming); + + let showProcessingInfoTop = $derived( + message?.role === MessageRole.ASSISTANT && + isActivelyProcessing && + hasNoContent && + isLastAssistantMessage + ); + + let showProcessingInfoBottom = $derived( + message?.role === MessageRole.ASSISTANT && + isActivelyProcessing && + !hasNoContent && + isLastAssistantMessage + ); + function handleCopyModel() { void copyToClipboard(displayedModel ?? ''); } @@ -111,7 +132,7 @@ }); $effect(() => { - if (isLoading() && !message?.content?.trim()) { + if (showProcessingInfoTop || showProcessingInfoBottom) { processingState.startMonitoring(); } }); @@ -122,11 +143,13 @@ role="group" aria-label="Assistant message with actions" > - {#if message?.role === MessageRole.ASSISTANT && isLoading() && !message?.content?.trim()} + {#if showProcessingInfoTop}
- {processingState.getPromptProgressText() ?? processingState.getProcessingMessage()} + {processingState.getPromptProgressText() ?? + processingState.getProcessingMessage() ?? + 'Processing...'}
@@ -193,7 +216,19 @@ {/if} -
+ {#if showProcessingInfoBottom} +
+
+ + {processingState.getPromptProgressText() ?? + processingState.getProcessingMessage() ?? + 'Processing...'} + +
+
+ {/if} + +
{#if displayedModel}
{#if isRouter} diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte index be6c87d2bd..07e526e555 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte @@ -100,16 +100,26 @@ return []; } - // Filter out system messages if showSystemMessage is false const filteredMessages = currentConfig.showSystemMessage ? messages : messages.filter((msg) => msg.type !== MessageRole.SYSTEM); - return filteredMessages.map((message) => { + let lastAssistantIndex = -1; + for (let i = filteredMessages.length - 1; i >= 0; i--) { + if (filteredMessages[i].role === MessageRole.ASSISTANT) { + lastAssistantIndex = i; + break; + } + } + + return filteredMessages.map((message, index) => { const siblingInfo = getMessageSiblings(allConversationMessages, message.id); + const isLastAssistantMessage = + message.role === MessageRole.ASSISTANT && index === lastAssistantIndex; return { message, + isLastAssistantMessage, siblingInfo: siblingInfo || { message, siblingIds: [message.id], @@ -122,7 +132,12 @@
- {#each displayMessages as { message, siblingInfo } (message.id)} - + {#each displayMessages as { message, isLastAssistantMessage, siblingInfo } (message.id)} + {/each}
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreenProcessingInfo.svelte b/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreenProcessingInfo.svelte index bf52b1f0b6..cc7b22cfd8 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreenProcessingInfo.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreenProcessingInfo.svelte @@ -11,7 +11,7 @@ let isCurrentConversationLoading = $derived(isLoading()); let isStreaming = $derived(isChatStreaming()); let hasProcessingData = $derived(processingState.processingState !== null); - let processingDetails = $derived(processingState.getProcessingDetails()); + let processingDetails = $derived(processingState.getTechnicalDetails()); let showProcessingInfo = $derived( isCurrentConversationLoading || isStreaming || config().keepStatsVisible || hasProcessingData diff --git a/tools/server/webui/src/lib/hooks/use-processing-state.svelte.ts b/tools/server/webui/src/lib/hooks/use-processing-state.svelte.ts index 1205d9b973..d23edb5677 100644 --- a/tools/server/webui/src/lib/hooks/use-processing-state.svelte.ts +++ b/tools/server/webui/src/lib/hooks/use-processing-state.svelte.ts @@ -197,11 +197,7 @@ export function useProcessingState(): UseProcessingStateReturn { const details: string[] = []; // Always show context info when we have valid data - if ( - typeof stateToUse.contextTotal === 'number' && - stateToUse.contextUsed >= 0 && - stateToUse.contextTotal > 0 - ) { + if (stateToUse.contextUsed >= 0 && stateToUse.contextTotal > 0) { const contextPercent = Math.round((stateToUse.contextUsed / stateToUse.contextTotal) * 100); details.push(