From cb9201bd3490b471cbc9a6f671f24efafa7a2382 Mon Sep 17 00:00:00 2001 From: Li Xin Date: Thu, 24 Apr 2025 22:15:17 +0800 Subject: [PATCH] feat: enhance replay and fast-forward replay --- web/src/app/_components/messages-block.tsx | 50 ++++++++++++++++------ web/src/app/_components/research-block.tsx | 3 ++ web/src/core/api/chat.ts | 25 ++++++++--- web/src/core/replay/get-replay-id.ts | 7 +-- web/src/core/replay/hooks.ts | 11 +++-- 5 files changed, 68 insertions(+), 28 deletions(-) diff --git a/web/src/app/_components/messages-block.tsx b/web/src/app/_components/messages-block.tsx index ecde6a8..5dc5845 100644 --- a/web/src/app/_components/messages-block.tsx +++ b/web/src/app/_components/messages-block.tsx @@ -1,16 +1,19 @@ // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates // SPDX-License-Identifier: MIT -import { useSearchParams } from "next/navigation"; +import { FastForward } from "lucide-react"; import { useCallback, useRef, useState } from "react"; +import { Button } from "~/components/ui/button"; import { Card, CardDescription, CardHeader, CardTitle, } from "~/components/ui/card"; +import { fastForwardReplay } from "~/core/api"; import type { Option } from "~/core/messages"; +import { useReplay } from "~/core/replay"; import { sendMessage, useStore } from "~/core/store"; import { cn } from "~/lib/utils"; @@ -22,7 +25,7 @@ import { RainbowText } from "./rainbow-text"; export function MessagesBlock({ className }: { className?: string }) { const messageCount = useStore((state) => state.messageIds.length); const responding = useStore((state) => state.responding); - const replaying = useSearchParams().get("replay") !== null; + const { isReplay } = useReplay(); const abortControllerRef = useRef(null); const [feedback, setFeedback] = useState<{ option: Option } | null>(null); const handleSend = useCallback( @@ -58,6 +61,11 @@ export function MessagesBlock({ className }: { className?: string }) { const handleRemoveFeedback = useCallback(() => { setFeedback(null); }, [setFeedback]); + const [fastForwarding, setFastForwarding] = useState(false); + const handleFastForwardReplay = useCallback(() => { + setFastForwarding(!fastForwarding); + fastForwardReplay(!fastForwarding); + }, [fastForwarding]); return (
- {!replaying ? ( + {!isReplay ? (
{!responding && messageCount === 0 && ( - - - Replay Mode - - - - DeerFlow is now replaying the conversation... - - - +
+
+ + + Replay Mode + + + + DeerFlow is now replaying the conversation... + + + +
+
+ {responding && ( + + )} +
+
)} diff --git a/web/src/app/_components/research-block.tsx b/web/src/app/_components/research-block.tsx index 9ca576e..fe842e6 100644 --- a/web/src/app/_components/research-block.tsx +++ b/web/src/app/_components/research-block.tsx @@ -7,6 +7,7 @@ import { useCallback, useEffect, useState } from "react"; import { Button } from "~/components/ui/button"; import { Card } from "~/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; +import { useReplay } from "~/core/replay"; import { listenToPodcast, openResearch, useStore } from "~/core/store"; import { cn } from "~/lib/utils"; @@ -32,6 +33,7 @@ export function ResearchBlock({ const reportStreaming = useStore((state) => reportId ? (state.messages.get(reportId)?.isStreaming ?? false) : false, ); + const { isReplay } = useReplay(); useEffect(() => { if (hasReport) { setActiveTab("report"); @@ -72,6 +74,7 @@ export function ResearchBlock({ className="text-gray-400" size="icon" variant="ghost" + disabled={isReplay} onClick={handleGeneratePodcast} > diff --git a/web/src/core/api/chat.ts b/web/src/core/api/chat.ts index e79625b..68078b4 100644 --- a/web/src/core/api/chat.ts +++ b/web/src/core/api/chat.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MIT import type { MCPServerMetadata } from "../mcp"; -import { extractReplayIdFromURL } from "../replay/get-replay-id"; +import { extractReplayIdFromSearchParams } from "../replay/get-replay-id"; import { fetchStream } from "../sse"; import { sleep } from "../utils"; @@ -66,7 +66,7 @@ async function* chatReplayStream( ? "/mock/before-interrupt.txt" : "/mock/after-interrupt.txt"; } else { - const replayId = extractReplayIdFromURL(); + const replayId = extractReplayIdFromSearchParams(window.location.search); if (replayId) { replayFilePath = `/replay/${replayId}.txt`; } else { @@ -90,17 +90,17 @@ async function* chatReplayStream( } as ChatEvent; if (chatEvent.type === "message_chunk") { if (!chatEvent.data.finish_reason) { - await sleep(250 + Math.random() * 250); + await sleepInReplay(250 + Math.random() * 250); } } else if (chatEvent.type === "tool_call_result") { - await sleep(1500); + await sleepInReplay(1500); } yield chatEvent; if (chatEvent.type === "tool_call_result") { - await sleep(800); + await sleepInReplay(800); } else if (chatEvent.type === "message_chunk") { if (chatEvent.data.role === "user") { - await sleep(1000); + await sleepInReplay(1000); } } } catch (e) { @@ -108,3 +108,16 @@ async function* chatReplayStream( } } } + +export async function sleepInReplay(ms: number) { + if (fastForwardReplaying) { + await sleep(0); + } else { + await sleep(ms); + } +} + +let fastForwardReplaying = false; +export function fastForwardReplay(value: boolean) { + fastForwardReplaying = value; +} diff --git a/web/src/core/replay/get-replay-id.ts b/web/src/core/replay/get-replay-id.ts index a8c1085..4b124bc 100644 --- a/web/src/core/replay/get-replay-id.ts +++ b/web/src/core/replay/get-replay-id.ts @@ -1,11 +1,8 @@ // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates // SPDX-License-Identifier: MIT -export function extractReplayIdFromURL() { - if (typeof window === "undefined") { - return null; - } - const urlParams = new URLSearchParams(window.location.search); +export function extractReplayIdFromSearchParams(params: string) { + const urlParams = new URLSearchParams(params); if (urlParams.has("replay")) { return urlParams.get("replay"); } diff --git a/web/src/core/replay/hooks.ts b/web/src/core/replay/hooks.ts index 5920fdb..a4e9b1c 100644 --- a/web/src/core/replay/hooks.ts +++ b/web/src/core/replay/hooks.ts @@ -1,13 +1,16 @@ // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates // SPDX-License-Identifier: MIT +import { useSearchParams } from "next/navigation"; import { useMemo } from "react"; -import { extractReplayIdFromURL } from "./get-replay-id"; +import { extractReplayIdFromSearchParams } from "./get-replay-id"; export function useReplay() { - const replayId = useMemo(() => { - return extractReplayIdFromURL(); - }, []); + const searchParams = useSearchParams(); + const replayId = useMemo( + () => extractReplayIdFromSearchParams(searchParams.toString()), + [searchParams], + ); return { isReplay: replayId != null, replayId }; }