From 99fdd91a01e0d3a595363f9938b3fbae165b6c22 Mon Sep 17 00:00:00 2001 From: Debanjum Date: Tue, 27 May 2025 17:53:25 -0700 Subject: [PATCH] Latch to bottom instantly and well when auto scroll chat stream on web --- .../components/chatHistory/chatHistory.tsx | 43 ++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/src/interface/web/app/components/chatHistory/chatHistory.tsx b/src/interface/web/app/components/chatHistory/chatHistory.tsx index 41989eb9..711f5a98 100644 --- a/src/interface/web/app/components/chatHistory/chatHistory.tsx +++ b/src/interface/web/app/components/chatHistory/chatHistory.tsx @@ -125,6 +125,7 @@ export default function ChatHistory(props: ChatHistoryProps) { const [currentTurnId, setCurrentTurnId] = useState(null); const sentinelRef = useRef(null); const scrollAreaRef = useRef(null); + const scrollableContentWrapperRef = useRef(null); const latestUserMessageRef = useRef(null); const latestFetchedMessageRef = useRef(null); @@ -151,16 +152,46 @@ export default function ChatHistory(props: ChatHistoryProps) { }; scrollAreaEl.addEventListener("scroll", detectIsNearBottom); + detectIsNearBottom(); // Initial check return () => scrollAreaEl.removeEventListener("scroll", detectIsNearBottom); - }, []); + }, [scrollAreaRef]); // Auto scroll while incoming message is streamed useEffect(() => { if (props.incomingMessages && props.incomingMessages.length > 0 && isNearBottom) { - scrollToBottom(); + scrollToBottom(true); } }, [props.incomingMessages, isNearBottom]); + // ResizeObserver to handle content height changes (e.g., images loading) + useEffect(() => { + const contentWrapper = scrollableContentWrapperRef.current; + const scrollViewport = scrollAreaRef.current?.querySelector(scrollAreaSelector); + + if (!contentWrapper || !scrollViewport) return; + + const observer = new ResizeObserver(() => { + // Check current scroll position to decide if auto-scroll is warranted + const { scrollTop, scrollHeight, clientHeight } = scrollViewport; + const bottomThreshold = 50; + const currentlyNearBottom = (scrollHeight - (scrollTop + clientHeight)) <= bottomThreshold; + + if (currentlyNearBottom) { + // Only auto-scroll if there are incoming messages being processed + if (props.incomingMessages && props.incomingMessages.length > 0) { + const lastMessage = props.incomingMessages[props.incomingMessages.length - 1]; + // If the last message is not completed, or it just completed (indicated by incompleteIncomingMessageIndex still being set) + if (!lastMessage.completed || (lastMessage.completed && incompleteIncomingMessageIndex !== null)) { + scrollToBottom(true); // Use instant scroll + } + } + } + }); + + observer.observe(contentWrapper); + return () => observer.disconnect(); + }, [props.incomingMessages, incompleteIncomingMessageIndex, scrollAreaRef]); // Dependencies + // Scroll to most recent user message after the first page of chat messages is loaded. useEffect(() => { if (data && data.chat && data.chat.length > 0 && currentPage < 2) { @@ -297,7 +328,10 @@ export default function ChatHistory(props: ChatHistoryProps) { behavior: instant ? "auto" : "smooth", }); }); - setIsNearBottom(true); + // Optimistically set, the scroll listener will verify + if (instant || scrollAreaEl && (scrollAreaEl.scrollHeight - (scrollAreaEl.scrollTop + scrollAreaEl.clientHeight)) < 5) { + setIsNearBottom(true); + } }; function constructAgentLink() { @@ -356,7 +390,7 @@ export default function ChatHistory(props: ChatHistoryProps) { `} ref={scrollAreaRef} > -
+
{fetchingData && } @@ -519,7 +553,6 @@ export default function ChatHistory(props: ChatHistoryProps) { className="absolute bottom-0 right-0 bg-white dark:bg-[hsl(var(--background))] text-neutral-500 dark:text-white p-2 rounded-full shadow-xl" onClick={() => { scrollToBottom(); - setIsNearBottom(true); }} >