mirror of
https://git.mirrors.martin98.com/https://github.com/bytedance/deer-flow
synced 2025-08-20 15:09:06 +08:00
feat: enhance replay and fast-forward replay
This commit is contained in:
parent
cb4b7b7495
commit
cb9201bd34
@ -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<AbortController | null>(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 (
|
||||
<div className={cn("flex h-full flex-col", className)}>
|
||||
<MessageListView
|
||||
@ -65,7 +73,7 @@ export function MessagesBlock({ className }: { className?: string }) {
|
||||
onFeedback={handleFeedback}
|
||||
onSendMessage={handleSend}
|
||||
/>
|
||||
{!replaying ? (
|
||||
{!isReplay ? (
|
||||
<div className="relative flex h-42 shrink-0 pb-4">
|
||||
{!responding && messageCount === 0 && (
|
||||
<ConversationStarter
|
||||
@ -85,16 +93,32 @@ export function MessagesBlock({ className }: { className?: string }) {
|
||||
) : (
|
||||
<div className="flex h-42 w-full items-center justify-center">
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<RainbowText animated={responding}>Replay Mode</RainbowText>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<RainbowText animated={responding}>
|
||||
DeerFlow is now replaying the conversation...
|
||||
</RainbowText>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<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>
|
||||
<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>
|
||||
)}
|
||||
|
@ -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}
|
||||
>
|
||||
<Headphones />
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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 };
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user