machinaos 0.0.1 → 0.0.7

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 (422) hide show
  1. package/.env.template +71 -71
  2. package/LICENSE +21 -21
  3. package/README.md +163 -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/install.ps1 +308 -0
  114. package/install.sh +343 -0
  115. package/package.json +81 -70
  116. package/scripts/build.js +174 -51
  117. package/scripts/clean.js +40 -40
  118. package/scripts/start.js +234 -210
  119. package/scripts/stop.js +301 -325
  120. package/server/.dockerignore +44 -44
  121. package/server/Dockerfile +45 -45
  122. package/server/constants.py +244 -249
  123. package/server/core/cache.py +460 -460
  124. package/server/core/config.py +127 -127
  125. package/server/core/container.py +98 -98
  126. package/server/core/database.py +1296 -1210
  127. package/server/core/logging.py +313 -313
  128. package/server/main.py +288 -288
  129. package/server/middleware/__init__.py +5 -5
  130. package/server/middleware/auth.py +89 -89
  131. package/server/models/auth.py +52 -52
  132. package/server/models/cache.py +24 -24
  133. package/server/models/database.py +235 -210
  134. package/server/models/nodes.py +435 -455
  135. package/server/pyproject.toml +75 -72
  136. package/server/requirements.txt +83 -83
  137. package/server/routers/android.py +294 -294
  138. package/server/routers/auth.py +203 -203
  139. package/server/routers/database.py +150 -150
  140. package/server/routers/maps.py +141 -141
  141. package/server/routers/nodejs_compat.py +288 -288
  142. package/server/routers/webhook.py +90 -90
  143. package/server/routers/websocket.py +2239 -2127
  144. package/server/routers/whatsapp.py +761 -761
  145. package/server/routers/workflow.py +199 -199
  146. package/server/services/ai.py +2444 -2414
  147. package/server/services/android_service.py +588 -588
  148. package/server/services/auth.py +130 -130
  149. package/server/services/chat_client.py +160 -160
  150. package/server/services/deployment/manager.py +706 -706
  151. package/server/services/event_waiter.py +675 -785
  152. package/server/services/execution/executor.py +1351 -1351
  153. package/server/services/execution/models.py +1 -1
  154. package/server/services/handlers/__init__.py +122 -126
  155. package/server/services/handlers/ai.py +390 -355
  156. package/server/services/handlers/android.py +69 -260
  157. package/server/services/handlers/code.py +278 -278
  158. package/server/services/handlers/http.py +193 -193
  159. package/server/services/handlers/tools.py +146 -32
  160. package/server/services/handlers/triggers.py +107 -107
  161. package/server/services/handlers/utility.py +822 -822
  162. package/server/services/handlers/whatsapp.py +423 -476
  163. package/server/services/maps.py +288 -288
  164. package/server/services/memory_store.py +103 -103
  165. package/server/services/node_executor.py +372 -375
  166. package/server/services/scheduler.py +155 -155
  167. package/server/services/skill_loader.py +1 -1
  168. package/server/services/status_broadcaster.py +834 -826
  169. package/server/services/temporal/__init__.py +23 -23
  170. package/server/services/temporal/activities.py +344 -344
  171. package/server/services/temporal/client.py +76 -76
  172. package/server/services/temporal/executor.py +147 -147
  173. package/server/services/temporal/worker.py +251 -251
  174. package/server/services/temporal/workflow.py +355 -355
  175. package/server/services/temporal/ws_client.py +236 -236
  176. package/server/services/text.py +110 -110
  177. package/server/services/user_auth.py +172 -172
  178. package/server/services/websocket_client.py +29 -29
  179. package/server/services/workflow.py +597 -597
  180. package/server/skills/android-skill/SKILL.md +4 -4
  181. package/server/skills/code-skill/SKILL.md +123 -89
  182. package/server/skills/maps-skill/SKILL.md +3 -3
  183. package/server/skills/memory-skill/SKILL.md +1 -1
  184. package/server/skills/web-search-skill/SKILL.md +154 -0
  185. package/server/skills/whatsapp-skill/SKILL.md +3 -3
  186. package/server/uv.lock +461 -100
  187. package/server/whatsapp-rpc/.dockerignore +30 -30
  188. package/server/whatsapp-rpc/Dockerfile +44 -44
  189. package/server/whatsapp-rpc/Dockerfile.web +17 -17
  190. package/server/whatsapp-rpc/README.md +139 -139
  191. package/server/whatsapp-rpc/bin/whatsapp-rpc-server +0 -0
  192. package/server/whatsapp-rpc/cli.js +95 -95
  193. package/server/whatsapp-rpc/configs/config.yaml +6 -6
  194. package/server/whatsapp-rpc/docker-compose.yml +35 -35
  195. package/server/whatsapp-rpc/docs/API.md +410 -410
  196. package/server/whatsapp-rpc/node_modules/.package-lock.json +259 -0
  197. package/server/whatsapp-rpc/node_modules/chalk/license +9 -0
  198. package/server/whatsapp-rpc/node_modules/chalk/package.json +83 -0
  199. package/server/whatsapp-rpc/node_modules/chalk/readme.md +297 -0
  200. package/server/whatsapp-rpc/node_modules/chalk/source/index.d.ts +325 -0
  201. package/server/whatsapp-rpc/node_modules/chalk/source/index.js +225 -0
  202. package/server/whatsapp-rpc/node_modules/chalk/source/utilities.js +33 -0
  203. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  204. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  205. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  206. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  207. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  208. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  209. package/server/whatsapp-rpc/node_modules/commander/LICENSE +22 -0
  210. package/server/whatsapp-rpc/node_modules/commander/Readme.md +1148 -0
  211. package/server/whatsapp-rpc/node_modules/commander/esm.mjs +16 -0
  212. package/server/whatsapp-rpc/node_modules/commander/index.js +26 -0
  213. package/server/whatsapp-rpc/node_modules/commander/lib/argument.js +145 -0
  214. package/server/whatsapp-rpc/node_modules/commander/lib/command.js +2179 -0
  215. package/server/whatsapp-rpc/node_modules/commander/lib/error.js +43 -0
  216. package/server/whatsapp-rpc/node_modules/commander/lib/help.js +462 -0
  217. package/server/whatsapp-rpc/node_modules/commander/lib/option.js +329 -0
  218. package/server/whatsapp-rpc/node_modules/commander/lib/suggestSimilar.js +100 -0
  219. package/server/whatsapp-rpc/node_modules/commander/package-support.json +16 -0
  220. package/server/whatsapp-rpc/node_modules/commander/package.json +80 -0
  221. package/server/whatsapp-rpc/node_modules/commander/typings/esm.d.mts +3 -0
  222. package/server/whatsapp-rpc/node_modules/commander/typings/index.d.ts +884 -0
  223. package/server/whatsapp-rpc/node_modules/cross-spawn/LICENSE +21 -0
  224. package/server/whatsapp-rpc/node_modules/cross-spawn/README.md +89 -0
  225. package/server/whatsapp-rpc/node_modules/cross-spawn/index.js +39 -0
  226. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/enoent.js +59 -0
  227. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/parse.js +91 -0
  228. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/escape.js +47 -0
  229. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/readShebang.js +23 -0
  230. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/resolveCommand.js +52 -0
  231. package/server/whatsapp-rpc/node_modules/cross-spawn/package.json +73 -0
  232. package/server/whatsapp-rpc/node_modules/execa/index.d.ts +955 -0
  233. package/server/whatsapp-rpc/node_modules/execa/index.js +309 -0
  234. package/server/whatsapp-rpc/node_modules/execa/lib/command.js +119 -0
  235. package/server/whatsapp-rpc/node_modules/execa/lib/error.js +87 -0
  236. package/server/whatsapp-rpc/node_modules/execa/lib/kill.js +102 -0
  237. package/server/whatsapp-rpc/node_modules/execa/lib/pipe.js +42 -0
  238. package/server/whatsapp-rpc/node_modules/execa/lib/promise.js +36 -0
  239. package/server/whatsapp-rpc/node_modules/execa/lib/stdio.js +49 -0
  240. package/server/whatsapp-rpc/node_modules/execa/lib/stream.js +133 -0
  241. package/server/whatsapp-rpc/node_modules/execa/lib/verbose.js +19 -0
  242. package/server/whatsapp-rpc/node_modules/execa/license +9 -0
  243. package/server/whatsapp-rpc/node_modules/execa/package.json +90 -0
  244. package/server/whatsapp-rpc/node_modules/execa/readme.md +822 -0
  245. package/server/whatsapp-rpc/node_modules/get-stream/license +9 -0
  246. package/server/whatsapp-rpc/node_modules/get-stream/package.json +53 -0
  247. package/server/whatsapp-rpc/node_modules/get-stream/readme.md +291 -0
  248. package/server/whatsapp-rpc/node_modules/get-stream/source/array-buffer.js +84 -0
  249. package/server/whatsapp-rpc/node_modules/get-stream/source/array.js +32 -0
  250. package/server/whatsapp-rpc/node_modules/get-stream/source/buffer.js +20 -0
  251. package/server/whatsapp-rpc/node_modules/get-stream/source/contents.js +101 -0
  252. package/server/whatsapp-rpc/node_modules/get-stream/source/index.d.ts +119 -0
  253. package/server/whatsapp-rpc/node_modules/get-stream/source/index.js +5 -0
  254. package/server/whatsapp-rpc/node_modules/get-stream/source/string.js +36 -0
  255. package/server/whatsapp-rpc/node_modules/get-stream/source/utils.js +11 -0
  256. package/server/whatsapp-rpc/node_modules/get-them-args/LICENSE +21 -0
  257. package/server/whatsapp-rpc/node_modules/get-them-args/README.md +95 -0
  258. package/server/whatsapp-rpc/node_modules/get-them-args/index.js +97 -0
  259. package/server/whatsapp-rpc/node_modules/get-them-args/package.json +36 -0
  260. package/server/whatsapp-rpc/node_modules/human-signals/LICENSE +201 -0
  261. package/server/whatsapp-rpc/node_modules/human-signals/README.md +168 -0
  262. package/server/whatsapp-rpc/node_modules/human-signals/build/src/core.js +273 -0
  263. package/server/whatsapp-rpc/node_modules/human-signals/build/src/main.d.ts +73 -0
  264. package/server/whatsapp-rpc/node_modules/human-signals/build/src/main.js +70 -0
  265. package/server/whatsapp-rpc/node_modules/human-signals/build/src/realtime.js +16 -0
  266. package/server/whatsapp-rpc/node_modules/human-signals/build/src/signals.js +34 -0
  267. package/server/whatsapp-rpc/node_modules/human-signals/package.json +61 -0
  268. package/server/whatsapp-rpc/node_modules/is-stream/index.d.ts +81 -0
  269. package/server/whatsapp-rpc/node_modules/is-stream/index.js +29 -0
  270. package/server/whatsapp-rpc/node_modules/is-stream/license +9 -0
  271. package/server/whatsapp-rpc/node_modules/is-stream/package.json +44 -0
  272. package/server/whatsapp-rpc/node_modules/is-stream/readme.md +60 -0
  273. package/server/whatsapp-rpc/node_modules/isexe/LICENSE +15 -0
  274. package/server/whatsapp-rpc/node_modules/isexe/README.md +51 -0
  275. package/server/whatsapp-rpc/node_modules/isexe/index.js +57 -0
  276. package/server/whatsapp-rpc/node_modules/isexe/mode.js +41 -0
  277. package/server/whatsapp-rpc/node_modules/isexe/package.json +31 -0
  278. package/server/whatsapp-rpc/node_modules/isexe/test/basic.js +221 -0
  279. package/server/whatsapp-rpc/node_modules/isexe/windows.js +42 -0
  280. package/server/whatsapp-rpc/node_modules/kill-port/.editorconfig +12 -0
  281. package/server/whatsapp-rpc/node_modules/kill-port/.gitattributes +1 -0
  282. package/server/whatsapp-rpc/node_modules/kill-port/LICENSE +21 -0
  283. package/server/whatsapp-rpc/node_modules/kill-port/README.md +140 -0
  284. package/server/whatsapp-rpc/node_modules/kill-port/cli.js +25 -0
  285. package/server/whatsapp-rpc/node_modules/kill-port/example.js +21 -0
  286. package/server/whatsapp-rpc/node_modules/kill-port/index.js +46 -0
  287. package/server/whatsapp-rpc/node_modules/kill-port/logo.png +0 -0
  288. package/server/whatsapp-rpc/node_modules/kill-port/package.json +41 -0
  289. package/server/whatsapp-rpc/node_modules/kill-port/pnpm-lock.yaml +4606 -0
  290. package/server/whatsapp-rpc/node_modules/kill-port/test.js +16 -0
  291. package/server/whatsapp-rpc/node_modules/merge-stream/LICENSE +21 -0
  292. package/server/whatsapp-rpc/node_modules/merge-stream/README.md +78 -0
  293. package/server/whatsapp-rpc/node_modules/merge-stream/index.js +41 -0
  294. package/server/whatsapp-rpc/node_modules/merge-stream/package.json +19 -0
  295. package/server/whatsapp-rpc/node_modules/mimic-fn/index.d.ts +52 -0
  296. package/server/whatsapp-rpc/node_modules/mimic-fn/index.js +71 -0
  297. package/server/whatsapp-rpc/node_modules/mimic-fn/license +9 -0
  298. package/server/whatsapp-rpc/node_modules/mimic-fn/package.json +45 -0
  299. package/server/whatsapp-rpc/node_modules/mimic-fn/readme.md +90 -0
  300. package/server/whatsapp-rpc/node_modules/npm-run-path/index.d.ts +90 -0
  301. package/server/whatsapp-rpc/node_modules/npm-run-path/index.js +52 -0
  302. package/server/whatsapp-rpc/node_modules/npm-run-path/license +9 -0
  303. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/index.d.ts +31 -0
  304. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/index.js +12 -0
  305. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/license +9 -0
  306. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/package.json +41 -0
  307. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/readme.md +57 -0
  308. package/server/whatsapp-rpc/node_modules/npm-run-path/package.json +49 -0
  309. package/server/whatsapp-rpc/node_modules/npm-run-path/readme.md +104 -0
  310. package/server/whatsapp-rpc/node_modules/onetime/index.d.ts +59 -0
  311. package/server/whatsapp-rpc/node_modules/onetime/index.js +41 -0
  312. package/server/whatsapp-rpc/node_modules/onetime/license +9 -0
  313. package/server/whatsapp-rpc/node_modules/onetime/package.json +45 -0
  314. package/server/whatsapp-rpc/node_modules/onetime/readme.md +94 -0
  315. package/server/whatsapp-rpc/node_modules/path-key/index.d.ts +40 -0
  316. package/server/whatsapp-rpc/node_modules/path-key/index.js +16 -0
  317. package/server/whatsapp-rpc/node_modules/path-key/license +9 -0
  318. package/server/whatsapp-rpc/node_modules/path-key/package.json +39 -0
  319. package/server/whatsapp-rpc/node_modules/path-key/readme.md +61 -0
  320. package/server/whatsapp-rpc/node_modules/shebang-command/index.js +19 -0
  321. package/server/whatsapp-rpc/node_modules/shebang-command/license +9 -0
  322. package/server/whatsapp-rpc/node_modules/shebang-command/package.json +34 -0
  323. package/server/whatsapp-rpc/node_modules/shebang-command/readme.md +34 -0
  324. package/server/whatsapp-rpc/node_modules/shebang-regex/index.d.ts +22 -0
  325. package/server/whatsapp-rpc/node_modules/shebang-regex/index.js +2 -0
  326. package/server/whatsapp-rpc/node_modules/shebang-regex/license +9 -0
  327. package/server/whatsapp-rpc/node_modules/shebang-regex/package.json +35 -0
  328. package/server/whatsapp-rpc/node_modules/shebang-regex/readme.md +33 -0
  329. package/server/whatsapp-rpc/node_modules/shell-exec/LICENSE +21 -0
  330. package/server/whatsapp-rpc/node_modules/shell-exec/README.md +60 -0
  331. package/server/whatsapp-rpc/node_modules/shell-exec/index.js +47 -0
  332. package/server/whatsapp-rpc/node_modules/shell-exec/package.json +29 -0
  333. package/server/whatsapp-rpc/node_modules/signal-exit/LICENSE.txt +16 -0
  334. package/server/whatsapp-rpc/node_modules/signal-exit/README.md +74 -0
  335. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.d.ts +12 -0
  336. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.d.ts.map +1 -0
  337. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.js +10 -0
  338. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.js.map +1 -0
  339. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.d.ts +48 -0
  340. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.d.ts.map +1 -0
  341. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.js +279 -0
  342. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.js.map +1 -0
  343. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/package.json +3 -0
  344. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.d.ts +29 -0
  345. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.d.ts.map +1 -0
  346. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.js +42 -0
  347. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.js.map +1 -0
  348. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.d.ts +12 -0
  349. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.d.ts.map +1 -0
  350. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.js +4 -0
  351. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.js.map +1 -0
  352. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.d.ts +48 -0
  353. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.d.ts.map +1 -0
  354. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.js +275 -0
  355. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.js.map +1 -0
  356. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/package.json +3 -0
  357. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.d.ts +29 -0
  358. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.d.ts.map +1 -0
  359. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.js +39 -0
  360. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.js.map +1 -0
  361. package/server/whatsapp-rpc/node_modules/signal-exit/package.json +106 -0
  362. package/server/whatsapp-rpc/node_modules/strip-final-newline/index.js +14 -0
  363. package/server/whatsapp-rpc/node_modules/strip-final-newline/license +9 -0
  364. package/server/whatsapp-rpc/node_modules/strip-final-newline/package.json +43 -0
  365. package/server/whatsapp-rpc/node_modules/strip-final-newline/readme.md +35 -0
  366. package/server/whatsapp-rpc/node_modules/which/CHANGELOG.md +166 -0
  367. package/server/whatsapp-rpc/node_modules/which/LICENSE +15 -0
  368. package/server/whatsapp-rpc/node_modules/which/README.md +54 -0
  369. package/server/whatsapp-rpc/node_modules/which/bin/node-which +52 -0
  370. package/server/whatsapp-rpc/node_modules/which/package.json +43 -0
  371. package/server/whatsapp-rpc/node_modules/which/which.js +125 -0
  372. package/server/whatsapp-rpc/package-lock.json +272 -0
  373. package/server/whatsapp-rpc/package.json +30 -30
  374. package/server/whatsapp-rpc/schema.json +1294 -1294
  375. package/server/whatsapp-rpc/scripts/clean.cjs +66 -66
  376. package/server/whatsapp-rpc/scripts/cli.js +162 -162
  377. package/server/whatsapp-rpc/src/go/whatsapp/history.go +166 -166
  378. package/server/whatsapp-rpc/src/python/pyproject.toml +15 -15
  379. package/server/whatsapp-rpc/src/python/whatsapp_rpc/__init__.py +4 -4
  380. package/server/whatsapp-rpc/src/python/whatsapp_rpc/client.py +427 -427
  381. package/server/whatsapp-rpc/web/app.py +609 -609
  382. package/server/whatsapp-rpc/web/requirements.txt +6 -6
  383. package/server/whatsapp-rpc/web/rpc_client.py +427 -427
  384. package/server/whatsapp-rpc/web/static/openapi.yaml +59 -59
  385. package/server/whatsapp-rpc/web/templates/base.html +149 -149
  386. package/server/whatsapp-rpc/web/templates/contacts.html +240 -240
  387. package/server/whatsapp-rpc/web/templates/dashboard.html +319 -319
  388. package/server/whatsapp-rpc/web/templates/groups.html +328 -328
  389. package/server/whatsapp-rpc/web/templates/messages.html +465 -465
  390. package/server/whatsapp-rpc/web/templates/messaging.html +680 -680
  391. package/server/whatsapp-rpc/web/templates/send.html +258 -258
  392. package/server/whatsapp-rpc/web/templates/settings.html +459 -459
  393. package/client/src/components/ui/AndroidSettingsPanel.tsx +0 -401
  394. package/client/src/components/ui/WhatsAppSettingsPanel.tsx +0 -345
  395. package/client/src/nodeDefinitions/androidDeviceNodes.ts +0 -140
  396. package/docker-compose.prod.yml +0 -107
  397. package/docker-compose.yml +0 -104
  398. package/docs-MachinaOs/README.md +0 -85
  399. package/docs-MachinaOs/deployment/docker.mdx +0 -228
  400. package/docs-MachinaOs/deployment/production.mdx +0 -345
  401. package/docs-MachinaOs/docs.json +0 -75
  402. package/docs-MachinaOs/faq.mdx +0 -309
  403. package/docs-MachinaOs/favicon.svg +0 -5
  404. package/docs-MachinaOs/installation.mdx +0 -160
  405. package/docs-MachinaOs/introduction.mdx +0 -114
  406. package/docs-MachinaOs/logo/dark.svg +0 -6
  407. package/docs-MachinaOs/logo/light.svg +0 -6
  408. package/docs-MachinaOs/nodes/ai-agent.mdx +0 -216
  409. package/docs-MachinaOs/nodes/ai-models.mdx +0 -240
  410. package/docs-MachinaOs/nodes/android.mdx +0 -411
  411. package/docs-MachinaOs/nodes/overview.mdx +0 -181
  412. package/docs-MachinaOs/nodes/schedulers.mdx +0 -316
  413. package/docs-MachinaOs/nodes/webhooks.mdx +0 -330
  414. package/docs-MachinaOs/nodes/whatsapp.mdx +0 -305
  415. package/docs-MachinaOs/quickstart.mdx +0 -119
  416. package/docs-MachinaOs/tutorials/ai-agent-workflow.mdx +0 -177
  417. package/docs-MachinaOs/tutorials/android-automation.mdx +0 -242
  418. package/docs-MachinaOs/tutorials/first-workflow.mdx +0 -134
  419. package/docs-MachinaOs/tutorials/whatsapp-automation.mdx +0 -185
  420. package/nul +0 -0
  421. package/scripts/check-ports.ps1 +0 -33
  422. package/scripts/kill-port.ps1 +0 -154
