mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-22 13:44:46 +08:00
205 lines
6.3 KiB
TypeScript
205 lines
6.3 KiB
TypeScript
|
|
import {
|
||
|
|
BookOutlined,
|
||
|
|
PythonOutlined,
|
||
|
|
SearchOutlined,
|
||
|
|
} from "@ant-design/icons";
|
||
|
|
import { parse } from "best-effort-json-parser";
|
||
|
|
import { motion } from "framer-motion";
|
||
|
|
import { LRUCache } from "lru-cache";
|
||
|
|
import { useMemo } from "react";
|
||
|
|
import SyntaxHighlighter from "react-syntax-highlighter";
|
||
|
|
import { docco } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||
|
|
|
||
|
|
import type { ToolCallRuntime } from "~/core/messages";
|
||
|
|
import { useMessage, useStore } from "~/core/store";
|
||
|
|
import { cn } from "~/lib/utils";
|
||
|
|
|
||
|
|
import { FavIcon } from "./fav-icon";
|
||
|
|
import { LoadingAnimation } from "./loading-animation";
|
||
|
|
import { Markdown } from "./markdown";
|
||
|
|
import { RainbowText } from "./rainbow-text";
|
||
|
|
|
||
|
|
export function ResearchActivitiesBlock({
|
||
|
|
className,
|
||
|
|
researchId,
|
||
|
|
}: {
|
||
|
|
className?: string;
|
||
|
|
researchId: string;
|
||
|
|
}) {
|
||
|
|
const activityIds = useStore((state) =>
|
||
|
|
state.researchActivityIds.get(researchId),
|
||
|
|
)!;
|
||
|
|
const ongoing = useStore((state) => state.ongoingResearchId === researchId);
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
<ul className={cn("flex flex-col py-4", className)}>
|
||
|
|
{activityIds.map(
|
||
|
|
(activityId, i) =>
|
||
|
|
i !== 0 && (
|
||
|
|
<motion.li
|
||
|
|
key={activityId}
|
||
|
|
style={{ transition: "all 0.4s ease-out" }}
|
||
|
|
initial={{ opacity: 0, y: 24 }}
|
||
|
|
animate={{ opacity: 1, y: 0 }}
|
||
|
|
transition={{
|
||
|
|
duration: 0.4,
|
||
|
|
ease: "easeOut",
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<ActivityMessage messageId={activityId} />
|
||
|
|
<ActivityListItem messageId={activityId} />
|
||
|
|
{i !== activityIds.length - 1 && <hr className="my-8" />}
|
||
|
|
</motion.li>
|
||
|
|
),
|
||
|
|
)}
|
||
|
|
</ul>
|
||
|
|
{ongoing && <LoadingAnimation className="mx-4 my-12" />}
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function ActivityMessage({ messageId }: { messageId: string }) {
|
||
|
|
const message = useMessage(messageId);
|
||
|
|
if (message?.agent && message.content) {
|
||
|
|
if (message.agent !== "reporter" && message.agent !== "planner") {
|
||
|
|
return (
|
||
|
|
<div className="px-4 py-2">
|
||
|
|
<Markdown animate>{message.content}</Markdown>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function ActivityListItem({ messageId }: { messageId: string }) {
|
||
|
|
const message = useMessage(messageId);
|
||
|
|
if (message) {
|
||
|
|
if (!message.isStreaming && message.toolCalls?.length) {
|
||
|
|
for (const toolCall of message.toolCalls) {
|
||
|
|
if (toolCall.name === "web_search") {
|
||
|
|
return <WebSearchToolCall key={toolCall.id} toolCall={toolCall} />;
|
||
|
|
} else if (toolCall.name === "crawl_tool") {
|
||
|
|
return <CrawlToolCall key={toolCall.id} toolCall={toolCall} />;
|
||
|
|
} else if (toolCall.name === "python_repl_tool") {
|
||
|
|
return <PythonToolCall key={toolCall.id} toolCall={toolCall} />;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const __pageCache = new LRUCache<string, string>({ max: 100 });
|
||
|
|
function WebSearchToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||
|
|
const searchResults = useMemo<
|
||
|
|
{ title: string; url: string; content: string }[]
|
||
|
|
>(() => {
|
||
|
|
let results: { title: string; url: string; content: string }[] | undefined =
|
||
|
|
undefined;
|
||
|
|
try {
|
||
|
|
results = toolCall.result ? parse(toolCall.result) : undefined;
|
||
|
|
} catch {
|
||
|
|
results = undefined;
|
||
|
|
}
|
||
|
|
if (Array.isArray(results)) {
|
||
|
|
results.forEach((result: { url: string; title: string }) => {
|
||
|
|
__pageCache.set(result.url, result.title);
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
results = [];
|
||
|
|
}
|
||
|
|
return results;
|
||
|
|
}, [toolCall.result]);
|
||
|
|
return (
|
||
|
|
<section>
|
||
|
|
<div className="font-medium italic">
|
||
|
|
<RainbowText
|
||
|
|
className="flex items-center"
|
||
|
|
animated={searchResults === undefined}
|
||
|
|
>
|
||
|
|
<SearchOutlined className={"mr-2"} />
|
||
|
|
<span>Searching for </span>
|
||
|
|
<span className="max-w-[500px] overflow-hidden text-ellipsis whitespace-nowrap">
|
||
|
|
{(toolCall.args as { query: string }).query}
|
||
|
|
</span>
|
||
|
|
</RainbowText>
|
||
|
|
</div>
|
||
|
|
{searchResults && (
|
||
|
|
<div className="px-5">
|
||
|
|
<ul className="mt-2 flex flex-wrap gap-4">
|
||
|
|
{searchResults.map((searchResult, i) => (
|
||
|
|
<motion.li
|
||
|
|
key={`search-result-${i}`}
|
||
|
|
className="text-muted-foreground flex max-w-40 gap-2 rounded-md bg-slate-100 px-2 py-1 text-sm"
|
||
|
|
initial={{ opacity: 0, y: 10, scale: 0.66 }}
|
||
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||
|
|
transition={{
|
||
|
|
duration: 0.2,
|
||
|
|
delay: i * 0.1,
|
||
|
|
ease: "easeOut",
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<FavIcon url={searchResult.url} title={searchResult.title} />
|
||
|
|
<a href={searchResult.url} target="_blank">
|
||
|
|
{searchResult.title}
|
||
|
|
</a>
|
||
|
|
</motion.li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</section>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function CrawlToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||
|
|
const url = useMemo(
|
||
|
|
() => (toolCall.args as { url: string }).url,
|
||
|
|
[toolCall.args],
|
||
|
|
);
|
||
|
|
const title = useMemo(() => __pageCache.get(url), [url]);
|
||
|
|
return (
|
||
|
|
<section>
|
||
|
|
<div className="font-medium italic">
|
||
|
|
<RainbowText
|
||
|
|
className="flex items-center"
|
||
|
|
animated={toolCall.result === undefined}
|
||
|
|
>
|
||
|
|
<BookOutlined className={"mr-2"} />
|
||
|
|
<span>Reading </span>
|
||
|
|
<li className="flex w-fit gap-1 px-2 py-1 text-sm">
|
||
|
|
<FavIcon url={url} title={title} />
|
||
|
|
<a className="hover:underline" href={url} target="_blank">
|
||
|
|
{title}
|
||
|
|
</a>
|
||
|
|
</li>
|
||
|
|
</RainbowText>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function PythonToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||
|
|
const code = useMemo<string>(() => {
|
||
|
|
return (toolCall.args as { code: string }).code;
|
||
|
|
}, [toolCall.args]);
|
||
|
|
return (
|
||
|
|
<section>
|
||
|
|
<div className="font-medium italic">
|
||
|
|
<PythonOutlined className={"mr-2"} />
|
||
|
|
<RainbowText animated={toolCall.result === undefined}>
|
||
|
|
Running Python code
|
||
|
|
</RainbowText>
|
||
|
|
</div>
|
||
|
|
<div className="px-5">
|
||
|
|
<div className="mt-2 rounded-md bg-slate-50 p-2 text-sm">
|
||
|
|
<SyntaxHighlighter language="python" style={docco}>
|
||
|
|
{code}
|
||
|
|
</SyntaxHighlighter>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
);
|
||
|
|
}
|