mirror of
https://git.mirrors.martin98.com/https://github.com/bytedance/deer-flow
synced 2025-08-19 18:29:06 +08:00
fix: auto-scrolling to the bottom occasionally fails when toggling research (#7)
This commit is contained in:
parent
1f5197501d
commit
9260c84005
@ -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>
|
||||||
|
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user