feat: implement General tab

This commit is contained in:
Li Xin 2025-04-23 21:18:47 +08:00
parent 1e7036ed90
commit 17d53f98bd

View File

@ -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,
};
});