machinaos 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/.env.template +71 -0
  2. package/LICENSE +21 -0
  3. package/README.md +87 -0
  4. package/bin/cli.js +159 -0
  5. package/client/.dockerignore +45 -0
  6. package/client/Dockerfile +68 -0
  7. package/client/eslint.config.js +29 -0
  8. package/client/index.html +13 -0
  9. package/client/nginx.conf +66 -0
  10. package/client/package.json +48 -0
  11. package/client/src/App.tsx +27 -0
  12. package/client/src/Dashboard.tsx +1173 -0
  13. package/client/src/ParameterPanel.tsx +301 -0
  14. package/client/src/components/AIAgentNode.tsx +321 -0
  15. package/client/src/components/APIKeyValidator.tsx +118 -0
  16. package/client/src/components/ClaudeChatModelNode.tsx +18 -0
  17. package/client/src/components/ConditionalEdge.tsx +189 -0
  18. package/client/src/components/CredentialsModal.tsx +306 -0
  19. package/client/src/components/EdgeConditionEditor.tsx +443 -0
  20. package/client/src/components/GeminiChatModelNode.tsx +18 -0
  21. package/client/src/components/GenericNode.tsx +357 -0
  22. package/client/src/components/LocationParameterPanel.tsx +154 -0
  23. package/client/src/components/ModelNode.tsx +286 -0
  24. package/client/src/components/OpenAIChatModelNode.tsx +18 -0
  25. package/client/src/components/OutputPanel.tsx +471 -0
  26. package/client/src/components/ParameterRenderer.tsx +1874 -0
  27. package/client/src/components/SkillEditorModal.tsx +417 -0
  28. package/client/src/components/SquareNode.tsx +797 -0
  29. package/client/src/components/StartNode.tsx +250 -0
  30. package/client/src/components/ToolkitNode.tsx +365 -0
  31. package/client/src/components/TriggerNode.tsx +463 -0
  32. package/client/src/components/auth/LoginPage.tsx +247 -0
  33. package/client/src/components/auth/ProtectedRoute.tsx +59 -0
  34. package/client/src/components/base/BaseChatModelNode.tsx +271 -0
  35. package/client/src/components/icons/AIProviderIcons.tsx +50 -0
  36. package/client/src/components/maps/GoogleMapsPicker.tsx +137 -0
  37. package/client/src/components/maps/MapsPreviewPanel.tsx +110 -0
  38. package/client/src/components/maps/index.ts +26 -0
  39. package/client/src/components/parameterPanel/InputSection.tsx +1094 -0
  40. package/client/src/components/parameterPanel/LocationPanelLayout.tsx +65 -0
  41. package/client/src/components/parameterPanel/MapsSection.tsx +92 -0
  42. package/client/src/components/parameterPanel/MiddleSection.tsx +571 -0
  43. package/client/src/components/parameterPanel/OutputSection.tsx +81 -0
  44. package/client/src/components/parameterPanel/ParameterPanelLayout.tsx +82 -0
  45. package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +436 -0
  46. package/client/src/components/parameterPanel/index.ts +42 -0
  47. package/client/src/components/shared/DataPanel.tsx +142 -0
  48. package/client/src/components/shared/JSONTreeRenderer.tsx +106 -0
  49. package/client/src/components/ui/AIResultModal.tsx +204 -0
  50. package/client/src/components/ui/AndroidSettingsPanel.tsx +401 -0
  51. package/client/src/components/ui/CodeEditor.tsx +81 -0
  52. package/client/src/components/ui/CollapsibleSection.tsx +88 -0
  53. package/client/src/components/ui/ComponentItem.tsx +154 -0
  54. package/client/src/components/ui/ComponentPalette.tsx +321 -0
  55. package/client/src/components/ui/ConsolePanel.tsx +1074 -0
  56. package/client/src/components/ui/ErrorBoundary.tsx +196 -0
  57. package/client/src/components/ui/InputNodesPanel.tsx +204 -0
  58. package/client/src/components/ui/MapSelector.tsx +314 -0
  59. package/client/src/components/ui/Modal.tsx +149 -0
  60. package/client/src/components/ui/NodeContextMenu.tsx +192 -0
  61. package/client/src/components/ui/NodeOutputPanel.tsx +1150 -0
  62. package/client/src/components/ui/OutputDisplayPanel.tsx +381 -0
  63. package/client/src/components/ui/SettingsPanel.tsx +243 -0
  64. package/client/src/components/ui/TopToolbar.tsx +736 -0
  65. package/client/src/components/ui/WhatsAppSettingsPanel.tsx +345 -0
  66. package/client/src/components/ui/WorkflowSidebar.tsx +294 -0
  67. package/client/src/config/antdTheme.ts +186 -0
  68. package/client/src/config/api.ts +54 -0
  69. package/client/src/contexts/AuthContext.tsx +221 -0
  70. package/client/src/contexts/ThemeContext.tsx +42 -0
  71. package/client/src/contexts/WebSocketContext.tsx +1971 -0
  72. package/client/src/factories/baseChatModelFactory.ts +256 -0
  73. package/client/src/hooks/useAndroidOperations.ts +164 -0
  74. package/client/src/hooks/useApiKeyValidation.ts +107 -0
  75. package/client/src/hooks/useApiKeys.ts +238 -0
  76. package/client/src/hooks/useAppTheme.ts +17 -0
  77. package/client/src/hooks/useComponentPalette.ts +51 -0
  78. package/client/src/hooks/useCopyPaste.ts +155 -0
  79. package/client/src/hooks/useDragAndDrop.ts +124 -0
  80. package/client/src/hooks/useDragVariable.ts +88 -0
  81. package/client/src/hooks/useExecution.ts +313 -0
  82. package/client/src/hooks/useParameterPanel.ts +176 -0
  83. package/client/src/hooks/useReactFlowNodes.ts +189 -0
  84. package/client/src/hooks/useToolSchema.ts +209 -0
  85. package/client/src/hooks/useWhatsApp.ts +196 -0
  86. package/client/src/hooks/useWorkflowManagement.ts +46 -0
  87. package/client/src/index.css +315 -0
  88. package/client/src/main.tsx +19 -0
  89. package/client/src/nodeDefinitions/aiAgentNodes.ts +336 -0
  90. package/client/src/nodeDefinitions/aiModelNodes.ts +340 -0
  91. package/client/src/nodeDefinitions/androidDeviceNodes.ts +140 -0
  92. package/client/src/nodeDefinitions/androidServiceNodes.ts +383 -0
  93. package/client/src/nodeDefinitions/chatNodes.ts +135 -0
  94. package/client/src/nodeDefinitions/codeNodes.ts +54 -0
  95. package/client/src/nodeDefinitions/documentNodes.ts +379 -0
  96. package/client/src/nodeDefinitions/index.ts +15 -0
  97. package/client/src/nodeDefinitions/locationNodes.ts +463 -0
  98. package/client/src/nodeDefinitions/schedulerNodes.ts +220 -0
  99. package/client/src/nodeDefinitions/skillNodes.ts +211 -0
  100. package/client/src/nodeDefinitions/toolNodes.ts +198 -0
  101. package/client/src/nodeDefinitions/utilityNodes.ts +284 -0
  102. package/client/src/nodeDefinitions/whatsappNodes.ts +865 -0
  103. package/client/src/nodeDefinitions/workflowNodes.ts +41 -0
  104. package/client/src/nodeDefinitions.ts +104 -0
  105. package/client/src/schemas/workflowSchema.ts +264 -0
  106. package/client/src/services/dynamicParameterService.ts +96 -0
  107. package/client/src/services/execution/aiAgentExecutionService.ts +35 -0
  108. package/client/src/services/executionService.ts +232 -0
  109. package/client/src/services/workflowApi.ts +91 -0
  110. package/client/src/store/useAppStore.ts +582 -0
  111. package/client/src/styles/theme.ts +508 -0
  112. package/client/src/styles/zIndex.ts +17 -0
  113. package/client/src/types/ComponentTypes.ts +39 -0
  114. package/client/src/types/EdgeCondition.ts +231 -0
  115. package/client/src/types/INodeProperties.ts +288 -0
  116. package/client/src/types/NodeTypes.ts +28 -0
  117. package/client/src/utils/formatters.ts +33 -0
  118. package/client/src/utils/googleMapsLoader.ts +140 -0
  119. package/client/src/utils/locationUtils.ts +85 -0
  120. package/client/src/utils/nodeUtils.ts +31 -0
  121. package/client/src/utils/workflow.ts +30 -0
  122. package/client/src/utils/workflowExport.ts +120 -0
  123. package/client/src/vite-env.d.ts +12 -0
  124. package/client/tailwind.config.js +60 -0
  125. package/client/tsconfig.json +25 -0
  126. package/client/tsconfig.node.json +11 -0
  127. package/client/vite.config.js +35 -0
  128. package/docker-compose.prod.yml +107 -0
  129. package/docker-compose.yml +104 -0
  130. package/docs-MachinaOs/README.md +85 -0
  131. package/docs-MachinaOs/deployment/docker.mdx +228 -0
  132. package/docs-MachinaOs/deployment/production.mdx +345 -0
  133. package/docs-MachinaOs/docs.json +75 -0
  134. package/docs-MachinaOs/faq.mdx +309 -0
  135. package/docs-MachinaOs/favicon.svg +5 -0
  136. package/docs-MachinaOs/installation.mdx +160 -0
  137. package/docs-MachinaOs/introduction.mdx +114 -0
  138. package/docs-MachinaOs/logo/dark.svg +6 -0
  139. package/docs-MachinaOs/logo/light.svg +6 -0
  140. package/docs-MachinaOs/nodes/ai-agent.mdx +216 -0
  141. package/docs-MachinaOs/nodes/ai-models.mdx +240 -0
  142. package/docs-MachinaOs/nodes/android.mdx +411 -0
  143. package/docs-MachinaOs/nodes/overview.mdx +181 -0
  144. package/docs-MachinaOs/nodes/schedulers.mdx +316 -0
  145. package/docs-MachinaOs/nodes/webhooks.mdx +330 -0
  146. package/docs-MachinaOs/nodes/whatsapp.mdx +305 -0
  147. package/docs-MachinaOs/quickstart.mdx +119 -0
  148. package/docs-MachinaOs/tutorials/ai-agent-workflow.mdx +177 -0
  149. package/docs-MachinaOs/tutorials/android-automation.mdx +242 -0
  150. package/docs-MachinaOs/tutorials/first-workflow.mdx +134 -0
  151. package/docs-MachinaOs/tutorials/whatsapp-automation.mdx +185 -0
  152. package/nul +0 -0
  153. package/package.json +70 -0
  154. package/scripts/build.js +158 -0
  155. package/scripts/check-ports.ps1 +33 -0
  156. package/scripts/clean.js +40 -0
  157. package/scripts/docker.js +93 -0
  158. package/scripts/kill-port.ps1 +154 -0
  159. package/scripts/start.js +210 -0
  160. package/scripts/stop.js +325 -0
  161. package/server/.dockerignore +44 -0
  162. package/server/Dockerfile +45 -0
  163. package/server/constants.py +249 -0
  164. package/server/core/__init__.py +1 -0
  165. package/server/core/cache.py +461 -0
  166. package/server/core/config.py +128 -0
  167. package/server/core/container.py +99 -0
  168. package/server/core/database.py +1211 -0
  169. package/server/core/logging.py +314 -0
  170. package/server/main.py +289 -0
  171. package/server/middleware/__init__.py +5 -0
  172. package/server/middleware/auth.py +89 -0
  173. package/server/models/__init__.py +1 -0
  174. package/server/models/auth.py +52 -0
  175. package/server/models/cache.py +24 -0
  176. package/server/models/database.py +211 -0
  177. package/server/models/nodes.py +455 -0
  178. package/server/package.json +9 -0
  179. package/server/pyproject.toml +72 -0
  180. package/server/requirements.txt +83 -0
  181. package/server/routers/__init__.py +1 -0
  182. package/server/routers/android.py +294 -0
  183. package/server/routers/auth.py +203 -0
  184. package/server/routers/database.py +151 -0
  185. package/server/routers/maps.py +142 -0
  186. package/server/routers/nodejs_compat.py +289 -0
  187. package/server/routers/webhook.py +90 -0
  188. package/server/routers/websocket.py +2127 -0
  189. package/server/routers/whatsapp.py +761 -0
  190. package/server/routers/workflow.py +200 -0
  191. package/server/services/__init__.py +1 -0
  192. package/server/services/ai.py +2415 -0
  193. package/server/services/android/__init__.py +27 -0
  194. package/server/services/android/broadcaster.py +114 -0
  195. package/server/services/android/client.py +608 -0
  196. package/server/services/android/manager.py +78 -0
  197. package/server/services/android/protocol.py +165 -0
  198. package/server/services/android_service.py +588 -0
  199. package/server/services/auth.py +131 -0
  200. package/server/services/chat_client.py +160 -0
  201. package/server/services/deployment/__init__.py +12 -0
  202. package/server/services/deployment/manager.py +706 -0
  203. package/server/services/deployment/state.py +47 -0
  204. package/server/services/deployment/triggers.py +275 -0
  205. package/server/services/event_waiter.py +785 -0
  206. package/server/services/execution/__init__.py +77 -0
  207. package/server/services/execution/cache.py +769 -0
  208. package/server/services/execution/conditions.py +373 -0
  209. package/server/services/execution/dlq.py +132 -0
  210. package/server/services/execution/executor.py +1351 -0
  211. package/server/services/execution/models.py +531 -0
  212. package/server/services/execution/recovery.py +235 -0
  213. package/server/services/handlers/__init__.py +126 -0
  214. package/server/services/handlers/ai.py +355 -0
  215. package/server/services/handlers/android.py +260 -0
  216. package/server/services/handlers/code.py +278 -0
  217. package/server/services/handlers/document.py +598 -0
  218. package/server/services/handlers/http.py +193 -0
  219. package/server/services/handlers/polyglot.py +105 -0
  220. package/server/services/handlers/tools.py +845 -0
  221. package/server/services/handlers/triggers.py +107 -0
  222. package/server/services/handlers/utility.py +822 -0
  223. package/server/services/handlers/whatsapp.py +476 -0
  224. package/server/services/maps.py +289 -0
  225. package/server/services/memory_store.py +103 -0
  226. package/server/services/node_executor.py +375 -0
  227. package/server/services/parameter_resolver.py +218 -0
  228. package/server/services/polyglot_client.py +169 -0
  229. package/server/services/scheduler.py +155 -0
  230. package/server/services/skill_loader.py +417 -0
  231. package/server/services/status_broadcaster.py +826 -0
  232. package/server/services/temporal/__init__.py +23 -0
  233. package/server/services/temporal/activities.py +344 -0
  234. package/server/services/temporal/client.py +76 -0
  235. package/server/services/temporal/executor.py +147 -0
  236. package/server/services/temporal/worker.py +251 -0
  237. package/server/services/temporal/workflow.py +355 -0
  238. package/server/services/temporal/ws_client.py +236 -0
  239. package/server/services/text.py +111 -0
  240. package/server/services/user_auth.py +172 -0
  241. package/server/services/websocket_client.py +29 -0
  242. package/server/services/workflow.py +597 -0
  243. package/server/skills/android-skill/SKILL.md +82 -0
  244. package/server/skills/assistant-personality/SKILL.md +45 -0
  245. package/server/skills/code-skill/SKILL.md +140 -0
  246. package/server/skills/http-skill/SKILL.md +161 -0
  247. package/server/skills/maps-skill/SKILL.md +170 -0
  248. package/server/skills/memory-skill/SKILL.md +154 -0
  249. package/server/skills/scheduler-skill/SKILL.md +84 -0
  250. package/server/skills/whatsapp-skill/SKILL.md +283 -0
  251. package/server/uv.lock +2916 -0
  252. package/server/whatsapp-rpc/.dockerignore +30 -0
  253. package/server/whatsapp-rpc/Dockerfile +44 -0
  254. package/server/whatsapp-rpc/Dockerfile.web +17 -0
  255. package/server/whatsapp-rpc/README.md +139 -0
  256. package/server/whatsapp-rpc/cli.js +95 -0
  257. package/server/whatsapp-rpc/configs/config.yaml +7 -0
  258. package/server/whatsapp-rpc/docker-compose.yml +35 -0
  259. package/server/whatsapp-rpc/docs/API.md +410 -0
  260. package/server/whatsapp-rpc/go.mod +67 -0
  261. package/server/whatsapp-rpc/go.sum +203 -0
  262. package/server/whatsapp-rpc/package.json +30 -0
  263. package/server/whatsapp-rpc/schema.json +1294 -0
  264. package/server/whatsapp-rpc/scripts/clean.cjs +66 -0
  265. package/server/whatsapp-rpc/scripts/cli.js +162 -0
  266. package/server/whatsapp-rpc/src/go/cmd/server/main.go +91 -0
  267. package/server/whatsapp-rpc/src/go/config/config.go +49 -0
  268. package/server/whatsapp-rpc/src/go/rpc/rpc.go +446 -0
  269. package/server/whatsapp-rpc/src/go/rpc/server.go +112 -0
  270. package/server/whatsapp-rpc/src/go/whatsapp/history.go +166 -0
  271. package/server/whatsapp-rpc/src/go/whatsapp/messages.go +390 -0
  272. package/server/whatsapp-rpc/src/go/whatsapp/service.go +2130 -0
  273. package/server/whatsapp-rpc/src/go/whatsapp/types.go +261 -0
  274. package/server/whatsapp-rpc/src/python/pyproject.toml +15 -0
  275. package/server/whatsapp-rpc/src/python/whatsapp_rpc/__init__.py +4 -0
  276. package/server/whatsapp-rpc/src/python/whatsapp_rpc/client.py +427 -0
  277. package/server/whatsapp-rpc/web/app.py +609 -0
  278. package/server/whatsapp-rpc/web/requirements.txt +6 -0
  279. package/server/whatsapp-rpc/web/rpc_client.py +427 -0
  280. package/server/whatsapp-rpc/web/static/openapi.yaml +59 -0
  281. package/server/whatsapp-rpc/web/templates/base.html +150 -0
  282. package/server/whatsapp-rpc/web/templates/contacts.html +240 -0
  283. package/server/whatsapp-rpc/web/templates/dashboard.html +320 -0
  284. package/server/whatsapp-rpc/web/templates/groups.html +328 -0
  285. package/server/whatsapp-rpc/web/templates/messages.html +465 -0
  286. package/server/whatsapp-rpc/web/templates/messaging.html +681 -0
  287. package/server/whatsapp-rpc/web/templates/send.html +259 -0
  288. package/server/whatsapp-rpc/web/templates/settings.html +459 -0
