feat: add elapsed time counter to thinking indicator
Shows how long the agent has been thinking (e.g. '5s', '1m 23s'). Timer appears after 2 seconds to avoid flicker on fast responses. Helps users gauge if a request is still processing or stuck.
This commit is contained in:
@@ -1,6 +1,28 @@
|
|||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { Bot } from 'lucide-react';
|
import { Bot } from 'lucide-react';
|
||||||
|
import { useT } from '../hooks/useLocale';
|
||||||
|
|
||||||
|
function formatElapsed(seconds: number): string {
|
||||||
|
if (seconds < 60) return `${seconds}s`;
|
||||||
|
const m = Math.floor(seconds / 60);
|
||||||
|
const s = seconds % 60;
|
||||||
|
return `${m}m ${s.toString().padStart(2, '0')}s`;
|
||||||
|
}
|
||||||
|
|
||||||
export function TypingIndicator() {
|
export function TypingIndicator() {
|
||||||
|
const t = useT();
|
||||||
|
const [elapsed, setElapsed] = useState(0);
|
||||||
|
const startRef = useRef(Date.now());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
startRef.current = Date.now();
|
||||||
|
setElapsed(0);
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setElapsed(Math.floor((Date.now() - startRef.current) / 1000));
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-fade-in flex items-start gap-3 px-4 py-3">
|
<div className="animate-fade-in flex items-start gap-3 px-4 py-3">
|
||||||
<div className="shrink-0 flex h-9 w-9 items-center justify-center rounded-2xl border border-white/10 bg-zinc-900/60">
|
<div className="shrink-0 flex h-9 w-9 items-center justify-center rounded-2xl border border-white/10 bg-zinc-900/60">
|
||||||
@@ -11,7 +33,10 @@ export function TypingIndicator() {
|
|||||||
<span className="bounce-dot h-2 w-2 rounded-full bg-gradient-to-r from-cyan-300/80 to-violet-400/80" />
|
<span className="bounce-dot h-2 w-2 rounded-full bg-gradient-to-r from-cyan-300/80 to-violet-400/80" />
|
||||||
<span className="bounce-dot h-2 w-2 rounded-full bg-gradient-to-r from-cyan-300/80 to-violet-400/80" />
|
<span className="bounce-dot h-2 w-2 rounded-full bg-gradient-to-r from-cyan-300/80 to-violet-400/80" />
|
||||||
<span className="bounce-dot h-2 w-2 rounded-full bg-gradient-to-r from-cyan-300/80 to-violet-400/80" />
|
<span className="bounce-dot h-2 w-2 rounded-full bg-gradient-to-r from-cyan-300/80 to-violet-400/80" />
|
||||||
<span className="ml-2 text-xs text-zinc-400">Thinking…</span>
|
<span className="ml-2 text-xs text-zinc-400">{t('chat.thinking')}</span>
|
||||||
|
{elapsed >= 2 && (
|
||||||
|
<span className="text-[10px] text-zinc-500 tabular-nums ml-1">{formatElapsed(elapsed)}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const en = {
|
|||||||
'chat.stop': 'Stop',
|
'chat.stop': 'Stop',
|
||||||
'chat.scrollToBottom': 'New messages',
|
'chat.scrollToBottom': 'New messages',
|
||||||
'chat.messages': 'Chat messages',
|
'chat.messages': 'Chat messages',
|
||||||
|
'chat.thinking': 'Thinking…',
|
||||||
|
|
||||||
// Sidebar
|
// Sidebar
|
||||||
'sidebar.title': 'Sessions',
|
'sidebar.title': 'Sessions',
|
||||||
@@ -121,6 +122,7 @@ const fr: Record<keyof typeof en, string> = {
|
|||||||
'chat.stop': 'Arrêter',
|
'chat.stop': 'Arrêter',
|
||||||
'chat.scrollToBottom': 'Nouveaux messages',
|
'chat.scrollToBottom': 'Nouveaux messages',
|
||||||
'chat.messages': 'Messages du chat',
|
'chat.messages': 'Messages du chat',
|
||||||
|
'chat.thinking': 'Réflexion…',
|
||||||
|
|
||||||
'sidebar.title': 'Sessions',
|
'sidebar.title': 'Sessions',
|
||||||
'sidebar.empty': 'Aucune session',
|
'sidebar.empty': 'Aucune session',
|
||||||
|
|||||||
Reference in New Issue
Block a user