π§ CRITICAL WORKFLOW STUDIO BUGS - FIX PLAN
Date: October 25, 2025
Status: π¨ URGENT FIXES NEEDED
Priority: P0 - Blocking production use
π BUGS IDENTIFIEDβ
1. Nodes/Modules Don't Move on Canvas β οΈ CRITICALβ
Symptom: Drag cursor appears but nodes don't move
Root Cause: ReactFlow missing nodesDraggable={true} prop
Location: WorkflowCanvas.tsx line ~605
2. ExecModules Toolbar Width π¨ UXβ
Symptom: Internal scrolling div not 100% width
Root Cause: .modules-list needs width: 100%
Location: FloatingExecModulesToolbar.css
3. Connector Handle Animation π¨ UXβ
Symptom: Handles bounce up/down, not mushroom-shaped
Root Cause: Animation uses translateY, needs scale instead
Location: WorkflowCanvas.tsx + CSS for handles
4. ExecModule Drop to Task β οΈ CRITICALβ
Symptom: task.addModule() may not persist correctly
Root Cause: Need robust RTK Query mutation with error handling
Location: ConsolidatedWorkflowStudio.tsx handleDropExecModule
5. Button Group Readability π¨ UXβ
Symptom: Test/Run/Save/Load buttons have light text (unreadable)
Root Cause: Button styling needs dark text
Location: Control panel buttons
6. Test/Run System Enhancement π FEATUREβ
Symptom: No real-time visualization, no WebSocket updates
Root Cause: Missing WebSocket integration + 3D viewer animation
Location: Multiple files (WebSocket client, Redux, 3D viewer)
π― FIX IMPLEMENTATION ORDERβ
Phase 1: Critical Bugs (30 mins)β
- Fix node dragging (1 line change)
- Fix toolbar width (1 CSS change)
- Fix button readability (CSS change)
Phase 2: UX Polish (1 hour)β
- Enhance handle animation (mushroom shape + scale)
- Robust drop-to-task with error handling
Phase 3: Test/Run Enhancement (3-4 hours)β
- WebSocket integration
- Redux state for execution
- 3D viewer real-time animation
π DETAILED FIXESβ
Fix 1: Enable Node Dragging β β
File: WorkflowCanvas.tsx
Line: ~605
// BEFORE:
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
// ... missing nodesDraggable
>
// AFTER:
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodesDraggable={true} // β
ENABLE DRAGGING
elementsSelectable={true}
selectNodesOnDrag={false}
panOnDrag={[1, 2]} // Pan with middle/right mouse
>
Fix 2: Toolbar Width β β
File: FloatingExecModulesToolbar.css
Line: ~85
/* BEFORE: */
.modules-list {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 14px 18px;
/* width missing */
}
/* AFTER: */
.modules-list {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 14px 18px;
width: 100%; /* β
FULL WIDTH */
box-sizing: border-box;
}
Fix 3: Mushroom Handle Animation β β
File: WorkflowCanvas.tsx
Function: buildHandleStyle
// Add hover animation via CSS class instead of inline styles
const buildHandleStyle = (
accent: string | undefined,
orientation: HandleOrientation,
variant: "source" | "target" = "source"
): React.CSSProperties => {
const normalizedAccent = normalizeHexColor(accent);
const highlight = lightenColor(normalizedAccent, 0.35);
const baseFill = hexToRgba(
normalizedAccent,
variant === "source" ? 0.95 : 0.9
);
const depth = darkenColor(normalizedAccent, 0.25);
const glow = hexToRgba(normalizedAccent, variant === "source" ? 0.3 : 0.22);
const style: React.CSSProperties = {
width: HANDLE_DIAMETER,
height: HANDLE_DIAMETER,
borderRadius: HANDLE_DIAMETER / 2,
border: `3px solid ${normalizedAccent}`,
background: `radial-gradient(circle at 32% 32%, ${highlight} 0%, ${baseFill} 55%, ${depth} 100%)`,
boxShadow: `0 14px 30px rgba(15, 23, 42, 0.55), 0 0 0 12px ${glow}`,
cursor: "grab",
touchAction: "none",
zIndex: 5,
transition: "transform 0.2s ease, box-shadow 0.2s ease", // β
ADD TRANSITION
};
// Mushroom shape: wider at top/bottom
if (orientation === "top" || orientation === "bottom") {
style.width = HANDLE_DIAMETER * 1.2; // β
WIDER
style.borderRadius = `${HANDLE_DIAMETER * 0.6}px / ${
HANDLE_DIAMETER * 0.5
}px`; // β
ELLIPSE
}
// ... position logic
return style;
};
CSS Addition:
/* Add to WorkflowCanvas.css or global styles */
.react-flow__handle:hover {
transform: scale(1.15) !important; /* β
BOUNCE-EXPAND */
box-shadow: 0 0 0 16px rgba(56, 189, 248, 0.25) !important;
}
.react-flow__handle:active {
transform: scale(0.95) !important;
cursor: grabbing !important;
}
Fix 4: Robust Drop-to-Task β β
File: ConsolidatedWorkflowStudio.tsx
Function: handleDropExecModule
const handleDropExecModule = React.useCallback(
async (
payload: {
className: string;
adapter?: any;
label?: string;
displayName?: string;
defaultModuleData?: string;
moduleType?: string;
accentColor?: string;
},
position: { x: number; y: number }
) => {
appendLog(
`π¦ Dropping ExecModule: ${payload.displayName || payload.className}`
);
try {
// 1. Find target node at drop position
const targetNode = nodes.find((n) => {
const nodeEl = document.querySelector(`[data-id="${n.id}"]`);
if (!nodeEl) return false;
const rect = nodeEl.getBoundingClientRect();
const canvasRect = canvasRef.current?.getBoundingClientRect();
if (!canvasRect) return false;
const relX = position.x - canvasRect.left;
const relY = position.y - canvasRect.top;
return (
relX >= rect.left - canvasRect.left &&
relX <= rect.right - canvasRect.left &&
relY >= rect.top - canvasRect.top &&
relY <= rect.bottom - canvasRect.top
);
});
if (targetNode && targetNode.type === "task") {
// β
DROP ON TASK - ADD MODULE TO TASK
appendLog(`π― Dropping module on task: ${targetNode.data.label}`);
// 2. Create ExecModule object
const newModule: Partial<ExecModule> = {
name:
payload.displayName ||
payload.label ||
payload.className.split(".").pop(),
className: payload.className,
moduleType: payload.moduleType || "INTEGRATION",
moduleData:
payload.defaultModuleData || payload.adapter
? JSON.stringify(payload.adapter)
: "{}",
status: "DRAFT",
notes: `Dropped at ${new Date().toISOString()}`,
};
// 3. Save to backend via RTK Query
appendLog("πΎ Saving module to backend...");
const saveResult = await dispatchRequest(
requestAsync(postExecModule({ execModule: newModule }))
);
if (!saveResult?.value?.body?.id) {
throw new Error("Failed to save module - no ID returned");
}
const savedModule = saveResult.value.body;
appendLog(`β
Module saved with ID: ${savedModule.id}`);
// 4. Associate module with task (if Task has modules array in backend)
// TODO: Implement task.addModule() API endpoint
appendLog(
`π Associating module ${savedModule.id} with task ${targetNode.id}`
);
// 5. Update node data to show module
setNodes((nds) =>
nds.map((n) =>
n.id === targetNode.id
? {
...n,
data: {
...n.data,
modules: [...(n.data.modules || []), savedModule],
},
}
: n
)
);
appendLog("π Module successfully added to task!");
} else {
// β
DROP ON CANVAS - CREATE STANDALONE TASK WITH MODULE
appendLog("π Dropping module on canvas (creating new task)");
handleDropNewNode("task", position, {
label: payload.displayName || payload.label || "Task",
execModule: payload.className,
modules: [
{
name: payload.displayName || payload.label,
className: payload.className,
moduleType: payload.moduleType,
moduleData: payload.defaultModuleData,
},
],
});
}
} catch (error) {
appendLog(`β ERROR dropping module: ${(error as Error).message}`);
console.error("handleDropExecModule error:", error);
// TODO: Show user-friendly error toast
}
},
[appendLog, handleDropNewNode, nodes, dispatchRequest]
);
Fix 5: Button Readability β β
File: Find control panel CSS or add inline styles
// In ConsolidatedWorkflowStudio.tsx or control panel component:
<ButtonGroup>
<CoolButton
variant="dark"
style={{
color: "#1e293b", // β
DARK TEXT
backgroundColor: "#f8fafc",
fontWeight: 600,
}}
>
Test
</CoolButton>
<CoolButton
variant="dark"
style={{ color: "#1e293b", backgroundColor: "#f8fafc", fontWeight: 600 }}
>
Run
</CoolButton>
{/* ... etc */}
</ButtonGroup>
Fix 6: WebSocket + 3D Real-Time β β
This is a major feature requiring multiple files. See separate implementation plan below.
π WEBSOCKET INTEGRATION ARCHITECTUREβ
Step 1: WebSocket Client Hookβ
File: src/hooks/useWorkflowWebSocket.ts (NEW)
import { useEffect, useRef, useState } from "react";
import { useAppDispatch } from "../redux/hooks";
import {
updateTaskStatus,
updateWorkflowProgress,
} from "../redux/features/workflows/workflowCanvasSlice";
interface WorkflowExecutionUpdate {
workflowId: string;
taskId?: string;
status: "RUNNING" | "COMPLETED" | "FAILED";
progress?: number;
output?: any;
timestamp: string;
}
export const useWorkflowWebSocket = (workflowId: string | null) => {
const dispatch = useAppDispatch();
const wsRef = useRef<WebSocket | null>(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
if (!workflowId) return;
const ws = new WebSocket(
`ws://localhost:8080/v1/vaiworkflow/subscribe/${workflowId}`
);
wsRef.current = ws;
ws.onopen = () => {
console.log("π WebSocket connected for workflow:", workflowId);
setIsConnected(true);
};
ws.onmessage = (event) => {
try {
const update: WorkflowExecutionUpdate = JSON.parse(event.data);
console.log("π¨ Workflow update:", update);
// Update Redux state
if (update.taskId) {
dispatch(
updateTaskStatus({
taskId: update.taskId,
status: update.status,
output: update.output,
})
);
}
dispatch(
updateWorkflowProgress({
workflowId: update.workflowId,
progress: update.progress || 0,
})
);
} catch (error) {
console.error("Failed to parse WebSocket message:", error);
}
};
ws.onerror = (error) => {
console.error("β WebSocket error:", error);
setIsConnected(false);
};
ws.onclose = () => {
console.log("π WebSocket disconnected");
setIsConnected(false);
};
return () => {
ws.close();
};
}, [workflowId, dispatch]);
return { isConnected };
};
Step 2: Redux State Updatesβ
File: redux/features/workflows/workflowCanvasSlice.ts
// Add to existing slice:
interface TaskExecutionState {
taskId: string;
status: 'IDLE' | 'RUNNING' | 'COMPLETED' | 'FAILED';
startTime?: number;
endTime?: number;
output?: any;
error?: string;
}
interface WorkflowCanvasState {
// ... existing state
executionStates: Record<string, TaskExecutionState>;
currentProgress: number;
isExecuting: boolean;
}
// Add reducers:
updateTaskStatus: (state, action: PayloadAction<{
taskId: string;
status: TaskExecutionState['status'];
output?: any;
error?: string;
}>) => {
state.executionStates[action.payload.taskId] = {
...state.executionStates[action.payload.taskId],
taskId: action.payload.taskId,
status: action.payload.status,
output: action.payload.output,
error: action.payload.error,
...(action.payload.status === 'RUNNING' && { startTime: Date.now() }),
...(action.payload.status === 'COMPLETED' || action.payload.status === 'FAILED') && { endTime: Date.now() },
};
},
updateWorkflowProgress: (state, action: PayloadAction<{
workflowId: string;
progress: number;
}>) => {
state.currentProgress = action.payload.progress;
state.isExecuting = action.payload.progress < 100;
},
Step 3: 3D Viewer Animationβ
File: Workflow3DViewer.tsx
// Add animation for active tasks:
const TaskMesh = ({ task, isActive, isCompleted }) => {
const meshRef = useRef();
useFrame((state) => {
if (!meshRef.current) return;
if (isActive) {
// β
PULSE ANIMATION for running task
meshRef.current.scale.setScalar(
1 + Math.sin(state.clock.elapsedTime * 3) * 0.1
);
meshRef.current.material.emissive.setHex(0x00ff00);
meshRef.current.material.emissiveIntensity =
0.5 + Math.sin(state.clock.elapsedTime * 2) * 0.3;
} else if (isCompleted) {
// β
COMPLETED GLOW
meshRef.current.material.emissive.setHex(0x00aaff);
meshRef.current.material.emissiveIntensity = 0.3;
} else {
// IDLE
meshRef.current.scale.setScalar(1);
meshRef.current.material.emissiveIntensity = 0;
}
});
return (
<mesh ref={meshRef} position={[task.x, task.y, 0]}>
<boxGeometry args={[1, 1, 0.2]} />
<meshStandardMaterial color="#6366f1" />
</mesh>
);
};
Step 4: Integrationβ
File: ConsolidatedWorkflowStudio.tsx
import { useWorkflowWebSocket } from '../../hooks/useWorkflowWebSocket';
export const ConsolidatedWorkflowStudio = () => {
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
const { isConnected } = useWorkflowWebSocket(currentWorkflowId);
const handleRunWorkflow = async () => {
appendLog("βΆοΈ Starting workflow execution...");
try {
// 1. Save workflow if not saved
if (!currentWorkflowId) {
const result = await saveWorkflowToDatabase();
setCurrentWorkflowId(result.id);
}
// 2. Trigger execution
const response = await fetch(`/v1/vaiworkflow/${currentWorkflowId}/execute`, {
method: 'POST',
});
if (!response.ok) {
throw new Error('Failed to start workflow');
}
appendLog("β
Workflow execution started!");
appendLog(`π WebSocket ${isConnected ? 'connected' : 'connecting'}...`);
} catch (error) {
appendLog(`β ERROR: ${(error as Error).message}`);
}
};
return (
// ... JSX with Run button calling handleRunWorkflow
);
};
β TESTING CHECKLISTβ
Phase 1 Testsβ
- Nodes drag and move freely on canvas
- ExecModules toolbar shows full width scroll area
- Button text is readable (dark text on light background)
Phase 2 Testsβ
- Connector handles are mushroom-shaped
- Handles scale up on hover (not bounce up/down)
- Dropping ExecModule on task adds module correctly
- Error handling works (console shows errors)
- Module persists after refresh
Phase 3 Testsβ
- WebSocket connects when running workflow
- Redux state updates in real-time
- 3D viewer shows pulse animation on active tasks
- Completed tasks show blue glow
- Progress bar updates smoothly
- Console shows real-time execution logs
π― SUCCESS METRICSβ
- β All drag operations work smoothly
- β UI is readable and professional
- β Drop-to-task is 100% reliable
- β Real-time execution visualization works
- β Zero console errors
- β Users say "This is the best workflow editor I've ever used"
Status: π Plan ready - awaiting implementation
Estimated Time: 5-6 hours total
Priority Order: Fix 1-3 β Fix 4-5 β Fix 6
LET'S MAKE THIS THE BEST WORKFLOW STUDIO ON THE PLANET! π