diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx index 458af8590..99f133889 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx @@ -20,13 +20,16 @@ export default function ChatHistory({ regenerateAssistantMessage, hasAttachments = false, }) { + const lastScrollTopRef = useRef(0); const { user } = useUser(); const { threadSlug = null } = useParams(); const { showing, showModal, hideModal } = useManageWorkspaceModal(); const [isAtBottom, setIsAtBottom] = useState(true); const chatHistoryRef = useRef(null); const [textSize, setTextSize] = useState("normal"); + const [isUserScrolling, setIsUserScrolling] = useState(false); const showScrollbar = Appearance.getSettings()?.showScrollbar || false; + const isStreaming = history[history.length - 1]?.animate; const getTextSizeClass = (size) => { switch (size) { @@ -58,35 +61,44 @@ export default function ChatHistory({ }, []); useEffect(() => { - if (isAtBottom) scrollToBottom(); - }, [history]); + if (!isUserScrolling && (isAtBottom || isStreaming)) { + scrollToBottom(false); // Use instant scroll for auto-scrolling + } + }, [history, isAtBottom, isStreaming, isUserScrolling]); + + const handleScroll = (e) => { + const { scrollTop, scrollHeight, clientHeight } = e.target; + const isBottom = scrollHeight - scrollTop === clientHeight; + + // Detect if this is a user-initiated scroll + if (Math.abs(scrollTop - lastScrollTopRef.current) > 10) { + setIsUserScrolling(!isBottom); + } - const handleScroll = () => { - const diff = - chatHistoryRef.current.scrollHeight - - chatHistoryRef.current.scrollTop - - chatHistoryRef.current.clientHeight; - // Fuzzy margin for what qualifies as "bottom". Stronger than straight comparison since that may change over time. - const isBottom = diff <= 10; setIsAtBottom(isBottom); + lastScrollTopRef.current = scrollTop; }; const debouncedScroll = debounce(handleScroll, 100); + useEffect(() => { - function watchScrollEvent() { - if (!chatHistoryRef.current) return null; - const chatHistoryElement = chatHistoryRef.current; - if (!chatHistoryElement) return null; + const chatHistoryElement = chatHistoryRef.current; + if (chatHistoryElement) { chatHistoryElement.addEventListener("scroll", debouncedScroll); + return () => + chatHistoryElement.removeEventListener("scroll", debouncedScroll); } - watchScrollEvent(); }, []); - const scrollToBottom = () => { + const scrollToBottom = (smooth = false) => { if (chatHistoryRef.current) { chatHistoryRef.current.scrollTo({ top: chatHistoryRef.current.scrollHeight, - behavior: "smooth", + + // Smooth is on when user clicks the button but disabled during auto scroll + // We must disable this during auto scroll because it causes issues with + // detecting when we are at the bottom of the chat. + ...(smooth ? { behavior: "smooth" } : {}), }); } }; @@ -197,6 +209,7 @@ export default function ChatHistory({ }`} id="chat-history" ref={chatHistoryRef} + onScroll={handleScroll} > {history.map((props, index) => { const isLastBotReply = @@ -251,12 +264,14 @@ export default function ChatHistory({ {!isAtBottom && (
-
- +
{ + scrollToBottom(true); + setIsUserScrolling(false); + }} + > +