diff --git a/web/src/app/chat/components/messages-block.tsx b/web/src/app/chat/components/messages-block.tsx index 09cc226..2fe6ea4 100644 --- a/web/src/app/chat/components/messages-block.tsx +++ b/web/src/app/chat/components/messages-block.tsx @@ -14,9 +14,11 @@ import { CardTitle, } from "~/components/ui/card"; import { fastForwardReplay } from "~/core/api"; +import { useReplayMetadata } from "~/core/api/hooks"; import type { Option } from "~/core/messages"; import { useReplay } from "~/core/replay"; import { sendMessage, useStore } from "~/core/store"; +import { env } from "~/env"; import { cn } from "~/lib/utils"; import { ConversationStarter } from "./conversation-starter"; @@ -28,6 +30,7 @@ export function MessagesBlock({ className }: { className?: string }) { const messageCount = useStore((state) => state.messageIds.length); const responding = useStore((state) => state.responding); const { isReplay } = useReplay(); + const { title: replayTitle, hasError: replayHasError } = useReplayMetadata(); const [replayStarted, setReplayStarted] = useState(false); const abortControllerRef = useRef(null); const [feedback, setFeedback] = useState<{ option: Option } | null>(null); @@ -123,7 +126,7 @@ export function MessagesBlock({ className }: { className?: string }) { - {responding ? "Replaying" : "Replay Mode"} + {responding ? "Replaying" : `${replayTitle}`} @@ -137,26 +140,43 @@ export function MessagesBlock({ className }: { className?: string }) { -
- {responding && ( - - )} - {!replayStarted && ( - - )} -
+ {!replayHasError && ( +
+ {responding && ( + + )} + {!replayStarted && ( + + )} +
+ )} + {!replayStarted && env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY && ( +
+ * This site is for demo purposes only. If you want to try your + own question, please{" "} + + click here + {" "} + to clone it locally and run it. +
+ )} )} diff --git a/web/src/app/chat/components/welcome.tsx b/web/src/app/chat/components/welcome.tsx index 10269a5..82c24e8 100644 --- a/web/src/app/chat/components/welcome.tsx +++ b/web/src/app/chat/components/welcome.tsx @@ -26,8 +26,8 @@ export function Welcome({ className }: { className?: string }) { > 🦌 DeerFlow - , a research tool built on cutting-edge language models, helps you - search on web, browse information, and handle complex tasks. + , a deep research assistant built on cutting-edge language models, helps + you search on web, browse information, and handle complex tasks. ); diff --git a/web/src/core/api/chat.ts b/web/src/core/api/chat.ts index 627d533..e1c81fb 100644 --- a/web/src/core/api/chat.ts +++ b/web/src/core/api/chat.ts @@ -1,6 +1,8 @@ // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates // SPDX-License-Identifier: MIT +import { env } from "~/env"; + import type { MCPServerMetadata } from "../mcp"; import { extractReplayIdFromSearchParams } from "../replay/get-replay-id"; import { fetchStream } from "../sse"; @@ -30,7 +32,11 @@ export async function* chatStream( }, 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); } const stream = fetchStream(resolveServiceURL("chat/stream"), { @@ -77,13 +83,13 @@ async function* chatReplayStream( if (replayId) { replayFilePath = `/replay/${replayId}.txt`; } else { - replayFilePath = "/mock/before-interrupt.txt"; + // Fallback to a default replay + replayFilePath = `/replay/eiffel-tower-vs-tallest-building.txt`; } } - const res = await fetch(replayFilePath, { - signal: options.abortSignal, + const text = await fetchReplay(replayFilePath, { + abortSignal: options.abortSignal, }); - const text = await res.text(); const chunks = text.split("\n\n"); for (const chunk of chunks) { const [eventRaw, dataRaw] = chunk.split("\n") as [string, string]; @@ -116,6 +122,43 @@ async function* chatReplayStream( } } +const replayCache = new Map(); +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) { if (fastForwardReplaying) { await sleep(0); diff --git a/web/src/core/api/hooks.ts b/web/src/core/api/hooks.ts new file mode 100644 index 0000000..200dd54 --- /dev/null +++ b/web/src/core/api/hooks.ts @@ -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(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(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 }; +}