feat: enhance Replay mode

This commit is contained in:
Li Xin
2025-04-25 11:59:36 +08:00
parent e578c90bb6
commit d68d3ee67a
3 changed files with 148 additions and 97 deletions

View File

@@ -1,7 +1,8 @@
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
// SPDX-License-Identifier: MIT
import { FastForward } from "lucide-react";
import { motion } from "framer-motion";
import { FastForward, Play } from "lucide-react";
import { useCallback, useRef, useState } from "react";
import { Button } from "~/components/ui/button";
@@ -21,11 +22,13 @@ import { ConversationStarter } from "./conversation-starter";
import { InputBox } from "./input-box";
import { MessageListView } from "./message-list-view";
import { RainbowText } from "./rainbow-text";
import { Welcome } from "./welcome";
export function MessagesBlock({ className }: { className?: string }) {
const messageCount = useStore((state) => state.messageIds.length);
const responding = useStore((state) => state.responding);
const { isReplay } = useReplay();
const [replayStarted, setReplayStarted] = useState(false);
const abortControllerRef = useRef<AbortController | null>(null);
const [feedback, setFeedback] = useState<{ option: Option } | null>(null);
const handleSend = useCallback(
@@ -61,6 +64,10 @@ export function MessagesBlock({ className }: { className?: string }) {
const handleRemoveFeedback = useCallback(() => {
setFeedback(null);
}, [setFeedback]);
const handleStartReplay = useCallback(() => {
setReplayStarted(true);
void sendMessage();
}, [setReplayStarted]);
const [fastForwarding, setFastForwarding] = useState(false);
const handleFastForwardReplay = useCallback(() => {
setFastForwarding(!fastForwarding);
@@ -91,36 +98,68 @@ export function MessagesBlock({ className }: { className?: string }) {
/>
</div>
) : (
<div className="flex h-42 w-full items-center justify-center">
<Card className="w-full">
<div className="flex items-center justify-between">
<div className="flex-grow">
<CardHeader>
<CardTitle>
<RainbowText animated={responding}>Replay Mode</RainbowText>
</CardTitle>
<CardDescription>
<RainbowText animated={responding}>
DeerFlow is now replaying the conversation...
</RainbowText>
</CardDescription>
</CardHeader>
<>
<div
className={cn(
"fixed bottom-[calc(50vh+80px)] left-0 transition-all duration-500 ease-out",
replayStarted && "pointer-events-none scale-150 opacity-0",
)}
>
<Welcome />
</div>
<motion.div
className="h-42 w-full items-center justify-center transition-all duration-300"
initial={{ opacity: 0, y: 200 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<Card
className={cn(
"w-full transition-all duration-300",
!replayStarted && "translate-y-[-40vh]",
)}
>
<div className="flex items-center justify-between">
<div className="flex-grow">
<CardHeader>
<CardTitle>
<RainbowText animated={responding}>
Replay Mode
</RainbowText>
</CardTitle>
<CardDescription>
<RainbowText animated={responding}>
{responding
? "DeerFlow is now replaying the conversation..."
: replayStarted
? "The replay has been stopped."
: `You're now in DeerFlow's replay mode. Click the start button on the right to replay.`}
</RainbowText>
</CardDescription>
</CardHeader>
</div>
<div className="pr-4">
{responding && (
<Button
className={cn(fastForwarding && "animate-pulse")}
variant={fastForwarding ? "default" : "outline"}
onClick={handleFastForwardReplay}
>
<FastForward size={16} />
Fast Forward
</Button>
)}
{!replayStarted && (
<Button onClick={handleStartReplay}>
<Play size={16} />
Start
</Button>
)}
</div>
</div>
<div className="pr-4">
{responding && (
<Button
className={cn(fastForwarding && "animate-pulse")}
variant={fastForwarding ? "default" : "outline"}
onClick={handleFastForwardReplay}
>
Fast Forward
<FastForward size={16} />
</Button>
)}
</div>
</div>
</Card>
</div>
</Card>
</motion.div>
</>
)}
</div>
);

73
web/src/app/app.tsx Normal file
View File

@@ -0,0 +1,73 @@
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
// SPDX-License-Identifier: MIT
"use client";
import { GithubOutlined } from "@ant-design/icons";
import Link from "next/link";
import { useMemo } from "react";
import { Button } from "~/components/ui/button";
import { useReplay } from "~/core/replay";
import { useStore } from "~/core/store";
import { cn } from "~/lib/utils";
import { Logo } from "./_components/logo";
import { MessagesBlock } from "./_components/messages-block";
import { ResearchBlock } from "./_components/research-block";
import { ThemeToggle } from "./_components/theme-toggle";
import { Tooltip } from "./_components/tooltip";
import { SettingsDialog } from "./_settings/dialogs/settings-dialog";
export default function App() {
const { isReplay } = useReplay();
const openResearchId = useStore((state) => state.openResearchId);
const doubleColumnMode = useMemo(
() => openResearchId !== null,
[openResearchId],
);
return (
<div className="flex h-full w-full justify-center">
<header className="fixed top-0 left-0 flex h-12 w-full w-screen items-center justify-between px-4">
<Logo />
<div className="flex items-center">
<Tooltip title="Visit DeerFlow on GitHub">
<Button variant="ghost" size="icon" asChild>
<Link
href="https://github.com/bytedance/deer-flow"
target="_blank"
>
<GithubOutlined />
</Link>
</Button>
</Tooltip>
<ThemeToggle />
{!isReplay && <SettingsDialog />}
</div>
</header>
<div
className={cn(
"flex h-full w-full justify-center px-4 pt-12 pb-4",
doubleColumnMode && "gap-8",
)}
>
<MessagesBlock
className={cn(
"shrink-0 transition-all duration-300 ease-out",
!doubleColumnMode &&
`w-[768px] translate-x-[min(calc((100vw-538px)*0.75/2),960px/2)]`,
doubleColumnMode && `w-[538px]`,
)}
/>
<ResearchBlock
className={cn(
"w-[min(calc((100vw-538px)*0.75),960px)] pb-4 transition-all duration-300 ease-out",
!doubleColumnMode && "scale-0",
doubleColumnMode && "",
)}
researchId={openResearchId}
/>
</div>
</div>
);
}

View File

@@ -3,76 +3,15 @@
"use client";
import { GithubOutlined } from "@ant-design/icons";
import Link from "next/link";
import { useEffect, useMemo } from "react";
import dynamic from "next/dynamic";
import { Suspense } from "react";
import { Button } from "~/components/ui/button";
import { useReplay } from "~/core/replay";
import { sendMessage, useStore } from "~/core/store";
import { cn } from "~/lib/utils";
import { Logo } from "./_components/logo";
import { MessagesBlock } from "./_components/messages-block";
import { ResearchBlock } from "./_components/research-block";
import { ThemeToggle } from "./_components/theme-toggle";
import { Tooltip } from "./_components/tooltip";
import { SettingsDialog } from "./_settings/dialogs/settings-dialog";
const App = dynamic(() => import("./app"), { ssr: false });
export default function HomePage() {
const { isReplay } = useReplay();
const openResearchId = useStore((state) => state.openResearchId);
const doubleColumnMode = useMemo(
() => openResearchId !== null,
[openResearchId],
);
useEffect(() => {
if (isReplay) {
void sendMessage();
}
}, [isReplay]);
return (
<div className="flex h-full w-full justify-center">
<header className="fixed top-0 left-0 flex h-12 w-full w-screen items-center justify-between px-4">
<Logo />
<div className="flex items-center">
<Tooltip title="Visit DeerFlow on GitHub">
<Button variant="ghost" size="icon" asChild>
<Link
href="https://github.com/bytedance/deer-flow"
target="_blank"
>
<GithubOutlined />
</Link>
</Button>
</Tooltip>
<ThemeToggle />
{!isReplay && <SettingsDialog />}
</div>
</header>
<div
className={cn(
"flex h-full w-full justify-center px-4 pt-12 pb-4",
doubleColumnMode && "gap-8",
)}
>
<MessagesBlock
className={cn(
"shrink-0 transition-all duration-300 ease-out",
!doubleColumnMode &&
`w-[768px] translate-x-[min(calc((100vw-538px)*0.75/2),960px/2)]`,
doubleColumnMode && `w-[538px]`,
)}
/>
<ResearchBlock
className={cn(
"w-[min(calc((100vw-538px)*0.75),960px)] pb-4 transition-all duration-300 ease-out",
!doubleColumnMode && "scale-0",
doubleColumnMode && "",
)}
researchId={openResearchId}
/>
</div>
</div>
<Suspense fallback={<div>Loading DeerFlow...</div>}>
<App />
</Suspense>
);
}