fix: auto-scrolling to the bottom occasionally fails when toggling research (#7)

This commit is contained in:
Nonoroazoro 2025-05-08 19:49:56 +08:00 committed by GitHub
parent 1f5197501d
commit 9260c84005
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 53 additions and 22 deletions

View File

@ -4,13 +4,13 @@
import { LoadingOutlined } from "@ant-design/icons"; import { LoadingOutlined } from "@ant-design/icons";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Download, Headphones } from "lucide-react"; 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 { LoadingAnimation } from "~/components/deer-flow/loading-animation";
import { Markdown } from "~/components/deer-flow/markdown"; import { Markdown } from "~/components/deer-flow/markdown";
import { RainbowText } from "~/components/deer-flow/rainbow-text"; import { RainbowText } from "~/components/deer-flow/rainbow-text";
import { RollingText } from "~/components/deer-flow/rolling-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 { Tooltip } from "~/components/deer-flow/tooltip";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { import {
@ -43,6 +43,7 @@ export function MessageListView({
options?: { interruptFeedback?: string }, options?: { interruptFeedback?: string },
) => void; ) => void;
}) { }) {
const scrollContainerRef = useRef<ScrollContainerRef>(null);
const messageIds = useStore((state) => state.messageIds); const messageIds = useStore((state) => state.messageIds);
const interruptMessage = useStore((state) => { const interruptMessage = useStore((state) => {
if (messageIds.length >= 2) { if (messageIds.length >= 2) {
@ -72,11 +73,23 @@ export function MessageListView({
(state) => state.ongoingResearchId === state.openResearchId, (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 ( return (
<ScrollContainer <ScrollContainer
className={cn("flex h-full w-full flex-col overflow-hidden", className)} className={cn("flex h-full w-full flex-col overflow-hidden", className)}
scrollShadowColor="var(--app-background)" scrollShadowColor="var(--app-background)"
autoScrollToBottom autoScrollToBottom
ref={scrollContainerRef}
> >
<ul className="flex flex-col"> <ul className="flex flex-col">
{messageIds.map((messageId) => ( {messageIds.map((messageId) => (
@ -87,6 +100,7 @@ export function MessageListView({
interruptMessage={interruptMessage} interruptMessage={interruptMessage}
onFeedback={onFeedback} onFeedback={onFeedback}
onSendMessage={onSendMessage} onSendMessage={onSendMessage}
onToggleResearch={handleToggleResearch}
/> />
))} ))}
<div className="flex h-8 w-full shrink-0"></div> <div className="flex h-8 w-full shrink-0"></div>
@ -105,6 +119,7 @@ function MessageListItem({
interruptMessage, interruptMessage,
onFeedback, onFeedback,
onSendMessage, onSendMessage,
onToggleResearch
}: { }: {
className?: string; className?: string;
messageId: string; messageId: string;
@ -115,6 +130,7 @@ function MessageListItem({
message: string, message: string,
options?: { interruptFeedback?: string }, options?: { interruptFeedback?: string },
) => void; ) => void;
onToggleResearch?: () => void;
}) { }) {
const message = useMessage(messageId); const message = useMessage(messageId);
const startOfResearch = useStore((state) => const startOfResearch = useStore((state) =>
@ -150,7 +166,7 @@ function MessageListItem({
} else if (startOfResearch) { } else if (startOfResearch) {
content = ( content = (
<div className="w-full px-4"> <div className="w-full px-4">
<ResearchCard researchId={message.id} /> <ResearchCard researchId={message.id} onToggleResearch={onToggleResearch} />
</div> </div>
); );
} else { } else {
@ -205,7 +221,7 @@ function MessageListItem({
className={cn( className={cn(
`flex w-fit max-w-[85%] flex-col rounded-2xl px-4 py-3 shadow`, `flex w-fit max-w-[85%] flex-col rounded-2xl px-4 py-3 shadow`,
message.role === "user" && 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", message.role === "assistant" && "bg-card rounded-es-none",
className, className,
)} )}
@ -218,9 +234,11 @@ function MessageListItem({
function ResearchCard({ function ResearchCard({
className, className,
researchId, researchId,
onToggleResearch
}: { }: {
className?: string; className?: string;
researchId: string; researchId: string;
onToggleResearch?: () => void;
}) { }) {
const reportId = useStore((state) => const reportId = useStore((state) =>
state.researchReportIds.get(researchId), state.researchReportIds.get(researchId),
@ -245,7 +263,8 @@ function MessageListItem({
} else { } else {
openResearch(researchId); openResearch(researchId);
} }
}, [openResearchId, researchId]); onToggleResearch?.();
}, [openResearchId, researchId, onToggleResearch]);
return ( return (
<Card className={cn("w-full", className)}> <Card className={cn("w-full", className)}>
<CardHeader> <CardHeader>
@ -314,11 +333,10 @@ function PlanCard({
<CardHeader> <CardHeader>
<CardTitle> <CardTitle>
<Markdown animate> <Markdown animate>
{`### ${ {`### ${plan.title !== undefined && plan.title !== ""
plan.title !== undefined && plan.title !== "" ? plan.title
? plan.title : "Deep Research"
: "Deep Research" }`}
}`}
</Markdown> </Markdown>
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>

View File

@ -1,31 +1,44 @@
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
// SPDX-License-Identifier: MIT // 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 { useStickToBottom } from "use-stick-to-bottom";
import { ScrollArea } from "~/components/ui/scroll-area"; import { ScrollArea } from "~/components/ui/scroll-area";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
export interface ScrollContainerProps {
className?: string;
children?: ReactNode;
scrollShadow?: boolean;
scrollShadowColor?: string;
autoScrollToBottom?: boolean;
ref?: RefObject<ScrollContainerRef | null>;
}
export interface ScrollContainerRef {
scrollToBottom(): void;
}
export function ScrollContainer({ export function ScrollContainer({
className, className,
children, children,
scrollShadow = true, scrollShadow = true,
scrollShadowColor = "var(--background)", scrollShadowColor = "var(--background)",
autoScrollToBottom = false, autoScrollToBottom = false,
}: { ref
className?: string; }: ScrollContainerProps) {
children?: React.ReactNode; const { scrollRef, contentRef, scrollToBottom, isAtBottom } = useStickToBottom({ initial: "instant" });
scrollShadow?: boolean; useImperativeHandle(ref, () => ({
scrollShadowColor?: string; scrollToBottom() {
autoScrollToBottom?: boolean; if (isAtBottom) {
}) { scrollToBottom();
const { scrollRef, contentRef } = useStickToBottom({ }
initial: "instant", }
}); }));
const tempScrollRef = useRef<HTMLElement>(null); const tempScrollRef = useRef<HTMLElement>(null);
const tempContentRef = useRef<HTMLElement>(null); const tempContentRef = useRef<HTMLElement>(null);
useEffect(() => { useEffect(() => {
if (!autoScrollToBottom) { if (!autoScrollToBottom) {
tempScrollRef.current = scrollRef.current; tempScrollRef.current = scrollRef.current;