machinaos 0.0.1 → 0.0.6

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 (420) hide show
  1. package/.env.template +71 -71
  2. package/LICENSE +21 -21
  3. package/README.md +145 -87
  4. package/bin/cli.js +62 -106
  5. package/client/.dockerignore +45 -45
  6. package/client/Dockerfile +68 -68
  7. package/client/dist/assets/index-DFSC53FP.css +1 -0
  8. package/client/dist/assets/index-fJ-1gTf5.js +613 -0
  9. package/client/dist/index.html +14 -0
  10. package/client/eslint.config.js +34 -16
  11. package/client/nginx.conf +66 -66
  12. package/client/package.json +61 -48
  13. package/client/src/App.tsx +27 -27
  14. package/client/src/Dashboard.tsx +1200 -1172
  15. package/client/src/ParameterPanel.tsx +302 -300
  16. package/client/src/components/AIAgentNode.tsx +315 -321
  17. package/client/src/components/APIKeyValidator.tsx +117 -117
  18. package/client/src/components/ClaudeChatModelNode.tsx +17 -17
  19. package/client/src/components/CredentialsModal.tsx +1200 -306
  20. package/client/src/components/GeminiChatModelNode.tsx +17 -17
  21. package/client/src/components/GenericNode.tsx +356 -356
  22. package/client/src/components/LocationParameterPanel.tsx +153 -153
  23. package/client/src/components/ModelNode.tsx +285 -285
  24. package/client/src/components/OpenAIChatModelNode.tsx +17 -17
  25. package/client/src/components/OutputPanel.tsx +470 -470
  26. package/client/src/components/ParameterRenderer.tsx +1873 -1873
  27. package/client/src/components/SkillEditorModal.tsx +3 -3
  28. package/client/src/components/SquareNode.tsx +812 -796
  29. package/client/src/components/ToolkitNode.tsx +365 -365
  30. package/client/src/components/auth/LoginPage.tsx +247 -247
  31. package/client/src/components/auth/ProtectedRoute.tsx +59 -59
  32. package/client/src/components/base/BaseChatModelNode.tsx +270 -270
  33. package/client/src/components/icons/AIProviderIcons.tsx +50 -50
  34. package/client/src/components/maps/GoogleMapsPicker.tsx +136 -136
  35. package/client/src/components/maps/MapsPreviewPanel.tsx +109 -109
  36. package/client/src/components/maps/index.ts +25 -25
  37. package/client/src/components/parameterPanel/InputSection.tsx +1094 -1094
  38. package/client/src/components/parameterPanel/LocationPanelLayout.tsx +64 -64
  39. package/client/src/components/parameterPanel/MapsSection.tsx +91 -91
  40. package/client/src/components/parameterPanel/MiddleSection.tsx +867 -571
  41. package/client/src/components/parameterPanel/OutputSection.tsx +80 -80
  42. package/client/src/components/parameterPanel/ParameterPanelLayout.tsx +81 -81
  43. package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +436 -436
  44. package/client/src/components/parameterPanel/index.ts +41 -41
  45. package/client/src/components/shared/DataPanel.tsx +142 -142
  46. package/client/src/components/shared/JSONTreeRenderer.tsx +105 -105
  47. package/client/src/components/ui/AIResultModal.tsx +203 -203
  48. package/client/src/components/ui/ApiKeyInput.tsx +93 -0
  49. package/client/src/components/ui/CodeEditor.tsx +81 -81
  50. package/client/src/components/ui/CollapsibleSection.tsx +87 -87
  51. package/client/src/components/ui/ComponentItem.tsx +153 -153
  52. package/client/src/components/ui/ComponentPalette.tsx +320 -320
  53. package/client/src/components/ui/ConsolePanel.tsx +151 -43
  54. package/client/src/components/ui/ErrorBoundary.tsx +195 -195
  55. package/client/src/components/ui/InputNodesPanel.tsx +203 -203
  56. package/client/src/components/ui/MapSelector.tsx +313 -313
  57. package/client/src/components/ui/Modal.tsx +151 -148
  58. package/client/src/components/ui/NodeOutputPanel.tsx +1150 -1150
  59. package/client/src/components/ui/OutputDisplayPanel.tsx +381 -381
  60. package/client/src/components/ui/QRCodeDisplay.tsx +182 -0
  61. package/client/src/components/ui/TopToolbar.tsx +736 -736
  62. package/client/src/components/ui/WorkflowSidebar.tsx +293 -293
  63. package/client/src/config/antdTheme.ts +186 -186
  64. package/client/src/contexts/AuthContext.tsx +221 -221
  65. package/client/src/contexts/ThemeContext.tsx +42 -42
  66. package/client/src/contexts/WebSocketContext.tsx +2144 -1971
  67. package/client/src/factories/baseChatModelFactory.ts +255 -255
  68. package/client/src/hooks/useAndroidOperations.ts +118 -164
  69. package/client/src/hooks/useApiKeyValidation.ts +106 -106
  70. package/client/src/hooks/useApiKeys.ts +238 -238
  71. package/client/src/hooks/useAppTheme.ts +17 -17
  72. package/client/src/hooks/useComponentPalette.ts +50 -50
  73. package/client/src/hooks/useDragAndDrop.ts +123 -123
  74. package/client/src/hooks/useDragVariable.ts +88 -88
  75. package/client/src/hooks/useExecution.ts +319 -313
  76. package/client/src/hooks/useParameterPanel.ts +176 -176
  77. package/client/src/hooks/useReactFlowNodes.ts +188 -188
  78. package/client/src/hooks/useToolSchema.ts +209 -209
  79. package/client/src/hooks/useWhatsApp.ts +196 -196
  80. package/client/src/hooks/useWorkflowManagement.ts +45 -45
  81. package/client/src/index.css +314 -314
  82. package/client/src/nodeDefinitions/aiAgentNodes.ts +335 -335
  83. package/client/src/nodeDefinitions/aiModelNodes.ts +340 -340
  84. package/client/src/nodeDefinitions/androidServiceNodes.ts +383 -383
  85. package/client/src/nodeDefinitions/chatNodes.ts +135 -135
  86. package/client/src/nodeDefinitions/codeNodes.ts +54 -54
  87. package/client/src/nodeDefinitions/index.ts +14 -14
  88. package/client/src/nodeDefinitions/locationNodes.ts +462 -462
  89. package/client/src/nodeDefinitions/schedulerNodes.ts +220 -220
  90. package/client/src/nodeDefinitions/skillNodes.ts +17 -5
  91. package/client/src/nodeDefinitions/utilityNodes.ts +284 -284
  92. package/client/src/nodeDefinitions/whatsappNodes.ts +821 -865
  93. package/client/src/nodeDefinitions.ts +101 -103
  94. package/client/src/services/dynamicParameterService.ts +95 -95
  95. package/client/src/services/execution/aiAgentExecutionService.ts +34 -34
  96. package/client/src/services/executionService.ts +227 -231
  97. package/client/src/services/workflowApi.ts +91 -91
  98. package/client/src/store/useAppStore.ts +578 -581
  99. package/client/src/styles/theme.ts +513 -508
  100. package/client/src/styles/zIndex.ts +16 -16
  101. package/client/src/types/ComponentTypes.ts +38 -38
  102. package/client/src/types/INodeProperties.ts +287 -287
  103. package/client/src/types/NodeTypes.ts +27 -27
  104. package/client/src/utils/formatters.ts +32 -32
  105. package/client/src/utils/googleMapsLoader.ts +139 -139
  106. package/client/src/utils/locationUtils.ts +84 -84
  107. package/client/src/utils/nodeUtils.ts +30 -30
  108. package/client/src/utils/workflow.ts +29 -29
  109. package/client/src/vite-env.d.ts +12 -12
  110. package/client/tailwind.config.js +59 -59
  111. package/client/tsconfig.json +25 -25
  112. package/client/vite.config.js +35 -35
  113. package/package.json +78 -70
  114. package/scripts/build.js +153 -45
  115. package/scripts/clean.js +40 -40
  116. package/scripts/start.js +234 -210
  117. package/scripts/stop.js +301 -325
  118. package/server/.dockerignore +44 -44
  119. package/server/Dockerfile +45 -45
  120. package/server/constants.py +244 -249
  121. package/server/core/cache.py +460 -460
  122. package/server/core/config.py +127 -127
  123. package/server/core/container.py +98 -98
  124. package/server/core/database.py +1296 -1210
  125. package/server/core/logging.py +313 -313
  126. package/server/main.py +288 -288
  127. package/server/middleware/__init__.py +5 -5
  128. package/server/middleware/auth.py +89 -89
  129. package/server/models/auth.py +52 -52
  130. package/server/models/cache.py +24 -24
  131. package/server/models/database.py +235 -210
  132. package/server/models/nodes.py +435 -455
  133. package/server/pyproject.toml +75 -72
  134. package/server/requirements.txt +83 -83
  135. package/server/routers/android.py +294 -294
  136. package/server/routers/auth.py +203 -203
  137. package/server/routers/database.py +150 -150
  138. package/server/routers/maps.py +141 -141
  139. package/server/routers/nodejs_compat.py +288 -288
  140. package/server/routers/webhook.py +90 -90
  141. package/server/routers/websocket.py +2239 -2127
  142. package/server/routers/whatsapp.py +761 -761
  143. package/server/routers/workflow.py +199 -199
  144. package/server/services/ai.py +2444 -2414
  145. package/server/services/android_service.py +588 -588
  146. package/server/services/auth.py +130 -130
  147. package/server/services/chat_client.py +160 -160
  148. package/server/services/deployment/manager.py +706 -706
  149. package/server/services/event_waiter.py +675 -785
  150. package/server/services/execution/executor.py +1351 -1351
  151. package/server/services/execution/models.py +1 -1
  152. package/server/services/handlers/__init__.py +122 -126
  153. package/server/services/handlers/ai.py +390 -355
  154. package/server/services/handlers/android.py +69 -260
  155. package/server/services/handlers/code.py +278 -278
  156. package/server/services/handlers/http.py +193 -193
  157. package/server/services/handlers/tools.py +146 -32
  158. package/server/services/handlers/triggers.py +107 -107
  159. package/server/services/handlers/utility.py +822 -822
  160. package/server/services/handlers/whatsapp.py +423 -476
  161. package/server/services/maps.py +288 -288
  162. package/server/services/memory_store.py +103 -103
  163. package/server/services/node_executor.py +372 -375
  164. package/server/services/scheduler.py +155 -155
  165. package/server/services/skill_loader.py +1 -1
  166. package/server/services/status_broadcaster.py +834 -826
  167. package/server/services/temporal/__init__.py +23 -23
  168. package/server/services/temporal/activities.py +344 -344
  169. package/server/services/temporal/client.py +76 -76
  170. package/server/services/temporal/executor.py +147 -147
  171. package/server/services/temporal/worker.py +251 -251
  172. package/server/services/temporal/workflow.py +355 -355
  173. package/server/services/temporal/ws_client.py +236 -236
  174. package/server/services/text.py +110 -110
  175. package/server/services/user_auth.py +172 -172
  176. package/server/services/websocket_client.py +29 -29
  177. package/server/services/workflow.py +597 -597
  178. package/server/skills/android-skill/SKILL.md +4 -4
  179. package/server/skills/code-skill/SKILL.md +123 -89
  180. package/server/skills/maps-skill/SKILL.md +3 -3
  181. package/server/skills/memory-skill/SKILL.md +1 -1
  182. package/server/skills/web-search-skill/SKILL.md +154 -0
  183. package/server/skills/whatsapp-skill/SKILL.md +3 -3
  184. package/server/uv.lock +461 -100
  185. package/server/whatsapp-rpc/.dockerignore +30 -30
  186. package/server/whatsapp-rpc/Dockerfile +44 -44
  187. package/server/whatsapp-rpc/Dockerfile.web +17 -17
  188. package/server/whatsapp-rpc/README.md +139 -139
  189. package/server/whatsapp-rpc/bin/whatsapp-rpc-server +0 -0
  190. package/server/whatsapp-rpc/cli.js +95 -95
  191. package/server/whatsapp-rpc/configs/config.yaml +6 -6
  192. package/server/whatsapp-rpc/docker-compose.yml +35 -35
  193. package/server/whatsapp-rpc/docs/API.md +410 -410
  194. package/server/whatsapp-rpc/node_modules/.package-lock.json +259 -0
  195. package/server/whatsapp-rpc/node_modules/chalk/license +9 -0
  196. package/server/whatsapp-rpc/node_modules/chalk/package.json +83 -0
  197. package/server/whatsapp-rpc/node_modules/chalk/readme.md +297 -0
  198. package/server/whatsapp-rpc/node_modules/chalk/source/index.d.ts +325 -0
  199. package/server/whatsapp-rpc/node_modules/chalk/source/index.js +225 -0
  200. package/server/whatsapp-rpc/node_modules/chalk/source/utilities.js +33 -0
  201. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  202. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  203. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  204. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  205. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  206. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  207. package/server/whatsapp-rpc/node_modules/commander/LICENSE +22 -0
  208. package/server/whatsapp-rpc/node_modules/commander/Readme.md +1148 -0
  209. package/server/whatsapp-rpc/node_modules/commander/esm.mjs +16 -0
  210. package/server/whatsapp-rpc/node_modules/commander/index.js +26 -0
  211. package/server/whatsapp-rpc/node_modules/commander/lib/argument.js +145 -0
  212. package/server/whatsapp-rpc/node_modules/commander/lib/command.js +2179 -0
  213. package/server/whatsapp-rpc/node_modules/commander/lib/error.js +43 -0
  214. package/server/whatsapp-rpc/node_modules/commander/lib/help.js +462 -0
  215. package/server/whatsapp-rpc/node_modules/commander/lib/option.js +329 -0
  216. package/server/whatsapp-rpc/node_modules/commander/lib/suggestSimilar.js +100 -0
  217. package/server/whatsapp-rpc/node_modules/commander/package-support.json +16 -0
  218. package/server/whatsapp-rpc/node_modules/commander/package.json +80 -0
  219. package/server/whatsapp-rpc/node_modules/commander/typings/esm.d.mts +3 -0
  220. package/server/whatsapp-rpc/node_modules/commander/typings/index.d.ts +884 -0
  221. package/server/whatsapp-rpc/node_modules/cross-spawn/LICENSE +21 -0
  222. package/server/whatsapp-rpc/node_modules/cross-spawn/README.md +89 -0
  223. package/server/whatsapp-rpc/node_modules/cross-spawn/index.js +39 -0
  224. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/enoent.js +59 -0
  225. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/parse.js +91 -0
  226. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/escape.js +47 -0
  227. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/readShebang.js +23 -0
  228. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/resolveCommand.js +52 -0
  229. package/server/whatsapp-rpc/node_modules/cross-spawn/package.json +73 -0
  230. package/server/whatsapp-rpc/node_modules/execa/index.d.ts +955 -0
  231. package/server/whatsapp-rpc/node_modules/execa/index.js +309 -0
  232. package/server/whatsapp-rpc/node_modules/execa/lib/command.js +119 -0
  233. package/server/whatsapp-rpc/node_modules/execa/lib/error.js +87 -0
  234. package/server/whatsapp-rpc/node_modules/execa/lib/kill.js +102 -0
  235. package/server/whatsapp-rpc/node_modules/execa/lib/pipe.js +42 -0
  236. package/server/whatsapp-rpc/node_modules/execa/lib/promise.js +36 -0
  237. package/server/whatsapp-rpc/node_modules/execa/lib/stdio.js +49 -0
  238. package/server/whatsapp-rpc/node_modules/execa/lib/stream.js +133 -0
  239. package/server/whatsapp-rpc/node_modules/execa/lib/verbose.js +19 -0
  240. package/server/whatsapp-rpc/node_modules/execa/license +9 -0
  241. package/server/whatsapp-rpc/node_modules/execa/package.json +90 -0
  242. package/server/whatsapp-rpc/node_modules/execa/readme.md +822 -0
  243. package/server/whatsapp-rpc/node_modules/get-stream/license +9 -0
  244. package/server/whatsapp-rpc/node_modules/get-stream/package.json +53 -0
  245. package/server/whatsapp-rpc/node_modules/get-stream/readme.md +291 -0
  246. package/server/whatsapp-rpc/node_modules/get-stream/source/array-buffer.js +84 -0
  247. package/server/whatsapp-rpc/node_modules/get-stream/source/array.js +32 -0
  248. package/server/whatsapp-rpc/node_modules/get-stream/source/buffer.js +20 -0
  249. package/server/whatsapp-rpc/node_modules/get-stream/source/contents.js +101 -0
  250. package/server/whatsapp-rpc/node_modules/get-stream/source/index.d.ts +119 -0
  251. package/server/whatsapp-rpc/node_modules/get-stream/source/index.js +5 -0
  252. package/server/whatsapp-rpc/node_modules/get-stream/source/string.js +36 -0
  253. package/server/whatsapp-rpc/node_modules/get-stream/source/utils.js +11 -0
  254. package/server/whatsapp-rpc/node_modules/get-them-args/LICENSE +21 -0
  255. package/server/whatsapp-rpc/node_modules/get-them-args/README.md +95 -0
  256. package/server/whatsapp-rpc/node_modules/get-them-args/index.js +97 -0
  257. package/server/whatsapp-rpc/node_modules/get-them-args/package.json +36 -0
  258. package/server/whatsapp-rpc/node_modules/human-signals/LICENSE +201 -0
  259. package/server/whatsapp-rpc/node_modules/human-signals/README.md +168 -0
  260. package/server/whatsapp-rpc/node_modules/human-signals/build/src/core.js +273 -0
  261. package/server/whatsapp-rpc/node_modules/human-signals/build/src/main.d.ts +73 -0
  262. package/server/whatsapp-rpc/node_modules/human-signals/build/src/main.js +70 -0
  263. package/server/whatsapp-rpc/node_modules/human-signals/build/src/realtime.js +16 -0
  264. package/server/whatsapp-rpc/node_modules/human-signals/build/src/signals.js +34 -0
  265. package/server/whatsapp-rpc/node_modules/human-signals/package.json +61 -0
  266. package/server/whatsapp-rpc/node_modules/is-stream/index.d.ts +81 -0
  267. package/server/whatsapp-rpc/node_modules/is-stream/index.js +29 -0
  268. package/server/whatsapp-rpc/node_modules/is-stream/license +9 -0
  269. package/server/whatsapp-rpc/node_modules/is-stream/package.json +44 -0
  270. package/server/whatsapp-rpc/node_modules/is-stream/readme.md +60 -0
  271. package/server/whatsapp-rpc/node_modules/isexe/LICENSE +15 -0
  272. package/server/whatsapp-rpc/node_modules/isexe/README.md +51 -0
  273. package/server/whatsapp-rpc/node_modules/isexe/index.js +57 -0
  274. package/server/whatsapp-rpc/node_modules/isexe/mode.js +41 -0
  275. package/server/whatsapp-rpc/node_modules/isexe/package.json +31 -0
  276. package/server/whatsapp-rpc/node_modules/isexe/test/basic.js +221 -0
  277. package/server/whatsapp-rpc/node_modules/isexe/windows.js +42 -0
  278. package/server/whatsapp-rpc/node_modules/kill-port/.editorconfig +12 -0
  279. package/server/whatsapp-rpc/node_modules/kill-port/.gitattributes +1 -0
  280. package/server/whatsapp-rpc/node_modules/kill-port/LICENSE +21 -0
  281. package/server/whatsapp-rpc/node_modules/kill-port/README.md +140 -0
  282. package/server/whatsapp-rpc/node_modules/kill-port/cli.js +25 -0
  283. package/server/whatsapp-rpc/node_modules/kill-port/example.js +21 -0
  284. package/server/whatsapp-rpc/node_modules/kill-port/index.js +46 -0
  285. package/server/whatsapp-rpc/node_modules/kill-port/logo.png +0 -0
  286. package/server/whatsapp-rpc/node_modules/kill-port/package.json +41 -0
  287. package/server/whatsapp-rpc/node_modules/kill-port/pnpm-lock.yaml +4606 -0
  288. package/server/whatsapp-rpc/node_modules/kill-port/test.js +16 -0
  289. package/server/whatsapp-rpc/node_modules/merge-stream/LICENSE +21 -0
  290. package/server/whatsapp-rpc/node_modules/merge-stream/README.md +78 -0
  291. package/server/whatsapp-rpc/node_modules/merge-stream/index.js +41 -0
  292. package/server/whatsapp-rpc/node_modules/merge-stream/package.json +19 -0
  293. package/server/whatsapp-rpc/node_modules/mimic-fn/index.d.ts +52 -0
  294. package/server/whatsapp-rpc/node_modules/mimic-fn/index.js +71 -0
  295. package/server/whatsapp-rpc/node_modules/mimic-fn/license +9 -0
  296. package/server/whatsapp-rpc/node_modules/mimic-fn/package.json +45 -0
  297. package/server/whatsapp-rpc/node_modules/mimic-fn/readme.md +90 -0
  298. package/server/whatsapp-rpc/node_modules/npm-run-path/index.d.ts +90 -0
  299. package/server/whatsapp-rpc/node_modules/npm-run-path/index.js +52 -0
  300. package/server/whatsapp-rpc/node_modules/npm-run-path/license +9 -0
  301. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/index.d.ts +31 -0
  302. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/index.js +12 -0
  303. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/license +9 -0
  304. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/package.json +41 -0
  305. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/readme.md +57 -0
  306. package/server/whatsapp-rpc/node_modules/npm-run-path/package.json +49 -0
  307. package/server/whatsapp-rpc/node_modules/npm-run-path/readme.md +104 -0
  308. package/server/whatsapp-rpc/node_modules/onetime/index.d.ts +59 -0
  309. package/server/whatsapp-rpc/node_modules/onetime/index.js +41 -0
  310. package/server/whatsapp-rpc/node_modules/onetime/license +9 -0
  311. package/server/whatsapp-rpc/node_modules/onetime/package.json +45 -0
  312. package/server/whatsapp-rpc/node_modules/onetime/readme.md +94 -0
  313. package/server/whatsapp-rpc/node_modules/path-key/index.d.ts +40 -0
  314. package/server/whatsapp-rpc/node_modules/path-key/index.js +16 -0
  315. package/server/whatsapp-rpc/node_modules/path-key/license +9 -0
  316. package/server/whatsapp-rpc/node_modules/path-key/package.json +39 -0
  317. package/server/whatsapp-rpc/node_modules/path-key/readme.md +61 -0
  318. package/server/whatsapp-rpc/node_modules/shebang-command/index.js +19 -0
  319. package/server/whatsapp-rpc/node_modules/shebang-command/license +9 -0
  320. package/server/whatsapp-rpc/node_modules/shebang-command/package.json +34 -0
  321. package/server/whatsapp-rpc/node_modules/shebang-command/readme.md +34 -0
  322. package/server/whatsapp-rpc/node_modules/shebang-regex/index.d.ts +22 -0
  323. package/server/whatsapp-rpc/node_modules/shebang-regex/index.js +2 -0
  324. package/server/whatsapp-rpc/node_modules/shebang-regex/license +9 -0
  325. package/server/whatsapp-rpc/node_modules/shebang-regex/package.json +35 -0
  326. package/server/whatsapp-rpc/node_modules/shebang-regex/readme.md +33 -0
  327. package/server/whatsapp-rpc/node_modules/shell-exec/LICENSE +21 -0
  328. package/server/whatsapp-rpc/node_modules/shell-exec/README.md +60 -0
  329. package/server/whatsapp-rpc/node_modules/shell-exec/index.js +47 -0
  330. package/server/whatsapp-rpc/node_modules/shell-exec/package.json +29 -0
  331. package/server/whatsapp-rpc/node_modules/signal-exit/LICENSE.txt +16 -0
  332. package/server/whatsapp-rpc/node_modules/signal-exit/README.md +74 -0
  333. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.d.ts +12 -0
  334. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.d.ts.map +1 -0
  335. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.js +10 -0
  336. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.js.map +1 -0
  337. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.d.ts +48 -0
  338. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.d.ts.map +1 -0
  339. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.js +279 -0
  340. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.js.map +1 -0
  341. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/package.json +3 -0
  342. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.d.ts +29 -0
  343. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.d.ts.map +1 -0
  344. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.js +42 -0
  345. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.js.map +1 -0
  346. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.d.ts +12 -0
  347. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.d.ts.map +1 -0
  348. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.js +4 -0
  349. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.js.map +1 -0
  350. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.d.ts +48 -0
  351. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.d.ts.map +1 -0
  352. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.js +275 -0
  353. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.js.map +1 -0
  354. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/package.json +3 -0
  355. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.d.ts +29 -0
  356. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.d.ts.map +1 -0
  357. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.js +39 -0
  358. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.js.map +1 -0
  359. package/server/whatsapp-rpc/node_modules/signal-exit/package.json +106 -0
  360. package/server/whatsapp-rpc/node_modules/strip-final-newline/index.js +14 -0
  361. package/server/whatsapp-rpc/node_modules/strip-final-newline/license +9 -0
  362. package/server/whatsapp-rpc/node_modules/strip-final-newline/package.json +43 -0
  363. package/server/whatsapp-rpc/node_modules/strip-final-newline/readme.md +35 -0
  364. package/server/whatsapp-rpc/node_modules/which/CHANGELOG.md +166 -0
  365. package/server/whatsapp-rpc/node_modules/which/LICENSE +15 -0
  366. package/server/whatsapp-rpc/node_modules/which/README.md +54 -0
  367. package/server/whatsapp-rpc/node_modules/which/bin/node-which +52 -0
  368. package/server/whatsapp-rpc/node_modules/which/package.json +43 -0
  369. package/server/whatsapp-rpc/node_modules/which/which.js +125 -0
  370. package/server/whatsapp-rpc/package-lock.json +272 -0
  371. package/server/whatsapp-rpc/package.json +30 -30
  372. package/server/whatsapp-rpc/schema.json +1294 -1294
  373. package/server/whatsapp-rpc/scripts/clean.cjs +66 -66
  374. package/server/whatsapp-rpc/scripts/cli.js +162 -162
  375. package/server/whatsapp-rpc/src/go/whatsapp/history.go +166 -166
  376. package/server/whatsapp-rpc/src/python/pyproject.toml +15 -15
  377. package/server/whatsapp-rpc/src/python/whatsapp_rpc/__init__.py +4 -4
  378. package/server/whatsapp-rpc/src/python/whatsapp_rpc/client.py +427 -427
  379. package/server/whatsapp-rpc/web/app.py +609 -609
  380. package/server/whatsapp-rpc/web/requirements.txt +6 -6
  381. package/server/whatsapp-rpc/web/rpc_client.py +427 -427
  382. package/server/whatsapp-rpc/web/static/openapi.yaml +59 -59
  383. package/server/whatsapp-rpc/web/templates/base.html +149 -149
  384. package/server/whatsapp-rpc/web/templates/contacts.html +240 -240
  385. package/server/whatsapp-rpc/web/templates/dashboard.html +319 -319
  386. package/server/whatsapp-rpc/web/templates/groups.html +328 -328
  387. package/server/whatsapp-rpc/web/templates/messages.html +465 -465
  388. package/server/whatsapp-rpc/web/templates/messaging.html +680 -680
  389. package/server/whatsapp-rpc/web/templates/send.html +258 -258
  390. package/server/whatsapp-rpc/web/templates/settings.html +459 -459
  391. package/client/src/components/ui/AndroidSettingsPanel.tsx +0 -401
  392. package/client/src/components/ui/WhatsAppSettingsPanel.tsx +0 -345
  393. package/client/src/nodeDefinitions/androidDeviceNodes.ts +0 -140
  394. package/docker-compose.prod.yml +0 -107
  395. package/docker-compose.yml +0 -104
  396. package/docs-MachinaOs/README.md +0 -85
  397. package/docs-MachinaOs/deployment/docker.mdx +0 -228
  398. package/docs-MachinaOs/deployment/production.mdx +0 -345
  399. package/docs-MachinaOs/docs.json +0 -75
  400. package/docs-MachinaOs/faq.mdx +0 -309
  401. package/docs-MachinaOs/favicon.svg +0 -5
  402. package/docs-MachinaOs/installation.mdx +0 -160
  403. package/docs-MachinaOs/introduction.mdx +0 -114
  404. package/docs-MachinaOs/logo/dark.svg +0 -6
  405. package/docs-MachinaOs/logo/light.svg +0 -6
  406. package/docs-MachinaOs/nodes/ai-agent.mdx +0 -216
  407. package/docs-MachinaOs/nodes/ai-models.mdx +0 -240
  408. package/docs-MachinaOs/nodes/android.mdx +0 -411
  409. package/docs-MachinaOs/nodes/overview.mdx +0 -181
  410. package/docs-MachinaOs/nodes/schedulers.mdx +0 -316
  411. package/docs-MachinaOs/nodes/webhooks.mdx +0 -330
  412. package/docs-MachinaOs/nodes/whatsapp.mdx +0 -305
  413. package/docs-MachinaOs/quickstart.mdx +0 -119
  414. package/docs-MachinaOs/tutorials/ai-agent-workflow.mdx +0 -177
  415. package/docs-MachinaOs/tutorials/android-automation.mdx +0 -242
  416. package/docs-MachinaOs/tutorials/first-workflow.mdx +0 -134
  417. package/docs-MachinaOs/tutorials/whatsapp-automation.mdx +0 -185
  418. package/nul +0 -0
  419. package/scripts/check-ports.ps1 +0 -33
  420. package/scripts/kill-port.ps1 +0 -154
