refactor: extract components folder

This commit is contained in:
Li Xin
2025-05-02 10:43:14 +08:00
parent 18d896d15d
commit fdfc607747
44 changed files with 44 additions and 44 deletions

View 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 &quot;Add&quot; 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>
);
}

View 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>
);
}