mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-26 15:24:48 +08:00
feat: implement MCP UIs
This commit is contained in:
14
web/src/app/_settings/tabs/about-tab.tsx
Normal file
14
web/src/app/_settings/tabs/about-tab.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { BadgeInfo } from "lucide-react";
|
||||
|
||||
import { Markdown } from "~/app/_components/markdown";
|
||||
|
||||
import about from "./about.md";
|
||||
import type { Tab } from "./types";
|
||||
|
||||
export const AboutTab: Tab = () => {
|
||||
return <Markdown>{about}</Markdown>;
|
||||
};
|
||||
AboutTab.icon = BadgeInfo;
|
||||
39
web/src/app/_settings/tabs/about.md
Normal file
39
web/src/app/_settings/tabs/about.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# 🦌 [About DeerFlow](https://github.com/bytedance/deer-flow)
|
||||
|
||||
> **From Open Source, Back to Open Source**
|
||||
|
||||
**DeerFlow** (**D**eep **E**xploration and **E**fficient **R**esearch **Flow**) is a community-driven AI automation framework inspired by the remarkable contributions of the open source community. Our mission is to seamlessly integrate language models with specialized tools for tasks such as web search, crawling, and Python code execution—all while giving back to the community that made this innovation possible.
|
||||
|
||||
---
|
||||
|
||||
## 🌟 GitHub Repository
|
||||
|
||||
Explore DeerFlow on GitHub: [github.com/bytedance/deer-flow](https://github.com/bytedance/deer-flow)
|
||||
|
||||
---
|
||||
|
||||
## 📜 License
|
||||
|
||||
DeerFlow is proudly open source and distributed under the **MIT License**.
|
||||
|
||||
---
|
||||
|
||||
## 🙌 Acknowledgments
|
||||
|
||||
We extend our heartfelt gratitude to the open source projects and contributors who have made DeerFlow a reality. We truly stand on the shoulders of giants.
|
||||
|
||||
### Core Frameworks
|
||||
- **[LangChain](https://github.com/langchain-ai/langchain)**: A phenomenal framework that powers our LLM interactions and chains.
|
||||
- **[LangGraph](https://github.com/langchain-ai/langgraph)**: Enabling sophisticated multi-agent orchestration.
|
||||
- **[Next.js](https://nextjs.org/)**: A cutting-edge framework for building web applications.
|
||||
|
||||
### UI Libraries
|
||||
- **[Shadcn](https://ui.shadcn.com/)**: Minimalistic components that power our UI.
|
||||
- **[Zustand](https://zustand.docs.pmnd.rs/)**: A stunning state management library.
|
||||
- **[Framer Motion](https://www.framer.com/motion/)**: An amazing animation library.
|
||||
- **[React Markdown](https://www.npmjs.com/package/react-markdown)**: Exceptional markdown rendering with customizability.
|
||||
|
||||
### Special Thanks
|
||||
- **[SToneX](https://github.com/stonexer)**: For his invaluable contribution to token-by-token visual effects.
|
||||
|
||||
These outstanding projects form the backbone of DeerFlow and exemplify the transformative power of open source collaboration.
|
||||
127
web/src/app/_settings/tabs/general-tab.tsx
Normal file
127
web/src/app/_settings/tabs/general-tab.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Settings } from "lucide-react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "~/components/ui/form";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import type { SettingsState } from "~/core/store";
|
||||
|
||||
import type { Tab } from "./types";
|
||||
|
||||
const generalFormSchema = z.object({
|
||||
maxPlanIterations: z.number().min(1, {
|
||||
message: "Max plan iterations must be at least 1.",
|
||||
}),
|
||||
maxStepNum: z.number().min(1, {
|
||||
message: "Max step number must be at least 1.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const GeneralTab: Tab = ({
|
||||
settings,
|
||||
onChange,
|
||||
}: {
|
||||
settings: SettingsState;
|
||||
onChange: (changes: Partial<SettingsState>) => void;
|
||||
}) => {
|
||||
const generalSettings = useMemo(() => settings.general, [settings]);
|
||||
const form = useForm<z.infer<typeof generalFormSchema>>({
|
||||
resolver: zodResolver(generalFormSchema, undefined, undefined),
|
||||
values: generalSettings,
|
||||
});
|
||||
|
||||
const currentSettings = form.watch();
|
||||
useEffect(() => {
|
||||
let hasChanges = false;
|
||||
for (const key in currentSettings) {
|
||||
if (
|
||||
currentSettings[key as keyof typeof currentSettings] !==
|
||||
settings.general[key as keyof SettingsState["general"]]
|
||||
) {
|
||||
hasChanges = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasChanges) {
|
||||
onChange({ general: currentSettings });
|
||||
}
|
||||
}, [currentSettings, onChange, settings]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<header>
|
||||
<h1 className="text-lg font-medium">General</h1>
|
||||
</header>
|
||||
<main>
|
||||
<Form {...form}>
|
||||
<form className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="maxPlanIterations"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Max plan iterations</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="w-60"
|
||||
type="number"
|
||||
{...field}
|
||||
min={1}
|
||||
onChange={(event) =>
|
||||
field.onChange(parseInt(event.target.value))
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Set to 1 for single-step planning. Set to 2 to enable
|
||||
re-planning.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="maxStepNum"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Max steps of a research plan</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="w-60"
|
||||
type="number"
|
||||
{...field}
|
||||
min={1}
|
||||
onChange={(event) =>
|
||||
field.onChange(parseInt(event.target.value))
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
By default, each research plan has 3 steps.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
GeneralTab.displayName = "";
|
||||
GeneralTab.icon = Settings;
|
||||
19
web/src/app/_settings/tabs/index.tsx
Normal file
19
web/src/app/_settings/tabs/index.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { Settings, type LucideIcon } from "lucide-react";
|
||||
|
||||
import { AboutTab } from "./about-tab";
|
||||
import { GeneralTab } from "./general-tab";
|
||||
import { MCPTab } from "./mcp-tab";
|
||||
|
||||
export const SETTINGS_TABS = [GeneralTab, MCPTab, AboutTab].map((tab) => {
|
||||
const name = tab.name ?? tab.displayName;
|
||||
return {
|
||||
...tab,
|
||||
id: name.replace(/Tab$/, "").toLocaleLowerCase(),
|
||||
label: name.replace(/Tab$/, ""),
|
||||
icon: (tab.icon ?? <Settings />) as LucideIcon,
|
||||
component: tab,
|
||||
};
|
||||
});
|
||||
152
web/src/app/_settings/tabs/mcp-tab.tsx
Normal file
152
web/src/app/_settings/tabs/mcp-tab.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { Blocks, PencilRuler, Trash } from "lucide-react";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { Tooltip } from "~/app/_components/tooltip";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import type { MCPServerMetadata } from "~/core/mcp";
|
||||
|
||||
import { AddMCPServerDialog } from "../dialogs/add-mcp-server-dialog";
|
||||
|
||||
import type { Tab } from "./types";
|
||||
|
||||
export const MCPTab: Tab = ({ settings, onChange }) => {
|
||||
const [servers, setServers] = useState<MCPServerMetadata[]>(
|
||||
settings.mcp.servers,
|
||||
);
|
||||
const [newlyAdded, setNewlyAdded] = useState(false);
|
||||
const handleAddServers = useCallback(
|
||||
(servers: MCPServerMetadata[]) => {
|
||||
const merged = mergeServers(settings.mcp.servers, servers);
|
||||
setServers(merged);
|
||||
onChange({ ...settings, mcp: { ...settings.mcp, servers: merged } });
|
||||
setNewlyAdded(true);
|
||||
setTimeout(() => {
|
||||
setNewlyAdded(false);
|
||||
}, 1000);
|
||||
setTimeout(() => {
|
||||
document.getElementById("settings-content-scrollable")?.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}, 100);
|
||||
},
|
||||
[onChange, settings],
|
||||
);
|
||||
const handleDeleteServer = useCallback(
|
||||
(name: string) => {
|
||||
const merged = settings.mcp.servers.filter(
|
||||
(server) => server.name !== name,
|
||||
);
|
||||
setServers(merged);
|
||||
onChange({ ...settings, mcp: { ...settings.mcp, servers: merged } });
|
||||
},
|
||||
[onChange, settings],
|
||||
);
|
||||
const animationProps = {
|
||||
initial: { backgroundColor: "gray" },
|
||||
animate: { backgroundColor: "transparent" },
|
||||
transition: { duration: 1 },
|
||||
style: {
|
||||
transition: "background-color 1s ease-out",
|
||||
},
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<header>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h1 className="text-lg font-medium">MCP Servers</h1>
|
||||
<AddMCPServerDialog onAdd={handleAddServers} />
|
||||
</div>
|
||||
<div className="text-muted-foreground markdown text-sm">
|
||||
The Model Context Protocol boosts DeerFlow by integrating external
|
||||
tools for tasks like private domain searches, web browsing, food
|
||||
ordering, and more. Click here to
|
||||
<a
|
||||
className="ml-1"
|
||||
target="_blank"
|
||||
href="https://modelcontextprotocol.io/"
|
||||
>
|
||||
learn more about MCP.
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<ul id="mcp-servers-list" className="flex flex-col gap-4">
|
||||
{servers.map((server) => {
|
||||
const isNew =
|
||||
server.createdAt &&
|
||||
server.createdAt > Date.now() - 1000 * 60 * 60 * 1;
|
||||
return (
|
||||
<motion.li
|
||||
className="!bg-card group relative overflow-hidden rounded-lg border shadow"
|
||||
key={server.name}
|
||||
{...(isNew && newlyAdded && animationProps)}
|
||||
>
|
||||
<div className="absolute top-1 right-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<Tooltip title="Delete server">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDeleteServer(server.name)}
|
||||
>
|
||||
<Trash />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex flex-col items-start px-4 py-2">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<div className="text-lg font-medium">{server.name}</div>
|
||||
<div className="bg-primary text-primary-foreground h-fit rounded px-1.5 py-0.5 text-xs">
|
||||
{server.transport}
|
||||
</div>
|
||||
{isNew && (
|
||||
<div className="bg-primary text-primary-foreground h-fit rounded px-1.5 py-0.5 text-xs">
|
||||
New
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ul className="flex flex-wrap items-center gap-2">
|
||||
<PencilRuler size={16} />
|
||||
{server.tools.map((tool) => (
|
||||
<li
|
||||
key={tool.name}
|
||||
className="text-muted-foreground border-muted-foreground w-fit rounded-md border px-2"
|
||||
>
|
||||
<Tooltip key={tool.name} title={tool.description}>
|
||||
<div className="w-fit text-sm">{tool.name}</div>
|
||||
</Tooltip>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</motion.li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
MCPTab.icon = Blocks;
|
||||
MCPTab.badge = "Beta";
|
||||
|
||||
function mergeServers(
|
||||
existing: MCPServerMetadata[],
|
||||
added: MCPServerMetadata[],
|
||||
): MCPServerMetadata[] {
|
||||
const serverMap = new Map(existing.map((server) => [server.name, server]));
|
||||
|
||||
for (const addedServer of added) {
|
||||
addedServer.createdAt = Date.now();
|
||||
addedServer.updatedAt = Date.now();
|
||||
serverMap.set(addedServer.name, addedServer);
|
||||
}
|
||||
|
||||
const result = Array.from(serverMap.values());
|
||||
result.sort((a, b) => b.createdAt - a.createdAt);
|
||||
return result;
|
||||
}
|
||||
16
web/src/app/_settings/tabs/types.ts
Normal file
16
web/src/app/_settings/tabs/types.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { FunctionComponent } from "react";
|
||||
|
||||
import type { SettingsState } from "~/core/store";
|
||||
|
||||
export type Tab = FunctionComponent<{
|
||||
settings: SettingsState;
|
||||
onChange: (changes: Partial<SettingsState>) => void;
|
||||
}> & {
|
||||
displayName?: string;
|
||||
icon?: LucideIcon;
|
||||
badge?: string;
|
||||
};
|
||||
Reference in New Issue
Block a user