mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-19 12:24:46 +08:00
feat: enhance Replay mode
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
// 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 { FastForward } from "lucide-react";
|
import { motion } from "framer-motion";
|
||||||
|
import { FastForward, Play } from "lucide-react";
|
||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
@@ -21,11 +22,13 @@ import { ConversationStarter } from "./conversation-starter";
|
|||||||
import { InputBox } from "./input-box";
|
import { InputBox } from "./input-box";
|
||||||
import { MessageListView } from "./message-list-view";
|
import { MessageListView } from "./message-list-view";
|
||||||
import { RainbowText } from "./rainbow-text";
|
import { RainbowText } from "./rainbow-text";
|
||||||
|
import { Welcome } from "./welcome";
|
||||||
|
|
||||||
export function MessagesBlock({ className }: { className?: string }) {
|
export function MessagesBlock({ className }: { className?: string }) {
|
||||||
const messageCount = useStore((state) => state.messageIds.length);
|
const messageCount = useStore((state) => state.messageIds.length);
|
||||||
const responding = useStore((state) => state.responding);
|
const responding = useStore((state) => state.responding);
|
||||||
const { isReplay } = useReplay();
|
const { isReplay } = useReplay();
|
||||||
|
const [replayStarted, setReplayStarted] = useState(false);
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
const [feedback, setFeedback] = useState<{ option: Option } | null>(null);
|
const [feedback, setFeedback] = useState<{ option: Option } | null>(null);
|
||||||
const handleSend = useCallback(
|
const handleSend = useCallback(
|
||||||
@@ -61,6 +64,10 @@ export function MessagesBlock({ className }: { className?: string }) {
|
|||||||
const handleRemoveFeedback = useCallback(() => {
|
const handleRemoveFeedback = useCallback(() => {
|
||||||
setFeedback(null);
|
setFeedback(null);
|
||||||
}, [setFeedback]);
|
}, [setFeedback]);
|
||||||
|
const handleStartReplay = useCallback(() => {
|
||||||
|
setReplayStarted(true);
|
||||||
|
void sendMessage();
|
||||||
|
}, [setReplayStarted]);
|
||||||
const [fastForwarding, setFastForwarding] = useState(false);
|
const [fastForwarding, setFastForwarding] = useState(false);
|
||||||
const handleFastForwardReplay = useCallback(() => {
|
const handleFastForwardReplay = useCallback(() => {
|
||||||
setFastForwarding(!fastForwarding);
|
setFastForwarding(!fastForwarding);
|
||||||
@@ -91,17 +98,42 @@ export function MessagesBlock({ className }: { className?: string }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-42 w-full items-center justify-center">
|
<>
|
||||||
<Card className="w-full">
|
<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 items-center justify-between">
|
||||||
<div className="flex-grow">
|
<div className="flex-grow">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
<RainbowText animated={responding}>Replay Mode</RainbowText>
|
<RainbowText animated={responding}>
|
||||||
|
Replay Mode
|
||||||
|
</RainbowText>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
<RainbowText animated={responding}>
|
<RainbowText animated={responding}>
|
||||||
DeerFlow is now replaying the conversation...
|
{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>
|
</RainbowText>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -113,14 +145,21 @@ export function MessagesBlock({ className }: { className?: string }) {
|
|||||||
variant={fastForwarding ? "default" : "outline"}
|
variant={fastForwarding ? "default" : "outline"}
|
||||||
onClick={handleFastForwardReplay}
|
onClick={handleFastForwardReplay}
|
||||||
>
|
>
|
||||||
Fast Forward
|
|
||||||
<FastForward size={16} />
|
<FastForward size={16} />
|
||||||
|
Fast Forward
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!replayStarted && (
|
||||||
|
<Button onClick={handleStartReplay}>
|
||||||
|
<Play size={16} />
|
||||||
|
Start
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</motion.div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
73
web/src/app/app.tsx
Normal file
73
web/src/app/app.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,76 +3,15 @@
|
|||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { GithubOutlined } from "@ant-design/icons";
|
import dynamic from "next/dynamic";
|
||||||
import Link from "next/link";
|
import { Suspense } from "react";
|
||||||
import { useEffect, useMemo } from "react";
|
|
||||||
|
|
||||||
import { Button } from "~/components/ui/button";
|
const App = dynamic(() => import("./app"), { ssr: false });
|
||||||
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";
|
|
||||||
|
|
||||||
export default function HomePage() {
|
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 (
|
return (
|
||||||
<div className="flex h-full w-full justify-center">
|
<Suspense fallback={<div>Loading DeerFlow...</div>}>
|
||||||
<header className="fixed top-0 left-0 flex h-12 w-full w-screen items-center justify-between px-4">
|
<App />
|
||||||
<Logo />
|
</Suspense>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user