Unverified 提交 83126a7e authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Add scroll to bottom button (#1484)

Based on #1425 <!-- CURSOR_SUMMARY --> --- > [!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. > > <sup>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).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: 's avatarMd Rakibul Islam Rocky <mdrirocky08@outlook.com>
上级 9691c983
...@@ -11,6 +11,8 @@ import { MessagesList } from "./chat/MessagesList"; ...@@ -11,6 +11,8 @@ import { MessagesList } from "./chat/MessagesList";
import { ChatInput } from "./chat/ChatInput"; import { ChatInput } from "./chat/ChatInput";
import { VersionPane } from "./chat/VersionPane"; import { VersionPane } from "./chat/VersionPane";
import { ChatError } from "./chat/ChatError"; import { ChatError } from "./chat/ChatError";
import { Button } from "@/components/ui/button";
import { ArrowDown } from "lucide-react";
interface ChatPanelProps { interface ChatPanelProps {
chatId?: number; chatId?: number;
...@@ -35,21 +37,44 @@ export function ChatPanel({ ...@@ -35,21 +37,44 @@ export function ChatPanel({
// Scroll-related properties // Scroll-related properties
const [isUserScrolling, setIsUserScrolling] = useState(false); const [isUserScrolling, setIsUserScrolling] = useState(false);
const [showScrollButton, setShowScrollButton] = useState(false);
const userScrollTimeoutRef = useRef<number | null>(null); const userScrollTimeoutRef = useRef<number | null>(null);
const lastScrollTopRef = useRef<number>(0); const lastScrollTopRef = useRef<number>(0);
const scrollToBottom = (behavior: ScrollBehavior = "smooth") => { const scrollToBottom = (behavior: ScrollBehavior = "smooth") => {
messagesEndRef.current?.scrollIntoView({ behavior }); messagesEndRef.current?.scrollIntoView({ behavior });
}; };
const handleScroll = () => { const handleScrollButtonClick = () => {
if (!messagesContainerRef.current) return; if (!messagesContainerRef.current) return;
scrollToBottom("smooth");
};
const getDistanceFromBottom = () => {
if (!messagesContainerRef.current) return 0;
const container = messagesContainerRef.current; const container = messagesContainerRef.current;
const currentScrollTop = container.scrollTop; 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;
if (currentScrollTop < lastScrollTopRef.current) { const container = messagesContainerRef.current;
const distanceFromBottom =
container.scrollHeight - (container.scrollTop + container.clientHeight);
// User has scrolled away from bottom
if (distanceFromBottom > scrollAwayThreshold) {
setIsUserScrolling(true); setIsUserScrolling(true);
setShowScrollButton(true);
if (userScrollTimeoutRef.current) { if (userScrollTimeoutRef.current) {
window.clearTimeout(userScrollTimeoutRef.current); window.clearTimeout(userScrollTimeoutRef.current);
...@@ -57,15 +82,18 @@ export function ChatPanel({ ...@@ -57,15 +82,18 @@ export function ChatPanel({
userScrollTimeoutRef.current = window.setTimeout(() => { userScrollTimeoutRef.current = window.setTimeout(() => {
setIsUserScrolling(false); setIsUserScrolling(false);
}, 1000); }, 2000); // Increased timeout to 2 seconds
} else {
// User is near bottom
setIsUserScrolling(false);
setShowScrollButton(false);
} }
lastScrollTopRef.current = container.scrollTop;
lastScrollTopRef.current = currentScrollTop; }, []);
};
useEffect(() => { useEffect(() => {
const streamCount = chatId ? (streamCountById.get(chatId) ?? 0) : 0; const streamCount = chatId ? (streamCountById.get(chatId) ?? 0) : 0;
console.log("streamCount", streamCount); console.log("streamCount - scrolling to bottom", streamCount);
scrollToBottom(); scrollToBottom();
}, [chatId, chatId ? (streamCountById.get(chatId) ?? 0) : 0]); }, [chatId, chatId ? (streamCountById.get(chatId) ?? 0) : 0]);
...@@ -83,7 +111,7 @@ export function ChatPanel({ ...@@ -83,7 +111,7 @@ export function ChatPanel({
window.clearTimeout(userScrollTimeoutRef.current); window.clearTimeout(userScrollTimeoutRef.current);
} }
}; };
}, []); }, [handleScroll]);
const fetchChatMessages = useCallback(async () => { const fetchChatMessages = useCallback(async () => {
if (!chatId) { if (!chatId) {
...@@ -110,13 +138,8 @@ export function ChatPanel({ ...@@ -110,13 +138,8 @@ export function ChatPanel({
messagesContainerRef.current && messagesContainerRef.current &&
messages.length > 0 messages.length > 0
) { ) {
const { scrollTop, clientHeight, scrollHeight } = // Only auto-scroll if user is close to bottom
messagesContainerRef.current; if (isNearBottom(280)) {
const threshold = 280;
const isNearBottom =
scrollHeight - (scrollTop + clientHeight) <= threshold;
if (isNearBottom) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
scrollToBottom("instant"); scrollToBottom("instant");
}); });
...@@ -135,11 +158,29 @@ export function ChatPanel({ ...@@ -135,11 +158,29 @@ export function ChatPanel({
<div className="flex flex-1 overflow-hidden"> <div className="flex flex-1 overflow-hidden">
{!isVersionPaneOpen && ( {!isVersionPaneOpen && (
<div className="flex-1 flex flex-col min-w-0"> <div className="flex-1 flex flex-col min-w-0">
<MessagesList <div className="flex-1 relative overflow-hidden">
messages={messages} <MessagesList
messagesEndRef={messagesEndRef} messages={messages}
ref={messagesContainerRef} messagesEndRef={messagesEndRef}
/> ref={messagesContainerRef}
/>
{/* Scroll to bottom button */}
{showScrollButton && (
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 z-10">
<Button
onClick={handleScrollButtonClick}
size="icon"
className="rounded-full shadow-lg hover:shadow-xl transition-all border border-border/50 backdrop-blur-sm bg-background/95 hover:bg-accent"
variant="outline"
title={"Scroll to bottom"}
>
<ArrowDown className="h-4 w-4" />
</Button>
</div>
)}
</div>
<ChatError error={error} onDismiss={() => setError(null)} /> <ChatError error={error} onDismiss={() => setError(null)} />
<ChatInput chatId={chatId} /> <ChatInput chatId={chatId} />
</div> </div>
......
...@@ -54,7 +54,7 @@ export const MessagesList = forwardRef<HTMLDivElement, MessagesListProps>( ...@@ -54,7 +54,7 @@ export const MessagesList = forwardRef<HTMLDivElement, MessagesListProps>(
return ( return (
<div <div
className="flex-1 overflow-y-auto p-4" className="absolute inset-0 overflow-y-auto p-4"
ref={ref} ref={ref}
data-testid="messages-list" data-testid="messages-list"
> >
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论