mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-20 23:21:06 +00:00
268 lines
7.4 KiB
TypeScript
268 lines
7.4 KiB
TypeScript
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { BadgeInfo, Blocks, Settings, type LucideIcon } from "lucide-react";
|
|
import {
|
|
type FunctionComponent,
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { z } from "zod";
|
|
|
|
import { Button } from "~/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "~/components/ui/dialog";
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormDescription,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from "~/components/ui/form";
|
|
import { Input } from "~/components/ui/input";
|
|
import { Tabs, TabsContent } from "~/components/ui/tabs";
|
|
import {
|
|
type SettingsState,
|
|
changeSettings,
|
|
saveSettings,
|
|
useSettingsStore,
|
|
} from "~/core/store";
|
|
import { cn } from "~/lib/utils";
|
|
|
|
import { Markdown } from "../_components/markdown";
|
|
import { Tooltip } from "../_components/tooltip";
|
|
|
|
import about from "./about.md";
|
|
|
|
export function SettingsDialog() {
|
|
const [activeTabId, setActiveTabId] = useState(SETTINGS_TABS[0]!.id);
|
|
const [open, setOpen] = useState(false);
|
|
const [settings, setSettings] = useState(useSettingsStore.getState());
|
|
const changes = useRef<Partial<SettingsState>>({});
|
|
|
|
const handleTabChange = useCallback((newChanges: Partial<SettingsState>) => {
|
|
changes.current = {
|
|
...changes.current,
|
|
...newChanges,
|
|
};
|
|
}, []);
|
|
|
|
const handleSave = useCallback(() => {
|
|
if (Object.keys(changes.current).length > 0) {
|
|
const newSettings: SettingsState = {
|
|
...settings,
|
|
...changes.current,
|
|
};
|
|
setSettings(newSettings);
|
|
changes.current = {};
|
|
changeSettings(newSettings);
|
|
saveSettings();
|
|
}
|
|
setOpen(false);
|
|
}, [settings, changes]);
|
|
|
|
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-[800px]">
|
|
<DialogHeader>
|
|
<DialogTitle>DeerFlow Settings</DialogTitle>
|
|
<DialogDescription>
|
|
Manage your DeerFlow settings here.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<Tabs value={activeTabId}>
|
|
<div className="flex h-100 w-full overflow-auto border-y">
|
|
<ul className="flex w-60 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>
|
|
</li>
|
|
))}
|
|
</div>
|
|
</ul>
|
|
<div className="min-w-0 flex-grow">
|
|
<div className="size-full overflow-auto p-4">
|
|
{SETTINGS_TABS.map((tab) => (
|
|
<TabsContent key={tab.id} value={tab.id}>
|
|
<tab.component
|
|
settings={settings}
|
|
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>
|
|
);
|
|
}
|
|
|
|
type Tab = FunctionComponent<{
|
|
settings: SettingsState;
|
|
onChange: (changes: Partial<SettingsState>) => void;
|
|
}> & {
|
|
displayName?: string;
|
|
icon?: LucideIcon;
|
|
};
|
|
|
|
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.",
|
|
}),
|
|
});
|
|
|
|
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),
|
|
defaultValues: 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 (
|
|
<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>
|
|
);
|
|
};
|
|
GeneralTab.displayName = "GeneralTab";
|
|
GeneralTab.icon = Settings;
|
|
|
|
const MCPTab: Tab = () => {
|
|
return (
|
|
<div className="text-muted-foreground">
|
|
<p>Coming soon...</p>
|
|
</div>
|
|
);
|
|
};
|
|
MCPTab.icon = Blocks;
|
|
|
|
const AboutTab: Tab = () => {
|
|
return <Markdown>{about}</Markdown>;
|
|
};
|
|
AboutTab.icon = BadgeInfo;
|
|
|
|
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,
|
|
};
|
|
});
|