diff --git a/studio/frontend/src/components/assistant-ui/reasoning.tsx b/studio/frontend/src/components/assistant-ui/reasoning.tsx index 4f3f8075a4..157a08297a 100644 --- a/studio/frontend/src/components/assistant-ui/reasoning.tsx +++ b/studio/frontend/src/components/assistant-ui/reasoning.tsx @@ -17,7 +17,6 @@ import { type ReasoningGroupComponent, type ReasoningMessagePartComponent, useAuiState, - useScrollLock, } from "@assistant-ui/react"; import { copyToClipboard } from "@/lib/copy-to-clipboard"; import { Idea01Icon } from "@hugeicons/core-free-icons"; @@ -67,29 +66,23 @@ function ReasoningRoot({ children, ...props }: ReasoningRootProps) { - const collapsibleRef = useRef(null); const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen); - const lockScroll = useScrollLock(collapsibleRef, ANIMATION_DURATION); const isControlled = controlledOpen !== undefined; const isOpen = isControlled ? controlledOpen : uncontrolledOpen; const handleOpenChange = useCallback( (open: boolean) => { - if (!open) { - lockScroll(); - } if (!isControlled) { setUncontrolledOpen(open); } controlledOnOpenChange?.(open); }, - [lockScroll, isControlled, controlledOnOpenChange], + [isControlled, controlledOnOpenChange], ); return ( & { streaming?: boolean }) { const scrollRef = useRef(null); + const shouldAutoScrollRef = useRef(true); useEffect(() => { if (!(streaming && scrollRef.current)) { return; } const el = scrollRef.current; + const updateAutoScroll = () => { + const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight; + shouldAutoScrollRef.current = distanceFromBottom <= 24; + }; const observer = new MutationObserver(() => { - el.scrollTop = el.scrollHeight; + if (shouldAutoScrollRef.current) { + el.scrollTop = el.scrollHeight; + } }); + el.addEventListener("scroll", updateAutoScroll); observer.observe(el, { childList: true, subtree: true, characterData: true, }); + shouldAutoScrollRef.current = true; el.scrollTop = el.scrollHeight; - return () => observer.disconnect(); + return () => { + observer.disconnect(); + el.removeEventListener("scroll", updateAutoScroll); + }; }, [streaming]); return ( @@ -330,6 +335,7 @@ const ReasoningGroupImpl: ReasoningGroupComponent = ({ }); const [manualOpen, setManualOpen] = useState(false); + const [dismissedWhileStreaming, setDismissedWhileStreaming] = useState(false); const [duration, setDuration] = useState(0); const startTimeRef = useRef(null); @@ -345,17 +351,23 @@ const ReasoningGroupImpl: ReasoningGroupComponent = ({ } }, [isReasoningStreaming]); - const isOpen = isReasoningStreaming || manualOpen; + // Reset dismissed flag when a new stream starts + useEffect(() => { + if (isReasoningStreaming) { + setDismissedWhileStreaming(false); + } + }, [isReasoningStreaming]); - const variant = isReasoningStreaming - ? "outline" - : manualOpen - ? "outline" - : "ghost"; + // Derived: open during streaming (unless dismissed), or if user manually opened after + const isOpen = (isReasoningStreaming && !dismissedWhileStreaming) || manualOpen; + const variant = isOpen ? "outline" : "ghost"; + // Allow closing during streaming (matches ChatGPT) const handleOpenChange = useCallback( (open: boolean) => { - if (!isReasoningStreaming) { + if (isReasoningStreaming) { + setDismissedWhileStreaming(!open); + } else { setManualOpen(open); } }, @@ -368,14 +380,17 @@ const ReasoningGroupImpl: ReasoningGroupComponent = ({ onOpenChange={handleOpenChange} variant={variant} > -
+
- {isOpen && !isReasoningStreaming && ( - - )} +
+ {isOpen && !isReasoningStreaming && ( + + )} +