mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-02-04 23:46:32 -05:00
feat: Implement dynamic plugin definition loading from remote/local sources and standardize action IDs using plugin metadata.
This commit is contained in:
@@ -161,7 +161,7 @@ export default function NaoTestPage() {
|
||||
data.topic?.includes("touch") ||
|
||||
data.topic?.includes("sonar")
|
||||
) {
|
||||
setSensorData((prev) => ({
|
||||
setSensorData((prev: any) => ({
|
||||
...prev,
|
||||
[data.topic]: data.msg,
|
||||
}));
|
||||
@@ -196,14 +196,14 @@ export default function NaoTestPage() {
|
||||
|
||||
const walkForward = () => {
|
||||
publishMessage("/cmd_vel", "geometry_msgs/Twist", {
|
||||
linear: { x: walkSpeed[0], y: 0, z: 0 },
|
||||
linear: { x: walkSpeed[0] ?? 0, y: 0, z: 0 },
|
||||
angular: { x: 0, y: 0, z: 0 },
|
||||
});
|
||||
};
|
||||
|
||||
const walkBackward = () => {
|
||||
publishMessage("/cmd_vel", "geometry_msgs/Twist", {
|
||||
linear: { x: -walkSpeed[0], y: 0, z: 0 },
|
||||
linear: { x: -(walkSpeed[0] ?? 0), y: 0, z: 0 },
|
||||
angular: { x: 0, y: 0, z: 0 },
|
||||
});
|
||||
};
|
||||
@@ -211,14 +211,14 @@ export default function NaoTestPage() {
|
||||
const turnLeft = () => {
|
||||
publishMessage("/cmd_vel", "geometry_msgs/Twist", {
|
||||
linear: { x: 0, y: 0, z: 0 },
|
||||
angular: { x: 0, y: 0, z: turnSpeed[0] },
|
||||
angular: { x: 0, y: 0, z: turnSpeed[0] ?? 0 },
|
||||
});
|
||||
};
|
||||
|
||||
const turnRight = () => {
|
||||
publishMessage("/cmd_vel", "geometry_msgs/Twist", {
|
||||
linear: { x: 0, y: 0, z: 0 },
|
||||
angular: { x: 0, y: 0, z: -turnSpeed[0] },
|
||||
angular: { x: 0, y: 0, z: -(turnSpeed[0] ?? 0) },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -232,7 +232,7 @@ export default function NaoTestPage() {
|
||||
const moveHead = () => {
|
||||
publishMessage("/joint_angles", "naoqi_bridge_msgs/JointAnglesWithSpeed", {
|
||||
joint_names: ["HeadYaw", "HeadPitch"],
|
||||
joint_angles: [headYaw[0], headPitch[0]],
|
||||
joint_angles: [headYaw[0] ?? 0, headPitch[0] ?? 0],
|
||||
speed: 0.3,
|
||||
});
|
||||
};
|
||||
@@ -365,7 +365,7 @@ export default function NaoTestPage() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Walk Speed: {walkSpeed[0].toFixed(2)} m/s</Label>
|
||||
<Label>Walk Speed: {(walkSpeed[0] ?? 0).toFixed(2)} m/s</Label>
|
||||
<Slider
|
||||
value={walkSpeed}
|
||||
onValueChange={setWalkSpeed}
|
||||
@@ -375,7 +375,7 @@ export default function NaoTestPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Turn Speed: {turnSpeed[0].toFixed(2)} rad/s</Label>
|
||||
<Label>Turn Speed: {(turnSpeed[0] ?? 0).toFixed(2)} rad/s</Label>
|
||||
<Slider
|
||||
value={turnSpeed}
|
||||
onValueChange={setTurnSpeed}
|
||||
@@ -415,7 +415,7 @@ export default function NaoTestPage() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Head Yaw: {headYaw[0].toFixed(2)} rad</Label>
|
||||
<Label>Head Yaw: {(headYaw[0] ?? 0).toFixed(2)} rad</Label>
|
||||
<Slider
|
||||
value={headYaw}
|
||||
onValueChange={setHeadYaw}
|
||||
@@ -425,7 +425,7 @@ export default function NaoTestPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Head Pitch: {headPitch[0].toFixed(2)} rad</Label>
|
||||
<Label>Head Pitch: {(headPitch[0] ?? 0).toFixed(2)} rad</Label>
|
||||
<Slider
|
||||
value={headPitch}
|
||||
onValueChange={setHeadPitch}
|
||||
|
||||
@@ -260,7 +260,6 @@ export default function StudyAnalyticsPage() {
|
||||
setSelectedTrialId={setSelectedTrialId}
|
||||
trialsList={trialsList ?? []}
|
||||
isLoadingList={isLoadingList}
|
||||
studyId={studyId}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
@@ -26,21 +26,17 @@ import {
|
||||
import { toast } from "sonner";
|
||||
import { api } from "~/trpc/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { type Experiment } from "~/lib/experiments/types";
|
||||
import { type experiments, experimentStatusEnum } from "~/server/db/schema";
|
||||
import { type InferSelectModel } from "drizzle-orm";
|
||||
|
||||
type Experiment = InferSelectModel<typeof experiments>;
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2, {
|
||||
message: "Name must be at least 2 characters.",
|
||||
}),
|
||||
description: z.string().optional(),
|
||||
status: z.enum([
|
||||
"draft",
|
||||
"ready",
|
||||
"data_collection",
|
||||
"analysis",
|
||||
"completed",
|
||||
"archived",
|
||||
]),
|
||||
status: z.enum(experimentStatusEnum.enumValues),
|
||||
});
|
||||
|
||||
interface ExperimentFormProps {
|
||||
@@ -133,11 +129,9 @@ export function ExperimentForm({ experiment }: ExperimentFormProps) {
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="draft">Draft</SelectItem>
|
||||
<SelectItem value="testing">Testing</SelectItem>
|
||||
<SelectItem value="ready">Ready</SelectItem>
|
||||
<SelectItem value="data_collection">Data Collection</SelectItem>
|
||||
<SelectItem value="analysis">Analysis</SelectItem>
|
||||
<SelectItem value="completed">Completed</SelectItem>
|
||||
<SelectItem value="archived">Archived</SelectItem>
|
||||
<SelectItem value="deprecated">Deprecated</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { type Experiment } from "~/lib/experiments/types";
|
||||
import { type experiments } from "~/server/db/schema";
|
||||
import { type InferSelectModel } from "drizzle-orm";
|
||||
|
||||
type Experiment = InferSelectModel<typeof experiments>;
|
||||
import { api } from "~/trpc/server";
|
||||
import { ExperimentForm } from "./experiment-form";
|
||||
import {
|
||||
@@ -43,14 +46,6 @@ export default async function ExperimentEditPage({
|
||||
title="Edit Experiment"
|
||||
subtitle={`Update settings for ${experiment.name}`}
|
||||
icon="Edit"
|
||||
backButton={
|
||||
<Button variant="ghost" size="sm" asChild className="-ml-2 mb-2">
|
||||
<Link href={`/studies/${studyId}/experiments/${experimentId}`}>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Experiment
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="max-w-2xl">
|
||||
|
||||
@@ -39,11 +39,10 @@ export default async function ParticipantDetailPage({
|
||||
title={participant.participantCode}
|
||||
subtitle={participant.name ?? "Unnamed Participant"}
|
||||
icon="Users"
|
||||
badge={
|
||||
<Badge variant={participant.consentGiven ? "default" : "secondary"}>
|
||||
{participant.consentGiven ? "Consent Given" : "No Consent"}
|
||||
</Badge>
|
||||
}
|
||||
status={{
|
||||
label: participant.consentGiven ? "Consent Given" : "No Consent",
|
||||
variant: participant.consentGiven ? "default" : "secondary"
|
||||
}}
|
||||
actions={
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href={`/studies/${studyId}/participants/${participantId}/edit`}>
|
||||
|
||||
@@ -318,20 +318,11 @@ export class ActionRegistry {
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
}>;
|
||||
metadata?: Record<string, any>;
|
||||
};
|
||||
}>,
|
||||
): void {
|
||||
console.log("ActionRegistry.loadPluginActions called with:", {
|
||||
studyId,
|
||||
pluginCount: studyPlugins?.length ?? 0,
|
||||
plugins: studyPlugins?.map((sp) => ({
|
||||
id: sp.plugin.id,
|
||||
actionCount: Array.isArray(sp.plugin.actionDefinitions)
|
||||
? sp.plugin.actionDefinitions.length
|
||||
: 0,
|
||||
hasActionDefs: !!sp.plugin.actionDefinitions,
|
||||
})),
|
||||
});
|
||||
// console.log("ActionRegistry.loadPluginActions called with:", { studyId, pluginCount: studyPlugins?.length ?? 0 });
|
||||
|
||||
if (this.pluginActionsLoaded && this.loadedStudyId === studyId) return;
|
||||
|
||||
@@ -347,11 +338,7 @@ export class ActionRegistry {
|
||||
? plugin.actionDefinitions
|
||||
: undefined;
|
||||
|
||||
console.log(`Plugin ${plugin.id}:`, {
|
||||
actionDefinitions: plugin.actionDefinitions,
|
||||
isArray: Array.isArray(plugin.actionDefinitions),
|
||||
actionCount: actionDefs?.length ?? 0,
|
||||
});
|
||||
// console.log(`Plugin ${plugin.id}:`, { actionCount: actionDefs?.length ?? 0 });
|
||||
|
||||
if (!actionDefs) return;
|
||||
|
||||
@@ -399,9 +386,13 @@ export class ActionRegistry {
|
||||
retryable: action.retryable,
|
||||
};
|
||||
|
||||
// Extract semantic ID from metadata if available, otherwise fall back to database IDs (which typically causes mismatch if seed uses semantic)
|
||||
// Ideally, plugin.metadata.robotId should populate this.
|
||||
const semanticRobotId = plugin.metadata?.robotId || plugin.robotId || plugin.id;
|
||||
|
||||
const actionDef: ActionDefinition = {
|
||||
id: `${plugin.robotId ?? plugin.id}.${action.id}`,
|
||||
type: `${plugin.robotId ?? plugin.id}.${action.id}`,
|
||||
id: `${semanticRobotId}.${action.id}`,
|
||||
type: `${semanticRobotId}.${action.id}`,
|
||||
name: action.name,
|
||||
description: action.description ?? "",
|
||||
category,
|
||||
@@ -412,7 +403,7 @@ export class ActionRegistry {
|
||||
),
|
||||
source: {
|
||||
kind: "plugin",
|
||||
pluginId: plugin.robotId ?? plugin.id,
|
||||
pluginId: semanticRobotId, // Use semantic ID here too
|
||||
robotId: plugin.robotId,
|
||||
pluginVersion: plugin.version ?? undefined,
|
||||
baseActionId: action.id,
|
||||
@@ -439,15 +430,8 @@ export class ActionRegistry {
|
||||
console.log(
|
||||
`ActionRegistry: Loaded ${totalActionsLoaded} plugin actions for study ${studyId}`,
|
||||
);
|
||||
console.log("Current action registry state:", {
|
||||
totalActions: this.actions.size,
|
||||
actionsByCategory: {
|
||||
wizard: this.getActionsByCategory("wizard").length,
|
||||
robot: this.getActionsByCategory("robot").length,
|
||||
control: this.getActionsByCategory("control").length,
|
||||
observation: this.getActionsByCategory("observation").length,
|
||||
},
|
||||
});
|
||||
// console.log("Current action registry state:", { totalActions: this.actions.size });
|
||||
|
||||
|
||||
this.pluginActionsLoaded = true;
|
||||
this.loadedStudyId = studyId;
|
||||
|
||||
@@ -180,6 +180,8 @@ export function DesignerRoot({
|
||||
robotId: sp.plugin.robotId ?? "",
|
||||
name: sp.plugin.name,
|
||||
version: sp.plugin.version,
|
||||
actionDefinitions: sp.plugin.actionDefinitions as any[],
|
||||
metadata: sp.plugin.metadata as Record<string, any>,
|
||||
})),
|
||||
[studyPluginsRaw],
|
||||
);
|
||||
@@ -272,11 +274,11 @@ export function DesignerRoot({
|
||||
if (initialized) return;
|
||||
if (loadingExperiment && !initialDesign) return;
|
||||
|
||||
console.log('[DesignerRoot] 🚀 INITIALIZING', {
|
||||
hasExperiment: !!experiment,
|
||||
hasInitialDesign: !!initialDesign,
|
||||
loadingExperiment,
|
||||
});
|
||||
// console.log('[DesignerRoot] 🚀 INITIALIZING', {
|
||||
// hasExperiment: !!experiment,
|
||||
// hasInitialDesign: !!initialDesign,
|
||||
// loadingExperiment,
|
||||
// });
|
||||
|
||||
const adapted =
|
||||
initialDesign ??
|
||||
@@ -304,7 +306,7 @@ export function DesignerRoot({
|
||||
setInitialized(true);
|
||||
// NOTE: We don't call recomputeHash() here because the automatic
|
||||
// hash recomputation useEffect will trigger when setSteps() updates the steps array
|
||||
console.log('[DesignerRoot] 🚀 Initialization complete, steps set');
|
||||
// console.log('[DesignerRoot] 🚀 Initialization complete, steps set');
|
||||
}, [
|
||||
initialized,
|
||||
loadingExperiment,
|
||||
@@ -346,7 +348,7 @@ export function DesignerRoot({
|
||||
// Small delay to ensure all components have rendered
|
||||
const timer = setTimeout(() => {
|
||||
setIsReady(true);
|
||||
console.log('[DesignerRoot] ✅ Designer ready (plugins loaded), fading in');
|
||||
// console.log('[DesignerRoot] ✅ Designer ready (plugins loaded), fading in');
|
||||
}, 150);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
@@ -357,19 +359,13 @@ export function DesignerRoot({
|
||||
useEffect(() => {
|
||||
if (!initialized) return;
|
||||
|
||||
console.log('[DesignerRoot] Steps changed, scheduling hash recomputation', {
|
||||
stepsCount: steps.length,
|
||||
actionsCount: steps.reduce((sum, s) => sum + s.actions.length, 0),
|
||||
});
|
||||
// console.log('[DesignerRoot] Steps changed, scheduling hash recomputation');
|
||||
|
||||
const timeoutId = setTimeout(async () => {
|
||||
console.log('[DesignerRoot] Executing debounced hash recomputation');
|
||||
// console.log('[DesignerRoot] Executing debounced hash recomputation');
|
||||
const result = await recomputeHash();
|
||||
if (result) {
|
||||
console.log('[DesignerRoot] Hash recomputed:', {
|
||||
newHash: result.designHash.slice(0, 16),
|
||||
fullHash: result.designHash,
|
||||
});
|
||||
// console.log('[DesignerRoot] Hash recomputed:', result.designHash.slice(0, 16));
|
||||
}
|
||||
}, 300); // Debounce 300ms
|
||||
|
||||
@@ -383,13 +379,12 @@ export function DesignerRoot({
|
||||
|
||||
// Debug logging to track hash updates and save button state
|
||||
useEffect(() => {
|
||||
console.log('[DesignerRoot] Hash State:', {
|
||||
currentDesignHash: currentDesignHash?.slice(0, 10),
|
||||
lastPersistedHash: lastPersistedHash?.slice(0, 10),
|
||||
hasUnsavedChanges,
|
||||
stepsCount: steps.length,
|
||||
});
|
||||
}, [currentDesignHash, lastPersistedHash, hasUnsavedChanges, steps.length]);
|
||||
// console.log('[DesignerRoot] Hash State:', {
|
||||
// currentDesignHash: currentDesignHash?.slice(0, 10),
|
||||
// lastPersistedHash: lastPersistedHash?.slice(0, 10),
|
||||
// hasUnsavedChanges,
|
||||
// });
|
||||
}, [currentDesignHash, lastPersistedHash, hasUnsavedChanges]);
|
||||
|
||||
/* ------------------------------- Step Ops -------------------------------- */
|
||||
const createNewStep = useCallback(() => {
|
||||
@@ -426,13 +421,28 @@ export function DesignerRoot({
|
||||
});
|
||||
// Debug: log validation results for troubleshooting
|
||||
|
||||
console.debug("[DesignerRoot] validation", {
|
||||
valid: result.valid,
|
||||
errors: result.errorCount,
|
||||
warnings: result.warningCount,
|
||||
infos: result.infoCount,
|
||||
issues: result.issues,
|
||||
});
|
||||
// Debug: Improved structured logging for validation results
|
||||
console.group("🧪 Experiment Validation Results");
|
||||
if (result.valid) {
|
||||
console.log(`%c✓ VALID (0 errors, ${result.warningCount} warnings, ${result.infoCount} hints)`, "color: green; font-weight: bold; font-size: 12px;");
|
||||
} else {
|
||||
console.log(`%c✗ INVALID (${result.errorCount} errors, ${result.warningCount} warnings)`, "color: red; font-weight: bold; font-size: 12px;");
|
||||
}
|
||||
|
||||
if (result.issues.length > 0) {
|
||||
console.table(
|
||||
result.issues.map(i => ({
|
||||
Severity: i.severity.toUpperCase(),
|
||||
Category: i.category,
|
||||
Message: i.message,
|
||||
Suggest: i.suggestion,
|
||||
Location: i.actionId ? `Action ${i.actionId}` : (i.stepId ? `Step ${i.stepId}` : 'Global')
|
||||
}))
|
||||
);
|
||||
} else {
|
||||
console.log("No issues found. Design is perfectly compliant.");
|
||||
}
|
||||
console.groupEnd();
|
||||
// Persist issues to store for inspector rendering
|
||||
const grouped = groupIssuesByEntity(result.issues);
|
||||
clearAllValidationIssues();
|
||||
|
||||
@@ -232,9 +232,9 @@ export function PropertiesPanelBase({
|
||||
</Badge>
|
||||
{/* internal plugin identifiers hidden from UI */}
|
||||
<Badge variant="outline" className="h-4 text-[10px]">
|
||||
{selectedAction.execution.transport}
|
||||
{selectedAction.execution?.transport}
|
||||
</Badge>
|
||||
{selectedAction.execution.retryable && (
|
||||
{selectedAction.execution?.retryable && (
|
||||
<Badge variant="outline" className="h-4 text-[10px]">
|
||||
retryable
|
||||
</Badge>
|
||||
@@ -473,7 +473,7 @@ const ParameterEditor = React.memo(function ParameterEditor({
|
||||
}: ParameterEditorProps) {
|
||||
// Local state for immediate feedback
|
||||
const [localValue, setLocalValue] = useState<unknown>(rawValue);
|
||||
const debounceRef = useRef<NodeJS.Timeout | undefined>();
|
||||
const debounceRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||
|
||||
// Sync from prop if it changes externally
|
||||
useEffect(() => {
|
||||
|
||||
@@ -207,6 +207,18 @@ export function InspectorPanel({
|
||||
/* ------------------------------------------------------------------------ */
|
||||
/* Render */
|
||||
/* ------------------------------------------------------------------------ */
|
||||
const designObject = useMemo(
|
||||
() => ({
|
||||
id: "design",
|
||||
name: "Design",
|
||||
description: "",
|
||||
version: 1,
|
||||
steps,
|
||||
lastSaved: new Date(),
|
||||
}),
|
||||
[steps],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -284,17 +296,7 @@ export function InspectorPanel({
|
||||
<div className="flex-1 overflow-x-hidden overflow-y-auto">
|
||||
<div className="w-full px-0 py-2 break-words whitespace-normal">
|
||||
<PropertiesPanel
|
||||
design={useMemo(
|
||||
() => ({
|
||||
id: "design",
|
||||
name: "Design",
|
||||
description: "",
|
||||
version: 1,
|
||||
steps,
|
||||
lastSaved: new Date(),
|
||||
}),
|
||||
[steps],
|
||||
)}
|
||||
design={designObject}
|
||||
selectedStep={selectedStep}
|
||||
selectedAction={selectedAction}
|
||||
onActionUpdate={handleActionUpdate}
|
||||
|
||||
@@ -659,8 +659,8 @@ export function validateExecution(
|
||||
const robotActions = steps.flatMap((step) =>
|
||||
step.actions.filter(
|
||||
(action) =>
|
||||
action.execution.transport === "ros2" ||
|
||||
action.execution.transport === "rest",
|
||||
action.execution?.transport === "ros2" ||
|
||||
action.execution?.transport === "rest",
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -505,7 +505,7 @@ export function ParticipantForm({
|
||||
error={error}
|
||||
onDelete={mode === "edit" ? onDelete : undefined}
|
||||
isDeleting={isDeleting}
|
||||
isDeleting={isDeleting}
|
||||
|
||||
// sidebar={sidebar} // Removed for cleaner UI per user request
|
||||
submitText={mode === "create" ? "Register Participant" : "Save Changes"}
|
||||
>
|
||||
|
||||
@@ -188,7 +188,7 @@ export function EventTimeline() {
|
||||
<div className="text-[10px] font-mono opacity-70 mb-1">
|
||||
{new Date(event.timestamp).toLocaleTimeString()}
|
||||
</div>
|
||||
{event.data && (
|
||||
{!!event.data && (
|
||||
<div className="bg-muted/50 p-1 rounded font-mono text-[9px] max-w-[200px] break-all">
|
||||
{JSON.stringify(event.data as object).slice(0, 100)}
|
||||
</div>
|
||||
|
||||
@@ -114,8 +114,8 @@ export function PlaybackPlayer({ src }: PlaybackPlayerProps) {
|
||||
step={0.1}
|
||||
onValueChange={([val]) => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.currentTime = val;
|
||||
setCurrentTime(val);
|
||||
videoRef.current.currentTime = val ?? 0;
|
||||
setCurrentTime(val ?? 0);
|
||||
}
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
|
||||
@@ -156,7 +156,7 @@ export function TrialAnalysisView({ trial }: TrialAnalysisViewProps) {
|
||||
{event.eventType.replace(/_/g, " ")}
|
||||
</span>
|
||||
</div>
|
||||
{event.data && (
|
||||
{!!event.data && (
|
||||
<div className="text-[10px] text-muted-foreground bg-muted p-1.5 rounded border font-mono whitespace-pre-wrap break-all opacity-80 group-hover:opacity-100">
|
||||
{JSON.stringify(event.data as object, null, 1).replace(/"/g, '').replace(/[{}]/g, '').trim()}
|
||||
</div>
|
||||
|
||||
@@ -255,10 +255,10 @@ export function RobotActionsPanel({
|
||||
// Look for ROS2 configuration in the action definition
|
||||
const actionConfig = (actionDef as any).ros2
|
||||
? {
|
||||
topic: (actionDef as any).ros2.topic,
|
||||
messageType: (actionDef as any).ros2.messageType,
|
||||
payloadMapping: (actionDef as any).ros2.payloadMapping,
|
||||
}
|
||||
topic: (actionDef as any).ros2.topic,
|
||||
messageType: (actionDef as any).ros2.messageType,
|
||||
payloadMapping: (actionDef as any).ros2.payloadMapping,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
await executeRosAction(
|
||||
@@ -635,7 +635,7 @@ export function RobotActionsPanel({
|
||||
<CardContent className="space-y-4">
|
||||
{/* Parameters */}
|
||||
{selectedAction.parameters &&
|
||||
selectedAction.parameters.length > 0 ? (
|
||||
selectedAction.parameters.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
<Label className="text-base">Parameters</Label>
|
||||
{selectedAction.parameters.map((param, index) =>
|
||||
@@ -662,9 +662,9 @@ export function RobotActionsPanel({
|
||||
className="w-full"
|
||||
>
|
||||
{selectedPluginData &&
|
||||
executingActions.has(
|
||||
`${selectedPluginData.plugin.name}.${selectedAction.id}`,
|
||||
) ? (
|
||||
executingActions.has(
|
||||
`${selectedPluginData.plugin.name}.${selectedAction.id}`,
|
||||
) ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Executing...
|
||||
@@ -900,50 +900,50 @@ export function RobotActionsPanel({
|
||||
{selectedPluginData &&
|
||||
Object.entries(
|
||||
groupActionsByCategory(
|
||||
(selectedPluginData.plugin
|
||||
(selectedPluginData?.plugin
|
||||
.actionDefinitions as RobotAction[]) ?? [],
|
||||
),
|
||||
).map(([category, actions]) => {
|
||||
const CategoryIcon = getCategoryIcon(category);
|
||||
const isExpanded = expandedCategories.has(category);
|
||||
const CategoryIcon = getCategoryIcon(category);
|
||||
const isExpanded = expandedCategories.has(category);
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
key={category}
|
||||
open={isExpanded}
|
||||
onOpenChange={() => toggleCategory(category)}
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-start p-2"
|
||||
>
|
||||
<CategoryIcon className="mr-2 h-4 w-4" />
|
||||
{category.charAt(0).toUpperCase() + category.slice(1)}
|
||||
<Badge variant="secondary" className="ml-auto">
|
||||
{actions.length}
|
||||
</Badge>
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="ml-6 space-y-1">
|
||||
{actions.map((action) => (
|
||||
return (
|
||||
<Collapsible
|
||||
key={category}
|
||||
open={isExpanded}
|
||||
onOpenChange={() => toggleCategory(category)}
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
key={action.id}
|
||||
variant={
|
||||
selectedAction?.id === action.id
|
||||
? "default"
|
||||
: "ghost"
|
||||
}
|
||||
className="w-full justify-start text-sm"
|
||||
onClick={() => setSelectedAction(action)}
|
||||
variant="ghost"
|
||||
className="w-full justify-start p-2"
|
||||
>
|
||||
{action.name}
|
||||
<CategoryIcon className="mr-2 h-4 w-4" />
|
||||
{category.charAt(0).toUpperCase() + category.slice(1)}
|
||||
<Badge variant="secondary" className="ml-auto">
|
||||
{actions.length}
|
||||
</Badge>
|
||||
</Button>
|
||||
))}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
})}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="ml-6 space-y-1">
|
||||
{actions.map((action) => (
|
||||
<Button
|
||||
key={action.id}
|
||||
variant={
|
||||
selectedAction?.id === action.id
|
||||
? "default"
|
||||
: "ghost"
|
||||
}
|
||||
className="w-full justify-start text-sm"
|
||||
onClick={() => setSelectedAction(action)}
|
||||
>
|
||||
{action.name}
|
||||
</Button>
|
||||
))}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
@@ -962,7 +962,7 @@ export function RobotActionsPanel({
|
||||
<CardContent className="space-y-4">
|
||||
{/* Parameters */}
|
||||
{selectedAction?.parameters &&
|
||||
(selectedAction.parameters?.length ?? 0) > 0 ? (
|
||||
(selectedAction?.parameters?.length ?? 0) > 0 ? (
|
||||
<div className="space-y-4">
|
||||
<Label className="text-base">Parameters</Label>
|
||||
{selectedAction?.parameters?.map((param, index) =>
|
||||
@@ -990,10 +990,10 @@ export function RobotActionsPanel({
|
||||
className="w-full"
|
||||
>
|
||||
{selectedPluginData &&
|
||||
selectedAction &&
|
||||
executingActions.has(
|
||||
`${selectedPluginData?.plugin.name}.${selectedAction?.id}`,
|
||||
) ? (
|
||||
selectedAction &&
|
||||
executingActions.has(
|
||||
`${selectedPluginData?.plugin.name}.${selectedAction?.id}`,
|
||||
) ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Executing...
|
||||
|
||||
@@ -149,10 +149,10 @@ export function convertActionToDatabase(
|
||||
pluginVersion: action.source.pluginVersion,
|
||||
robotId: action.source.robotId,
|
||||
baseActionId: action.source.baseActionId,
|
||||
transport: action.execution.transport,
|
||||
ros2: action.execution.ros2,
|
||||
rest: action.execution.rest,
|
||||
retryable: action.execution.retryable,
|
||||
transport: action.execution?.transport,
|
||||
ros2: action.execution?.ros2,
|
||||
rest: action.execution?.rest,
|
||||
retryable: action.execution?.retryable,
|
||||
parameterSchemaRaw: action.parameterSchemaRaw,
|
||||
sourceKind: action.source.kind,
|
||||
category: action.category,
|
||||
|
||||
@@ -267,7 +267,7 @@ export function parseVisualDesignSteps(raw: unknown): {
|
||||
if (!act.source.kind) {
|
||||
issues.push(`Action "${act.id}" missing source.kind`);
|
||||
}
|
||||
if (!act.execution.transport) {
|
||||
if (!act.execution?.transport) {
|
||||
issues.push(`Action "${act.id}" missing execution transport`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,6 +471,7 @@ export const robotsRouter = createTRPCRouter({
|
||||
actionDefinitions: plugins.actionDefinitions,
|
||||
createdAt: plugins.createdAt,
|
||||
updatedAt: plugins.updatedAt,
|
||||
metadata: plugins.metadata,
|
||||
},
|
||||
installation: {
|
||||
id: studyPlugins.id,
|
||||
|
||||
Reference in New Issue
Block a user