mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-28 16:24:47 +08:00
feat: enhance replay mode in static website
This commit is contained in:
@@ -14,9 +14,11 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "~/components/ui/card";
|
} from "~/components/ui/card";
|
||||||
import { fastForwardReplay } from "~/core/api";
|
import { fastForwardReplay } from "~/core/api";
|
||||||
|
import { useReplayMetadata } from "~/core/api/hooks";
|
||||||
import type { Option } from "~/core/messages";
|
import type { Option } from "~/core/messages";
|
||||||
import { useReplay } from "~/core/replay";
|
import { useReplay } from "~/core/replay";
|
||||||
import { sendMessage, useStore } from "~/core/store";
|
import { sendMessage, useStore } from "~/core/store";
|
||||||
|
import { env } from "~/env";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
import { ConversationStarter } from "./conversation-starter";
|
import { ConversationStarter } from "./conversation-starter";
|
||||||
@@ -28,6 +30,7 @@ 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 { title: replayTitle, hasError: replayHasError } = useReplayMetadata();
|
||||||
const [replayStarted, setReplayStarted] = useState(false);
|
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);
|
||||||
@@ -123,7 +126,7 @@ export function MessagesBlock({ className }: { className?: string }) {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
<RainbowText animated={responding}>
|
<RainbowText animated={responding}>
|
||||||
{responding ? "Replaying" : "Replay Mode"}
|
{responding ? "Replaying" : `${replayTitle}`}
|
||||||
</RainbowText>
|
</RainbowText>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
@@ -137,26 +140,43 @@ export function MessagesBlock({ className }: { className?: string }) {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</div>
|
</div>
|
||||||
<div className="pr-4">
|
{!replayHasError && (
|
||||||
{responding && (
|
<div className="pr-4">
|
||||||
<Button
|
{responding && (
|
||||||
className={cn(fastForwarding && "animate-pulse")}
|
<Button
|
||||||
variant={fastForwarding ? "default" : "outline"}
|
className={cn(fastForwarding && "animate-pulse")}
|
||||||
onClick={handleFastForwardReplay}
|
variant={fastForwarding ? "default" : "outline"}
|
||||||
>
|
onClick={handleFastForwardReplay}
|
||||||
<FastForward size={16} />
|
>
|
||||||
Fast Forward
|
<FastForward size={16} />
|
||||||
</Button>
|
Fast Forward
|
||||||
)}
|
</Button>
|
||||||
{!replayStarted && (
|
)}
|
||||||
<Button className="w-24" onClick={handleStartReplay}>
|
{!replayStarted && (
|
||||||
<Play size={16} />
|
<Button className="w-24" onClick={handleStartReplay}>
|
||||||
Play
|
<Play size={16} />
|
||||||
</Button>
|
Play
|
||||||
)}
|
</Button>
|
||||||
</div>
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
{!replayStarted && env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY && (
|
||||||
|
<div className="text-muted-foreground w-full text-center text-xs">
|
||||||
|
* This site is for demo purposes only. If you want to try your
|
||||||
|
own question, please{" "}
|
||||||
|
<a
|
||||||
|
className="underline"
|
||||||
|
href="https://github.com/bytedance/deer-flow"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
click here
|
||||||
|
</a>{" "}
|
||||||
|
to clone it locally and run it.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ export function Welcome({ className }: { className?: string }) {
|
|||||||
>
|
>
|
||||||
🦌 DeerFlow
|
🦌 DeerFlow
|
||||||
</a>
|
</a>
|
||||||
, a research tool built on cutting-edge language models, helps you
|
, a deep research assistant built on cutting-edge language models, helps
|
||||||
search on web, browse information, and handle complex tasks.
|
you search on web, browse information, and handle complex tasks.
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +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 { env } from "~/env";
|
||||||
|
|
||||||
import type { MCPServerMetadata } from "../mcp";
|
import type { MCPServerMetadata } from "../mcp";
|
||||||
import { extractReplayIdFromSearchParams } from "../replay/get-replay-id";
|
import { extractReplayIdFromSearchParams } from "../replay/get-replay-id";
|
||||||
import { fetchStream } from "../sse";
|
import { fetchStream } from "../sse";
|
||||||
@@ -30,7 +32,11 @@ export async function* chatStream(
|
|||||||
},
|
},
|
||||||
options: { abortSignal?: AbortSignal } = {},
|
options: { abortSignal?: AbortSignal } = {},
|
||||||
) {
|
) {
|
||||||
if (location.search.includes("mock") || location.search.includes("replay=")) {
|
if (
|
||||||
|
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY ||
|
||||||
|
location.search.includes("mock") ||
|
||||||
|
location.search.includes("replay=")
|
||||||
|
) {
|
||||||
return yield* chatReplayStream(userMessage, params, options);
|
return yield* chatReplayStream(userMessage, params, options);
|
||||||
}
|
}
|
||||||
const stream = fetchStream(resolveServiceURL("chat/stream"), {
|
const stream = fetchStream(resolveServiceURL("chat/stream"), {
|
||||||
@@ -77,13 +83,13 @@ async function* chatReplayStream(
|
|||||||
if (replayId) {
|
if (replayId) {
|
||||||
replayFilePath = `/replay/${replayId}.txt`;
|
replayFilePath = `/replay/${replayId}.txt`;
|
||||||
} else {
|
} else {
|
||||||
replayFilePath = "/mock/before-interrupt.txt";
|
// Fallback to a default replay
|
||||||
|
replayFilePath = `/replay/eiffel-tower-vs-tallest-building.txt`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const res = await fetch(replayFilePath, {
|
const text = await fetchReplay(replayFilePath, {
|
||||||
signal: options.abortSignal,
|
abortSignal: options.abortSignal,
|
||||||
});
|
});
|
||||||
const text = await res.text();
|
|
||||||
const chunks = text.split("\n\n");
|
const chunks = text.split("\n\n");
|
||||||
for (const chunk of chunks) {
|
for (const chunk of chunks) {
|
||||||
const [eventRaw, dataRaw] = chunk.split("\n") as [string, string];
|
const [eventRaw, dataRaw] = chunk.split("\n") as [string, string];
|
||||||
@@ -116,6 +122,43 @@ async function* chatReplayStream(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const replayCache = new Map<string, string>();
|
||||||
|
export async function fetchReplay(
|
||||||
|
url: string,
|
||||||
|
options: { abortSignal?: AbortSignal } = {},
|
||||||
|
) {
|
||||||
|
if (replayCache.has(url)) {
|
||||||
|
return replayCache.get(url)!;
|
||||||
|
}
|
||||||
|
const res = await fetch(url, {
|
||||||
|
signal: options.abortSignal,
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Failed to fetch replay: ${res.statusText}`);
|
||||||
|
}
|
||||||
|
const text = await res.text();
|
||||||
|
replayCache.set(url, text);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchReplayTitle() {
|
||||||
|
const res = chatReplayStream(
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
thread_id: "__mock__",
|
||||||
|
auto_accepted_plan: false,
|
||||||
|
max_plan_iterations: 3,
|
||||||
|
max_step_num: 1,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
for await (const event of res) {
|
||||||
|
if (event.type === "message_chunk") {
|
||||||
|
return event.data.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function sleepInReplay(ms: number) {
|
export async function sleepInReplay(ms: number) {
|
||||||
if (fastForwardReplaying) {
|
if (fastForwardReplaying) {
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
|
|||||||
35
web/src/core/api/hooks.ts
Normal file
35
web/src/core/api/hooks.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { fetchReplayTitle } from "./chat";
|
||||||
|
|
||||||
|
export function useReplayMetadata() {
|
||||||
|
const [title, setTitle] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<boolean>(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (title || isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoading(true);
|
||||||
|
fetchReplayTitle()
|
||||||
|
.then((title) => {
|
||||||
|
setError(false);
|
||||||
|
setTitle(title ?? null);
|
||||||
|
if (title) {
|
||||||
|
document.title = `${title} - DeerFlow`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setError(true);
|
||||||
|
setTitle("Error: the replay is not available.");
|
||||||
|
document.title = "DeerFlow";
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}, [isLoading, title]);
|
||||||
|
return { title, isLoading, hasError: error };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user