mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-09-23 04:20:16 -04:00
278 lines
8.4 KiB
TypeScript
Executable File
278 lines
8.4 KiB
TypeScript
Executable File
import type {
|
|
ExperimentStep,
|
|
ExperimentAction,
|
|
ExecutionDescriptor,
|
|
} from "./types";
|
|
|
|
// Convert step-based design to database records
|
|
export function convertStepsToDatabase(
|
|
steps: ExperimentStep[],
|
|
): ConvertedStep[] {
|
|
return steps.map((step, index) => ({
|
|
name: step.name,
|
|
description: step.description,
|
|
type: mapStepTypeToDatabase(step.type),
|
|
orderIndex: index,
|
|
durationEstimate: calculateStepDuration(step.actions),
|
|
required: true,
|
|
conditions: step.trigger.conditions,
|
|
actions: step.actions.map((action, actionIndex) =>
|
|
convertActionToDatabase(action, actionIndex),
|
|
),
|
|
}));
|
|
}
|
|
|
|
// Map designer step types to database step types
|
|
function mapStepTypeToDatabase(
|
|
stepType: ExperimentStep["type"],
|
|
): "wizard" | "robot" | "parallel" | "conditional" {
|
|
switch (stepType) {
|
|
case "sequential":
|
|
return "wizard";
|
|
case "parallel":
|
|
return "parallel";
|
|
case "conditional":
|
|
case "loop":
|
|
return "conditional";
|
|
default:
|
|
return "wizard";
|
|
}
|
|
}
|
|
|
|
// Calculate step duration from actions
|
|
function calculateStepDuration(actions: ExperimentAction[]): number {
|
|
let total = 0;
|
|
|
|
for (const action of actions) {
|
|
switch (action.type) {
|
|
case "wizard_speak":
|
|
case "robot_speak":
|
|
// Estimate based on text length if available
|
|
const text = action.parameters.text as string;
|
|
if (text) {
|
|
total += Math.max(2, text.length / 10); // ~10 chars per second
|
|
} else {
|
|
total += 3;
|
|
}
|
|
break;
|
|
case "wait":
|
|
total += (action.parameters.duration as number) || 2;
|
|
break;
|
|
case "robot_move":
|
|
total += 5; // Movement takes longer
|
|
break;
|
|
case "wizard_gesture":
|
|
total += 2;
|
|
break;
|
|
case "observe":
|
|
total += (action.parameters.duration as number) || 5;
|
|
break;
|
|
default:
|
|
total += 2; // Default duration
|
|
}
|
|
}
|
|
|
|
return Math.max(1, Math.round(total));
|
|
}
|
|
|
|
// Estimate action timeout
|
|
function estimateActionTimeout(action: ExperimentAction): number {
|
|
switch (action.type) {
|
|
case "wizard_speak":
|
|
case "robot_speak":
|
|
return 30;
|
|
case "robot_move":
|
|
return 60;
|
|
case "wait": {
|
|
const duration = action.parameters.duration as number | undefined;
|
|
return (duration ?? 2) + 10; // Add buffer
|
|
}
|
|
case "observe":
|
|
return 120; // Observation can take longer
|
|
default:
|
|
return 30;
|
|
}
|
|
}
|
|
|
|
// Database conversion types (same as before)
|
|
export interface ConvertedStep {
|
|
name: string;
|
|
description?: string;
|
|
type: "wizard" | "robot" | "parallel" | "conditional";
|
|
orderIndex: number;
|
|
durationEstimate?: number;
|
|
required: boolean;
|
|
conditions: Record<string, unknown>;
|
|
actions: ConvertedAction[];
|
|
}
|
|
|
|
export interface ConvertedAction {
|
|
name: string;
|
|
description?: string;
|
|
type: string;
|
|
orderIndex: number;
|
|
parameters: Record<string, unknown>;
|
|
timeout?: number;
|
|
// Provenance & execution metadata (flattened for now)
|
|
pluginId?: string;
|
|
pluginVersion?: string;
|
|
robotId?: string | null;
|
|
baseActionId?: string;
|
|
transport?: ExecutionDescriptor["transport"];
|
|
ros2?: ExecutionDescriptor["ros2"];
|
|
rest?: ExecutionDescriptor["rest"];
|
|
retryable?: boolean;
|
|
parameterSchemaRaw?: unknown;
|
|
sourceKind?: "core" | "plugin";
|
|
category?: string;
|
|
}
|
|
|
|
// Deprecated legacy compatibility function removed to eliminate unsafe any usage.
|
|
// If needed, implement a proper migration path elsewhere.
|
|
|
|
/**
|
|
* Convert a single experiment action into a ConvertedAction (DB shape),
|
|
* preserving provenance and execution metadata for reproducibility.
|
|
*/
|
|
export function convertActionToDatabase(
|
|
action: ExperimentAction,
|
|
orderIndex: number,
|
|
): ConvertedAction {
|
|
// Serialize nested children into parameters for storage
|
|
const parameters = { ...action.parameters };
|
|
|
|
if (action.children && action.children.length > 0) {
|
|
// Recursively convert children for container actions (sequence, parallel, loop)
|
|
// Branch actions don't have children - they control step routing
|
|
parameters.children = action.children.map((child, idx) =>
|
|
convertActionToDatabase(child, idx),
|
|
);
|
|
}
|
|
|
|
return {
|
|
name: action.name,
|
|
description: `${action.type} action`,
|
|
type: action.type,
|
|
orderIndex,
|
|
parameters,
|
|
timeout: estimateActionTimeout(action),
|
|
pluginId: action.source.pluginId,
|
|
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,
|
|
parameterSchemaRaw: action.parameterSchemaRaw,
|
|
sourceKind: action.source.kind,
|
|
category: action.category,
|
|
};
|
|
}
|
|
|
|
// Reconstruct designer steps from database records
|
|
export function convertDatabaseToSteps(
|
|
dbSteps: any[], // Typing as any[] because Drizzle types are complex to import here without circular deps
|
|
): ExperimentStep[] {
|
|
// Paranoid Sort: Ensure steps are strictly ordered by index before assigning Triggers.
|
|
// This safeguards against API returning unsorted data.
|
|
const sortedSteps = [...dbSteps].sort(
|
|
(a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0),
|
|
);
|
|
|
|
return sortedSteps.map((dbStep, idx) => {
|
|
return {
|
|
id: dbStep.id,
|
|
name: dbStep.name,
|
|
description: dbStep.description ?? undefined,
|
|
type: mapDatabaseToStepType(dbStep.type),
|
|
order: dbStep.orderIndex ?? idx, // Fallback to array index if missing
|
|
trigger: {
|
|
// Enforce Sequential Architecture: Validated by user requirement.
|
|
// Index 0 is Trial Start, all others are Previous Step.
|
|
type: idx === 0 ? "trial_start" : "previous_step",
|
|
conditions: (dbStep.conditions as Record<string, unknown>) || {},
|
|
},
|
|
expanded: true, // Default to expanded in designer
|
|
actions: (dbStep.actions || []).map((dbAction: any) =>
|
|
convertDatabaseToAction(dbAction),
|
|
),
|
|
};
|
|
});
|
|
}
|
|
|
|
function mapDatabaseToStepType(type: string): ExperimentStep["type"] {
|
|
switch (type) {
|
|
case "wizard":
|
|
return "sequential";
|
|
case "parallel":
|
|
return "parallel";
|
|
case "conditional":
|
|
return "conditional"; // Loop is also stored as conditional, distinction lost unless encoded in metadata
|
|
default:
|
|
return "sequential";
|
|
}
|
|
}
|
|
|
|
export function convertDatabaseToAction(dbAction: any): ExperimentAction {
|
|
// Reconstruct nested source object
|
|
const source: ExperimentAction["source"] = {
|
|
kind: (dbAction.sourceKind || dbAction.source_kind || "core") as
|
|
| "core"
|
|
| "plugin",
|
|
pluginId: dbAction.pluginId || dbAction.plugin_id || undefined,
|
|
pluginVersion:
|
|
dbAction.pluginVersion || dbAction.plugin_version || undefined,
|
|
robotId: dbAction.robotId || dbAction.robot_id || undefined,
|
|
baseActionId: dbAction.baseActionId || dbAction.base_action_id || undefined,
|
|
};
|
|
|
|
// Robust Inference: If properties are missing but Type suggests a plugin (e.g., "nao6-ros2.say_text"),
|
|
// assume/infer the pluginId to ensure validation passes.
|
|
if (dbAction.type && dbAction.type.includes(".") && !source.pluginId) {
|
|
const parts = dbAction.type.split(".");
|
|
if (parts.length === 2) {
|
|
source.kind = "plugin";
|
|
source.pluginId = parts[0];
|
|
// Fallback robotId if missing
|
|
if (!source.robotId) source.robotId = parts[0];
|
|
}
|
|
}
|
|
|
|
// Reconstruct execution object
|
|
const execution: ExecutionDescriptor = {
|
|
transport: dbAction.transport as ExecutionDescriptor["transport"],
|
|
ros2: dbAction.ros2 as ExecutionDescriptor["ros2"],
|
|
rest: dbAction.rest as ExecutionDescriptor["rest"],
|
|
retryable: dbAction.retryable ?? false,
|
|
};
|
|
|
|
// Convert definitions to runtime action, handling nested children
|
|
const parameters = (dbAction.parameters as Record<string, unknown>) || {};
|
|
|
|
// Hydrate nested children (Sequence, Parallel, Loop only)
|
|
// Branch actions control step routing, not nested actions
|
|
let children: ExperimentAction[] | undefined = undefined;
|
|
|
|
const paramChildren = parameters.children;
|
|
|
|
if (Array.isArray(paramChildren)) {
|
|
children = paramChildren.map((child: any) =>
|
|
convertDatabaseToAction(child),
|
|
);
|
|
}
|
|
|
|
return {
|
|
id: dbAction.id,
|
|
name: dbAction.name,
|
|
description: dbAction.description ?? undefined,
|
|
type: dbAction.type,
|
|
category: dbAction.category ?? "general",
|
|
parameters,
|
|
source,
|
|
execution,
|
|
parameterSchemaRaw: dbAction.parameterSchemaRaw,
|
|
children,
|
|
};
|
|
}
|