mirror of
https://git.mirrors.martin98.com/https://github.com/bytedance/deer-flow
synced 2025-08-19 20:29:11 +08:00
feat: implement General
tab
This commit is contained in:
parent
1e7036ed90
commit
17d53f98bd
@ -1,5 +1,15 @@
|
||||
import { BadgeInfo, Blocks, Settings } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
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 {
|
||||
@ -11,7 +21,23 @@ import {
|
||||
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";
|
||||
@ -19,27 +45,35 @@ import { Tooltip } from "../_components/tooltip";
|
||||
|
||||
import about from "./about.md";
|
||||
|
||||
const SETTINGS_TABS = [
|
||||
{
|
||||
id: "general",
|
||||
label: "General",
|
||||
icon: Settings,
|
||||
},
|
||||
{
|
||||
id: "mcp",
|
||||
label: "MCP",
|
||||
icon: Blocks,
|
||||
},
|
||||
{
|
||||
id: "about",
|
||||
label: "About",
|
||||
icon: BadgeInfo,
|
||||
},
|
||||
];
|
||||
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>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<Tooltip title="Settings">
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
@ -76,22 +110,23 @@ export function SettingsDialog() {
|
||||
</ul>
|
||||
<div className="min-w-0 flex-grow">
|
||||
<div className="size-full overflow-auto p-4">
|
||||
<TabsContent value="general">
|
||||
<div>General</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="mcp">
|
||||
<div>Coming soon...</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="about">
|
||||
<Markdown>{about}</Markdown>
|
||||
</TabsContent>
|
||||
{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">Cancel</Button>
|
||||
<Button className="w-24" type="submit">
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button className="w-24" type="submit" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@ -99,3 +134,134 @@ export function SettingsDialog() {
|
||||
</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,
|
||||
};
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user