/** * ThinkingBlock - Collapsible thinking/reasoning display * * - While streaming: expanded, "Thinking" label with shimmer + elapsed time * - When done: auto-collapses to "Thought for Xs", click to expand * - Content area has max-height with scroll and top gradient fade */ import { ChevronRight } from 'lucide-react'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useI18n } from '../../application/i18n/I18nProvider'; import { cn } from '../../lib/utils'; interface ThinkingBlockProps { content: string; isStreaming: boolean; durationMs?: number; } function formatDuration(ms: number): string { const seconds = Math.floor(ms / 1000); if (seconds < 60) return `${seconds}s`; const minutes = Math.floor(seconds / 60); const remaining = seconds % 60; return `${minutes}m ${remaining}s`; } const ThinkingBlock: React.FC = ({ content, isStreaming, durationMs, }) => { const { t } = useI18n(); const [isExpanded, setIsExpanded] = useState(isStreaming); const [elapsed, setElapsed] = useState(0); const wasStreamingRef = useRef(false); const startRef = useRef(Date.now()); const scrollRef = useRef(null); // Auto-collapse when streaming ends useEffect(() => { if (wasStreamingRef.current && !isStreaming) { setIsExpanded(false); } wasStreamingRef.current = isStreaming; }, [isStreaming]); // Expand when streaming starts useEffect(() => { if (isStreaming) { setIsExpanded(true); startRef.current = Date.now(); } }, [isStreaming]); // Elapsed time ticker useEffect(() => { if (!isStreaming) return; const timer = setInterval(() => { setElapsed(Date.now() - startRef.current); }, 1000); return () => clearInterval(timer); }, [isStreaming]); // Auto-scroll to bottom while streaming useEffect(() => { if (isStreaming && isExpanded && scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [content, isStreaming, isExpanded]); const toggle = useCallback(() => setIsExpanded(e => !e), []); const displayDuration = durationMs || elapsed; const preview = content.length > 60 ? content.slice(0, 60) + '…' : content; return (
{/* Header */} {/* Content */} {isExpanded && content && (
{/* Top gradient fade */} {isStreaming && (
)}
{content}
)}
); }; export default React.memo(ThinkingBlock);