mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-24 06:34:46 +08:00
refactor: extract components folder
This commit is contained in:
174
web/src/app/settings/dialogs/add-mcp-server-dialog.tsx
Normal file
174
web/src/app/settings/dialogs/add-mcp-server-dialog.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "~/components/ui/dialog";
|
||||
import { Textarea } from "~/components/ui/textarea";
|
||||
import { queryMCPServerMetadata } from "~/core/api";
|
||||
import {
|
||||
MCPConfigSchema,
|
||||
type MCPServerMetadata,
|
||||
type SimpleMCPServerMetadata,
|
||||
type SimpleSSEMCPServerMetadata,
|
||||
type SimpleStdioMCPServerMetadata,
|
||||
} from "~/core/mcp";
|
||||
|
||||
export function AddMCPServerDialog({
|
||||
onAdd,
|
||||
}: {
|
||||
onAdd?: (servers: MCPServerMetadata[]) => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [input, setInput] = useState("");
|
||||
const [validationError, setValidationError] = useState<string | null>("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const handleChange = useCallback((value: string) => {
|
||||
setInput(value);
|
||||
if (!value.trim()) {
|
||||
setValidationError(null);
|
||||
return;
|
||||
}
|
||||
setValidationError(null);
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (!("mcpServers" in parsed)) {
|
||||
setValidationError("Missing `mcpServers` in JSON");
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
setValidationError("Invalid JSON");
|
||||
return;
|
||||
}
|
||||
const result = MCPConfigSchema.safeParse(JSON.parse(value));
|
||||
if (!result.success) {
|
||||
if (result.error.errors[0]) {
|
||||
const error = result.error.errors[0];
|
||||
if (error.code === "invalid_union") {
|
||||
if (error.unionErrors[0]?.errors[0]) {
|
||||
setValidationError(error.unionErrors[0].errors[0].message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
const errorMessage =
|
||||
result.error.errors[0]?.message ?? "Validation failed";
|
||||
setValidationError(errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = Object.keys(result.data.mcpServers);
|
||||
if (keys.length === 0) {
|
||||
setValidationError("Missing server name in `mcpServers`");
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
const handleAdd = useCallback(async () => {
|
||||
const config = MCPConfigSchema.parse(JSON.parse(input));
|
||||
setInput(JSON.stringify(config, null, 2));
|
||||
const addingServers: SimpleMCPServerMetadata[] = [];
|
||||
for (const [key, server] of Object.entries(config.mcpServers)) {
|
||||
if ("command" in server) {
|
||||
const metadata: SimpleStdioMCPServerMetadata = {
|
||||
transport: "stdio",
|
||||
name: key,
|
||||
command: server.command,
|
||||
args: server.args,
|
||||
env: server.env,
|
||||
};
|
||||
addingServers.push(metadata);
|
||||
} else if ("url" in server) {
|
||||
const metadata: SimpleSSEMCPServerMetadata = {
|
||||
transport: "sse",
|
||||
name: key,
|
||||
url: server.url,
|
||||
};
|
||||
addingServers.push(metadata);
|
||||
}
|
||||
}
|
||||
setProcessing(true);
|
||||
|
||||
const results: MCPServerMetadata[] = [];
|
||||
let processingServer: string | null = null;
|
||||
try {
|
||||
setError(null);
|
||||
for (const server of addingServers) {
|
||||
processingServer = server.name;
|
||||
const metadata = await queryMCPServerMetadata(server);
|
||||
results.push({ ...metadata, name: server.name, enabled: true });
|
||||
}
|
||||
if (results.length > 0) {
|
||||
onAdd?.(results);
|
||||
}
|
||||
setInput("");
|
||||
setOpen(false);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError(`Failed to add server: ${processingServer}`);
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
}, [input, onAdd]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm">Add Servers</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[560px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add New MCP Servers</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
DeerFlow uses the standard JSON MCP config to create a new server.
|
||||
<br />
|
||||
Paste your config below and click "Add" to add new servers.
|
||||
</DialogDescription>
|
||||
|
||||
<main>
|
||||
<Textarea
|
||||
className="h-[360px]"
|
||||
placeholder={
|
||||
'Example:\n\n{\n "mcpServers": {\n "My Server": {\n "command": "python",\n "args": [\n "-m", "mcp_server"\n ],\n "env": {\n "API_KEY": "YOUR_API_KEY"\n }\n }\n }\n}'
|
||||
}
|
||||
value={input}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
/>
|
||||
</main>
|
||||
|
||||
<DialogFooter>
|
||||
<div className="flex h-10 w-full items-center justify-between gap-2">
|
||||
<div className="text-destructive flex-grow overflow-hidden text-sm">
|
||||
{validationError ?? error}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="w-24"
|
||||
type="submit"
|
||||
disabled={!input.trim() || !!validationError || processing}
|
||||
onClick={handleAdd}
|
||||
>
|
||||
{processing && <Loader2 className="animate-spin" />}
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
169
web/src/app/settings/dialogs/settings-dialog.tsx
Normal file
169
web/src/app/settings/dialogs/settings-dialog.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { Settings } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { Tooltip } from "~/components/deer-flow/tooltip";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "~/components/ui/dialog";
|
||||
import { Tabs, TabsContent } from "~/components/ui/tabs";
|
||||
import { useReplay } from "~/core/replay";
|
||||
import {
|
||||
type SettingsState,
|
||||
changeSettings,
|
||||
saveSettings,
|
||||
useSettingsStore,
|
||||
} from "~/core/store";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
import { SETTINGS_TABS } from "../tabs";
|
||||
|
||||
export function SettingsDialog() {
|
||||
const { isReplay } = useReplay();
|
||||
const [activeTabId, setActiveTabId] = useState(SETTINGS_TABS[0]!.id);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [settings, setSettings] = useState(useSettingsStore.getState());
|
||||
const [changes, setChanges] = useState<Partial<SettingsState>>({});
|
||||
|
||||
const handleTabChange = useCallback(
|
||||
(newChanges: Partial<SettingsState>) => {
|
||||
setTimeout(() => {
|
||||
if (open) {
|
||||
setChanges((prev) => ({
|
||||
...prev,
|
||||
...newChanges,
|
||||
}));
|
||||
}
|
||||
}, 0);
|
||||
},
|
||||
[open],
|
||||
);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
if (Object.keys(changes).length > 0) {
|
||||
const newSettings: SettingsState = {
|
||||
...settings,
|
||||
...changes,
|
||||
};
|
||||
setSettings(newSettings);
|
||||
setChanges({});
|
||||
changeSettings(newSettings);
|
||||
saveSettings();
|
||||
}
|
||||
setOpen(false);
|
||||
}, [settings, changes]);
|
||||
|
||||
const handleOpen = useCallback(() => {
|
||||
setSettings(useSettingsStore.getState());
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setChanges({});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
handleOpen();
|
||||
} else {
|
||||
handleClose();
|
||||
}
|
||||
}, [open, handleOpen, handleClose]);
|
||||
|
||||
const mergedSettings = useMemo<SettingsState>(() => {
|
||||
return {
|
||||
...settings,
|
||||
...changes,
|
||||
};
|
||||
}, [settings, changes]);
|
||||
|
||||
if (isReplay) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<Tooltip title="Settings">
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Settings />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</Tooltip>
|
||||
<DialogContent className="sm:max-w-[850px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>DeerFlow Settings</DialogTitle>
|
||||
<DialogDescription>
|
||||
Manage your DeerFlow settings here.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Tabs value={activeTabId}>
|
||||
<div className="flex h-120 w-full overflow-auto border-y">
|
||||
<ul className="flex w-50 shrink-0 border-r p-1">
|
||||
<div className="size-full">
|
||||
{SETTINGS_TABS.map((tab) => (
|
||||
<li
|
||||
key={tab.id}
|
||||
className={cn(
|
||||
"hover:accent-foreground hover:bg-accent mb-1 flex h-8 w-full cursor-pointer items-center gap-1.5 rounded px-2",
|
||||
activeTabId === tab.id &&
|
||||
"!bg-primary !text-primary-foreground",
|
||||
)}
|
||||
onClick={() => setActiveTabId(tab.id)}
|
||||
>
|
||||
<tab.icon size={16} />
|
||||
<span>{tab.label}</span>
|
||||
{tab.badge && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"border-muted-foreground text-muted-foreground ml-auto px-1 py-0 text-xs",
|
||||
activeTabId === tab.id &&
|
||||
"border-primary-foreground text-primary-foreground",
|
||||
)}
|
||||
>
|
||||
{tab.badge}
|
||||
</Badge>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</ul>
|
||||
<div className="min-w-0 flex-grow">
|
||||
<div
|
||||
id="settings-content-scrollable"
|
||||
className="size-full overflow-auto p-4"
|
||||
>
|
||||
{SETTINGS_TABS.map((tab) => (
|
||||
<TabsContent key={tab.id} value={tab.id}>
|
||||
<tab.component
|
||||
settings={mergedSettings}
|
||||
onChange={handleTabChange}
|
||||
/>
|
||||
</TabsContent>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button className="w-24" type="submit" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user