@@ -1,1874 +1,1874 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { NodeParameter } from '../types/NodeTypes';
3
- import { INodeProperties, INodePropertyOption } from '../types/INodeProperties';
4
- import APIKeyValidator from './APIKeyValidator';
5
- import CodeEditor from './ui/CodeEditor';
6
- import DynamicParameterService from '../services/dynamicParameterService';
7
- import { useAppStore } from '../store/useAppStore';
8
- import { ANDROID_SERVICE_NODE_TYPES } from '../nodeDefinitions/androidServiceNodes';
9
- import { nodeDefinitions } from '../nodeDefinitions';
10
- import { useAppTheme } from '../hooks/useAppTheme';
11
- import { API_CONFIG } from '../config/api';
12
- import { useWebSocket } from '../contexts/WebSocketContext';
13
- import { useApiKeys } from '../hooks/useApiKeys';
14
-
15
- // Map node types to provider keys for AI model nodes
16
- const NODE_TYPE_TO_PROVIDER: Record<string, string> = {
17
- 'openaiChatModel': 'openai',
18
- 'anthropicChatModel': 'anthropic',
19
- 'claudeChatModel': 'anthropic',
20
- 'googleChatModel': 'gemini',
21
- 'geminiChatModel': 'gemini',
22
- 'azureChatModel': 'azure_openai',
23
- 'cohereChatModel': 'cohere',
24
- 'ollamaChatModel': 'ollama',
25
- 'mistralChatModel': 'mistral',
26
- 'openrouterChatModel': 'openrouter',
27
- 'groqChatModel': 'groq',
28
- 'cerebrasChatModel': 'cerebras'
29
- };
30
-
31
- // Collection Renderer - n8n official style
32
- const CollectionRenderer: React.FC<{
33
- parameter: any;
34
- value: any;
35
- onChange: (value: any) => void;
36
- allParameters?: Record<string, any>;
37
- theme: ReturnType<typeof useAppTheme>;
38
- }> = ({ parameter, value, onChange, allParameters, theme }) => {
39
- const [showAddOption, setShowAddOption] = useState(false);
40
- const currentValue = value || {};
41
- const addedOptions = Object.keys(currentValue).filter(key => currentValue[key] !== undefined);
42
- const availableOptions = parameter.options?.filter((opt: any) => !addedOptions.includes(opt.name)) || [];
43
-
44
- const addOption = (optionName: string) => {
45
- const option = parameter.options?.find((opt: any) => opt.name === optionName);
46
- if (option) {
47
- onChange({
48
- ...currentValue,
49
- [optionName]: option.default
50
- });
51
- setShowAddOption(false);
52
- }
53
- };
54
-
55
- const removeOption = (optionName: string) => {
56
- const newValue = { ...currentValue };
57
- delete newValue[optionName];
58
- onChange(newValue);
59
- };
60
-
61
- const updateOption = (optionName: string, optionValue: any) => {
62
- onChange({
63
- ...currentValue,
64
- [optionName]: optionValue
65
- });
66
- };
67
-
68
- return (
69
- <div>
70
- {addedOptions.length === 0 && (
71
- <div style={{
72
- fontSize: '14px',
73
- color: theme.colors.textSecondary,
74
- marginBottom: '12px',
75
- padding: '8px 0'
76
- }}>
77
- No properties
78
- </div>
79
- )}
80
-
81
- {addedOptions.map((optionName) => {
82
- const option = parameter.options?.find((opt: any) => opt.name === optionName);
83
- if (!option) return null;
84
-
85
- return (
86
- <div key={optionName} style={{
87
- marginBottom: '16px',
88
- padding: '12px',
89
- border: `1px solid ${theme.colors.border}`,
90
- borderRadius: '4px',
91
- backgroundColor: theme.colors.backgroundAlt,
92
- position: 'relative'
93
- }}>
94
- <button
95
- onClick={() => removeOption(optionName)}
96
- style={{
97
- position: 'absolute',
98
- top: '6px',
99
- right: '6px',
100
- background: 'none',
101
- border: 'none',
102
- color: theme.colors.textSecondary,
103
- cursor: 'pointer',
104
- fontSize: '14px',
105
- padding: '2px 4px',
106
- borderRadius: '2px'
107
- }}
108
- onMouseEnter={(e) => e.currentTarget.style.backgroundColor = theme.colors.border}
109
- onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
110
- title="Remove"
111
- >
112
-
113
- </button>
114
- <ParameterRenderer
115
- parameter={option}
116
- value={currentValue[optionName]}
117
- onChange={(newValue) => updateOption(optionName, newValue)}
118
- allParameters={allParameters}
119
- />
120
- </div>
121
- );
122
- })}
123
-
124
- {availableOptions.length > 0 && (
125
- <div style={{ position: 'relative' }}>
126
- <button
127
- onClick={() => setShowAddOption(!showAddOption)}
128
- style={{
129
- width: '100%',
130
- padding: '10px 12px',
131
- border: `1px solid ${theme.colors.border}`,
132
- borderRadius: '4px',
133
- backgroundColor: theme.colors.backgroundAlt,
134
- color: theme.colors.textSecondary,
135
- cursor: 'pointer',
136
- fontSize: '14px',
137
- display: 'flex',
138
- alignItems: 'center',
139
- justifyContent: 'space-between',
140
- transition: 'all 0.2s ease'
141
- }}
142
- onMouseEnter={(e) => e.currentTarget.style.backgroundColor = theme.colors.borderHover}
143
- onMouseLeave={(e) => e.currentTarget.style.backgroundColor = theme.colors.backgroundAlt}
144
- >
145
- {parameter.placeholder || 'Add Option'}
146
- <span style={{
147
- transform: showAddOption ? 'rotate(180deg)' : 'rotate(0deg)',
148
- transition: 'transform 0.2s ease'
149
- }}>
150
-
151
- </span>
152
- </button>
153
-
154
- {showAddOption && (
155
- <div style={{
156
- position: 'absolute',
157
- top: '100%',
158
- left: 0,
159
- right: 0,
160
- zIndex: 1000,
161
- backgroundColor: theme.colors.background,
162
- border: `1px solid ${theme.colors.border}`,
163
- borderRadius: '4px',
164
- marginTop: '2px',
165
- boxShadow: `0 4px 6px -1px ${theme.colors.shadow}`,
166
- maxHeight: '200px',
167
- overflowY: 'auto'
168
- }}>
169
- {availableOptions.map((option: any, index: number) => (
170
- <button
171
- key={option.name}
172
- onClick={() => addOption(option.name)}
173
- style={{
174
- width: '100%',
175
- padding: '10px 12px',
176
- border: 'none',
177
- backgroundColor: 'transparent',
178
- color: theme.colors.text,
179
- cursor: 'pointer',
180
- fontSize: '14px',
181
- textAlign: 'left',
182
- borderBottom: index < availableOptions.length - 1 ? `1px solid ${theme.colors.border}` : 'none'
183
- }}
184
- onMouseEnter={(e) => e.currentTarget.style.backgroundColor = theme.colors.backgroundAlt}
185
- onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
186
- >
187
- <div style={{ fontWeight: '500' }}>
188
- {option.displayName}
189
- </div>
190
- {option.description && (
191
- <div style={{
192
- fontSize: '12px',
193
- color: theme.colors.textSecondary,
194
- marginTop: '2px'
195
- }}>
196
- {option.description}
197
- </div>
198
- )}
199
- </button>
200
- ))}
201
- </div>
202
- )}
203
- </div>
204
- )}
205
- </div>
206
- );
207
- };
208
-
209
- // Group ID Selector - with Load Groups button and dropdown
210
- const GroupIdSelector: React.FC<{
211
- value: string;
212
- onChange: (value: string) => void;
213
- onNameChange?: (name: string) => void;
214
- storedName?: string;
215
- placeholder?: string;
216
- theme: ReturnType<typeof useAppTheme>;
217
- isDragOver: boolean;
218
- onDragOver: (e: React.DragEvent) => void;
219
- onDragLeave: (e: React.DragEvent) => void;
220
- onDrop: (e: React.DragEvent) => void;
221
- }> = ({ value, onChange, onNameChange, storedName, placeholder, theme, isDragOver, onDragOver, onDragLeave, onDrop }) => {
222
- const [groups, setGroups] = useState<Array<{ jid: string; name: string; topic?: string; size?: number; is_community?: boolean }>>([]);
223
- const [isLoading, setIsLoading] = useState(false);
224
- const [showDropdown, setShowDropdown] = useState(false);
225
- const [error, setError] = useState<string | null>(null);
226
- // Use stored name if available, otherwise local state
227
- const [localGroupName, setLocalGroupName] = useState<string | null>(null);
228
- const selectedGroupName = storedName || localGroupName;
229
- const { getWhatsAppGroups } = useWebSocket();
230
-
231
- // Sync local state with stored name
232
- useEffect(() => {
233
- if (storedName) {
234
- setLocalGroupName(storedName);
235
- }
236
- }, [storedName]);
237
-
238
- const handleLoadGroups = async () => {
239
- setIsLoading(true);
240
- setError(null);
241
- try {
242
- const result = await getWhatsAppGroups();
243
- console.log('[GroupIdSelector] Raw groups from API:', result.groups?.map(g => ({ name: g.name, jid: g.jid, is_community: g.is_community })));
244
- if (result.success && result.groups.length > 0) {
245
- // Filter out communities - they don't have regular chat history
246
- const regularGroups = result.groups.filter(g => !g.is_community);
247
- console.log('[GroupIdSelector] After filtering communities:', regularGroups.length, 'groups remaining');
248
- if (regularGroups.length === 0) {
249
- setError('Only communities found (no chat history available)');
250
- return;
251
- }
252
- setGroups(regularGroups);
253
- setShowDropdown(true);
254
- // If we already have a value, try to find its name and update storage
255
- if (value) {
256
- const matchingGroup = regularGroups.find(g => g.jid === value);
257
- if (matchingGroup && matchingGroup.name !== storedName) {
258
- setLocalGroupName(matchingGroup.name);
259
- onNameChange?.(matchingGroup.name);
260
- }
261
- }
262
- } else if (result.error) {
263
- setError(result.error);
264
- } else {
265
- setError('No groups found');
266
- }
267
- } catch (err: any) {
268
- setError(err.message || 'Failed to load groups');
269
- } finally {
270
- setIsLoading(false);
271
- }
272
- };
273
-
274
- const handleSelectGroup = (group: { jid: string; name: string }) => {
275
- onChange(group.jid);
276
- setLocalGroupName(group.name);
277
- onNameChange?.(group.name);
278
- setShowDropdown(false);
279
- };
280
-
281
- const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
282
- onChange(e.target.value);
283
- // Clear group name when user types manually
284
- setLocalGroupName(null);
285
- onNameChange?.('');
286
- };
287
-
288
- // Display value: show group name if selected, otherwise show JID
289
- const displayValue = selectedGroupName || value;
290
- const isGroupSelected = selectedGroupName !== null && value;
291
-
292
- return (
293
- <div style={{ position: 'relative' }}>
294
- <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
295
- <input
296
- type="text"
297
- value={displayValue}
298
- onChange={handleInputChange}
299
- placeholder={placeholder || '123456789@g.us'}
300
- onDragOver={onDragOver}
301
- onDragLeave={onDragLeave}
302
- onDrop={onDrop}
303
- style={{
304
- flex: 1,
305
- padding: '8px 12px',
306
- border: isDragOver ? `2px solid ${theme.colors.focus}` : `1px solid ${theme.colors.border}`,
307
- borderRadius: '6px',
308
- fontSize: '14px',
309
- backgroundColor: isGroupSelected ? theme.colors.backgroundAlt : (isDragOver ? theme.colors.focusRing : theme.colors.background),
310
- color: isGroupSelected ? theme.colors.success : (value && value.includes('{{') ? theme.colors.templateVariable : theme.colors.text),
311
- outline: 'none',
312
- transition: 'all 0.2s ease',
313
- fontFamily: value && value.includes('{{') ? 'monospace' : 'system-ui, sans-serif',
314
- fontWeight: isGroupSelected ? '500' : 'normal'
315
- }}
316
- onFocus={(e) => e.target.style.borderColor = theme.colors.focus}
317
- onBlur={(e) => e.target.style.borderColor = theme.colors.border}
318
- />
319
- <button
320
- onClick={handleLoadGroups}
321
- disabled={isLoading}
322
- style={{
323
- padding: '8px 12px',
324
- border: `1px solid ${isLoading ? theme.colors.border : `${theme.colors.focus}40`}`,
325
- borderRadius: '6px',
326
- backgroundColor: isLoading ? 'transparent' : `${theme.colors.focus}18`,
327
- color: isLoading ? theme.colors.textMuted : theme.colors.focus,
328
- cursor: isLoading ? 'wait' : 'pointer',
329
- fontSize: '13px',
330
- fontWeight: 600,
331
- transition: 'all 0.2s ease',
332
- whiteSpace: 'nowrap',
333
- opacity: isLoading ? 0.7 : 1
334
- }}
335
- onMouseEnter={(e) => {
336
- if (!isLoading) {
337
- e.currentTarget.style.backgroundColor = `${theme.colors.focus}30`;
338
- }
339
- }}
340
- onMouseLeave={(e) => {
341
- e.currentTarget.style.backgroundColor = isLoading ? 'transparent' : `${theme.colors.focus}18`;
342
- }}
343
- title="Load WhatsApp groups"
344
- >
345
- {isLoading ? 'Loading...' : 'Load'}
346
- </button>
347
- </div>
348
- {/* Show JID below when group name is displayed */}
349
- {isGroupSelected && (
350
- <div style={{
351
- fontSize: '11px',
352
- color: theme.colors.textSecondary,
353
- marginTop: '4px',
354
- fontFamily: 'monospace'
355
- }}>
356
- {value}
357
- </div>
358
- )}
359
-
360
- {error && (
361
- <div style={{
362
- fontSize: '12px',
363
- color: theme.colors.error,
364
- marginTop: '4px'
365
- }}>
366
- {error}
367
- </div>
368
- )}
369
-
370
- {showDropdown && groups.length > 0 && (
371
- <div style={{
372
- position: 'absolute',
373
- top: '100%',
374
- left: 0,
375
- right: 0,
376
- marginTop: '4px',
377
- backgroundColor: theme.colors.background,
378
- border: `1px solid ${theme.colors.border}`,
379
- borderRadius: '6px',
380
- boxShadow: `0 4px 12px ${theme.colors.shadow}`,
381
- maxHeight: '200px',
382
- overflowY: 'auto',
383
- zIndex: 1000
384
- }}>
385
- {groups.map((group, index) => (
386
- <button
387
- key={group.jid}
388
- onClick={() => handleSelectGroup(group)}
389
- style={{
390
- width: '100%',
391
- padding: '10px 12px',
392
- border: 'none',
393
- backgroundColor: 'transparent',
394
- color: theme.colors.text,
395
- cursor: 'pointer',
396
- fontSize: '13px',
397
- textAlign: 'left',
398
- borderBottom: index < groups.length - 1 ? `1px solid ${theme.colors.border}` : 'none'
399
- }}
400
- onMouseEnter={(e) => e.currentTarget.style.backgroundColor = theme.colors.backgroundAlt}
401
- onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
402
- >
403
- <div style={{ fontWeight: '500' }}>{group.name}</div>
404
- <div style={{
405
- fontSize: '11px',
406
- color: theme.colors.textSecondary,
407
- marginTop: '2px',
408
- fontFamily: 'monospace'
409
- }}>
410
- {group.jid}
411
- {group.size && <span style={{ marginLeft: '8px' }}>({group.size} members)</span>}
412
- </div>
413
- </button>
414
- ))}
415
- <button
416
- onClick={() => setShowDropdown(false)}
417
- style={{
418
- width: '100%',
419
- padding: '8px 12px',
420
- border: 'none',
421
- borderTop: `1px solid ${theme.colors.border}`,
422
- backgroundColor: theme.colors.backgroundAlt,
423
- color: theme.colors.textSecondary,
424
- cursor: 'pointer',
425
- fontSize: '12px',
426
- textAlign: 'center'
427
- }}
428
- onMouseEnter={(e) => e.currentTarget.style.color = theme.colors.text}
429
- onMouseLeave={(e) => e.currentTarget.style.color = theme.colors.textSecondary}
430
- >
431
- Close
432
- </button>
433
- </div>
434
- )}
435
- </div>
436
- );
437
- };
438
-
439
- // Sender Number Selector - with Load Members button and dropdown (loads from selected group)
440
- const SenderNumberSelector: React.FC<{
441
- value: string;
442
- onChange: (value: string) => void;
443
- onNameChange?: (name: string) => void;
444
- storedName?: string;
445
- placeholder?: string;
446
- theme: ReturnType<typeof useAppTheme>;
447
- isDragOver: boolean;
448
- onDragOver: (e: React.DragEvent) => void;
449
- onDragLeave: (e: React.DragEvent) => void;
450
- onDrop: (e: React.DragEvent) => void;
451
- groupId: string; // The selected group to load members from
452
- }> = ({ value, onChange, onNameChange, storedName, placeholder, theme, isDragOver, onDragOver, onDragLeave, onDrop, groupId }) => {
453
- const [members, setMembers] = useState<Array<{ phone: string; name: string; jid: string; is_admin?: boolean }>>([]);
454
- const [isLoading, setIsLoading] = useState(false);
455
- const [showDropdown, setShowDropdown] = useState(false);
456
- const [error, setError] = useState<string | null>(null);
457
- // Use stored name if available, otherwise local state
458
- const [localMemberName, setLocalMemberName] = useState<string | null>(null);
459
- const selectedMemberName = storedName || localMemberName;
460
- const { getWhatsAppGroupInfo } = useWebSocket();
461
-
462
- // Sync local state with stored name
463
- useEffect(() => {
464
- if (storedName) {
465
- setLocalMemberName(storedName);
466
- }
467
- }, [storedName]);
468
-
469
- const handleLoadMembers = async () => {
470
- if (!groupId) {
471
- setError('Select a group first');
472
- return;
473
- }
474
-
475
- setIsLoading(true);
476
- setError(null);
477
- try {
478
- const result = await getWhatsAppGroupInfo(groupId);
479
- if (result.success && result.participants && result.participants.length > 0) {
480
- setMembers(result.participants);
481
- setShowDropdown(true);
482
- // If we already have a value, try to find its name and update storage
483
- if (value) {
484
- const matchingMember = result.participants.find((m: any) => m.phone === value);
485
- if (matchingMember) {
486
- const name = matchingMember.name || matchingMember.phone;
487
- if (name !== storedName) {
488
- setLocalMemberName(name);
489
- onNameChange?.(name);
490
- }
491
- }
492
- }
493
- } else if (result.error) {
494
- setError(result.error);
495
- } else {
496
- setError('No members found');
497
- }
498
- } catch (err: any) {
499
- setError(err.message || 'Failed to load members');
500
- } finally {
501
- setIsLoading(false);
502
- }
503
- };
504
-
505
- const handleSelectMember = (member: { phone: string; name: string }) => {
506
- const name = member.name || member.phone;
507
- onChange(member.phone);
508
- setLocalMemberName(name);
509
- onNameChange?.(name);
510
- setShowDropdown(false);
511
- };
512
-
513
- const handleClearSelection = () => {
514
- onChange('');
515
- setLocalMemberName(null);
516
- onNameChange?.('');
517
- setShowDropdown(false);
518
- };
519
-
520
- const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
521
- onChange(e.target.value);
522
- // Clear member name when user types manually
523
- setLocalMemberName(null);
524
- onNameChange?.('');
525
- };
526
-
527
- // Display value: show member name if selected, otherwise show phone
528
- const displayValue = selectedMemberName || value;
529
- const isMemberSelected = selectedMemberName !== null && value;
530
-
531
- return (
532
- <div style={{ position: 'relative' }}>
533
- <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
534
- <input
535
- type="text"
536
- value={displayValue}
537
- onChange={handleInputChange}
538
- placeholder={placeholder || 'All members (leave empty)'}
539
- onDragOver={onDragOver}
540
- onDragLeave={onDragLeave}
541
- onDrop={onDrop}
542
- style={{
543
- flex: 1,
544
- padding: '8px 12px',
545
- border: isDragOver ? `2px solid ${theme.colors.focus}` : `1px solid ${theme.colors.border}`,
546
- borderRadius: '6px',
547
- fontSize: '14px',
548
- backgroundColor: isMemberSelected ? theme.colors.backgroundAlt : (isDragOver ? theme.colors.focusRing : theme.colors.background),
549
- color: isMemberSelected ? theme.colors.success : (value && value.includes('{{') ? theme.colors.templateVariable : theme.colors.text),
550
- outline: 'none',
551
- transition: 'all 0.2s ease',
552
- fontFamily: value && value.includes('{{') ? 'monospace' : 'system-ui, sans-serif',
553
- fontWeight: isMemberSelected ? '500' : 'normal'
554
- }}
555
- onFocus={(e) => e.target.style.borderColor = theme.colors.focus}
556
- onBlur={(e) => e.target.style.borderColor = theme.colors.border}
557
- />
558
- <button
559
- onClick={handleLoadMembers}
560
- disabled={isLoading || !groupId}
561
- style={{
562
- padding: '8px 12px',
563
- border: `1px solid ${(isLoading || !groupId) ? theme.colors.border : `${theme.colors.focus}40`}`,
564
- borderRadius: '6px',
565
- backgroundColor: (isLoading || !groupId) ? 'transparent' : `${theme.colors.focus}18`,
566
- color: (isLoading || !groupId) ? theme.colors.textMuted : theme.colors.focus,
567
- cursor: (isLoading || !groupId) ? 'not-allowed' : 'pointer',
568
- fontSize: '13px',
569
- fontWeight: 600,
570
- transition: 'all 0.2s ease',
571
- whiteSpace: 'nowrap',
572
- opacity: (isLoading || !groupId) ? 0.7 : 1
573
- }}
574
- onMouseEnter={(e) => {
575
- if (!isLoading && groupId) {
576
- e.currentTarget.style.backgroundColor = `${theme.colors.focus}30`;
577
- }
578
- }}
579
- onMouseLeave={(e) => {
580
- e.currentTarget.style.backgroundColor = (isLoading || !groupId) ? 'transparent' : `${theme.colors.focus}18`;
581
- }}
582
- title={groupId ? "Load group members" : "Select a group first"}
583
- >
584
- {isLoading ? 'Loading...' : 'Load'}
585
- </button>
586
- </div>
587
- {/* Show phone below when member name is displayed */}
588
- {isMemberSelected && (
589
- <div style={{
590
- fontSize: '11px',
591
- color: theme.colors.textSecondary,
592
- marginTop: '4px',
593
- fontFamily: 'monospace'
594
- }}>
595
- {value}
596
- </div>
597
- )}
598
-
599
- {error && (
600
- <div style={{
601
- fontSize: '12px',
602
- color: theme.colors.error,
603
- marginTop: '4px'
604
- }}>
605
- {error}
606
- </div>
607
- )}
608
-
609
- {showDropdown && members.length > 0 && (
610
- <div style={{
611
- position: 'absolute',
612
- top: '100%',
613
- left: 0,
614
- right: 0,
615
- marginTop: '4px',
616
- backgroundColor: theme.colors.background,
617
- border: `1px solid ${theme.colors.border}`,
618
- borderRadius: '6px',
619
- boxShadow: `0 4px 12px ${theme.colors.shadow}`,
620
- maxHeight: '250px',
621
- overflowY: 'auto',
622
- zIndex: 1000
623
- }}>
624
- {/* All Members option */}
625
- <button
626
- onClick={handleClearSelection}
627
- style={{
628
- width: '100%',
629
- padding: '10px 12px',
630
- border: 'none',
631
- backgroundColor: !value ? theme.colors.backgroundAlt : 'transparent',
632
- color: theme.colors.text,
633
- cursor: 'pointer',
634
- fontSize: '13px',
635
- textAlign: 'left',
636
- borderBottom: `1px solid ${theme.colors.border}`
637
- }}
638
- onMouseEnter={(e) => e.currentTarget.style.backgroundColor = theme.colors.backgroundAlt}
639
- onMouseLeave={(e) => e.currentTarget.style.backgroundColor = !value ? theme.colors.backgroundAlt : 'transparent'}
640
- >
641
- <div style={{ fontWeight: '500', color: theme.colors.textSecondary }}>All Members</div>
642
- <div style={{ fontSize: '11px', color: theme.colors.textMuted, marginTop: '2px' }}>
643
- Receive from anyone in group
644
- </div>
645
- </button>
646
- {members.map((member, index) => (
647
- <button
648
- key={member.jid || member.phone}
649
- onClick={() => handleSelectMember(member)}
650
- style={{
651
- width: '100%',
652
- padding: '10px 12px',
653
- border: 'none',
654
- backgroundColor: value === member.phone ? theme.colors.backgroundAlt : 'transparent',
655
- color: theme.colors.text,
656
- cursor: 'pointer',
657
- fontSize: '13px',
658
- textAlign: 'left',
659
- borderBottom: index < members.length - 1 ? `1px solid ${theme.colors.border}` : 'none'
660
- }}
661
- onMouseEnter={(e) => e.currentTarget.style.backgroundColor = theme.colors.backgroundAlt}
662
- onMouseLeave={(e) => e.currentTarget.style.backgroundColor = value === member.phone ? theme.colors.backgroundAlt : 'transparent'}
663
- >
664
- <div style={{ fontWeight: '500' }}>
665
- {member.name || member.phone}
666
- {member.is_admin && <span style={{ marginLeft: '8px', fontSize: '10px', color: theme.colors.warning }}>(Admin)</span>}
667
- </div>
668
- <div style={{
669
- fontSize: '11px',
670
- color: theme.colors.textSecondary,
671
- marginTop: '2px',
672
- fontFamily: 'monospace'
673
- }}>
674
- {member.phone}
675
- </div>
676
- </button>
677
- ))}
678
- <button
679
- onClick={() => setShowDropdown(false)}
680
- style={{
681
- width: '100%',
682
- padding: '8px 12px',
683
- border: 'none',
684
- borderTop: `1px solid ${theme.colors.border}`,
685
- backgroundColor: theme.colors.backgroundAlt,
686
- color: theme.colors.textSecondary,
687
- cursor: 'pointer',
688
- fontSize: '12px',
689
- textAlign: 'center'
690
- }}
691
- onMouseEnter={(e) => e.currentTarget.style.color = theme.colors.text}
692
- onMouseLeave={(e) => e.currentTarget.style.color = theme.colors.textSecondary}
693
- >
694
- Close
695
- </button>
696
- </div>
697
- )}
698
- </div>
699
- );
700
- };
701
-
702
- // Fixed Collection Renderer - n8n style fixed collection
703
- const FixedCollectionRenderer: React.FC<{
704
- parameter: any;
705
- value: any;
706
- onChange: (value: any) => void;
707
- allParameters?: Record<string, any>;
708
- theme: ReturnType<typeof useAppTheme>;
709
- }> = ({ parameter, value, onChange, allParameters, theme }) => {
710
- const currentValue = value || {};
711
-
712
- return (
713
- <div style={{
714
- border: `1px solid ${theme.colors.border}`,
715
- borderRadius: '6px',
716
- backgroundColor: theme.colors.backgroundAlt,
717
- padding: '12px'
718
- }}>
719
- {parameter.options?.map((option: any) => {
720
- const optionValue = currentValue[option.name] || {};
721
-
722
- return (
723
- <div key={option.name} style={{ marginBottom: '16px' }}>
724
- <div style={{
725
- fontSize: '14px',
726
- fontWeight: '500',
727
- color: theme.colors.text,
728
- marginBottom: '8px'
729
- }}>
730
- {option.displayName}
731
- </div>
732
- <div style={{
733
- border: `1px solid ${theme.colors.border}`,
734
- borderRadius: '6px',
735
- backgroundColor: theme.colors.background,
736
- padding: '12px'
737
- }}>
738
- {option.values?.map((valueParam: any) => (
739
- <ParameterRenderer
740
- key={valueParam.name}
741
- parameter={valueParam}
742
- value={optionValue[valueParam.name]}
743
- onChange={(newValue) => {
744
- onChange({
745
- ...currentValue,
746
- [option.name]: {
747
- ...optionValue,
748
- [valueParam.name]: newValue
749
- }
750
- });
751
- }}
752
- allParameters={allParameters}
753
- />
754
- ))}
755
- </div>
756
- </div>
757
- );
758
- })}
759
- </div>
760
- );
761
- };
762
-
763
- interface ParameterRendererProps {
764
- parameter: NodeParameter | INodeProperties;
765
- value: any;
766
- onChange: (value: any) => void;
767
- allParameters?: Record<string, any>;
768
- onParameterChange?: (paramName: string, value: any) => void;
769
- onClosePanel?: () => void;
770
- isLoadingParameters?: boolean;
771
- }
772
-
773
- // Type guard to check if parameter is INodeProperties
774
- const isINodeProperties = (param: NodeParameter | INodeProperties): param is INodeProperties => {
775
- return 'typeOptions' in param;
776
- };
777
-
778
- const ParameterRenderer: React.FC<ParameterRendererProps> = ({
779
- parameter,
780
- value,
781
- onChange,
782
- allParameters,
783
- onParameterChange,
784
- isLoadingParameters = false,
785
- }) => {
786
- const theme = useAppTheme();
787
- // Don't use default while loading - wait for actual saved value to load
788
- // This prevents showing template code briefly before saved code appears
789
- const currentValue = isLoadingParameters ? (value ?? '') : (value !== undefined ? value : parameter.default);
790
- const [isDragOver, setIsDragOver] = useState(false);
791
- const [dynamicOptions, setDynamicOptions] = useState<INodePropertyOption[]>([]);
792
- const [nodeParameters, setNodeParameters] = useState<Record<string, any>>({});
793
-
794
- const { selectedNode } = useAppStore();
795
- const { getNodeParameters } = useWebSocket();
796
- const { getStoredApiKey, hasStoredKey, getStoredModels } = useApiKeys();
797
-
798
- // Don't render hidden parameters
799
- if (parameter.type === 'hidden') {
800
- return null;
801
- }
802
-
803
- // Load node parameters for expression resolution
804
- useEffect(() => {
805
- const loadParameters = async () => {
806
- if (selectedNode?.id) {
807
- const result = await getNodeParameters(selectedNode.id);
808
- if (result?.parameters) setNodeParameters(result.parameters);
809
- }
810
- };
811
- loadParameters();
812
- }, [selectedNode?.id, getNodeParameters]);
813
-
814
- // Auto-load stored API key and models when provider changes
815
- // Use ref to track previous provider to prevent infinite loops
816
- const prevProviderRef = React.useRef<string | null>(null);
817
- // Track if we've done initial auto-select after parameters loaded
818
- const hasAutoSelectedRef = React.useRef(false);
819
-
820
- // Reset auto-select tracking when node changes
821
- useEffect(() => {
822
- hasAutoSelectedRef.current = false;
823
- prevProviderRef.current = null;
824
- }, [selectedNode?.id]);
825
-
826
- useEffect(() => {
827
- const loadStoredKeyForProvider = async () => {
828
- // Only run for apiKey or model parameters
829
- if (parameter.name !== 'apiKey' && parameter.name !== 'model') return;
830
-
831
- // Don't run while parameters are still loading from database
832
- if (isLoadingParameters) return;
833
-
834
- // Get provider from allParameters or derive from node type
835
- let provider = allParameters?.provider;
836
- if (!provider && selectedNode) {
837
- const nodeType = selectedNode.type || selectedNode.data?.nodeType;
838
- if (nodeType) {
839
- provider = NODE_TYPE_TO_PROVIDER[nodeType];
840
- }
841
- }
842
- if (!provider) return;
843
-
844
- // Distinguish between initial load (prevProvider was null) and actual user-initiated provider change
845
- // On initial load: respect saved model if it exists
846
- // On provider change: reset to first model to prevent mismatched provider/model
847
- const isInitialLoad = prevProviderRef.current === null;
848
- const isActualProviderChange = !isInitialLoad && prevProviderRef.current !== provider;
849
- const shouldAutoSelectModel = parameter.name === 'model' &&
850
- (isActualProviderChange || isInitialLoad || !hasAutoSelectedRef.current);
851
-
852
- // Skip if provider hasn't changed (except for initial model load)
853
- if (!isActualProviderChange && !isInitialLoad && parameter.name !== 'model') return;
854
- if (!isActualProviderChange && !isInitialLoad && hasAutoSelectedRef.current) return;
855
-
856
- prevProviderRef.current = provider;
857
-
858
- try {
859
- const hasKey = await hasStoredKey(provider);
860
-
861
- if (hasKey) {
862
- // Auto-load API key for apiKey parameter - always update when provider changes
863
- if (parameter.name === 'apiKey' && isActualProviderChange) {
864
- const storedKey = await getStoredApiKey(provider);
865
- if (storedKey) {
866
- onChange(storedKey);
867
- }
868
- }
869
-
870
- // Auto-load models for model parameter
871
- if (shouldAutoSelectModel && selectedNode) {
872
- const models = await getStoredModels(provider);
873
- if (models?.length) {
874
- const modelOptions = DynamicParameterService.createModelOptions(models);
875
- DynamicParameterService.updateParameterOptions(selectedNode.id, 'model', modelOptions);
876
-
877
- // Extract model ID (handles both string and object formats)
878
- const getModelId = (model: any) => typeof model === 'string' ? model : model.id;
879
- const firstModelId = getModelId(models[0]);
880
-
881
- // When user actively changes provider, reset to first model
882
- // to prevent mismatched provider/model combinations (e.g., OpenAI model with Anthropic provider)
883
- if (isActualProviderChange) {
884
- onChange(firstModelId);
885
- } else {
886
- // Initial load or no provider change - only auto-select if no saved model exists
887
- const savedModel = value || allParameters?.model;
888
- if (!savedModel || savedModel === '') {
889
- onChange(firstModelId);
890
- }
891
- // If saved model exists, keep it (don't call onChange)
892
- }
893
- hasAutoSelectedRef.current = true;
894
- }
895
- }
896
- } else {
897
- // No stored key for this provider - clear the fields
898
- if (parameter.name === 'apiKey') {
899
- onChange('');
900
- }
901
- if (parameter.name === 'model') {
902
- onChange('');
903
- hasAutoSelectedRef.current = true;
904
- }
905
- }
906
- } catch (error) {
907
- console.warn('Error loading stored key info:', error);
908
- }
909
- };
910
-
911
- loadStoredKeyForProvider();
912
- }, [allParameters?.provider, parameter.name, hasStoredKey, getStoredApiKey, getStoredModels, selectedNode?.id, selectedNode?.type, onChange, isLoadingParameters, value, allParameters?.model]);
913
-
914
- // Merge database params with current form params (current takes precedence)
915
- const resolvedParameters = { ...nodeParameters, ...allParameters };
916
-
917
- // Helper functions to get values from both interface types
918
- const getMin = () => (parameter as any).min || (parameter as any).typeOptions?.minValue || 0;
919
- const getMax = () => (parameter as any).max || (parameter as any).typeOptions?.maxValue || 100;
920
- const getStep = () => (parameter as any).step || (parameter as any).typeOptions?.numberStepSize || 1;
921
-
922
- // Load dynamic options based on loadOptionsMethod
923
- useEffect(() => {
924
- const loadDynamicOptions = async () => {
925
- if (!selectedNode || !isINodeProperties(parameter) || !parameter.typeOptions?.loadOptionsMethod) return;
926
-
927
- const dependsOn = parameter.typeOptions.loadOptionsDependsOn || [];
928
- const allParamsResolved = { ...nodeParameters, ...allParameters };
929
-
930
- // Check if all dependencies are satisfied
931
- const hasAllDependencies = dependsOn.every((dep: string) => allParamsResolved[dep]);
932
- if (dependsOn.length > 0 && !hasAllDependencies) return;
933
-
934
- try {
935
- // Get the node definition to access methods
936
- const nodeType = selectedNode.data?.nodeType || selectedNode.type;
937
- const nodeDef = nodeType ? nodeDefinitions[nodeType] : null;
938
-
939
- if (nodeDef?.methods?.loadOptions?.[parameter.typeOptions.loadOptionsMethod]) {
940
- const loadMethod = nodeDef.methods.loadOptions[parameter.typeOptions.loadOptionsMethod];
941
-
942
- // Create context for the load method
943
- const context = {
944
- getCurrentNodeParameter: (paramName: string) => allParamsResolved[paramName]
945
- };
946
-
947
- // Call the load method with context
948
- const options = await loadMethod.call(context);
949
- setDynamicOptions(options);
950
-
951
- // Also update the DynamicParameterService for consistency
952
- DynamicParameterService.updateParameterOptions(selectedNode.id, parameter.name, options);
953
-
954
- // Auto-select first option if current value is empty and options are available
955
- if (options.length > 0 && (!currentValue || currentValue === '')) {
956
- console.log(`[ParameterRenderer] Auto-selecting first option for ${parameter.name}:`, options[0].value);
957
- onChange(options[0].value);
958
- }
959
- }
960
- } catch (error) {
961
- console.error('Error loading dynamic options:', error);
962
- }
963
- };
964
-
965
- loadDynamicOptions();
966
- }, [selectedNode?.id, isINodeProperties(parameter) && parameter.typeOptions?.loadOptionsMethod, nodeParameters, allParameters, parameter.name]);
967
-
968
- // Load default parameters for Android service nodes when service_id or action changes
969
- useEffect(() => {
970
- const loadDefaultParameters = async () => {
971
- if (!selectedNode || parameter.name !== 'parameters') return;
972
-
973
- const nodeType = selectedNode.data?.nodeType || selectedNode.type;
974
- if (!ANDROID_SERVICE_NODE_TYPES.includes(nodeType)) return;
975
-
976
- // Merge database params with current form params (current takes precedence)
977
- const allParamsResolved = { ...nodeParameters, ...allParameters };
978
- const serviceId = allParamsResolved.service_id;
979
- const action = allParamsResolved.action;
980
-
981
- if (!serviceId || !action) {
982
- console.log('[AndroidService] Skipping - missing serviceId or action:', { serviceId, action });
983
- return;
984
- }
985
-
986
- try {
987
- console.log('[AndroidService] Fetching default parameters for:', { serviceId, action });
988
- const response = await fetch(`${API_CONFIG.PYTHON_BASE_URL}/api/android/services/${serviceId}/actions/${action}/parameters`, {
989
- credentials: 'include'
990
- });
991
- const data = await response.json();
992
- console.log('[AndroidService] Default parameters response:', data);
993
-
994
- if (data.success && data.default_parameters) {
995
- // Always update with new defaults when service/action changes
996
- console.log('[AndroidService] Setting parameters to:', data.default_parameters);
997
- onChange(data.default_parameters);
998
- }
999
- } catch (error) {
1000
- console.error('[AndroidService] Error loading default parameters:', error);
1001
- }
1002
- };
1003
-
1004
- loadDefaultParameters();
1005
- }, [
1006
- selectedNode?.id,
1007
- parameter.name,
1008
- allParameters?.service_id,
1009
- allParameters?.action,
1010
- nodeParameters?.service_id,
1011
- nodeParameters?.action
1012
- ]);
1013
-
1014
- // Subscribe to dynamic parameter updates
1015
- useEffect(() => {
1016
- if (!selectedNode) return;
1017
-
1018
-
1019
- const unsubscribe = DynamicParameterService.subscribe((nodeId, parameterName, options) => {
1020
-
1021
- if (nodeId === selectedNode.id && parameterName === parameter.name) {
1022
- setDynamicOptions(options);
1023
- }
1024
- });
1025
-
1026
- // Check for existing dynamic options
1027
- const existingOptions = DynamicParameterService.getParameterOptions(selectedNode.id, parameter.name);
1028
-
1029
- if (existingOptions) {
1030
- setDynamicOptions(existingOptions);
1031
- }
1032
-
1033
- return unsubscribe;
1034
- }, [selectedNode?.id, parameter.name]);
1035
-
1036
- // Handle API key validation success
1037
- const handleApiKeyValidationSuccess = (models: string[]) => {
1038
-
1039
- if (!selectedNode) {
1040
- console.warn('ParameterRenderer: No selected node for dynamic options update');
1041
- return;
1042
- }
1043
-
1044
- // Always update the 'model' parameter with dynamic options when API key validation succeeds
1045
- // This callback can be triggered from any parameter (usually the apiKey parameter)
1046
- const modelOptions = DynamicParameterService.createModelOptions(models);
1047
- DynamicParameterService.updateParameterOptions(selectedNode.id, 'model', modelOptions);
1048
-
1049
- // If this callback is triggered from the model parameter itself and it's empty, auto-select first model
1050
- if (parameter.name === 'model' && !currentValue && models.length > 0) {
1051
- onChange(models[0]);
1052
- }
1053
- };
1054
-
1055
- const handleDragOver = (e: React.DragEvent) => {
1056
- e.preventDefault();
1057
- e.dataTransfer.dropEffect = 'copy';
1058
- setIsDragOver(true);
1059
- };
1060
-
1061
- const handleDragLeave = (e: React.DragEvent) => {
1062
- e.preventDefault();
1063
- setIsDragOver(false);
1064
- };
1065
-
1066
- const handleDrop = (e: React.DragEvent) => {
1067
- e.preventDefault();
1068
- setIsDragOver(false);
1069
-
1070
- // Check if this is a coordinate parameter (lat/lng) for special handling
1071
- const paramName = parameter.name.toLowerCase();
1072
- const isCoordinate = paramName.includes('lat') || paramName.includes('lng') ||
1073
- paramName.includes('lon') || paramName === 'latitude' ||
1074
- paramName === 'longitude';
1075
-
1076
- // Try to get JSON data first (from connected node outputs)
1077
- const jsonData = e.dataTransfer.getData('application/json');
1078
- if (jsonData) {
1079
- try {
1080
- const parsedData = JSON.parse(jsonData);
1081
- if (parsedData.type === 'nodeVariable') {
1082
- // For coordinate parameters, try to extract actual numeric value from connected node data
1083
- if (isCoordinate && typeof parsedData.dataType === 'string' && parsedData.dataType === 'number') {
1084
- // Look for actual coordinate values in global execution data
1085
- // This is a simplified approach - in production you'd want more robust data access
1086
-
1087
- // For now, use template string but mark it for coordinate processing
1088
- const existingValue = currentValue || '';
1089
- const needsSpace = existingValue && !existingValue.endsWith(' ') && existingValue.length > 0;
1090
- const newValue = existingValue + (needsSpace ? ' ' : '') + parsedData.variableTemplate;
1091
- onChange(newValue);
1092
- return;
1093
- }
1094
-
1095
- // Handle variable template data - use the template string
1096
- const existingValue = currentValue || '';
1097
- // Add smart spacing - add space if existing content doesn't end with space
1098
- const needsSpace = existingValue && !existingValue.endsWith(' ') && existingValue.length > 0;
1099
- const newValue = existingValue + (needsSpace ? ' ' : '') + parsedData.variableTemplate;
1100
- onChange(newValue);
1101
- return;
1102
- }
1103
- if (parsedData.type === 'nodeOutput') {
1104
- // Handle node output data - use the actual value for direct mapping
1105
- onChange(parsedData.value);
1106
- return;
1107
- }
1108
- } catch (err) {
1109
- console.warn('Failed to parse JSON drag data:', err);
1110
- }
1111
- }
1112
-
1113
- // Fallback to existing text/plain format (OutputPanel drag-drop)
1114
- const data = e.dataTransfer.getData('text/plain');
1115
- if (data && data.startsWith('{{') && data.endsWith('}}')) {
1116
- // For coordinate parameters, allow template strings but process them appropriately
1117
- if (isCoordinate) {
1118
- onChange(data); // Set the template directly for coordinate resolution
1119
- return;
1120
- }
1121
-
1122
- // Append to existing content instead of replacing
1123
- const existingValue = currentValue || '';
1124
- // Add smart spacing - add space if existing content doesn't end with space
1125
- const needsSpace = existingValue && !existingValue.endsWith(' ') && existingValue.length > 0;
1126
- const newValue = existingValue + (needsSpace ? ' ' : '') + data;
1127
- onChange(newValue);
1128
- }
1129
- };
1130
-
1131
- const renderInput = () => {
1132
- switch (parameter.type) {
1133
- case 'string':
1134
- // Check if this should be a textarea based on typeOptions.rows
1135
- const shouldUseTextarea = (parameter as any).typeOptions?.rows > 1;
1136
- // Check if this should be a password field
1137
- const isPassword = (parameter as any).typeOptions?.password;
1138
- // Check if this is a code editor
1139
- const isCodeEditor = (parameter as any).typeOptions?.editor === 'code';
1140
-
1141
- if (shouldUseTextarea) {
1142
- // Use CodeEditor for code editing
1143
- if (isCodeEditor) {
1144
- // Show loading state while parameters are being fetched
1145
- if (isLoadingParameters) {
1146
- return (
1147
- <div style={{
1148
- height: '100%',
1149
- minHeight: '200px',
1150
- display: 'flex',
1151
- alignItems: 'center',
1152
- justifyContent: 'center',
1153
- backgroundColor: theme.colors.backgroundAlt,
1154
- border: `1px solid ${theme.colors.border}`,
1155
- borderRadius: theme.borderRadius.md,
1156
- color: theme.colors.textMuted,
1157
- fontSize: '14px'
1158
- }}>
1159
- Loading code...
1160
- </div>
1161
- );
1162
- }
1163
- // Get language from typeOptions or default to python
1164
- const codeLanguage = (parameter as any).typeOptions?.editorLanguage || 'python';
1165
- return (
1166
- <CodeEditor
1167
- value={currentValue || ''}
1168
- onChange={onChange}
1169
- language={codeLanguage}
1170
- placeholder={parameter.placeholder}
1171
- />
1172
- );
1173
- }
1174
-
1175
- // Regular textarea for non-code
1176
- return (
1177
- <textarea
1178
- value={currentValue || ''}
1179
- onChange={(e) => onChange(e.target.value)}
1180
- placeholder={parameter.placeholder}
1181
- rows={(parameter as any).typeOptions?.rows || 3}
1182
- spellCheck={true}
1183
- onDragOver={handleDragOver}
1184
- onDragLeave={handleDragLeave}
1185
- onDrop={handleDrop}
1186
- style={{
1187
- width: '100%',
1188
- padding: '10px 12px',
1189
- border: isDragOver ? `2px solid ${theme.accent.cyan}` : `1px solid ${theme.colors.border}`,
1190
- borderRadius: '6px',
1191
- fontSize: '14px',
1192
- backgroundColor: isDragOver ? `${theme.accent.cyan}10` : theme.colors.backgroundAlt,
1193
- color: currentValue && currentValue.includes('{{') ? theme.accent.yellow : theme.colors.text,
1194
- outline: 'none',
1195
- transition: 'all 0.2s ease',
1196
- fontFamily: currentValue && currentValue.includes('{{') ? 'monospace' : 'system-ui, sans-serif',
1197
- resize: 'vertical',
1198
- minHeight: '80px',
1199
- boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.05)',
1200
- lineHeight: '1.5'
1201
- }}
1202
- onFocus={(e) => {
1203
- e.target.style.borderColor = theme.accent.cyan;
1204
- e.target.style.boxShadow = `0 0 0 3px ${theme.accent.cyan}20, inset 0 1px 2px rgba(0,0,0,0.05)`;
1205
- }}
1206
- onBlur={(e) => {
1207
- e.target.style.borderColor = theme.colors.border;
1208
- e.target.style.boxShadow = 'inset 0 1px 2px rgba(0,0,0,0.05)';
1209
- }}
1210
- />
1211
- );
1212
- }
1213
-
1214
- // Check if this parameter has API key validation
1215
- const validationArray = (parameter as any).validation;
1216
- const apiKeyValidation = validationArray?.find((v: any) => v.type === 'apiKey' && v.showValidateButton);
1217
-
1218
- if (apiKeyValidation) {
1219
- // Resolve provider expression if it's a template like {{ $parameter["provider"] }}
1220
- let resolvedProvider = apiKeyValidation.provider;
1221
- if (typeof resolvedProvider === 'string' && resolvedProvider.includes('$parameter[')) {
1222
- // Extract parameter name from expression like {{ $parameter["provider"] }}
1223
- const match = resolvedProvider.match(/\$parameter\["([^"]+)"\]|\$parameter\['([^']+)'\]/);
1224
- if (match) {
1225
- const paramName = match[1] || match[2];
1226
- resolvedProvider = resolvedParameters[paramName] || resolvedProvider;
1227
- }
1228
- }
1229
-
1230
- const resolvedValidationConfig = {
1231
- ...apiKeyValidation,
1232
- provider: resolvedProvider
1233
- };
1234
-
1235
- return (
1236
- <APIKeyValidator
1237
- value={currentValue || ''}
1238
- onChange={onChange}
1239
- placeholder={parameter.placeholder}
1240
- validationConfig={resolvedValidationConfig}
1241
- onValidationSuccess={handleApiKeyValidationSuccess}
1242
- isDragOver={isDragOver}
1243
- onDragOver={handleDragOver}
1244
- onDragLeave={handleDragLeave}
1245
- onDrop={handleDrop}
1246
- />
1247
- );
1248
- }
1249
-
1250
- // Check if this parameter has dynamic options (like models after API key validation)
1251
-
1252
- if (dynamicOptions.length > 0 && parameter.type === 'string') {
1253
- // Check if OpenRouter (has [FREE] tagged models)
1254
- const hasFreeModels = dynamicOptions.some(opt => String(opt.label || opt.value).includes('[FREE]'));
1255
-
1256
- if (hasFreeModels) {
1257
- // Group into Free and Paid for OpenRouter using native select with optgroup
1258
- const freeModels = dynamicOptions.filter(opt => String(opt.label || opt.value).includes('[FREE]'));
1259
- const paidModels = dynamicOptions.filter(opt => !String(opt.label || opt.value).includes('[FREE]'));
1260
-
1261
- return (
1262
- <select
1263
- value={currentValue || ''}
1264
- onChange={(e) => onChange(e.target.value)}
1265
- style={{
1266
- width: '100%',
1267
- padding: theme.spacing.sm,
1268
- border: `1px solid ${theme.colors.border}`,
1269
- borderRadius: theme.borderRadius.md,
1270
- fontSize: theme.fontSize.sm,
1271
- outline: 'none',
1272
- transition: 'border-color 0.2s ease',
1273
- cursor: 'pointer',
1274
- backgroundColor: theme.colors.background,
1275
- color: theme.colors.text,
1276
- fontFamily: 'system-ui, sans-serif'
1277
- }}
1278
- onFocus={(e) => e.target.style.borderColor = theme.colors.focus}
1279
- onBlur={(e) => e.target.style.borderColor = theme.colors.border}
1280
- >
1281
- {!currentValue && (
1282
- <option value="" disabled>
1283
- Select a model...
1284
- </option>
1285
- )}
1286
- <optgroup label={`Free Models (${freeModels.length})`}>
1287
- {freeModels.map((option) => (
1288
- <option key={String(option.value)} value={String(option.value)}>
1289
- {option.label || option.name || String(option.value)}
1290
- </option>
1291
- ))}
1292
- </optgroup>
1293
- <optgroup label={`Paid Models (${paidModels.length})`}>
1294
- {paidModels.map((option) => (
1295
- <option key={String(option.value)} value={String(option.value)}>
1296
- {option.label || option.name || String(option.value)}
1297
- </option>
1298
- ))}
1299
- </optgroup>
1300
- </select>
1301
- );
1302
- }
1303
-
1304
- // Use native select for non-OpenRouter (original working code)
1305
- return (
1306
- <select
1307
- value={currentValue || ''}
1308
- onChange={(e) => onChange(e.target.value)}
1309
- style={{
1310
- width: '100%',
1311
- padding: '8px 12px',
1312
- border: `1px solid ${theme.colors.border}`,
1313
- borderRadius: '6px',
1314
- fontSize: '14px',
1315
- outline: 'none',
1316
- transition: 'border-color 0.2s ease',
1317
- cursor: 'pointer',
1318
- backgroundColor: theme.colors.background,
1319
- color: theme.colors.text,
1320
- fontFamily: 'system-ui, sans-serif'
1321
- }}
1322
- onFocus={(e) => e.target.style.borderColor = theme.colors.focus}
1323
- onBlur={(e) => e.target.style.borderColor = theme.colors.border}
1324
- >
1325
- {!currentValue && (
1326
- <option value="" disabled>
1327
- Select a model...
1328
- </option>
1329
- )}
1330
- {dynamicOptions.map((option) => (
1331
- <option key={String(option.value)} value={String(option.value)}>
1332
- {option.label || option.name || String(option.value)}
1333
- </option>
1334
- ))}
1335
- </select>
1336
- );
1337
- }
1338
-
1339
- // Log why we're not using dynamic options for this parameter
1340
- if (parameter.type === 'string' && parameter.name === 'model') {
1341
- }
1342
-
1343
- // Special case for group_id parameter - add Load Groups button
1344
- if (parameter.name === 'group_id') {
1345
- const storedGroupName = allParameters?.group_name || '';
1346
- return (
1347
- <GroupIdSelector
1348
- value={currentValue || ''}
1349
- onChange={onChange}
1350
- onNameChange={(name) => onParameterChange?.('group_name', name)}
1351
- storedName={storedGroupName}
1352
- placeholder={parameter.placeholder}
1353
- theme={theme}
1354
- isDragOver={isDragOver}
1355
- onDragOver={handleDragOver}
1356
- onDragLeave={handleDragLeave}
1357
- onDrop={handleDrop}
1358
- />
1359
- );
1360
- }
1361
-
1362
- // Special case for senderNumber parameter - add Load Members button (uses group_id)
1363
- if (parameter.name === 'senderNumber') {
1364
- const groupId = resolvedParameters?.group_id || allParameters?.group_id || '';
1365
- const storedSenderName = allParameters?.sender_name || '';
1366
- return (
1367
- <SenderNumberSelector
1368
- value={currentValue || ''}
1369
- onChange={onChange}
1370
- onNameChange={(name) => onParameterChange?.('sender_name', name)}
1371
- storedName={storedSenderName}
1372
- placeholder={parameter.placeholder}
1373
- theme={theme}
1374
- isDragOver={isDragOver}
1375
- onDragOver={handleDragOver}
1376
- onDragLeave={handleDragLeave}
1377
- onDrop={handleDrop}
1378
- groupId={groupId}
1379
- />
1380
- );
1381
- }
1382
-
1383
- return (
1384
- <input
1385
- type={isPassword ? "password" : "text"}
1386
- value={currentValue || ''}
1387
- onChange={(e) => onChange(e.target.value)}
1388
- placeholder={parameter.placeholder}
1389
- onDragOver={handleDragOver}
1390
- onDragLeave={handleDragLeave}
1391
- onDrop={handleDrop}
1392
- style={{
1393
- width: '100%',
1394
- padding: '10px 12px',
1395
- border: isDragOver ? `2px solid ${theme.accent.cyan}` : `1px solid ${theme.colors.border}`,
1396
- borderRadius: '6px',
1397
- fontSize: '14px',
1398
- backgroundColor: isDragOver ? `${theme.accent.cyan}10` : theme.colors.backgroundAlt,
1399
- color: currentValue && currentValue.includes('{{') ? theme.accent.yellow : theme.colors.text,
1400
- outline: 'none',
1401
- transition: 'all 0.2s ease',
1402
- fontFamily: currentValue && currentValue.includes('{{') ? 'monospace' : 'system-ui, sans-serif',
1403
- boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.05)'
1404
- }}
1405
- onFocus={(e) => {
1406
- e.target.style.borderColor = theme.accent.cyan;
1407
- e.target.style.boxShadow = `0 0 0 3px ${theme.accent.cyan}20, inset 0 1px 2px rgba(0,0,0,0.05)`;
1408
- }}
1409
- onBlur={(e) => {
1410
- e.target.style.borderColor = theme.colors.border;
1411
- e.target.style.boxShadow = 'inset 0 1px 2px rgba(0,0,0,0.05)';
1412
- }}
1413
- />
1414
- );
1415
-
1416
- case 'number':
1417
-
1418
- return (
1419
- <input
1420
- type="number"
1421
- value={currentValue !== undefined ? currentValue : (parameter.default || 0)}
1422
- onChange={(e) => onChange(Number(e.target.value))}
1423
- min={getMin()}
1424
- max={getMax()}
1425
- step={getStep()}
1426
- onDragOver={handleDragOver}
1427
- onDragLeave={handleDragLeave}
1428
- onDrop={handleDrop}
1429
- style={{
1430
- width: '100%',
1431
- padding: '8px 12px',
1432
- border: isDragOver ? `2px solid ${theme.colors.focus}` : `1px solid ${theme.colors.border}`,
1433
- borderRadius: '6px',
1434
- fontSize: '14px',
1435
- backgroundColor: isDragOver ? theme.colors.focusRing : theme.colors.background,
1436
- color: theme.colors.text,
1437
- outline: 'none',
1438
- transition: 'all 0.2s ease',
1439
- fontFamily: 'system-ui, sans-serif'
1440
- }}
1441
- onFocus={(e) => e.target.style.borderColor = theme.colors.focus}
1442
- onBlur={(e) => e.target.style.borderColor = theme.colors.border}
1443
- />
1444
- );
1445
-
1446
- case 'boolean':
1447
- return (
1448
- <label style={{
1449
- display: 'flex',
1450
- alignItems: 'center',
1451
- gap: '8px',
1452
- cursor: 'pointer',
1453
- fontSize: '14px',
1454
- fontFamily: 'system-ui, sans-serif',
1455
- color: theme.colors.text
1456
- }}>
1457
- <input
1458
- type="checkbox"
1459
- checked={currentValue || false}
1460
- onChange={(e) => onChange(e.target.checked)}
1461
- style={{ width: '16px', height: '16px', accentColor: theme.colors.focus }}
1462
- />
1463
- {parameter.displayName}
1464
- </label>
1465
- );
1466
-
1467
- case 'select':
1468
- case 'options':
1469
- // Use dynamic options if available, otherwise use static options
1470
- const optionsToRender = dynamicOptions.length > 0 ? dynamicOptions : (parameter.options || []);
1471
- const selectOptions = optionsToRender.filter((option): option is import('../types/INodeProperties').INodePropertyOption =>
1472
- 'value' in option
1473
- );
1474
-
1475
- return (
1476
- <select
1477
- value={currentValue || parameter.default}
1478
- onChange={(e) => onChange(e.target.value)}
1479
- style={{
1480
- width: '100%',
1481
- padding: '10px 12px',
1482
- border: `1px solid ${theme.colors.border}`,
1483
- borderRadius: '6px',
1484
- fontSize: '14px',
1485
- outline: 'none',
1486
- transition: 'all 0.2s ease',
1487
- cursor: 'pointer',
1488
- backgroundColor: theme.colors.backgroundAlt,
1489
- color: theme.colors.text,
1490
- fontFamily: 'system-ui, sans-serif',
1491
- boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.05)',
1492
- appearance: 'none',
1493
- backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23657b83' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E")`,
1494
- backgroundRepeat: 'no-repeat',
1495
- backgroundPosition: 'right 12px center',
1496
- paddingRight: '36px'
1497
- }}
1498
- onFocus={(e) => {
1499
- e.target.style.borderColor = theme.accent.cyan;
1500
- e.target.style.boxShadow = `0 0 0 3px ${theme.accent.cyan}20, inset 0 1px 2px rgba(0,0,0,0.05)`;
1501
- }}
1502
- onBlur={(e) => {
1503
- e.target.style.borderColor = theme.colors.border;
1504
- e.target.style.boxShadow = 'inset 0 1px 2px rgba(0,0,0,0.05)';
1505
- }}
1506
- >
1507
- {selectOptions.map((option) => (
1508
- <option key={String(option.value)} value={String(option.value)}>
1509
- {option.label || option.name || String(option.value)}
1510
- </option>
1511
- ))}
1512
- </select>
1513
- );
1514
-
1515
- case 'slider':
1516
- return (
1517
- <div>
1518
- <input
1519
- type="range"
1520
- min={getMin()}
1521
- max={getMax()}
1522
- step={getStep()}
1523
- value={currentValue !== undefined ? currentValue : (parameter.default || 0)}
1524
- onChange={(e) => onChange(Number(e.target.value))}
1525
- style={{
1526
- width: '100%',
1527
- height: '8px',
1528
- backgroundColor: theme.colors.backgroundAlt,
1529
- borderRadius: '8px',
1530
- outline: 'none',
1531
- accentColor: theme.colors.focus
1532
- }}
1533
- />
1534
- <div style={{
1535
- textAlign: 'center',
1536
- fontSize: '12px',
1537
- color: theme.colors.textSecondary,
1538
- marginTop: '4px',
1539
- fontFamily: 'system-ui, sans-serif'
1540
- }}>
1541
- {currentValue !== undefined ? currentValue : (parameter.default || 0)}
1542
- {parameter.type === 'slider' ? '%' : ''}
1543
- </div>
1544
- </div>
1545
- );
1546
-
1547
- case 'percentage':
1548
- return (
1549
- <div>
1550
- <input
1551
- type="range"
1552
- min={getMin()}
1553
- max={getMax()}
1554
- step={getStep()}
1555
- value={currentValue !== undefined ? currentValue : (parameter.default || 0)}
1556
- onChange={(e) => onChange(Number(e.target.value))}
1557
- style={{
1558
- width: '100%',
1559
- height: '8px',
1560
- backgroundColor: theme.colors.backgroundAlt,
1561
- borderRadius: '8px',
1562
- outline: 'none',
1563
- accentColor: theme.colors.success
1564
- }}
1565
- />
1566
- <div style={{
1567
- textAlign: 'center',
1568
- fontSize: '12px',
1569
- color: theme.colors.textSecondary,
1570
- marginTop: '4px',
1571
- fontFamily: 'system-ui, sans-serif'
1572
- }}>
1573
- {currentValue !== undefined ? currentValue : (parameter.default || 0)}%
1574
- </div>
1575
- </div>
1576
- );
1577
-
1578
- case 'text':
1579
- return (
1580
- <input
1581
- type="text"
1582
- value={currentValue || ''}
1583
- onChange={(e) => onChange(e.target.value)}
1584
- placeholder={parameter.placeholder}
1585
- onDragOver={handleDragOver}
1586
- onDragLeave={handleDragLeave}
1587
- onDrop={handleDrop}
1588
- style={{
1589
- width: '100%',
1590
- padding: '8px 12px',
1591
- border: isDragOver ? `2px solid ${theme.colors.focus}` : `1px solid ${theme.colors.border}`,
1592
- borderRadius: '6px',
1593
- fontSize: '14px',
1594
- backgroundColor: isDragOver ? theme.colors.focusRing : theme.colors.background,
1595
- color: currentValue && currentValue.includes('{{') ? theme.colors.templateVariable : theme.colors.text,
1596
- outline: 'none',
1597
- transition: 'all 0.2s ease',
1598
- fontFamily: currentValue && currentValue.includes('{{') ? 'monospace' : 'system-ui, sans-serif'
1599
- }}
1600
- onFocus={(e) => e.target.style.borderColor = theme.colors.focus}
1601
- onBlur={(e) => e.target.style.borderColor = theme.colors.border}
1602
- />
1603
- );
1604
-
1605
- case 'file':
1606
- const fileInputRef = React.useRef<HTMLInputElement>(null);
1607
-
1608
- const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
1609
- const file = e.target.files?.[0];
1610
- if (!file) return;
1611
-
1612
- const reader = new FileReader();
1613
- reader.onload = () => {
1614
- const base64 = (reader.result as string).split(',')[1]; // Remove data:mime;base64, prefix
1615
- // Store as object with base64 data, filename, and mime type
1616
- onChange({
1617
- type: 'upload',
1618
- data: base64,
1619
- filename: file.name,
1620
- mimeType: file.type || 'application/octet-stream'
1621
- });
1622
- };
1623
- reader.readAsDataURL(file);
1624
- };
1625
-
1626
- const isUploadedFile = currentValue && typeof currentValue === 'object' && currentValue.type === 'upload';
1627
-
1628
- // Determine file accept type based on context (e.g., messageType for WhatsApp)
1629
- const getFileAcceptType = () => {
1630
- const messageType = allParameters?.messageType;
1631
- if (messageType) {
1632
- switch (messageType) {
1633
- case 'image':
1634
- return 'image/*';
1635
- case 'video':
1636
- return 'video/*';
1637
- case 'audio':
1638
- return 'audio/*,.ogg,.opus,.mp3,.wav,.m4a';
1639
- case 'document':
1640
- return '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.csv,.zip,.rar';
1641
- case 'sticker':
1642
- return 'image/webp,.webp';
1643
- default:
1644
- return (parameter as any).typeOptions?.accept || '*/*';
1645
- }
1646
- }
1647
- return (parameter as any).typeOptions?.accept || '*/*';
1648
- };
1649
-
1650
- return (
1651
- <div>
1652
- <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
1653
- <input
1654
- type="text"
1655
- value={isUploadedFile ? `[Uploaded] ${currentValue.filename}` : (currentValue || '')}
1656
- onChange={(e) => onChange(e.target.value)}
1657
- placeholder={parameter.placeholder || 'Enter file path or upload'}
1658
- onDragOver={handleDragOver}
1659
- onDragLeave={handleDragLeave}
1660
- onDrop={handleDrop}
1661
- readOnly={isUploadedFile}
1662
- style={{
1663
- flex: 1,
1664
- padding: '8px 12px',
1665
- border: isDragOver ? `2px solid ${theme.colors.focus}` : `1px solid ${theme.colors.border}`,
1666
- borderRadius: '6px',
1667
- fontSize: '13px',
1668
- backgroundColor: isUploadedFile ? theme.colors.backgroundAlt : (isDragOver ? theme.colors.focusRing : theme.colors.background),
1669
- color: isUploadedFile ? theme.colors.success : (currentValue && currentValue.includes?.('{{') ? theme.colors.templateVariable : theme.colors.text),
1670
- outline: 'none',
1671
- transition: 'all 0.2s ease',
1672
- fontFamily: 'monospace'
1673
- }}
1674
- onFocus={(e) => e.target.style.borderColor = theme.colors.focus}
1675
- onBlur={(e) => e.target.style.borderColor = theme.colors.border}
1676
- />
1677
- <input
1678
- key={`file-input-${allParameters?.messageType || 'default'}`}
1679
- ref={fileInputRef}
1680
- type="file"
1681
- onChange={handleFileUpload}
1682
- style={{ display: 'none' }}
1683
- accept={getFileAcceptType()}
1684
- />
1685
- <button
1686
- onClick={() => fileInputRef.current?.click()}
1687
- style={{
1688
- padding: '8px 12px',
1689
- border: `1px solid ${theme.colors.focus}40`,
1690
- borderRadius: '6px',
1691
- backgroundColor: `${theme.colors.focus}18`,
1692
- color: theme.colors.focus,
1693
- cursor: 'pointer',
1694
- fontSize: '13px',
1695
- fontWeight: 600,
1696
- transition: 'all 0.2s ease',
1697
- whiteSpace: 'nowrap'
1698
- }}
1699
- onMouseEnter={(e) => {
1700
- e.currentTarget.style.backgroundColor = `${theme.colors.focus}30`;
1701
- }}
1702
- onMouseLeave={(e) => {
1703
- e.currentTarget.style.backgroundColor = `${theme.colors.focus}18`;
1704
- }}
1705
- title="Upload file"
1706
- >
1707
- Upload
1708
- </button>
1709
- {isUploadedFile && (
1710
- <button
1711
- onClick={() => {
1712
- onChange('');
1713
- if (fileInputRef.current) fileInputRef.current.value = '';
1714
- }}
1715
- style={{
1716
- padding: '8px 10px',
1717
- border: `1px solid ${theme.colors.error}40`,
1718
- borderRadius: '6px',
1719
- backgroundColor: `${theme.colors.error}18`,
1720
- color: theme.colors.error,
1721
- cursor: 'pointer',
1722
- fontSize: '13px',
1723
- fontWeight: 600,
1724
- transition: 'all 0.2s ease'
1725
- }}
1726
- onMouseEnter={(e) => {
1727
- e.currentTarget.style.backgroundColor = `${theme.colors.error}30`;
1728
- }}
1729
- onMouseLeave={(e) => {
1730
- e.currentTarget.style.backgroundColor = `${theme.colors.error}18`;
1731
- }}
1732
- title="Clear uploaded file"
1733
- >
1734
- X
1735
- </button>
1736
- )}
1737
- </div>
1738
- <div style={{
1739
- fontSize: '11px',
1740
- color: theme.colors.textSecondary,
1741
- marginTop: '4px',
1742
- fontStyle: 'italic'
1743
- }}>
1744
- {isUploadedFile
1745
- ? `Size: ${(currentValue.data.length * 0.75 / 1024).toFixed(1)} KB | Type: ${currentValue.mimeType}`
1746
- : 'Enter server path or click Upload to select a file'}
1747
- </div>
1748
- </div>
1749
- );
1750
-
1751
- case 'array':
1752
- const arrayValue = Array.isArray(currentValue) ? currentValue : [];
1753
- return (
1754
- <div>
1755
- <div style={{
1756
- border: `1px solid ${theme.colors.border}`,
1757
- borderRadius: '6px',
1758
- backgroundColor: theme.colors.background,
1759
- maxHeight: '120px',
1760
- overflowY: 'auto'
1761
- }}>
1762
- {parameter.options?.map((option) => (
1763
- <label key={option.value} style={{
1764
- display: 'flex',
1765
- alignItems: 'center',
1766
- gap: '8px',
1767
- padding: '8px 12px',
1768
- cursor: 'pointer',
1769
- fontSize: '14px',
1770
- fontFamily: 'system-ui, sans-serif',
1771
- color: theme.colors.text,
1772
- borderBottom: `1px solid ${theme.colors.border}`
1773
- }}>
1774
- <input
1775
- type="checkbox"
1776
- checked={arrayValue.includes(option.value)}
1777
- onChange={(e) => {
1778
- if (e.target.checked) {
1779
- onChange([...arrayValue, option.value]);
1780
- } else {
1781
- onChange(arrayValue.filter((v: any) => v !== option.value));
1782
- }
1783
- }}
1784
- style={{ width: '16px', height: '16px', accentColor: theme.colors.focus }}
1785
- />
1786
- {option.label}
1787
- </label>
1788
- ))}
1789
- </div>
1790
- <div style={{
1791
- fontSize: '11px',
1792
- color: theme.colors.textSecondary,
1793
- marginTop: '4px'
1794
- }}>
1795
- Selected: {arrayValue.length} item{arrayValue.length !== 1 ? 's' : ''}
1796
- </div>
1797
- </div>
1798
- );
1799
-
1800
- case 'collection':
1801
- return <CollectionRenderer parameter={parameter} value={currentValue} onChange={onChange} allParameters={allParameters} theme={theme} />;
1802
-
1803
- case 'fixedCollection':
1804
- return <FixedCollectionRenderer parameter={parameter} value={currentValue} onChange={onChange} allParameters={allParameters} theme={theme} />;
1805
-
1806
- case 'notice':
1807
- // Info/notice display - shows informational text without input
1808
- return (
1809
- <div style={{
1810
- padding: '10px 12px',
1811
- backgroundColor: theme.colors.backgroundAlt,
1812
- border: `1px solid ${theme.colors.border}`,
1813
- borderRadius: '6px',
1814
- fontSize: '13px',
1815
- color: theme.colors.textSecondary,
1816
- lineHeight: '1.5'
1817
- }}>
1818
- {parameter.default || parameter.description || ''}
1819
- </div>
1820
- );
1821
-
1822
- default:
1823
- return <div style={{ color: theme.colors.error, fontSize: '14px', padding: '8px 12px', backgroundColor: `${theme.colors.error}15`, border: `1px solid ${theme.colors.error}30`, borderRadius: '6px' }}>Unsupported parameter type: {parameter.type}</div>;
1824
- }
1825
- };
1826
-
1827
- return (
1828
- <div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
1829
- {parameter.type !== 'boolean' && (
1830
- <label style={{
1831
- display: 'flex',
1832
- alignItems: 'center',
1833
- gap: '6px',
1834
- marginBottom: '8px',
1835
- fontSize: '13px',
1836
- fontWeight: 600,
1837
- color: theme.colors.text,
1838
- fontFamily: 'system-ui, sans-serif',
1839
- flexShrink: 0
1840
- }}>
1841
- <span>{parameter.displayName}</span>
1842
- {parameter.required && (
1843
- <span style={{
1844
- color: theme.accent.red,
1845
- fontSize: '14px',
1846
- fontWeight: 700
1847
- }}>*</span>
1848
- )}
1849
- </label>
1850
- )}
1851
-
1852
- <div style={{ flex: 1, minHeight: 0 }}>
1853
- {renderInput()}
1854
- </div>
1855
-
1856
- {parameter.description && (
1857
- <div style={{
1858
- fontSize: '12px',
1859
- color: theme.colors.textSecondary,
1860
- marginTop: '6px',
1861
- lineHeight: '1.5',
1862
- fontFamily: 'system-ui, sans-serif',
1863
- paddingLeft: '2px',
1864
- flexShrink: 0
1865
- }}>
1866
- {parameter.description}
1867
- </div>
1868
- )}
1869
-
1870
- </div>
1871
- );
1872
- };
1873
-
1
+ import React, { useState, useEffect } from 'react';
2
+ import { NodeParameter } from '../types/NodeTypes';
3
+ import { INodeProperties, INodePropertyOption } from '../types/INodeProperties';
4
+ import APIKeyValidator from './APIKeyValidator';
5
+ import CodeEditor from './ui/CodeEditor';
6
+ import DynamicParameterService from '../services/dynamicParameterService';
7
+ import { useAppStore } from '../store/useAppStore';
8
+ import { ANDROID_SERVICE_NODE_TYPES } from '../nodeDefinitions/androidServiceNodes';
9
+ import { nodeDefinitions } from '../nodeDefinitions';
10
+ import { useAppTheme } from '../hooks/useAppTheme';
11
+ import { API_CONFIG } from '../config/api';
12
+ import { useWebSocket } from '../contexts/WebSocketContext';
13
+ import { useApiKeys } from '../hooks/useApiKeys';
14
+
15
+ // Map node types to provider keys for AI model nodes
16
+ const NODE_TYPE_TO_PROVIDER: Record<string, string> = {
17
+ 'openaiChatModel': 'openai',
18
+ 'anthropicChatModel': 'anthropic',
19
+ 'claudeChatModel': 'anthropic',
20
+ 'googleChatModel': 'gemini',
21
+ 'geminiChatModel': 'gemini',
22
+ 'azureChatModel': 'azure_openai',
23
+ 'cohereChatModel': 'cohere',
24
+ 'ollamaChatModel': 'ollama',
25
+ 'mistralChatModel': 'mistral',
26
+ 'openrouterChatModel': 'openrouter',
27
+ 'groqChatModel': 'groq',
28
+ 'cerebrasChatModel': 'cerebras'
29
+ };
30
+
31
+ // Collection Renderer - n8n official style
32
+ const CollectionRenderer: React.FC<{
33
+ parameter: any;
34
+ value: any;
35
+ onChange: (value: any) => void;
36
+ allParameters?: Record<string, any>;
37
+ theme: ReturnType<typeof useAppTheme>;
38
+ }> = ({ parameter, value, onChange, allParameters, theme }) => {
39
+ const [showAddOption, setShowAddOption] = useState(false);
40
+ const currentValue = value || {};
41
+ const addedOptions = Object.keys(currentValue).filter(key => currentValue[key] !== undefined);
42
+ const availableOptions = parameter.options?.filter((opt: any) => !addedOptions.includes(opt.name)) || [];
43
+
44
+ const addOption = (optionName: string) => {
45
+ const option = parameter.options?.find((opt: any) => opt.name === optionName);
46
+ if (option) {
47
+ onChange({
48
+ ...currentValue,
49
+ [optionName]: option.default
50
+ });
51
+ setShowAddOption(false);
52
+ }
53
+ };
54
+
55
+ const removeOption = (optionName: string) => {
56
+ const newValue = { ...currentValue };
57
+ delete newValue[optionName];
58
+ onChange(newValue);
59
+ };
60
+
61
+ const updateOption = (optionName: string, optionValue: any) => {
62
+ onChange({
63
+ ...currentValue,
64
+ [optionName]: optionValue
65
+ });
66
+ };
67
+
68
+ return (
69
+ <div>
70
+ {addedOptions.length === 0 && (
71
+ <div style={{
72
+ fontSize: '14px',
73
+ color: theme.colors.textSecondary,
74
+ marginBottom: '12px',
75
+ padding: '8px 0'
76
+ }}>
77
+ No properties
78
+ </div>
79
+ )}
80
+
81
+ {addedOptions.map((optionName) => {
82
+ const option = parameter.options?.find((opt: any) => opt.name === optionName);
83
+ if (!option) return null;
84
+
85
+ return (
86
+ <div key={optionName} style={{
87
+ marginBottom: '16px',
88
+ padding: '12px',
89
+ border: `1px solid ${theme.colors.border}`,
90
+ borderRadius: '4px',
91
+ backgroundColor: theme.colors.backgroundAlt,
92
+ position: 'relative'
93
+ }}>
94
+ <button
95
+ onClick={() => removeOption(optionName)}
96
+ style={{
97
+ position: 'absolute',
98
+ top: '6px',
99
+ right: '6px',
100
+ background: 'none',
101
+ border: 'none',
102
+ color: theme.colors.textSecondary,
103
+ cursor: 'pointer',
104
+ fontSize: '14px',
105
+ padding: '2px 4px',
106
+ borderRadius: '2px'
107
+ }}
108
+ onMouseEnter={(e) => e.currentTarget.style.backgroundColor = theme.colors.border}
109
+ onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
110
+ title="Remove"
111
+ >
112
+
113
+ </button>
114
+ <ParameterRenderer
115
+ parameter={option}
116
+ value={currentValue[optionName]}
117
+ onChange={(newValue) => updateOption(optionName, newValue)}
118
+ allParameters={allParameters}
119
+ />
120
+ </div>
121
+ );
122
+ })}
123
+
124
+ {availableOptions.length > 0 && (
125
+ <div style={{ position: 'relative' }}>
126
+ <button
127
+ onClick={() => setShowAddOption(!showAddOption)}
128
+ style={{
129
+ width: '100%',
130
+ padding: '10px 12px',
131
+ border: `1px solid ${theme.colors.border}`,
132
+ borderRadius: '4px',
133
+ backgroundColor: theme.colors.backgroundAlt,
134
+ color: theme.colors.textSecondary,
135
+ cursor: 'pointer',
136
+ fontSize: '14px',
137
+ display: 'flex',
138
+ alignItems: 'center',
139
+ justifyContent: 'space-between',
140
+ transition: 'all 0.2s ease'
141
+ }}
142
+ onMouseEnter={(e) => e.currentTarget.style.backgroundColor = theme.colors.borderHover}
143
+ onMouseLeave={(e) => e.currentTarget.style.backgroundColor = theme.colors.backgroundAlt}
144
+ >
145
+ {parameter.placeholder || 'Add Option'}
146
+ <span style={{
147
+ transform: showAddOption ? 'rotate(180deg)' : 'rotate(0deg)',
148
+ transition: 'transform 0.2s ease'
149
+ }}>
150
+
151
+ </span>
152
+ </button>
153
+
154
+ {showAddOption && (
155
+ <div style={{
156
+ position: 'absolute',
157
+ top: '100%',
158
+ left: 0,
159
+ right: 0,
160
+ zIndex: 1000,
161
+ backgroundColor: theme.colors.background,
162
+ border: `1px solid ${theme.colors.border}`,
163
+ borderRadius: '4px',
164
+ marginTop: '2px',
165
+ boxShadow: `0 4px 6px -1px ${theme.colors.shadow}`,
166
+ maxHeight: '200px',
167
+ overflowY: 'auto'
168
+ }}>
169
+ {availableOptions.map((option: any, index: number) => (
170
+ <button
171
+ key={option.name}
172
+ onClick={() => addOption(option.name)}
173
+ style={{
174
+ width: '100%',
175
+ padding: '10px 12px',
176
+ border: 'none',
177
+ backgroundColor: 'transparent',
178
+ color: theme.colors.text,
179
+ cursor: 'pointer',
180
+ fontSize: '14px',
181
+ textAlign: 'left',
182
+ borderBottom: index < availableOptions.length - 1 ? `1px solid ${theme.colors.border}` : 'none'
183
+ }}
184
+ onMouseEnter={(e) => e.currentTarget.style.backgroundColor = theme.colors.backgroundAlt}
185
+ onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
186
+ >
187
+ <div style={{ fontWeight: '500' }}>
188
+ {option.displayName}
189
+ </div>
190
+ {option.description && (
191
+ <div style={{
192
+ fontSize: '12px',
193
+ color: theme.colors.textSecondary,
194
+ marginTop: '2px'
195
+ }}>
196
+ {option.description}
197
+ </div>
198
+ )}
199
+ </button>
200
+ ))}
201
+ </div>
202
+ )}
203
+ </div>
204
+ )}
205
+ </div>
206
+ );
207
+ };
208
+
209
+ // Group ID Selector - with Load Groups button and dropdown
210
+ const GroupIdSelector: React.FC<{
211
+ value: string;
212
+ onChange: (value: string) => void;
213
+ onNameChange?: (name: string) => void;
214
+ storedName?: string;
215
+ placeholder?: string;
216
+ theme: ReturnType<typeof useAppTheme>;
217
+ isDragOver: boolean;
218
+ onDragOver: (e: React.DragEvent) => void;
219
+ onDragLeave: (e: React.DragEvent) => void;
220
+ onDrop: (e: React.DragEvent) => void;
221
+ }> = ({ value, onChange, onNameChange, storedName, placeholder, theme, isDragOver, onDragOver, onDragLeave, onDrop }) => {
222
+ const [groups, setGroups] = useState<Array<{ jid: string; name: string; topic?: string; size?: number; is_community?: boolean }>>([]);
223
+ const [isLoading, setIsLoading] = useState(false);
224
+ const [showDropdown, setShowDropdown] = useState(false);
225
+ const [error, setError] = useState<string | null>(null);
226
+ // Use stored name if available, otherwise local state
227
+ const [localGroupName, setLocalGroupName] = useState<string | null>(null);
228
+ const selectedGroupName = storedName || localGroupName;
229
+ const { getWhatsAppGroups } = useWebSocket();
230
+
231
+ // Sync local state with stored name
232
+ useEffect(() => {
233
+ if (storedName) {
234
+ setLocalGroupName(storedName);
235
+ }
236
+ }, [storedName]);
237
+
238
+ const handleLoadGroups = async () => {
239
+ setIsLoading(true);
240
+ setError(null);
241
+ try {
242
+ const result = await getWhatsAppGroups();
243
+ console.log('[GroupIdSelector] Raw groups from API:', result.groups?.map(g => ({ name: g.name, jid: g.jid, is_community: g.is_community })));
244
+ if (result.success && result.groups.length > 0) {
245
+ // Filter out communities - they don't have regular chat history
246
+ const regularGroups = result.groups.filter(g => !g.is_community);
247
+ console.log('[GroupIdSelector] After filtering communities:', regularGroups.length, 'groups remaining');
248
+ if (regularGroups.length === 0) {
249
+ setError('Only communities found (no chat history available)');
250
+ return;
251
+ }
252
+ setGroups(regularGroups);
253
+ setShowDropdown(true);
254
+ // If we already have a value, try to find its name and update storage
255
+ if (value) {
256
+ const matchingGroup = regularGroups.find(g => g.jid === value);
257
+ if (matchingGroup && matchingGroup.name !== storedName) {
258
+ setLocalGroupName(matchingGroup.name);
259
+ onNameChange?.(matchingGroup.name);
260
+ }
261
+ }
262
+ } else if (result.error) {
263
+ setError(result.error);
264
+ } else {
265
+ setError('No groups found');
266
+ }
267
+ } catch (err: any) {
268
+ setError(err.message || 'Failed to load groups');
269
+ } finally {
270
+ setIsLoading(false);
271
+ }
272
+ };
273
+
274
+ const handleSelectGroup = (group: { jid: string; name: string }) => {
275
+ onChange(group.jid);
276
+ setLocalGroupName(group.name);
277
+ onNameChange?.(group.name);
278
+ setShowDropdown(false);
279
+ };
280
+
281
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
282
+ onChange(e.target.value);
283
+ // Clear group name when user types manually
284
+ setLocalGroupName(null);
285
+ onNameChange?.('');
286
+ };
287
+
288
+ // Display value: show group name if selected, otherwise show JID
289
+ const displayValue = selectedGroupName || value;
290
+ const isGroupSelected = selectedGroupName !== null && value;
291
+
292
+ return (
293
+ <div style={{ position: 'relative' }}>
294
+ <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
295
+ <input
296
+ type="text"
297
+ value={displayValue}
298
+ onChange={handleInputChange}
299
+ placeholder={placeholder || '123456789@g.us'}
300
+ onDragOver={onDragOver}
301
+ onDragLeave={onDragLeave}
302
+ onDrop={onDrop}
303
+ style={{
304
+ flex: 1,
305
+ padding: '8px 12px',
306
+ border: isDragOver ? `2px solid ${theme.colors.focus}` : `1px solid ${theme.colors.border}`,
307
+ borderRadius: '6px',
308
+ fontSize: '14px',
309
+ backgroundColor: isGroupSelected ? theme.colors.backgroundAlt : (isDragOver ? theme.colors.focusRing : theme.colors.background),
310
+ color: isGroupSelected ? theme.colors.success : (value && value.includes('{{') ? theme.colors.templateVariable : theme.colors.text),
311
+ outline: 'none',
312
+ transition: 'all 0.2s ease',
313
+ fontFamily: value && value.includes('{{') ? 'monospace' : 'system-ui, sans-serif',
314
+ fontWeight: isGroupSelected ? '500' : 'normal'
315
+ }}
316
+ onFocus={(e) => e.target.style.borderColor = theme.colors.focus}
317
+ onBlur={(e) => e.target.style.borderColor = theme.colors.border}
318
+ />
319
+ <button
320
+ onClick={handleLoadGroups}
321
+ disabled={isLoading}
322
+ style={{
323
+ padding: '8px 12px',
324
+ border: `1px solid ${isLoading ? theme.colors.border : `${theme.colors.focus}40`}`,
325
+ borderRadius: '6px',
326
+ backgroundColor: isLoading ? 'transparent' : `${theme.colors.focus}18`,
327
+ color: isLoading ? theme.colors.textMuted : theme.colors.focus,
328
+ cursor: isLoading ? 'wait' : 'pointer',
329
+ fontSize: '13px',
330
+ fontWeight: 600,
331
+ transition: 'all 0.2s ease',
332
+ whiteSpace: 'nowrap',
333
+ opacity: isLoading ? 0.7 : 1
334
+ }}
335
+ onMouseEnter={(e) => {
336
+ if (!isLoading) {
337
+ e.currentTarget.style.backgroundColor = `${theme.colors.focus}30`;
338
+ }
339
+ }}
340
+ onMouseLeave={(e) => {
341
+ e.currentTarget.style.backgroundColor = isLoading ? 'transparent' : `${theme.colors.focus}18`;
342
+ }}
343
+ title="Load WhatsApp groups"
344
+ >
345
+ {isLoading ? 'Loading...' : 'Load'}
346
+ </button>
347
+ </div>
348
+ {/* Show JID below when group name is displayed */}
349
+ {isGroupSelected && (
350
+ <div style={{
351
+ fontSize: '11px',
352
+ color: theme.colors.textSecondary,
353
+ marginTop: '4px',
354
+ fontFamily: 'monospace'
355
+ }}>
356
+ {value}
357
+ </div>
358
+ )}
359
+
360
+ {error && (
361
+ <div style={{
362
+ fontSize: '12px',
363
+ color: theme.colors.error,
364
+ marginTop: '4px'
365
+ }}>
366
+ {error}
367
+ </div>
368
+ )}
369
+
370
+ {showDropdown && groups.length > 0 && (
371
+ <div style={{
372
+ position: 'absolute',
373
+ top: '100%',
374
+ left: 0,
375
+ right: 0,
376
+ marginTop: '4px',
377
+ backgroundColor: theme.colors.background,
378
+ border: `1px solid ${theme.colors.border}`,
379
+ borderRadius: '6px',
380
+ boxShadow: `0 4px 12px ${theme.colors.shadow}`,
381
+ maxHeight: '200px',
382
+ overflowY: 'auto',
383
+ zIndex: 1000
384
+ }}>
385
+ {groups.map((group, index) => (
386
+ <button
387
+ key={group.jid}
388
+ onClick={() => handleSelectGroup(group)}
389
+ style={{
390
+ width: '100%',
391
+ padding: '10px 12px',
392
+ border: 'none',
393
+ backgroundColor: 'transparent',
394
+ color: theme.colors.text,
395
+ cursor: 'pointer',
396
+ fontSize: '13px',
397
+ textAlign: 'left',
398
+ borderBottom: index < groups.length - 1 ? `1px solid ${theme.colors.border}` : 'none'
399
+ }}
400
+ onMouseEnter={(e) => e.currentTarget.style.backgroundColor = theme.colors.backgroundAlt}
401
+ onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
402
+ >
403
+ <div style={{ fontWeight: '500' }}>{group.name}</div>
404
+ <div style={{
405
+ fontSize: '11px',
406
+ color: theme.colors.textSecondary,
407
+ marginTop: '2px',
408
+ fontFamily: 'monospace'
409
+ }}>
410
+ {group.jid}
411
+ {group.size && <span style={{ marginLeft: '8px' }}>({group.size} members)</span>}
412
+ </div>
413
+ </button>
414
+ ))}
415
+ <button
416
+ onClick={() => setShowDropdown(false)}
417
+ style={{
418
+ width: '100%',
419
+ padding: '8px 12px',
420
+ border: 'none',
421
+ borderTop: `1px solid ${theme.colors.border}`,
422
+ backgroundColor: theme.colors.backgroundAlt,
423
+ color: theme.colors.textSecondary,
424
+ cursor: 'pointer',
425
+ fontSize: '12px',
426
+ textAlign: 'center'
427
+ }}
428
+ onMouseEnter={(e) => e.currentTarget.style.color = theme.colors.text}
429
+ onMouseLeave={(e) => e.currentTarget.style.color = theme.colors.textSecondary}
430
+ >
431
+ Close
432
+ </button>
433
+ </div>
434
+ )}
435
+ </div>
436
+ );
437
+ };
438
+
439
+ // Sender Number Selector - with Load Members button and dropdown (loads from selected group)
440
+ const SenderNumberSelector: React.FC<{
441
+ value: string;
442
+ onChange: (value: string) => void;
443
+ onNameChange?: (name: string) => void;
444
+ storedName?: string;
445
+ placeholder?: string;
446
+ theme: ReturnType<typeof useAppTheme>;
447
+ isDragOver: boolean;
448
+ onDragOver: (e: React.DragEvent) => void;
449
+ onDragLeave: (e: React.DragEvent) => void;
450
+ onDrop: (e: React.DragEvent) => void;
451
+ groupId: string; // The selected group to load members from
452
+ }> = ({ value, onChange, onNameChange, storedName, placeholder, theme, isDragOver, onDragOver, onDragLeave, onDrop, groupId }) => {
453
+ const [members, setMembers] = useState<Array<{ phone: string; name: string; jid: string; is_admin?: boolean }>>([]);
454
+ const [isLoading, setIsLoading] = useState(false);
455
+ const [showDropdown, setShowDropdown] = useState(false);
456
+ const [error, setError] = useState<string | null>(null);
457
+ // Use stored name if available, otherwise local state
458
+ const [localMemberName, setLocalMemberName] = useState<string | null>(null);
459
+ const selectedMemberName = storedName || localMemberName;
460
+ const { getWhatsAppGroupInfo } = useWebSocket();
461
+
462
+ // Sync local state with stored name
463
+ useEffect(() => {
464
+ if (storedName) {
465
+ setLocalMemberName(storedName);
466
+ }
467
+ }, [storedName]);
468
+
469
+ const handleLoadMembers = async () => {
470
+ if (!groupId) {
471
+ setError('Select a group first');
472
+ return;
473
+ }
474
+
475
+ setIsLoading(true);
476
+ setError(null);
477
+ try {
478
+ const result = await getWhatsAppGroupInfo(groupId);
479
+ if (result.success && result.participants && result.participants.length > 0) {
480
+ setMembers(result.participants);
481
+ setShowDropdown(true);
482
+ // If we already have a value, try to find its name and update storage
483
+ if (value) {
484
+ const matchingMember = result.participants.find((m: any) => m.phone === value);
485
+ if (matchingMember) {
486
+ const name = matchingMember.name || matchingMember.phone;
487
+ if (name !== storedName) {
488
+ setLocalMemberName(name);
489
+ onNameChange?.(name);
490
+ }
491
+ }
492
+ }
493
+ } else if (result.error) {
494
+ setError(result.error);
495
+ } else {
496
+ setError('No members found');
497
+ }
498
+ } catch (err: any) {
499
+ setError(err.message || 'Failed to load members');
500
+ } finally {
501
+ setIsLoading(false);
502
+ }
503
+ };
504
+
505
+ const handleSelectMember = (member: { phone: string; name: string }) => {
506
+ const name = member.name || member.phone;
507
+ onChange(member.phone);
508
+ setLocalMemberName(name);
509
+ onNameChange?.(name);
510
+ setShowDropdown(false);
511
+ };
512
+
513
+ const handleClearSelection = () => {
514
+ onChange('');
515
+ setLocalMemberName(null);
516
+ onNameChange?.('');
517
+ setShowDropdown(false);
518
+ };
519
+
520
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
521
+ onChange(e.target.value);
522
+ // Clear member name when user types manually
523
+ setLocalMemberName(null);
524
+ onNameChange?.('');
525
+ };
526
+
527
+ // Display value: show member name if selected, otherwise show phone
528
+ const displayValue = selectedMemberName || value;
529
+ const isMemberSelected = selectedMemberName !== null && value;
530
+
531
+ return (
532
+ <div style={{ position: 'relative' }}>
533
+ <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
534
+ <input
535
+ type="text"
536
+ value={displayValue}
537
+ onChange={handleInputChange}
538
+ placeholder={placeholder || 'All members (leave empty)'}
539
+ onDragOver={onDragOver}
540
+ onDragLeave={onDragLeave}
541
+ onDrop={onDrop}
542
+ style={{
543
+ flex: 1,
544
+ padding: '8px 12px',
545
+ border: isDragOver ? `2px solid ${theme.colors.focus}` : `1px solid ${theme.colors.border}`,
546
+ borderRadius: '6px',
547
+ fontSize: '14px',
548
+ backgroundColor: isMemberSelected ? theme.colors.backgroundAlt : (isDragOver ? theme.colors.focusRing : theme.colors.background),
549
+ color: isMemberSelected ? theme.colors.success : (value && value.includes('{{') ? theme.colors.templateVariable : theme.colors.text),
550
+ outline: 'none',
551
+ transition: 'all 0.2s ease',
552
+ fontFamily: value && value.includes('{{') ? 'monospace' : 'system-ui, sans-serif',
553
+ fontWeight: isMemberSelected ? '500' : 'normal'
554
+ }}
555
+ onFocus={(e) => e.target.style.borderColor = theme.colors.focus}
556
+ onBlur={(e) => e.target.style.borderColor = theme.colors.border}
557
+ />
558
+ <button
559
+ onClick={handleLoadMembers}
560
+ disabled={isLoading || !groupId}
561
+ style={{
562
+ padding: '8px 12px',
563
+ border: `1px solid ${(isLoading || !groupId) ? theme.colors.border : `${theme.colors.focus}40`}`,
564
+ borderRadius: '6px',
565
+ backgroundColor: (isLoading || !groupId) ? 'transparent' : `${theme.colors.focus}18`,
566
+ color: (isLoading || !groupId) ? theme.colors.textMuted : theme.colors.focus,
567
+ cursor: (isLoading || !groupId) ? 'not-allowed' : 'pointer',
568
+ fontSize: '13px',
569
+ fontWeight: 600,
570
+ transition: 'all 0.2s ease',
571
+ whiteSpace: 'nowrap',
572
+ opacity: (isLoading || !groupId) ? 0.7 : 1
573
+ }}
574
+ onMouseEnter={(e) => {
575
+ if (!isLoading && groupId) {
576
+ e.currentTarget.style.backgroundColor = `${theme.colors.focus}30`;
577
+ }
578
+ }}
579
+ onMouseLeave={(e) => {
580
+ e.currentTarget.style.backgroundColor = (isLoading || !groupId) ? 'transparent' : `${theme.colors.focus}18`;
581
+ }}
582
+ title={groupId ? "Load group members" : "Select a group first"}
583
+ >
584
+ {isLoading ? 'Loading...' : 'Load'}
585
+ </button>
586
+ </div>
587
+ {/* Show phone below when member name is displayed */}
588
+ {isMemberSelected && (
589
+ <div style={{
590
+ fontSize: '11px',
591
+ color: theme.colors.textSecondary,
592
+ marginTop: '4px',
593
+ fontFamily: 'monospace'
594
+ }}>
595
+ {value}
596
+ </div>
597
+ )}
598
+
599
+ {error && (
600
+ <div style={{
601
+ fontSize: '12px',
602
+ color: theme.colors.error,
603
+ marginTop: '4px'
604
+ }}>
605
+ {error}
606
+ </div>
607
+ )}
608
+
609
+ {showDropdown && members.length > 0 && (
610
+ <div style={{
611
+ position: 'absolute',
612
+ top: '100%',
613
+ left: 0,
614
+ right: 0,
615
+ marginTop: '4px',
616
+ backgroundColor: theme.colors.background,
617
+ border: `1px solid ${theme.colors.border}`,
618
+ borderRadius: '6px',
619
+ boxShadow: `0 4px 12px ${theme.colors.shadow}`,
620
+ maxHeight: '250px',
621
+ overflowY: 'auto',
622
+ zIndex: 1000
623
+ }}>
624
+ {/* All Members option */}
625
+ <button
626
+ onClick={handleClearSelection}
627
+ style={{
628
+ width: '100%',
629
+ padding: '10px 12px',
630
+ border: 'none',
631
+ backgroundColor: !value ? theme.colors.backgroundAlt : 'transparent',
632
+ color: theme.colors.text,
633
+ cursor: 'pointer',
634
+ fontSize: '13px',
635
+ textAlign: 'left',
636
+ borderBottom: `1px solid ${theme.colors.border}`
637
+ }}
638
+ onMouseEnter={(e) => e.currentTarget.style.backgroundColor = theme.colors.backgroundAlt}
639
+ onMouseLeave={(e) => e.currentTarget.style.backgroundColor = !value ? theme.colors.backgroundAlt : 'transparent'}
640
+ >
641
+ <div style={{ fontWeight: '500', color: theme.colors.textSecondary }}>All Members</div>
642
+ <div style={{ fontSize: '11px', color: theme.colors.textMuted, marginTop: '2px' }}>
643
+ Receive from anyone in group
644
+ </div>
645
+ </button>
646
+ {members.map((member, index) => (
647
+ <button
648
+ key={member.jid || member.phone}
649
+ onClick={() => handleSelectMember(member)}
650
+ style={{
651
+ width: '100%',
652
+ padding: '10px 12px',
653
+ border: 'none',
654
+ backgroundColor: value === member.phone ? theme.colors.backgroundAlt : 'transparent',
655
+ color: theme.colors.text,
656
+ cursor: 'pointer',
657
+ fontSize: '13px',
658
+ textAlign: 'left',
659
+ borderBottom: index < members.length - 1 ? `1px solid ${theme.colors.border}` : 'none'
660
+ }}
661
+ onMouseEnter={(e) => e.currentTarget.style.backgroundColor = theme.colors.backgroundAlt}
662
+ onMouseLeave={(e) => e.currentTarget.style.backgroundColor = value === member.phone ? theme.colors.backgroundAlt : 'transparent'}
663
+ >
664
+ <div style={{ fontWeight: '500' }}>
665
+ {member.name || member.phone}
666
+ {member.is_admin && <span style={{ marginLeft: '8px', fontSize: '10px', color: theme.colors.warning }}>(Admin)</span>}
667
+ </div>
668
+ <div style={{
669
+ fontSize: '11px',
670
+ color: theme.colors.textSecondary,
671
+ marginTop: '2px',
672
+ fontFamily: 'monospace'
673
+ }}>
674
+ {member.phone}
675
+ </div>
676
+ </button>
677
+ ))}
678
+ <button
679
+ onClick={() => setShowDropdown(false)}
680
+ style={{
681
+ width: '100%',
682
+ padding: '8px 12px',
683
+ border: 'none',
684
+ borderTop: `1px solid ${theme.colors.border}`,
685
+ backgroundColor: theme.colors.backgroundAlt,
686
+ color: theme.colors.textSecondary,
687
+ cursor: 'pointer',
688
+ fontSize: '12px',
689
+ textAlign: 'center'
690
+ }}
691
+ onMouseEnter={(e) => e.currentTarget.style.color = theme.colors.text}
692
+ onMouseLeave={(e) => e.currentTarget.style.color = theme.colors.textSecondary}
693
+ >
694
+ Close
695
+ </button>
696
+ </div>
697
+ )}
698
+ </div>
699
+ );
700
+ };
701
+
702
+ // Fixed Collection Renderer - n8n style fixed collection
703
+ const FixedCollectionRenderer: React.FC<{
704
+ parameter: any;
705
+ value: any;
706
+ onChange: (value: any) => void;
707
+ allParameters?: Record<string, any>;
708
+ theme: ReturnType<typeof useAppTheme>;
709
+ }> = ({ parameter, value, onChange, allParameters, theme }) => {
710
+ const currentValue = value || {};
711
+
712
+ return (
713
+ <div style={{
714
+ border: `1px solid ${theme.colors.border}`,
715
+ borderRadius: '6px',
716
+ backgroundColor: theme.colors.backgroundAlt,
717
+ padding: '12px'
718
+ }}>
719
+ {parameter.options?.map((option: any) => {
720
+ const optionValue = currentValue[option.name] || {};
721
+
722
+ return (
723
+ <div key={option.name} style={{ marginBottom: '16px' }}>
724
+ <div style={{
725
+ fontSize: '14px',
726
+ fontWeight: '500',
727
+ color: theme.colors.text,
728
+ marginBottom: '8px'
729
+ }}>
730
+ {option.displayName}
731
+ </div>
732
+ <div style={{
733
+ border: `1px solid ${theme.colors.border}`,
734
+ borderRadius: '6px',
735
+ backgroundColor: theme.colors.background,
736
+ padding: '12px'
737
+ }}>
738
+ {option.values?.map((valueParam: any) => (
739
+ <ParameterRenderer
740
+ key={valueParam.name}
741
+ parameter={valueParam}
742
+ value={optionValue[valueParam.name]}
743
+ onChange={(newValue) => {
744
+ onChange({
745
+ ...currentValue,
746
+ [option.name]: {
747
+ ...optionValue,
748
+ [valueParam.name]: newValue
749
+ }
750
+ });
751
+ }}
752
+ allParameters={allParameters}
753
+ />
754
+ ))}
755
+ </div>
756
+ </div>
757
+ );
758
+ })}
759
+ </div>
760
+ );
761
+ };
762
+
763
+ interface ParameterRendererProps {
764
+ parameter: NodeParameter | INodeProperties;
765
+ value: any;
766
+ onChange: (value: any) => void;
767
+ allParameters?: Record<string, any>;
768
+ onParameterChange?: (paramName: string, value: any) => void;
769
+ onClosePanel?: () => void;
770
+ isLoadingParameters?: boolean;
771
+ }
772
+
773
+ // Type guard to check if parameter is INodeProperties
774
+ const isINodeProperties = (param: NodeParameter | INodeProperties): param is INodeProperties => {
775
+ return 'typeOptions' in param;
776
+ };
777
+
778
+ const ParameterRenderer: React.FC<ParameterRendererProps> = ({
779
+ parameter,
780
+ value,
781
+ onChange,
782
+ allParameters,
783
+ onParameterChange,
784
+ isLoadingParameters = false,
785
+ }) => {
786
+ const theme = useAppTheme();
787
+ // Don't use default while loading - wait for actual saved value to load
788
+ // This prevents showing template code briefly before saved code appears
789
+ const currentValue = isLoadingParameters ? (value ?? '') : (value !== undefined ? value : parameter.default);
790
+ const [isDragOver, setIsDragOver] = useState(false);
791
+ const [dynamicOptions, setDynamicOptions] = useState<INodePropertyOption[]>([]);
792
+ const [nodeParameters, setNodeParameters] = useState<Record<string, any>>({});
793
+
794
+ const { selectedNode } = useAppStore();
795
+ const { getNodeParameters } = useWebSocket();
796
+ const { getStoredApiKey, hasStoredKey, getStoredModels } = useApiKeys();
797
+
798
+ // Don't render hidden parameters
799
+ if (parameter.type === 'hidden') {
800
+ return null;
801
+ }
802
+
803
+ // Load node parameters for expression resolution
804
+ useEffect(() => {
805
+ const loadParameters = async () => {
806
+ if (selectedNode?.id) {
807
+ const result = await getNodeParameters(selectedNode.id);
808
+ if (result?.parameters) setNodeParameters(result.parameters);
809
+ }
810
+ };
811
+ loadParameters();
812
+ }, [selectedNode?.id, getNodeParameters]);
813
+
814
+ // Auto-load stored API key and models when provider changes
815
+ // Use ref to track previous provider to prevent infinite loops
816
+ const prevProviderRef = React.useRef<string | null>(null);
817
+ // Track if we've done initial auto-select after parameters loaded
818
+ const hasAutoSelectedRef = React.useRef(false);
819
+
820
+ // Reset auto-select tracking when node changes
821
+ useEffect(() => {
822
+ hasAutoSelectedRef.current = false;
823
+ prevProviderRef.current = null;
824
+ }, [selectedNode?.id]);
825
+
826
+ useEffect(() => {
827
+ const loadStoredKeyForProvider = async () => {
828
+ // Only run for apiKey or model parameters
829
+ if (parameter.name !== 'apiKey' && parameter.name !== 'model') return;
830
+
831
+ // Don't run while parameters are still loading from database
832
+ if (isLoadingParameters) return;
833
+
834
+ // Get provider from allParameters or derive from node type
835
+ let provider = allParameters?.provider;
836
+ if (!provider && selectedNode) {
837
+ const nodeType = selectedNode.type || selectedNode.data?.nodeType;
838
+ if (nodeType) {
839
+ provider = NODE_TYPE_TO_PROVIDER[nodeType];
840
+ }
841
+ }
842
+ if (!provider) return;
843
+
844
+ // Distinguish between initial load (prevProvider was null) and actual user-initiated provider change
845
+ // On initial load: respect saved model if it exists
846
+ // On provider change: reset to first model to prevent mismatched provider/model
847
+ const isInitialLoad = prevProviderRef.current === null;
848
+ const isActualProviderChange = !isInitialLoad && prevProviderRef.current !== provider;
849
+ const shouldAutoSelectModel = parameter.name === 'model' &&
850
+ (isActualProviderChange || isInitialLoad || !hasAutoSelectedRef.current);
851
+
852
+ // Skip if provider hasn't changed (except for initial model load)
853
+ if (!isActualProviderChange && !isInitialLoad && parameter.name !== 'model') return;
854
+ if (!isActualProviderChange && !isInitialLoad && hasAutoSelectedRef.current) return;
855
+
856
+ prevProviderRef.current = provider;
857
+
858
+ try {
859
+ const hasKey = await hasStoredKey(provider);
860
+
861
+ if (hasKey) {
862
+ // Auto-load API key for apiKey parameter - always update when provider changes
863
+ if (parameter.name === 'apiKey' && isActualProviderChange) {
864
+ const storedKey = await getStoredApiKey(provider);
865
+ if (storedKey) {
866
+ onChange(storedKey);
867
+ }
868
+ }
869
+
870
+ // Auto-load models for model parameter
871
+ if (shouldAutoSelectModel && selectedNode) {
872
+ const models = await getStoredModels(provider);
873
+ if (models?.length) {
874
+ const modelOptions = DynamicParameterService.createModelOptions(models);
875
+ DynamicParameterService.updateParameterOptions(selectedNode.id, 'model', modelOptions);
876
+
877
+ // Extract model ID (handles both string and object formats)
878
+ const getModelId = (model: any) => typeof model === 'string' ? model : model.id;
879
+ const firstModelId = getModelId(models[0]);
880
+
881
+ // When user actively changes provider, reset to first model
882
+ // to prevent mismatched provider/model combinations (e.g., OpenAI model with Anthropic provider)
883
+ if (isActualProviderChange) {
884
+ onChange(firstModelId);
885
+ } else {
886
+ // Initial load or no provider change - only auto-select if no saved model exists
887
+ const savedModel = value || allParameters?.model;
888
+ if (!savedModel || savedModel === '') {
889
+ onChange(firstModelId);
890
+ }
891
+ // If saved model exists, keep it (don't call onChange)
892
+ }
893
+ hasAutoSelectedRef.current = true;
894
+ }
895
+ }
896
+ } else {
897
+ // No stored key for this provider - clear the fields
898
+ if (parameter.name === 'apiKey') {
899
+ onChange('');
900
+ }
901
+ if (parameter.name === 'model') {
902
+ onChange('');
903
+ hasAutoSelectedRef.current = true;
904
+ }
905
+ }
906
+ } catch (error) {
907
+ console.warn('Error loading stored key info:', error);
908
+ }
909
+ };
910
+
911
+ loadStoredKeyForProvider();
912
+ }, [allParameters?.provider, parameter.name, hasStoredKey, getStoredApiKey, getStoredModels, selectedNode?.id, selectedNode?.type, onChange, isLoadingParameters, value, allParameters?.model]);
913
+
914
+ // Merge database params with current form params (current takes precedence)
915
+ const resolvedParameters = { ...nodeParameters, ...allParameters };
916
+
917
+ // Helper functions to get values from both interface types
918
+ const getMin = () => (parameter as any).min || (parameter as any).typeOptions?.minValue || 0;
919
+ const getMax = () => (parameter as any).max || (parameter as any).typeOptions?.maxValue || 100;
920
+ const getStep = () => (parameter as any).step || (parameter as any).typeOptions?.numberStepSize || 1;
921
+
922
+ // Load dynamic options based on loadOptionsMethod
923
+ useEffect(() => {
924
+ const loadDynamicOptions = async () => {
925
+ if (!selectedNode || !isINodeProperties(parameter) || !parameter.typeOptions?.loadOptionsMethod) return;
926
+
927
+ const dependsOn = parameter.typeOptions.loadOptionsDependsOn || [];
928
+ const allParamsResolved = { ...nodeParameters, ...allParameters };
929
+
930
+ // Check if all dependencies are satisfied
931
+ const hasAllDependencies = dependsOn.every((dep: string) => allParamsResolved[dep]);
932
+ if (dependsOn.length > 0 && !hasAllDependencies) return;
933
+
934
+ try {
935
+ // Get the node definition to access methods
936
+ const nodeType = selectedNode.data?.nodeType || selectedNode.type;
937
+ const nodeDef = nodeType ? nodeDefinitions[nodeType] : null;
938
+
939
+ if (nodeDef?.methods?.loadOptions?.[parameter.typeOptions.loadOptionsMethod]) {
940
+ const loadMethod = nodeDef.methods.loadOptions[parameter.typeOptions.loadOptionsMethod];
941
+
942
+ // Create context for the load method
943
+ const context = {
944
+ getCurrentNodeParameter: (paramName: string) => allParamsResolved[paramName]
945
+ };
946
+
947
+ // Call the load method with context
948
+ const options = await loadMethod.call(context);
949
+ setDynamicOptions(options);
950
+
951
+ // Also update the DynamicParameterService for consistency
952
+ DynamicParameterService.updateParameterOptions(selectedNode.id, parameter.name, options);
953
+
954
+ // Auto-select first option if current value is empty and options are available
955
+ if (options.length > 0 && (!currentValue || currentValue === '')) {
956
+ console.log(`[ParameterRenderer] Auto-selecting first option for ${parameter.name}:`, options[0].value);
957
+ onChange(options[0].value);
958
+ }
959
+ }
960
+ } catch (error) {
961
+ console.error('Error loading dynamic options:', error);
962
+ }
963
+ };
964
+
965
+ loadDynamicOptions();
966
+ }, [selectedNode?.id, isINodeProperties(parameter) && parameter.typeOptions?.loadOptionsMethod, nodeParameters, allParameters, parameter.name]);
967
+
968
+ // Load default parameters for Android service nodes when service_id or action changes
969
+ useEffect(() => {
970
+ const loadDefaultParameters = async () => {
971
+ if (!selectedNode || parameter.name !== 'parameters') return;
972
+
973
+ const nodeType = selectedNode.data?.nodeType || selectedNode.type;
974
+ if (!ANDROID_SERVICE_NODE_TYPES.includes(nodeType)) return;
975
+
976
+ // Merge database params with current form params (current takes precedence)
977
+ const allParamsResolved = { ...nodeParameters, ...allParameters };
978
+ const serviceId = allParamsResolved.service_id;
979
+ const action = allParamsResolved.action;
980
+
981
+ if (!serviceId || !action) {
982
+ console.log('[AndroidService] Skipping - missing serviceId or action:', { serviceId, action });
983
+ return;
984
+ }
985
+
986
+ try {
987
+ console.log('[AndroidService] Fetching default parameters for:', { serviceId, action });
988
+ const response = await fetch(`${API_CONFIG.PYTHON_BASE_URL}/api/android/services/${serviceId}/actions/${action}/parameters`, {
989
+ credentials: 'include'
990
+ });
991
+ const data = await response.json();
992
+ console.log('[AndroidService] Default parameters response:', data);
993
+
994
+ if (data.success && data.default_parameters) {
995
+ // Always update with new defaults when service/action changes
996
+ console.log('[AndroidService] Setting parameters to:', data.default_parameters);
997
+ onChange(data.default_parameters);
998
+ }
999
+ } catch (error) {
1000
+ console.error('[AndroidService] Error loading default parameters:', error);
1001
+ }
1002
+ };
1003
+
1004
+ loadDefaultParameters();
1005
+ }, [
1006
+ selectedNode?.id,
1007
+ parameter.name,
1008
+ allParameters?.service_id,
1009
+ allParameters?.action,
1010
+ nodeParameters?.service_id,
1011
+ nodeParameters?.action
1012
+ ]);
1013
+
1014
+ // Subscribe to dynamic parameter updates
1015
+ useEffect(() => {
1016
+ if (!selectedNode) return;
1017
+
1018
+
1019
+ const unsubscribe = DynamicParameterService.subscribe((nodeId, parameterName, options) => {
1020
+
1021
+ if (nodeId === selectedNode.id && parameterName === parameter.name) {
1022
+ setDynamicOptions(options);
1023
+ }
1024
+ });
1025
+
1026
+ // Check for existing dynamic options
1027
+ const existingOptions = DynamicParameterService.getParameterOptions(selectedNode.id, parameter.name);
1028
+
1029
+ if (existingOptions) {
1030
+ setDynamicOptions(existingOptions);
1031
+ }
1032
+
1033
+ return unsubscribe;
1034
+ }, [selectedNode?.id, parameter.name]);
1035
+
1036
+ // Handle API key validation success
1037
+ const handleApiKeyValidationSuccess = (models: string[]) => {
1038
+
1039
+ if (!selectedNode) {
1040
+ console.warn('ParameterRenderer: No selected node for dynamic options update');
1041
+ return;
1042
+ }
1043
+
1044
+ // Always update the 'model' parameter with dynamic options when API key validation succeeds
1045
+ // This callback can be triggered from any parameter (usually the apiKey parameter)
1046
+ const modelOptions = DynamicParameterService.createModelOptions(models);
1047
+ DynamicParameterService.updateParameterOptions(selectedNode.id, 'model', modelOptions);
1048
+
1049
+ // If this callback is triggered from the model parameter itself and it's empty, auto-select first model
1050
+ if (parameter.name === 'model' && !currentValue && models.length > 0) {
1051
+ onChange(models[0]);
1052
+ }
1053
+ };
1054
+
1055
+ const handleDragOver = (e: React.DragEvent) => {
1056
+ e.preventDefault();
1057
+ e.dataTransfer.dropEffect = 'copy';
1058
+ setIsDragOver(true);
1059
+ };
1060
+
1061
+ const handleDragLeave = (e: React.DragEvent) => {
1062
+ e.preventDefault();
1063
+ setIsDragOver(false);
1064
+ };
1065
+
1066
+ const handleDrop = (e: React.DragEvent) => {
1067
+ e.preventDefault();
1068
+ setIsDragOver(false);
1069
+
1070
+ // Check if this is a coordinate parameter (lat/lng) for special handling
1071
+ const paramName = parameter.name.toLowerCase();
1072
+ const isCoordinate = paramName.includes('lat') || paramName.includes('lng') ||
1073
+ paramName.includes('lon') || paramName === 'latitude' ||
1074
+ paramName === 'longitude';
1075
+
1076
+ // Try to get JSON data first (from connected node outputs)
1077
+ const jsonData = e.dataTransfer.getData('application/json');
1078
+ if (jsonData) {
1079
+ try {
1080
+ const parsedData = JSON.parse(jsonData);
1081
+ if (parsedData.type === 'nodeVariable') {
1082
+ // For coordinate parameters, try to extract actual numeric value from connected node data
1083
+ if (isCoordinate && typeof parsedData.dataType === 'string' && parsedData.dataType === 'number') {
1084
+ // Look for actual coordinate values in global execution data
1085
+ // This is a simplified approach - in production you'd want more robust data access
1086
+
1087
+ // For now, use template string but mark it for coordinate processing
1088
+ const existingValue = currentValue || '';
1089
+ const needsSpace = existingValue && !existingValue.endsWith(' ') && existingValue.length > 0;
1090
+ const newValue = existingValue + (needsSpace ? ' ' : '') + parsedData.variableTemplate;
1091
+ onChange(newValue);
1092
+ return;
1093
+ }
1094
+
1095
+ // Handle variable template data - use the template string
1096
+ const existingValue = currentValue || '';
1097
+ // Add smart spacing - add space if existing content doesn't end with space
1098
+ const needsSpace = existingValue && !existingValue.endsWith(' ') && existingValue.length > 0;
1099
+ const newValue = existingValue + (needsSpace ? ' ' : '') + parsedData.variableTemplate;
1100
+ onChange(newValue);
1101
+ return;
1102
+ }
1103
+ if (parsedData.type === 'nodeOutput') {
1104
+ // Handle node output data - use the actual value for direct mapping
1105
+ onChange(parsedData.value);
1106
+ return;
1107
+ }
1108
+ } catch (err) {
1109
+ console.warn('Failed to parse JSON drag data:', err);
1110
+ }
1111
+ }
1112
+
1113
+ // Fallback to existing text/plain format (OutputPanel drag-drop)
1114
+ const data = e.dataTransfer.getData('text/plain');
1115
+ if (data && data.startsWith('{{') && data.endsWith('}}')) {
1116
+ // For coordinate parameters, allow template strings but process them appropriately
1117
+ if (isCoordinate) {
1118
+ onChange(data); // Set the template directly for coordinate resolution
1119
+ return;
1120
+ }
1121
+
1122
+ // Append to existing content instead of replacing
1123
+ const existingValue = currentValue || '';
1124
+ // Add smart spacing - add space if existing content doesn't end with space
1125
+ const needsSpace = existingValue && !existingValue.endsWith(' ') && existingValue.length > 0;
1126
+ const newValue = existingValue + (needsSpace ? ' ' : '') + data;
1127
+ onChange(newValue);
1128
+ }
1129
+ };
1130
+
1131
+ const renderInput = () => {
1132
+ switch (parameter.type) {
1133
+ case 'string':
1134
+ // Check if this should be a textarea based on typeOptions.rows
1135
+ const shouldUseTextarea = (parameter as any).typeOptions?.rows > 1;
1136
+ // Check if this should be a password field
1137
+ const isPassword = (parameter as any).typeOptions?.password;
1138
+ // Check if this is a code editor
1139
+ const isCodeEditor = (parameter as any).typeOptions?.editor === 'code';
1140
+
1141
+ if (shouldUseTextarea) {
1142
+ // Use CodeEditor for code editing
1143
+ if (isCodeEditor) {
1144
+ // Show loading state while parameters are being fetched
1145
+ if (isLoadingParameters) {
1146
+ return (
1147
+ <div style={{
1148
+ height: '100%',
1149
+ minHeight: '200px',
1150
+ display: 'flex',
1151
+ alignItems: 'center',
1152
+ justifyContent: 'center',
1153
+ backgroundColor: theme.colors.backgroundAlt,
1154
+ border: `1px solid ${theme.colors.border}`,
1155
+ borderRadius: theme.borderRadius.md,
1156
+ color: theme.colors.textMuted,
1157
+ fontSize: '14px'
1158
+ }}>
1159
+ Loading code...
1160
+ </div>
1161
+ );
1162
+ }
1163
+ // Get language from typeOptions or default to python
1164
+ const codeLanguage = (parameter as any).typeOptions?.editorLanguage || 'python';
1165
+ return (
1166
+ <CodeEditor
1167
+ value={currentValue || ''}
1168
+ onChange={onChange}
1169
+ language={codeLanguage}
1170
+ placeholder={parameter.placeholder}
1171
+ />
1172
+ );
1173
+ }
1174
+
1175
+ // Regular textarea for non-code
1176
+ return (
1177
+ <textarea
1178
+ value={currentValue || ''}
1179
+ onChange={(e) => onChange(e.target.value)}
1180
+ placeholder={parameter.placeholder}
1181
+ rows={(parameter as any).typeOptions?.rows || 3}
1182
+ spellCheck={true}
1183
+ onDragOver={handleDragOver}
1184
+ onDragLeave={handleDragLeave}
1185
+ onDrop={handleDrop}
1186
+ style={{
1187
+ width: '100%',
1188
+ padding: '10px 12px',
1189
+ border: isDragOver ? `2px solid ${theme.accent.cyan}` : `1px solid ${theme.colors.border}`,
1190
+ borderRadius: '6px',
1191
+ fontSize: '14px',
1192
+ backgroundColor: isDragOver ? `${theme.accent.cyan}10` : theme.colors.backgroundAlt,
1193
+ color: currentValue && currentValue.includes('{{') ? theme.accent.yellow : theme.colors.text,
1194
+ outline: 'none',
1195
+ transition: 'all 0.2s ease',
1196
+ fontFamily: currentValue && currentValue.includes('{{') ? 'monospace' : 'system-ui, sans-serif',
1197
+ resize: 'vertical',
1198
+ minHeight: '80px',
1199
+ boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.05)',
1200
+ lineHeight: '1.5'
1201
+ }}
1202
+ onFocus={(e) => {
1203
+ e.target.style.borderColor = theme.accent.cyan;
1204
+ e.target.style.boxShadow = `0 0 0 3px ${theme.accent.cyan}20, inset 0 1px 2px rgba(0,0,0,0.05)`;
1205
+ }}
1206
+ onBlur={(e) => {
1207
+ e.target.style.borderColor = theme.colors.border;
1208
+ e.target.style.boxShadow = 'inset 0 1px 2px rgba(0,0,0,0.05)';
1209
+ }}
1210
+ />
1211
+ );
1212
+ }
1213
+
1214
+ // Check if this parameter has API key validation
1215
+ const validationArray = (parameter as any).validation;
1216
+ const apiKeyValidation = validationArray?.find((v: any) => v.type === 'apiKey' && v.showValidateButton);
1217
+
1218
+ if (apiKeyValidation) {
1219
+ // Resolve provider expression if it's a template like {{ $parameter["provider"] }}
1220
+ let resolvedProvider = apiKeyValidation.provider;
1221
+ if (typeof resolvedProvider === 'string' && resolvedProvider.includes('$parameter[')) {
1222
+ // Extract parameter name from expression like {{ $parameter["provider"] }}
1223
+ const match = resolvedProvider.match(/\$parameter\["([^"]+)"\]|\$parameter\['([^']+)'\]/);
1224
+ if (match) {
1225
+ const paramName = match[1] || match[2];
1226
+ resolvedProvider = resolvedParameters[paramName] || resolvedProvider;
1227
+ }
1228
+ }
1229
+
1230
+ const resolvedValidationConfig = {
1231
+ ...apiKeyValidation,
1232
+ provider: resolvedProvider
1233
+ };
1234
+
1235
+ return (
1236
+ <APIKeyValidator
1237
+ value={currentValue || ''}
1238
+ onChange={onChange}
1239
+ placeholder={parameter.placeholder}
1240
+ validationConfig={resolvedValidationConfig}
1241
+ onValidationSuccess={handleApiKeyValidationSuccess}
1242
+ isDragOver={isDragOver}
1243
+ onDragOver={handleDragOver}
1244
+ onDragLeave={handleDragLeave}
1245
+ onDrop={handleDrop}
1246
+ />
1247
+ );
1248
+ }
1249
+
1250
+ // Check if this parameter has dynamic options (like models after API key validation)
1251
+
1252
+ if (dynamicOptions.length > 0 && parameter.type === 'string') {
1253
+ // Check if OpenRouter (has [FREE] tagged models)
1254
+ const hasFreeModels = dynamicOptions.some(opt => String(opt.label || opt.value).includes('[FREE]'));
1255
+
1256
+ if (hasFreeModels) {
1257
+ // Group into Free and Paid for OpenRouter using native select with optgroup
1258
+ const freeModels = dynamicOptions.filter(opt => String(opt.label || opt.value).includes('[FREE]'));
1259
+ const paidModels = dynamicOptions.filter(opt => !String(opt.label || opt.value).includes('[FREE]'));
1260
+
1261
+ return (
1262
+ <select
1263
+ value={currentValue || ''}
1264
+ onChange={(e) => onChange(e.target.value)}
1265
+ style={{
1266
+ width: '100%',
1267
+ padding: theme.spacing.sm,
1268
+ border: `1px solid ${theme.colors.border}`,
1269
+ borderRadius: theme.borderRadius.md,
1270
+ fontSize: theme.fontSize.sm,
1271
+ outline: 'none',
1272
+ transition: 'border-color 0.2s ease',
1273
+ cursor: 'pointer',
1274
+ backgroundColor: theme.colors.background,
1275
+ color: theme.colors.text,
1276
+ fontFamily: 'system-ui, sans-serif'
1277
+ }}
1278
+ onFocus={(e) => e.target.style.borderColor = theme.colors.focus}
1279
+ onBlur={(e) => e.target.style.borderColor = theme.colors.border}
1280
+ >
1281
+ {!currentValue && (
1282
+ <option value="" disabled>
1283
+ Select a model...
1284
+ </option>
1285
+ )}
1286
+ <optgroup label={`Free Models (${freeModels.length})`}>
1287
+ {freeModels.map((option) => (
1288
+ <option key={String(option.value)} value={String(option.value)}>
1289
+ {option.label || option.name || String(option.value)}
1290
+ </option>
1291
+ ))}
1292
+ </optgroup>
1293
+ <optgroup label={`Paid Models (${paidModels.length})`}>
1294
+ {paidModels.map((option) => (
1295
+ <option key={String(option.value)} value={String(option.value)}>
1296
+ {option.label || option.name || String(option.value)}
1297
+ </option>
1298
+ ))}
1299
+ </optgroup>
1300
+ </select>
1301
+ );
1302
+ }
1303
+
1304
+ // Use native select for non-OpenRouter (original working code)
1305
+ return (
1306
+ <select
1307
+ value={currentValue || ''}
1308
+ onChange={(e) => onChange(e.target.value)}
1309
+ style={{
1310
+ width: '100%',
1311
+ padding: '8px 12px',
1312
+ border: `1px solid ${theme.colors.border}`,
1313
+ borderRadius: '6px',
1314
+ fontSize: '14px',
1315
+ outline: 'none',
1316
+ transition: 'border-color 0.2s ease',
1317
+ cursor: 'pointer',
1318
+ backgroundColor: theme.colors.background,
1319
+ color: theme.colors.text,
1320
+ fontFamily: 'system-ui, sans-serif'
1321
+ }}
1322
+ onFocus={(e) => e.target.style.borderColor = theme.colors.focus}
1323
+ onBlur={(e) => e.target.style.borderColor = theme.colors.border}
1324
+ >
1325
+ {!currentValue && (
1326
+ <option value="" disabled>
1327
+ Select a model...
1328
+ </option>
1329
+ )}
1330
+ {dynamicOptions.map((option) => (
1331
+ <option key={String(option.value)} value={String(option.value)}>
1332
+ {option.label || option.name || String(option.value)}
1333
+ </option>
1334
+ ))}
1335
+ </select>
1336
+ );
1337
+ }
1338
+
1339
+ // Log why we're not using dynamic options for this parameter
1340
+ if (parameter.type === 'string' && parameter.name === 'model') {
1341
+ }
1342
+
1343
+ // Special case for group_id parameter - add Load Groups button
1344
+ if (parameter.name === 'group_id') {
1345
+ const storedGroupName = allParameters?.group_name || '';
1346
+ return (
1347
+ <GroupIdSelector
1348
+ value={currentValue || ''}
1349
+ onChange={onChange}
1350
+ onNameChange={(name) => onParameterChange?.('group_name', name)}
1351
+ storedName={storedGroupName}
1352
+ placeholder={parameter.placeholder}
1353
+ theme={theme}
1354
+ isDragOver={isDragOver}
1355
+ onDragOver={handleDragOver}
1356
+ onDragLeave={handleDragLeave}
1357
+ onDrop={handleDrop}
1358
+ />
1359
+ );
1360
+ }
1361
+
1362
+ // Special case for senderNumber parameter - add Load Members button (uses group_id)
1363
+ if (parameter.name === 'senderNumber') {
1364
+ const groupId = resolvedParameters?.group_id || allParameters?.group_id || '';
1365
+ const storedSenderName = allParameters?.sender_name || '';
1366
+ return (
1367
+ <SenderNumberSelector
1368
+ value={currentValue || ''}
1369
+ onChange={onChange}
1370
+ onNameChange={(name) => onParameterChange?.('sender_name', name)}
1371
+ storedName={storedSenderName}
1372
+ placeholder={parameter.placeholder}
1373
+ theme={theme}
1374
+ isDragOver={isDragOver}
1375
+ onDragOver={handleDragOver}
1376
+ onDragLeave={handleDragLeave}
1377
+ onDrop={handleDrop}
1378
+ groupId={groupId}
1379
+ />
1380
+ );
1381
+ }
1382
+
1383
+ return (
1384
+ <input
1385
+ type={isPassword ? "password" : "text"}
1386
+ value={currentValue || ''}
1387
+ onChange={(e) => onChange(e.target.value)}
1388
+ placeholder={parameter.placeholder}
1389
+ onDragOver={handleDragOver}
1390
+ onDragLeave={handleDragLeave}
1391
+ onDrop={handleDrop}
1392
+ style={{
1393
+ width: '100%',
1394
+ padding: '10px 12px',
1395
+ border: isDragOver ? `2px solid ${theme.accent.cyan}` : `1px solid ${theme.colors.border}`,
1396
+ borderRadius: '6px',
1397
+ fontSize: '14px',
1398
+ backgroundColor: isDragOver ? `${theme.accent.cyan}10` : theme.colors.backgroundAlt,
1399
+ color: currentValue && currentValue.includes('{{') ? theme.accent.yellow : theme.colors.text,
1400
+ outline: 'none',
1401
+ transition: 'all 0.2s ease',
1402
+ fontFamily: currentValue && currentValue.includes('{{') ? 'monospace' : 'system-ui, sans-serif',
1403
+ boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.05)'
1404
+ }}
1405
+ onFocus={(e) => {
1406
+ e.target.style.borderColor = theme.accent.cyan;
1407
+ e.target.style.boxShadow = `0 0 0 3px ${theme.accent.cyan}20, inset 0 1px 2px rgba(0,0,0,0.05)`;
1408
+ }}
1409
+ onBlur={(e) => {
1410
+ e.target.style.borderColor = theme.colors.border;
1411
+ e.target.style.boxShadow = 'inset 0 1px 2px rgba(0,0,0,0.05)';
1412
+ }}
1413
+ />
1414
+ );
1415
+
1416
+ case 'number':
1417
+
1418
+ return (
1419
+ <input
1420
+ type="number"
1421
+ value={currentValue !== undefined ? currentValue : (parameter.default || 0)}
1422
+ onChange={(e) => onChange(Number(e.target.value))}
1423
+ min={getMin()}
1424
+ max={getMax()}
1425
+ step={getStep()}
1426
+ onDragOver={handleDragOver}
1427
+ onDragLeave={handleDragLeave}
1428
+ onDrop={handleDrop}
1429
+ style={{
1430
+ width: '100%',
1431
+ padding: '8px 12px',
1432
+ border: isDragOver ? `2px solid ${theme.colors.focus}` : `1px solid ${theme.colors.border}`,
1433
+ borderRadius: '6px',
1434
+ fontSize: '14px',
1435
+ backgroundColor: isDragOver ? theme.colors.focusRing : theme.colors.background,
1436
+ color: theme.colors.text,
1437
+ outline: 'none',
1438
+ transition: 'all 0.2s ease',
1439
+ fontFamily: 'system-ui, sans-serif'
1440
+ }}
1441
+ onFocus={(e) => e.target.style.borderColor = theme.colors.focus}
1442
+ onBlur={(e) => e.target.style.borderColor = theme.colors.border}
1443
+ />
1444
+ );
1445
+
1446
+ case 'boolean':
1447
+ return (
1448
+ <label style={{
1449
+ display: 'flex',
1450
+ alignItems: 'center',
1451
+ gap: '8px',
1452
+ cursor: 'pointer',
1453
+ fontSize: '14px',
1454
+ fontFamily: 'system-ui, sans-serif',
1455
+ color: theme.colors.text
1456
+ }}>
1457
+ <input
1458
+ type="checkbox"
1459
+ checked={currentValue || false}
1460
+ onChange={(e) => onChange(e.target.checked)}
1461
+ style={{ width: '16px', height: '16px', accentColor: theme.colors.focus }}
1462
+ />
1463
+ {parameter.displayName}
1464
+ </label>
1465
+ );
1466
+
1467
+ case 'select':
1468
+ case 'options':
1469
+ // Use dynamic options if available, otherwise use static options
1470
+ const optionsToRender = dynamicOptions.length > 0 ? dynamicOptions : (parameter.options || []);
1471
+ const selectOptions = optionsToRender.filter((option): option is import('../types/INodeProperties').INodePropertyOption =>
1472
+ 'value' in option
1473
+ );
1474
+
1475
+ return (
1476
+ <select
1477
+ value={currentValue || parameter.default}
1478
+ onChange={(e) => onChange(e.target.value)}
1479
+ style={{
1480
+ width: '100%',
1481
+ padding: '10px 12px',
1482
+ border: `1px solid ${theme.colors.border}`,
1483
+ borderRadius: '6px',
1484
+ fontSize: '14px',
1485
+ outline: 'none',
1486
+ transition: 'all 0.2s ease',
1487
+ cursor: 'pointer',
1488
+ backgroundColor: theme.colors.backgroundAlt,
1489
+ color: theme.colors.text,
1490
+ fontFamily: 'system-ui, sans-serif',
1491
+ boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.05)',
1492
+ appearance: 'none',
1493
+ backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23657b83' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E")`,
1494
+ backgroundRepeat: 'no-repeat',
1495
+ backgroundPosition: 'right 12px center',
1496
+ paddingRight: '36px'
1497
+ }}
1498
+ onFocus={(e) => {
1499
+ e.target.style.borderColor = theme.accent.cyan;
1500
+ e.target.style.boxShadow = `0 0 0 3px ${theme.accent.cyan}20, inset 0 1px 2px rgba(0,0,0,0.05)`;
1501
+ }}
1502
+ onBlur={(e) => {
1503
+ e.target.style.borderColor = theme.colors.border;
1504
+ e.target.style.boxShadow = 'inset 0 1px 2px rgba(0,0,0,0.05)';
1505
+ }}
1506
+ >
1507
+ {selectOptions.map((option) => (
1508
+ <option key={String(option.value)} value={String(option.value)}>
1509
+ {option.label || option.name || String(option.value)}
1510
+ </option>
1511
+ ))}
1512
+ </select>
1513
+ );
1514
+
1515
+ case 'slider':
1516
+ return (
1517
+ <div>
1518
+ <input
1519
+ type="range"
1520
+ min={getMin()}
1521
+ max={getMax()}
1522
+ step={getStep()}
1523
+ value={currentValue !== undefined ? currentValue : (parameter.default || 0)}
1524
+ onChange={(e) => onChange(Number(e.target.value))}
1525
+ style={{
1526
+ width: '100%',
1527
+ height: '8px',
1528
+ backgroundColor: theme.colors.backgroundAlt,
1529
+ borderRadius: '8px',
1530
+ outline: 'none',
1531
+ accentColor: theme.colors.focus
1532
+ }}
1533
+ />
1534
+ <div style={{
1535
+ textAlign: 'center',
1536
+ fontSize: '12px',
1537
+ color: theme.colors.textSecondary,
1538
+ marginTop: '4px',
1539
+ fontFamily: 'system-ui, sans-serif'
1540
+ }}>
1541
+ {currentValue !== undefined ? currentValue : (parameter.default || 0)}
1542
+ {parameter.type === 'slider' ? '%' : ''}
1543
+ </div>
1544
+ </div>
1545
+ );
1546
+
1547
+ case 'percentage':
1548
+ return (
1549
+ <div>
1550
+ <input
1551
+ type="range"
1552
+ min={getMin()}
1553
+ max={getMax()}
1554
+ step={getStep()}
1555
+ value={currentValue !== undefined ? currentValue : (parameter.default || 0)}
1556
+ onChange={(e) => onChange(Number(e.target.value))}
1557
+ style={{
1558
+ width: '100%',
1559
+ height: '8px',
1560
+ backgroundColor: theme.colors.backgroundAlt,
1561
+ borderRadius: '8px',
1562
+ outline: 'none',
1563
+ accentColor: theme.colors.success
1564
+ }}
1565
+ />
1566
+ <div style={{
1567
+ textAlign: 'center',
1568
+ fontSize: '12px',
1569
+ color: theme.colors.textSecondary,
1570
+ marginTop: '4px',
1571
+ fontFamily: 'system-ui, sans-serif'
1572
+ }}>
1573
+ {currentValue !== undefined ? currentValue : (parameter.default || 0)}%
1574
+ </div>
1575
+ </div>
1576
+ );
1577
+
1578
+ case 'text':
1579
+ return (
1580
+ <input
1581
+ type="text"
1582
+ value={currentValue || ''}
1583
+ onChange={(e) => onChange(e.target.value)}
1584
+ placeholder={parameter.placeholder}
1585
+ onDragOver={handleDragOver}
1586
+ onDragLeave={handleDragLeave}
1587
+ onDrop={handleDrop}
1588
+ style={{
1589
+ width: '100%',
1590
+ padding: '8px 12px',
1591
+ border: isDragOver ? `2px solid ${theme.colors.focus}` : `1px solid ${theme.colors.border}`,
1592
+ borderRadius: '6px',
1593
+ fontSize: '14px',
1594
+ backgroundColor: isDragOver ? theme.colors.focusRing : theme.colors.background,
1595
+ color: currentValue && currentValue.includes('{{') ? theme.colors.templateVariable : theme.colors.text,
1596
+ outline: 'none',
1597
+ transition: 'all 0.2s ease',
1598
+ fontFamily: currentValue && currentValue.includes('{{') ? 'monospace' : 'system-ui, sans-serif'
1599
+ }}
1600
+ onFocus={(e) => e.target.style.borderColor = theme.colors.focus}
1601
+ onBlur={(e) => e.target.style.borderColor = theme.colors.border}
1602
+ />
1603
+ );
1604
+
1605
+ case 'file':
1606
+ const fileInputRef = React.useRef<HTMLInputElement>(null);
1607
+
1608
+ const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
1609
+ const file = e.target.files?.[0];
1610
+ if (!file) return;
1611
+
1612
+ const reader = new FileReader();
1613
+ reader.onload = () => {
1614
+ const base64 = (reader.result as string).split(',')[1]; // Remove data:mime;base64, prefix
1615
+ // Store as object with base64 data, filename, and mime type
1616
+ onChange({
1617
+ type: 'upload',
1618
+ data: base64,
1619
+ filename: file.name,
1620
+ mimeType: file.type || 'application/octet-stream'
1621
+ });
1622
+ };
1623
+ reader.readAsDataURL(file);
1624
+ };
1625
+
1626
+ const isUploadedFile = currentValue && typeof currentValue === 'object' && currentValue.type === 'upload';
1627
+
1628
+ // Determine file accept type based on context (e.g., messageType for WhatsApp)
1629
+ const getFileAcceptType = () => {
1630
+ const messageType = allParameters?.messageType;
1631
+ if (messageType) {
1632
+ switch (messageType) {
1633
+ case 'image':
1634
+ return 'image/*';
1635
+ case 'video':
1636
+ return 'video/*';
1637
+ case 'audio':
1638
+ return 'audio/*,.ogg,.opus,.mp3,.wav,.m4a';
1639
+ case 'document':
1640
+ return '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.csv,.zip,.rar';
1641
+ case 'sticker':
1642
+ return 'image/webp,.webp';
1643
+ default:
1644
+ return (parameter as any).typeOptions?.accept || '*/*';
1645
+ }
1646
+ }
1647
+ return (parameter as any).typeOptions?.accept || '*/*';
1648
+ };
1649
+
1650
+ return (
1651
+ <div>
1652
+ <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
1653
+ <input
1654
+ type="text"
1655
+ value={isUploadedFile ? `[Uploaded] ${currentValue.filename}` : (currentValue || '')}
1656
+ onChange={(e) => onChange(e.target.value)}
1657
+ placeholder={parameter.placeholder || 'Enter file path or upload'}
1658
+ onDragOver={handleDragOver}
1659
+ onDragLeave={handleDragLeave}
1660
+ onDrop={handleDrop}
1661
+ readOnly={isUploadedFile}
1662
+ style={{
1663
+ flex: 1,
1664
+ padding: '8px 12px',
1665
+ border: isDragOver ? `2px solid ${theme.colors.focus}` : `1px solid ${theme.colors.border}`,
1666
+ borderRadius: '6px',
1667
+ fontSize: '13px',
1668
+ backgroundColor: isUploadedFile ? theme.colors.backgroundAlt : (isDragOver ? theme.colors.focusRing : theme.colors.background),
1669
+ color: isUploadedFile ? theme.colors.success : (currentValue && currentValue.includes?.('{{') ? theme.colors.templateVariable : theme.colors.text),
1670
+ outline: 'none',
1671
+ transition: 'all 0.2s ease',
1672
+ fontFamily: 'monospace'
1673
+ }}
1674
+ onFocus={(e) => e.target.style.borderColor = theme.colors.focus}
1675
+ onBlur={(e) => e.target.style.borderColor = theme.colors.border}
1676
+ />
1677
+ <input
1678
+ key={`file-input-${allParameters?.messageType || 'default'}`}
1679
+ ref={fileInputRef}
1680
+ type="file"
1681
+ onChange={handleFileUpload}
1682
+ style={{ display: 'none' }}
1683
+ accept={getFileAcceptType()}
1684
+ />
1685
+ <button
1686
+ onClick={() => fileInputRef.current?.click()}
1687
+ style={{
1688
+ padding: '8px 12px',
1689
+ border: `1px solid ${theme.colors.focus}40`,
1690
+ borderRadius: '6px',
1691
+ backgroundColor: `${theme.colors.focus}18`,
1692
+ color: theme.colors.focus,
1693
+ cursor: 'pointer',
1694
+ fontSize: '13px',
1695
+ fontWeight: 600,
1696
+ transition: 'all 0.2s ease',
1697
+ whiteSpace: 'nowrap'
1698
+ }}
1699
+ onMouseEnter={(e) => {
1700
+ e.currentTarget.style.backgroundColor = `${theme.colors.focus}30`;
1701
+ }}
1702
+ onMouseLeave={(e) => {
1703
+ e.currentTarget.style.backgroundColor = `${theme.colors.focus}18`;
1704
+ }}
1705
+ title="Upload file"
1706
+ >
1707
+ Upload
1708
+ </button>
1709
+ {isUploadedFile && (
1710
+ <button
1711
+ onClick={() => {
1712
+ onChange('');
1713
+ if (fileInputRef.current) fileInputRef.current.value = '';
1714
+ }}
1715
+ style={{
1716
+ padding: '8px 10px',
1717
+ border: `1px solid ${theme.colors.error}40`,
1718
+ borderRadius: '6px',
1719
+ backgroundColor: `${theme.colors.error}18`,
1720
+ color: theme.colors.error,
1721
+ cursor: 'pointer',
1722
+ fontSize: '13px',
1723
+ fontWeight: 600,
1724
+ transition: 'all 0.2s ease'
1725
+ }}
1726
+ onMouseEnter={(e) => {
1727
+ e.currentTarget.style.backgroundColor = `${theme.colors.error}30`;
1728
+ }}
1729
+ onMouseLeave={(e) => {
1730
+ e.currentTarget.style.backgroundColor = `${theme.colors.error}18`;
1731
+ }}
1732
+ title="Clear uploaded file"
1733
+ >
1734
+ X
1735
+ </button>
1736
+ )}
1737
+ </div>
1738
+ <div style={{
1739
+ fontSize: '11px',
1740
+ color: theme.colors.textSecondary,
1741
+ marginTop: '4px',
1742
+ fontStyle: 'italic'
1743
+ }}>
1744
+ {isUploadedFile
1745
+ ? `Size: ${(currentValue.data.length * 0.75 / 1024).toFixed(1)} KB | Type: ${currentValue.mimeType}`
1746
+ : 'Enter server path or click Upload to select a file'}
1747
+ </div>
1748
+ </div>
1749
+ );
1750
+
1751
+ case 'array':
1752
+ const arrayValue = Array.isArray(currentValue) ? currentValue : [];
1753
+ return (
1754
+ <div>
1755
+ <div style={{
1756
+ border: `1px solid ${theme.colors.border}`,
1757
+ borderRadius: '6px',
1758
+ backgroundColor: theme.colors.background,
1759
+ maxHeight: '120px',
1760
+ overflowY: 'auto'
1761
+ }}>
1762
+ {parameter.options?.map((option) => (
1763
+ <label key={option.value} style={{
1764
+ display: 'flex',
1765
+ alignItems: 'center',
1766
+ gap: '8px',
1767
+ padding: '8px 12px',
1768
+ cursor: 'pointer',
1769
+ fontSize: '14px',
1770
+ fontFamily: 'system-ui, sans-serif',
1771
+ color: theme.colors.text,
1772
+ borderBottom: `1px solid ${theme.colors.border}`
1773
+ }}>
1774
+ <input
1775
+ type="checkbox"
1776
+ checked={arrayValue.includes(option.value)}
1777
+ onChange={(e) => {
1778
+ if (e.target.checked) {
1779
+ onChange([...arrayValue, option.value]);
1780
+ } else {
1781
+ onChange(arrayValue.filter((v: any) => v !== option.value));
1782
+ }
1783
+ }}
1784
+ style={{ width: '16px', height: '16px', accentColor: theme.colors.focus }}
1785
+ />
1786
+ {option.label}
1787
+ </label>
1788
+ ))}
1789
+ </div>
1790
+ <div style={{
1791
+ fontSize: '11px',
1792
+ color: theme.colors.textSecondary,
1793
+ marginTop: '4px'
1794
+ }}>
1795
+ Selected: {arrayValue.length} item{arrayValue.length !== 1 ? 's' : ''}
1796
+ </div>
1797
+ </div>
1798
+ );
1799
+
1800
+ case 'collection':
1801
+ return <CollectionRenderer parameter={parameter} value={currentValue} onChange={onChange} allParameters={allParameters} theme={theme} />;
1802
+
1803
+ case 'fixedCollection':
1804
+ return <FixedCollectionRenderer parameter={parameter} value={currentValue} onChange={onChange} allParameters={allParameters} theme={theme} />;
1805
+
1806
+ case 'notice':
1807
+ // Info/notice display - shows informational text without input
1808
+ return (
1809
+ <div style={{
1810
+ padding: '10px 12px',
1811
+ backgroundColor: theme.colors.backgroundAlt,
1812
+ border: `1px solid ${theme.colors.border}`,
1813
+ borderRadius: '6px',
1814
+ fontSize: '13px',
1815
+ color: theme.colors.textSecondary,
1816
+ lineHeight: '1.5'
1817
+ }}>
1818
+ {parameter.default || parameter.description || ''}
1819
+ </div>
1820
+ );
1821
+
1822
+ default:
1823
+ return <div style={{ color: theme.colors.error, fontSize: '14px', padding: '8px 12px', backgroundColor: `${theme.colors.error}15`, border: `1px solid ${theme.colors.error}30`, borderRadius: '6px' }}>Unsupported parameter type: {parameter.type}</div>;
1824
+ }
1825
+ };
1826
+
1827
+ return (
1828
+ <div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
1829
+ {parameter.type !== 'boolean' && (
1830
+ <label style={{
1831
+ display: 'flex',
1832
+ alignItems: 'center',
1833
+ gap: '6px',
1834
+ marginBottom: '8px',
1835
+ fontSize: '13px',
1836
+ fontWeight: 600,
1837
+ color: theme.colors.text,
1838
+ fontFamily: 'system-ui, sans-serif',
1839
+ flexShrink: 0
1840
+ }}>
1841
+ <span>{parameter.displayName}</span>
1842
+ {parameter.required && (
1843
+ <span style={{
1844
+ color: theme.accent.red,
1845
+ fontSize: '14px',
1846
+ fontWeight: 700
1847
+ }}>*</span>
1848
+ )}
1849
+ </label>
1850
+ )}
1851
+
1852
+ <div style={{ flex: 1, minHeight: 0 }}>
1853
+ {renderInput()}
1854
+ </div>
1855
+
1856
+ {parameter.description && (
1857
+ <div style={{
1858
+ fontSize: '12px',
1859
+ color: theme.colors.textSecondary,
1860
+ marginTop: '6px',
1861
+ lineHeight: '1.5',
1862
+ fontFamily: 'system-ui, sans-serif',
1863
+ paddingLeft: '2px',
1864
+ flexShrink: 0
1865
+ }}>
1866
+ {parameter.description}
1867
+ </div>
1868
+ )}
1869
+
1870
+ </div>
1871
+ );
1872
+ };
1873
+
1874
1874
  export default ParameterRenderer;