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";
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<number | null>(null);
const lastScrollTopRef = useRef<number>(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({
<div className="flex flex-1 overflow-hidden">
{!isVersionPaneOpen && (
<div className="flex-1 flex flex-col min-w-0">
<div className="flex-1 relative overflow-hidden">
<MessagesList
messages={messages}
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)} />
<ChatInput chatId={chatId} />
</div>
......
......@@ -54,7 +54,7 @@ export const MessagesList = forwardRef<HTMLDivElement, MessagesListProps>(
return (
<div
className="flex-1 overflow-y-auto p-4"
className="absolute inset-0 overflow-y-auto p-4"
ref={ref}
data-testid="messages-list"
>
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论