PluginProbe
WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services / 8.8.1
WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services v8.8.1
8.8.1 8.8.0 8.7.9 8.7.8 8.7.7 8.7.6 8.7.5 8.7.4 8.7.3 8.7.2 8.7.1 8.7.0 8.6.9 8.6.8 8.6.7 8.6.6 8.6.5 8.6.4 8.6.2 8.6.1 8.6.0 8.5.9 8.5.8 8.5.7 8.5.6 All 537 releases
chatbot / addons / automator / src / components / WorkflowCanvas.jsx

WorkflowCanvas.jsx in WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services 8.8.1, at addons/automator/src/components/WorkflowCanvas.jsx

265 lines 7.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 "use client";
2
3 import React, { useState, useCallback, useRef } from "react";
4 import { WorkflowNode } from "./WorkflowNode";
5 import { Connections } from "./Connections";
6
7 export function WorkflowCanvas({
8 scale,
9 setScale,
10 nodes,
11 setNodes,
12 connections,
13 setConnections,
14 selectedNodeId,
15 onNodeClick,
16 }) {
17 const [isPanning, setIsPanning] = useState(false);
18 const [panOffset, setPanOffset] = useState({ x: 0, y: 0 });
19 const [draggedNode, setDraggedNode] = useState(null);
20 const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
21 const [connectingFrom, setConnectingFrom] = useState(null);
22 const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
23 const canvasRef = useRef(null);
24
25 const handleMouseDown = useCallback(
26 (e) => {
27 if (e.target === canvasRef.current || e.target.closest('[data-grid]')) {
28 setIsPanning(true);
29 }
30 },
31 []
32 );
33
34 const handleNodeDragStart = useCallback(
35 (e, nodeId) => {
36 e.stopPropagation();
37 setDraggedNode(nodeId);
38
39 const node = nodes.find((n) => n.id === nodeId);
40 if (node) {
41 setDragOffset({
42 x: e.clientX - (node.position.x * scale + panOffset.x),
43 y: e.clientY - (node.position.y * scale + panOffset.y),
44 });
45 }
46 },
47 [nodes, scale, panOffset]
48 );
49
50 const handleConnectionStart = useCallback(
51 (nodeId, handle) => {
52 setConnectingFrom({ nodeId, handle });
53 },
54 []
55 );
56
57 const handleConnectionEnd = useCallback(
58 (nodeId, handle) => {
59 if (connectingFrom && connectingFrom.nodeId !== nodeId) {
60 const newConnection = {
61 id: `conn-${Date.now()}`,
62 from: connectingFrom.handle === "right" ? connectingFrom.nodeId : nodeId,
63 to: connectingFrom.handle === "right" ? nodeId : connectingFrom.nodeId,
64 fromHandle: "right",
65 toHandle: "left",
66 };
67 setConnections((prev) => [...prev, newConnection]);
68 }
69 setConnectingFrom(null);
70 },
71 [connectingFrom, setConnections]
72 );
73
74 const handleMouseMove = useCallback(
75 (e) => {
76 const rect = canvasRef.current?.getBoundingClientRect();
77 if (rect) {
78 setMousePos({
79 x: e.clientX - rect.left,
80 y: e.clientY - rect.top,
81 });
82 }
83
84 if (isPanning) {
85 setPanOffset((prev) => ({
86 x: prev.x + e.movementX,
87 y: prev.y + e.movementY,
88 }));
89 } else if (draggedNode) {
90 const newX = (e.clientX - dragOffset.x - panOffset.x) / scale;
91 const newY = (e.clientY - dragOffset.y - panOffset.y) / scale;
92
93 setNodes((prev) =>
94 prev.map((node) =>
95 node.id === draggedNode
96 ? { ...node, position: { x: newX, y: newY } }
97 : node
98 )
99 );
100 }
101 },
102 [isPanning, draggedNode, dragOffset, scale, panOffset, setNodes]
103 );
104
105 const handleMouseUp = useCallback(() => {
106 setIsPanning(false);
107 setDraggedNode(null);
108 setConnectingFrom(null);
109 }, []);
110
111 const handleDrop = useCallback(
112 (e) => {
113 e.preventDefault();
114 const dataStr = e.dataTransfer.getData("application/json");
115 if (!dataStr) return;
116
117 const app = JSON.parse(dataStr);
118 const rect = canvasRef.current?.getBoundingClientRect();
119 if (!rect) return;
120
121 const x = (e.clientX - rect.left - panOffset.x) / scale;
122 const y = (e.clientY - rect.top - panOffset.y) / scale;
123
124 // Type heuristic:
125 // 1. If it's the first node, it's likely a trigger.
126 // 2. If it's a tool (app.id starts with 't'), it's an action.
127 // 3. For apps, if we already have a trigger, it's an action.
128 let nodeType = "trigger";
129 if (app.id.startsWith('t')) {
130 nodeType = "action";
131 } else if (nodes.some(n => n.type === 'trigger')) {
132 nodeType = "action";
133 }
134
135 const newNode = {
136 id: `node-${Date.now()}`,
137 type: nodeType,
138 title: app.name,
139 subtitle: "Configure action",
140 position: { x, y },
141 icon: app.icon,
142 iconBg: app.iconBg,
143 actionNumber: nodes.length + 1,
144 appData: app, // Store the full app data for actions
145 };
146
147 setNodes((prev) => [...prev, newNode]);
148 },
149 [panOffset, scale, nodes.length, setNodes]
150 );
151
152 const handleDragOver = useCallback((e) => {
153 e.preventDefault();
154 e.dataTransfer.dropEffect = "copy";
155 }, []);
156
157 const handleWheel = useCallback(
158 (e) => {
159 if (e.ctrlKey || e.metaKey) {
160 e.preventDefault();
161 const delta = e.deltaY > 0 ? -0.1 : 0.1;
162 const newScale = Math.min(Math.max(scale + delta, 0.5), 2);
163
164 if (newScale !== scale) {
165 const rect = canvasRef.current?.getBoundingClientRect();
166 if (rect) {
167 const mouseX = e.clientX - rect.left;
168 const mouseY = e.clientY - rect.top;
169
170 // Calculate the mouse position relative to the content before zoom
171 const contentMouseX = (mouseX - panOffset.x) / scale;
172 const contentMouseY = (mouseY - panOffset.y) / scale;
173
174 // Calculate the new pan offset to keep the content under the mouse at the same position
175 const newPanX = mouseX - contentMouseX * newScale;
176 const newPanY = mouseY - contentMouseY * newScale;
177
178 setPanOffset({ x: newPanX, y: newPanY });
179 setScale(newScale);
180 }
181 }
182 }
183 },
184 [scale, setScale, panOffset]
185 );
186
187 const handleDeleteConnection = useCallback(
188 (connectionId) => {
189 setConnections((prev) => prev.filter((c) => c.id !== connectionId));
190 },
191 [setConnections]
192 );
193
194 const handleDeleteNode = useCallback(
195 (nodeId) => {
196 setNodes((prev) => prev.filter((n) => n.id !== nodeId));
197 setConnections((prev) =>
198 prev.filter((c) => c.from !== nodeId && c.to !== nodeId)
199 );
200 },
201 [setNodes, setConnections]
202 );
203
204 return (
205 <div
206 ref={canvasRef}
207 className="flex-1 bg-canvas relative overflow-hidden cursor-grab active:cursor-grabbing"
208 onMouseDown={handleMouseDown}
209 onMouseMove={handleMouseMove}
210 onMouseUp={handleMouseUp}
211 onMouseLeave={handleMouseUp}
212 onDrop={handleDrop}
213 onDragOver={handleDragOver}
214 onWheel={handleWheel}
215 >
216 {/* Grid Pattern */}
217 <div
218 data-grid
219 className="absolute inset-0 opacity-30 pointer-events-none"
220 style={{
221 backgroundImage: `radial-gradient(circle, var(--sidebar-border) 1px, transparent 1px)`,
222 backgroundSize: `${20 * scale}px ${20 * scale}px`,
223 backgroundPosition: `${panOffset.x}px ${panOffset.y}px`,
224 }}
225 />
226
227 {/* Canvas Content */}
228 <div
229 className="absolute"
230 style={{
231 transform: `translate(${panOffset.x}px, ${panOffset.y}px)`,
232 }}
233 >
234 {/* Connections */}
235 <Connections
236 connections={connections}
237 nodes={nodes}
238 scale={scale}
239 onDeleteConnection={handleDeleteConnection}
240 connectingFrom={connectingFrom}
241 mousePos={mousePos}
242 panOffset={panOffset}
243 />
244
245 {/* Nodes */}
246 {nodes.map((node) => (
247 <WorkflowNode
248 key={node.id}
249 node={node}
250 scale={scale}
251 onDragStart={handleNodeDragStart}
252 onConnectionStart={handleConnectionStart}
253 onConnectionEnd={handleConnectionEnd}
254 onDelete={handleDeleteNode}
255 isSelected={node.id === selectedNodeId}
256 onClick={() => onNodeClick(node.id)}
257 />
258 ))}
259 </div>
260 </div>
261 );
262 }
263
264 export default WorkflowCanvas;
265