From 83126a7e65d4cee24c6b225bcf66d63952c3fe0a Mon Sep 17 00:00:00 2001 From: Will Chen Date: Thu, 9 Oct 2025 11:32:06 -0700 Subject: [PATCH] Add scroll to bottom button (#1484) Based on #1425 --- > [!NOTE] > Adds a floating scroll-to-bottom button and refines scroll/auto-scroll behavior with layout tweaks to support an overlay. > > - **Chat UI**: > - **Scroll-to-bottom button**: Adds floating button in `ChatPanel` (uses `Button` and `ArrowDown`) that appears when scrolled away and scrolls smoothly to the latest message. > - **Scroll logic**: Introduces `getDistanceFromBottom`, `isNearBottom`, and a `scrollAwayThreshold`; auto-scroll now triggers only when near the bottom; refactors `handleScroll` with `useCallback` and longer idle timeout. > - **Layout**: Wraps `MessagesList` in a `relative` container and renders a centered absolute button overlay; adjusts `MessagesList` root to `absolute inset-0` for proper overlay behavior. > - **Misc**: Updates effect dependencies and console log message related to streaming/scrolling. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 2e1b844830ae26cfc40840b9e8216fefad112a5e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). Co-authored-by: Md Rakibul Islam Rocky --- src/components/ChatPanel.tsx | 85 +++++++++++++++++++++------- src/components/chat/MessagesList.tsx | 2 +- 2 files changed, 64 insertions(+), 23 deletions(-) diff --git a/src/components/ChatPanel.tsx b/src/components/ChatPanel.tsx index 1e3385b..01c7e91 100644 --- a/src/components/ChatPanel.tsx +++ b/src/components/ChatPanel.tsx @@ -11,6 +11,8 @@ import { MessagesList } from "./chat/MessagesList"; import { ChatInput } from "./chat/ChatInput"; import { VersionPane } from "./chat/VersionPane"; import { ChatError } from "./chat/ChatError"; +import { Button } from "@/components/ui/button"; +import { ArrowDown } from "lucide-react"; interface ChatPanelProps { chatId?: number; @@ -35,21 +37,44 @@ export function ChatPanel({ // Scroll-related properties const [isUserScrolling, setIsUserScrolling] = useState(false); + const [showScrollButton, setShowScrollButton] = useState(false); const userScrollTimeoutRef = useRef(null); const lastScrollTopRef = useRef(0); - const scrollToBottom = (behavior: ScrollBehavior = "smooth") => { messagesEndRef.current?.scrollIntoView({ behavior }); }; - const handleScroll = () => { + const handleScrollButtonClick = () => { + if (!messagesContainerRef.current) return; + + scrollToBottom("smooth"); + }; + + const getDistanceFromBottom = () => { + if (!messagesContainerRef.current) return 0; + const container = messagesContainerRef.current; + return ( + container.scrollHeight - (container.scrollTop + container.clientHeight) + ); + }; + + const isNearBottom = (threshold: number = 100) => { + return getDistanceFromBottom() <= threshold; + }; + + const scrollAwayThreshold = 150; // pixels from bottom to consider "scrolled away" + + const handleScroll = useCallback(() => { if (!messagesContainerRef.current) return; const container = messagesContainerRef.current; - const currentScrollTop = container.scrollTop; + const distanceFromBottom = + container.scrollHeight - (container.scrollTop + container.clientHeight); - if (currentScrollTop < lastScrollTopRef.current) { + // User has scrolled away from bottom + if (distanceFromBottom > scrollAwayThreshold) { setIsUserScrolling(true); + setShowScrollButton(true); if (userScrollTimeoutRef.current) { window.clearTimeout(userScrollTimeoutRef.current); @@ -57,15 +82,18 @@ export function ChatPanel({ userScrollTimeoutRef.current = window.setTimeout(() => { setIsUserScrolling(false); - }, 1000); + }, 2000); // Increased timeout to 2 seconds + } else { + // User is near bottom + setIsUserScrolling(false); + setShowScrollButton(false); } - - lastScrollTopRef.current = currentScrollTop; - }; + lastScrollTopRef.current = container.scrollTop; + }, []); useEffect(() => { const streamCount = chatId ? (streamCountById.get(chatId) ?? 0) : 0; - console.log("streamCount", streamCount); + console.log("streamCount - scrolling to bottom", streamCount); scrollToBottom(); }, [chatId, chatId ? (streamCountById.get(chatId) ?? 0) : 0]); @@ -83,7 +111,7 @@ export function ChatPanel({ window.clearTimeout(userScrollTimeoutRef.current); } }; - }, []); + }, [handleScroll]); const fetchChatMessages = useCallback(async () => { if (!chatId) { @@ -110,13 +138,8 @@ export function ChatPanel({ messagesContainerRef.current && messages.length > 0 ) { - const { scrollTop, clientHeight, scrollHeight } = - messagesContainerRef.current; - const threshold = 280; - const isNearBottom = - scrollHeight - (scrollTop + clientHeight) <= threshold; - - if (isNearBottom) { + // Only auto-scroll if user is close to bottom + if (isNearBottom(280)) { requestAnimationFrame(() => { scrollToBottom("instant"); }); @@ -135,11 +158,29 @@ export function ChatPanel({
{!isVersionPaneOpen && (
- +
+ + + {/* Scroll to bottom button */} + {showScrollButton && ( +
+ +
+ )} +
+ setError(null)} />
diff --git a/src/components/chat/MessagesList.tsx b/src/components/chat/MessagesList.tsx index cf36521..61ca90f 100644 --- a/src/components/chat/MessagesList.tsx +++ b/src/components/chat/MessagesList.tsx @@ -54,7 +54,7 @@ export const MessagesList = forwardRef( return (