3 ChatMessageActionIcons,
7 } from '$lib/components/app/chat';
8 import { getMessageEditContext } from '$lib/contexts';
9 import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
10 import { isLoading } from '$lib/stores/chat.svelte';
11 import { MessageRole, ChatMessageStatisticsMode } from '$lib/enums';
12 import { config } from '$lib/stores/settings.svelte';
16 message: DatabaseMessage;
17 siblingInfo?: ChatMessageSiblingInfo | null;
21 assistantMessages: number;
22 messageTypes: string[];
24 isLastUserMessage?: boolean;
25 nextAssistantMessage?: DatabaseMessage | null;
26 showDeleteDialog: boolean;
29 onConfirmDelete: () => void;
30 onForkConversation?: (options: { name: string; includeAttachments: boolean }) => void;
31 onShowDeleteDialogChange: (show: boolean) => void;
32 onNavigateToSibling?: (siblingId: string) => void;
37 class: className = '',
41 isLastUserMessage = false,
42 nextAssistantMessage = null,
48 onShowDeleteDialogChange,
54 const editCtx = getMessageEditContext();
55 const processingState = useProcessingState();
57 const currentConfig = $derived(config());
58 const isActivelyProcessing = $derived(isLastUserMessage && isLoading());
60 // For agentic turns, prefer the cumulative agentic.llm totals over per-call timings.
61 let storedReadingStats = $derived.by(() => {
62 const timings = nextAssistantMessage?.timings;
63 if (!timings?.prompt_n || !timings?.prompt_ms) return null;
65 const agentic = timings.agentic;
68 promptTokens: agentic ? agentic.llm.prompt_n : timings.prompt_n,
69 promptMs: agentic ? agentic.llm.prompt_ms : timings.prompt_ms
73 let showStoredReadingStats = $derived(
74 Boolean(currentConfig.showMessageStats) && storedReadingStats !== null
77 let showLiveReadingStats = $derived(
78 Boolean(currentConfig.showMessageStats) && isActivelyProcessing && storedReadingStats === null
82 if (showLiveReadingStats) {
83 processingState.startMonitoring();
89 aria-label="User message with actions"
90 class="chat-message-user group flex flex-col items-end gap-3 md:gap-2 {className}"
93 {#if editCtx.isEditing}
94 <ChatMessageEditForm />
96 <ChatMessageUserBubble
97 content={message.content}
98 attachments={message.extra}
102 {#if showStoredReadingStats}
103 <!-- Reading stats sourced from the assistant message that followed this turn -->
104 <div class="info my-2 grid w-full justify-items-end gap-4 tabular-nums">
106 class="inline-flex flex-wrap items-start justify-end gap-2 text-xs text-muted-foreground"
108 <ChatMessageStatistics
109 mode={ChatMessageStatisticsMode.READING}
110 promptTokens={storedReadingStats!.promptTokens}
111 promptMs={storedReadingStats!.promptMs}
115 {:else if showLiveReadingStats}
116 {@const liveStats = processingState.getLiveProcessingStats()}
118 <div class="info my-2 grid w-full justify-items-end gap-4 tabular-nums">
120 class="inline-flex flex-wrap items-start justify-end gap-2 text-xs text-muted-foreground"
122 <ChatMessageStatistics
123 mode={ChatMessageStatisticsMode.READING}
125 promptTokens={liveStats.tokensProcessed}
126 promptMs={liveStats.timeMs}
133 {#if message.timestamp}
134 <div class="max-w-[80%]">
135 <ChatMessageActionIcons
136 actionsPosition="right"
144 {onNavigateToSibling}
145 {onShowDeleteDialogChange}
148 role={MessageRole.USER}