From 25e63f8d1838277473809f7b4c39c6440d078566 Mon Sep 17 00:00:00 2001 From: Nicolas Varrot Date: Thu, 12 Feb 2026 23:42:39 +0000 Subject: [PATCH] feat: improved thinking/reasoning indicator with elapsed time counter When the agent is reasoning with hidden thinking (thinking=low), show a pulsing 'Reasoning...' indicator with elapsed time instead of generic bouncing dots. Once text content starts streaming, falls back to the regular streaming dots. Closes feedback #40 --- src/components/ChatMessage.tsx | 23 +++++++++------ src/components/ThinkingIndicator.tsx | 42 ++++++++++++++++++++++++++++ src/lib/i18n.ts | 2 ++ 3 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 src/components/ThinkingIndicator.tsx diff --git a/src/components/ChatMessage.tsx b/src/components/ChatMessage.tsx index 4b507b7..c9f7108 100644 --- a/src/components/ChatMessage.tsx +++ b/src/components/ChatMessage.tsx @@ -7,6 +7,7 @@ import remarkBreaks from 'remark-breaks'; import rehypeHighlight from 'rehype-highlight'; import type { ChatMessage as ChatMessageType, MessageBlock } from '../types'; import { ThinkingBlock } from './ThinkingBlock'; +import { ThinkingIndicator } from './ThinkingIndicator'; import { CodeBlock } from './CodeBlock'; import { ToolCall } from './ToolCall'; import { ImageBlock } from './ImageBlock'; @@ -388,14 +389,20 @@ export function ChatMessageComponent({ message, onRetry, agentAvatarUrl }: { mes {/* Inline images */} {renderImageBlocks(message.blocks)} - {/* Streaming dots */} - {message.isStreaming && ( -
- - - -
- )} + {/* Streaming indicator */} + {message.isStreaming && (() => { + const hasVisibleContent = message.content?.trim(); + if (!hasVisibleContent) { + return ; + } + return ( +
+ + + +
+ ); + })()} {/* Tool calls & thinking (inline) */} {!isUser && } diff --git a/src/components/ThinkingIndicator.tsx b/src/components/ThinkingIndicator.tsx new file mode 100644 index 0000000..5cadeeb --- /dev/null +++ b/src/components/ThinkingIndicator.tsx @@ -0,0 +1,42 @@ +import { useState, useEffect, useRef } from 'react'; +import { Brain } from 'lucide-react'; +import { useT } from '../hooks/useLocale'; + +/** + * Animated reasoning/thinking indicator shown during streaming + * when no text content has appeared yet (thinking=low mode). + * Displays elapsed time and a pulsing animation. + */ +export function ThinkingIndicator() { + const t = useT(); + const [elapsed, setElapsed] = useState(0); + const startRef = useRef(Date.now()); + + useEffect(() => { + const interval = setInterval(() => { + setElapsed(Math.floor((Date.now() - startRef.current) / 1000)); + }, 1000); + return () => clearInterval(interval); + }, []); + + const formatElapsed = (s: number) => { + if (s < 60) return `${s}s`; + const m = Math.floor(s / 60); + const rem = s % 60; + return `${m}m ${rem.toString().padStart(2, '0')}s`; + }; + + return ( +
+
+ + + {t('thinking.reasoning')} + + + {formatElapsed(elapsed)} + +
+
+ ); +} diff --git a/src/lib/i18n.ts b/src/lib/i18n.ts index 8827705..1e5566e 100644 --- a/src/lib/i18n.ts +++ b/src/lib/i18n.ts @@ -60,6 +60,7 @@ const en = { // Thinking 'thinking.label': 'Thinking', + 'thinking.reasoning': 'Reasoning…', // Tool call 'tool.parameters': 'Parameters', @@ -151,6 +152,7 @@ const fr: Record = { 'sidebar.deleteCancel': 'Annuler', 'thinking.label': 'Réflexion', + 'thinking.reasoning': 'Réflexion…', 'tool.parameters': 'Paramètres', 'tool.result': 'Résultat',