@@ -0,0 +1,1173 @@
1
+ import React, { useEffect } from 'react';
2
+ import {
3
+ ReactFlow,
4
+ ReactFlowProvider,
5
+ Controls,
6
+ useNodesState,
7
+ useEdgesState,
8
+ useReactFlow,
9
+ ConnectionMode,
10
+ ConnectionLineType,
11
+ SelectionMode,
12
+ Node,
13
+ Edge,
14
+ } from 'reactflow';
15
+ import GenericNode from './components/GenericNode';
16
+ import AIAgentNode from './components/AIAgentNode';
17
+ import ModelNode from './components/ModelNode';
18
+ import SquareNode from './components/SquareNode';
19
+ import TriggerNode from './components/TriggerNode';
20
+ import ToolkitNode from './components/ToolkitNode';
21
+ import ConditionalEdge from './components/ConditionalEdge';
22
+ import NodeContextMenu from './components/ui/NodeContextMenu';
23
+ import { nodeDefinitions } from './nodeDefinitions';
24
+ import { ANDROID_SERVICE_NODE_TYPES } from './nodeDefinitions/androidServiceNodes';
25
+ import { ANDROID_DEVICE_NODE_TYPES } from './nodeDefinitions/androidDeviceNodes';
26
+ import { SCHEDULER_NODE_TYPES } from './nodeDefinitions/schedulerNodes';
27
+ import { CHAT_NODE_TYPES } from './nodeDefinitions/chatNodes';
28
+ import { CODE_NODE_TYPES } from './nodeDefinitions/codeNodes';
29
+ import { UTILITY_NODE_TYPES } from './nodeDefinitions/utilityNodes';
30
+ import { TOOL_NODE_TYPES } from './nodeDefinitions/toolNodes';
31
+ import { SKILL_NODE_TYPES } from './nodeDefinitions/skillNodes';
32
+ import { DOCUMENT_NODE_TYPES } from './nodeDefinitions/documentNodes';
33
+ import ParameterPanel from './ParameterPanel';
34
+ import LocationParameterPanel from './components/LocationParameterPanel';
35
+ import { useAppStore } from './store/useAppStore';
36
+ import ComponentPalette from './components/ui/ComponentPalette';
37
+ import TopToolbar from './components/ui/TopToolbar';
38
+ import WorkflowSidebar from './components/ui/WorkflowSidebar';
39
+ import SettingsPanel, { WorkflowSettings, defaultSettings } from './components/ui/SettingsPanel';
40
+ import WhatsAppSettingsPanel from './components/ui/WhatsAppSettingsPanel';
41
+ import AndroidSettingsPanel from './components/ui/AndroidSettingsPanel';
42
+ import AIResultModal from './components/ui/AIResultModal';
43
+ import CredentialsModal from './components/CredentialsModal';
44
+ import ErrorBoundary from './components/ui/ErrorBoundary';
45
+ import ConsolePanel from './components/ui/ConsolePanel';
46
+ import { useAppTheme } from './hooks/useAppTheme';
47
+ import { useWorkflowManagement } from './hooks/useWorkflowManagement';
48
+ import { useDragAndDrop } from './hooks/useDragAndDrop';
49
+ import { useComponentPalette } from './hooks/useComponentPalette';
50
+ import { useReactFlowNodes } from './hooks/useReactFlowNodes';
51
+ import { useCopyPaste } from './hooks/useCopyPaste';
52
+ import { useWebSocket } from './contexts/WebSocketContext';
53
+ import { useTheme } from './contexts/ThemeContext';
54
+ import {
55
+ sanitizeNodesForComparison,
56
+ sanitizeEdgesForComparison,
57
+ generateWorkflowId
58
+ } from './utils/workflow';
59
+ import { importWorkflowFromFile } from './utils/workflowExport';
60
+
61
+ import 'reactflow/dist/style.css';
62
+
63
+ // Node types configuration - defined outside component to prevent recreation on re-renders
64
+ // This is required by React Flow to avoid performance issues
65
+ const createNodeTypes = (): Record<string, React.ComponentType<any>> => {
66
+ const types: Record<string, React.ComponentType<any>> = {};
67
+
68
+ // Trigger nodes (no input handles) - check by group or specific types
69
+ const TRIGGER_NODE_TYPES = ['start', 'cronScheduler', 'webhookTrigger', 'whatsappReceive', 'chatTrigger'];
70
+
71
+ Object.keys(nodeDefinitions).forEach(type => {
72
+ const definition = nodeDefinitions[type];
73
+
74
+ // Trigger nodes - no input connections (start workflows)
75
+ if (TRIGGER_NODE_TYPES.includes(type)) {
76
+ types[type] = TriggerNode;
77
+ } else if (type === 'openaiChatModel' || type === 'anthropicChatModel' || type === 'geminiChatModel' || type === 'openrouterChatModel' || type === 'groqChatModel' || type === 'cerebrasChatModel') {
78
+ // AI chat model nodes use square design
79
+ types[type] = SquareNode;
80
+ } else if (type === 'aiAgent' || type === 'chatAgent') {
81
+ types[type] = AIAgentNode;
82
+ } else if (type === 'simpleMemory') {
83
+ // Simple Memory node for AI conversation history - uses circular ModelNode design
84
+ types[type] = ModelNode;
85
+ } else if (type === 'whatsappConnect' || type === 'whatsappSend' || type === 'whatsappDb') {
86
+ // WhatsApp action nodes use SquareNode (whatsappReceive is a trigger)
87
+ types[type] = SquareNode;
88
+ } else if (ANDROID_SERVICE_NODE_TYPES.includes(type) || ANDROID_DEVICE_NODE_TYPES.includes(type)) {
89
+ // Android service and device nodes use SquareNode component
90
+ types[type] = SquareNode;
91
+ } else if (SCHEDULER_NODE_TYPES.includes(type)) {
92
+ // Timer uses SquareNode (has input), cronScheduler already handled as trigger above
93
+ types[type] = SquareNode;
94
+ } else if (CHAT_NODE_TYPES.includes(type)) {
95
+ // Chat nodes use SquareNode component
96
+ types[type] = SquareNode;
97
+ } else if (CODE_NODE_TYPES.includes(type)) {
98
+ // Code execution nodes use SquareNode component
99
+ types[type] = SquareNode;
100
+ } else if (UTILITY_NODE_TYPES.includes(type)) {
101
+ // Utility nodes (HTTP, Webhooks) use SquareNode component
102
+ // Note: webhookTrigger is already handled as trigger above
103
+ types[type] = SquareNode;
104
+ } else if (TOOL_NODE_TYPES.includes(type)) {
105
+ // Most tool nodes use circular ModelNode
106
+ // Exception: androidTool uses ToolkitNode with top/bottom handles
107
+ if (type === 'androidTool') {
108
+ types[type] = ToolkitNode;
109
+ } else {
110
+ types[type] = ModelNode;
111
+ }
112
+ } else if (SKILL_NODE_TYPES.includes(type)) {
113
+ // Skill nodes use ToolkitNode (vertical handle layout like Android Toolkit)
114
+ types[type] = ToolkitNode;
115
+ } else if (DOCUMENT_NODE_TYPES.includes(type)) {
116
+ // Document processing nodes use SquareNode component
117
+ types[type] = SquareNode;
118
+ } else if (definition?.group?.includes('model')) {
119
+ // Fallback for other model nodes
120
+ types[type] = ModelNode;
121
+ } else if (definition?.group?.includes('service')) {
122
+ // Any node with 'service' group uses SquareNode component
123
+ types[type] = SquareNode;
124
+ } else {
125
+ types[type] = GenericNode;
126
+ }
127
+ });
128
+
129
+ return types;
130
+ };
131
+
132
+ // Create node types once at module load time
133
+ const moduleNodeTypes = createNodeTypes();
134
+
135
+ // Edge types configuration - enables conditional edge rendering
136
+ const moduleEdgeTypes = {
137
+ conditional: ConditionalEdge,
138
+ };
139
+
140
+ // Edge styles generator using theme colors - supports light and dark modes
141
+ const getEdgeStyles = (colors: {
142
+ edgeDefault: string;
143
+ edgeSelected: string;
144
+ edgeExecuting: string;
145
+ edgeCompleted: string;
146
+ edgeError: string;
147
+ }, isDark: boolean) => `
148
+ /* Base style for ALL edges */
149
+ .react-flow__edge path {
150
+ stroke: ${colors.edgeDefault} !important;
151
+ stroke-width: 2px;
152
+ }
153
+
154
+ .react-flow__edge.selected path {
155
+ stroke: ${colors.edgeSelected} !important;
156
+ stroke-width: 4px !important;
157
+ }
158
+
159
+ /* Executing edge - subtle blue in light mode, cyan in dark mode */
160
+ .react-flow__edge.executing path {
161
+ stroke: ${isDark ? colors.edgeExecuting : '#2563eb'} !important;
162
+ stroke-width: 3px !important;
163
+ stroke-dasharray: 8 4;
164
+ animation: dashFlow 0.5s linear infinite;
165
+ }
166
+
167
+ /* Completed edge - subtle green in both modes */
168
+ .react-flow__edge.completed path {
169
+ stroke: ${isDark ? colors.edgeCompleted : '#16a34a'} !important;
170
+ stroke-width: 2px !important;
171
+ }
172
+
173
+ /* Error edge - keep red for visibility */
174
+ .react-flow__edge.error path {
175
+ stroke: ${colors.edgeError} !important;
176
+ stroke-width: 3px !important;
177
+ }
178
+
179
+ /* Pending edge - animated dash in both modes */
180
+ .react-flow__edge.pending path {
181
+ stroke: ${isDark ? colors.edgeDefault : '#6b7280'} !important;
182
+ stroke-width: 2px !important;
183
+ stroke-dasharray: 8 4;
184
+ animation: dashFlow 0.5s linear infinite;
185
+ }
186
+
187
+ /* Memory connection active */
188
+ .react-flow__edge.memory-active path {
189
+ stroke: ${isDark ? '#ff79c6' : '#db2777'} !important;
190
+ stroke-width: 3px !important;
191
+ }
192
+
193
+ /* Tool connection active */
194
+ .react-flow__edge.tool-active path {
195
+ stroke: ${isDark ? '#ffb86c' : '#ea580c'} !important;
196
+ stroke-width: 3px !important;
197
+ }
198
+
199
+ @keyframes dashFlow {
200
+ 0% { stroke-dashoffset: 24; }
201
+ 100% { stroke-dashoffset: 0; }
202
+ }
203
+
204
+ /* Executing node - visible glow in both modes */
205
+ .react-flow__node.executing {
206
+ filter: ${isDark
207
+ ? `drop-shadow(0 0 8px ${colors.edgeExecuting}) drop-shadow(0 0 16px ${colors.edgeExecuting}80)`
208
+ : 'drop-shadow(0 0 10px rgba(37, 99, 235, 0.8)) drop-shadow(0 0 20px rgba(37, 99, 235, 0.6))'};
209
+ animation: ${isDark ? 'nodeGlowDark' : 'nodeGlowLight'} 1.2s ease-in-out infinite;
210
+ }
211
+
212
+ @keyframes nodeGlowDark {
213
+ 0%, 100% {
214
+ filter: drop-shadow(0 0 8px ${colors.edgeExecuting}) drop-shadow(0 0 16px ${colors.edgeExecuting}80);
215
+ }
216
+ 50% {
217
+ filter: drop-shadow(0 0 14px ${colors.edgeExecuting}) drop-shadow(0 0 24px ${colors.edgeExecuting});
218
+ }
219
+ }
220
+
221
+ @keyframes nodeGlowLight {
222
+ 0%, 100% {
223
+ filter: drop-shadow(0 0 10px rgba(37, 99, 235, 0.8)) drop-shadow(0 0 20px rgba(37, 99, 235, 0.6));
224
+ }
225
+ 50% {
226
+ filter: drop-shadow(0 0 16px rgba(37, 99, 235, 1)) drop-shadow(0 0 30px rgba(37, 99, 235, 0.8));
227
+ }
228
+ }
229
+ `;
230
+
231
+ const initialNodes: Node[] = [];
232
+ const initialEdges: Edge[] = [];
233
+
234
+ // Inner component that uses useReactFlow() - must be inside ReactFlowProvider
235
+ const DashboardContent: React.FC = () => {
236
+ const theme = useAppTheme();
237
+ const { isDarkMode } = useTheme();
238
+ const {
239
+ currentWorkflow,
240
+ hasUnsavedChanges,
241
+ savedWorkflows,
242
+ sidebarVisible,
243
+ componentPaletteVisible,
244
+ updateWorkflow,
245
+ loadSavedWorkflows,
246
+ createNewWorkflow,
247
+ deleteWorkflow,
248
+ migrateCurrentWorkflow,
249
+ toggleSidebar,
250
+ toggleComponentPalette,
251
+ proMode,
252
+ toggleProMode,
253
+ exportWorkflowToJSON,
254
+ exportWorkflowToFile,
255
+ setCurrentWorkflow,
256
+ whatsappSettingsOpen,
257
+ setWhatsAppSettingsOpen,
258
+ androidSettingsOpen,
259
+ setAndroidSettingsOpen,
260
+ selectedNode,
261
+ setSelectedNode,
262
+ renamingNodeId,
263
+ setRenamingNodeId,
264
+ // Per-workflow UI state (n8n pattern)
265
+ setWorkflowExecuting,
266
+ setWorkflowExecutionOrder,
267
+ setWorkflowViewport,
268
+ clearWorkflowExecutionState,
269
+ } = useAppStore();
270
+
271
+ // ReactFlow state management (local state for performance)
272
+ const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
273
+ const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
274
+
275
+ // ReactFlow instance for viewport control (n8n pattern - per-workflow viewport)
276
+ const reactFlowInstance = useReactFlow();
277
+
278
+ // AI execution state - result and modal are local, execution tracking is per-workflow
279
+ const [executionResult, setExecutionResult] = React.useState<any>(null);
280
+ const [showResult, setShowResult] = React.useState(false);
281
+
282
+ // Get per-workflow execution state (n8n pattern - isolated per workflow)
283
+ // Subscribe to workflowUIStates directly so Zustand triggers re-renders when it changes
284
+ const workflowUIStates = useAppStore(state => state.workflowUIStates);
285
+ const workflowUIState = React.useMemo(() => {
286
+ if (!currentWorkflow?.id) return null;
287
+ return workflowUIStates[currentWorkflow.id] || { isExecuting: false, executedNodes: [], executionOrder: [], selectedNodeId: null };
288
+ }, [workflowUIStates, currentWorkflow?.id]);
289
+ const isExecuting = workflowUIState?.isExecuting || false;
290
+ const executedNodes = React.useMemo(() => new Set(workflowUIState?.executedNodes || []), [workflowUIState?.executedNodes]);
291
+ const executionOrder = workflowUIState?.executionOrder || [];
292
+ // Custom hooks for different concerns
293
+ const {
294
+ handleWorkflowNameChange,
295
+ handleSave,
296
+ handleNew,
297
+ handleOpen,
298
+ handleSelectWorkflow,
299
+ } = useWorkflowManagement();
300
+
301
+ const { collapsedSections, searchQuery, setSearchQuery, toggleSection } = useComponentPalette();
302
+ const { saveNodeParameters, executeWorkflow, deployWorkflow, cancelDeployment, nodeStatuses, deploymentStatus, workflowLock } = useWebSocket();
303
+
304
+ // Scope deployment and lock to current workflow (n8n pattern)
305
+ // Only show as "running" or "locked" if it applies to the currently viewed workflow
306
+ const isCurrentWorkflowDeployed = deploymentStatus.isRunning &&
307
+ deploymentStatus.workflow_id === currentWorkflow?.id;
308
+ const isCurrentWorkflowLocked = workflowLock.locked &&
309
+ workflowLock.workflow_id === currentWorkflow?.id;
310
+ const { onDragOver, onDrop, handleComponentDragStart } = useDragAndDrop({ nodes, setNodes, saveNodeParameters });
311
+ const { onConnect, onNodesDelete, onEdgesDelete } = useReactFlowNodes({ setNodes, setEdges });
312
+ const { copySelectedNodes, pasteNodes } = useCopyPaste({ nodes, edges, setNodes, setEdges, saveNodeParameters });
313
+
314
+ // Toggle disabled state on selected nodes
315
+ const toggleDisableSelected = React.useCallback(() => {
316
+ setNodes(nds => nds.map(node => {
317
+ if (node.selected) {
318
+ return {
319
+ ...node,
320
+ data: {
321
+ ...node.data,
322
+ disabled: !node.data?.disabled,
323
+ },
324
+ };
325
+ }
326
+ return node;
327
+ }));
328
+ }, [setNodes]);
329
+
330
+ // Note: executedNodes and executionOrder are now derived from per-workflow state above
331
+
332
+ // Settings state with localStorage persistence
333
+ const [settings, setSettings] = React.useState<WorkflowSettings>(() => {
334
+ try {
335
+ const saved = localStorage.getItem('workflow_settings');
336
+ return saved ? { ...defaultSettings, ...JSON.parse(saved) } : defaultSettings;
337
+ } catch {
338
+ return defaultSettings;
339
+ }
340
+ });
341
+ const [settingsOpen, setSettingsOpen] = React.useState(false);
342
+ const [credentialsOpen, setCredentialsOpen] = React.useState(false);
343
+ const [consolePanelOpen, setConsolePanelOpen] = React.useState(false);
344
+
345
+ // Context menu state for node right-click
346
+ const [contextMenu, setContextMenu] = React.useState<{
347
+ nodeId: string;
348
+ x: number;
349
+ y: number;
350
+ } | null>(null);
351
+
352
+ // Persist settings to localStorage
353
+ React.useEffect(() => {
354
+ localStorage.setItem('workflow_settings', JSON.stringify(settings));
355
+ }, [settings]);
356
+
357
+ // Update nodes with execution status classes
358
+ const styledNodes = React.useMemo(() => {
359
+ return nodes.map(node => {
360
+ const nodeStatus = nodeStatuses[node.id];
361
+ let className = '';
362
+
363
+ if (nodeStatus?.status === 'executing' || nodeStatus?.status === 'waiting') {
364
+ className = 'executing';
365
+ } else if (nodeStatus?.status === 'success') {
366
+ className = 'completed';
367
+ } else if (nodeStatus?.status === 'error') {
368
+ className = 'error';
369
+ } else if (isExecuting && executionOrder.includes(node.id) && !executedNodes.has(node.id)) {
370
+ className = 'pending';
371
+ }
372
+
373
+ return {
374
+ ...node,
375
+ className
376
+ };
377
+ });
378
+ }, [nodes, nodeStatuses, isExecuting, executionOrder, executedNodes]);
379
+
380
+ // Update edges with execution status classes
381
+ const styledEdges = React.useMemo(() => {
382
+ return edges.map(edge => {
383
+ const sourceStatus = nodeStatuses[edge.source];
384
+ const targetStatus = nodeStatuses[edge.target];
385
+ const targetNode = nodes.find(n => n.id === edge.target);
386
+
387
+ let className = '';
388
+
389
+ // Check if this edge connects to an AI Agent's memory or tools/skill handle
390
+ const isMemoryConnection = edge.targetHandle === 'input-memory';
391
+ const isToolConnection = edge.targetHandle === 'input-tools';
392
+ const isSkillConnection = edge.targetHandle === 'input-skill';
393
+ const isAIAgentTarget = targetNode?.type === 'aiAgent' || targetNode?.type === 'chatAgent';
394
+
395
+ // Highlight memory/tool connections when AI Agent is executing and using them
396
+ if (isAIAgentTarget && targetStatus?.status === 'executing') {
397
+ const phase = targetStatus?.data?.phase as string | undefined;
398
+ const hasMemory = targetStatus?.data?.has_memory;
399
+
400
+ // Memory connection highlights during memory phases
401
+ if (isMemoryConnection && hasMemory) {
402
+ if (phase === 'loading_memory' || phase === 'memory_loaded' || phase === 'saving_memory') {
403
+ className = 'memory-active';
404
+ } else if (phase === 'invoking_llm') {
405
+ // Keep memory edge highlighted during LLM invocation to show context is being used
406
+ className = 'memory-active';
407
+ }
408
+ }
409
+ // Tool connection highlights when the specific tool node is executing
410
+ // Only highlight the edge whose source (tool node) is actually being used
411
+ else if (isToolConnection) {
412
+ const toolNodeStatus = sourceStatus?.status;
413
+ if (toolNodeStatus === 'executing') {
414
+ // This specific tool is being executed - highlight its edge
415
+ className = 'tool-active';
416
+ } else if ((phase === 'invoking_llm' || phase === 'building_graph') && toolNodeStatus === 'success') {
417
+ // Tool completed successfully - keep edge showing success
418
+ className = 'completed';
419
+ }
420
+ }
421
+ // Skill connection highlights during skill loading phase (Chat Agent)
422
+ // Skills provide context to LLM, so highlight only when loading skills
423
+ else if (isSkillConnection) {
424
+ if (phase === 'loading_skills') {
425
+ className = 'skill-active';
426
+ }
427
+ }
428
+ }
429
+
430
+ // Standard edge status classes - ONLY apply during active execution or deployment
431
+ // When not executing/deploying, all edges should have the same default cyan color
432
+ const isActiveExecution = isExecuting || isCurrentWorkflowDeployed;
433
+ if (!className && isActiveExecution) {
434
+ const srcStatus = sourceStatus?.status;
435
+ const tgtStatus = targetStatus?.status;
436
+
437
+ // Edge is executing if target is currently executing (data flowing into it)
438
+ if (tgtStatus === 'executing') {
439
+ className = 'executing';
440
+ }
441
+ // Edge is completed if both source and target are successful during this execution
442
+ else if (srcStatus === 'success' && tgtStatus === 'success') {
443
+ className = 'completed';
444
+ }
445
+ // Edge has error if target has error
446
+ else if (tgtStatus === 'error') {
447
+ className = 'error';
448
+ }
449
+ // Edge shows data flowing when source completed and target is waiting for inputs
450
+ // This indicates data has been produced and is available to the target
451
+ else if (srcStatus === 'success' && tgtStatus === 'waiting') {
452
+ className = 'executing';
453
+ }
454
+ // Edge is pending if source completed but target hasn't started
455
+ else if (srcStatus === 'success' && !tgtStatus) {
456
+ className = 'pending';
457
+ }
458
+ // Edge is pending if source is waiting (hasn't produced output yet)
459
+ // This keeps downstream edges from glowing until source completes
460
+ else if (srcStatus === 'waiting') {
461
+ className = 'pending';
462
+ }
463
+ }
464
+
465
+ return {
466
+ ...edge,
467
+ className
468
+ };
469
+ });
470
+ }, [edges, nodeStatuses, isExecuting, isCurrentWorkflowDeployed, nodes]);
471
+
472
+ // Memoize ReactFlow options to prevent unnecessary re-renders
473
+ const defaultEdgeOptions = React.useMemo(() => ({
474
+ type: 'smoothstep',
475
+ animated: true,
476
+ style: { stroke: theme.dracula.cyan, strokeWidth: 3 },
477
+ }), [theme.dracula.cyan]);
478
+
479
+ const connectionLineStyle = React.useMemo(() => ({
480
+ stroke: theme.dracula.cyan,
481
+ strokeWidth: 2
482
+ }), [theme.dracula.cyan]);
483
+
484
+ const reactFlowStyle = React.useMemo(() => ({
485
+ width: '100%',
486
+ height: '100%',
487
+ backgroundColor: theme.colors.background,
488
+ }), [theme.colors.background]);
489
+
490
+ const snapGrid: [number, number] = React.useMemo(() => [20, 20], []);
491
+
492
+ const proOptions = React.useMemo(() => ({ hideAttribution: true }), []);
493
+
494
+ // Use useRef for nodeTypes to guarantee the same object reference across all renders
495
+ // including React.StrictMode's double-render cycle. useMemo can't guarantee this
496
+ // because it may run during both render cycles.
497
+ const nodeTypesRef = React.useRef(moduleNodeTypes);
498
+ const edgeTypesRef = React.useRef(moduleEdgeTypes);
499
+
500
+ // Execute entire workflow from start node to end
501
+ const handleRun = async () => {
502
+ if (!currentWorkflow) return;
503
+ const workflowId = currentWorkflow.id;
504
+
505
+ // Use per-workflow state setters (n8n pattern)
506
+ setWorkflowExecuting(workflowId, true);
507
+ setExecutionResult(null);
508
+ clearWorkflowExecutionState(workflowId);
509
+ setWorkflowExecuting(workflowId, true); // Re-set after clear
510
+
511
+ try {
512
+ // Check if there's a start node
513
+ const startNode = nodes.find(node => node.type === 'start');
514
+ if (!startNode) {
515
+ alert('No Start node found in workflow.\n\nAdd a Start node to begin workflow execution.');
516
+ setWorkflowExecuting(workflowId, false);
517
+ return;
518
+ }
519
+
520
+ // Build execution order for visual feedback (BFS from start node)
521
+ const buildOrder = () => {
522
+ const order: string[] = [];
523
+ const visited = new Set<string>();
524
+ const queue = [startNode.id];
525
+ const adjacencyMap = new Map<string, string[]>();
526
+
527
+ edges.forEach(edge => {
528
+ const sources = adjacencyMap.get(edge.source) || [];
529
+ sources.push(edge.target);
530
+ adjacencyMap.set(edge.source, sources);
531
+ });
532
+
533
+ while (queue.length > 0) {
534
+ const currentId = queue.shift()!;
535
+ if (visited.has(currentId)) continue;
536
+ visited.add(currentId);
537
+ order.push(currentId);
538
+
539
+ const connected = adjacencyMap.get(currentId) || [];
540
+ connected.forEach(id => {
541
+ if (!visited.has(id)) queue.push(id);
542
+ });
543
+ }
544
+ return order;
545
+ };
546
+
547
+ const order = buildOrder();
548
+ setWorkflowExecutionOrder(workflowId, order);
549
+
550
+ console.log('[Workflow Run] Starting workflow execution with', nodes.length, 'nodes and', edges.length, 'edges');
551
+ console.log('[Workflow Run] Execution order:', order);
552
+
553
+ // Execute the entire workflow via WebSocket
554
+ const workflowResult = await executeWorkflow(nodes, edges);
555
+
556
+ console.log('[Workflow Run] Execution complete:', workflowResult);
557
+
558
+ // Build result for display
559
+ const result = {
560
+ success: workflowResult.success,
561
+ nodeId: 'workflow',
562
+ nodeName: currentWorkflow.name || 'Workflow',
563
+ timestamp: new Date().toISOString(),
564
+ executionTime: workflowResult.execution_time || 0,
565
+ outputs: workflowResult.node_results || {},
566
+ data: workflowResult,
567
+ error: workflowResult.error || (workflowResult.errors?.length > 0 ? workflowResult.errors[0].error : undefined),
568
+ nodeData: workflowResult,
569
+ // Workflow-specific display data
570
+ nodesExecuted: workflowResult.nodes_executed || [],
571
+ executionOrder: workflowResult.execution_order || [],
572
+ totalNodes: workflowResult.total_nodes || 0,
573
+ completedNodes: workflowResult.completed_nodes || 0,
574
+ nodeResults: workflowResult.node_results || {},
575
+ errors: workflowResult.errors || [],
576
+ // For backwards compatibility with AI result modal
577
+ response: workflowResult.success
578
+ ? `Workflow executed successfully. ${workflowResult.completed_nodes}/${workflowResult.total_nodes} nodes completed.`
579
+ : `Workflow failed: ${workflowResult.error || 'Unknown error'}`,
580
+ model: 'workflow'
581
+ };
582
+
583
+ // Set result and show modal
584
+ setExecutionResult(result);
585
+ setShowResult(true);
586
+
587
+ } catch (error: any) {
588
+ console.error('Workflow execution error:', error);
589
+
590
+ // Create error result for modal display
591
+ const errorResult = {
592
+ success: false,
593
+ nodeId: 'workflow',
594
+ nodeName: currentWorkflow?.name || 'Workflow',
595
+ timestamp: new Date().toISOString(),
596
+ executionTime: 0,
597
+ error: error.message || 'Unknown execution error',
598
+ response: `Error: ${error.message}`,
599
+ model: 'workflow'
600
+ };
601
+
602
+ setExecutionResult(errorResult);
603
+ setShowResult(true);
604
+ } finally {
605
+ setWorkflowExecuting(workflowId, false);
606
+ }
607
+ };
608
+
609
+ // Deploy workflow - runs continuously until cancelled
610
+ const handleDeploy = async () => {
611
+ if (!currentWorkflow) return;
612
+
613
+ // Check if there's at least one trigger node (workflow entry points)
614
+ // Trigger types: start, cronScheduler, webhookTrigger, whatsappReceive, workflowTrigger, chatTrigger
615
+ const triggerTypes = ['start', 'cronScheduler', 'webhookTrigger', 'whatsappReceive', 'workflowTrigger', 'chatTrigger'];
616
+ const hasTriggerNode = nodes.some(node => triggerTypes.includes(node.type || ''));
617
+ if (!hasTriggerNode) {
618
+ alert('No trigger node found in workflow.\n\nAdd a trigger node (Cron Scheduler, WhatsApp Receive, Webhook, Chat Trigger, etc.) to begin deployment.');
619
+ return;
620
+ }
621
+
622
+ try {
623
+ // Settings are already synced to backend via WebSocket from SettingsPanel
624
+ // Backend will use the stored settings
625
+
626
+ // DEBUG: Log edges being sent to deployment
627
+ console.log('[Dashboard] Deploying with edges:', {
628
+ edgeCount: edges.length,
629
+ edges: edges.map(e => ({
630
+ id: e.id,
631
+ source: e.source,
632
+ target: e.target,
633
+ sourceHandle: e.sourceHandle,
634
+ targetHandle: e.targetHandle
635
+ })),
636
+ // Check for toolkit connections specifically
637
+ toolkitEdges: edges.filter(e =>
638
+ e.target?.includes('androidTool') || e.source?.includes('androidTool')
639
+ )
640
+ });
641
+
642
+ const result = await deployWorkflow(currentWorkflow.id, nodes, edges, 'default');
643
+
644
+ if (!result.success) {
645
+ console.error('[Dashboard] Deployment failed:', result.error);
646
+ alert(`Failed to start deployment: ${result.error}`);
647
+ }
648
+ } catch (error: any) {
649
+ console.error('[Dashboard] Deployment error:', error);
650
+ alert(`Deployment error: ${error.message}`);
651
+ }
652
+ };
653
+
654
+ // Cancel running deployment for current workflow
655
+ const handleCancelDeployment = async () => {
656
+ try {
657
+ const workflowId = currentWorkflow?.id;
658
+ console.log('[Dashboard] Cancelling deployment for workflow:', workflowId);
659
+ const result = await cancelDeployment(workflowId);
660
+
661
+ if (result.success) {
662
+ console.log('[Dashboard] Deployment cancelled:', result);
663
+ } else {
664
+ console.error('[Dashboard] Failed to cancel deployment:', result.message);
665
+ }
666
+ } catch (error: any) {
667
+ console.error('[Dashboard] Cancel deployment error:', error);
668
+ }
669
+ };
670
+
671
+ const handleExportJSON = async () => {
672
+ try {
673
+ const jsonString = exportWorkflowToJSON();
674
+ await navigator.clipboard.writeText(jsonString);
675
+ alert('Workflow JSON copied to clipboard');
676
+ } catch (error) {
677
+ console.error('Export JSON error:', error);
678
+ alert('Failed to export workflow JSON');
679
+ }
680
+ };
681
+
682
+ const handleExportFile = () => {
683
+ try {
684
+ exportWorkflowToFile();
685
+ } catch (error) {
686
+ console.error('Export file error:', error);
687
+ alert('Failed to export workflow file');
688
+ }
689
+ };
690
+
691
+ const handleImportJSON = () => {
692
+ const fileInput = document.createElement('input');
693
+ fileInput.type = 'file';
694
+ fileInput.accept = '.json';
695
+ fileInput.onchange = async (e) => {
696
+ try {
697
+ const file = (e.target as HTMLInputElement).files?.[0];
698
+ if (!file) return;
699
+
700
+ const importedWorkflow = await importWorkflowFromFile(file);
701
+
702
+ const workflow = {
703
+ ...importedWorkflow,
704
+ id: generateWorkflowId(),
705
+ createdAt: new Date(),
706
+ lastModified: new Date()
707
+ };
708
+
709
+ console.log('Importing workflow:', workflow);
710
+
711
+ for (const node of workflow.nodes) {
712
+ if (node.data && Object.keys(node.data).length > 0) {
713
+ try {
714
+ await saveNodeParameters(node.id, node.data);
715
+ console.log(`Saved parameters for node ${node.id}:`, node.data);
716
+ } catch (error) {
717
+ console.error(`Failed to save parameters for node ${node.id}:`, error);
718
+ }
719
+ }
720
+ }
721
+
722
+ setCurrentWorkflow(workflow);
723
+
724
+ console.log('Workflow imported successfully');
725
+ alert(`Workflow "${workflow.name}" imported with ${workflow.nodes.length} nodes and ${workflow.edges.length} connections`);
726
+ } catch (error: any) {
727
+ console.error('Import error:', error);
728
+ alert(`Failed to import workflow: ${error.message}`);
729
+ }
730
+ };
731
+ fileInput.click();
732
+ };
733
+ // Load saved workflows on mount and create default workflow if needed
734
+ const hasMigrated = React.useRef(false);
735
+ useEffect(() => {
736
+ console.log('[Dashboard] Mount effect - loading workflows', {
737
+ hasCurrentWorkflow: !!currentWorkflow,
738
+ currentWorkflowId: currentWorkflow?.id,
739
+ });
740
+ loadSavedWorkflows();
741
+ if (!currentWorkflow) {
742
+ console.log('[Dashboard] No current workflow, creating new one');
743
+ createNewWorkflow();
744
+ } else if (!hasMigrated.current) {
745
+ console.log('[Dashboard] Migrating current workflow');
746
+ migrateCurrentWorkflow();
747
+ hasMigrated.current = true;
748
+ }
749
+ }, [loadSavedWorkflows, currentWorkflow, createNewWorkflow, migrateCurrentWorkflow]);
750
+
751
+ // Sync workflow state → ReactFlow state (when loading workflows or data changes)
752
+ // Note: Database is the source of truth for parameters - node.data should NOT store parameters
753
+ // Parameters are loaded from database when parameter panel opens (useParameterPanel hook)
754
+ // and when backend executes nodes (NodeExecutor._prepare_parameters)
755
+ useEffect(() => {
756
+ if (currentWorkflow && currentWorkflow.id) {
757
+ console.log('[Dashboard] Sync Store -> ReactFlow', {
758
+ workflowId: currentWorkflow.id,
759
+ storeEdgeCount: (currentWorkflow.edges || []).length,
760
+ storeEdges: (currentWorkflow.edges || []).map(e => ({ id: e.id, source: e.source, target: e.target })),
761
+ lastModified: currentWorkflow.lastModified
762
+ });
763
+ const workflowNodes = currentWorkflow.nodes || [];
764
+ setNodes(workflowNodes);
765
+ setEdges(currentWorkflow.edges || []);
766
+ // Do NOT sync database parameters to node.data
767
+ // Database is the single source of truth for parameters
768
+ // This prevents dual storage issues where node.data could diverge from database
769
+ }
770
+ }, [currentWorkflow?.id, currentWorkflow?.lastModified, setNodes, setEdges]);
771
+
772
+ // Sync ReactFlow state → workflow state (debounced for performance)
773
+ useEffect(() => {
774
+ if (!currentWorkflow || !currentWorkflow.id) return;
775
+
776
+ const timeoutId = setTimeout(() => {
777
+ try {
778
+ const currentNodesStr = JSON.stringify(sanitizeNodesForComparison(nodes));
779
+ const currentEdgesStr = JSON.stringify(sanitizeEdgesForComparison(edges));
780
+ const workflowNodesStr = JSON.stringify(sanitizeNodesForComparison(currentWorkflow.nodes || []));
781
+ const workflowEdgesStr = JSON.stringify(sanitizeEdgesForComparison(currentWorkflow.edges || []));
782
+
783
+ if (currentNodesStr !== workflowNodesStr || currentEdgesStr !== workflowEdgesStr) {
784
+ console.log('[Dashboard] Syncing ReactFlow -> Store', {
785
+ reactFlowEdgeCount: edges.length,
786
+ storeEdgeCount: (currentWorkflow.edges || []).length,
787
+ newEdges: edges.filter(e => !(currentWorkflow.edges || []).find(we => we.id === e.id))
788
+ });
789
+ updateWorkflow({ nodes, edges });
790
+ }
791
+ } catch (error) {
792
+ console.warn('Failed to sync workflow state:', error);
793
+ }
794
+ }, theme.constants.debounceDelay.workflowUpdate);
795
+
796
+ return () => clearTimeout(timeoutId);
797
+ }, [nodes, edges, currentWorkflow?.id, updateWorkflow]);
798
+
799
+ // Track previous workflow ID for viewport save/restore (n8n pattern)
800
+ const prevWorkflowIdRef = React.useRef<string | null>(null);
801
+ // Track if we've already restored viewport for current workflow (prevent duplicate restores)
802
+ const viewportRestoredForRef = React.useRef<string | null>(null);
803
+
804
+ // Save viewport when switching workflows, restore after nodes load (n8n pattern)
805
+ useEffect(() => {
806
+ const currentId = currentWorkflow?.id;
807
+ const prevId = prevWorkflowIdRef.current;
808
+
809
+ // Save viewport of previous workflow before switching
810
+ if (prevId && prevId !== currentId) {
811
+ try {
812
+ const viewport = reactFlowInstance.getViewport();
813
+ setWorkflowViewport(prevId, viewport);
814
+ } catch {
815
+ // Failed to save viewport - ignore
816
+ }
817
+ // Reset the restored flag when switching to new workflow
818
+ viewportRestoredForRef.current = null;
819
+ }
820
+
821
+ prevWorkflowIdRef.current = currentId || null;
822
+ }, [currentWorkflow?.id, reactFlowInstance, setWorkflowViewport]);
823
+
824
+ // Restore viewport AFTER nodes are loaded and rendered
825
+ // Only restores saved viewport - never auto-centers
826
+ useEffect(() => {
827
+ const currentId = currentWorkflow?.id;
828
+ if (!currentId) return;
829
+
830
+ // Skip if we already restored viewport for this workflow
831
+ if (viewportRestoredForRef.current === currentId) {
832
+ return;
833
+ }
834
+
835
+ // Get saved viewport from store
836
+ const uiState = workflowUIStates[currentId];
837
+ const savedViewport = uiState?.viewport;
838
+
839
+ // Only restore if we have a saved viewport
840
+ if (!savedViewport) {
841
+ viewportRestoredForRef.current = currentId;
842
+ return;
843
+ }
844
+
845
+ // Use delay to ensure ReactFlow has finished rendering nodes
846
+ const timeoutId = setTimeout(() => {
847
+ try {
848
+ reactFlowInstance.setViewport(savedViewport, { duration: 0 });
849
+ viewportRestoredForRef.current = currentId;
850
+ } catch {
851
+ // Viewport restore failed - ignore silently
852
+ }
853
+ }, 100);
854
+
855
+ return () => clearTimeout(timeoutId);
856
+ }, [currentWorkflow?.id, nodes.length, workflowUIStates, reactFlowInstance]);
857
+
858
+ // Node context menu handler (right-click)
859
+ const onNodeContextMenu = React.useCallback(
860
+ (event: React.MouseEvent, node: Node) => {
861
+ event.preventDefault();
862
+ // Select the node when right-clicking
863
+ setSelectedNode(node);
864
+ setContextMenu({
865
+ nodeId: node.id,
866
+ x: event.clientX,
867
+ y: event.clientY,
868
+ });
869
+ },
870
+ [setSelectedNode]
871
+ );
872
+
873
+ // Close context menu
874
+ const closeContextMenu = React.useCallback(() => {
875
+ setContextMenu(null);
876
+ }, []);
877
+
878
+ // Context menu actions
879
+ const handleContextMenuRename = React.useCallback(() => {
880
+ if (contextMenu) {
881
+ setRenamingNodeId(contextMenu.nodeId);
882
+ }
883
+ closeContextMenu();
884
+ }, [contextMenu, setRenamingNodeId, closeContextMenu]);
885
+
886
+ const handleContextMenuCopy = React.useCallback(() => {
887
+ if (contextMenu) {
888
+ // Select the node first, then copy
889
+ const node = nodes.find(n => n.id === contextMenu.nodeId);
890
+ if (node) {
891
+ setNodes(nds => nds.map(n => ({ ...n, selected: n.id === contextMenu.nodeId })));
892
+ // Small delay to ensure selection is applied before copy
893
+ setTimeout(() => copySelectedNodes(), 0);
894
+ }
895
+ }
896
+ closeContextMenu();
897
+ }, [contextMenu, nodes, setNodes, copySelectedNodes, closeContextMenu]);
898
+
899
+ const handleContextMenuDelete = React.useCallback(() => {
900
+ if (contextMenu) {
901
+ onNodesDelete([nodes.find(n => n.id === contextMenu.nodeId)].filter(Boolean) as Node[]);
902
+ }
903
+ closeContextMenu();
904
+ }, [contextMenu, nodes, onNodesDelete, closeContextMenu]);
905
+
906
+ // Keyboard shortcut handler for workflow operations
907
+ useEffect(() => {
908
+ const handleKeyDown = (event: KeyboardEvent) => {
909
+ // Ignore shortcuts when typing in input/textarea
910
+ if (event.target instanceof HTMLInputElement ||
911
+ event.target instanceof HTMLTextAreaElement) {
912
+ return;
913
+ }
914
+
915
+ // Ignore shortcuts when renaming a node
916
+ if (renamingNodeId) {
917
+ return;
918
+ }
919
+
920
+ // F2 to rename selected node
921
+ if (event.key === 'F2' && selectedNode) {
922
+ event.preventDefault();
923
+ setRenamingNodeId(selectedNode.id);
924
+ return;
925
+ }
926
+
927
+ // Check for Ctrl/Cmd key shortcuts
928
+ if (event.ctrlKey || event.metaKey) {
929
+ switch (event.key.toLowerCase()) {
930
+ case 's':
931
+ event.preventDefault();
932
+ handleSave();
933
+ break;
934
+ case 'c':
935
+ event.preventDefault();
936
+ copySelectedNodes();
937
+ break;
938
+ case 'v':
939
+ event.preventDefault();
940
+ pasteNodes();
941
+ break;
942
+ }
943
+ } else {
944
+ // Non-modifier shortcuts
945
+ switch (event.key.toLowerCase()) {
946
+ case 'd':
947
+ // Toggle disable on selected nodes
948
+ event.preventDefault();
949
+ toggleDisableSelected();
950
+ break;
951
+ }
952
+ }
953
+ };
954
+
955
+ document.addEventListener('keydown', handleKeyDown);
956
+
957
+ return () => {
958
+ document.removeEventListener('keydown', handleKeyDown);
959
+ };
960
+ }, [handleSave, copySelectedNodes, pasteNodes, toggleDisableSelected, selectedNode, renamingNodeId, setRenamingNodeId]);
961
+
962
+ return (
963
+ <>
964
+ <style>{getEdgeStyles(theme.colors, isDarkMode)}</style>
965
+ <div style={{
966
+ width: '100%',
967
+ height: '100vh',
968
+ display: 'flex',
969
+ flexDirection: 'column',
970
+ backgroundColor: theme.colors.background,
971
+ fontFamily: 'system-ui, sans-serif',
972
+ }}>
973
+ {/* Top Toolbar */}
974
+ <TopToolbar
975
+ workflowName={currentWorkflow?.name || 'Untitled Workflow'}
976
+ onWorkflowNameChange={handleWorkflowNameChange}
977
+ onSave={handleSave}
978
+ onNew={handleNew}
979
+ onOpen={handleOpen}
980
+ onRun={handleRun}
981
+ isRunning={isExecuting}
982
+ onDeploy={handleDeploy}
983
+ onCancelDeployment={handleCancelDeployment}
984
+ isDeploying={isCurrentWorkflowDeployed}
985
+ hasUnsavedChanges={hasUnsavedChanges}
986
+ sidebarVisible={sidebarVisible}
987
+ onToggleSidebar={toggleSidebar}
988
+ componentPaletteVisible={componentPaletteVisible}
989
+ onToggleComponentPalette={toggleComponentPalette}
990
+ proMode={proMode}
991
+ onToggleProMode={toggleProMode}
992
+ onOpenSettings={() => setSettingsOpen(true)}
993
+ onOpenCredentials={() => setCredentialsOpen(true)}
994
+ onExportJSON={handleExportJSON}
995
+ onExportFile={handleExportFile}
996
+ onImportJSON={handleImportJSON}
997
+ />
998
+
999
+ {/* Main Content Area */}
1000
+ <div style={{
1001
+ flex: 1,
1002
+ display: 'flex',
1003
+ overflow: 'hidden',
1004
+ }}>
1005
+ {/* Left Workflow Sidebar */}
1006
+ <div style={{
1007
+ width: sidebarVisible ? '280px' : '0px',
1008
+ overflow: 'hidden',
1009
+ transition: 'width 0.3s ease',
1010
+ borderRight: sidebarVisible ? `1px solid ${theme.colors.border}` : 'none',
1011
+ display: 'flex',
1012
+ flexDirection: 'column',
1013
+ }}>
1014
+ {sidebarVisible && (
1015
+ <WorkflowSidebar
1016
+ workflows={savedWorkflows}
1017
+ currentWorkflowId={currentWorkflow?.id}
1018
+ onSelectWorkflow={handleSelectWorkflow}
1019
+ onDeleteWorkflow={deleteWorkflow}
1020
+ />
1021
+ )}
1022
+ </div>
1023
+
1024
+ {/* Canvas Area */}
1025
+ <div style={{
1026
+ flex: 1,
1027
+ display: 'flex',
1028
+ position: 'relative',
1029
+ }}>
1030
+ <div style={{
1031
+ flex: 1,
1032
+ backgroundColor: theme.colors.backgroundAlt,
1033
+ }}>
1034
+ <ErrorBoundary>
1035
+ <ReactFlow
1036
+ nodes={styledNodes}
1037
+ edges={styledEdges}
1038
+ onNodesChange={onNodesChange}
1039
+ onEdgesChange={onEdgesChange}
1040
+ onNodesDelete={onNodesDelete}
1041
+ onEdgesDelete={onEdgesDelete}
1042
+ onConnect={onConnect}
1043
+ onDragOver={onDragOver}
1044
+ onDrop={onDrop}
1045
+ onNodeContextMenu={onNodeContextMenu}
1046
+ nodeTypes={nodeTypesRef.current}
1047
+ edgeTypes={edgeTypesRef.current}
1048
+ connectionMode={ConnectionMode.Loose}
1049
+ deleteKeyCode={isCurrentWorkflowLocked ? [] : ['Delete', 'Backspace']}
1050
+ edgesFocusable={!isCurrentWorkflowLocked}
1051
+ edgesUpdatable={!isCurrentWorkflowLocked}
1052
+ nodesDraggable={!isCurrentWorkflowLocked}
1053
+ nodesConnectable={!isCurrentWorkflowLocked}
1054
+ nodesFocusable={!isCurrentWorkflowLocked}
1055
+ elementsSelectable={!isCurrentWorkflowLocked}
1056
+ selectNodesOnDrag={false}
1057
+ selectionOnDrag={true}
1058
+ selectionMode={SelectionMode.Partial}
1059
+ selectionKeyCode="Control"
1060
+ panOnDrag={true}
1061
+ panOnScroll={false}
1062
+ zoomOnScroll={true}
1063
+ preventScrolling={true}
1064
+ proOptions={proOptions}
1065
+ defaultEdgeOptions={defaultEdgeOptions}
1066
+ connectionLineStyle={connectionLineStyle}
1067
+ connectionLineType={ConnectionLineType.SmoothStep}
1068
+ snapToGrid={true}
1069
+ snapGrid={snapGrid}
1070
+ style={reactFlowStyle}
1071
+ >
1072
+ <Controls />
1073
+ </ReactFlow>
1074
+ </ErrorBoundary>
1075
+ </div>
1076
+
1077
+ {/* Right Component Palette */}
1078
+ <div style={{
1079
+ width: componentPaletteVisible ? theme.layout.sidebarWidth : '0px',
1080
+ overflow: 'hidden',
1081
+ transition: 'width 0.3s ease',
1082
+ borderLeft: componentPaletteVisible ? `1px solid ${theme.colors.border}` : 'none',
1083
+ display: 'flex',
1084
+ flexDirection: 'column',
1085
+ }}>
1086
+ {componentPaletteVisible && (
1087
+ <ComponentPalette
1088
+ nodeDefinitions={nodeDefinitions}
1089
+ searchQuery={searchQuery}
1090
+ onSearchChange={setSearchQuery}
1091
+ collapsedSections={collapsedSections}
1092
+ onToggleSection={toggleSection}
1093
+ onDragStart={handleComponentDragStart}
1094
+ proMode={proMode}
1095
+ />
1096
+ )}
1097
+ </div>
1098
+ </div>
1099
+ </div>
1100
+
1101
+ {/* Console Panel - n8n-style debug output at bottom */}
1102
+ <ConsolePanel
1103
+ isOpen={consolePanelOpen}
1104
+ onToggle={() => setConsolePanelOpen(prev => !prev)}
1105
+ nodes={nodes}
1106
+ />
1107
+
1108
+ {/* Parameter Panels */}
1109
+ <ErrorBoundary>
1110
+ <ParameterPanel />
1111
+ <LocationParameterPanel />
1112
+ </ErrorBoundary>
1113
+
1114
+ {/* AI Result Modal */}
1115
+ <AIResultModal
1116
+ isOpen={showResult}
1117
+ onClose={() => setShowResult(false)}
1118
+ result={executionResult}
1119
+ />
1120
+
1121
+ {/* Settings Panel Modal */}
1122
+ <SettingsPanel
1123
+ isOpen={settingsOpen}
1124
+ onClose={() => setSettingsOpen(false)}
1125
+ settings={settings}
1126
+ onSettingsChange={setSettings}
1127
+ />
1128
+
1129
+ {/* WhatsApp Settings Panel Modal */}
1130
+ <WhatsAppSettingsPanel
1131
+ isOpen={whatsappSettingsOpen}
1132
+ onClose={() => setWhatsAppSettingsOpen(false)}
1133
+ />
1134
+
1135
+ {/* Android Settings Panel Modal */}
1136
+ <AndroidSettingsPanel
1137
+ isOpen={androidSettingsOpen}
1138
+ onClose={() => setAndroidSettingsOpen(false)}
1139
+ />
1140
+
1141
+ {/* Credentials Modal */}
1142
+ <CredentialsModal
1143
+ visible={credentialsOpen}
1144
+ onClose={() => setCredentialsOpen(false)}
1145
+ />
1146
+
1147
+ {/* Node Context Menu (right-click) */}
1148
+ {contextMenu && (
1149
+ <NodeContextMenu
1150
+ nodeId={contextMenu.nodeId}
1151
+ x={contextMenu.x}
1152
+ y={contextMenu.y}
1153
+ onClose={closeContextMenu}
1154
+ onRename={handleContextMenuRename}
1155
+ onCopy={handleContextMenuCopy}
1156
+ onDelete={handleContextMenuDelete}
1157
+ />
1158
+ )}
1159
+ </div>
1160
+ </>
1161
+ );
1162
+ };
1163
+
1164
+ // Outer wrapper component that provides ReactFlowProvider context
1165
+ const Dashboard: React.FC = () => {
1166
+ return (
1167
+ <ReactFlowProvider>
1168
+ <DashboardContent />
1169
+ </ReactFlowProvider>
1170
+ );
1171
+ };
1172
+
1173
+ export default Dashboard;