From 9260c84005bd45a1e015a1c2d62adfc12c7b5594 Mon Sep 17 00:00:00 2001 From: Nonoroazoro Date: Thu, 8 May 2025 19:49:56 +0800 Subject: [PATCH] fix: auto-scrolling to the bottom occasionally fails when toggling research (#7) --- .../app/chat/components/message-list-view.tsx | 38 ++++++++++++++----- .../components/deer-flow/scroll-container.tsx | 37 ++++++++++++------ 2 files changed, 53 insertions(+), 22 deletions(-) diff --git a/web/src/app/chat/components/message-list-view.tsx b/web/src/app/chat/components/message-list-view.tsx index 29ca98a..b925835 100644 --- a/web/src/app/chat/components/message-list-view.tsx +++ b/web/src/app/chat/components/message-list-view.tsx @@ -4,13 +4,13 @@ import { LoadingOutlined } from "@ant-design/icons"; import { motion } from "framer-motion"; import { Download, Headphones } from "lucide-react"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; import { LoadingAnimation } from "~/components/deer-flow/loading-animation"; import { Markdown } from "~/components/deer-flow/markdown"; import { RainbowText } from "~/components/deer-flow/rainbow-text"; import { RollingText } from "~/components/deer-flow/rolling-text"; -import { ScrollContainer } from "~/components/deer-flow/scroll-container"; +import { ScrollContainer, type ScrollContainerRef } from "~/components/deer-flow/scroll-container"; import { Tooltip } from "~/components/deer-flow/tooltip"; import { Button } from "~/components/ui/button"; import { @@ -43,6 +43,7 @@ export function MessageListView({ options?: { interruptFeedback?: string }, ) => void; }) { + const scrollContainerRef = useRef(null); const messageIds = useStore((state) => state.messageIds); const interruptMessage = useStore((state) => { if (messageIds.length >= 2) { @@ -72,11 +73,23 @@ export function MessageListView({ (state) => state.ongoingResearchId === state.openResearchId, ); + const handleToggleResearch = useCallback(() => { + // Fix the issue where auto-scrolling to the bottom + // occasionally fails when toggling research. + const timer = setTimeout(() => { + if (scrollContainerRef.current) { + scrollContainerRef.current.scrollToBottom(); + } + }, 500); + return () => { clearTimeout(timer); }; + }, []); + return (
    {messageIds.map((messageId) => ( @@ -87,6 +100,7 @@ export function MessageListView({ interruptMessage={interruptMessage} onFeedback={onFeedback} onSendMessage={onSendMessage} + onToggleResearch={handleToggleResearch} /> ))}
    @@ -105,6 +119,7 @@ function MessageListItem({ interruptMessage, onFeedback, onSendMessage, + onToggleResearch }: { className?: string; messageId: string; @@ -115,6 +130,7 @@ function MessageListItem({ message: string, options?: { interruptFeedback?: string }, ) => void; + onToggleResearch?: () => void; }) { const message = useMessage(messageId); const startOfResearch = useStore((state) => @@ -150,7 +166,7 @@ function MessageListItem({ } else if (startOfResearch) { content = (
    - +
    ); } else { @@ -205,7 +221,7 @@ function MessageListItem({ className={cn( `flex w-fit max-w-[85%] flex-col rounded-2xl px-4 py-3 shadow`, message.role === "user" && - "text-primary-foreground bg-brand rounded-ee-none", + "text-primary-foreground bg-brand rounded-ee-none", message.role === "assistant" && "bg-card rounded-es-none", className, )} @@ -218,9 +234,11 @@ function MessageListItem({ function ResearchCard({ className, researchId, + onToggleResearch }: { className?: string; researchId: string; + onToggleResearch?: () => void; }) { const reportId = useStore((state) => state.researchReportIds.get(researchId), @@ -245,7 +263,8 @@ function MessageListItem({ } else { openResearch(researchId); } - }, [openResearchId, researchId]); + onToggleResearch?.(); + }, [openResearchId, researchId, onToggleResearch]); return ( @@ -314,11 +333,10 @@ function PlanCard({ - {`### ${ - plan.title !== undefined && plan.title !== "" - ? plan.title - : "Deep Research" - }`} + {`### ${plan.title !== undefined && plan.title !== "" + ? plan.title + : "Deep Research" + }`} diff --git a/web/src/components/deer-flow/scroll-container.tsx b/web/src/components/deer-flow/scroll-container.tsx index ecc7d01..795bdee 100644 --- a/web/src/components/deer-flow/scroll-container.tsx +++ b/web/src/components/deer-flow/scroll-container.tsx @@ -1,31 +1,44 @@ // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates // SPDX-License-Identifier: MIT -import { useEffect, useRef } from "react"; +import { useEffect, useImperativeHandle, useRef, type ReactNode, type RefObject } from "react"; import { useStickToBottom } from "use-stick-to-bottom"; import { ScrollArea } from "~/components/ui/scroll-area"; import { cn } from "~/lib/utils"; +export interface ScrollContainerProps { + className?: string; + children?: ReactNode; + scrollShadow?: boolean; + scrollShadowColor?: string; + autoScrollToBottom?: boolean; + ref?: RefObject; +} + +export interface ScrollContainerRef { + scrollToBottom(): void; +} + export function ScrollContainer({ className, children, scrollShadow = true, scrollShadowColor = "var(--background)", autoScrollToBottom = false, -}: { - className?: string; - children?: React.ReactNode; - scrollShadow?: boolean; - scrollShadowColor?: string; - autoScrollToBottom?: boolean; -}) { - const { scrollRef, contentRef } = useStickToBottom({ - initial: "instant", - }); + ref +}: ScrollContainerProps) { + const { scrollRef, contentRef, scrollToBottom, isAtBottom } = useStickToBottom({ initial: "instant" }); + useImperativeHandle(ref, () => ({ + scrollToBottom() { + if (isAtBottom) { + scrollToBottom(); + } + } + })); + const tempScrollRef = useRef(null); const tempContentRef = useRef(null); - useEffect(() => { if (!autoScrollToBottom) { tempScrollRef.current = scrollRef.current;