mirror of
https://git.mirrors.martin98.com/https://github.com/bytedance/deer-flow
synced 2025-08-20 18:19:04 +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
|
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { useSearchParams } from "next/navigation";
|
import { FastForward } from "lucide-react";
|
||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "~/components/ui/card";
|
} from "~/components/ui/card";
|
||||||
|
import { fastForwardReplay } from "~/core/api";
|
||||||
import type { Option } from "~/core/messages";
|
import type { Option } from "~/core/messages";
|
||||||
|
import { useReplay } from "~/core/replay";
|
||||||
import { sendMessage, useStore } from "~/core/store";
|
import { sendMessage, useStore } from "~/core/store";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
@ -22,7 +25,7 @@ import { RainbowText } from "./rainbow-text";
|
|||||||
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 replaying = useSearchParams().get("replay") !== null;
|
const { isReplay } = useReplay();
|
||||||
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(
|
||||||
@ -58,6 +61,11 @@ export function MessagesBlock({ className }: { className?: string }) {
|
|||||||
const handleRemoveFeedback = useCallback(() => {
|
const handleRemoveFeedback = useCallback(() => {
|
||||||
setFeedback(null);
|
setFeedback(null);
|
||||||
}, [setFeedback]);
|
}, [setFeedback]);
|
||||||
|
const [fastForwarding, setFastForwarding] = useState(false);
|
||||||
|
const handleFastForwardReplay = useCallback(() => {
|
||||||
|
setFastForwarding(!fastForwarding);
|
||||||
|
fastForwardReplay(!fastForwarding);
|
||||||
|
}, [fastForwarding]);
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex h-full flex-col", className)}>
|
<div className={cn("flex h-full flex-col", className)}>
|
||||||
<MessageListView
|
<MessageListView
|
||||||
@ -65,7 +73,7 @@ export function MessagesBlock({ className }: { className?: string }) {
|
|||||||
onFeedback={handleFeedback}
|
onFeedback={handleFeedback}
|
||||||
onSendMessage={handleSend}
|
onSendMessage={handleSend}
|
||||||
/>
|
/>
|
||||||
{!replaying ? (
|
{!isReplay ? (
|
||||||
<div className="relative flex h-42 shrink-0 pb-4">
|
<div className="relative flex h-42 shrink-0 pb-4">
|
||||||
{!responding && messageCount === 0 && (
|
{!responding && messageCount === 0 && (
|
||||||
<ConversationStarter
|
<ConversationStarter
|
||||||
@ -85,16 +93,32 @@ export function MessagesBlock({ className }: { className?: string }) {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex h-42 w-full items-center justify-center">
|
<div className="flex h-42 w-full items-center justify-center">
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
<CardHeader>
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle>
|
<div className="flex-grow">
|
||||||
<RainbowText animated={responding}>Replay Mode</RainbowText>
|
<CardHeader>
|
||||||
</CardTitle>
|
<CardTitle>
|
||||||
<CardDescription>
|
<RainbowText animated={responding}>Replay Mode</RainbowText>
|
||||||
<RainbowText animated={responding}>
|
</CardTitle>
|
||||||
DeerFlow is now replaying the conversation...
|
<CardDescription>
|
||||||
</RainbowText>
|
<RainbowText animated={responding}>
|
||||||
</CardDescription>
|
DeerFlow is now replaying the conversation...
|
||||||
</CardHeader>
|
</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>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -7,6 +7,7 @@ import { useCallback, useEffect, useState } from "react";
|
|||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Card } from "~/components/ui/card";
|
import { Card } from "~/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
|
||||||
|
import { useReplay } from "~/core/replay";
|
||||||
import { listenToPodcast, openResearch, useStore } from "~/core/store";
|
import { listenToPodcast, openResearch, useStore } from "~/core/store";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ export function ResearchBlock({
|
|||||||
const reportStreaming = useStore((state) =>
|
const reportStreaming = useStore((state) =>
|
||||||
reportId ? (state.messages.get(reportId)?.isStreaming ?? false) : false,
|
reportId ? (state.messages.get(reportId)?.isStreaming ?? false) : false,
|
||||||
);
|
);
|
||||||
|
const { isReplay } = useReplay();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasReport) {
|
if (hasReport) {
|
||||||
setActiveTab("report");
|
setActiveTab("report");
|
||||||
@ -72,6 +74,7 @@ export function ResearchBlock({
|
|||||||
className="text-gray-400"
|
className="text-gray-400"
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
disabled={isReplay}
|
||||||
onClick={handleGeneratePodcast}
|
onClick={handleGeneratePodcast}
|
||||||
>
|
>
|
||||||
<Headphones />
|
<Headphones />
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import type { MCPServerMetadata } from "../mcp";
|
import type { MCPServerMetadata } from "../mcp";
|
||||||
import { extractReplayIdFromURL } from "../replay/get-replay-id";
|
import { extractReplayIdFromSearchParams } from "../replay/get-replay-id";
|
||||||
import { fetchStream } from "../sse";
|
import { fetchStream } from "../sse";
|
||||||
import { sleep } from "../utils";
|
import { sleep } from "../utils";
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ async function* chatReplayStream(
|
|||||||
? "/mock/before-interrupt.txt"
|
? "/mock/before-interrupt.txt"
|
||||||
: "/mock/after-interrupt.txt";
|
: "/mock/after-interrupt.txt";
|
||||||
} else {
|
} else {
|
||||||
const replayId = extractReplayIdFromURL();
|
const replayId = extractReplayIdFromSearchParams(window.location.search);
|
||||||
if (replayId) {
|
if (replayId) {
|
||||||
replayFilePath = `/replay/${replayId}.txt`;
|
replayFilePath = `/replay/${replayId}.txt`;
|
||||||
} else {
|
} else {
|
||||||
@ -90,17 +90,17 @@ async function* chatReplayStream(
|
|||||||
} as ChatEvent;
|
} as ChatEvent;
|
||||||
if (chatEvent.type === "message_chunk") {
|
if (chatEvent.type === "message_chunk") {
|
||||||
if (!chatEvent.data.finish_reason) {
|
if (!chatEvent.data.finish_reason) {
|
||||||
await sleep(250 + Math.random() * 250);
|
await sleepInReplay(250 + Math.random() * 250);
|
||||||
}
|
}
|
||||||
} else if (chatEvent.type === "tool_call_result") {
|
} else if (chatEvent.type === "tool_call_result") {
|
||||||
await sleep(1500);
|
await sleepInReplay(1500);
|
||||||
}
|
}
|
||||||
yield chatEvent;
|
yield chatEvent;
|
||||||
if (chatEvent.type === "tool_call_result") {
|
if (chatEvent.type === "tool_call_result") {
|
||||||
await sleep(800);
|
await sleepInReplay(800);
|
||||||
} else if (chatEvent.type === "message_chunk") {
|
} else if (chatEvent.type === "message_chunk") {
|
||||||
if (chatEvent.data.role === "user") {
|
if (chatEvent.data.role === "user") {
|
||||||
await sleep(1000);
|
await sleepInReplay(1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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
|
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
export function extractReplayIdFromURL() {
|
export function extractReplayIdFromSearchParams(params: string) {
|
||||||
if (typeof window === "undefined") {
|
const urlParams = new URLSearchParams(params);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
if (urlParams.has("replay")) {
|
if (urlParams.has("replay")) {
|
||||||
return urlParams.get("replay");
|
return urlParams.get("replay");
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
// 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 { useSearchParams } from "next/navigation";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import { extractReplayIdFromURL } from "./get-replay-id";
|
import { extractReplayIdFromSearchParams } from "./get-replay-id";
|
||||||
|
|
||||||
export function useReplay() {
|
export function useReplay() {
|
||||||
const replayId = useMemo(() => {
|
const searchParams = useSearchParams();
|
||||||
return extractReplayIdFromURL();
|
const replayId = useMemo(
|
||||||
}, []);
|
() => extractReplayIdFromSearchParams(searchParams.toString()),
|
||||||
|
[searchParams],
|
||||||
|
);
|
||||||
return { isReplay: replayId != null, replayId };
|
return { isReplay: replayId != null, replayId };
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user