diff --git a/packages/ui/src/components/chat/message/parts/ReasoningPart.tsx b/packages/ui/src/components/chat/message/parts/ReasoningPart.tsx index 59797252..272552fe 100644 --- a/packages/ui/src/components/chat/message/parts/ReasoningPart.tsx +++ b/packages/ui/src/components/chat/message/parts/ReasoningPart.tsx @@ -97,6 +97,8 @@ export const ReasoningTimelineBlock: React.FC = ({ const { t } = useI18n(); const hasEnded = typeof time?.end === 'number'; const [isExpanded, setIsExpanded] = React.useState(hasEnded ? false : (defaultExpanded ?? isStreaming)); + const userToggledRef = React.useRef(false); + const effectiveIsExpanded = hasEnded && !userToggledRef.current ? false : isExpanded; const contentId = React.useId(); const scrollRef = React.useRef(null); const contentRef = React.useRef(null); @@ -107,11 +109,12 @@ export const ReasoningTimelineBlock: React.FC = ({ const prevIsStreamingRef = React.useRef(isStreaming); const summary = React.useMemo(() => getReasoningSummary(text), [text]); - const toggleAriaLabel = isExpanded + const toggleAriaLabel = effectiveIsExpanded ? t('chat.reasoningTrace.collapseAria') : t('chat.reasoningTrace.expandAria'); const handleToggle = React.useCallback(() => { + userToggledRef.current = true; setIsExpanded((prev) => !prev); onContentChange?.('structural'); }, [onContentChange]); @@ -141,10 +144,10 @@ export const ReasoningTimelineBlock: React.FC = ({ }, [onContentChange, text]); React.useEffect(() => { - if (isStreaming && isExpanded && scrollRef.current) { + if (isStreaming && effectiveIsExpanded && scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } - }, [text, isStreaming, isExpanded]); + }, [text, isStreaming, effectiveIsExpanded]); React.useLayoutEffect(() => { const element = contentRef.current; @@ -154,17 +157,25 @@ export const ReasoningTimelineBlock: React.FC = ({ contentAnimationRef.current?.stop(); + if (hasEnded && !userToggledRef.current) { + element.style.height = '0px'; + element.style.opacity = '0'; + element.style.overflow = 'hidden'; + contentMountedRef.current = true; + return; + } + if (!contentMountedRef.current) { contentMountedRef.current = true; - element.style.height = isExpanded ? 'auto' : '0px'; - element.style.opacity = isExpanded ? '1' : '0'; - element.style.overflow = isExpanded ? 'visible' : 'hidden'; + element.style.height = effectiveIsExpanded ? 'auto' : '0px'; + element.style.opacity = effectiveIsExpanded ? '1' : '0'; + element.style.overflow = effectiveIsExpanded ? 'visible' : 'hidden'; return; } element.style.overflow = 'hidden'; - if (isExpanded) { + if (effectiveIsExpanded) { element.style.height = '0px'; element.style.opacity = '0'; } else { @@ -174,7 +185,7 @@ export const ReasoningTimelineBlock: React.FC = ({ const animation = animate( element, - { height: isExpanded ? 'auto' : '0px', opacity: isExpanded ? 1 : 0 }, + { height: effectiveIsExpanded ? 'auto' : '0px', opacity: effectiveIsExpanded ? 1 : 0 }, EXPANDED_CONTENT_SPRING, ); contentAnimationRef.current = animation; @@ -184,7 +195,7 @@ export const ReasoningTimelineBlock: React.FC = ({ return; } contentAnimationRef.current = null; - if (isExpanded) { + if (effectiveIsExpanded) { element.style.overflow = 'visible'; element.style.height = 'auto'; } else { @@ -198,7 +209,7 @@ export const ReasoningTimelineBlock: React.FC = ({ contentAnimationRef.current = null; } }; - }, [isExpanded]); + }, [effectiveIsExpanded, hasEnded]); React.useEffect(() => { return () => { @@ -242,7 +253,7 @@ export const ReasoningTimelineBlock: React.FC = ({
= ({
@@ -266,12 +277,12 @@ export const ReasoningTimelineBlock: React.FC = ({
- {isExpanded ? : } + {effectiveIsExpanded ? : }
@@ -280,7 +291,7 @@ export const ReasoningTimelineBlock: React.FC = ({ {t(variant === 'justification' ? 'chat.reasoningTrace.justification' : 'chat.reasoningTrace.thinking')} - ) : isExpanded ? ( + ) : effectiveIsExpanded ? ( = ({
- {!isStreaming && !isExpanded && summary ? ( + {!isStreaming && !effectiveIsExpanded && summary ? ( = ({