mirror of
https://github.com/soconnor0919/hristudio.git
synced 2025-12-11 06:34:44 -05:00
- Add plugin store system with dynamic loading of robot actions - Implement plugin store API routes and database schema - Update experiment designer to support plugin-based actions - Refactor environment configuration and sidebar navigation - Improve authentication session handling with additional user details - Update Tailwind CSS configuration and global styles - Remove deprecated files and consolidate project structure
8.5 KiB
8.5 KiB
Experiment Designer
Overview
The Experiment Designer is a core feature of HRIStudio that enables researchers to create and configure robot experiments using a visual, flow-based interface. It supports drag-and-drop functionality, real-time updates, and integration with the plugin system.
Architecture
Core Components
interface ExperimentDesignerProps {
className?: string;
defaultSteps?: Step[];
onChange?: (steps: Step[]) => void;
readOnly?: boolean;
}
export function ExperimentDesigner({
className,
defaultSteps = [],
onChange,
readOnly = false,
}: ExperimentDesignerProps) {
// Implementation
}
Data Types
export type ActionType =
| "move" // Robot movement
| "speak" // Robot speech
| "wait" // Wait for a duration
| "input" // Wait for user input
| "gesture" // Robot gesture
| "record" // Start/stop recording
| "condition" // Conditional branching
| "loop"; // Repeat actions
export interface Action {
id: string;
type: ActionType;
parameters: Record<string, any>;
order: number;
}
export interface Step {
id: string;
title: string;
description?: string;
actions: Action[];
order: number;
}
export interface Experiment {
id: number;
studyId: number;
title: string;
description?: string;
version: number;
status: "draft" | "active" | "archived";
steps: Step[];
createdAt: Date;
updatedAt: Date;
}
Visual Components
Action Node
interface ActionNodeData {
type: string;
parameters: Record<string, any>;
onChange?: (parameters: Record<string, any>) => void;
}
export const ActionNode = memo(({ data, selected }: NodeProps<ActionNodeData>) => {
const [configOpen, setConfigOpen] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const actionConfig = AVAILABLE_ACTIONS.find((a) => a.type === data.type);
return (
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.2 }}
className={cn(
"relative",
"before:absolute before:inset-[-2px] before:rounded-xl before:bg-gradient-to-br",
selected && "before:from-primary/50 before:to-primary/20"
)}
>
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-gradient-to-br">
{actionConfig?.icon}
</div>
<CardTitle>{actionConfig?.title}</CardTitle>
</div>
</CardHeader>
<CardContent>
<CardDescription>{actionConfig?.description}</CardDescription>
</CardContent>
</Card>
</motion.div>
);
});
Flow Edge
export function FlowEdge({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerEnd,
}: EdgeProps) {
const [edgePath] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
return (
<>
<BaseEdge path={edgePath} markerEnd={markerEnd} style={style} />
<motion.path
id={id}
style={{
strokeWidth: 3,
fill: "none",
stroke: "hsl(var(--primary))",
strokeDasharray: "5,5",
opacity: 0.5,
}}
d={edgePath}
animate={{
strokeDashoffset: [0, -10],
}}
transition={{
duration: 1,
repeat: Infinity,
ease: "linear",
}}
/>
</>
);
}
Action Configuration
Available Actions
export const AVAILABLE_ACTIONS: ActionConfig[] = [
{
type: "move",
title: "Move Robot",
description: "Move the robot to a specific position",
icon: <Move className="h-4 w-4" />,
defaultParameters: {
position: { x: 0, y: 0, z: 0 },
speed: 1,
easing: "linear",
},
},
{
type: "speak",
title: "Robot Speech",
description: "Make the robot say something",
icon: <MessageSquare className="h-4 w-4" />,
defaultParameters: {
text: "",
speed: 1,
pitch: 1,
volume: 1,
},
},
// Additional actions...
];
Parameter Configuration Dialog
interface ActionConfigDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
type: ActionType;
parameters: Record<string, any>;
onSubmit: (parameters: Record<string, any>) => void;
}
export function ActionConfigDialog({
open,
onOpenChange,
type,
parameters,
onSubmit,
}: ActionConfigDialogProps) {
const actionConfig = AVAILABLE_ACTIONS.find(a => a.type === type);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Configure {actionConfig?.title}</DialogTitle>
<DialogDescription>
{actionConfig?.description}
</DialogDescription>
</DialogHeader>
<Form>
{/* Parameter fields */}
</Form>
</DialogContent>
</Dialog>
);
}
Database Schema
export const experiments = createTable("experiment", {
id: integer("id").primaryKey().notNull().generatedAlwaysAsIdentity(),
studyId: integer("study_id")
.notNull()
.references(() => studies.id, { onDelete: "cascade" }),
title: varchar("title", { length: 256 }).notNull(),
description: text("description"),
version: integer("version").notNull().default(1),
status: experimentStatusEnum("status").notNull().default("draft"),
steps: jsonb("steps").$type<Step[]>().default([]),
createdById: varchar("created_by", { length: 255 })
.notNull()
.references(() => users.id),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
Integration with Plugin System
Action Transformation
interface ActionTransform {
type: "direct" | "transform";
transformFn?: string;
map?: Record<string, string>;
}
function transformActionParameters(
parameters: Record<string, any>,
transform: ActionTransform
): unknown {
if (transform.type === "direct") {
return parameters;
}
const transformFn = getTransformFunction(transform.transformFn!);
return transformFn(parameters);
}
Plugin Action Integration
function getAvailableActions(plugin: RobotPlugin): ActionConfig[] {
return plugin.actions.map(action => ({
type: action.type,
title: action.title,
description: action.description,
icon: getActionIcon(action.type),
defaultParameters: getDefaultParameters(action.parameters),
transform: action.ros2?.payloadMapping,
}));
}
User Interface Features
Drag and Drop
function onDragStart(event: DragEvent, nodeType: string) {
event.dataTransfer.setData("application/reactflow", nodeType);
event.dataTransfer.effectAllowed = "move";
}
function onDrop(event: DragEvent) {
event.preventDefault();
const type = event.dataTransfer.getData("application/reactflow");
const position = project({
x: event.clientX,
y: event.clientY,
});
const newNode = {
id: getId(),
type,
position,
data: { label: `${type} node` },
};
setNodes((nds) => nds.concat(newNode));
}
Step Organization
function reorderSteps(steps: Step[], sourceIndex: number, targetIndex: number): Step[] {
const result = Array.from(steps);
const [removed] = result.splice(sourceIndex, 1);
result.splice(targetIndex, 0, removed);
return result.map((step, index) => ({
...step,
order: index,
}));
}
Best Practices
-
Performance:
- Use React.memo for expensive components
- Implement virtualization for large flows
- Optimize drag and drop operations
-
User Experience:
- Provide clear visual feedback
- Implement undo/redo functionality
- Show validation errors inline
-
Data Management:
- Validate experiment data
- Implement auto-save
- Version control experiments
-
Error Handling:
- Validate action parameters
- Handle plugin loading errors
- Provide clear error messages
Future Enhancements
-
Advanced Flow Control:
- Conditional branching
- Parallel execution
- Loop constructs
-
Visual Improvements:
- Custom node themes
- Animation preview
- Mini-map navigation
-
Collaboration:
- Real-time collaboration
- Comment system
- Version history
-
Analysis Tools:
- Flow validation
- Performance analysis
- Debug tools