machinaos 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/.env.template +71 -0
  2. package/LICENSE +21 -0
  3. package/README.md +87 -0
  4. package/bin/cli.js +159 -0
  5. package/client/.dockerignore +45 -0
  6. package/client/Dockerfile +68 -0
  7. package/client/eslint.config.js +29 -0
  8. package/client/index.html +13 -0
  9. package/client/nginx.conf +66 -0
  10. package/client/package.json +48 -0
  11. package/client/src/App.tsx +27 -0
  12. package/client/src/Dashboard.tsx +1173 -0
  13. package/client/src/ParameterPanel.tsx +301 -0
  14. package/client/src/components/AIAgentNode.tsx +321 -0
  15. package/client/src/components/APIKeyValidator.tsx +118 -0
  16. package/client/src/components/ClaudeChatModelNode.tsx +18 -0
  17. package/client/src/components/ConditionalEdge.tsx +189 -0
  18. package/client/src/components/CredentialsModal.tsx +306 -0
  19. package/client/src/components/EdgeConditionEditor.tsx +443 -0
  20. package/client/src/components/GeminiChatModelNode.tsx +18 -0
  21. package/client/src/components/GenericNode.tsx +357 -0
  22. package/client/src/components/LocationParameterPanel.tsx +154 -0
  23. package/client/src/components/ModelNode.tsx +286 -0
  24. package/client/src/components/OpenAIChatModelNode.tsx +18 -0
  25. package/client/src/components/OutputPanel.tsx +471 -0
  26. package/client/src/components/ParameterRenderer.tsx +1874 -0
  27. package/client/src/components/SkillEditorModal.tsx +417 -0
  28. package/client/src/components/SquareNode.tsx +797 -0
  29. package/client/src/components/StartNode.tsx +250 -0
  30. package/client/src/components/ToolkitNode.tsx +365 -0
  31. package/client/src/components/TriggerNode.tsx +463 -0
  32. package/client/src/components/auth/LoginPage.tsx +247 -0
  33. package/client/src/components/auth/ProtectedRoute.tsx +59 -0
  34. package/client/src/components/base/BaseChatModelNode.tsx +271 -0
  35. package/client/src/components/icons/AIProviderIcons.tsx +50 -0
  36. package/client/src/components/maps/GoogleMapsPicker.tsx +137 -0
  37. package/client/src/components/maps/MapsPreviewPanel.tsx +110 -0
  38. package/client/src/components/maps/index.ts +26 -0
  39. package/client/src/components/parameterPanel/InputSection.tsx +1094 -0
  40. package/client/src/components/parameterPanel/LocationPanelLayout.tsx +65 -0
  41. package/client/src/components/parameterPanel/MapsSection.tsx +92 -0
  42. package/client/src/components/parameterPanel/MiddleSection.tsx +571 -0
  43. package/client/src/components/parameterPanel/OutputSection.tsx +81 -0
  44. package/client/src/components/parameterPanel/ParameterPanelLayout.tsx +82 -0
  45. package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +436 -0
  46. package/client/src/components/parameterPanel/index.ts +42 -0
  47. package/client/src/components/shared/DataPanel.tsx +142 -0
  48. package/client/src/components/shared/JSONTreeRenderer.tsx +106 -0
  49. package/client/src/components/ui/AIResultModal.tsx +204 -0
  50. package/client/src/components/ui/AndroidSettingsPanel.tsx +401 -0
  51. package/client/src/components/ui/CodeEditor.tsx +81 -0
  52. package/client/src/components/ui/CollapsibleSection.tsx +88 -0
  53. package/client/src/components/ui/ComponentItem.tsx +154 -0
  54. package/client/src/components/ui/ComponentPalette.tsx +321 -0
  55. package/client/src/components/ui/ConsolePanel.tsx +1074 -0
  56. package/client/src/components/ui/ErrorBoundary.tsx +196 -0
  57. package/client/src/components/ui/InputNodesPanel.tsx +204 -0
  58. package/client/src/components/ui/MapSelector.tsx +314 -0
  59. package/client/src/components/ui/Modal.tsx +149 -0
  60. package/client/src/components/ui/NodeContextMenu.tsx +192 -0
  61. package/client/src/components/ui/NodeOutputPanel.tsx +1150 -0
  62. package/client/src/components/ui/OutputDisplayPanel.tsx +381 -0
  63. package/client/src/components/ui/SettingsPanel.tsx +243 -0
  64. package/client/src/components/ui/TopToolbar.tsx +736 -0
  65. package/client/src/components/ui/WhatsAppSettingsPanel.tsx +345 -0
  66. package/client/src/components/ui/WorkflowSidebar.tsx +294 -0
  67. package/client/src/config/antdTheme.ts +186 -0
  68. package/client/src/config/api.ts +54 -0
  69. package/client/src/contexts/AuthContext.tsx +221 -0
  70. package/client/src/contexts/ThemeContext.tsx +42 -0
  71. package/client/src/contexts/WebSocketContext.tsx +1971 -0
  72. package/client/src/factories/baseChatModelFactory.ts +256 -0
  73. package/client/src/hooks/useAndroidOperations.ts +164 -0
  74. package/client/src/hooks/useApiKeyValidation.ts +107 -0
  75. package/client/src/hooks/useApiKeys.ts +238 -0
  76. package/client/src/hooks/useAppTheme.ts +17 -0
  77. package/client/src/hooks/useComponentPalette.ts +51 -0
  78. package/client/src/hooks/useCopyPaste.ts +155 -0
  79. package/client/src/hooks/useDragAndDrop.ts +124 -0
  80. package/client/src/hooks/useDragVariable.ts +88 -0
  81. package/client/src/hooks/useExecution.ts +313 -0
  82. package/client/src/hooks/useParameterPanel.ts +176 -0
  83. package/client/src/hooks/useReactFlowNodes.ts +189 -0
  84. package/client/src/hooks/useToolSchema.ts +209 -0
  85. package/client/src/hooks/useWhatsApp.ts +196 -0
  86. package/client/src/hooks/useWorkflowManagement.ts +46 -0
  87. package/client/src/index.css +315 -0
  88. package/client/src/main.tsx +19 -0
  89. package/client/src/nodeDefinitions/aiAgentNodes.ts +336 -0
  90. package/client/src/nodeDefinitions/aiModelNodes.ts +340 -0
  91. package/client/src/nodeDefinitions/androidDeviceNodes.ts +140 -0
  92. package/client/src/nodeDefinitions/androidServiceNodes.ts +383 -0
  93. package/client/src/nodeDefinitions/chatNodes.ts +135 -0
  94. package/client/src/nodeDefinitions/codeNodes.ts +54 -0
  95. package/client/src/nodeDefinitions/documentNodes.ts +379 -0
  96. package/client/src/nodeDefinitions/index.ts +15 -0
  97. package/client/src/nodeDefinitions/locationNodes.ts +463 -0
  98. package/client/src/nodeDefinitions/schedulerNodes.ts +220 -0
  99. package/client/src/nodeDefinitions/skillNodes.ts +211 -0
  100. package/client/src/nodeDefinitions/toolNodes.ts +198 -0
  101. package/client/src/nodeDefinitions/utilityNodes.ts +284 -0
  102. package/client/src/nodeDefinitions/whatsappNodes.ts +865 -0
  103. package/client/src/nodeDefinitions/workflowNodes.ts +41 -0
  104. package/client/src/nodeDefinitions.ts +104 -0
  105. package/client/src/schemas/workflowSchema.ts +264 -0
  106. package/client/src/services/dynamicParameterService.ts +96 -0
  107. package/client/src/services/execution/aiAgentExecutionService.ts +35 -0
  108. package/client/src/services/executionService.ts +232 -0
  109. package/client/src/services/workflowApi.ts +91 -0
  110. package/client/src/store/useAppStore.ts +582 -0
  111. package/client/src/styles/theme.ts +508 -0
  112. package/client/src/styles/zIndex.ts +17 -0
  113. package/client/src/types/ComponentTypes.ts +39 -0
  114. package/client/src/types/EdgeCondition.ts +231 -0
  115. package/client/src/types/INodeProperties.ts +288 -0
  116. package/client/src/types/NodeTypes.ts +28 -0
  117. package/client/src/utils/formatters.ts +33 -0
  118. package/client/src/utils/googleMapsLoader.ts +140 -0
  119. package/client/src/utils/locationUtils.ts +85 -0
  120. package/client/src/utils/nodeUtils.ts +31 -0
  121. package/client/src/utils/workflow.ts +30 -0
  122. package/client/src/utils/workflowExport.ts +120 -0
  123. package/client/src/vite-env.d.ts +12 -0
  124. package/client/tailwind.config.js +60 -0
  125. package/client/tsconfig.json +25 -0
  126. package/client/tsconfig.node.json +11 -0
  127. package/client/vite.config.js +35 -0
  128. package/docker-compose.prod.yml +107 -0
  129. package/docker-compose.yml +104 -0
  130. package/docs-MachinaOs/README.md +85 -0
  131. package/docs-MachinaOs/deployment/docker.mdx +228 -0
  132. package/docs-MachinaOs/deployment/production.mdx +345 -0
  133. package/docs-MachinaOs/docs.json +75 -0
  134. package/docs-MachinaOs/faq.mdx +309 -0
  135. package/docs-MachinaOs/favicon.svg +5 -0
  136. package/docs-MachinaOs/installation.mdx +160 -0
  137. package/docs-MachinaOs/introduction.mdx +114 -0
  138. package/docs-MachinaOs/logo/dark.svg +6 -0
  139. package/docs-MachinaOs/logo/light.svg +6 -0
  140. package/docs-MachinaOs/nodes/ai-agent.mdx +216 -0
  141. package/docs-MachinaOs/nodes/ai-models.mdx +240 -0
  142. package/docs-MachinaOs/nodes/android.mdx +411 -0
  143. package/docs-MachinaOs/nodes/overview.mdx +181 -0
  144. package/docs-MachinaOs/nodes/schedulers.mdx +316 -0
  145. package/docs-MachinaOs/nodes/webhooks.mdx +330 -0
  146. package/docs-MachinaOs/nodes/whatsapp.mdx +305 -0
  147. package/docs-MachinaOs/quickstart.mdx +119 -0
  148. package/docs-MachinaOs/tutorials/ai-agent-workflow.mdx +177 -0
  149. package/docs-MachinaOs/tutorials/android-automation.mdx +242 -0
  150. package/docs-MachinaOs/tutorials/first-workflow.mdx +134 -0
  151. package/docs-MachinaOs/tutorials/whatsapp-automation.mdx +185 -0
  152. package/nul +0 -0
  153. package/package.json +70 -0
  154. package/scripts/build.js +158 -0
  155. package/scripts/check-ports.ps1 +33 -0
  156. package/scripts/clean.js +40 -0
  157. package/scripts/docker.js +93 -0
  158. package/scripts/kill-port.ps1 +154 -0
  159. package/scripts/start.js +210 -0
  160. package/scripts/stop.js +325 -0
  161. package/server/.dockerignore +44 -0
  162. package/server/Dockerfile +45 -0
  163. package/server/constants.py +249 -0
  164. package/server/core/__init__.py +1 -0
  165. package/server/core/cache.py +461 -0
  166. package/server/core/config.py +128 -0
  167. package/server/core/container.py +99 -0
  168. package/server/core/database.py +1211 -0
  169. package/server/core/logging.py +314 -0
  170. package/server/main.py +289 -0
  171. package/server/middleware/__init__.py +5 -0
  172. package/server/middleware/auth.py +89 -0
  173. package/server/models/__init__.py +1 -0
  174. package/server/models/auth.py +52 -0
  175. package/server/models/cache.py +24 -0
  176. package/server/models/database.py +211 -0
  177. package/server/models/nodes.py +455 -0
  178. package/server/package.json +9 -0
  179. package/server/pyproject.toml +72 -0
  180. package/server/requirements.txt +83 -0
  181. package/server/routers/__init__.py +1 -0
  182. package/server/routers/android.py +294 -0
  183. package/server/routers/auth.py +203 -0
  184. package/server/routers/database.py +151 -0
  185. package/server/routers/maps.py +142 -0
  186. package/server/routers/nodejs_compat.py +289 -0
  187. package/server/routers/webhook.py +90 -0
  188. package/server/routers/websocket.py +2127 -0
  189. package/server/routers/whatsapp.py +761 -0
  190. package/server/routers/workflow.py +200 -0
  191. package/server/services/__init__.py +1 -0
  192. package/server/services/ai.py +2415 -0
  193. package/server/services/android/__init__.py +27 -0
  194. package/server/services/android/broadcaster.py +114 -0
  195. package/server/services/android/client.py +608 -0
  196. package/server/services/android/manager.py +78 -0
  197. package/server/services/android/protocol.py +165 -0
  198. package/server/services/android_service.py +588 -0
  199. package/server/services/auth.py +131 -0
  200. package/server/services/chat_client.py +160 -0
  201. package/server/services/deployment/__init__.py +12 -0
  202. package/server/services/deployment/manager.py +706 -0
  203. package/server/services/deployment/state.py +47 -0
  204. package/server/services/deployment/triggers.py +275 -0
  205. package/server/services/event_waiter.py +785 -0
  206. package/server/services/execution/__init__.py +77 -0
  207. package/server/services/execution/cache.py +769 -0
  208. package/server/services/execution/conditions.py +373 -0
  209. package/server/services/execution/dlq.py +132 -0
  210. package/server/services/execution/executor.py +1351 -0
  211. package/server/services/execution/models.py +531 -0
  212. package/server/services/execution/recovery.py +235 -0
  213. package/server/services/handlers/__init__.py +126 -0
  214. package/server/services/handlers/ai.py +355 -0
  215. package/server/services/handlers/android.py +260 -0
  216. package/server/services/handlers/code.py +278 -0
  217. package/server/services/handlers/document.py +598 -0
  218. package/server/services/handlers/http.py +193 -0
  219. package/server/services/handlers/polyglot.py +105 -0
  220. package/server/services/handlers/tools.py +845 -0
  221. package/server/services/handlers/triggers.py +107 -0
  222. package/server/services/handlers/utility.py +822 -0
  223. package/server/services/handlers/whatsapp.py +476 -0
  224. package/server/services/maps.py +289 -0
  225. package/server/services/memory_store.py +103 -0
  226. package/server/services/node_executor.py +375 -0
  227. package/server/services/parameter_resolver.py +218 -0
  228. package/server/services/polyglot_client.py +169 -0
  229. package/server/services/scheduler.py +155 -0
  230. package/server/services/skill_loader.py +417 -0
  231. package/server/services/status_broadcaster.py +826 -0
  232. package/server/services/temporal/__init__.py +23 -0
  233. package/server/services/temporal/activities.py +344 -0
  234. package/server/services/temporal/client.py +76 -0
  235. package/server/services/temporal/executor.py +147 -0
  236. package/server/services/temporal/worker.py +251 -0
  237. package/server/services/temporal/workflow.py +355 -0
  238. package/server/services/temporal/ws_client.py +236 -0
  239. package/server/services/text.py +111 -0
  240. package/server/services/user_auth.py +172 -0
  241. package/server/services/websocket_client.py +29 -0
  242. package/server/services/workflow.py +597 -0
  243. package/server/skills/android-skill/SKILL.md +82 -0
  244. package/server/skills/assistant-personality/SKILL.md +45 -0
  245. package/server/skills/code-skill/SKILL.md +140 -0
  246. package/server/skills/http-skill/SKILL.md +161 -0
  247. package/server/skills/maps-skill/SKILL.md +170 -0
  248. package/server/skills/memory-skill/SKILL.md +154 -0
  249. package/server/skills/scheduler-skill/SKILL.md +84 -0
  250. package/server/skills/whatsapp-skill/SKILL.md +283 -0
  251. package/server/uv.lock +2916 -0
  252. package/server/whatsapp-rpc/.dockerignore +30 -0
  253. package/server/whatsapp-rpc/Dockerfile +44 -0
  254. package/server/whatsapp-rpc/Dockerfile.web +17 -0
  255. package/server/whatsapp-rpc/README.md +139 -0
  256. package/server/whatsapp-rpc/cli.js +95 -0
  257. package/server/whatsapp-rpc/configs/config.yaml +7 -0
  258. package/server/whatsapp-rpc/docker-compose.yml +35 -0
  259. package/server/whatsapp-rpc/docs/API.md +410 -0
  260. package/server/whatsapp-rpc/go.mod +67 -0
  261. package/server/whatsapp-rpc/go.sum +203 -0
  262. package/server/whatsapp-rpc/package.json +30 -0
  263. package/server/whatsapp-rpc/schema.json +1294 -0
  264. package/server/whatsapp-rpc/scripts/clean.cjs +66 -0
  265. package/server/whatsapp-rpc/scripts/cli.js +162 -0
  266. package/server/whatsapp-rpc/src/go/cmd/server/main.go +91 -0
  267. package/server/whatsapp-rpc/src/go/config/config.go +49 -0
  268. package/server/whatsapp-rpc/src/go/rpc/rpc.go +446 -0
  269. package/server/whatsapp-rpc/src/go/rpc/server.go +112 -0
  270. package/server/whatsapp-rpc/src/go/whatsapp/history.go +166 -0
  271. package/server/whatsapp-rpc/src/go/whatsapp/messages.go +390 -0
  272. package/server/whatsapp-rpc/src/go/whatsapp/service.go +2130 -0
  273. package/server/whatsapp-rpc/src/go/whatsapp/types.go +261 -0
  274. package/server/whatsapp-rpc/src/python/pyproject.toml +15 -0
  275. package/server/whatsapp-rpc/src/python/whatsapp_rpc/__init__.py +4 -0
  276. package/server/whatsapp-rpc/src/python/whatsapp_rpc/client.py +427 -0
  277. package/server/whatsapp-rpc/web/app.py +609 -0
  278. package/server/whatsapp-rpc/web/requirements.txt +6 -0
  279. package/server/whatsapp-rpc/web/rpc_client.py +427 -0
  280. package/server/whatsapp-rpc/web/static/openapi.yaml +59 -0
  281. package/server/whatsapp-rpc/web/templates/base.html +150 -0
  282. package/server/whatsapp-rpc/web/templates/contacts.html +240 -0
  283. package/server/whatsapp-rpc/web/templates/dashboard.html +320 -0
  284. package/server/whatsapp-rpc/web/templates/groups.html +328 -0
  285. package/server/whatsapp-rpc/web/templates/messages.html +465 -0
  286. package/server/whatsapp-rpc/web/templates/messaging.html +681 -0
  287. package/server/whatsapp-rpc/web/templates/send.html +259 -0
  288. package/server/whatsapp-rpc/web/templates/settings.html +459 -0
@@ -0,0 +1,1971 @@
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;