diff --git a/src/app/chat/ChatMessageList.tsx b/src/app/chat/ChatMessageList.tsx index 64bea2a..9963169 100644 --- a/src/app/chat/ChatMessageList.tsx +++ b/src/app/chat/ChatMessageList.tsx @@ -342,7 +342,10 @@ function StreamingBubble({ parts, isDone }: { parts: StreamPart[]; isDone: boole } }, [displayParts.length]); - const firstThinkingIdx = displayParts.findIndex((p) => p.type === "thinking"); + const activeThinkingIdx = + !displayDone && displayParts[displayParts.length - 1]?.type === "thinking" + ? displayParts.length - 1 + : -1; return (
@@ -364,12 +367,12 @@ function StreamingBubble({ parts, isDone }: { parts: StreamPart[]; isDone: boole
{displayParts.map((part, i) => { if (part.type === "thinking") { - const isActivelyThinking = !displayDone && i === firstThinkingIdx; return ( - - - {part.content} - + ); } if (part.type === "tool_call") { @@ -407,6 +410,59 @@ function StreamingBubble({ parts, isDone }: { parts: StreamPart[]; isDone: boole ); } +function StreamingReasoningBlock({ + content, + isActivelyThinking, +}: { + content: string; + isActivelyThinking: boolean; +}) { + const [manualOpen, setManualOpen] = useState(false); + const [autoCollapsed, setAutoCollapsed] = useState(false); + const wasActivelyThinkingRef = useRef(isActivelyThinking); + + useEffect(() => { + const wasActivelyThinking = wasActivelyThinkingRef.current; + + if (isActivelyThinking && !wasActivelyThinking) { + setAutoCollapsed(false); + setManualOpen(false); + } + + if (!isActivelyThinking && wasActivelyThinking) { + setAutoCollapsed(false); + setManualOpen(false); + } + + wasActivelyThinkingRef.current = isActivelyThinking; + }, [isActivelyThinking]); + + const isOpen = isActivelyThinking ? !autoCollapsed : manualOpen; + + const handleOpenChange = useCallback( + (nextOpen: boolean) => { + if (isActivelyThinking) { + setAutoCollapsed(!nextOpen); + return; + } + setManualOpen(nextOpen); + }, + [isActivelyThinking] + ); + + return ( + + + {content} + + ); +} + function StreamingCursor() { return ( & { defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; duration?: number; + autoLifecycle?: boolean; }; const AUTO_CLOSE_DELAY = 1000; @@ -64,6 +65,7 @@ export const Reasoning = memo( defaultOpen, onOpenChange, duration: durationProp, + autoLifecycle = true, children, ...props }: ReasoningProps) => { @@ -100,15 +102,16 @@ export const Reasoning = memo( // Auto-open when streaming starts (unless explicitly closed) useEffect(() => { - if (isStreaming && !isOpen && !isExplicitlyClosed) { + if (autoLifecycle && isStreaming && !isOpen && !isExplicitlyClosed) { setIsOpen(true); } - }, [isStreaming, isOpen, setIsOpen, isExplicitlyClosed]); + }, [autoLifecycle, isStreaming, isOpen, setIsOpen, isExplicitlyClosed]); // Auto-close when streaming ends (once only, and only if it ever streamed) useEffect(() => { if ( hasEverStreamedRef.current && + autoLifecycle && !isStreaming && isOpen && !hasAutoClosed @@ -120,7 +123,7 @@ export const Reasoning = memo( return () => clearTimeout(timer); } - }, [isStreaming, isOpen, setIsOpen, hasAutoClosed]); + }, [autoLifecycle, isStreaming, isOpen, setIsOpen, hasAutoClosed]); const handleOpenChange = useCallback( (newOpen: boolean) => {