diff --git a/web/src/app/_dialogs/settings-dialog.tsx b/web/src/app/_dialogs/settings-dialog.tsx index 44064a5..c663549 100644 --- a/web/src/app/_dialogs/settings-dialog.tsx +++ b/web/src/app/_dialogs/settings-dialog.tsx @@ -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>({}); + + const handleTabChange = useCallback((newChanges: Partial) => { + 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 ( - + - + @@ -99,3 +134,134 @@ export function SettingsDialog() { ); } + +type Tab = FunctionComponent<{ + settings: SettingsState; + onChange: (changes: Partial) => 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) => void; +}) => { + const generalSettings = useMemo(() => settings.general, [settings]); + const form = useForm>({ + 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 ( +
+ + ( + + Max plan iterations + + + field.onChange(parseInt(event.target.value)) + } + /> + + + Set to 1 for single-step planning. Set to 2 to enable + re-planning. + + + + )} + /> + ( + + Max steps of a research plan + + + field.onChange(parseInt(event.target.value)) + } + /> + + + By default, each research plan has 3 steps. + + + + )} + /> + + + ); +}; +GeneralTab.displayName = "GeneralTab"; +GeneralTab.icon = Settings; + +const MCPTab: Tab = () => { + return ( +
+

Coming soon...

+
+ ); +}; +MCPTab.icon = Blocks; + +const AboutTab: Tab = () => { + return {about}; +}; +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 ?? ) as LucideIcon, + component: tab, + }; +});