refactor: Refine Chat Message Processing State Display

This commit is contained in:
Aleksander Grygier 2026-01-28 18:28:03 +01:00
parent 6047da3f72
commit aa4fb786a2
6 changed files with 70 additions and 17 deletions

View File

@ -17,10 +17,16 @@
interface Props { interface Props {
class?: string; class?: string;
message: DatabaseMessage; message: DatabaseMessage;
isLastAssistantMessage?: boolean;
siblingInfo?: ChatMessageSiblingInfo | null; siblingInfo?: ChatMessageSiblingInfo | null;
} }
let { class: className = '', message, siblingInfo = null }: Props = $props(); let {
class: className = '',
message,
isLastAssistantMessage = false,
siblingInfo = null
}: Props = $props();
const chatActions = getChatActionsContext(); const chatActions = getChatActionsContext();
@ -278,6 +284,7 @@
bind:textareaElement bind:textareaElement
class={className} class={className}
{deletionInfo} {deletionInfo}
{isLastAssistantMessage}
{message} {message}
messageContent={message.content} messageContent={message.content}
onConfirmDelete={handleConfirmDelete} onConfirmDelete={handleConfirmDelete}

View File

@ -67,7 +67,7 @@
{:else if section.type === AgenticSectionType.TOOL_CALL_STREAMING} {:else if section.type === AgenticSectionType.TOOL_CALL_STREAMING}
{@const streamingIcon = isStreaming ? Loader2 : AlertTriangle} {@const streamingIcon = isStreaming ? Loader2 : AlertTriangle}
{@const streamingIconClass = isStreaming ? 'h-4 w-4 animate-spin' : 'h-4 w-4 text-yellow-500'} {@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'}
<CollapsibleContentBlock <CollapsibleContentBlock
open={isExpanded(index, section)} open={isExpanded(index, section)}
@ -171,7 +171,7 @@
</CollapsibleContentBlock> </CollapsibleContentBlock>
{:else if section.type === AgenticSectionType.REASONING_PENDING} {:else if section.type === AgenticSectionType.REASONING_PENDING}
{@const reasoningTitle = isStreaming ? 'Reasoning...' : 'Reasoning'} {@const reasoningTitle = isStreaming ? 'Reasoning...' : 'Reasoning'}
{@const reasoningSubtitle = isStreaming ? 'streaming...' : 'incomplete'} {@const reasoningSubtitle = isStreaming ? '' : 'incomplete'}
<CollapsibleContentBlock <CollapsibleContentBlock
open={isExpanded(index, section)} open={isExpanded(index, section)}

View File

@ -33,6 +33,7 @@
assistantMessages: number; assistantMessages: number;
messageTypes: string[]; messageTypes: string[];
} | null; } | null;
isLastAssistantMessage?: boolean;
message: DatabaseMessage; message: DatabaseMessage;
messageContent: string | undefined; messageContent: string | undefined;
onCopy: () => void; onCopy: () => void;
@ -51,6 +52,7 @@
let { let {
class: className = '', class: className = '',
deletionInfo, deletionInfo,
isLastAssistantMessage = false,
message, message,
messageContent, messageContent,
onConfirmDelete, onConfirmDelete,
@ -100,6 +102,25 @@
let displayedModel = $derived(message.model ?? null); 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() { function handleCopyModel() {
void copyToClipboard(displayedModel ?? ''); void copyToClipboard(displayedModel ?? '');
} }
@ -111,7 +132,7 @@
}); });
$effect(() => { $effect(() => {
if (isLoading() && !message?.content?.trim()) { if (showProcessingInfoTop || showProcessingInfoBottom) {
processingState.startMonitoring(); processingState.startMonitoring();
} }
}); });
@ -122,11 +143,13 @@
role="group" role="group"
aria-label="Assistant message with actions" aria-label="Assistant message with actions"
> >
{#if message?.role === MessageRole.ASSISTANT && isLoading() && !message?.content?.trim()} {#if showProcessingInfoTop}
<div class="mt-6 w-full max-w-[48rem]" in:fade> <div class="mt-6 w-full max-w-[48rem]" in:fade>
<div class="processing-container"> <div class="processing-container">
<span class="processing-text"> <span class="processing-text">
{processingState.getPromptProgressText() ?? processingState.getProcessingMessage()} {processingState.getPromptProgressText() ??
processingState.getProcessingMessage() ??
'Processing...'}
</span> </span>
</div> </div>
</div> </div>
@ -193,7 +216,19 @@
</div> </div>
{/if} {/if}
<div class="info my-6 grid gap-4"> {#if showProcessingInfoBottom}
<div class="mt-4 w-full max-w-[48rem]" in:fade>
<div class="processing-container">
<span class="processing-text">
{processingState.getPromptProgressText() ??
processingState.getProcessingMessage() ??
'Processing...'}
</span>
</div>
</div>
{/if}
<div class="info my-6 grid gap-4 tabular-nums">
{#if displayedModel} {#if displayedModel}
<div class="inline-flex flex-wrap items-start gap-2 text-xs text-muted-foreground"> <div class="inline-flex flex-wrap items-start gap-2 text-xs text-muted-foreground">
{#if isRouter} {#if isRouter}

View File

@ -100,16 +100,26 @@
return []; return [];
} }
// Filter out system messages if showSystemMessage is false
const filteredMessages = currentConfig.showSystemMessage const filteredMessages = currentConfig.showSystemMessage
? messages ? messages
: messages.filter((msg) => msg.type !== MessageRole.SYSTEM); : 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 siblingInfo = getMessageSiblings(allConversationMessages, message.id);
const isLastAssistantMessage =
message.role === MessageRole.ASSISTANT && index === lastAssistantIndex;
return { return {
message, message,
isLastAssistantMessage,
siblingInfo: siblingInfo || { siblingInfo: siblingInfo || {
message, message,
siblingIds: [message.id], siblingIds: [message.id],
@ -122,7 +132,12 @@
</script> </script>
<div class="flex h-full flex-col space-y-10 pt-16 md:pt-24 {className}" style="height: auto; "> <div class="flex h-full flex-col space-y-10 pt-16 md:pt-24 {className}" style="height: auto; ">
{#each displayMessages as { message, siblingInfo } (message.id)} {#each displayMessages as { message, isLastAssistantMessage, siblingInfo } (message.id)}
<ChatMessage class="mx-auto w-full max-w-[48rem]" {message} {siblingInfo} /> <ChatMessage
class="mx-auto w-full max-w-[48rem]"
{message}
{isLastAssistantMessage}
{siblingInfo}
/>
{/each} {/each}
</div> </div>

View File

@ -11,7 +11,7 @@
let isCurrentConversationLoading = $derived(isLoading()); let isCurrentConversationLoading = $derived(isLoading());
let isStreaming = $derived(isChatStreaming()); let isStreaming = $derived(isChatStreaming());
let hasProcessingData = $derived(processingState.processingState !== null); let hasProcessingData = $derived(processingState.processingState !== null);
let processingDetails = $derived(processingState.getProcessingDetails()); let processingDetails = $derived(processingState.getTechnicalDetails());
let showProcessingInfo = $derived( let showProcessingInfo = $derived(
isCurrentConversationLoading || isStreaming || config().keepStatsVisible || hasProcessingData isCurrentConversationLoading || isStreaming || config().keepStatsVisible || hasProcessingData

View File

@ -197,11 +197,7 @@ export function useProcessingState(): UseProcessingStateReturn {
const details: string[] = []; const details: string[] = [];
// Always show context info when we have valid data // Always show context info when we have valid data
if ( if (stateToUse.contextUsed >= 0 && stateToUse.contextTotal > 0) {
typeof stateToUse.contextTotal === 'number' &&
stateToUse.contextUsed >= 0 &&
stateToUse.contextTotal > 0
) {
const contextPercent = Math.round((stateToUse.contextUsed / stateToUse.contextTotal) * 100); const contextPercent = Math.round((stateToUse.contextUsed / stateToUse.contextTotal) * 100);
details.push( details.push(