import { useState } from "react"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "~/components/ui/dialog"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; import { Switch } from "~/components/ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "~/components/ui/select"; import { Separator } from "~/components/ui/separator"; import { Loader2, Settings2 } from "lucide-react"; import { api } from "~/trpc/react"; import { toast } from "sonner"; interface RobotSettingsModalProps { open: boolean; onOpenChange: (open: boolean) => void; studyId: string; pluginId: string; settingsSchema: SettingsSchema | null; } interface SettingsSchema { type: "object"; title?: string; description?: string; properties: Record; } interface PropertySchema { type: "object" | "string" | "number" | "integer" | "boolean"; title?: string; description?: string; properties?: Record; enum?: string[]; enumNames?: string[]; minimum?: number; maximum?: number; default?: unknown; pattern?: string; } export function RobotSettingsModal({ open, onOpenChange, studyId, pluginId, settingsSchema, }: RobotSettingsModalProps) { const [settings, setSettings] = useState>({}); const [isSaving, setIsSaving] = useState(false); // Fetch current settings const { data: currentSettings, isLoading } = api.studies.getPluginConfiguration.useQuery( { studyId, pluginId }, { enabled: open }, ); // Update settings mutation const updateSettings = api.studies.updatePluginConfiguration.useMutation({ onSuccess: () => { toast.success("Robot settings updated successfully"); onOpenChange(false); }, onError: (error: { message: string }) => { toast.error(`Failed to update settings: ${error.message}`); }, }); // Initialize settings from current configuration // eslint-disable-next-line react-hooks/exhaustive-deps useState(() => { if (currentSettings) { setSettings(currentSettings as Record); } }); const handleSave = async () => { setIsSaving(true); try { await updateSettings.mutateAsync({ studyId, pluginId, configuration: settings, }); } finally { setIsSaving(false); } }; const renderField = ( key: string, schema: PropertySchema, parentPath: string = "", ) => { const fullPath = parentPath ? `${parentPath}.${key}` : key; const value = getNestedValue(settings, fullPath); const defaultValue = schema.default; const updateValue = (newValue: unknown) => { setSettings((prev) => setNestedValue({ ...prev }, fullPath, newValue)); }; // Object type - render nested fields if (schema.type === "object" && schema.properties) { return (

{schema.title || key}

{schema.description && (

{schema.description}

)}
{Object.entries(schema.properties).map(([subKey, subSchema]) => renderField(subKey, subSchema, fullPath), )}
); } // Boolean type - render switch if (schema.type === "boolean") { return (
{schema.description && (

{schema.description}

)}
); } // Enum type - render select if (schema.enum) { return (
{schema.description && (

{schema.description}

)}
); } // Number/Integer type - render number input if (schema.type === "number" || schema.type === "integer") { return (
{schema.description && (

{schema.description}

)} { const newValue = schema.type === "integer" ? parseInt(e.target.value, 10) : parseFloat(e.target.value); updateValue(isNaN(newValue) ? defaultValue : newValue); }} />
); } // String type - render text input return (
{schema.description && (

{schema.description}

)} updateValue(e.target.value)} />
); }; if (!settingsSchema) { return null; } return ( {settingsSchema.title || "Robot Settings"} {settingsSchema.description && ( {settingsSchema.description} )} {isLoading ? (
) : (
{Object.entries(settingsSchema.properties).map( ([key, schema], idx) => (
{renderField(key, schema)} {idx < Object.keys(settingsSchema.properties).length - 1 && ( )}
), )}
)}
); } // Helper functions for nested object access function getNestedValue(obj: Record, path: string): unknown { return path.split(".").reduce((current, key) => { return current && typeof current === "object" ? (current as Record)[key] : undefined; }, obj as unknown); } function setNestedValue( obj: Record, path: string, value: unknown, ): Record { const keys = path.split("."); const lastKey = keys.pop()!; const target = keys.reduce((current, key) => { if (!current[key] || typeof current[key] !== "object") { current[key] = {}; } return current[key] as Record; }, obj); target[lastKey] = value; return obj; }