diff --git a/robot-plugins b/robot-plugins index 334dc68..ad8dcef 160000 --- a/robot-plugins +++ b/robot-plugins @@ -1 +1 @@ -Subproject commit 334dc68a22b1265b44e5991b8939d3589718ecca +Subproject commit ad8dcefe97b2a53f00ed1430a49ec0d3d15ab116 diff --git a/src/components/trials/wizard/RobotActionsPanel.tsx b/src/components/trials/wizard/RobotActionsPanel.tsx new file mode 100644 index 0000000..2e75327 --- /dev/null +++ b/src/components/trials/wizard/RobotActionsPanel.tsx @@ -0,0 +1,614 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { + Bot, + Play, + Settings, + AlertCircle, + CheckCircle, + Loader2, + Volume2, + Move, + Eye, + Hand, + Zap, +} from "lucide-react"; +import { Button } from "~/components/ui/button"; +import { Badge } from "~/components/ui/badge"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { Textarea } from "~/components/ui/textarea"; +import { Slider } from "~/components/ui/slider"; +import { Switch } from "~/components/ui/switch"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "~/components/ui/select"; +import { ScrollArea } from "~/components/ui/scroll-area"; +import { Separator } from "~/components/ui/separator"; +import { Alert, AlertDescription } from "~/components/ui/alert"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "~/components/ui/card"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "~/components/ui/collapsible"; +import { api } from "~/trpc/react"; +import { toast } from "sonner"; + +interface RobotAction { + id: string; + name: string; + description: string; + category: string; + parameters?: Array<{ + name: string; + type: "text" | "number" | "boolean" | "select"; + description: string; + required: boolean; + min?: number; + max?: number; + step?: number; + default?: unknown; + options?: Array<{ value: string; label: string }>; + placeholder?: string; + maxLength?: number; + }>; +} + +interface Plugin { + plugin: { + id: string; + name: string; + version: string; + description: string; + trustLevel: string; + actionDefinitions: RobotAction[]; + }; + installation: { + id: string; + configuration: Record; + installedAt: Date; + }; +} + +interface RobotActionsPanelProps { + studyId: string; + trialId: string; + onExecuteAction: ( + pluginName: string, + actionId: string, + parameters: Record, + ) => Promise; +} + +export function RobotActionsPanel({ + studyId, + trialId, + onExecuteAction, +}: RobotActionsPanelProps) { + const [selectedPlugin, setSelectedPlugin] = useState(""); + const [selectedAction, setSelectedAction] = useState( + null, + ); + const [actionParameters, setActionParameters] = useState< + Record + >({}); + const [executingActions, setExecutingActions] = useState>( + new Set(), + ); + const [expandedCategories, setExpandedCategories] = useState>( + new Set(["movement", "speech"]), + ); + + // Get installed plugins for the study + const { data: plugins = [], isLoading } = + api.robots.plugins.getStudyPlugins.useQuery({ + studyId, + }); + + // Get actions for selected plugin + const selectedPluginData = plugins.find( + (p) => p.plugin.id === selectedPlugin, + ); + + // Initialize parameters when action changes + useEffect(() => { + if (selectedAction) { + const defaultParams: Record = {}; + + selectedAction.parameters?.forEach((param) => { + if (param.default !== undefined) { + defaultParams[param.name] = param.default; + } else if (param.required) { + // Set reasonable defaults for required params + switch (param.type) { + case "text": + defaultParams[param.name] = ""; + break; + case "number": + defaultParams[param.name] = param.min ?? 0; + break; + case "boolean": + defaultParams[param.name] = false; + break; + case "select": + defaultParams[param.name] = param.options?.[0]?.value ?? ""; + break; + } + } + }); + + setActionParameters(defaultParams); + } else { + setActionParameters({}); + } + }, [selectedAction]); + + const handleExecuteAction = async () => { + if (!selectedAction || !selectedPluginData) return; + + const actionKey = `${selectedPluginData.plugin.name}.${selectedAction.id}`; + setExecutingActions((prev) => new Set([...prev, actionKey])); + + try { + await onExecuteAction( + selectedPluginData.plugin.name, + selectedAction.id, + actionParameters, + ); + + toast.success(`Executed: ${selectedAction.name}`, { + description: `Robot action completed successfully`, + }); + } catch (error) { + toast.error(`Failed to execute: ${selectedAction.name}`, { + description: error instanceof Error ? error.message : "Unknown error", + }); + } finally { + setExecutingActions((prev) => { + const next = new Set(prev); + next.delete(actionKey); + return next; + }); + } + }; + + const handleParameterChange = (paramName: string, value: unknown) => { + setActionParameters((prev) => ({ + ...prev, + [paramName]: value, + })); + }; + + const renderParameterInput = ( + param: NonNullable[0], + paramIndex: number, + ) => { + if (!param) return null; + + const value = actionParameters[param.name]; + + switch (param.type) { + case "text": + return ( +
+ + {param.maxLength && param.maxLength > 100 ? ( +