@@ -1,1971 +1,2144 @@
1
- /**
2
- * WebSocket Context for real-time communication with Python backend.
3
- *
4
- * Provides WebSocket connection for:
5
- * - Request/response operations (parameters, execution, API keys)
6
- * - Real-time broadcasts (status updates, multi-client sync)
7
- * - Android device connection status
8
- * - Node execution status (scoped by workflow_id - n8n pattern)
9
- * - Variable/parameter updates
10
- * - Workflow state changes
11
- */
12
-
13
- import React, { createContext, useContext, useEffect, useState, useCallback, useRef, useMemo } from 'react';
14
- import { API_CONFIG } from '../config/api';
15
- import { useAppStore } from '../store/useAppStore';
16
- import { useAuth } from './AuthContext';
17
-
18
- // Generate unique request ID
19
- const generateRequestId = (): string => {
20
- return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
21
- };
22
-
23
- // Pending request tracking
24
- interface PendingRequest {
25
- resolve: (value: any) => void;
26
- reject: (reason: any) => void;
27
- timeout: NodeJS.Timeout | null; // null for no timeout (trigger nodes)
28
- }
29
-
30
- // Request timeout (30 seconds)
31
- const REQUEST_TIMEOUT = 30000;
32
-
33
- // Trigger node types that wait indefinitely for events
34
- const TRIGGER_NODE_TYPES = ['whatsappReceive', 'webhookTrigger', 'cronScheduler', 'chatTrigger'];
35
-
36
- // Status types
37
- export interface AndroidStatus {
38
- connected: boolean;
39
- paired: boolean;
40
- device_id: string | null;
41
- device_name: string | null;
42
- connected_devices: string[];
43
- connection_type: string | null;
44
- qr_data: string | null;
45
- session_token: string | null;
46
- }
47
-
48
- export interface NodeStatus {
49
- status: 'idle' | 'executing' | 'success' | 'error' | 'waiting';
50
- data?: Record<string, any>;
51
- output?: any;
52
- timestamp?: number;
53
- // Per-workflow scoping (n8n pattern)
54
- workflow_id?: string;
55
- // Waiting state data
56
- message?: string;
57
- waiter_id?: string;
58
- timeout?: number;
59
- }
60
-
61
- export interface WorkflowStatus {
62
- executing: boolean;
63
- current_node: string | null;
64
- progress?: number;
65
- }
66
-
67
- export interface DeploymentStatus {
68
- isRunning: boolean;
69
- activeRuns: number;
70
- status: 'idle' | 'starting' | 'running' | 'stopped' | 'cancelled' | 'error';
71
- workflow_id?: string | null; // Which workflow is deployed (for scoping)
72
- totalTime?: number;
73
- error?: string;
74
- }
75
-
76
- export interface WorkflowLock {
77
- locked: boolean;
78
- workflow_id: string | null;
79
- locked_at: number | null;
80
- reason: string | null;
81
- }
82
-
83
- export interface WhatsAppStatus {
84
- connected: boolean;
85
- has_session: boolean;
86
- running: boolean;
87
- pairing: boolean;
88
- device_id?: string;
89
- qr?: string;
90
- timestamp?: number;
91
- }
92
-
93
- export interface ApiKeyStatus {
94
- valid: boolean;
95
- hasKey?: boolean;
96
- message?: string;
97
- models?: string[];
98
- timestamp?: number;
99
- }
100
-
101
- // Console log entry from Console nodes
102
- export interface ConsoleLogEntry {
103
- node_id: string;
104
- label: string;
105
- timestamp: string;
106
- data: any;
107
- formatted: string;
108
- format: 'json' | 'json_compact' | 'text' | 'table';
109
- workflow_id?: string;
110
- // Source node info (the node whose output is being logged)
111
- source_node_id?: string;
112
- source_node_type?: string;
113
- source_node_label?: string;
114
- }
115
-
116
- // Terminal/server log entry
117
- export interface TerminalLogEntry {
118
- timestamp: string;
119
- level: 'debug' | 'info' | 'warning' | 'error';
120
- message: string;
121
- source?: string; // e.g., 'workflow', 'ai', 'android', 'whatsapp'
122
- details?: any;
123
- }
124
-
125
- // Chat message for chatTrigger nodes
126
- export interface ChatMessage {
127
- role: 'user' | 'assistant';
128
- message: string;
129
- timestamp: string;
130
- session_id?: string;
131
- }
132
-
133
- // WhatsApp received message structure (from Go service via whatsapp_message_received event)
134
- export interface WhatsAppMessage {
135
- message_id: string;
136
- sender: string;
137
- chat_id: string;
138
- type: 'text' | 'image' | 'video' | 'audio' | 'document' | 'location' | 'contact' | 'sticker';
139
- text?: string;
140
- timestamp: number;
141
- is_group: boolean;
142
- push_name?: string;
143
- media_url?: string;
144
- media_data?: string; // Base64 if includeMediaData is enabled
145
- caption?: string;
146
- // Location message fields
147
- latitude?: number;
148
- longitude?: number;
149
- // Contact message fields
150
- contact_name?: string;
151
- vcard?: string;
152
- }
153
-
154
- export interface NodeParameters {
155
- parameters: Record<string, any>;
156
- version: number;
157
- timestamp?: number;
158
- }
159
-
160
- export interface FullStatus {
161
- android: AndroidStatus;
162
- api_keys: Record<string, ApiKeyStatus>;
163
- nodes: Record<string, NodeStatus>;
164
- node_parameters: Record<string, NodeParameters>;
165
- variables: Record<string, any>;
166
- workflow: WorkflowStatus;
167
- }
168
-
169
- // Context value type
170
- interface WebSocketContextValue {
171
- // Connection state
172
- isConnected: boolean;
173
- reconnecting: boolean;
174
-
175
- // Status data
176
- androidStatus: AndroidStatus;
177
- whatsappStatus: WhatsAppStatus;
178
- whatsappMessages: WhatsAppMessage[]; // History of received messages
179
- lastWhatsAppMessage: WhatsAppMessage | null; // Most recent message
180
- apiKeyStatuses: Record<string, ApiKeyStatus>;
181
- consoleLogs: ConsoleLogEntry[]; // Console node output logs
182
- terminalLogs: TerminalLogEntry[]; // Server/terminal logs
183
- chatMessages: ChatMessage[]; // Chat messages for chatTrigger
184
- nodeStatuses: Record<string, NodeStatus>; // Current workflow's node statuses
185
- nodeParameters: Record<string, NodeParameters>;
186
- variables: Record<string, any>;
187
- workflowStatus: WorkflowStatus;
188
- deploymentStatus: DeploymentStatus;
189
- workflowLock: WorkflowLock;
190
-
191
- // Status getters
192
- getNodeStatus: (nodeId: string) => NodeStatus | undefined;
193
- getApiKeyStatus: (provider: string) => ApiKeyStatus | undefined;
194
- getVariable: (name: string) => any;
195
- requestStatus: () => void;
196
- clearNodeStatus: (nodeId: string) => Promise<void>;
197
- clearWhatsAppMessages: () => void;
198
- clearConsoleLogs: () => void;
199
- clearTerminalLogs: () => void;
200
- clearChatMessages: () => void;
201
- sendChatMessage: (message: string, nodeId?: string) => Promise<void>;
202
-
203
- // Generic request method
204
- sendRequest: <T = any>(type: string, data?: Record<string, any>) => Promise<T>;
205
-
206
- // Node Parameters
207
- getNodeParameters: (nodeId: string) => Promise<NodeParameters | null>;
208
- getAllNodeParameters: (nodeIds: string[]) => Promise<Record<string, NodeParameters>>;
209
- saveNodeParameters: (nodeId: string, parameters: Record<string, any>, version?: number) => Promise<boolean>;
210
- deleteNodeParameters: (nodeId: string) => Promise<boolean>;
211
-
212
- // Node Execution
213
- executeNode: (nodeId: string, nodeType: string, parameters: Record<string, any>, nodes?: any[], edges?: any[]) => Promise<any>;
214
- executeWorkflow: (nodes: any[], edges: any[], sessionId?: string) => Promise<any>;
215
- getNodeOutput: (nodeId: string, outputName?: string) => Promise<any>;
216
-
217
- // Trigger/Event Waiting
218
- cancelEventWait: (nodeId: string, waiterId?: string) => Promise<{ success: boolean; cancelled_count?: number }>;
219
-
220
- // Deployment Operations
221
- deployWorkflow: (workflowId: string, nodes: any[], edges: any[], sessionId?: string) => Promise<any>;
222
- cancelDeployment: (workflowId?: string) => Promise<any>;
223
- getDeploymentStatus: (workflowId?: string) => Promise<{ isRunning: boolean; activeRuns: number; settings?: any; workflow_id?: string }>;
224
-
225
- // AI Operations
226
- executeAiNode: (nodeId: string, nodeType: string, parameters: Record<string, any>, model: string) => Promise<any>;
227
- getAiModels: (provider: string, apiKey: string) => Promise<string[]>;
228
-
229
- // API Key Operations
230
- validateApiKey: (provider: string, apiKey: string) => Promise<{ valid: boolean; message?: string; models?: string[] }>;
231
- getStoredApiKey: (provider: string) => Promise<{ hasKey: boolean; apiKey?: string; models?: string[] }>;
232
- saveApiKey: (provider: string, apiKey: string, models?: string[]) => Promise<boolean>;
233
- deleteApiKey: (provider: string) => Promise<boolean>;
234
-
235
- // Android Operations
236
- getAndroidDevices: () => Promise<string[]>;
237
- executeAndroidAction: (serviceId: string, action: string, parameters: Record<string, any>, deviceId?: string) => Promise<any>;
238
- setupAndroidDevice: (connectionType: string, deviceId?: string, websocketUrl?: string) => Promise<any>;
239
-
240
- // Maps Operations
241
- validateMapsKey: (apiKey: string) => Promise<{ valid: boolean; message?: string }>;
242
-
243
- // WhatsApp Operations
244
- getWhatsAppStatus: () => Promise<{ connected: boolean; deviceId?: string; data?: any }>;
245
- getWhatsAppQR: () => Promise<{ connected: boolean; qr?: string; message?: string }>;
246
- sendWhatsAppMessage: (phone: string, message: string) => Promise<{ success: boolean; messageId?: string; error?: string }>;
247
- startWhatsAppConnection: () => Promise<{ success: boolean; message?: string }>;
248
- restartWhatsAppConnection: () => Promise<{ success: boolean; message?: string }>;
249
- getWhatsAppGroups: () => Promise<{ success: boolean; groups: Array<{ jid: string; name: string; topic?: string; size?: number; is_community?: boolean }>; error?: string }>;
250
- getWhatsAppGroupInfo: (groupId: string) => Promise<{ success: boolean; participants: Array<{ phone: string; name: string; jid: string; is_admin?: boolean }>; name?: string; error?: string }>;
251
- }
252
-
253
- // Default values
254
- const defaultAndroidStatus: AndroidStatus = {
255
- connected: false,
256
- paired: false,
257
- device_id: null,
258
- device_name: null,
259
- connected_devices: [],
260
- connection_type: null,
261
- qr_data: null,
262
- session_token: null
263
- };
264
-
265
- const defaultWorkflowStatus: WorkflowStatus = {
266
- executing: false,
267
- current_node: null
268
- };
269
-
270
- const defaultDeploymentStatus: DeploymentStatus = {
271
- isRunning: false,
272
- activeRuns: 0,
273
- status: 'idle'
274
- };
275
-
276
- const defaultWorkflowLock: WorkflowLock = {
277
- locked: false,
278
- workflow_id: null,
279
- locked_at: null,
280
- reason: null
281
- };
282
-
283
- const defaultWhatsAppStatus: WhatsAppStatus = {
284
- connected: false,
285
- has_session: false,
286
- running: false,
287
- pairing: false
288
- };
289
-
290
- const WebSocketContext = createContext<WebSocketContextValue | null>(null);
291
-
292
- // WebSocket URL (convert http to ws)
293
- const getWebSocketUrl = () => {
294
- const baseUrl = API_CONFIG.PYTHON_BASE_URL;
295
-
296
- // Production: empty base URL means use current origin
297
- if (!baseUrl) {
298
- const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
299
- return `${wsProtocol}://${window.location.host}/ws/status`;
300
- }
301
-
302
- // Development: convert http(s) to ws(s)
303
- const wsProtocol = baseUrl.startsWith('https') ? 'wss' : 'ws';
304
- const wsUrl = baseUrl.replace(/^https?/, wsProtocol);
305
- return `${wsUrl}/ws/status`;
306
- };
307
-
308
- // Max number of WhatsApp messages to keep in history
309
- const MAX_WHATSAPP_MESSAGE_HISTORY = 100;
310
-
311
- export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
312
- // Get authentication state - only connect WebSocket when authenticated
313
- const { isAuthenticated, isLoading: authLoading } = useAuth();
314
-
315
- // Get current workflow ID for filtering node status updates (n8n pattern)
316
- const currentWorkflow = useAppStore(state => state.currentWorkflow);
317
- const currentWorkflowId = currentWorkflow?.id;
318
-
319
- const [isConnected, setIsConnected] = useState(false);
320
- const [reconnecting, setReconnecting] = useState(false);
321
- const [androidStatus, setAndroidStatus] = useState<AndroidStatus>(defaultAndroidStatus);
322
- const [whatsappStatus, setWhatsappStatus] = useState<WhatsAppStatus>(defaultWhatsAppStatus);
323
- const [whatsappMessages, setWhatsappMessages] = useState<WhatsAppMessage[]>([]);
324
- const [lastWhatsAppMessage, setLastWhatsAppMessage] = useState<WhatsAppMessage | null>(null);
325
- const [apiKeyStatuses, setApiKeyStatuses] = useState<Record<string, ApiKeyStatus>>({});
326
- const [consoleLogs, setConsoleLogs] = useState<ConsoleLogEntry[]>([]);
327
- const [terminalLogs, setTerminalLogs] = useState<TerminalLogEntry[]>([]);
328
- const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
329
- // Per-workflow node statuses: workflow_id -> node_id -> NodeStatus (n8n pattern)
330
- const [allNodeStatuses, setAllNodeStatuses] = useState<Record<string, Record<string, NodeStatus>>>({});
331
- const [nodeParameters, setNodeParameters] = useState<Record<string, NodeParameters>>({});
332
- // Per-workflow variables: workflow_id -> variable_name -> value (n8n pattern)
333
- const [allVariables, setAllVariables] = useState<Record<string, Record<string, any>>>({});
334
- const [workflowStatus, setWorkflowStatus] = useState<WorkflowStatus>(defaultWorkflowStatus);
335
- const [deploymentStatus, setDeploymentStatus] = useState<DeploymentStatus>(defaultDeploymentStatus);
336
- const [workflowLock, setWorkflowLock] = useState<WorkflowLock>(defaultWorkflowLock);
337
-
338
- const wsRef = useRef<WebSocket | null>(null);
339
- const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
340
- const pingIntervalRef = useRef<NodeJS.Timeout | null>(null);
341
- const pendingRequestsRef = useRef<Map<string, PendingRequest>>(new Map());
342
- // Ref for current workflow ID - allows message handler to access latest value
343
- // without recreating the WebSocket connection (n8n pattern)
344
- const currentWorkflowIdRef = useRef<string | undefined>(currentWorkflowId);
345
-
346
- // Keep the ref in sync with the state and clear node statuses on workflow switch (n8n pattern)
347
- useEffect(() => {
348
- const previousWorkflowId = currentWorkflowIdRef.current;
349
- currentWorkflowIdRef.current = currentWorkflowId;
350
-
351
- // No need to clear node statuses - they are now stored per-workflow (n8n pattern)
352
- // Each workflow's statuses are isolated in allNodeStatuses[workflow_id]
353
- if (previousWorkflowId && currentWorkflowId && previousWorkflowId !== currentWorkflowId) {
354
-
355
- // Fetch deployment status for the new workflow (n8n pattern)
356
- // This ensures the deploy button shows correct state when switching workflows
357
- if (wsRef.current?.readyState === WebSocket.OPEN) {
358
- const fetchDeploymentStatus = async () => {
359
- try {
360
- const requestId = generateRequestId();
361
- const response = await new Promise<any>((resolve, reject) => {
362
- const timeout = setTimeout(() => reject(new Error('Timeout')), 5000);
363
-
364
- const handler = (event: MessageEvent) => {
365
- try {
366
- const msg = JSON.parse(event.data);
367
- if (msg.request_id === requestId) {
368
- clearTimeout(timeout);
369
- wsRef.current?.removeEventListener('message', handler);
370
- resolve(msg);
371
- }
372
- } catch {}
373
- };
374
-
375
- wsRef.current?.addEventListener('message', handler);
376
- wsRef.current?.send(JSON.stringify({
377
- type: 'get_deployment_status',
378
- request_id: requestId,
379
- workflow_id: currentWorkflowId
380
- }));
381
- });
382
-
383
- // Update deployment status based on response
384
- const isRunning = response.is_running || false;
385
- setDeploymentStatus({
386
- isRunning,
387
- activeRuns: response.active_runs || 0,
388
- status: isRunning ? 'running' : 'idle',
389
- workflow_id: response.workflow_id || null
390
- });
391
-
392
- // Sync with Zustand store's per-workflow isExecuting state (n8n pattern)
393
- // This ensures Dashboard's isExecuting reflects the actual backend state
394
- const { setWorkflowExecuting } = useAppStore.getState();
395
- setWorkflowExecuting(currentWorkflowId, isRunning);
396
-
397
- // Also update workflow lock based on deployment status (n8n pattern)
398
- // A running workflow should be locked
399
- setWorkflowLock({
400
- locked: isRunning,
401
- workflow_id: isRunning ? currentWorkflowId : null,
402
- locked_at: isRunning ? Date.now() : null,
403
- reason: isRunning ? 'Workflow is running' : null
404
- });
405
- } catch (err) {
406
- console.error('[WebSocket] Failed to fetch deployment status:', err);
407
- }
408
- };
409
- fetchDeploymentStatus();
410
- }
411
- }
412
- }, [currentWorkflowId]);
413
-
414
- // Handle incoming messages
415
- const handleMessage = useCallback((event: MessageEvent) => {
416
- try {
417
- const message = JSON.parse(event.data);
418
- const { type, data, node_id, name, value, output, variables: varsUpdate, request_id } = message;
419
-
420
- // Handle request/response pattern - resolve pending requests
421
- if (request_id && pendingRequestsRef.current.has(request_id)) {
422
- const pending = pendingRequestsRef.current.get(request_id)!;
423
- if (pending.timeout) {
424
- clearTimeout(pending.timeout);
425
- }
426
- pendingRequestsRef.current.delete(request_id);
427
- pending.resolve(message);
428
- return; // Response handled, don't process as broadcast
429
- }
430
-
431
- switch (type) {
432
- case 'initial_status':
433
- case 'full_status':
434
- if (data) {
435
- if (data.android) setAndroidStatus(data.android);
436
- if (data.whatsapp) setWhatsappStatus(data.whatsapp);
437
- if (data.api_keys) setApiKeyStatuses(data.api_keys);
438
- // Node statuses from initial_status - group by workflow_id (n8n pattern)
439
- if (data.nodes) {
440
- const groupedStatuses: Record<string, Record<string, NodeStatus>> = {};
441
- for (const [nodeId, status] of Object.entries(data.nodes)) {
442
- const nodeStatus = status as NodeStatus;
443
- const wfId = nodeStatus?.workflow_id || 'unknown';
444
- if (!groupedStatuses[wfId]) groupedStatuses[wfId] = {};
445
- groupedStatuses[wfId][nodeId] = nodeStatus;
446
- }
447
- setAllNodeStatuses(prev => ({ ...prev, ...groupedStatuses }));
448
- }
449
- if (data.node_parameters) setNodeParameters(data.node_parameters);
450
- // Variables from initial_status - group by workflow_id (n8n pattern)
451
- if (data.variables) {
452
- // Variables may come with workflow_id or need grouping
453
- const groupedVars: Record<string, Record<string, any>> = {};
454
- for (const [varName, varData] of Object.entries(data.variables)) {
455
- const wfId = (varData as any)?.workflow_id || 'unknown';
456
- if (!groupedVars[wfId]) groupedVars[wfId] = {};
457
- groupedVars[wfId][varName] = varData;
458
- }
459
- setAllVariables(prev => ({ ...prev, ...groupedVars }));
460
- }
461
- if (data.workflow) setWorkflowStatus(data.workflow);
462
- if (data.workflow_lock) setWorkflowLock(data.workflow_lock);
463
- // Handle deployment status from initial_status (n8n/Conductor pattern)
464
- if (data.deployment) {
465
- setDeploymentStatus({
466
- isRunning: data.deployment.isRunning || false,
467
- activeRuns: data.deployment.activeRuns || 0,
468
- status: data.deployment.status || 'idle'
469
- });
470
- }
471
- }
472
- break;
473
-
474
- case 'api_key_status':
475
- if (message.provider) {
476
- setApiKeyStatuses(prev => ({
477
- ...prev,
478
- [message.provider]: data
479
- }));
480
- }
481
- break;
482
-
483
- case 'android_status':
484
- setAndroidStatus(data || defaultAndroidStatus);
485
- break;
486
-
487
- case 'whatsapp_status':
488
- setWhatsappStatus(data || defaultWhatsAppStatus);
489
- break;
490
-
491
- case 'whatsapp_message_received':
492
- // Handle incoming WhatsApp message from Go service
493
- if (data) {
494
- const message: WhatsAppMessage = {
495
- message_id: data.message_id || data.id || '',
496
- sender: data.sender || data.from || '',
497
- chat_id: data.chat_id || data.chat || '',
498
- type: data.type || 'text',
499
- text: data.text || data.message || data.body || '',
500
- timestamp: data.timestamp || Date.now(),
501
- is_group: data.is_group || data.isGroup || false,
502
- push_name: data.push_name || data.pushName || data.name,
503
- media_url: data.media_url || data.mediaUrl,
504
- media_data: data.media_data || data.mediaData,
505
- caption: data.caption,
506
- latitude: data.latitude,
507
- longitude: data.longitude,
508
- contact_name: data.contact_name || data.contactName,
509
- vcard: data.vcard
510
- };
511
-
512
- // Update last message
513
- setLastWhatsAppMessage(message);
514
-
515
- // Add to message history (newest first, limit size)
516
- setWhatsappMessages(prev => {
517
- const updated = [message, ...prev];
518
- return updated.slice(0, MAX_WHATSAPP_MESSAGE_HISTORY);
519
- });
520
-
521
- }
522
- break;
523
-
524
- case 'node_status':
525
- // Per-workflow node status storage (n8n pattern)
526
- // Store status under workflow_id -> node_id structure
527
- if (node_id) {
528
- const statusWorkflowId = message.workflow_id || 'unknown';
529
- // Phase and tool_name are inside data.data (nested structure from broadcaster)
530
- const innerData = data?.data || {};
531
-
532
- // Flatten the structure: merge inner data with outer data for easier access
533
- const flattenedData = { ...data, ...innerData, workflow_id: statusWorkflowId };
534
-
535
- setAllNodeStatuses((prev: Record<string, Record<string, NodeStatus>>) => ({
536
- ...prev,
537
- [statusWorkflowId]: {
538
- ...(prev[statusWorkflowId] || {}),
539
- [node_id]: flattenedData
540
- }
541
- }));
542
- }
543
- break;
544
-
545
- case 'node_output':
546
- // Per-workflow node output storage (n8n pattern)
547
- if (node_id) {
548
- const outputWorkflowId = message.workflow_id || 'unknown';
549
- setAllNodeStatuses((prev: Record<string, Record<string, NodeStatus>>) => ({
550
- ...prev,
551
- [outputWorkflowId]: {
552
- ...(prev[outputWorkflowId] || {}),
553
- [node_id]: {
554
- ...(prev[outputWorkflowId]?.[node_id] || {}),
555
- output,
556
- workflow_id: outputWorkflowId
557
- }
558
- }
559
- }));
560
- }
561
- break;
562
-
563
- case 'node_status_cleared':
564
- // Handle broadcast from server when node status is cleared
565
- if (node_id || message.node_id) {
566
- const clearedNodeId = node_id || message.node_id;
567
- const clearWorkflowId = message.workflow_id;
568
- setAllNodeStatuses((prev: Record<string, Record<string, NodeStatus>>) => {
569
- // If workflow_id specified, only clear from that workflow
570
- if (clearWorkflowId && prev[clearWorkflowId]) {
571
- const workflowStatuses = { ...prev[clearWorkflowId] };
572
- delete workflowStatuses[clearedNodeId];
573
- return { ...prev, [clearWorkflowId]: workflowStatuses };
574
- }
575
- // Otherwise clear from all workflows
576
- const newStatuses: Record<string, Record<string, NodeStatus>> = {};
577
- for (const [wfId, nodes] of Object.entries(prev)) {
578
- const filteredNodes = { ...nodes };
579
- delete filteredNodes[clearedNodeId];
580
- newStatuses[wfId] = filteredNodes;
581
- }
582
- return newStatuses;
583
- });
584
- }
585
- break;
586
-
587
- // Node parameters broadcasts (from other clients)
588
- case 'node_parameters_updated':
589
- if (node_id) {
590
- setNodeParameters(prev => ({
591
- ...prev,
592
- [node_id]: {
593
- parameters: message.parameters,
594
- version: message.version,
595
- timestamp: message.timestamp
596
- }
597
- }));
598
- }
599
- break;
600
-
601
- case 'node_parameters_deleted':
602
- if (node_id) {
603
- setNodeParameters(prev => {
604
- const updated = { ...prev };
605
- delete updated[node_id];
606
- return updated;
607
- });
608
- }
609
- break;
610
-
611
- case 'variable_update':
612
- // Per-workflow variable storage (n8n pattern)
613
- if (name !== undefined) {
614
- const varWorkflowId = message.workflow_id || 'unknown';
615
- setAllVariables((prev: Record<string, Record<string, any>>) => ({
616
- ...prev,
617
- [varWorkflowId]: {
618
- ...(prev[varWorkflowId] || {}),
619
- [name]: value
620
- }
621
- }));
622
- }
623
- break;
624
-
625
- case 'variables_update':
626
- // Per-workflow batch variable update (n8n pattern)
627
- if (varsUpdate) {
628
- const batchWorkflowId = message.workflow_id || 'unknown';
629
- setAllVariables((prev: Record<string, Record<string, any>>) => ({
630
- ...prev,
631
- [batchWorkflowId]: {
632
- ...(prev[batchWorkflowId] || {}),
633
- ...varsUpdate
634
- }
635
- }));
636
- }
637
- break;
638
-
639
- case 'workflow_status':
640
- setWorkflowStatus(data || defaultWorkflowStatus);
641
- break;
642
-
643
- case 'deployment_status':
644
- // Handle deployment status updates (event-driven, no iterations)
645
- // Per-workflow scoping (n8n pattern): Only apply updates for current workflow
646
- if (message.status) {
647
- const deploymentWorkflowId = message.workflow_id;
648
- const activeWorkflowId = currentWorkflowIdRef.current;
649
-
650
- // Apply deployment update if:
651
- // 1. It's for the current workflow, OR
652
- // 2. It's a stop/cancel/error (affects any workflow that was running), OR
653
- // 3. No specific workflow context (backward compatibility)
654
- const isTerminalStatus = ['stopped', 'cancelled', 'error'].includes(message.status);
655
- const shouldApplyDeployment = !deploymentWorkflowId ||
656
- deploymentWorkflowId === activeWorkflowId ||
657
- isTerminalStatus;
658
-
659
- if (shouldApplyDeployment) {
660
- setDeploymentStatus(prev => {
661
- const newStatus: DeploymentStatus = { ...prev };
662
- // Capture workflow_id from message
663
- if (message.workflow_id) {
664
- newStatus.workflow_id = message.workflow_id;
665
- }
666
-
667
- switch (message.status) {
668
- case 'starting':
669
- newStatus.isRunning = true;
670
- newStatus.status = 'starting';
671
- newStatus.activeRuns = 0;
672
- break;
673
- case 'running':
674
- case 'started':
675
- newStatus.isRunning = true;
676
- newStatus.status = 'running';
677
- newStatus.activeRuns = message.data?.active_runs ?? prev.activeRuns;
678
- break;
679
- case 'run_started':
680
- newStatus.isRunning = true;
681
- newStatus.status = 'running';
682
- newStatus.activeRuns = message.data?.active_runs || prev.activeRuns + 1;
683
- break;
684
- case 'run_complete':
685
- newStatus.activeRuns = Math.max(0, message.data?.active_runs || prev.activeRuns - 1);
686
- break;
687
- case 'stopped':
688
- // Only clear if this was our workflow or no workflow was tracked
689
- if (!prev.workflow_id || prev.workflow_id === deploymentWorkflowId) {
690
- newStatus.isRunning = false;
691
- newStatus.status = 'stopped';
692
- newStatus.totalTime = message.data?.total_time;
693
- newStatus.activeRuns = 0;
694
- newStatus.workflow_id = null;
695
- }
696
- break;
697
- case 'cancelled':
698
- // Only clear if this was our workflow or no workflow was tracked
699
- if (!prev.workflow_id || prev.workflow_id === deploymentWorkflowId) {
700
- newStatus.isRunning = false;
701
- newStatus.status = 'cancelled';
702
- newStatus.activeRuns = 0;
703
- newStatus.workflow_id = null;
704
- }
705
- break;
706
- case 'error':
707
- // Only clear if this was our workflow or no workflow was tracked
708
- if (!prev.workflow_id || prev.workflow_id === deploymentWorkflowId) {
709
- newStatus.isRunning = false;
710
- newStatus.status = 'error';
711
- newStatus.error = message.error;
712
- newStatus.workflow_id = null;
713
- }
714
- break;
715
- }
716
-
717
- return newStatus;
718
- });
719
- // Sync with Zustand store's per-workflow isExecuting state (n8n pattern)
720
- if (deploymentWorkflowId) {
721
- const { setWorkflowExecuting } = useAppStore.getState();
722
- const isRunning = ['starting', 'running', 'started', 'run_started'].includes(message.status);
723
- const isStopped = ['stopped', 'cancelled', 'error'].includes(message.status);
724
- if (isRunning || isStopped) {
725
- setWorkflowExecuting(deploymentWorkflowId, isRunning);
726
- }
727
- }
728
- }
729
- }
730
- break;
731
-
732
- case 'pong':
733
- // Keep-alive response, no action needed
734
- break;
735
-
736
- case 'console_log':
737
- // Handle console log entries from Console nodes
738
- if (data) {
739
- const logEntry: ConsoleLogEntry = {
740
- node_id: data.node_id || '',
741
- label: data.label || 'Console',
742
- timestamp: data.timestamp || new Date().toISOString(),
743
- data: data.data,
744
- formatted: data.formatted || JSON.stringify(data.data, null, 2),
745
- format: data.format || 'json',
746
- workflow_id: data.workflow_id,
747
- source_node_id: data.source_node_id,
748
- source_node_type: data.source_node_type,
749
- source_node_label: data.source_node_label
750
- };
751
- // Add to logs (newest first, limit to 100 entries)
752
- setConsoleLogs(prev => {
753
- const updated = [logEntry, ...prev];
754
- return updated.slice(0, 100);
755
- });
756
- }
757
- break;
758
-
759
- case 'console_logs_cleared':
760
- // Handle console logs cleared from server
761
- if (message.workflow_id) {
762
- setConsoleLogs(prev => prev.filter(log => log.workflow_id !== message.workflow_id));
763
- } else {
764
- setConsoleLogs([]);
765
- }
766
- break;
767
-
768
- case 'terminal_log':
769
- // Handle terminal/server log entries
770
- if (data) {
771
- const terminalEntry: TerminalLogEntry = {
772
- timestamp: data.timestamp || new Date().toISOString(),
773
- level: data.level || 'info',
774
- message: data.message || '',
775
- source: data.source,
776
- details: data.details
777
- };
778
- // Add to logs (newest first, limit to 200 entries)
779
- setTerminalLogs(prev => {
780
- const updated = [terminalEntry, ...prev];
781
- return updated.slice(0, 200);
782
- });
783
- }
784
- break;
785
-
786
- case 'terminal_logs_cleared':
787
- // Handle terminal logs cleared from server
788
- setTerminalLogs([]);
789
- break;
790
-
791
- case 'workflow_lock':
792
- // Handle workflow lock status updates (per-workflow locking - n8n pattern)
793
- // Only update lock state if it's for the current workflow or if unlocking
794
- if (data) {
795
- const lockWorkflowId = message.workflow_id || data.workflow_id;
796
- const activeWorkflowId = currentWorkflowIdRef.current;
797
-
798
- // Apply lock update if:
799
- // 1. It's for the current workflow, OR
800
- // 2. We're unlocking (locked=false), OR
801
- // 3. No specific workflow context (backward compatibility)
802
- const shouldApplyLock = !lockWorkflowId ||
803
- lockWorkflowId === activeWorkflowId ||
804
- !data.locked;
805
-
806
- if (shouldApplyLock) {
807
- setWorkflowLock({
808
- locked: data.locked || false,
809
- workflow_id: data.workflow_id || null,
810
- locked_at: data.locked_at || null,
811
- reason: data.reason || null
812
- });
813
- }
814
- }
815
- break;
816
-
817
- case 'error':
818
- console.error('[WebSocket] Server error:', message.code, message.message);
819
- break;
820
-
821
- default:
822
- break;
823
- }
824
- } catch (error) {
825
- console.error('[WebSocket] Failed to parse message:', error);
826
- }
827
- }, []); // Empty deps - uses ref for currentWorkflowId to avoid reconnecting WebSocket
828
-
829
- // Connect to WebSocket
830
- const connect = useCallback(() => {
831
- if (wsRef.current?.readyState === WebSocket.OPEN) {
832
- return;
833
- }
834
-
835
- const wsUrl = getWebSocketUrl();
836
-
837
- try {
838
- const ws = new WebSocket(wsUrl);
839
-
840
- ws.onopen = async () => {
841
- setIsConnected(true);
842
- setReconnecting(false);
843
-
844
- // Start ping interval
845
- pingIntervalRef.current = setInterval(() => {
846
- if (ws.readyState === WebSocket.OPEN) {
847
- ws.send(JSON.stringify({ type: 'ping' }));
848
- }
849
- }, 30000);
850
-
851
- // Load initial API key statuses for known providers
852
- const providers = ['openai', 'anthropic', 'gemini', 'google_maps', 'android_remote'];
853
- for (const provider of providers) {
854
- try {
855
- const response = await new Promise<any>((resolve, reject) => {
856
- const requestId = `init_${provider}_${Date.now()}`;
857
- const timeout = setTimeout(() => reject(new Error('Timeout')), 5000);
858
-
859
- const handler = (event: MessageEvent) => {
860
- try {
861
- const msg = JSON.parse(event.data);
862
- if (msg.request_id === requestId) {
863
- clearTimeout(timeout);
864
- ws.removeEventListener('message', handler);
865
- resolve(msg);
866
- }
867
- } catch {}
868
- };
869
-
870
- ws.addEventListener('message', handler);
871
- ws.send(JSON.stringify({ type: 'get_stored_api_key', provider, request_id: requestId }));
872
- });
873
-
874
- if (response.has_key) {
875
- setApiKeyStatuses(prev => ({
876
- ...prev,
877
- [provider]: { hasKey: true, valid: true }
878
- }));
879
- }
880
- } catch {
881
- // Ignore errors during initial check
882
- }
883
- }
884
-
885
- // Load terminal log history
886
- try {
887
- const terminalResponse = await new Promise<any>((resolve, reject) => {
888
- const requestId = `terminal_logs_${Date.now()}`;
889
- const timeout = setTimeout(() => reject(new Error('Timeout')), 5000);
890
-
891
- const handler = (event: MessageEvent) => {
892
- try {
893
- const msg = JSON.parse(event.data);
894
- if (msg.request_id === requestId) {
895
- clearTimeout(timeout);
896
- ws.removeEventListener('message', handler);
897
- resolve(msg);
898
- }
899
- } catch {}
900
- };
901
-
902
- ws.addEventListener('message', handler);
903
- ws.send(JSON.stringify({ type: 'get_terminal_logs', request_id: requestId }));
904
- });
905
-
906
- if (terminalResponse.success && terminalResponse.logs) {
907
- // Map server logs to TerminalLogEntry format (newest first)
908
- const logs: TerminalLogEntry[] = terminalResponse.logs.map((log: any) => ({
909
- timestamp: log.timestamp || new Date().toISOString(),
910
- level: log.level || 'info',
911
- message: log.message || '',
912
- source: log.source,
913
- details: log.details
914
- })).reverse(); // Server stores oldest first, we want newest first
915
- setTerminalLogs(logs);
916
- }
917
- } catch {
918
- // Ignore errors loading terminal logs
919
- }
920
-
921
- // Load chat message history from database
922
- try {
923
- const chatResponse = await new Promise<any>((resolve, reject) => {
924
- const requestId = `chat_messages_${Date.now()}`;
925
- const timeout = setTimeout(() => reject(new Error('Timeout')), 5000);
926
-
927
- const handler = (event: MessageEvent) => {
928
- try {
929
- const msg = JSON.parse(event.data);
930
- if (msg.request_id === requestId) {
931
- clearTimeout(timeout);
932
- ws.removeEventListener('message', handler);
933
- resolve(msg);
934
- }
935
- } catch {}
936
- };
937
-
938
- ws.addEventListener('message', handler);
939
- ws.send(JSON.stringify({ type: 'get_chat_messages', session_id: 'default', request_id: requestId }));
940
- });
941
-
942
- if (chatResponse.success && chatResponse.messages) {
943
- const messages: ChatMessage[] = chatResponse.messages.map((msg: any) => ({
944
- role: msg.role as 'user' | 'assistant',
945
- message: msg.message,
946
- timestamp: msg.timestamp
947
- }));
948
- setChatMessages(messages);
949
- }
950
- } catch {
951
- // Ignore errors loading chat messages
952
- }
953
- };
954
-
955
- ws.onmessage = handleMessage;
956
-
957
- ws.onclose = (event) => {
958
- console.log('[WebSocket] Disconnected:', event.code, event.reason);
959
- setIsConnected(false);
960
- wsRef.current = null;
961
-
962
- // Clear ping interval
963
- if (pingIntervalRef.current) {
964
- clearInterval(pingIntervalRef.current);
965
- pingIntervalRef.current = null;
966
- }
967
-
968
- // Reconnect after delay (unless intentional close)
969
- if (event.code !== 1000) {
970
- setReconnecting(true);
971
- reconnectTimeoutRef.current = setTimeout(() => {
972
- connect();
973
- }, 3000);
974
- }
975
- };
976
-
977
- ws.onerror = (error) => {
978
- console.error('[WebSocket] Error:', error);
979
- };
980
-
981
- wsRef.current = ws;
982
- } catch (error) {
983
- console.error('[WebSocket] Failed to create connection:', error);
984
- setReconnecting(true);
985
- reconnectTimeoutRef.current = setTimeout(connect, 3000);
986
- }
987
- }, [handleMessage]);
988
-
989
- // Request current status
990
- const requestStatus = useCallback(() => {
991
- if (wsRef.current?.readyState === WebSocket.OPEN) {
992
- wsRef.current.send(JSON.stringify({ type: 'get_status' }));
993
- }
994
- }, []);
995
-
996
- // Get node status for current workflow (n8n pattern)
997
- // IMPORTANT: Use currentWorkflowId state directly (not ref) to ensure reactivity on workflow switch
998
- const getNodeStatus = useCallback((nodeId: string) => {
999
- if (!currentWorkflowId) {
1000
- return undefined;
1001
- }
1002
- return allNodeStatuses[currentWorkflowId]?.[nodeId];
1003
- }, [allNodeStatuses, currentWorkflowId]);
1004
-
1005
- // Get API key status
1006
- const getApiKeyStatus = useCallback((provider: string) => {
1007
- return apiKeyStatuses[provider];
1008
- }, [apiKeyStatuses]);
1009
-
1010
- // Get variable value for current workflow (n8n pattern)
1011
- // IMPORTANT: Use currentWorkflowId state directly (not ref) to ensure reactivity on workflow switch
1012
- const getVariable = useCallback((name: string) => {
1013
- if (!currentWorkflowId) return undefined;
1014
- return allVariables[currentWorkflowId]?.[name];
1015
- }, [allVariables, currentWorkflowId]);
1016
-
1017
- // Clear node status (used when clearing execution results)
1018
- // Also clears the backend node_outputs storage
1019
- const clearNodeStatus = useCallback(async (nodeId: string) => {
1020
- const workflowId = currentWorkflowIdRef.current;
1021
- // Clear local state for current workflow
1022
- setAllNodeStatuses((prev: Record<string, Record<string, NodeStatus>>) => {
1023
- if (!workflowId || !prev[workflowId]) return prev;
1024
- const workflowStatuses = { ...prev[workflowId] };
1025
- delete workflowStatuses[nodeId];
1026
- return { ...prev, [workflowId]: workflowStatuses };
1027
- });
1028
- // Clear backend storage
1029
- try {
1030
- if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
1031
- wsRef.current.send(JSON.stringify({
1032
- type: 'clear_node_output',
1033
- node_id: nodeId,
1034
- workflow_id: workflowId
1035
- }));
1036
- }
1037
- } catch (err) {
1038
- console.error('[WebSocket] Failed to clear backend node output:', err);
1039
- }
1040
- }, []);
1041
-
1042
- // Clear WhatsApp message history
1043
- const clearWhatsAppMessages = useCallback(() => {
1044
- setWhatsappMessages([]);
1045
- setLastWhatsAppMessage(null);
1046
- }, []);
1047
-
1048
- // Clear console logs
1049
- const clearConsoleLogs = useCallback(() => {
1050
- setConsoleLogs([]);
1051
- }, []);
1052
-
1053
- // Clear terminal logs (also clears on server)
1054
- const clearTerminalLogs = useCallback(() => {
1055
- setTerminalLogs([]);
1056
- // Also notify server to clear its terminal log history
1057
- if (wsRef.current?.readyState === WebSocket.OPEN) {
1058
- wsRef.current.send(JSON.stringify({ type: 'clear_terminal_logs' }));
1059
- }
1060
- }, []);
1061
-
1062
- // Clear chat messages (both local state and database)
1063
- // Uses direct WebSocket send to avoid dependency on sendRequest (which is defined later)
1064
- const clearChatMessages = useCallback(() => {
1065
- setChatMessages([]);
1066
- // Also clear from database via direct WebSocket send
1067
- if (wsRef.current?.readyState === WebSocket.OPEN) {
1068
- wsRef.current.send(JSON.stringify({ type: 'clear_chat_messages', session_id: 'default' }));
1069
- }
1070
- }, []);
1071
-
1072
- // Derive current workflow's node statuses (n8n pattern)
1073
- // This provides a flat Record<nodeId, NodeStatus> for the current workflow
1074
- // IMPORTANT: Use currentWorkflowId state directly, not ref, to ensure re-render on workflow switch
1075
- const nodeStatuses = useMemo(() => {
1076
- if (!currentWorkflowId) return {};
1077
- return allNodeStatuses[currentWorkflowId] || {};
1078
- }, [allNodeStatuses, currentWorkflowId]);
1079
-
1080
- // Derive current workflow's variables (n8n pattern)
1081
- // This provides a flat Record<varName, value> for the current workflow
1082
- // IMPORTANT: Use currentWorkflowId state directly, not ref, to ensure re-render on workflow switch
1083
- const variables = useMemo(() => {
1084
- if (!currentWorkflowId) return {};
1085
- return allVariables[currentWorkflowId] || {};
1086
- }, [allVariables, currentWorkflowId]);
1087
-
1088
- // =========================================================================
1089
- // Core Request/Response Pattern
1090
- // =========================================================================
1091
-
1092
- // Send a request and wait for response
1093
- // timeoutMs: undefined/0 = use default, negative = no timeout (for trigger nodes)
1094
- const sendRequest = useCallback(async <T = any>(
1095
- type: string,
1096
- data?: Record<string, any>,
1097
- timeoutMs?: number
1098
- ): Promise<T> => {
1099
- return new Promise((resolve, reject) => {
1100
- if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
1101
- reject(new Error('WebSocket not connected'));
1102
- return;
1103
- }
1104
-
1105
- const requestId = generateRequestId();
1106
- const useTimeout = timeoutMs === undefined || timeoutMs >= 0;
1107
- const actualTimeout = timeoutMs && timeoutMs > 0 ? timeoutMs : REQUEST_TIMEOUT;
1108
-
1109
- let timeout: NodeJS.Timeout | null = null;
1110
- if (useTimeout && timeoutMs !== -1) {
1111
- timeout = setTimeout(() => {
1112
- pendingRequestsRef.current.delete(requestId);
1113
- reject(new Error(`Request timeout: ${type}`));
1114
- }, actualTimeout);
1115
- }
1116
-
1117
- pendingRequestsRef.current.set(requestId, { resolve, reject, timeout });
1118
-
1119
- wsRef.current.send(JSON.stringify({
1120
- type,
1121
- request_id: requestId,
1122
- ...data
1123
- }));
1124
- });
1125
- }, []);
1126
-
1127
- // =========================================================================
1128
- // Chat Message Operations
1129
- // =========================================================================
1130
-
1131
- // Send chat message (triggers chatTrigger nodes and saves to database)
1132
- // nodeId: optional specific chatTrigger node to target
1133
- const sendChatMessageAsync = useCallback(async (message: string, nodeId?: string): Promise<void> => {
1134
- const timestamp = new Date().toISOString();
1135
- const chatMessage: ChatMessage = {
1136
- role: 'user',
1137
- message,
1138
- timestamp
1139
- };
1140
-
1141
- // Add to local messages immediately for UI feedback
1142
- setChatMessages(prev => [...prev, chatMessage]);
1143
-
1144
- // Send to backend to dispatch to chatTrigger nodes (also saves to database)
1145
- try {
1146
- await sendRequest('send_chat_message', {
1147
- message,
1148
- role: 'user',
1149
- node_id: nodeId, // Target specific chatTrigger node if specified
1150
- session_id: 'default',
1151
- timestamp
1152
- });
1153
- } catch (error) {
1154
- console.error('[WebSocket] Failed to send chat message:', error);
1155
- throw error;
1156
- }
1157
- }, [sendRequest]);
1158
-
1159
- // =========================================================================
1160
- // Node Parameters Operations
1161
- // =========================================================================
1162
-
1163
- const getNodeParametersAsync = useCallback(async (nodeId: string): Promise<NodeParameters | null> => {
1164
- try {
1165
- const response = await sendRequest<any>('get_node_parameters', { node_id: nodeId });
1166
- if (response.parameters) {
1167
- const params: NodeParameters = {
1168
- parameters: response.parameters,
1169
- version: response.version || 0,
1170
- timestamp: response.timestamp
1171
- };
1172
- // Update local cache
1173
- setNodeParameters(prev => ({ ...prev, [nodeId]: params }));
1174
- return params;
1175
- }
1176
- return null;
1177
- } catch (error) {
1178
- console.error('[WebSocket] Failed to get node parameters:', error);
1179
- return null;
1180
- }
1181
- }, [sendRequest]);
1182
-
1183
- const getAllNodeParametersAsync = useCallback(async (nodeIds: string[]): Promise<Record<string, NodeParameters>> => {
1184
- if (!nodeIds.length) return {};
1185
- try {
1186
- const response = await sendRequest<any>('get_all_node_parameters', { node_ids: nodeIds });
1187
- const result: Record<string, NodeParameters> = {};
1188
-
1189
- if (response.parameters) {
1190
- for (const [nodeId, data] of Object.entries(response.parameters as Record<string, any>)) {
1191
- result[nodeId] = {
1192
- parameters: data.parameters || {},
1193
- version: data.version || 0,
1194
- timestamp: response.timestamp
1195
- };
1196
- }
1197
- // Update local cache with all parameters
1198
- setNodeParameters(prev => ({ ...prev, ...result }));
1199
- }
1200
- return result;
1201
- } catch (error) {
1202
- console.error('[WebSocket] Failed to get all node parameters:', error);
1203
- return {};
1204
- }
1205
- }, [sendRequest]);
1206
-
1207
- const saveNodeParametersAsync = useCallback(async (
1208
- nodeId: string,
1209
- parameters: Record<string, any>,
1210
- version?: number
1211
- ): Promise<boolean> => {
1212
- try {
1213
- const currentVersion = nodeParameters[nodeId]?.version || version || 0;
1214
- const response = await sendRequest<any>('save_node_parameters', {
1215
- node_id: nodeId,
1216
- parameters,
1217
- version: currentVersion
1218
- });
1219
- if (response.success !== false) {
1220
- // Update local cache
1221
- setNodeParameters(prev => ({
1222
- ...prev,
1223
- [nodeId]: {
1224
- parameters: response.parameters || parameters,
1225
- version: response.version || currentVersion + 1,
1226
- timestamp: response.timestamp
1227
- }
1228
- }));
1229
- return true;
1230
- }
1231
- return false;
1232
- } catch (error) {
1233
- console.error('[WebSocket] Failed to save node parameters:', error);
1234
- return false;
1235
- }
1236
- }, [sendRequest, nodeParameters]);
1237
-
1238
- const deleteNodeParametersAsync = useCallback(async (nodeId: string): Promise<boolean> => {
1239
- try {
1240
- await sendRequest<any>('delete_node_parameters', { node_id: nodeId });
1241
- setNodeParameters(prev => {
1242
- const updated = { ...prev };
1243
- delete updated[nodeId];
1244
- return updated;
1245
- });
1246
- return true;
1247
- } catch (error) {
1248
- console.error('[WebSocket] Failed to delete node parameters:', error);
1249
- return false;
1250
- }
1251
- }, [sendRequest]);
1252
-
1253
- // =========================================================================
1254
- // Node Execution Operations
1255
- // =========================================================================
1256
-
1257
- const executeNodeAsync = useCallback(async (
1258
- nodeId: string,
1259
- nodeType: string,
1260
- parameters: Record<string, any>,
1261
- nodes?: any[],
1262
- edges?: any[]
1263
- ): Promise<any> => {
1264
- try {
1265
- // Trigger nodes wait indefinitely for events - no timeout
1266
- const isTriggerNode = TRIGGER_NODE_TYPES.includes(nodeType);
1267
- const timeoutMs = isTriggerNode ? -1 : undefined; // -1 = no timeout
1268
-
1269
- const response = await sendRequest<any>('execute_node', {
1270
- node_id: nodeId,
1271
- node_type: nodeType,
1272
- parameters,
1273
- nodes,
1274
- edges,
1275
- workflow_id: currentWorkflowId // Include workflow_id for per-workflow status scoping
1276
- }, timeoutMs);
1277
- return response;
1278
- } catch (error) {
1279
- console.error('[WebSocket] Failed to execute node:', error);
1280
- throw error;
1281
- }
1282
- }, [sendRequest, currentWorkflowId]);
1283
-
1284
- const getNodeOutputAsync = useCallback(async (
1285
- nodeId: string,
1286
- outputName?: string
1287
- ): Promise<any> => {
1288
- try {
1289
- const response = await sendRequest<any>('get_node_output', {
1290
- node_id: nodeId,
1291
- output_name: outputName || 'output_0'
1292
- });
1293
- if (response.success) {
1294
- return response.data;
1295
- }
1296
- return null;
1297
- } catch (error) {
1298
- console.error('[WebSocket] Failed to get node output:', error);
1299
- return null;
1300
- }
1301
- }, [sendRequest]);
1302
-
1303
- // Cancel event wait (for trigger nodes)
1304
- const cancelEventWaitAsync = useCallback(async (
1305
- nodeId: string,
1306
- waiterId?: string
1307
- ): Promise<{ success: boolean; cancelled_count?: number }> => {
1308
- try {
1309
- const response = await sendRequest<{ success: boolean; cancelled_count?: number }>('cancel_event_wait', {
1310
- node_id: nodeId,
1311
- waiter_id: waiterId
1312
- });
1313
- return response;
1314
- } catch (error) {
1315
- console.error('[WebSocket] Failed to cancel event wait:', error);
1316
- return { success: false };
1317
- }
1318
- }, [sendRequest]);
1319
-
1320
- const executeWorkflowAsync = useCallback(async (
1321
- nodes: any[],
1322
- edges: any[],
1323
- sessionId?: string
1324
- ): Promise<any> => {
1325
- try {
1326
- const response = await sendRequest<any>('execute_workflow', {
1327
- nodes: nodes.map(node => ({
1328
- id: node.id,
1329
- type: node.type || '',
1330
- data: node.data || {}
1331
- })),
1332
- edges: edges.map(edge => ({
1333
- id: edge.id,
1334
- source: edge.source,
1335
- target: edge.target,
1336
- sourceHandle: edge.sourceHandle || undefined,
1337
- targetHandle: edge.targetHandle || undefined
1338
- })),
1339
- session_id: sessionId || 'default'
1340
- });
1341
-
1342
- return response;
1343
- } catch (error) {
1344
- console.error('[WebSocket] Failed to execute workflow:', error);
1345
- throw error;
1346
- }
1347
- }, [sendRequest]);
1348
-
1349
- // =========================================================================
1350
- // Deployment Operations
1351
- // =========================================================================
1352
-
1353
- const deployWorkflowAsync = useCallback(async (
1354
- workflowId: string,
1355
- nodes: any[],
1356
- edges: any[],
1357
- sessionId?: string
1358
- ): Promise<any> => {
1359
- try {
1360
- const response = await sendRequest<any>('deploy_workflow', {
1361
- workflow_id: workflowId,
1362
- nodes: nodes.map(node => ({
1363
- id: node.id,
1364
- type: node.type || '',
1365
- data: node.data || {}
1366
- })),
1367
- edges: edges.map(edge => ({
1368
- id: edge.id,
1369
- source: edge.source,
1370
- target: edge.target,
1371
- sourceHandle: edge.sourceHandle || undefined,
1372
- targetHandle: edge.targetHandle || undefined
1373
- })),
1374
- session_id: sessionId || 'default'
1375
- });
1376
-
1377
- return response;
1378
- } catch (error) {
1379
- console.error('[WebSocket] Failed to start deployment:', error);
1380
- throw error;
1381
- }
1382
- }, [sendRequest]);
1383
-
1384
- const cancelDeploymentAsync = useCallback(async (workflowId?: string): Promise<any> => {
1385
- try {
1386
- const response = await sendRequest<any>('cancel_deployment', {
1387
- workflow_id: workflowId
1388
- });
1389
-
1390
- // Reset deployment status only if the cancelled workflow matches current
1391
- if (!workflowId || workflowId === deploymentStatus.workflow_id) {
1392
- setDeploymentStatus(defaultDeploymentStatus);
1393
- }
1394
-
1395
- return response;
1396
- } catch (error) {
1397
- console.error('[WebSocket] Failed to cancel deployment:', error);
1398
- throw error;
1399
- }
1400
- }, [sendRequest, deploymentStatus.workflow_id]);
1401
-
1402
- const getDeploymentStatusAsync = useCallback(async (workflowId?: string): Promise<{ isRunning: boolean; activeRuns: number; settings?: any; workflow_id?: string }> => {
1403
- try {
1404
- const response = await sendRequest<any>('get_deployment_status', { workflow_id: workflowId });
1405
- return {
1406
- isRunning: response.is_running || false,
1407
- activeRuns: response.active_runs || 0,
1408
- settings: response.settings,
1409
- workflow_id: response.workflow_id
1410
- };
1411
- } catch (error) {
1412
- console.error('[WebSocket] Failed to get deployment status:', error);
1413
- return { isRunning: false, activeRuns: 0 };
1414
- }
1415
- }, [sendRequest]);
1416
-
1417
- // =========================================================================
1418
- // AI Operations
1419
- // =========================================================================
1420
-
1421
- const executeAiNodeAsync = useCallback(async (
1422
- nodeId: string,
1423
- nodeType: string,
1424
- parameters: Record<string, any>,
1425
- model: string
1426
- ): Promise<any> => {
1427
- try {
1428
- const response = await sendRequest<any>('execute_ai_node', {
1429
- node_id: nodeId,
1430
- node_type: nodeType,
1431
- parameters,
1432
- model
1433
- });
1434
- return response;
1435
- } catch (error) {
1436
- console.error('[WebSocket] Failed to execute AI node:', error);
1437
- throw error;
1438
- }
1439
- }, [sendRequest]);
1440
-
1441
- const getAiModelsAsync = useCallback(async (provider: string, apiKey: string): Promise<string[]> => {
1442
- try {
1443
- const response = await sendRequest<any>('get_ai_models', {
1444
- provider,
1445
- api_key: apiKey
1446
- });
1447
- return response.models || [];
1448
- } catch (error) {
1449
- console.error('[WebSocket] Failed to get AI models:', error);
1450
- return [];
1451
- }
1452
- }, [sendRequest]);
1453
-
1454
- // =========================================================================
1455
- // API Key Operations
1456
- // =========================================================================
1457
-
1458
- const validateApiKeyAsync = useCallback(async (
1459
- provider: string,
1460
- apiKey: string
1461
- ): Promise<{ valid: boolean; message?: string; models?: string[] }> => {
1462
- try {
1463
- const response = await sendRequest<any>('validate_api_key', {
1464
- provider,
1465
- api_key: apiKey
1466
- });
1467
- const result = {
1468
- valid: response.valid || false,
1469
- message: response.message,
1470
- models: response.models
1471
- };
1472
-
1473
- // Update apiKeyStatuses on successful validation
1474
- if (result.valid) {
1475
- setApiKeyStatuses(prev => ({
1476
- ...prev,
1477
- [provider]: { hasKey: true, valid: true, models: result.models }
1478
- }));
1479
- }
1480
-
1481
- return result;
1482
- } catch (error) {
1483
- console.error('[WebSocket] Failed to validate API key:', error);
1484
- return { valid: false, message: 'Validation failed' };
1485
- }
1486
- }, [sendRequest]);
1487
-
1488
- const getStoredApiKeyAsync = useCallback(async (
1489
- provider: string
1490
- ): Promise<{ hasKey: boolean; apiKey?: string; models?: string[] }> => {
1491
- try {
1492
- const response = await sendRequest<any>('get_stored_api_key', { provider });
1493
- const result = {
1494
- hasKey: response.has_key || false,
1495
- apiKey: response.api_key,
1496
- models: response.models
1497
- };
1498
-
1499
- // Update apiKeyStatuses with stored models
1500
- if (result.hasKey) {
1501
- setApiKeyStatuses(prev => ({
1502
- ...prev,
1503
- [provider]: { hasKey: true, valid: true, models: result.models }
1504
- }));
1505
- }
1506
-
1507
- return result;
1508
- } catch (error) {
1509
- console.error('[WebSocket] Failed to get stored API key:', error);
1510
- return { hasKey: false };
1511
- }
1512
- }, [sendRequest]);
1513
-
1514
- const saveApiKeyAsync = useCallback(async (
1515
- provider: string,
1516
- apiKey: string,
1517
- models?: string[]
1518
- ): Promise<boolean> => {
1519
- try {
1520
- const response = await sendRequest<any>('save_api_key', {
1521
- provider,
1522
- api_key: apiKey,
1523
- models
1524
- });
1525
- const success = response.success !== false;
1526
-
1527
- // Update apiKeyStatuses on successful save
1528
- if (success) {
1529
- setApiKeyStatuses(prev => ({
1530
- ...prev,
1531
- [provider]: { hasKey: true, valid: true, models }
1532
- }));
1533
- }
1534
-
1535
- return success;
1536
- } catch (error) {
1537
- console.error('[WebSocket] Failed to save API key:', error);
1538
- return false;
1539
- }
1540
- }, [sendRequest]);
1541
-
1542
- const deleteApiKeyAsync = useCallback(async (provider: string): Promise<boolean> => {
1543
- try {
1544
- await sendRequest<any>('delete_api_key', { provider });
1545
-
1546
- // Remove from apiKeyStatuses on successful delete
1547
- setApiKeyStatuses(prev => {
1548
- const newStatuses = { ...prev };
1549
- delete newStatuses[provider];
1550
- return newStatuses;
1551
- });
1552
-
1553
- return true;
1554
- } catch (error) {
1555
- console.error('[WebSocket] Failed to delete API key:', error);
1556
- return false;
1557
- }
1558
- }, [sendRequest]);
1559
-
1560
- // =========================================================================
1561
- // Android Operations
1562
- // =========================================================================
1563
-
1564
- const getAndroidDevicesAsync = useCallback(async (): Promise<string[]> => {
1565
- try {
1566
- const response = await sendRequest<any>('get_android_devices', {});
1567
- return response.devices || [];
1568
- } catch (error) {
1569
- console.error('[WebSocket] Failed to get Android devices:', error);
1570
- return [];
1571
- }
1572
- }, [sendRequest]);
1573
-
1574
- const executeAndroidActionAsync = useCallback(async (
1575
- serviceId: string,
1576
- action: string,
1577
- parameters: Record<string, any>,
1578
- deviceId?: string
1579
- ): Promise<any> => {
1580
- try {
1581
- const response = await sendRequest<any>('execute_android_action', {
1582
- service_id: serviceId,
1583
- action,
1584
- parameters,
1585
- device_id: deviceId
1586
- });
1587
- return response;
1588
- } catch (error) {
1589
- console.error('[WebSocket] Failed to execute Android action:', error);
1590
- throw error;
1591
- }
1592
- }, [sendRequest]);
1593
-
1594
- const setupAndroidDeviceAsync = useCallback(async (
1595
- connectionType: string,
1596
- deviceId?: string,
1597
- websocketUrl?: string
1598
- ): Promise<any> => {
1599
- try {
1600
- const response = await sendRequest<any>('setup_android_device', {
1601
- connection_type: connectionType,
1602
- device_id: deviceId,
1603
- websocket_url: websocketUrl
1604
- });
1605
- return response;
1606
- } catch (error) {
1607
- console.error('[WebSocket] Failed to setup Android device:', error);
1608
- throw error;
1609
- }
1610
- }, [sendRequest]);
1611
-
1612
- // =========================================================================
1613
- // Maps Operations
1614
- // =========================================================================
1615
-
1616
- const validateMapsKeyAsync = useCallback(async (
1617
- apiKey: string
1618
- ): Promise<{ valid: boolean; message?: string }> => {
1619
- try {
1620
- const response = await sendRequest<any>('validate_maps_key', { api_key: apiKey });
1621
- return {
1622
- valid: response.valid || false,
1623
- message: response.message
1624
- };
1625
- } catch (error) {
1626
- console.error('[WebSocket] Failed to validate Maps key:', error);
1627
- return { valid: false, message: 'Validation failed' };
1628
- }
1629
- }, [sendRequest]);
1630
-
1631
- // =========================================================================
1632
- // WhatsApp Operations
1633
- // =========================================================================
1634
-
1635
- const getWhatsAppStatusAsync = useCallback(async (): Promise<{ connected: boolean; deviceId?: string; data?: any }> => {
1636
- try {
1637
- const response = await sendRequest<any>('whatsapp_status', {});
1638
- return {
1639
- connected: response.connected || false,
1640
- deviceId: response.device_id,
1641
- data: response.data
1642
- };
1643
- } catch (error) {
1644
- console.error('[WebSocket] Failed to get WhatsApp status:', error);
1645
- return { connected: false };
1646
- }
1647
- }, [sendRequest]);
1648
-
1649
- const getWhatsAppQRAsync = useCallback(async (): Promise<{ connected: boolean; qr?: string; message?: string }> => {
1650
- try {
1651
- const response = await sendRequest<any>('whatsapp_qr', {});
1652
- return {
1653
- connected: response.connected || false,
1654
- qr: response.qr,
1655
- message: response.message
1656
- };
1657
- } catch (error) {
1658
- console.error('[WebSocket] Failed to get WhatsApp QR:', error);
1659
- return { connected: false, message: 'Failed to get QR code' };
1660
- }
1661
- }, [sendRequest]);
1662
-
1663
- const sendWhatsAppMessageAsync = useCallback(async (
1664
- phone: string,
1665
- message: string
1666
- ): Promise<{ success: boolean; messageId?: string; error?: string }> => {
1667
- try {
1668
- const response = await sendRequest<any>('whatsapp_send', { phone, message });
1669
- return {
1670
- success: response.success || false,
1671
- messageId: response.messageId,
1672
- error: response.error
1673
- };
1674
- } catch (error: any) {
1675
- console.error('[WebSocket] Failed to send WhatsApp message:', error);
1676
- return { success: false, error: error.message || 'Send failed' };
1677
- }
1678
- }, [sendRequest]);
1679
-
1680
- const startWhatsAppConnectionAsync = useCallback(async (): Promise<{ success: boolean; message?: string }> => {
1681
- try {
1682
- const response = await sendRequest<any>('whatsapp_start', {});
1683
- return {
1684
- success: response.success !== false,
1685
- message: response.message
1686
- };
1687
- } catch (error: any) {
1688
- console.error('[WebSocket] Failed to start WhatsApp connection:', error);
1689
- return { success: false, message: error.message || 'Failed to start' };
1690
- }
1691
- }, [sendRequest]);
1692
-
1693
- const restartWhatsAppConnectionAsync = useCallback(async (): Promise<{ success: boolean; message?: string }> => {
1694
- try {
1695
- const response = await sendRequest<any>('whatsapp_restart', {});
1696
- return {
1697
- success: response.success !== false,
1698
- message: response.message
1699
- };
1700
- } catch (error: any) {
1701
- console.error('[WebSocket] Failed to restart WhatsApp connection:', error);
1702
- return { success: false, message: error.message || 'Failed to restart' };
1703
- }
1704
- }, [sendRequest]);
1705
-
1706
- const getWhatsAppGroupsAsync = useCallback(async (): Promise<{ success: boolean; groups: Array<{ jid: string; name: string; topic?: string; size?: number; is_community?: boolean }>; error?: string }> => {
1707
- try {
1708
- const response = await sendRequest<any>('whatsapp_groups', {});
1709
- return {
1710
- success: response.success !== false,
1711
- groups: response.groups || [],
1712
- error: response.error
1713
- };
1714
- } catch (error: any) {
1715
- console.error('[WebSocket] Failed to get WhatsApp groups:', error);
1716
- return { success: false, groups: [], error: error.message || 'Failed to get groups' };
1717
- }
1718
- }, [sendRequest]);
1719
-
1720
- const getWhatsAppGroupInfoAsync = useCallback(async (groupId: string): Promise<{ success: boolean; participants: Array<{ phone: string; name: string; jid: string; is_admin?: boolean }>; name?: string; error?: string }> => {
1721
- try {
1722
- const response = await sendRequest<any>('whatsapp_group_info', { group_id: groupId });
1723
- return {
1724
- success: response.success !== false,
1725
- participants: response.participants || [],
1726
- name: response.name,
1727
- error: response.error
1728
- };
1729
- } catch (error: any) {
1730
- console.error('[WebSocket] Failed to get WhatsApp group info:', error);
1731
- return { success: false, participants: [], error: error.message || 'Failed to get group info' };
1732
- }
1733
- }, [sendRequest]);
1734
-
1735
- // Track if component is mounted to prevent state updates after unmount
1736
- const isMountedRef = useRef(true);
1737
-
1738
- // Connect only when authenticated (not during auth loading)
1739
- useEffect(() => {
1740
- isMountedRef.current = true;
1741
-
1742
- // Don't connect if still loading auth or not authenticated
1743
- if (authLoading || !isAuthenticated) {
1744
- return;
1745
- }
1746
-
1747
- // Skip if already connected
1748
- if (wsRef.current?.readyState === WebSocket.OPEN) {
1749
- return;
1750
- }
1751
-
1752
- // Small delay to avoid React Strict Mode double-connection issues
1753
- const connectTimeout = setTimeout(() => {
1754
- if (isMountedRef.current && isAuthenticated && !wsRef.current) {
1755
- connect();
1756
- }
1757
- }, 100);
1758
-
1759
- return () => {
1760
- clearTimeout(connectTimeout);
1761
- };
1762
- }, [connect, isAuthenticated, authLoading]);
1763
-
1764
- // Handle logout - separate effect to avoid reconnect loops
1765
- useEffect(() => {
1766
- if (!isAuthenticated && wsRef.current) {
1767
- wsRef.current.close(1000, 'User logged out');
1768
- wsRef.current = null;
1769
- setIsConnected(false);
1770
- }
1771
- }, [isAuthenticated]);
1772
-
1773
- // Cleanup on unmount only
1774
- useEffect(() => {
1775
- return () => {
1776
- isMountedRef.current = false;
1777
- if (reconnectTimeoutRef.current) clearTimeout(reconnectTimeoutRef.current);
1778
- if (pingIntervalRef.current) clearInterval(pingIntervalRef.current);
1779
- if (wsRef.current?.readyState === WebSocket.OPEN) {
1780
- wsRef.current.close(1000, 'Component unmounted');
1781
- }
1782
- };
1783
- }, []);
1784
-
1785
- const value: WebSocketContextValue = {
1786
- // Connection state
1787
- isConnected,
1788
- reconnecting,
1789
-
1790
- // Status data
1791
- androidStatus,
1792
- whatsappStatus,
1793
- whatsappMessages,
1794
- lastWhatsAppMessage,
1795
- apiKeyStatuses,
1796
- consoleLogs,
1797
- terminalLogs,
1798
- chatMessages,
1799
- nodeStatuses,
1800
- nodeParameters,
1801
- variables,
1802
- workflowStatus,
1803
- deploymentStatus,
1804
- workflowLock,
1805
-
1806
- // Status getters
1807
- getNodeStatus,
1808
- getApiKeyStatus,
1809
- getVariable,
1810
- requestStatus,
1811
- clearNodeStatus,
1812
- clearWhatsAppMessages,
1813
- clearConsoleLogs,
1814
- clearTerminalLogs,
1815
- clearChatMessages,
1816
- sendChatMessage: sendChatMessageAsync,
1817
-
1818
- // Generic request method
1819
- sendRequest,
1820
-
1821
- // Node Parameters
1822
- getNodeParameters: getNodeParametersAsync,
1823
- getAllNodeParameters: getAllNodeParametersAsync,
1824
- saveNodeParameters: saveNodeParametersAsync,
1825
- deleteNodeParameters: deleteNodeParametersAsync,
1826
-
1827
- // Node Execution
1828
- executeNode: executeNodeAsync,
1829
- executeWorkflow: executeWorkflowAsync,
1830
- getNodeOutput: getNodeOutputAsync,
1831
-
1832
- // Trigger/Event Waiting
1833
- cancelEventWait: cancelEventWaitAsync,
1834
-
1835
- // Deployment Operations
1836
- deployWorkflow: deployWorkflowAsync,
1837
- cancelDeployment: cancelDeploymentAsync,
1838
- getDeploymentStatus: getDeploymentStatusAsync,
1839
-
1840
- // AI Operations
1841
- executeAiNode: executeAiNodeAsync,
1842
- getAiModels: getAiModelsAsync,
1843
-
1844
- // API Key Operations
1845
- validateApiKey: validateApiKeyAsync,
1846
- getStoredApiKey: getStoredApiKeyAsync,
1847
- saveApiKey: saveApiKeyAsync,
1848
- deleteApiKey: deleteApiKeyAsync,
1849
-
1850
- // Android Operations
1851
- getAndroidDevices: getAndroidDevicesAsync,
1852
- executeAndroidAction: executeAndroidActionAsync,
1853
- setupAndroidDevice: setupAndroidDeviceAsync,
1854
-
1855
- // Maps Operations
1856
- validateMapsKey: validateMapsKeyAsync,
1857
-
1858
- // WhatsApp Operations
1859
- getWhatsAppStatus: getWhatsAppStatusAsync,
1860
- getWhatsAppQR: getWhatsAppQRAsync,
1861
- sendWhatsAppMessage: sendWhatsAppMessageAsync,
1862
- startWhatsAppConnection: startWhatsAppConnectionAsync,
1863
- restartWhatsAppConnection: restartWhatsAppConnectionAsync,
1864
- getWhatsAppGroups: getWhatsAppGroupsAsync,
1865
- getWhatsAppGroupInfo: getWhatsAppGroupInfoAsync
1866
- };
1867
-
1868
- return (
1869
- <WebSocketContext.Provider value={value}>
1870
- {children}
1871
- </WebSocketContext.Provider>
1872
- );
1873
- };
1874
-
1875
- // Hook to use WebSocket context
1876
- export const useWebSocket = (): WebSocketContextValue => {
1877
- const context = useContext(WebSocketContext);
1878
- if (!context) {
1879
- throw new Error('useWebSocket must be used within a WebSocketProvider');
1880
- }
1881
- return context;
1882
- };
1883
-
1884
- // Hook specifically for Android status
1885
- export const useAndroidStatus = (): AndroidStatus & { isConnected: boolean } => {
1886
- const { androidStatus, isConnected } = useWebSocket();
1887
- return {
1888
- ...androidStatus,
1889
- isConnected
1890
- };
1891
- };
1892
-
1893
- // Hook specifically for node status
1894
- export const useNodeStatus = (nodeId: string): NodeStatus | undefined => {
1895
- const { getNodeStatus } = useWebSocket();
1896
- return getNodeStatus(nodeId);
1897
- };
1898
-
1899
- // Hook specifically for workflow status
1900
- export const useWorkflowStatus = (): WorkflowStatus => {
1901
- const { workflowStatus } = useWebSocket();
1902
- return workflowStatus;
1903
- };
1904
-
1905
- // Hook specifically for API key status
1906
- export const useApiKeyStatus = (provider: string): ApiKeyStatus | undefined => {
1907
- const { getApiKeyStatus } = useWebSocket();
1908
- return getApiKeyStatus(provider);
1909
- };
1910
-
1911
- // Hook specifically for WhatsApp status
1912
- export const useWhatsAppStatus = (): WhatsAppStatus => {
1913
- const { whatsappStatus } = useWebSocket();
1914
- return whatsappStatus;
1915
- };
1916
-
1917
- // Hook specifically for deployment status
1918
- export const useDeploymentStatus = (): DeploymentStatus => {
1919
- const { deploymentStatus } = useWebSocket();
1920
- return deploymentStatus;
1921
- };
1922
-
1923
- // Hook specifically for workflow lock status
1924
- export const useWorkflowLock = (): WorkflowLock => {
1925
- const { workflowLock } = useWebSocket();
1926
- return workflowLock;
1927
- };
1928
-
1929
- // Hook specifically for WhatsApp messages (for trigger nodes)
1930
- export const useWhatsAppMessages = (): {
1931
- messages: WhatsAppMessage[];
1932
- lastMessage: WhatsAppMessage | null;
1933
- clearMessages: () => void;
1934
- } => {
1935
- const { whatsappMessages, lastWhatsAppMessage, clearWhatsAppMessages } = useWebSocket();
1936
- return {
1937
- messages: whatsappMessages,
1938
- lastMessage: lastWhatsAppMessage,
1939
- clearMessages: clearWhatsAppMessages
1940
- };
1941
- };
1942
-
1943
- // Hook to check if a tool is currently being executed by any AI Agent
1944
- // Used by tool nodes to show spinning indicator when they're being used
1945
- export const useIsToolExecuting = (toolName: string): boolean => {
1946
- const { nodeStatuses } = useWebSocket();
1947
-
1948
- // Debug: Log what we're checking
1949
- if (toolName) {
1950
- const statusCount = Object.keys(nodeStatuses).length;
1951
- if (statusCount > 0) {
1952
- console.log(`[useIsToolExecuting] Checking for tool '${toolName}', nodeStatuses count:`, statusCount, nodeStatuses);
1953
- }
1954
- }
1955
-
1956
- // Scan all node statuses to find if any AI Agent is executing this tool
1957
- // The status object contains phase and tool_name directly (not nested under data)
1958
- for (const nodeId in nodeStatuses) {
1959
- const status = nodeStatuses[nodeId] as Record<string, any>;
1960
- if (status?.phase === 'executing_tool') {
1961
- console.log(`[useIsToolExecuting] Found executing_tool phase for node ${nodeId}:`, status);
1962
- if (status?.tool_name === toolName) {
1963
- console.log(`[useIsToolExecuting] MATCH! Tool '${toolName}' is executing`);
1964
- return true;
1965
- }
1966
- }
1967
- }
1968
- return false;
1969
- };
1970
-
1971
- export default WebSocketContext;
1
+ /**
2
+ * WebSocket Context for real-time communication with Python backend.
3
+ *
4
+ * Provides WebSocket connection for:
5
+ * - Request/response operations (parameters, execution, API keys)
6
+ * - Real-time broadcasts (status updates, multi-client sync)
7
+ * - Android device connection status
8
+ * - Node execution status (scoped by workflow_id - n8n pattern)
9
+ * - Variable/parameter updates
10
+ * - Workflow state changes
11
+ */
12
+
13
+ import React, { createContext, useContext, useEffect, useState, useCallback, useRef, useMemo } from 'react';
14
+ import { API_CONFIG } from '../config/api';
15
+ import { useAppStore } from '../store/useAppStore';
16
+ import { useAuth } from './AuthContext';
17
+
18
+ // Generate unique request ID
19
+ const generateRequestId = (): string => {
20
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
21
+ };
22
+
23
+ // Pending request tracking
24
+ interface PendingRequest {
25
+ resolve: (value: any) => void;
26
+ reject: (reason: any) => void;
27
+ timeout: NodeJS.Timeout | null; // null for no timeout (trigger nodes)
28
+ }
29
+
30
+ // Request timeout (30 seconds)
31
+ const REQUEST_TIMEOUT = 30000;
32
+
33
+ // Trigger node types that wait indefinitely for events
34
+ const TRIGGER_NODE_TYPES = ['whatsappReceive', 'webhookTrigger', 'cronScheduler', 'chatTrigger'];
35
+
36
+ // Status types
37
+ export interface AndroidStatus {
38
+ connected: boolean;
39
+ paired: boolean;
40
+ device_id: string | null;
41
+ device_name: string | null;
42
+ connected_devices: string[];
43
+ connection_type: string | null;
44
+ qr_data: string | null;
45
+ session_token: string | null;
46
+ }
47
+
48
+ export interface NodeStatus {
49
+ status: 'idle' | 'executing' | 'success' | 'error' | 'waiting';
50
+ data?: Record<string, any>;
51
+ output?: any;
52
+ timestamp?: number;
53
+ // Per-workflow scoping (n8n pattern)
54
+ workflow_id?: string;
55
+ // Waiting state data
56
+ message?: string;
57
+ waiter_id?: string;
58
+ timeout?: number;
59
+ }
60
+
61
+ export interface WorkflowStatus {
62
+ executing: boolean;
63
+ current_node: string | null;
64
+ progress?: number;
65
+ }
66
+
67
+ export interface DeploymentStatus {
68
+ isRunning: boolean;
69
+ activeRuns: number;
70
+ status: 'idle' | 'starting' | 'running' | 'stopped' | 'cancelled' | 'error';
71
+ workflow_id?: string | null; // Which workflow is deployed (for scoping)
72
+ totalTime?: number;
73
+ error?: string;
74
+ }
75
+
76
+ export interface WorkflowLock {
77
+ locked: boolean;
78
+ workflow_id: string | null;
79
+ locked_at: number | null;
80
+ reason: string | null;
81
+ }
82
+
83
+ export interface WhatsAppStatus {
84
+ connected: boolean;
85
+ has_session: boolean;
86
+ running: boolean;
87
+ pairing: boolean;
88
+ device_id?: string;
89
+ qr?: string;
90
+ timestamp?: number;
91
+ }
92
+
93
+ // WhatsApp Rate Limit types (from Go RPC schema)
94
+ export interface RateLimitConfig {
95
+ enabled: boolean;
96
+ min_delay_ms: number;
97
+ max_delay_ms: number;
98
+ typing_delay_ms: number;
99
+ link_extra_delay_ms: number;
100
+ max_messages_per_minute: number;
101
+ max_messages_per_hour: number;
102
+ max_new_contacts_per_day: number;
103
+ simulate_typing: boolean;
104
+ randomize_delays: boolean;
105
+ pause_on_low_response: boolean;
106
+ response_rate_threshold: number;
107
+ }
108
+
109
+ export interface RateLimitStats {
110
+ messages_sent_last_minute: number;
111
+ messages_sent_last_hour: number;
112
+ messages_sent_today: number;
113
+ new_contacts_today: number;
114
+ responses_received: number;
115
+ response_rate: number;
116
+ is_paused: boolean;
117
+ pause_reason?: string;
118
+ }
119
+
120
+ export interface ApiKeyStatus {
121
+ valid: boolean;
122
+ hasKey?: boolean;
123
+ message?: string;
124
+ models?: string[];
125
+ timestamp?: number;
126
+ }
127
+
128
+ // Console log entry from Console nodes
129
+ export interface ConsoleLogEntry {
130
+ node_id: string;
131
+ label: string;
132
+ timestamp: string;
133
+ data: any;
134
+ formatted: string;
135
+ format: 'json' | 'json_compact' | 'text' | 'table';
136
+ workflow_id?: string;
137
+ // Source node info (the node whose output is being logged)
138
+ source_node_id?: string;
139
+ source_node_type?: string;
140
+ source_node_label?: string;
141
+ }
142
+
143
+ // Terminal/server log entry
144
+ export interface TerminalLogEntry {
145
+ timestamp: string;
146
+ level: 'debug' | 'info' | 'warning' | 'error';
147
+ message: string;
148
+ source?: string; // e.g., 'workflow', 'ai', 'android', 'whatsapp'
149
+ details?: any;
150
+ }
151
+
152
+ // Chat message for chatTrigger nodes
153
+ export interface ChatMessage {
154
+ role: 'user' | 'assistant';
155
+ message: string;
156
+ timestamp: string;
157
+ session_id?: string;
158
+ }
159
+
160
+ // WhatsApp received message structure (from Go service via whatsapp_message_received event)
161
+ export interface WhatsAppMessage {
162
+ message_id: string;
163
+ sender: string;
164
+ chat_id: string;
165
+ type: 'text' | 'image' | 'video' | 'audio' | 'document' | 'location' | 'contact' | 'sticker';
166
+ text?: string;
167
+ timestamp: number;
168
+ is_group: boolean;
169
+ push_name?: string;
170
+ media_url?: string;
171
+ media_data?: string; // Base64 if includeMediaData is enabled
172
+ caption?: string;
173
+ // Location message fields
174
+ latitude?: number;
175
+ longitude?: number;
176
+ // Contact message fields
177
+ contact_name?: string;
178
+ vcard?: string;
179
+ }
180
+
181
+ export interface NodeParameters {
182
+ parameters: Record<string, any>;
183
+ version: number;
184
+ timestamp?: number;
185
+ }
186
+
187
+ export interface FullStatus {
188
+ android: AndroidStatus;
189
+ api_keys: Record<string, ApiKeyStatus>;
190
+ nodes: Record<string, NodeStatus>;
191
+ node_parameters: Record<string, NodeParameters>;
192
+ variables: Record<string, any>;
193
+ workflow: WorkflowStatus;
194
+ }
195
+
196
+ // Context value type
197
+ interface WebSocketContextValue {
198
+ // Connection state
199
+ isConnected: boolean;
200
+ reconnecting: boolean;
201
+
202
+ // Status data
203
+ androidStatus: AndroidStatus;
204
+ whatsappStatus: WhatsAppStatus;
205
+ whatsappMessages: WhatsAppMessage[]; // History of received messages
206
+ lastWhatsAppMessage: WhatsAppMessage | null; // Most recent message
207
+ apiKeyStatuses: Record<string, ApiKeyStatus>;
208
+ consoleLogs: ConsoleLogEntry[]; // Console node output logs
209
+ terminalLogs: TerminalLogEntry[]; // Server/terminal logs
210
+ chatMessages: ChatMessage[]; // Chat messages for chatTrigger
211
+ nodeStatuses: Record<string, NodeStatus>; // Current workflow's node statuses
212
+ nodeParameters: Record<string, NodeParameters>;
213
+ variables: Record<string, any>;
214
+ workflowStatus: WorkflowStatus;
215
+ deploymentStatus: DeploymentStatus;
216
+ workflowLock: WorkflowLock;
217
+
218
+ // Status getters
219
+ getNodeStatus: (nodeId: string) => NodeStatus | undefined;
220
+ getApiKeyStatus: (provider: string) => ApiKeyStatus | undefined;
221
+ getVariable: (name: string) => any;
222
+ requestStatus: () => void;
223
+ clearNodeStatus: (nodeId: string) => Promise<void>;
224
+ clearWhatsAppMessages: () => void;
225
+ clearConsoleLogs: () => void;
226
+ clearTerminalLogs: () => void;
227
+ clearChatMessages: () => void;
228
+ sendChatMessage: (message: string, nodeId?: string) => Promise<void>;
229
+
230
+ // Generic request method
231
+ sendRequest: <T = any>(type: string, data?: Record<string, any>) => Promise<T>;
232
+
233
+ // Node Parameters
234
+ getNodeParameters: (nodeId: string) => Promise<NodeParameters | null>;
235
+ getAllNodeParameters: (nodeIds: string[]) => Promise<Record<string, NodeParameters>>;
236
+ saveNodeParameters: (nodeId: string, parameters: Record<string, any>, version?: number) => Promise<boolean>;
237
+ deleteNodeParameters: (nodeId: string) => Promise<boolean>;
238
+
239
+ // Node Execution
240
+ executeNode: (nodeId: string, nodeType: string, parameters: Record<string, any>, nodes?: any[], edges?: any[]) => Promise<any>;
241
+ executeWorkflow: (nodes: any[], edges: any[], sessionId?: string) => Promise<any>;
242
+ getNodeOutput: (nodeId: string, outputName?: string) => Promise<any>;
243
+
244
+ // Trigger/Event Waiting
245
+ cancelEventWait: (nodeId: string, waiterId?: string) => Promise<{ success: boolean; cancelled_count?: number }>;
246
+
247
+ // Deployment Operations
248
+ deployWorkflow: (workflowId: string, nodes: any[], edges: any[], sessionId?: string) => Promise<any>;
249
+ cancelDeployment: (workflowId?: string) => Promise<any>;
250
+ getDeploymentStatus: (workflowId?: string) => Promise<{ isRunning: boolean; activeRuns: number; settings?: any; workflow_id?: string }>;
251
+
252
+ // AI Operations
253
+ executeAiNode: (nodeId: string, nodeType: string, parameters: Record<string, any>, model: string, workflowId: string, nodes: any[], edges: any[]) => Promise<any>;
254
+ getAiModels: (provider: string, apiKey: string) => Promise<string[]>;
255
+
256
+ // API Key Operations
257
+ validateApiKey: (provider: string, apiKey: string) => Promise<{ valid: boolean; message?: string; models?: string[] }>;
258
+ getStoredApiKey: (provider: string) => Promise<{ hasKey: boolean; apiKey?: string; models?: string[] }>;
259
+ saveApiKey: (provider: string, apiKey: string, models?: string[]) => Promise<boolean>;
260
+ deleteApiKey: (provider: string) => Promise<boolean>;
261
+
262
+ // Android Operations
263
+ getAndroidDevices: () => Promise<string[]>;
264
+ executeAndroidAction: (serviceId: string, action: string, parameters: Record<string, any>, deviceId?: string) => Promise<any>;
265
+
266
+ // Maps Operations
267
+ validateMapsKey: (apiKey: string) => Promise<{ valid: boolean; message?: string }>;
268
+
269
+ // WhatsApp Operations
270
+ getWhatsAppStatus: () => Promise<{ connected: boolean; deviceId?: string; data?: any }>;
271
+ getWhatsAppQR: () => Promise<{ connected: boolean; qr?: string; message?: string }>;
272
+ sendWhatsAppMessage: (phone: string, message: string) => Promise<{ success: boolean; messageId?: string; error?: string }>;
273
+ startWhatsAppConnection: () => Promise<{ success: boolean; message?: string }>;
274
+ restartWhatsAppConnection: () => Promise<{ success: boolean; message?: string }>;
275
+ getWhatsAppGroups: () => Promise<{ success: boolean; groups: Array<{ jid: string; name: string; topic?: string; size?: number; is_community?: boolean }>; error?: string }>;
276
+ getWhatsAppGroupInfo: (groupId: string) => Promise<{ success: boolean; participants: Array<{ phone: string; name: string; jid: string; is_admin?: boolean }>; name?: string; error?: string }>;
277
+ getWhatsAppRateLimitConfig: () => Promise<{ success: boolean; config?: RateLimitConfig; stats?: RateLimitStats; error?: string }>;
278
+ setWhatsAppRateLimitConfig: (config: Partial<RateLimitConfig>) => Promise<{ success: boolean; config?: RateLimitConfig; error?: string }>;
279
+ getWhatsAppRateLimitStats: () => Promise<{ success: boolean; stats?: RateLimitStats; error?: string }>;
280
+ unpauseWhatsAppRateLimit: () => Promise<{ success: boolean; stats?: RateLimitStats; error?: string }>;
281
+
282
+ // Memory and Skill Operations
283
+ clearMemory: (sessionId: string, clearLongTerm?: boolean) => Promise<{ success: boolean; default_content?: string; cleared_vector_store?: boolean; error?: string }>;
284
+ resetSkill: (skillName: string) => Promise<{ success: boolean; original_content?: string; is_builtin?: boolean; error?: string }>;
285
+ }
286
+
287
+ // Default values
288
+ const defaultAndroidStatus: AndroidStatus = {
289
+ connected: false,
290
+ paired: false,
291
+ device_id: null,
292
+ device_name: null,
293
+ connected_devices: [],
294
+ connection_type: null,
295
+ qr_data: null,
296
+ session_token: null
297
+ };
298
+
299
+ const defaultWorkflowStatus: WorkflowStatus = {
300
+ executing: false,
301
+ current_node: null
302
+ };
303
+
304
+ const defaultDeploymentStatus: DeploymentStatus = {
305
+ isRunning: false,
306
+ activeRuns: 0,
307
+ status: 'idle'
308
+ };
309
+
310
+ const defaultWorkflowLock: WorkflowLock = {
311
+ locked: false,
312
+ workflow_id: null,
313
+ locked_at: null,
314
+ reason: null
315
+ };
316
+
317
+ const defaultWhatsAppStatus: WhatsAppStatus = {
318
+ connected: false,
319
+ has_session: false,
320
+ running: false,
321
+ pairing: false
322
+ };
323
+
324
+ const WebSocketContext = createContext<WebSocketContextValue | null>(null);
325
+
326
+ // WebSocket URL (convert http to ws)
327
+ const getWebSocketUrl = () => {
328
+ const baseUrl = API_CONFIG.PYTHON_BASE_URL;
329
+
330
+ // Production: empty base URL means use current origin
331
+ if (!baseUrl) {
332
+ const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
333
+ return `${wsProtocol}://${window.location.host}/ws/status`;
334
+ }
335
+
336
+ // Development: convert http(s) to ws(s)
337
+ const wsProtocol = baseUrl.startsWith('https') ? 'wss' : 'ws';
338
+ const wsUrl = baseUrl.replace(/^https?/, wsProtocol);
339
+ return `${wsUrl}/ws/status`;
340
+ };
341
+
342
+ // Max number of WhatsApp messages to keep in history
343
+ const MAX_WHATSAPP_MESSAGE_HISTORY = 100;
344
+
345
+ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
346
+ // Get authentication state - only connect WebSocket when authenticated
347
+ const { isAuthenticated, isLoading: authLoading } = useAuth();
348
+
349
+ // Get current workflow ID for filtering node status updates (n8n pattern)
350
+ const currentWorkflow = useAppStore(state => state.currentWorkflow);
351
+ const currentWorkflowId = currentWorkflow?.id;
352
+
353
+ const [isConnected, setIsConnected] = useState(false);
354
+ const [reconnecting, setReconnecting] = useState(false);
355
+ const [androidStatus, setAndroidStatus] = useState<AndroidStatus>(defaultAndroidStatus);
356
+ const [whatsappStatus, setWhatsappStatus] = useState<WhatsAppStatus>(defaultWhatsAppStatus);
357
+ const [whatsappMessages, setWhatsappMessages] = useState<WhatsAppMessage[]>([]);
358
+ const [lastWhatsAppMessage, setLastWhatsAppMessage] = useState<WhatsAppMessage | null>(null);
359
+ const [apiKeyStatuses, setApiKeyStatuses] = useState<Record<string, ApiKeyStatus>>({});
360
+ const [consoleLogs, setConsoleLogs] = useState<ConsoleLogEntry[]>([]);
361
+ const [terminalLogs, setTerminalLogs] = useState<TerminalLogEntry[]>([]);
362
+ const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
363
+ // Per-workflow node statuses: workflow_id -> node_id -> NodeStatus (n8n pattern)
364
+ const [allNodeStatuses, setAllNodeStatuses] = useState<Record<string, Record<string, NodeStatus>>>({});
365
+ const [nodeParameters, setNodeParameters] = useState<Record<string, NodeParameters>>({});
366
+ // Per-workflow variables: workflow_id -> variable_name -> value (n8n pattern)
367
+ const [allVariables, setAllVariables] = useState<Record<string, Record<string, any>>>({});
368
+ const [workflowStatus, setWorkflowStatus] = useState<WorkflowStatus>(defaultWorkflowStatus);
369
+ const [deploymentStatus, setDeploymentStatus] = useState<DeploymentStatus>(defaultDeploymentStatus);
370
+ const [workflowLock, setWorkflowLock] = useState<WorkflowLock>(defaultWorkflowLock);
371
+
372
+ const wsRef = useRef<WebSocket | null>(null);
373
+ const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
374
+ const pingIntervalRef = useRef<NodeJS.Timeout | null>(null);
375
+ const pendingRequestsRef = useRef<Map<string, PendingRequest>>(new Map());
376
+ // Ref for current workflow ID - allows message handler to access latest value
377
+ // without recreating the WebSocket connection (n8n pattern)
378
+ const currentWorkflowIdRef = useRef<string | undefined>(currentWorkflowId);
379
+
380
+ // Keep the ref in sync with the state and clear node statuses on workflow switch (n8n pattern)
381
+ useEffect(() => {
382
+ const previousWorkflowId = currentWorkflowIdRef.current;
383
+ currentWorkflowIdRef.current = currentWorkflowId;
384
+
385
+ // No need to clear node statuses - they are now stored per-workflow (n8n pattern)
386
+ // Each workflow's statuses are isolated in allNodeStatuses[workflow_id]
387
+ if (previousWorkflowId && currentWorkflowId && previousWorkflowId !== currentWorkflowId) {
388
+
389
+ // Fetch deployment status for the new workflow (n8n pattern)
390
+ // This ensures the deploy button shows correct state when switching workflows
391
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
392
+ const fetchDeploymentStatus = async () => {
393
+ try {
394
+ const requestId = generateRequestId();
395
+ const response = await new Promise<any>((resolve, reject) => {
396
+ const timeout = setTimeout(() => reject(new Error('Timeout')), 5000);
397
+
398
+ const handler = (event: MessageEvent) => {
399
+ try {
400
+ const msg = JSON.parse(event.data);
401
+ if (msg.request_id === requestId) {
402
+ clearTimeout(timeout);
403
+ wsRef.current?.removeEventListener('message', handler);
404
+ resolve(msg);
405
+ }
406
+ } catch {}
407
+ };
408
+
409
+ wsRef.current?.addEventListener('message', handler);
410
+ wsRef.current?.send(JSON.stringify({
411
+ type: 'get_deployment_status',
412
+ request_id: requestId,
413
+ workflow_id: currentWorkflowId
414
+ }));
415
+ });
416
+
417
+ // Update deployment status based on response
418
+ const isRunning = response.is_running || false;
419
+ setDeploymentStatus({
420
+ isRunning,
421
+ activeRuns: response.active_runs || 0,
422
+ status: isRunning ? 'running' : 'idle',
423
+ workflow_id: response.workflow_id || null
424
+ });
425
+
426
+ // Sync with Zustand store's per-workflow isExecuting state (n8n pattern)
427
+ // This ensures Dashboard's isExecuting reflects the actual backend state
428
+ const { setWorkflowExecuting } = useAppStore.getState();
429
+ setWorkflowExecuting(currentWorkflowId, isRunning);
430
+
431
+ // Also update workflow lock based on deployment status (n8n pattern)
432
+ // A running workflow should be locked
433
+ setWorkflowLock({
434
+ locked: isRunning,
435
+ workflow_id: isRunning ? currentWorkflowId : null,
436
+ locked_at: isRunning ? Date.now() : null,
437
+ reason: isRunning ? 'Workflow is running' : null
438
+ });
439
+ } catch (err) {
440
+ console.error('[WebSocket] Failed to fetch deployment status:', err);
441
+ }
442
+ };
443
+ fetchDeploymentStatus();
444
+ }
445
+ }
446
+ }, [currentWorkflowId]);
447
+
448
+ // Handle incoming messages
449
+ const handleMessage = useCallback((event: MessageEvent) => {
450
+ try {
451
+ const message = JSON.parse(event.data);
452
+ const { type, data, node_id, name, value, output, variables: varsUpdate, request_id } = message;
453
+
454
+ // Handle request/response pattern - resolve pending requests
455
+ if (request_id && pendingRequestsRef.current.has(request_id)) {
456
+ const pending = pendingRequestsRef.current.get(request_id)!;
457
+ if (pending.timeout) {
458
+ clearTimeout(pending.timeout);
459
+ }
460
+ pendingRequestsRef.current.delete(request_id);
461
+ pending.resolve(message);
462
+ return; // Response handled, don't process as broadcast
463
+ }
464
+
465
+ switch (type) {
466
+ case 'initial_status':
467
+ case 'full_status':
468
+ if (data) {
469
+ if (data.android) setAndroidStatus(data.android);
470
+ if (data.whatsapp) setWhatsappStatus(data.whatsapp);
471
+ if (data.api_keys) setApiKeyStatuses(data.api_keys);
472
+ // Node statuses from initial_status - group by workflow_id (n8n pattern)
473
+ if (data.nodes) {
474
+ const groupedStatuses: Record<string, Record<string, NodeStatus>> = {};
475
+ for (const [nodeId, status] of Object.entries(data.nodes)) {
476
+ const nodeStatus = status as NodeStatus;
477
+ const wfId = nodeStatus?.workflow_id || 'unknown';
478
+ if (!groupedStatuses[wfId]) groupedStatuses[wfId] = {};
479
+ groupedStatuses[wfId][nodeId] = nodeStatus;
480
+ }
481
+ setAllNodeStatuses(prev => ({ ...prev, ...groupedStatuses }));
482
+ }
483
+ if (data.node_parameters) setNodeParameters(data.node_parameters);
484
+ // Variables from initial_status - group by workflow_id (n8n pattern)
485
+ if (data.variables) {
486
+ // Variables may come with workflow_id or need grouping
487
+ const groupedVars: Record<string, Record<string, any>> = {};
488
+ for (const [varName, varData] of Object.entries(data.variables)) {
489
+ const wfId = (varData as any)?.workflow_id || 'unknown';
490
+ if (!groupedVars[wfId]) groupedVars[wfId] = {};
491
+ groupedVars[wfId][varName] = varData;
492
+ }
493
+ setAllVariables(prev => ({ ...prev, ...groupedVars }));
494
+ }
495
+ if (data.workflow) setWorkflowStatus(data.workflow);
496
+ if (data.workflow_lock) setWorkflowLock(data.workflow_lock);
497
+ // Handle deployment status from initial_status (n8n/Conductor pattern)
498
+ if (data.deployment) {
499
+ setDeploymentStatus({
500
+ isRunning: data.deployment.isRunning || false,
501
+ activeRuns: data.deployment.activeRuns || 0,
502
+ status: data.deployment.status || 'idle'
503
+ });
504
+ }
505
+ }
506
+ break;
507
+
508
+ case 'api_key_status':
509
+ if (message.provider) {
510
+ setApiKeyStatuses(prev => ({
511
+ ...prev,
512
+ [message.provider]: data
513
+ }));
514
+ }
515
+ break;
516
+
517
+ case 'android_status':
518
+ setAndroidStatus(data || defaultAndroidStatus);
519
+ break;
520
+
521
+ case 'whatsapp_status':
522
+ setWhatsappStatus(data || defaultWhatsAppStatus);
523
+ break;
524
+
525
+ case 'whatsapp_message_received':
526
+ // Handle incoming WhatsApp message from Go service
527
+ if (data) {
528
+ const message: WhatsAppMessage = {
529
+ message_id: data.message_id || data.id || '',
530
+ sender: data.sender || data.from || '',
531
+ chat_id: data.chat_id || data.chat || '',
532
+ type: data.type || 'text',
533
+ text: data.text || data.message || data.body || '',
534
+ timestamp: data.timestamp || Date.now(),
535
+ is_group: data.is_group || data.isGroup || false,
536
+ push_name: data.push_name || data.pushName || data.name,
537
+ media_url: data.media_url || data.mediaUrl,
538
+ media_data: data.media_data || data.mediaData,
539
+ caption: data.caption,
540
+ latitude: data.latitude,
541
+ longitude: data.longitude,
542
+ contact_name: data.contact_name || data.contactName,
543
+ vcard: data.vcard
544
+ };
545
+
546
+ // Update last message
547
+ setLastWhatsAppMessage(message);
548
+
549
+ // Add to message history (newest first, limit size)
550
+ setWhatsappMessages(prev => {
551
+ const updated = [message, ...prev];
552
+ return updated.slice(0, MAX_WHATSAPP_MESSAGE_HISTORY);
553
+ });
554
+
555
+ }
556
+ break;
557
+
558
+ case 'node_status':
559
+ // Per-workflow node status storage (n8n pattern)
560
+ // Store status under workflow_id -> node_id structure
561
+ if (node_id) {
562
+ const statusWorkflowId = message.workflow_id || 'unknown';
563
+ // Phase and tool_name are inside data.data (nested structure from broadcaster)
564
+ const innerData = data?.data || {};
565
+
566
+ // Flatten the structure: merge inner data with outer data for easier access
567
+ const flattenedData = { ...data, ...innerData, workflow_id: statusWorkflowId };
568
+
569
+ setAllNodeStatuses((prev: Record<string, Record<string, NodeStatus>>) => ({
570
+ ...prev,
571
+ [statusWorkflowId]: {
572
+ ...(prev[statusWorkflowId] || {}),
573
+ [node_id]: flattenedData
574
+ }
575
+ }));
576
+ }
577
+ break;
578
+
579
+ case 'node_output':
580
+ // Per-workflow node output storage (n8n pattern)
581
+ if (node_id) {
582
+ const outputWorkflowId = message.workflow_id || 'unknown';
583
+ setAllNodeStatuses((prev: Record<string, Record<string, NodeStatus>>) => ({
584
+ ...prev,
585
+ [outputWorkflowId]: {
586
+ ...(prev[outputWorkflowId] || {}),
587
+ [node_id]: {
588
+ ...(prev[outputWorkflowId]?.[node_id] || {}),
589
+ output,
590
+ workflow_id: outputWorkflowId
591
+ }
592
+ }
593
+ }));
594
+ }
595
+ break;
596
+
597
+ case 'node_status_cleared':
598
+ // Handle broadcast from server when node status is cleared
599
+ if (node_id || message.node_id) {
600
+ const clearedNodeId = node_id || message.node_id;
601
+ const clearWorkflowId = message.workflow_id;
602
+ setAllNodeStatuses((prev: Record<string, Record<string, NodeStatus>>) => {
603
+ // If workflow_id specified, only clear from that workflow
604
+ if (clearWorkflowId && prev[clearWorkflowId]) {
605
+ const workflowStatuses = { ...prev[clearWorkflowId] };
606
+ delete workflowStatuses[clearedNodeId];
607
+ return { ...prev, [clearWorkflowId]: workflowStatuses };
608
+ }
609
+ // Otherwise clear from all workflows
610
+ const newStatuses: Record<string, Record<string, NodeStatus>> = {};
611
+ for (const [wfId, nodes] of Object.entries(prev)) {
612
+ const filteredNodes = { ...nodes };
613
+ delete filteredNodes[clearedNodeId];
614
+ newStatuses[wfId] = filteredNodes;
615
+ }
616
+ return newStatuses;
617
+ });
618
+ }
619
+ break;
620
+
621
+ // Node parameters broadcasts (from other clients)
622
+ case 'node_parameters_updated':
623
+ if (node_id) {
624
+ setNodeParameters(prev => ({
625
+ ...prev,
626
+ [node_id]: {
627
+ parameters: message.parameters,
628
+ version: message.version,
629
+ timestamp: message.timestamp
630
+ }
631
+ }));
632
+ }
633
+ break;
634
+
635
+ case 'node_parameters_deleted':
636
+ if (node_id) {
637
+ setNodeParameters(prev => {
638
+ const updated = { ...prev };
639
+ delete updated[node_id];
640
+ return updated;
641
+ });
642
+ }
643
+ break;
644
+
645
+ case 'variable_update':
646
+ // Per-workflow variable storage (n8n pattern)
647
+ if (name !== undefined) {
648
+ const varWorkflowId = message.workflow_id || 'unknown';
649
+ setAllVariables((prev: Record<string, Record<string, any>>) => ({
650
+ ...prev,
651
+ [varWorkflowId]: {
652
+ ...(prev[varWorkflowId] || {}),
653
+ [name]: value
654
+ }
655
+ }));
656
+ }
657
+ break;
658
+
659
+ case 'variables_update':
660
+ // Per-workflow batch variable update (n8n pattern)
661
+ if (varsUpdate) {
662
+ const batchWorkflowId = message.workflow_id || 'unknown';
663
+ setAllVariables((prev: Record<string, Record<string, any>>) => ({
664
+ ...prev,
665
+ [batchWorkflowId]: {
666
+ ...(prev[batchWorkflowId] || {}),
667
+ ...varsUpdate
668
+ }
669
+ }));
670
+ }
671
+ break;
672
+
673
+ case 'workflow_status':
674
+ setWorkflowStatus(data || defaultWorkflowStatus);
675
+ break;
676
+
677
+ case 'deployment_status':
678
+ // Handle deployment status updates (event-driven, no iterations)
679
+ // Per-workflow scoping (n8n pattern): Only apply updates for current workflow
680
+ if (message.status) {
681
+ const deploymentWorkflowId = message.workflow_id;
682
+ const activeWorkflowId = currentWorkflowIdRef.current;
683
+
684
+ // Apply deployment update if:
685
+ // 1. It's for the current workflow, OR
686
+ // 2. It's a stop/cancel/error (affects any workflow that was running), OR
687
+ // 3. No specific workflow context (backward compatibility)
688
+ const isTerminalStatus = ['stopped', 'cancelled', 'error'].includes(message.status);
689
+ const shouldApplyDeployment = !deploymentWorkflowId ||
690
+ deploymentWorkflowId === activeWorkflowId ||
691
+ isTerminalStatus;
692
+
693
+ if (shouldApplyDeployment) {
694
+ setDeploymentStatus(prev => {
695
+ const newStatus: DeploymentStatus = { ...prev };
696
+ // Capture workflow_id from message
697
+ if (message.workflow_id) {
698
+ newStatus.workflow_id = message.workflow_id;
699
+ }
700
+
701
+ switch (message.status) {
702
+ case 'starting':
703
+ newStatus.isRunning = true;
704
+ newStatus.status = 'starting';
705
+ newStatus.activeRuns = 0;
706
+ break;
707
+ case 'running':
708
+ case 'started':
709
+ newStatus.isRunning = true;
710
+ newStatus.status = 'running';
711
+ newStatus.activeRuns = message.data?.active_runs ?? prev.activeRuns;
712
+ break;
713
+ case 'run_started':
714
+ newStatus.isRunning = true;
715
+ newStatus.status = 'running';
716
+ newStatus.activeRuns = message.data?.active_runs || prev.activeRuns + 1;
717
+ break;
718
+ case 'run_complete':
719
+ newStatus.activeRuns = Math.max(0, message.data?.active_runs || prev.activeRuns - 1);
720
+ break;
721
+ case 'stopped':
722
+ // Only clear if this was our workflow or no workflow was tracked
723
+ if (!prev.workflow_id || prev.workflow_id === deploymentWorkflowId) {
724
+ newStatus.isRunning = false;
725
+ newStatus.status = 'stopped';
726
+ newStatus.totalTime = message.data?.total_time;
727
+ newStatus.activeRuns = 0;
728
+ newStatus.workflow_id = null;
729
+ }
730
+ break;
731
+ case 'cancelled':
732
+ // Only clear if this was our workflow or no workflow was tracked
733
+ if (!prev.workflow_id || prev.workflow_id === deploymentWorkflowId) {
734
+ newStatus.isRunning = false;
735
+ newStatus.status = 'cancelled';
736
+ newStatus.activeRuns = 0;
737
+ newStatus.workflow_id = null;
738
+ }
739
+ break;
740
+ case 'error':
741
+ // Only clear if this was our workflow or no workflow was tracked
742
+ if (!prev.workflow_id || prev.workflow_id === deploymentWorkflowId) {
743
+ newStatus.isRunning = false;
744
+ newStatus.status = 'error';
745
+ newStatus.error = message.error;
746
+ newStatus.workflow_id = null;
747
+ }
748
+ break;
749
+ }
750
+
751
+ return newStatus;
752
+ });
753
+ // Sync with Zustand store's per-workflow isExecuting state (n8n pattern)
754
+ if (deploymentWorkflowId) {
755
+ const { setWorkflowExecuting } = useAppStore.getState();
756
+ const isRunning = ['starting', 'running', 'started', 'run_started'].includes(message.status);
757
+ const isStopped = ['stopped', 'cancelled', 'error'].includes(message.status);
758
+ if (isRunning || isStopped) {
759
+ setWorkflowExecuting(deploymentWorkflowId, isRunning);
760
+ }
761
+ }
762
+ }
763
+ }
764
+ break;
765
+
766
+ case 'pong':
767
+ // Keep-alive response, no action needed
768
+ break;
769
+
770
+ case 'console_log':
771
+ // Handle console log entries from Console nodes
772
+ if (data) {
773
+ const logEntry: ConsoleLogEntry = {
774
+ node_id: data.node_id || '',
775
+ label: data.label || 'Console',
776
+ timestamp: data.timestamp || new Date().toISOString(),
777
+ data: data.data,
778
+ formatted: data.formatted || JSON.stringify(data.data, null, 2),
779
+ format: data.format || 'json',
780
+ workflow_id: data.workflow_id,
781
+ source_node_id: data.source_node_id,
782
+ source_node_type: data.source_node_type,
783
+ source_node_label: data.source_node_label
784
+ };
785
+ // Add to logs (newest first, limit to 100 entries)
786
+ setConsoleLogs(prev => {
787
+ const updated = [logEntry, ...prev];
788
+ return updated.slice(0, 100);
789
+ });
790
+ }
791
+ break;
792
+
793
+ case 'console_logs_cleared':
794
+ // Handle console logs cleared from server
795
+ if (message.workflow_id) {
796
+ setConsoleLogs(prev => prev.filter(log => log.workflow_id !== message.workflow_id));
797
+ } else {
798
+ setConsoleLogs([]);
799
+ }
800
+ break;
801
+
802
+ case 'terminal_log':
803
+ // Handle terminal/server log entries
804
+ if (data) {
805
+ const terminalEntry: TerminalLogEntry = {
806
+ timestamp: data.timestamp || new Date().toISOString(),
807
+ level: data.level || 'info',
808
+ message: data.message || '',
809
+ source: data.source,
810
+ details: data.details
811
+ };
812
+ // Add to logs (newest first, limit to 200 entries)
813
+ setTerminalLogs(prev => {
814
+ const updated = [terminalEntry, ...prev];
815
+ return updated.slice(0, 200);
816
+ });
817
+ }
818
+ break;
819
+
820
+ case 'terminal_logs_cleared':
821
+ // Handle terminal logs cleared from server
822
+ setTerminalLogs([]);
823
+ break;
824
+
825
+ case 'workflow_lock':
826
+ // Handle workflow lock status updates (per-workflow locking - n8n pattern)
827
+ // Only update lock state if it's for the current workflow or if unlocking
828
+ if (data) {
829
+ const lockWorkflowId = message.workflow_id || data.workflow_id;
830
+ const activeWorkflowId = currentWorkflowIdRef.current;
831
+
832
+ // Apply lock update if:
833
+ // 1. It's for the current workflow, OR
834
+ // 2. We're unlocking (locked=false), OR
835
+ // 3. No specific workflow context (backward compatibility)
836
+ const shouldApplyLock = !lockWorkflowId ||
837
+ lockWorkflowId === activeWorkflowId ||
838
+ !data.locked;
839
+
840
+ if (shouldApplyLock) {
841
+ setWorkflowLock({
842
+ locked: data.locked || false,
843
+ workflow_id: data.workflow_id || null,
844
+ locked_at: data.locked_at || null,
845
+ reason: data.reason || null
846
+ });
847
+ }
848
+ }
849
+ break;
850
+
851
+ case 'error':
852
+ console.error('[WebSocket] Server error:', message.code, message.message);
853
+ break;
854
+
855
+ default:
856
+ break;
857
+ }
858
+ } catch (error) {
859
+ console.error('[WebSocket] Failed to parse message:', error);
860
+ }
861
+ }, []); // Empty deps - uses ref for currentWorkflowId to avoid reconnecting WebSocket
862
+
863
+ // Connect to WebSocket
864
+ const connect = useCallback(() => {
865
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
866
+ return;
867
+ }
868
+
869
+ const wsUrl = getWebSocketUrl();
870
+
871
+ try {
872
+ const ws = new WebSocket(wsUrl);
873
+
874
+ ws.onopen = async () => {
875
+ setIsConnected(true);
876
+ setReconnecting(false);
877
+
878
+ // Start ping interval
879
+ pingIntervalRef.current = setInterval(() => {
880
+ if (ws.readyState === WebSocket.OPEN) {
881
+ ws.send(JSON.stringify({ type: 'ping' }));
882
+ }
883
+ }, 30000);
884
+
885
+ // Load initial API key statuses for known providers
886
+ const providers = ['openai', 'anthropic', 'gemini', 'google_maps', 'android_remote'];
887
+ for (const provider of providers) {
888
+ try {
889
+ const response = await new Promise<any>((resolve, reject) => {
890
+ const requestId = `init_${provider}_${Date.now()}`;
891
+ const timeout = setTimeout(() => reject(new Error('Timeout')), 5000);
892
+
893
+ const handler = (event: MessageEvent) => {
894
+ try {
895
+ const msg = JSON.parse(event.data);
896
+ if (msg.request_id === requestId) {
897
+ clearTimeout(timeout);
898
+ ws.removeEventListener('message', handler);
899
+ resolve(msg);
900
+ }
901
+ } catch {}
902
+ };
903
+
904
+ ws.addEventListener('message', handler);
905
+ ws.send(JSON.stringify({ type: 'get_stored_api_key', provider, request_id: requestId }));
906
+ });
907
+
908
+ if (response.has_key) {
909
+ setApiKeyStatuses(prev => ({
910
+ ...prev,
911
+ [provider]: { hasKey: true, valid: true }
912
+ }));
913
+ }
914
+ } catch {
915
+ // Ignore errors during initial check
916
+ }
917
+ }
918
+
919
+ // Load terminal log history
920
+ try {
921
+ const terminalResponse = await new Promise<any>((resolve, reject) => {
922
+ const requestId = `terminal_logs_${Date.now()}`;
923
+ const timeout = setTimeout(() => reject(new Error('Timeout')), 5000);
924
+
925
+ const handler = (event: MessageEvent) => {
926
+ try {
927
+ const msg = JSON.parse(event.data);
928
+ if (msg.request_id === requestId) {
929
+ clearTimeout(timeout);
930
+ ws.removeEventListener('message', handler);
931
+ resolve(msg);
932
+ }
933
+ } catch {}
934
+ };
935
+
936
+ ws.addEventListener('message', handler);
937
+ ws.send(JSON.stringify({ type: 'get_terminal_logs', request_id: requestId }));
938
+ });
939
+
940
+ if (terminalResponse.success && terminalResponse.logs) {
941
+ // Map server logs to TerminalLogEntry format (newest first)
942
+ const logs: TerminalLogEntry[] = terminalResponse.logs.map((log: any) => ({
943
+ timestamp: log.timestamp || new Date().toISOString(),
944
+ level: log.level || 'info',
945
+ message: log.message || '',
946
+ source: log.source,
947
+ details: log.details
948
+ })).reverse(); // Server stores oldest first, we want newest first
949
+ setTerminalLogs(logs);
950
+ }
951
+ } catch {
952
+ // Ignore errors loading terminal logs
953
+ }
954
+
955
+ // Load chat message history from database
956
+ try {
957
+ const chatResponse = await new Promise<any>((resolve, reject) => {
958
+ const requestId = `chat_messages_${Date.now()}`;
959
+ const timeout = setTimeout(() => reject(new Error('Timeout')), 5000);
960
+
961
+ const handler = (event: MessageEvent) => {
962
+ try {
963
+ const msg = JSON.parse(event.data);
964
+ if (msg.request_id === requestId) {
965
+ clearTimeout(timeout);
966
+ ws.removeEventListener('message', handler);
967
+ resolve(msg);
968
+ }
969
+ } catch {}
970
+ };
971
+
972
+ ws.addEventListener('message', handler);
973
+ ws.send(JSON.stringify({ type: 'get_chat_messages', session_id: 'default', request_id: requestId }));
974
+ });
975
+
976
+ if (chatResponse.success && chatResponse.messages) {
977
+ const messages: ChatMessage[] = chatResponse.messages.map((msg: any) => ({
978
+ role: msg.role as 'user' | 'assistant',
979
+ message: msg.message,
980
+ timestamp: msg.timestamp
981
+ }));
982
+ setChatMessages(messages);
983
+ }
984
+ } catch {
985
+ // Ignore errors loading chat messages
986
+ }
987
+
988
+ // Load console logs from database
989
+ try {
990
+ const consoleRequestId = `console_${Date.now()}`;
991
+ const consoleResponse = await new Promise<any>((resolve, reject) => {
992
+ const timeout = setTimeout(() => reject(new Error('Timeout')), 5000);
993
+
994
+ const handler = (event: MessageEvent) => {
995
+ try {
996
+ const msg = JSON.parse(event.data);
997
+ if (msg.request_id === consoleRequestId) {
998
+ clearTimeout(timeout);
999
+ ws.removeEventListener('message', handler);
1000
+ resolve(msg);
1001
+ }
1002
+ } catch {}
1003
+ };
1004
+
1005
+ ws.addEventListener('message', handler);
1006
+ ws.send(JSON.stringify({ type: 'get_console_logs', limit: 100, request_id: consoleRequestId }));
1007
+ });
1008
+
1009
+ if (consoleResponse.success && consoleResponse.logs) {
1010
+ const logs: ConsoleLogEntry[] = consoleResponse.logs.map((log: any) => ({
1011
+ node_id: log.node_id,
1012
+ label: log.label,
1013
+ timestamp: log.timestamp,
1014
+ data: log.data,
1015
+ formatted: log.formatted,
1016
+ format: log.format,
1017
+ workflow_id: log.workflow_id,
1018
+ source_node_id: log.source_node_id,
1019
+ source_node_type: log.source_node_type,
1020
+ source_node_label: log.source_node_label,
1021
+ }));
1022
+ setConsoleLogs(logs);
1023
+ }
1024
+ } catch {
1025
+ // Ignore errors loading console logs
1026
+ }
1027
+ };
1028
+
1029
+ ws.onmessage = handleMessage;
1030
+
1031
+ ws.onclose = (event) => {
1032
+ console.log('[WebSocket] Disconnected:', event.code, event.reason);
1033
+ setIsConnected(false);
1034
+ wsRef.current = null;
1035
+
1036
+ // Clear ping interval
1037
+ if (pingIntervalRef.current) {
1038
+ clearInterval(pingIntervalRef.current);
1039
+ pingIntervalRef.current = null;
1040
+ }
1041
+
1042
+ // Reconnect after delay (unless intentional close)
1043
+ if (event.code !== 1000) {
1044
+ setReconnecting(true);
1045
+ reconnectTimeoutRef.current = setTimeout(() => {
1046
+ connect();
1047
+ }, 3000);
1048
+ }
1049
+ };
1050
+
1051
+ ws.onerror = (error) => {
1052
+ console.error('[WebSocket] Error:', error);
1053
+ };
1054
+
1055
+ wsRef.current = ws;
1056
+ } catch (error) {
1057
+ console.error('[WebSocket] Failed to create connection:', error);
1058
+ setReconnecting(true);
1059
+ reconnectTimeoutRef.current = setTimeout(connect, 3000);
1060
+ }
1061
+ }, [handleMessage]);
1062
+
1063
+ // Request current status
1064
+ const requestStatus = useCallback(() => {
1065
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
1066
+ wsRef.current.send(JSON.stringify({ type: 'get_status' }));
1067
+ }
1068
+ }, []);
1069
+
1070
+ // Get node status for current workflow (n8n pattern)
1071
+ // IMPORTANT: Use currentWorkflowId state directly (not ref) to ensure reactivity on workflow switch
1072
+ const getNodeStatus = useCallback((nodeId: string) => {
1073
+ if (!currentWorkflowId) {
1074
+ return undefined;
1075
+ }
1076
+ return allNodeStatuses[currentWorkflowId]?.[nodeId];
1077
+ }, [allNodeStatuses, currentWorkflowId]);
1078
+
1079
+ // Get API key status
1080
+ const getApiKeyStatus = useCallback((provider: string) => {
1081
+ return apiKeyStatuses[provider];
1082
+ }, [apiKeyStatuses]);
1083
+
1084
+ // Get variable value for current workflow (n8n pattern)
1085
+ // IMPORTANT: Use currentWorkflowId state directly (not ref) to ensure reactivity on workflow switch
1086
+ const getVariable = useCallback((name: string) => {
1087
+ if (!currentWorkflowId) return undefined;
1088
+ return allVariables[currentWorkflowId]?.[name];
1089
+ }, [allVariables, currentWorkflowId]);
1090
+
1091
+ // Clear node status (used when clearing execution results)
1092
+ // Also clears the backend node_outputs storage
1093
+ const clearNodeStatus = useCallback(async (nodeId: string) => {
1094
+ const workflowId = currentWorkflowIdRef.current;
1095
+ // Clear local state for current workflow
1096
+ setAllNodeStatuses((prev: Record<string, Record<string, NodeStatus>>) => {
1097
+ if (!workflowId || !prev[workflowId]) return prev;
1098
+ const workflowStatuses = { ...prev[workflowId] };
1099
+ delete workflowStatuses[nodeId];
1100
+ return { ...prev, [workflowId]: workflowStatuses };
1101
+ });
1102
+ // Clear backend storage
1103
+ try {
1104
+ if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
1105
+ wsRef.current.send(JSON.stringify({
1106
+ type: 'clear_node_output',
1107
+ node_id: nodeId,
1108
+ workflow_id: workflowId
1109
+ }));
1110
+ }
1111
+ } catch (err) {
1112
+ console.error('[WebSocket] Failed to clear backend node output:', err);
1113
+ }
1114
+ }, []);
1115
+
1116
+ // Clear WhatsApp message history
1117
+ const clearWhatsAppMessages = useCallback(() => {
1118
+ setWhatsappMessages([]);
1119
+ setLastWhatsAppMessage(null);
1120
+ }, []);
1121
+
1122
+ // Clear console logs (both local state and database)
1123
+ const clearConsoleLogs = useCallback(() => {
1124
+ setConsoleLogs([]);
1125
+ // Also clear from database via direct WebSocket send
1126
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
1127
+ wsRef.current.send(JSON.stringify({ type: 'clear_console_logs' }));
1128
+ }
1129
+ }, []);
1130
+
1131
+ // Clear terminal logs (also clears on server)
1132
+ const clearTerminalLogs = useCallback(() => {
1133
+ setTerminalLogs([]);
1134
+ // Also notify server to clear its terminal log history
1135
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
1136
+ wsRef.current.send(JSON.stringify({ type: 'clear_terminal_logs' }));
1137
+ }
1138
+ }, []);
1139
+
1140
+ // Clear chat messages (both local state and database)
1141
+ // Uses direct WebSocket send to avoid dependency on sendRequest (which is defined later)
1142
+ const clearChatMessages = useCallback(() => {
1143
+ setChatMessages([]);
1144
+ // Also clear from database via direct WebSocket send
1145
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
1146
+ wsRef.current.send(JSON.stringify({ type: 'clear_chat_messages', session_id: 'default' }));
1147
+ }
1148
+ }, []);
1149
+
1150
+ // Derive current workflow's node statuses (n8n pattern)
1151
+ // This provides a flat Record<nodeId, NodeStatus> for the current workflow
1152
+ // IMPORTANT: Use currentWorkflowId state directly, not ref, to ensure re-render on workflow switch
1153
+ const nodeStatuses = useMemo(() => {
1154
+ if (!currentWorkflowId) return {};
1155
+ return allNodeStatuses[currentWorkflowId] || {};
1156
+ }, [allNodeStatuses, currentWorkflowId]);
1157
+
1158
+ // Derive current workflow's variables (n8n pattern)
1159
+ // This provides a flat Record<varName, value> for the current workflow
1160
+ // IMPORTANT: Use currentWorkflowId state directly, not ref, to ensure re-render on workflow switch
1161
+ const variables = useMemo(() => {
1162
+ if (!currentWorkflowId) return {};
1163
+ return allVariables[currentWorkflowId] || {};
1164
+ }, [allVariables, currentWorkflowId]);
1165
+
1166
+ // =========================================================================
1167
+ // Core Request/Response Pattern
1168
+ // =========================================================================
1169
+
1170
+ // Send a request and wait for response
1171
+ // timeoutMs: undefined/0 = use default, negative = no timeout (for trigger nodes)
1172
+ const sendRequest = useCallback(async <T = any>(
1173
+ type: string,
1174
+ data?: Record<string, any>,
1175
+ timeoutMs?: number
1176
+ ): Promise<T> => {
1177
+ return new Promise((resolve, reject) => {
1178
+ if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
1179
+ reject(new Error('WebSocket not connected'));
1180
+ return;
1181
+ }
1182
+
1183
+ const requestId = generateRequestId();
1184
+ const useTimeout = timeoutMs === undefined || timeoutMs >= 0;
1185
+ const actualTimeout = timeoutMs && timeoutMs > 0 ? timeoutMs : REQUEST_TIMEOUT;
1186
+
1187
+ let timeout: NodeJS.Timeout | null = null;
1188
+ if (useTimeout && timeoutMs !== -1) {
1189
+ timeout = setTimeout(() => {
1190
+ pendingRequestsRef.current.delete(requestId);
1191
+ reject(new Error(`Request timeout: ${type}`));
1192
+ }, actualTimeout);
1193
+ }
1194
+
1195
+ pendingRequestsRef.current.set(requestId, { resolve, reject, timeout });
1196
+
1197
+ wsRef.current.send(JSON.stringify({
1198
+ type,
1199
+ request_id: requestId,
1200
+ ...data
1201
+ }));
1202
+ });
1203
+ }, []);
1204
+
1205
+ // =========================================================================
1206
+ // Chat Message Operations
1207
+ // =========================================================================
1208
+
1209
+ // Send chat message (triggers chatTrigger nodes and saves to database)
1210
+ // nodeId: optional specific chatTrigger node to target
1211
+ const sendChatMessageAsync = useCallback(async (message: string, nodeId?: string): Promise<void> => {
1212
+ const timestamp = new Date().toISOString();
1213
+ const chatMessage: ChatMessage = {
1214
+ role: 'user',
1215
+ message,
1216
+ timestamp
1217
+ };
1218
+
1219
+ // Add to local messages immediately for UI feedback
1220
+ setChatMessages(prev => [...prev, chatMessage]);
1221
+
1222
+ // Send to backend to dispatch to chatTrigger nodes (also saves to database)
1223
+ try {
1224
+ await sendRequest('send_chat_message', {
1225
+ message,
1226
+ role: 'user',
1227
+ node_id: nodeId, // Target specific chatTrigger node if specified
1228
+ session_id: 'default',
1229
+ timestamp
1230
+ });
1231
+ } catch (error) {
1232
+ console.error('[WebSocket] Failed to send chat message:', error);
1233
+ throw error;
1234
+ }
1235
+ }, [sendRequest]);
1236
+
1237
+ // =========================================================================
1238
+ // Node Parameters Operations
1239
+ // =========================================================================
1240
+
1241
+ const getNodeParametersAsync = useCallback(async (nodeId: string): Promise<NodeParameters | null> => {
1242
+ try {
1243
+ const response = await sendRequest<any>('get_node_parameters', { node_id: nodeId });
1244
+ if (response.parameters) {
1245
+ const params: NodeParameters = {
1246
+ parameters: response.parameters,
1247
+ version: response.version || 0,
1248
+ timestamp: response.timestamp
1249
+ };
1250
+ // Update local cache
1251
+ setNodeParameters(prev => ({ ...prev, [nodeId]: params }));
1252
+ return params;
1253
+ }
1254
+ return null;
1255
+ } catch (error) {
1256
+ console.error('[WebSocket] Failed to get node parameters:', error);
1257
+ return null;
1258
+ }
1259
+ }, [sendRequest]);
1260
+
1261
+ const getAllNodeParametersAsync = useCallback(async (nodeIds: string[]): Promise<Record<string, NodeParameters>> => {
1262
+ if (!nodeIds.length) return {};
1263
+ try {
1264
+ const response = await sendRequest<any>('get_all_node_parameters', { node_ids: nodeIds });
1265
+ const result: Record<string, NodeParameters> = {};
1266
+
1267
+ if (response.parameters) {
1268
+ for (const [nodeId, data] of Object.entries(response.parameters as Record<string, any>)) {
1269
+ result[nodeId] = {
1270
+ parameters: data.parameters || {},
1271
+ version: data.version || 0,
1272
+ timestamp: response.timestamp
1273
+ };
1274
+ }
1275
+ // Update local cache with all parameters
1276
+ setNodeParameters(prev => ({ ...prev, ...result }));
1277
+ }
1278
+ return result;
1279
+ } catch (error) {
1280
+ console.error('[WebSocket] Failed to get all node parameters:', error);
1281
+ return {};
1282
+ }
1283
+ }, [sendRequest]);
1284
+
1285
+ const saveNodeParametersAsync = useCallback(async (
1286
+ nodeId: string,
1287
+ parameters: Record<string, any>,
1288
+ version?: number
1289
+ ): Promise<boolean> => {
1290
+ try {
1291
+ const currentVersion = nodeParameters[nodeId]?.version || version || 0;
1292
+ const response = await sendRequest<any>('save_node_parameters', {
1293
+ node_id: nodeId,
1294
+ parameters,
1295
+ version: currentVersion
1296
+ });
1297
+ if (response.success !== false) {
1298
+ // Update local cache
1299
+ setNodeParameters(prev => ({
1300
+ ...prev,
1301
+ [nodeId]: {
1302
+ parameters: response.parameters || parameters,
1303
+ version: response.version || currentVersion + 1,
1304
+ timestamp: response.timestamp
1305
+ }
1306
+ }));
1307
+ return true;
1308
+ }
1309
+ return false;
1310
+ } catch (error) {
1311
+ console.error('[WebSocket] Failed to save node parameters:', error);
1312
+ return false;
1313
+ }
1314
+ }, [sendRequest, nodeParameters]);
1315
+
1316
+ const deleteNodeParametersAsync = useCallback(async (nodeId: string): Promise<boolean> => {
1317
+ try {
1318
+ await sendRequest<any>('delete_node_parameters', { node_id: nodeId });
1319
+ setNodeParameters(prev => {
1320
+ const updated = { ...prev };
1321
+ delete updated[nodeId];
1322
+ return updated;
1323
+ });
1324
+ return true;
1325
+ } catch (error) {
1326
+ console.error('[WebSocket] Failed to delete node parameters:', error);
1327
+ return false;
1328
+ }
1329
+ }, [sendRequest]);
1330
+
1331
+ // =========================================================================
1332
+ // Node Execution Operations
1333
+ // =========================================================================
1334
+
1335
+ const executeNodeAsync = useCallback(async (
1336
+ nodeId: string,
1337
+ nodeType: string,
1338
+ parameters: Record<string, any>,
1339
+ nodes?: any[],
1340
+ edges?: any[]
1341
+ ): Promise<any> => {
1342
+ try {
1343
+ // Trigger nodes wait indefinitely for events - no timeout
1344
+ const isTriggerNode = TRIGGER_NODE_TYPES.includes(nodeType);
1345
+ const timeoutMs = isTriggerNode ? -1 : undefined; // -1 = no timeout
1346
+
1347
+ const response = await sendRequest<any>('execute_node', {
1348
+ node_id: nodeId,
1349
+ node_type: nodeType,
1350
+ parameters,
1351
+ nodes,
1352
+ edges,
1353
+ workflow_id: currentWorkflowId // Include workflow_id for per-workflow status scoping
1354
+ }, timeoutMs);
1355
+ return response;
1356
+ } catch (error) {
1357
+ console.error('[WebSocket] Failed to execute node:', error);
1358
+ throw error;
1359
+ }
1360
+ }, [sendRequest, currentWorkflowId]);
1361
+
1362
+ const getNodeOutputAsync = useCallback(async (
1363
+ nodeId: string,
1364
+ outputName?: string
1365
+ ): Promise<any> => {
1366
+ try {
1367
+ const response = await sendRequest<any>('get_node_output', {
1368
+ node_id: nodeId,
1369
+ output_name: outputName || 'output_0'
1370
+ });
1371
+ if (response.success) {
1372
+ return response.data;
1373
+ }
1374
+ return null;
1375
+ } catch (error) {
1376
+ console.error('[WebSocket] Failed to get node output:', error);
1377
+ return null;
1378
+ }
1379
+ }, [sendRequest]);
1380
+
1381
+ // Cancel event wait (for trigger nodes)
1382
+ const cancelEventWaitAsync = useCallback(async (
1383
+ nodeId: string,
1384
+ waiterId?: string
1385
+ ): Promise<{ success: boolean; cancelled_count?: number }> => {
1386
+ try {
1387
+ const response = await sendRequest<{ success: boolean; cancelled_count?: number }>('cancel_event_wait', {
1388
+ node_id: nodeId,
1389
+ waiter_id: waiterId
1390
+ });
1391
+ return response;
1392
+ } catch (error) {
1393
+ console.error('[WebSocket] Failed to cancel event wait:', error);
1394
+ return { success: false };
1395
+ }
1396
+ }, [sendRequest]);
1397
+
1398
+ const executeWorkflowAsync = useCallback(async (
1399
+ nodes: any[],
1400
+ edges: any[],
1401
+ sessionId?: string
1402
+ ): Promise<any> => {
1403
+ try {
1404
+ const response = await sendRequest<any>('execute_workflow', {
1405
+ nodes: nodes.map(node => ({
1406
+ id: node.id,
1407
+ type: node.type || '',
1408
+ data: node.data || {}
1409
+ })),
1410
+ edges: edges.map(edge => ({
1411
+ id: edge.id,
1412
+ source: edge.source,
1413
+ target: edge.target,
1414
+ sourceHandle: edge.sourceHandle || undefined,
1415
+ targetHandle: edge.targetHandle || undefined
1416
+ })),
1417
+ session_id: sessionId || 'default'
1418
+ });
1419
+
1420
+ return response;
1421
+ } catch (error) {
1422
+ console.error('[WebSocket] Failed to execute workflow:', error);
1423
+ throw error;
1424
+ }
1425
+ }, [sendRequest]);
1426
+
1427
+ // =========================================================================
1428
+ // Deployment Operations
1429
+ // =========================================================================
1430
+
1431
+ const deployWorkflowAsync = useCallback(async (
1432
+ workflowId: string,
1433
+ nodes: any[],
1434
+ edges: any[],
1435
+ sessionId?: string
1436
+ ): Promise<any> => {
1437
+ try {
1438
+ const response = await sendRequest<any>('deploy_workflow', {
1439
+ workflow_id: workflowId,
1440
+ nodes: nodes.map(node => ({
1441
+ id: node.id,
1442
+ type: node.type || '',
1443
+ data: node.data || {}
1444
+ })),
1445
+ edges: edges.map(edge => ({
1446
+ id: edge.id,
1447
+ source: edge.source,
1448
+ target: edge.target,
1449
+ sourceHandle: edge.sourceHandle || undefined,
1450
+ targetHandle: edge.targetHandle || undefined
1451
+ })),
1452
+ session_id: sessionId || 'default'
1453
+ });
1454
+
1455
+ return response;
1456
+ } catch (error) {
1457
+ console.error('[WebSocket] Failed to start deployment:', error);
1458
+ throw error;
1459
+ }
1460
+ }, [sendRequest]);
1461
+
1462
+ const cancelDeploymentAsync = useCallback(async (workflowId?: string): Promise<any> => {
1463
+ try {
1464
+ const response = await sendRequest<any>('cancel_deployment', {
1465
+ workflow_id: workflowId
1466
+ });
1467
+
1468
+ // Reset deployment status only if the cancelled workflow matches current
1469
+ if (!workflowId || workflowId === deploymentStatus.workflow_id) {
1470
+ setDeploymentStatus(defaultDeploymentStatus);
1471
+ }
1472
+
1473
+ return response;
1474
+ } catch (error) {
1475
+ console.error('[WebSocket] Failed to cancel deployment:', error);
1476
+ throw error;
1477
+ }
1478
+ }, [sendRequest, deploymentStatus.workflow_id]);
1479
+
1480
+ const getDeploymentStatusAsync = useCallback(async (workflowId?: string): Promise<{ isRunning: boolean; activeRuns: number; settings?: any; workflow_id?: string }> => {
1481
+ try {
1482
+ const response = await sendRequest<any>('get_deployment_status', { workflow_id: workflowId });
1483
+ return {
1484
+ isRunning: response.is_running || false,
1485
+ activeRuns: response.active_runs || 0,
1486
+ settings: response.settings,
1487
+ workflow_id: response.workflow_id
1488
+ };
1489
+ } catch (error) {
1490
+ console.error('[WebSocket] Failed to get deployment status:', error);
1491
+ return { isRunning: false, activeRuns: 0 };
1492
+ }
1493
+ }, [sendRequest]);
1494
+
1495
+ // =========================================================================
1496
+ // AI Operations
1497
+ // =========================================================================
1498
+
1499
+ const executeAiNodeAsync = useCallback(async (
1500
+ nodeId: string,
1501
+ nodeType: string,
1502
+ parameters: Record<string, any>,
1503
+ model: string,
1504
+ workflowId: string,
1505
+ nodes: any[],
1506
+ edges: any[]
1507
+ ): Promise<any> => {
1508
+ try {
1509
+ const response = await sendRequest<any>('execute_ai_node', {
1510
+ node_id: nodeId,
1511
+ node_type: nodeType,
1512
+ parameters,
1513
+ model,
1514
+ workflow_id: workflowId,
1515
+ nodes,
1516
+ edges
1517
+ });
1518
+ return response;
1519
+ } catch (error) {
1520
+ console.error('[WebSocket] Failed to execute AI node:', error);
1521
+ throw error;
1522
+ }
1523
+ }, [sendRequest]);
1524
+
1525
+ const getAiModelsAsync = useCallback(async (provider: string, apiKey: string): Promise<string[]> => {
1526
+ try {
1527
+ const response = await sendRequest<any>('get_ai_models', {
1528
+ provider,
1529
+ api_key: apiKey
1530
+ });
1531
+ return response.models || [];
1532
+ } catch (error) {
1533
+ console.error('[WebSocket] Failed to get AI models:', error);
1534
+ return [];
1535
+ }
1536
+ }, [sendRequest]);
1537
+
1538
+ // =========================================================================
1539
+ // API Key Operations
1540
+ // =========================================================================
1541
+
1542
+ const validateApiKeyAsync = useCallback(async (
1543
+ provider: string,
1544
+ apiKey: string
1545
+ ): Promise<{ valid: boolean; message?: string; models?: string[] }> => {
1546
+ try {
1547
+ const response = await sendRequest<any>('validate_api_key', {
1548
+ provider,
1549
+ api_key: apiKey
1550
+ });
1551
+ const result = {
1552
+ valid: response.valid || false,
1553
+ message: response.message,
1554
+ models: response.models
1555
+ };
1556
+
1557
+ // Update apiKeyStatuses on successful validation
1558
+ if (result.valid) {
1559
+ setApiKeyStatuses(prev => ({
1560
+ ...prev,
1561
+ [provider]: { hasKey: true, valid: true, models: result.models }
1562
+ }));
1563
+ }
1564
+
1565
+ return result;
1566
+ } catch (error) {
1567
+ console.error('[WebSocket] Failed to validate API key:', error);
1568
+ return { valid: false, message: 'Validation failed' };
1569
+ }
1570
+ }, [sendRequest]);
1571
+
1572
+ const getStoredApiKeyAsync = useCallback(async (
1573
+ provider: string
1574
+ ): Promise<{ hasKey: boolean; apiKey?: string; models?: string[] }> => {
1575
+ try {
1576
+ const response = await sendRequest<any>('get_stored_api_key', { provider });
1577
+ const result = {
1578
+ hasKey: response.has_key || false,
1579
+ apiKey: response.api_key,
1580
+ models: response.models
1581
+ };
1582
+
1583
+ // Update apiKeyStatuses with stored models
1584
+ if (result.hasKey) {
1585
+ setApiKeyStatuses(prev => ({
1586
+ ...prev,
1587
+ [provider]: { hasKey: true, valid: true, models: result.models }
1588
+ }));
1589
+ }
1590
+
1591
+ return result;
1592
+ } catch (error) {
1593
+ console.error('[WebSocket] Failed to get stored API key:', error);
1594
+ return { hasKey: false };
1595
+ }
1596
+ }, [sendRequest]);
1597
+
1598
+ const saveApiKeyAsync = useCallback(async (
1599
+ provider: string,
1600
+ apiKey: string,
1601
+ models?: string[]
1602
+ ): Promise<boolean> => {
1603
+ try {
1604
+ const response = await sendRequest<any>('save_api_key', {
1605
+ provider,
1606
+ api_key: apiKey,
1607
+ models
1608
+ });
1609
+ const success = response.success !== false;
1610
+
1611
+ // Update apiKeyStatuses on successful save
1612
+ if (success) {
1613
+ setApiKeyStatuses(prev => ({
1614
+ ...prev,
1615
+ [provider]: { hasKey: true, valid: true, models }
1616
+ }));
1617
+ }
1618
+
1619
+ return success;
1620
+ } catch (error) {
1621
+ console.error('[WebSocket] Failed to save API key:', error);
1622
+ return false;
1623
+ }
1624
+ }, [sendRequest]);
1625
+
1626
+ const deleteApiKeyAsync = useCallback(async (provider: string): Promise<boolean> => {
1627
+ try {
1628
+ await sendRequest<any>('delete_api_key', { provider });
1629
+
1630
+ // Remove from apiKeyStatuses on successful delete
1631
+ setApiKeyStatuses(prev => {
1632
+ const newStatuses = { ...prev };
1633
+ delete newStatuses[provider];
1634
+ return newStatuses;
1635
+ });
1636
+
1637
+ return true;
1638
+ } catch (error) {
1639
+ console.error('[WebSocket] Failed to delete API key:', error);
1640
+ return false;
1641
+ }
1642
+ }, [sendRequest]);
1643
+
1644
+ // =========================================================================
1645
+ // Android Operations
1646
+ // =========================================================================
1647
+
1648
+ const getAndroidDevicesAsync = useCallback(async (): Promise<string[]> => {
1649
+ try {
1650
+ const response = await sendRequest<any>('get_android_devices', {});
1651
+ return response.devices || [];
1652
+ } catch (error) {
1653
+ console.error('[WebSocket] Failed to get Android devices:', error);
1654
+ return [];
1655
+ }
1656
+ }, [sendRequest]);
1657
+
1658
+ const executeAndroidActionAsync = useCallback(async (
1659
+ serviceId: string,
1660
+ action: string,
1661
+ parameters: Record<string, any>,
1662
+ deviceId?: string
1663
+ ): Promise<any> => {
1664
+ try {
1665
+ const response = await sendRequest<any>('execute_android_action', {
1666
+ service_id: serviceId,
1667
+ action,
1668
+ parameters,
1669
+ device_id: deviceId
1670
+ });
1671
+ return response;
1672
+ } catch (error) {
1673
+ console.error('[WebSocket] Failed to execute Android action:', error);
1674
+ throw error;
1675
+ }
1676
+ }, [sendRequest]);
1677
+
1678
+ // =========================================================================
1679
+ // Maps Operations
1680
+ // =========================================================================
1681
+
1682
+ const validateMapsKeyAsync = useCallback(async (
1683
+ apiKey: string
1684
+ ): Promise<{ valid: boolean; message?: string }> => {
1685
+ try {
1686
+ const response = await sendRequest<any>('validate_maps_key', { api_key: apiKey });
1687
+ return {
1688
+ valid: response.valid || false,
1689
+ message: response.message
1690
+ };
1691
+ } catch (error) {
1692
+ console.error('[WebSocket] Failed to validate Maps key:', error);
1693
+ return { valid: false, message: 'Validation failed' };
1694
+ }
1695
+ }, [sendRequest]);
1696
+
1697
+ // =========================================================================
1698
+ // WhatsApp Operations
1699
+ // =========================================================================
1700
+
1701
+ const getWhatsAppStatusAsync = useCallback(async (): Promise<{ connected: boolean; deviceId?: string; data?: any }> => {
1702
+ try {
1703
+ const response = await sendRequest<any>('whatsapp_status', {});
1704
+ return {
1705
+ connected: response.connected || false,
1706
+ deviceId: response.device_id,
1707
+ data: response.data
1708
+ };
1709
+ } catch (error) {
1710
+ console.error('[WebSocket] Failed to get WhatsApp status:', error);
1711
+ return { connected: false };
1712
+ }
1713
+ }, [sendRequest]);
1714
+
1715
+ const getWhatsAppQRAsync = useCallback(async (): Promise<{ connected: boolean; qr?: string; message?: string }> => {
1716
+ try {
1717
+ const response = await sendRequest<any>('whatsapp_qr', {});
1718
+ return {
1719
+ connected: response.connected || false,
1720
+ qr: response.qr,
1721
+ message: response.message
1722
+ };
1723
+ } catch (error) {
1724
+ console.error('[WebSocket] Failed to get WhatsApp QR:', error);
1725
+ return { connected: false, message: 'Failed to get QR code' };
1726
+ }
1727
+ }, [sendRequest]);
1728
+
1729
+ const sendWhatsAppMessageAsync = useCallback(async (
1730
+ phone: string,
1731
+ message: string
1732
+ ): Promise<{ success: boolean; messageId?: string; error?: string }> => {
1733
+ try {
1734
+ const response = await sendRequest<any>('whatsapp_send', { phone, message });
1735
+ return {
1736
+ success: response.success || false,
1737
+ messageId: response.messageId,
1738
+ error: response.error
1739
+ };
1740
+ } catch (error: any) {
1741
+ console.error('[WebSocket] Failed to send WhatsApp message:', error);
1742
+ return { success: false, error: error.message || 'Send failed' };
1743
+ }
1744
+ }, [sendRequest]);
1745
+
1746
+ const startWhatsAppConnectionAsync = useCallback(async (): Promise<{ success: boolean; message?: string }> => {
1747
+ try {
1748
+ const response = await sendRequest<any>('whatsapp_start', {});
1749
+ return {
1750
+ success: response.success !== false,
1751
+ message: response.message
1752
+ };
1753
+ } catch (error: any) {
1754
+ console.error('[WebSocket] Failed to start WhatsApp connection:', error);
1755
+ return { success: false, message: error.message || 'Failed to start' };
1756
+ }
1757
+ }, [sendRequest]);
1758
+
1759
+ const restartWhatsAppConnectionAsync = useCallback(async (): Promise<{ success: boolean; message?: string }> => {
1760
+ try {
1761
+ const response = await sendRequest<any>('whatsapp_restart', {});
1762
+ return {
1763
+ success: response.success !== false,
1764
+ message: response.message
1765
+ };
1766
+ } catch (error: any) {
1767
+ console.error('[WebSocket] Failed to restart WhatsApp connection:', error);
1768
+ return { success: false, message: error.message || 'Failed to restart' };
1769
+ }
1770
+ }, [sendRequest]);
1771
+
1772
+ const getWhatsAppGroupsAsync = useCallback(async (): Promise<{ success: boolean; groups: Array<{ jid: string; name: string; topic?: string; size?: number; is_community?: boolean }>; error?: string }> => {
1773
+ try {
1774
+ const response = await sendRequest<any>('whatsapp_groups', {});
1775
+ return {
1776
+ success: response.success !== false,
1777
+ groups: response.groups || [],
1778
+ error: response.error
1779
+ };
1780
+ } catch (error: any) {
1781
+ console.error('[WebSocket] Failed to get WhatsApp groups:', error);
1782
+ return { success: false, groups: [], error: error.message || 'Failed to get groups' };
1783
+ }
1784
+ }, [sendRequest]);
1785
+
1786
+ const getWhatsAppGroupInfoAsync = useCallback(async (groupId: string): Promise<{ success: boolean; participants: Array<{ phone: string; name: string; jid: string; is_admin?: boolean }>; name?: string; error?: string }> => {
1787
+ try {
1788
+ const response = await sendRequest<any>('whatsapp_group_info', { group_id: groupId });
1789
+ return {
1790
+ success: response.success !== false,
1791
+ participants: response.participants || [],
1792
+ name: response.name,
1793
+ error: response.error
1794
+ };
1795
+ } catch (error: any) {
1796
+ console.error('[WebSocket] Failed to get WhatsApp group info:', error);
1797
+ return { success: false, participants: [], error: error.message || 'Failed to get group info' };
1798
+ }
1799
+ }, [sendRequest]);
1800
+
1801
+ const getWhatsAppRateLimitConfigAsync = useCallback(async (): Promise<{ success: boolean; config?: RateLimitConfig; stats?: RateLimitStats; error?: string }> => {
1802
+ try {
1803
+ const response = await sendRequest<any>('whatsapp_rate_limit_get', {});
1804
+ return {
1805
+ success: response.success !== false,
1806
+ config: response.config,
1807
+ stats: response.stats,
1808
+ error: response.error
1809
+ };
1810
+ } catch (error: any) {
1811
+ console.error('[WebSocket] Failed to get WhatsApp rate limit config:', error);
1812
+ return { success: false, error: error.message || 'Failed to get rate limit config' };
1813
+ }
1814
+ }, [sendRequest]);
1815
+
1816
+ const setWhatsAppRateLimitConfigAsync = useCallback(async (config: Partial<RateLimitConfig>): Promise<{ success: boolean; config?: RateLimitConfig; error?: string }> => {
1817
+ try {
1818
+ const response = await sendRequest<any>('whatsapp_rate_limit_set', { config });
1819
+ return {
1820
+ success: response.success !== false,
1821
+ config: response.config,
1822
+ error: response.error
1823
+ };
1824
+ } catch (error: any) {
1825
+ console.error('[WebSocket] Failed to set WhatsApp rate limit config:', error);
1826
+ return { success: false, error: error.message || 'Failed to set rate limit config' };
1827
+ }
1828
+ }, [sendRequest]);
1829
+
1830
+ const getWhatsAppRateLimitStatsAsync = useCallback(async (): Promise<{ success: boolean; stats?: RateLimitStats; error?: string }> => {
1831
+ try {
1832
+ const response = await sendRequest<any>('whatsapp_rate_limit_stats', {});
1833
+ return {
1834
+ success: response.success !== false,
1835
+ stats: response.stats || response,
1836
+ error: response.error
1837
+ };
1838
+ } catch (error: any) {
1839
+ console.error('[WebSocket] Failed to get WhatsApp rate limit stats:', error);
1840
+ return { success: false, error: error.message || 'Failed to get rate limit stats' };
1841
+ }
1842
+ }, [sendRequest]);
1843
+
1844
+ const unpauseWhatsAppRateLimitAsync = useCallback(async (): Promise<{ success: boolean; stats?: RateLimitStats; error?: string }> => {
1845
+ try {
1846
+ const response = await sendRequest<any>('whatsapp_rate_limit_unpause', {});
1847
+ return {
1848
+ success: response.success !== false,
1849
+ stats: response.stats,
1850
+ error: response.error
1851
+ };
1852
+ } catch (error: any) {
1853
+ console.error('[WebSocket] Failed to unpause WhatsApp rate limit:', error);
1854
+ return { success: false, error: error.message || 'Failed to unpause rate limit' };
1855
+ }
1856
+ }, [sendRequest]);
1857
+
1858
+ // =========================================================================
1859
+ // Memory and Skill Operations
1860
+ // =========================================================================
1861
+
1862
+ const clearMemoryAsync = useCallback(async (
1863
+ sessionId: string,
1864
+ clearLongTerm = false
1865
+ ): Promise<{ success: boolean; default_content?: string; cleared_vector_store?: boolean; error?: string }> => {
1866
+ try {
1867
+ const response = await sendRequest<any>('clear_memory', {
1868
+ session_id: sessionId,
1869
+ clear_long_term: clearLongTerm
1870
+ });
1871
+ return {
1872
+ success: response.success !== false,
1873
+ default_content: response.default_content,
1874
+ cleared_vector_store: response.cleared_vector_store,
1875
+ error: response.error
1876
+ };
1877
+ } catch (error: any) {
1878
+ console.error('[WebSocket] Failed to clear memory:', error);
1879
+ return { success: false, error: error.message || 'Failed to clear memory' };
1880
+ }
1881
+ }, [sendRequest]);
1882
+
1883
+ const resetSkillAsync = useCallback(async (
1884
+ skillName: string
1885
+ ): Promise<{ success: boolean; original_content?: string; is_builtin?: boolean; error?: string }> => {
1886
+ try {
1887
+ const response = await sendRequest<any>('reset_skill', {
1888
+ skill_name: skillName
1889
+ });
1890
+ return {
1891
+ success: response.success !== false,
1892
+ original_content: response.original_content,
1893
+ is_builtin: response.is_builtin,
1894
+ error: response.error
1895
+ };
1896
+ } catch (error: any) {
1897
+ console.error('[WebSocket] Failed to reset skill:', error);
1898
+ return { success: false, error: error.message || 'Failed to reset skill' };
1899
+ }
1900
+ }, [sendRequest]);
1901
+
1902
+ // Track if component is mounted to prevent state updates after unmount
1903
+ const isMountedRef = useRef(true);
1904
+
1905
+ // Connect only when authenticated (not during auth loading)
1906
+ useEffect(() => {
1907
+ isMountedRef.current = true;
1908
+
1909
+ // Don't connect if still loading auth or not authenticated
1910
+ if (authLoading || !isAuthenticated) {
1911
+ return;
1912
+ }
1913
+
1914
+ // Skip if already connected
1915
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
1916
+ return;
1917
+ }
1918
+
1919
+ // Small delay to avoid React Strict Mode double-connection issues
1920
+ const connectTimeout = setTimeout(() => {
1921
+ if (isMountedRef.current && isAuthenticated && !wsRef.current) {
1922
+ connect();
1923
+ }
1924
+ }, 100);
1925
+
1926
+ return () => {
1927
+ clearTimeout(connectTimeout);
1928
+ };
1929
+ }, [connect, isAuthenticated, authLoading]);
1930
+
1931
+ // Handle logout - separate effect to avoid reconnect loops
1932
+ useEffect(() => {
1933
+ if (!isAuthenticated && wsRef.current) {
1934
+ wsRef.current.close(1000, 'User logged out');
1935
+ wsRef.current = null;
1936
+ setIsConnected(false);
1937
+ }
1938
+ }, [isAuthenticated]);
1939
+
1940
+ // Cleanup on unmount only
1941
+ useEffect(() => {
1942
+ return () => {
1943
+ isMountedRef.current = false;
1944
+ if (reconnectTimeoutRef.current) clearTimeout(reconnectTimeoutRef.current);
1945
+ if (pingIntervalRef.current) clearInterval(pingIntervalRef.current);
1946
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
1947
+ wsRef.current.close(1000, 'Component unmounted');
1948
+ }
1949
+ };
1950
+ }, []);
1951
+
1952
+ const value: WebSocketContextValue = {
1953
+ // Connection state
1954
+ isConnected,
1955
+ reconnecting,
1956
+
1957
+ // Status data
1958
+ androidStatus,
1959
+ whatsappStatus,
1960
+ whatsappMessages,
1961
+ lastWhatsAppMessage,
1962
+ apiKeyStatuses,
1963
+ consoleLogs,
1964
+ terminalLogs,
1965
+ chatMessages,
1966
+ nodeStatuses,
1967
+ nodeParameters,
1968
+ variables,
1969
+ workflowStatus,
1970
+ deploymentStatus,
1971
+ workflowLock,
1972
+
1973
+ // Status getters
1974
+ getNodeStatus,
1975
+ getApiKeyStatus,
1976
+ getVariable,
1977
+ requestStatus,
1978
+ clearNodeStatus,
1979
+ clearWhatsAppMessages,
1980
+ clearConsoleLogs,
1981
+ clearTerminalLogs,
1982
+ clearChatMessages,
1983
+ sendChatMessage: sendChatMessageAsync,
1984
+
1985
+ // Generic request method
1986
+ sendRequest,
1987
+
1988
+ // Node Parameters
1989
+ getNodeParameters: getNodeParametersAsync,
1990
+ getAllNodeParameters: getAllNodeParametersAsync,
1991
+ saveNodeParameters: saveNodeParametersAsync,
1992
+ deleteNodeParameters: deleteNodeParametersAsync,
1993
+
1994
+ // Node Execution
1995
+ executeNode: executeNodeAsync,
1996
+ executeWorkflow: executeWorkflowAsync,
1997
+ getNodeOutput: getNodeOutputAsync,
1998
+
1999
+ // Trigger/Event Waiting
2000
+ cancelEventWait: cancelEventWaitAsync,
2001
+
2002
+ // Deployment Operations
2003
+ deployWorkflow: deployWorkflowAsync,
2004
+ cancelDeployment: cancelDeploymentAsync,
2005
+ getDeploymentStatus: getDeploymentStatusAsync,
2006
+
2007
+ // AI Operations
2008
+ executeAiNode: executeAiNodeAsync,
2009
+ getAiModels: getAiModelsAsync,
2010
+
2011
+ // API Key Operations
2012
+ validateApiKey: validateApiKeyAsync,
2013
+ getStoredApiKey: getStoredApiKeyAsync,
2014
+ saveApiKey: saveApiKeyAsync,
2015
+ deleteApiKey: deleteApiKeyAsync,
2016
+
2017
+ // Android Operations
2018
+ getAndroidDevices: getAndroidDevicesAsync,
2019
+ executeAndroidAction: executeAndroidActionAsync,
2020
+ // Maps Operations
2021
+ validateMapsKey: validateMapsKeyAsync,
2022
+
2023
+ // WhatsApp Operations
2024
+ getWhatsAppStatus: getWhatsAppStatusAsync,
2025
+ getWhatsAppQR: getWhatsAppQRAsync,
2026
+ sendWhatsAppMessage: sendWhatsAppMessageAsync,
2027
+ startWhatsAppConnection: startWhatsAppConnectionAsync,
2028
+ restartWhatsAppConnection: restartWhatsAppConnectionAsync,
2029
+ getWhatsAppGroups: getWhatsAppGroupsAsync,
2030
+ getWhatsAppGroupInfo: getWhatsAppGroupInfoAsync,
2031
+ getWhatsAppRateLimitConfig: getWhatsAppRateLimitConfigAsync,
2032
+ setWhatsAppRateLimitConfig: setWhatsAppRateLimitConfigAsync,
2033
+ getWhatsAppRateLimitStats: getWhatsAppRateLimitStatsAsync,
2034
+ unpauseWhatsAppRateLimit: unpauseWhatsAppRateLimitAsync,
2035
+
2036
+ // Memory and Skill Operations
2037
+ clearMemory: clearMemoryAsync,
2038
+ resetSkill: resetSkillAsync
2039
+ };
2040
+
2041
+ return (
2042
+ <WebSocketContext.Provider value={value}>
2043
+ {children}
2044
+ </WebSocketContext.Provider>
2045
+ );
2046
+ };
2047
+
2048
+ // Hook to use WebSocket context
2049
+ export const useWebSocket = (): WebSocketContextValue => {
2050
+ const context = useContext(WebSocketContext);
2051
+ if (!context) {
2052
+ throw new Error('useWebSocket must be used within a WebSocketProvider');
2053
+ }
2054
+ return context;
2055
+ };
2056
+
2057
+ // Hook specifically for Android status
2058
+ export const useAndroidStatus = (): AndroidStatus & { isConnected: boolean } => {
2059
+ const { androidStatus, isConnected } = useWebSocket();
2060
+ return {
2061
+ ...androidStatus,
2062
+ isConnected
2063
+ };
2064
+ };
2065
+
2066
+ // Hook specifically for node status
2067
+ export const useNodeStatus = (nodeId: string): NodeStatus | undefined => {
2068
+ const { getNodeStatus } = useWebSocket();
2069
+ return getNodeStatus(nodeId);
2070
+ };
2071
+
2072
+ // Hook specifically for workflow status
2073
+ export const useWorkflowStatus = (): WorkflowStatus => {
2074
+ const { workflowStatus } = useWebSocket();
2075
+ return workflowStatus;
2076
+ };
2077
+
2078
+ // Hook specifically for API key status
2079
+ export const useApiKeyStatus = (provider: string): ApiKeyStatus | undefined => {
2080
+ const { getApiKeyStatus } = useWebSocket();
2081
+ return getApiKeyStatus(provider);
2082
+ };
2083
+
2084
+ // Hook specifically for WhatsApp status
2085
+ export const useWhatsAppStatus = (): WhatsAppStatus => {
2086
+ const { whatsappStatus } = useWebSocket();
2087
+ return whatsappStatus;
2088
+ };
2089
+
2090
+ // Hook specifically for deployment status
2091
+ export const useDeploymentStatus = (): DeploymentStatus => {
2092
+ const { deploymentStatus } = useWebSocket();
2093
+ return deploymentStatus;
2094
+ };
2095
+
2096
+ // Hook specifically for workflow lock status
2097
+ export const useWorkflowLock = (): WorkflowLock => {
2098
+ const { workflowLock } = useWebSocket();
2099
+ return workflowLock;
2100
+ };
2101
+
2102
+ // Hook specifically for WhatsApp messages (for trigger nodes)
2103
+ export const useWhatsAppMessages = (): {
2104
+ messages: WhatsAppMessage[];
2105
+ lastMessage: WhatsAppMessage | null;
2106
+ clearMessages: () => void;
2107
+ } => {
2108
+ const { whatsappMessages, lastWhatsAppMessage, clearWhatsAppMessages } = useWebSocket();
2109
+ return {
2110
+ messages: whatsappMessages,
2111
+ lastMessage: lastWhatsAppMessage,
2112
+ clearMessages: clearWhatsAppMessages
2113
+ };
2114
+ };
2115
+
2116
+ // Hook to check if a tool is currently being executed by any AI Agent
2117
+ // Used by tool nodes to show spinning indicator when they're being used
2118
+ export const useIsToolExecuting = (toolName: string): boolean => {
2119
+ const { nodeStatuses } = useWebSocket();
2120
+
2121
+ // Debug: Log what we're checking
2122
+ if (toolName) {
2123
+ const statusCount = Object.keys(nodeStatuses).length;
2124
+ if (statusCount > 0) {
2125
+ console.log(`[useIsToolExecuting] Checking for tool '${toolName}', nodeStatuses count:`, statusCount, nodeStatuses);
2126
+ }
2127
+ }
2128
+
2129
+ // Scan all node statuses to find if any AI Agent is executing this tool
2130
+ // The status object contains phase and tool_name directly (not nested under data)
2131
+ for (const nodeId in nodeStatuses) {
2132
+ const status = nodeStatuses[nodeId] as Record<string, any>;
2133
+ if (status?.phase === 'executing_tool') {
2134
+ console.log(`[useIsToolExecuting] Found executing_tool phase for node ${nodeId}:`, status);
2135
+ if (status?.tool_name === toolName) {
2136
+ console.log(`[useIsToolExecuting] MATCH! Tool '${toolName}' is executing`);
2137
+ return true;
2138
+ }
2139
+ }
2140
+ }
2141
+ return false;
2142
+ };
2143
+
2144
+ export default WebSocketContext;