machinaos 0.0.1 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (420) hide show
  1. package/.env.template +71 -71
  2. package/LICENSE +21 -21
  3. package/README.md +145 -87
  4. package/bin/cli.js +62 -106
  5. package/client/.dockerignore +45 -45
  6. package/client/Dockerfile +68 -68
  7. package/client/dist/assets/index-DFSC53FP.css +1 -0
  8. package/client/dist/assets/index-fJ-1gTf5.js +613 -0
  9. package/client/dist/index.html +14 -0
  10. package/client/eslint.config.js +34 -16
  11. package/client/nginx.conf +66 -66
  12. package/client/package.json +61 -48
  13. package/client/src/App.tsx +27 -27
  14. package/client/src/Dashboard.tsx +1200 -1172
  15. package/client/src/ParameterPanel.tsx +302 -300
  16. package/client/src/components/AIAgentNode.tsx +315 -321
  17. package/client/src/components/APIKeyValidator.tsx +117 -117
  18. package/client/src/components/ClaudeChatModelNode.tsx +17 -17
  19. package/client/src/components/CredentialsModal.tsx +1200 -306
  20. package/client/src/components/GeminiChatModelNode.tsx +17 -17
  21. package/client/src/components/GenericNode.tsx +356 -356
  22. package/client/src/components/LocationParameterPanel.tsx +153 -153
  23. package/client/src/components/ModelNode.tsx +285 -285
  24. package/client/src/components/OpenAIChatModelNode.tsx +17 -17
  25. package/client/src/components/OutputPanel.tsx +470 -470
  26. package/client/src/components/ParameterRenderer.tsx +1873 -1873
  27. package/client/src/components/SkillEditorModal.tsx +3 -3
  28. package/client/src/components/SquareNode.tsx +812 -796
  29. package/client/src/components/ToolkitNode.tsx +365 -365
  30. package/client/src/components/auth/LoginPage.tsx +247 -247
  31. package/client/src/components/auth/ProtectedRoute.tsx +59 -59
  32. package/client/src/components/base/BaseChatModelNode.tsx +270 -270
  33. package/client/src/components/icons/AIProviderIcons.tsx +50 -50
  34. package/client/src/components/maps/GoogleMapsPicker.tsx +136 -136
  35. package/client/src/components/maps/MapsPreviewPanel.tsx +109 -109
  36. package/client/src/components/maps/index.ts +25 -25
  37. package/client/src/components/parameterPanel/InputSection.tsx +1094 -1094
  38. package/client/src/components/parameterPanel/LocationPanelLayout.tsx +64 -64
  39. package/client/src/components/parameterPanel/MapsSection.tsx +91 -91
  40. package/client/src/components/parameterPanel/MiddleSection.tsx +867 -571
  41. package/client/src/components/parameterPanel/OutputSection.tsx +80 -80
  42. package/client/src/components/parameterPanel/ParameterPanelLayout.tsx +81 -81
  43. package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +436 -436
  44. package/client/src/components/parameterPanel/index.ts +41 -41
  45. package/client/src/components/shared/DataPanel.tsx +142 -142
  46. package/client/src/components/shared/JSONTreeRenderer.tsx +105 -105
  47. package/client/src/components/ui/AIResultModal.tsx +203 -203
  48. package/client/src/components/ui/ApiKeyInput.tsx +93 -0
  49. package/client/src/components/ui/CodeEditor.tsx +81 -81
  50. package/client/src/components/ui/CollapsibleSection.tsx +87 -87
  51. package/client/src/components/ui/ComponentItem.tsx +153 -153
  52. package/client/src/components/ui/ComponentPalette.tsx +320 -320
  53. package/client/src/components/ui/ConsolePanel.tsx +151 -43
  54. package/client/src/components/ui/ErrorBoundary.tsx +195 -195
  55. package/client/src/components/ui/InputNodesPanel.tsx +203 -203
  56. package/client/src/components/ui/MapSelector.tsx +313 -313
  57. package/client/src/components/ui/Modal.tsx +151 -148
  58. package/client/src/components/ui/NodeOutputPanel.tsx +1150 -1150
  59. package/client/src/components/ui/OutputDisplayPanel.tsx +381 -381
  60. package/client/src/components/ui/QRCodeDisplay.tsx +182 -0
  61. package/client/src/components/ui/TopToolbar.tsx +736 -736
  62. package/client/src/components/ui/WorkflowSidebar.tsx +293 -293
  63. package/client/src/config/antdTheme.ts +186 -186
  64. package/client/src/contexts/AuthContext.tsx +221 -221
  65. package/client/src/contexts/ThemeContext.tsx +42 -42
  66. package/client/src/contexts/WebSocketContext.tsx +2144 -1971
  67. package/client/src/factories/baseChatModelFactory.ts +255 -255
  68. package/client/src/hooks/useAndroidOperations.ts +118 -164
  69. package/client/src/hooks/useApiKeyValidation.ts +106 -106
  70. package/client/src/hooks/useApiKeys.ts +238 -238
  71. package/client/src/hooks/useAppTheme.ts +17 -17
  72. package/client/src/hooks/useComponentPalette.ts +50 -50
  73. package/client/src/hooks/useDragAndDrop.ts +123 -123
  74. package/client/src/hooks/useDragVariable.ts +88 -88
  75. package/client/src/hooks/useExecution.ts +319 -313
  76. package/client/src/hooks/useParameterPanel.ts +176 -176
  77. package/client/src/hooks/useReactFlowNodes.ts +188 -188
  78. package/client/src/hooks/useToolSchema.ts +209 -209
  79. package/client/src/hooks/useWhatsApp.ts +196 -196
  80. package/client/src/hooks/useWorkflowManagement.ts +45 -45
  81. package/client/src/index.css +314 -314
  82. package/client/src/nodeDefinitions/aiAgentNodes.ts +335 -335
  83. package/client/src/nodeDefinitions/aiModelNodes.ts +340 -340
  84. package/client/src/nodeDefinitions/androidServiceNodes.ts +383 -383
  85. package/client/src/nodeDefinitions/chatNodes.ts +135 -135
  86. package/client/src/nodeDefinitions/codeNodes.ts +54 -54
  87. package/client/src/nodeDefinitions/index.ts +14 -14
  88. package/client/src/nodeDefinitions/locationNodes.ts +462 -462
  89. package/client/src/nodeDefinitions/schedulerNodes.ts +220 -220
  90. package/client/src/nodeDefinitions/skillNodes.ts +17 -5
  91. package/client/src/nodeDefinitions/utilityNodes.ts +284 -284
  92. package/client/src/nodeDefinitions/whatsappNodes.ts +821 -865
  93. package/client/src/nodeDefinitions.ts +101 -103
  94. package/client/src/services/dynamicParameterService.ts +95 -95
  95. package/client/src/services/execution/aiAgentExecutionService.ts +34 -34
  96. package/client/src/services/executionService.ts +227 -231
  97. package/client/src/services/workflowApi.ts +91 -91
  98. package/client/src/store/useAppStore.ts +578 -581
  99. package/client/src/styles/theme.ts +513 -508
  100. package/client/src/styles/zIndex.ts +16 -16
  101. package/client/src/types/ComponentTypes.ts +38 -38
  102. package/client/src/types/INodeProperties.ts +287 -287
  103. package/client/src/types/NodeTypes.ts +27 -27
  104. package/client/src/utils/formatters.ts +32 -32
  105. package/client/src/utils/googleMapsLoader.ts +139 -139
  106. package/client/src/utils/locationUtils.ts +84 -84
  107. package/client/src/utils/nodeUtils.ts +30 -30
  108. package/client/src/utils/workflow.ts +29 -29
  109. package/client/src/vite-env.d.ts +12 -12
  110. package/client/tailwind.config.js +59 -59
  111. package/client/tsconfig.json +25 -25
  112. package/client/vite.config.js +35 -35
  113. package/package.json +78 -70
  114. package/scripts/build.js +153 -45
  115. package/scripts/clean.js +40 -40
  116. package/scripts/start.js +234 -210
  117. package/scripts/stop.js +301 -325
  118. package/server/.dockerignore +44 -44
  119. package/server/Dockerfile +45 -45
  120. package/server/constants.py +244 -249
  121. package/server/core/cache.py +460 -460
  122. package/server/core/config.py +127 -127
  123. package/server/core/container.py +98 -98
  124. package/server/core/database.py +1296 -1210
  125. package/server/core/logging.py +313 -313
  126. package/server/main.py +288 -288
  127. package/server/middleware/__init__.py +5 -5
  128. package/server/middleware/auth.py +89 -89
  129. package/server/models/auth.py +52 -52
  130. package/server/models/cache.py +24 -24
  131. package/server/models/database.py +235 -210
  132. package/server/models/nodes.py +435 -455
  133. package/server/pyproject.toml +75 -72
  134. package/server/requirements.txt +83 -83
  135. package/server/routers/android.py +294 -294
  136. package/server/routers/auth.py +203 -203
  137. package/server/routers/database.py +150 -150
  138. package/server/routers/maps.py +141 -141
  139. package/server/routers/nodejs_compat.py +288 -288
  140. package/server/routers/webhook.py +90 -90
  141. package/server/routers/websocket.py +2239 -2127
  142. package/server/routers/whatsapp.py +761 -761
  143. package/server/routers/workflow.py +199 -199
  144. package/server/services/ai.py +2444 -2414
  145. package/server/services/android_service.py +588 -588
  146. package/server/services/auth.py +130 -130
  147. package/server/services/chat_client.py +160 -160
  148. package/server/services/deployment/manager.py +706 -706
  149. package/server/services/event_waiter.py +675 -785
  150. package/server/services/execution/executor.py +1351 -1351
  151. package/server/services/execution/models.py +1 -1
  152. package/server/services/handlers/__init__.py +122 -126
  153. package/server/services/handlers/ai.py +390 -355
  154. package/server/services/handlers/android.py +69 -260
  155. package/server/services/handlers/code.py +278 -278
  156. package/server/services/handlers/http.py +193 -193
  157. package/server/services/handlers/tools.py +146 -32
  158. package/server/services/handlers/triggers.py +107 -107
  159. package/server/services/handlers/utility.py +822 -822
  160. package/server/services/handlers/whatsapp.py +423 -476
  161. package/server/services/maps.py +288 -288
  162. package/server/services/memory_store.py +103 -103
  163. package/server/services/node_executor.py +372 -375
  164. package/server/services/scheduler.py +155 -155
  165. package/server/services/skill_loader.py +1 -1
  166. package/server/services/status_broadcaster.py +834 -826
  167. package/server/services/temporal/__init__.py +23 -23
  168. package/server/services/temporal/activities.py +344 -344
  169. package/server/services/temporal/client.py +76 -76
  170. package/server/services/temporal/executor.py +147 -147
  171. package/server/services/temporal/worker.py +251 -251
  172. package/server/services/temporal/workflow.py +355 -355
  173. package/server/services/temporal/ws_client.py +236 -236
  174. package/server/services/text.py +110 -110
  175. package/server/services/user_auth.py +172 -172
  176. package/server/services/websocket_client.py +29 -29
  177. package/server/services/workflow.py +597 -597
  178. package/server/skills/android-skill/SKILL.md +4 -4
  179. package/server/skills/code-skill/SKILL.md +123 -89
  180. package/server/skills/maps-skill/SKILL.md +3 -3
  181. package/server/skills/memory-skill/SKILL.md +1 -1
  182. package/server/skills/web-search-skill/SKILL.md +154 -0
  183. package/server/skills/whatsapp-skill/SKILL.md +3 -3
  184. package/server/uv.lock +461 -100
  185. package/server/whatsapp-rpc/.dockerignore +30 -30
  186. package/server/whatsapp-rpc/Dockerfile +44 -44
  187. package/server/whatsapp-rpc/Dockerfile.web +17 -17
  188. package/server/whatsapp-rpc/README.md +139 -139
  189. package/server/whatsapp-rpc/bin/whatsapp-rpc-server +0 -0
  190. package/server/whatsapp-rpc/cli.js +95 -95
  191. package/server/whatsapp-rpc/configs/config.yaml +6 -6
  192. package/server/whatsapp-rpc/docker-compose.yml +35 -35
  193. package/server/whatsapp-rpc/docs/API.md +410 -410
  194. package/server/whatsapp-rpc/node_modules/.package-lock.json +259 -0
  195. package/server/whatsapp-rpc/node_modules/chalk/license +9 -0
  196. package/server/whatsapp-rpc/node_modules/chalk/package.json +83 -0
  197. package/server/whatsapp-rpc/node_modules/chalk/readme.md +297 -0
  198. package/server/whatsapp-rpc/node_modules/chalk/source/index.d.ts +325 -0
  199. package/server/whatsapp-rpc/node_modules/chalk/source/index.js +225 -0
  200. package/server/whatsapp-rpc/node_modules/chalk/source/utilities.js +33 -0
  201. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  202. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  203. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  204. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  205. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  206. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  207. package/server/whatsapp-rpc/node_modules/commander/LICENSE +22 -0
  208. package/server/whatsapp-rpc/node_modules/commander/Readme.md +1148 -0
  209. package/server/whatsapp-rpc/node_modules/commander/esm.mjs +16 -0
  210. package/server/whatsapp-rpc/node_modules/commander/index.js +26 -0
  211. package/server/whatsapp-rpc/node_modules/commander/lib/argument.js +145 -0
  212. package/server/whatsapp-rpc/node_modules/commander/lib/command.js +2179 -0
  213. package/server/whatsapp-rpc/node_modules/commander/lib/error.js +43 -0
  214. package/server/whatsapp-rpc/node_modules/commander/lib/help.js +462 -0
  215. package/server/whatsapp-rpc/node_modules/commander/lib/option.js +329 -0
  216. package/server/whatsapp-rpc/node_modules/commander/lib/suggestSimilar.js +100 -0
  217. package/server/whatsapp-rpc/node_modules/commander/package-support.json +16 -0
  218. package/server/whatsapp-rpc/node_modules/commander/package.json +80 -0
  219. package/server/whatsapp-rpc/node_modules/commander/typings/esm.d.mts +3 -0
  220. package/server/whatsapp-rpc/node_modules/commander/typings/index.d.ts +884 -0
  221. package/server/whatsapp-rpc/node_modules/cross-spawn/LICENSE +21 -0
  222. package/server/whatsapp-rpc/node_modules/cross-spawn/README.md +89 -0
  223. package/server/whatsapp-rpc/node_modules/cross-spawn/index.js +39 -0
  224. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/enoent.js +59 -0
  225. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/parse.js +91 -0
  226. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/escape.js +47 -0
  227. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/readShebang.js +23 -0
  228. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/resolveCommand.js +52 -0
  229. package/server/whatsapp-rpc/node_modules/cross-spawn/package.json +73 -0
  230. package/server/whatsapp-rpc/node_modules/execa/index.d.ts +955 -0
  231. package/server/whatsapp-rpc/node_modules/execa/index.js +309 -0
  232. package/server/whatsapp-rpc/node_modules/execa/lib/command.js +119 -0
  233. package/server/whatsapp-rpc/node_modules/execa/lib/error.js +87 -0
  234. package/server/whatsapp-rpc/node_modules/execa/lib/kill.js +102 -0
  235. package/server/whatsapp-rpc/node_modules/execa/lib/pipe.js +42 -0
  236. package/server/whatsapp-rpc/node_modules/execa/lib/promise.js +36 -0
  237. package/server/whatsapp-rpc/node_modules/execa/lib/stdio.js +49 -0
  238. package/server/whatsapp-rpc/node_modules/execa/lib/stream.js +133 -0
  239. package/server/whatsapp-rpc/node_modules/execa/lib/verbose.js +19 -0
  240. package/server/whatsapp-rpc/node_modules/execa/license +9 -0
  241. package/server/whatsapp-rpc/node_modules/execa/package.json +90 -0
  242. package/server/whatsapp-rpc/node_modules/execa/readme.md +822 -0
  243. package/server/whatsapp-rpc/node_modules/get-stream/license +9 -0
  244. package/server/whatsapp-rpc/node_modules/get-stream/package.json +53 -0
  245. package/server/whatsapp-rpc/node_modules/get-stream/readme.md +291 -0
  246. package/server/whatsapp-rpc/node_modules/get-stream/source/array-buffer.js +84 -0
  247. package/server/whatsapp-rpc/node_modules/get-stream/source/array.js +32 -0
  248. package/server/whatsapp-rpc/node_modules/get-stream/source/buffer.js +20 -0
  249. package/server/whatsapp-rpc/node_modules/get-stream/source/contents.js +101 -0
  250. package/server/whatsapp-rpc/node_modules/get-stream/source/index.d.ts +119 -0
  251. package/server/whatsapp-rpc/node_modules/get-stream/source/index.js +5 -0
  252. package/server/whatsapp-rpc/node_modules/get-stream/source/string.js +36 -0
  253. package/server/whatsapp-rpc/node_modules/get-stream/source/utils.js +11 -0
  254. package/server/whatsapp-rpc/node_modules/get-them-args/LICENSE +21 -0
  255. package/server/whatsapp-rpc/node_modules/get-them-args/README.md +95 -0
  256. package/server/whatsapp-rpc/node_modules/get-them-args/index.js +97 -0
  257. package/server/whatsapp-rpc/node_modules/get-them-args/package.json +36 -0
  258. package/server/whatsapp-rpc/node_modules/human-signals/LICENSE +201 -0
  259. package/server/whatsapp-rpc/node_modules/human-signals/README.md +168 -0
  260. package/server/whatsapp-rpc/node_modules/human-signals/build/src/core.js +273 -0
  261. package/server/whatsapp-rpc/node_modules/human-signals/build/src/main.d.ts +73 -0
  262. package/server/whatsapp-rpc/node_modules/human-signals/build/src/main.js +70 -0
  263. package/server/whatsapp-rpc/node_modules/human-signals/build/src/realtime.js +16 -0
  264. package/server/whatsapp-rpc/node_modules/human-signals/build/src/signals.js +34 -0
  265. package/server/whatsapp-rpc/node_modules/human-signals/package.json +61 -0
  266. package/server/whatsapp-rpc/node_modules/is-stream/index.d.ts +81 -0
  267. package/server/whatsapp-rpc/node_modules/is-stream/index.js +29 -0
  268. package/server/whatsapp-rpc/node_modules/is-stream/license +9 -0
  269. package/server/whatsapp-rpc/node_modules/is-stream/package.json +44 -0
  270. package/server/whatsapp-rpc/node_modules/is-stream/readme.md +60 -0
  271. package/server/whatsapp-rpc/node_modules/isexe/LICENSE +15 -0
  272. package/server/whatsapp-rpc/node_modules/isexe/README.md +51 -0
  273. package/server/whatsapp-rpc/node_modules/isexe/index.js +57 -0
  274. package/server/whatsapp-rpc/node_modules/isexe/mode.js +41 -0
  275. package/server/whatsapp-rpc/node_modules/isexe/package.json +31 -0
  276. package/server/whatsapp-rpc/node_modules/isexe/test/basic.js +221 -0
  277. package/server/whatsapp-rpc/node_modules/isexe/windows.js +42 -0
  278. package/server/whatsapp-rpc/node_modules/kill-port/.editorconfig +12 -0
  279. package/server/whatsapp-rpc/node_modules/kill-port/.gitattributes +1 -0
  280. package/server/whatsapp-rpc/node_modules/kill-port/LICENSE +21 -0
  281. package/server/whatsapp-rpc/node_modules/kill-port/README.md +140 -0
  282. package/server/whatsapp-rpc/node_modules/kill-port/cli.js +25 -0
  283. package/server/whatsapp-rpc/node_modules/kill-port/example.js +21 -0
  284. package/server/whatsapp-rpc/node_modules/kill-port/index.js +46 -0
  285. package/server/whatsapp-rpc/node_modules/kill-port/logo.png +0 -0
  286. package/server/whatsapp-rpc/node_modules/kill-port/package.json +41 -0
  287. package/server/whatsapp-rpc/node_modules/kill-port/pnpm-lock.yaml +4606 -0
  288. package/server/whatsapp-rpc/node_modules/kill-port/test.js +16 -0
  289. package/server/whatsapp-rpc/node_modules/merge-stream/LICENSE +21 -0
  290. package/server/whatsapp-rpc/node_modules/merge-stream/README.md +78 -0
  291. package/server/whatsapp-rpc/node_modules/merge-stream/index.js +41 -0
  292. package/server/whatsapp-rpc/node_modules/merge-stream/package.json +19 -0
  293. package/server/whatsapp-rpc/node_modules/mimic-fn/index.d.ts +52 -0
  294. package/server/whatsapp-rpc/node_modules/mimic-fn/index.js +71 -0
  295. package/server/whatsapp-rpc/node_modules/mimic-fn/license +9 -0
  296. package/server/whatsapp-rpc/node_modules/mimic-fn/package.json +45 -0
  297. package/server/whatsapp-rpc/node_modules/mimic-fn/readme.md +90 -0
  298. package/server/whatsapp-rpc/node_modules/npm-run-path/index.d.ts +90 -0
  299. package/server/whatsapp-rpc/node_modules/npm-run-path/index.js +52 -0
  300. package/server/whatsapp-rpc/node_modules/npm-run-path/license +9 -0
  301. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/index.d.ts +31 -0
  302. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/index.js +12 -0
  303. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/license +9 -0
  304. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/package.json +41 -0
  305. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/readme.md +57 -0
  306. package/server/whatsapp-rpc/node_modules/npm-run-path/package.json +49 -0
  307. package/server/whatsapp-rpc/node_modules/npm-run-path/readme.md +104 -0
  308. package/server/whatsapp-rpc/node_modules/onetime/index.d.ts +59 -0
  309. package/server/whatsapp-rpc/node_modules/onetime/index.js +41 -0
  310. package/server/whatsapp-rpc/node_modules/onetime/license +9 -0
  311. package/server/whatsapp-rpc/node_modules/onetime/package.json +45 -0
  312. package/server/whatsapp-rpc/node_modules/onetime/readme.md +94 -0
  313. package/server/whatsapp-rpc/node_modules/path-key/index.d.ts +40 -0
  314. package/server/whatsapp-rpc/node_modules/path-key/index.js +16 -0
  315. package/server/whatsapp-rpc/node_modules/path-key/license +9 -0
  316. package/server/whatsapp-rpc/node_modules/path-key/package.json +39 -0
  317. package/server/whatsapp-rpc/node_modules/path-key/readme.md +61 -0
  318. package/server/whatsapp-rpc/node_modules/shebang-command/index.js +19 -0
  319. package/server/whatsapp-rpc/node_modules/shebang-command/license +9 -0
  320. package/server/whatsapp-rpc/node_modules/shebang-command/package.json +34 -0
  321. package/server/whatsapp-rpc/node_modules/shebang-command/readme.md +34 -0
  322. package/server/whatsapp-rpc/node_modules/shebang-regex/index.d.ts +22 -0
  323. package/server/whatsapp-rpc/node_modules/shebang-regex/index.js +2 -0
  324. package/server/whatsapp-rpc/node_modules/shebang-regex/license +9 -0
  325. package/server/whatsapp-rpc/node_modules/shebang-regex/package.json +35 -0
  326. package/server/whatsapp-rpc/node_modules/shebang-regex/readme.md +33 -0
  327. package/server/whatsapp-rpc/node_modules/shell-exec/LICENSE +21 -0
  328. package/server/whatsapp-rpc/node_modules/shell-exec/README.md +60 -0
  329. package/server/whatsapp-rpc/node_modules/shell-exec/index.js +47 -0
  330. package/server/whatsapp-rpc/node_modules/shell-exec/package.json +29 -0
  331. package/server/whatsapp-rpc/node_modules/signal-exit/LICENSE.txt +16 -0
  332. package/server/whatsapp-rpc/node_modules/signal-exit/README.md +74 -0
  333. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.d.ts +12 -0
  334. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.d.ts.map +1 -0
  335. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.js +10 -0
  336. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.js.map +1 -0
  337. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.d.ts +48 -0
  338. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.d.ts.map +1 -0
  339. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.js +279 -0
  340. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.js.map +1 -0
  341. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/package.json +3 -0
  342. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.d.ts +29 -0
  343. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.d.ts.map +1 -0
  344. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.js +42 -0
  345. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.js.map +1 -0
  346. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.d.ts +12 -0
  347. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.d.ts.map +1 -0
  348. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.js +4 -0
  349. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.js.map +1 -0
  350. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.d.ts +48 -0
  351. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.d.ts.map +1 -0
  352. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.js +275 -0
  353. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.js.map +1 -0
  354. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/package.json +3 -0
  355. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.d.ts +29 -0
  356. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.d.ts.map +1 -0
  357. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.js +39 -0
  358. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.js.map +1 -0
  359. package/server/whatsapp-rpc/node_modules/signal-exit/package.json +106 -0
  360. package/server/whatsapp-rpc/node_modules/strip-final-newline/index.js +14 -0
  361. package/server/whatsapp-rpc/node_modules/strip-final-newline/license +9 -0
  362. package/server/whatsapp-rpc/node_modules/strip-final-newline/package.json +43 -0
  363. package/server/whatsapp-rpc/node_modules/strip-final-newline/readme.md +35 -0
  364. package/server/whatsapp-rpc/node_modules/which/CHANGELOG.md +166 -0
  365. package/server/whatsapp-rpc/node_modules/which/LICENSE +15 -0
  366. package/server/whatsapp-rpc/node_modules/which/README.md +54 -0
  367. package/server/whatsapp-rpc/node_modules/which/bin/node-which +52 -0
  368. package/server/whatsapp-rpc/node_modules/which/package.json +43 -0
  369. package/server/whatsapp-rpc/node_modules/which/which.js +125 -0
  370. package/server/whatsapp-rpc/package-lock.json +272 -0
  371. package/server/whatsapp-rpc/package.json +30 -30
  372. package/server/whatsapp-rpc/schema.json +1294 -1294
  373. package/server/whatsapp-rpc/scripts/clean.cjs +66 -66
  374. package/server/whatsapp-rpc/scripts/cli.js +162 -162
  375. package/server/whatsapp-rpc/src/go/whatsapp/history.go +166 -166
  376. package/server/whatsapp-rpc/src/python/pyproject.toml +15 -15
  377. package/server/whatsapp-rpc/src/python/whatsapp_rpc/__init__.py +4 -4
  378. package/server/whatsapp-rpc/src/python/whatsapp_rpc/client.py +427 -427
  379. package/server/whatsapp-rpc/web/app.py +609 -609
  380. package/server/whatsapp-rpc/web/requirements.txt +6 -6
  381. package/server/whatsapp-rpc/web/rpc_client.py +427 -427
  382. package/server/whatsapp-rpc/web/static/openapi.yaml +59 -59
  383. package/server/whatsapp-rpc/web/templates/base.html +149 -149
  384. package/server/whatsapp-rpc/web/templates/contacts.html +240 -240
  385. package/server/whatsapp-rpc/web/templates/dashboard.html +319 -319
  386. package/server/whatsapp-rpc/web/templates/groups.html +328 -328
  387. package/server/whatsapp-rpc/web/templates/messages.html +465 -465
  388. package/server/whatsapp-rpc/web/templates/messaging.html +680 -680
  389. package/server/whatsapp-rpc/web/templates/send.html +258 -258
  390. package/server/whatsapp-rpc/web/templates/settings.html +459 -459
  391. package/client/src/components/ui/AndroidSettingsPanel.tsx +0 -401
  392. package/client/src/components/ui/WhatsAppSettingsPanel.tsx +0 -345
  393. package/client/src/nodeDefinitions/androidDeviceNodes.ts +0 -140
  394. package/docker-compose.prod.yml +0 -107
  395. package/docker-compose.yml +0 -104
  396. package/docs-MachinaOs/README.md +0 -85
  397. package/docs-MachinaOs/deployment/docker.mdx +0 -228
  398. package/docs-MachinaOs/deployment/production.mdx +0 -345
  399. package/docs-MachinaOs/docs.json +0 -75
  400. package/docs-MachinaOs/faq.mdx +0 -309
  401. package/docs-MachinaOs/favicon.svg +0 -5
  402. package/docs-MachinaOs/installation.mdx +0 -160
  403. package/docs-MachinaOs/introduction.mdx +0 -114
  404. package/docs-MachinaOs/logo/dark.svg +0 -6
  405. package/docs-MachinaOs/logo/light.svg +0 -6
  406. package/docs-MachinaOs/nodes/ai-agent.mdx +0 -216
  407. package/docs-MachinaOs/nodes/ai-models.mdx +0 -240
  408. package/docs-MachinaOs/nodes/android.mdx +0 -411
  409. package/docs-MachinaOs/nodes/overview.mdx +0 -181
  410. package/docs-MachinaOs/nodes/schedulers.mdx +0 -316
  411. package/docs-MachinaOs/nodes/webhooks.mdx +0 -330
  412. package/docs-MachinaOs/nodes/whatsapp.mdx +0 -305
  413. package/docs-MachinaOs/quickstart.mdx +0 -119
  414. package/docs-MachinaOs/tutorials/ai-agent-workflow.mdx +0 -177
  415. package/docs-MachinaOs/tutorials/android-automation.mdx +0 -242
  416. package/docs-MachinaOs/tutorials/first-workflow.mdx +0 -134
  417. package/docs-MachinaOs/tutorials/whatsapp-automation.mdx +0 -185
  418. package/nul +0 -0
  419. package/scripts/check-ports.ps1 +0 -33
  420. package/scripts/kill-port.ps1 +0 -154
@@ -1,306 +1,1200 @@
1
- /**
2
- * CredentialsModal - Modern categorized credentials management panel
3
- * Uses Ant Design Collapse + List for compact, modern UI
4
- */
5
-
6
- import React, { useState, useEffect, useCallback, useMemo } from 'react';
7
- import { Input, Button, message, Collapse, List, Tag, Space, Tooltip } from 'antd';
8
- import {
9
- CheckCircleOutlined,
10
- DeleteOutlined,
11
- SafetyOutlined,
12
- SearchOutlined,
13
- EyeOutlined,
14
- EyeInvisibleOutlined,
15
- } from '@ant-design/icons';
16
- import Modal from './ui/Modal';
17
- import { useApiKeys } from '../hooks/useApiKeys';
18
- import { useAppTheme } from '../hooks/useAppTheme';
19
- import {
20
- OpenAIIcon, ClaudeIcon, GeminiIcon, GroqIcon, OpenRouterIcon, CerebrasIcon,
21
- } from './icons/AIProviderIcons';
22
-
23
- // ============================================================================
24
- // TYPES & DATA
25
- // ============================================================================
26
-
27
- interface CredentialItem {
28
- id: string;
29
- name: string;
30
- placeholder: string;
31
- color: string;
32
- desc: string;
33
- Icon?: React.FC<{ size?: number }>;
34
- }
35
-
36
- interface Category {
37
- key: string;
38
- label: string;
39
- items: CredentialItem[];
40
- }
41
-
42
- const CATEGORIES: Category[] = [
43
- {
44
- key: 'ai',
45
- label: 'AI Providers',
46
- items: [
47
- { id: 'openai', name: 'OpenAI', placeholder: 'sk-...', color: '#10a37f', desc: 'GPT-4o, GPT-4, GPT-3.5', Icon: OpenAIIcon },
48
- { id: 'anthropic', name: 'Anthropic', placeholder: 'sk-ant-...', color: '#d97706', desc: 'Claude 3.5 Sonnet, Opus', Icon: ClaudeIcon },
49
- { id: 'gemini', name: 'Gemini', placeholder: 'AIza...', color: '#4285f4', desc: 'Gemini 1.5 Pro, Flash', Icon: GeminiIcon },
50
- { id: 'groq', name: 'Groq', placeholder: 'gsk_...', color: '#F55036', desc: 'Llama, Mixtral - Ultra-fast', Icon: GroqIcon },
51
- { id: 'cerebras', name: 'Cerebras', placeholder: 'csk-...', color: '#FF6600', desc: 'Llama, Qwen - Ultra-fast', Icon: CerebrasIcon },
52
- { id: 'openrouter', name: 'OpenRouter', placeholder: 'sk-or-...', color: '#6366f1', desc: 'Unified API - 100+ models', Icon: OpenRouterIcon },
53
- ],
54
- },
55
- {
56
- key: 'services',
57
- label: 'Services',
58
- items: [
59
- { id: 'google_maps', name: 'Google Maps', placeholder: 'AIza...', color: '#34a853', desc: 'Geocoding, Places API' },
60
- { id: 'android_remote', name: 'Android Remote', placeholder: 'your-api-key...', color: '#3ddc84', desc: 'WebSocket device control' },
61
- ],
62
- },
63
- ];
64
-
65
- // ============================================================================
66
- // MAIN COMPONENT
67
- // ============================================================================
68
-
69
- interface Props {
70
- visible: boolean;
71
- onClose: () => void;
72
- }
73
-
74
- const CredentialsModal: React.FC<Props> = ({ visible, onClose }) => {
75
- const theme = useAppTheme();
76
- const { validateApiKey, saveApiKey, getStoredApiKey, hasStoredKey, removeApiKey, validateGoogleMapsKey } = useApiKeys();
77
-
78
- const [validKeys, setValidKeys] = useState<Record<string, boolean>>({});
79
- const [loading, setLoading] = useState<Record<string, boolean>>({});
80
- const [models, setModels] = useState<Record<string, string[]>>({});
81
- const [keys, setKeys] = useState<Record<string, string>>({});
82
- const [expanded, setExpanded] = useState<string | null>(null);
83
- const [search, setSearch] = useState('');
84
- const [showKey, setShowKey] = useState<Record<string, boolean>>({});
85
-
86
- // Load stored keys
87
- const loadKeys = useCallback(async () => {
88
- const allItems = CATEGORIES.flatMap(c => c.items);
89
- const newKeys: Record<string, string> = {};
90
- const newValid: Record<string, boolean> = {};
91
-
92
- for (const item of allItems) {
93
- if (await hasStoredKey(item.id)) {
94
- const key = await getStoredApiKey(item.id);
95
- if (key) {
96
- newKeys[item.id] = key;
97
- newValid[item.id] = true;
98
- }
99
- }
100
- }
101
- setKeys(newKeys);
102
- setValidKeys(newValid);
103
- }, [hasStoredKey, getStoredApiKey]);
104
-
105
- useEffect(() => {
106
- if (visible) {
107
- loadKeys();
108
- setExpanded(null);
109
- setSearch('');
110
- }
111
- }, [visible, loadKeys]);
112
-
113
- const handleValidate = async (id: string) => {
114
- const key = keys[id];
115
- if (!key?.trim()) return message.warning('Enter an API key first');
116
-
117
- setLoading(l => ({ ...l, [id]: true }));
118
- const result = id === 'google_maps'
119
- ? await validateGoogleMapsKey(key)
120
- : id === 'android_remote'
121
- ? await saveApiKey(id, key)
122
- : await validateApiKey(id, key);
123
- setLoading(l => ({ ...l, [id]: false }));
124
-
125
- if (result.isValid) {
126
- setValidKeys(v => ({ ...v, [id]: true }));
127
- if (result.models) setModels(m => ({ ...m, [id]: result.models! }));
128
- message.success('Key saved and validated');
129
- } else {
130
- message.error(result.error || 'Invalid key');
131
- }
132
- };
133
-
134
- const handleRemove = async (id: string) => {
135
- await removeApiKey(id);
136
- setKeys(k => ({ ...k, [id]: '' }));
137
- setValidKeys(v => ({ ...v, [id]: false }));
138
- setModels(m => ({ ...m, [id]: [] }));
139
- message.success('Key removed');
140
- };
141
-
142
- // Filter by search
143
- const filteredCategories = useMemo(() => {
144
- if (!search.trim()) return CATEGORIES;
145
- const q = search.toLowerCase();
146
- return CATEGORIES.map(cat => ({
147
- ...cat,
148
- items: cat.items.filter(i => i.name.toLowerCase().includes(q) || i.desc.toLowerCase().includes(q)),
149
- })).filter(cat => cat.items.length > 0);
150
- }, [search]);
151
-
152
- // Count configured per category
153
- const getCount = (items: typeof CATEGORIES[0]['items']) =>
154
- items.filter(i => validKeys[i.id]).length;
155
-
156
- return (
157
- <Modal isOpen={visible} onClose={onClose} title="API Credentials" maxWidth="95vw" maxHeight="95vh">
158
- <div style={{ padding: 20 }}>
159
- {/* Info banner */}
160
- <div style={{
161
- display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16,
162
- padding: 12, borderRadius: 8,
163
- backgroundColor: `${theme.dracula.green}10`,
164
- border: `1px solid ${theme.dracula.green}30`,
165
- }}>
166
- <SafetyOutlined style={{ color: theme.dracula.green }} />
167
- <span style={{ fontSize: 13, color: theme.colors.textSecondary }}>
168
- Keys are stored securely and used automatically by AI nodes.
169
- </span>
170
- </div>
171
-
172
- {/* Search */}
173
- <Input
174
- placeholder="Search credentials..."
175
- prefix={<SearchOutlined style={{ color: theme.colors.textMuted }} />}
176
- value={search}
177
- onChange={e => setSearch(e.target.value)}
178
- allowClear
179
- style={{ marginBottom: 16 }}
180
- />
181
-
182
- {/* Categories */}
183
- <Collapse
184
- defaultActiveKey={['ai', 'services']}
185
- ghost
186
- style={{ background: 'transparent' }}
187
- items={filteredCategories.map(cat => ({
188
- key: cat.key,
189
- label: (
190
- <Space>
191
- <span style={{ fontWeight: 600, textTransform: 'uppercase', fontSize: 11, letterSpacing: '0.05em' }}>
192
- {cat.label}
193
- </span>
194
- <Tag color={getCount(cat.items) === cat.items.length ? 'success' : 'default'}>
195
- {getCount(cat.items)}/{cat.items.length}
196
- </Tag>
197
- </Space>
198
- ),
199
- children: (
200
- <List
201
- dataSource={cat.items}
202
- renderItem={item => {
203
- const isExpanded = expanded === item.id;
204
- const isValid = validKeys[item.id];
205
- const Icon = item.Icon;
206
-
207
- return (
208
- <List.Item
209
- style={{
210
- cursor: 'pointer',
211
- padding: '12px 8px',
212
- borderRadius: 8,
213
- marginBottom: 4,
214
- backgroundColor: isExpanded ? theme.colors.backgroundAlt : 'transparent',
215
- flexDirection: 'column',
216
- alignItems: 'stretch',
217
- }}
218
- onClick={() => setExpanded(isExpanded ? null : item.id)}
219
- >
220
- {/* Row header */}
221
- <div style={{ display: 'flex', alignItems: 'center', width: '100%' }}>
222
- <div style={{
223
- width: 32, height: 32, borderRadius: 6, marginRight: 12,
224
- backgroundColor: `${item.color}15`,
225
- display: 'flex', alignItems: 'center', justifyContent: 'center',
226
- }}>
227
- {Icon ? <Icon size={18} /> : <span>{item.id === 'google_maps' ? '📍' : '📱'}</span>}
228
- </div>
229
- <div style={{ flex: 1 }}>
230
- <div style={{ fontWeight: 500, fontSize: 14 }}>{item.name}</div>
231
- <div style={{ fontSize: 12, color: theme.colors.textMuted }}>{item.desc}</div>
232
- </div>
233
- {isValid && (
234
- <Tag icon={<CheckCircleOutlined />} color="success">Valid</Tag>
235
- )}
236
- </div>
237
-
238
- {/* Expanded content */}
239
- {isExpanded && (
240
- <div style={{ marginTop: 12, paddingTop: 12, borderTop: `1px solid ${theme.colors.border}` }}
241
- onClick={e => e.stopPropagation()}
242
- >
243
- <Space.Compact style={{ width: '100%', marginBottom: 8 }}>
244
- <Input
245
- type={showKey[item.id] ? 'text' : 'password'}
246
- value={keys[item.id] || ''}
247
- onChange={e => {
248
- setKeys(k => ({ ...k, [item.id]: e.target.value }));
249
- setValidKeys(v => ({ ...v, [item.id]: false }));
250
- }}
251
- placeholder={item.placeholder}
252
- style={{ fontFamily: 'monospace', fontSize: 13 }}
253
- suffix={
254
- <Tooltip title={showKey[item.id] ? 'Hide' : 'Show'}>
255
- <span
256
- onClick={() => setShowKey(s => ({ ...s, [item.id]: !s[item.id] }))}
257
- style={{ cursor: 'pointer', color: theme.colors.textMuted }}
258
- >
259
- {showKey[item.id] ? <EyeInvisibleOutlined /> : <EyeOutlined />}
260
- </span>
261
- </Tooltip>
262
- }
263
- />
264
- <Button
265
- loading={loading[item.id]}
266
- onClick={() => handleValidate(item.id)}
267
- type={isValid ? 'primary' : 'default'}
268
- style={isValid ? { backgroundColor: theme.dracula.green, borderColor: theme.dracula.green } : {}}
269
- >
270
- {isValid ? 'Valid' : 'Validate'}
271
- </Button>
272
- </Space.Compact>
273
-
274
- {models[item.id]?.length > 0 && (
275
- <div style={{ fontSize: 12, color: theme.colors.textMuted, marginBottom: 8 }}>
276
- Models: {models[item.id].slice(0, 4).join(', ')}{models[item.id].length > 4 && ` +${models[item.id].length - 4}`}
277
- </div>
278
- )}
279
-
280
- {isValid && (
281
- <Button size="small" danger icon={<DeleteOutlined />} onClick={() => handleRemove(item.id)}>
282
- Remove
283
- </Button>
284
- )}
285
- </div>
286
- )}
287
- </List.Item>
288
- );
289
- }}
290
- />
291
- ),
292
- }))}
293
- />
294
-
295
- {/* Footer */}
296
- <div style={{ marginTop: 20, textAlign: 'right' }}>
297
- <Button type="primary" onClick={onClose} style={{ backgroundColor: theme.dracula.green, borderColor: theme.dracula.green }}>
298
- Done
299
- </Button>
300
- </div>
301
- </div>
302
- </Modal>
303
- );
304
- };
305
-
306
- export default CredentialsModal;
1
+ /**
2
+ * CredentialsModal - Two-panel credentials management
3
+ * Left: Category list with items
4
+ * Right: Detail/configuration panel for selected item (including QR pairing)
5
+ */
6
+
7
+ import React, { useState, useEffect, useCallback } from 'react';
8
+ import { Button, Tag, Alert, Descriptions, Space, InputNumber, Switch } from 'antd';
9
+ import {
10
+ CheckCircleOutlined,
11
+ SafetyOutlined,
12
+ } from '@ant-design/icons';
13
+ import Modal from './ui/Modal';
14
+ import QRCodeDisplay from './ui/QRCodeDisplay';
15
+ import ApiKeyInput from './ui/ApiKeyInput';
16
+ import { useApiKeys } from '../hooks/useApiKeys';
17
+ import { useAppTheme } from '../hooks/useAppTheme';
18
+ import { useWhatsAppStatus, useAndroidStatus, useWebSocket, RateLimitConfig, RateLimitStats } from '../contexts/WebSocketContext';
19
+ import { useWhatsApp } from '../hooks/useWhatsApp';
20
+ import {
21
+ OpenAIIcon, ClaudeIcon, GeminiIcon, GroqIcon, OpenRouterIcon, CerebrasIcon,
22
+ } from './icons/AIProviderIcons';
23
+
24
+ // ============================================================================
25
+ // SERVICE ICONS
26
+ // ============================================================================
27
+
28
+ const GoogleMapsIcon = () => (
29
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none">
30
+ <path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z" fill="#EA4335"/>
31
+ <circle cx="12" cy="9" r="2.5" fill="#fff"/>
32
+ </svg>
33
+ );
34
+
35
+ const AndroidIcon = () => (
36
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="#3DDC84">
37
+ <path d="M6 18c0 .55.45 1 1 1h1v3.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5V19h2v3.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5V19h1c.55 0 1-.45 1-1V8H6v10zM3.5 8C2.67 8 2 8.67 2 9.5v7c0 .83.67 1.5 1.5 1.5S5 17.33 5 16.5v-7C5 8.67 4.33 8 3.5 8zm17 0c-.83 0-1.5.67-1.5 1.5v7c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5v-7c0-.83-.67-1.5-1.5-1.5zm-4.97-5.84l1.3-1.3c.2-.2.2-.51 0-.71-.2-.2-.51-.2-.71 0l-1.48 1.48C13.85 1.23 12.95 1 12 1c-.96 0-1.86.23-2.66.63L7.85.15c-.2-.2-.51-.2-.71 0-.2.2-.2.51 0 .71l1.31 1.31C6.97 3.26 6 5.01 6 7h12c0-1.99-.97-3.75-2.47-4.84zM10 5H9V4h1v1zm5 0h-1V4h1v1z"/>
38
+ </svg>
39
+ );
40
+
41
+ const WhatsAppIcon = () => (
42
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="#25D366">
43
+ <path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/>
44
+ </svg>
45
+ );
46
+
47
+ // ============================================================================
48
+ // TYPES & DATA
49
+ // ============================================================================
50
+
51
+ interface CredentialItem {
52
+ id: string;
53
+ name: string;
54
+ placeholder: string;
55
+ color: string;
56
+ desc: string;
57
+ Icon?: React.FC<{ size?: number }>;
58
+ CustomIcon?: React.FC;
59
+ isSpecial?: boolean;
60
+ panelType?: 'whatsapp' | 'android';
61
+ }
62
+
63
+ interface Category {
64
+ key: string;
65
+ label: string;
66
+ items: CredentialItem[];
67
+ }
68
+
69
+ const CATEGORIES: Category[] = [
70
+ {
71
+ key: 'ai',
72
+ label: 'AI Providers',
73
+ items: [
74
+ { id: 'openai', name: 'OpenAI', placeholder: 'sk-...', color: '#10a37f', desc: 'GPT-4o, GPT-4, GPT-3.5', Icon: OpenAIIcon },
75
+ { id: 'anthropic', name: 'Anthropic', placeholder: 'sk-ant-...', color: '#d97706', desc: 'Claude 3.5 Sonnet, Opus', Icon: ClaudeIcon },
76
+ { id: 'gemini', name: 'Gemini', placeholder: 'AIza...', color: '#4285f4', desc: 'Gemini 2.0, 1.5 Pro/Flash', Icon: GeminiIcon },
77
+ { id: 'groq', name: 'Groq', placeholder: 'gsk_...', color: '#F55036', desc: 'Llama, Mixtral - Ultra-fast', Icon: GroqIcon },
78
+ { id: 'cerebras', name: 'Cerebras', placeholder: 'csk-...', color: '#FF6600', desc: 'Llama, Qwen - Ultra-fast', Icon: CerebrasIcon },
79
+ { id: 'openrouter', name: 'OpenRouter', placeholder: 'sk-or-...', color: '#6366f1', desc: 'Unified API - 200+ models', Icon: OpenRouterIcon },
80
+ ],
81
+ },
82
+ {
83
+ key: 'social',
84
+ label: 'Social Media',
85
+ items: [
86
+ { id: 'whatsapp_personal', name: 'WhatsApp Personal', placeholder: '', color: '#25D366', desc: 'Connect via QR code pairing', CustomIcon: WhatsAppIcon, isSpecial: true, panelType: 'whatsapp' },
87
+ ],
88
+ },
89
+ {
90
+ key: 'android',
91
+ label: 'Android',
92
+ items: [
93
+ { id: 'android_remote', name: 'Android Device', placeholder: 'your-api-key...', color: '#3DDC84', desc: 'API key + QR code pairing', CustomIcon: AndroidIcon, isSpecial: true, panelType: 'android' },
94
+ ],
95
+ },
96
+ {
97
+ key: 'services',
98
+ label: 'Services',
99
+ items: [
100
+ { id: 'google_maps', name: 'Google Maps', placeholder: 'AIza...', color: '#EA4335', desc: 'Geocoding, Places, Directions', CustomIcon: GoogleMapsIcon },
101
+ ],
102
+ },
103
+ ];
104
+
105
+
106
+ // ============================================================================
107
+ // MAIN COMPONENT
108
+ // ============================================================================
109
+
110
+ interface Props {
111
+ visible: boolean;
112
+ onClose: () => void;
113
+ }
114
+
115
+ const CredentialsModal: React.FC<Props> = ({ visible, onClose }) => {
116
+ const theme = useAppTheme();
117
+ const { validateApiKey, saveApiKey, getStoredApiKey, hasStoredKey, removeApiKey, validateGoogleMapsKey } = useApiKeys();
118
+ const whatsappStatus = useWhatsAppStatus();
119
+ const androidStatus = useAndroidStatus();
120
+
121
+ // Tag style helper - consistent theming for status tags
122
+ const getTagStyle = (type: 'success' | 'error' | 'warning'): React.CSSProperties => {
123
+ const color = type === 'success' ? theme.dracula.green
124
+ : type === 'error' ? theme.dracula.pink
125
+ : theme.dracula.orange;
126
+ return {
127
+ backgroundColor: `${color}25`,
128
+ borderColor: `${color}60`,
129
+ color: color,
130
+ };
131
+ };
132
+
133
+ const [validKeys, setValidKeys] = useState<Record<string, boolean>>({});
134
+ const [loading, setLoading] = useState<Record<string, boolean>>({});
135
+ const [models, setModels] = useState<Record<string, string[]>>({});
136
+ const [keys, setKeys] = useState<Record<string, string>>({});
137
+ const [selectedItem, setSelectedItem] = useState<CredentialItem | null>(null);
138
+
139
+ // Load stored keys
140
+ const loadKeys = useCallback(async () => {
141
+ const allItems = CATEGORIES.flatMap(c => c.items);
142
+ const newKeys: Record<string, string> = {};
143
+ const newValid: Record<string, boolean> = {};
144
+
145
+ for (const item of allItems) {
146
+ if (item.isSpecial) continue;
147
+ if (await hasStoredKey(item.id)) {
148
+ const key = await getStoredApiKey(item.id);
149
+ if (key) {
150
+ newKeys[item.id] = key;
151
+ newValid[item.id] = true;
152
+ }
153
+ }
154
+ }
155
+ setKeys(newKeys);
156
+ setValidKeys(newValid);
157
+ }, [hasStoredKey, getStoredApiKey]);
158
+
159
+ useEffect(() => {
160
+ if (visible) {
161
+ loadKeys();
162
+ // Select first item by default
163
+ const firstItem = CATEGORIES[0]?.items[0];
164
+ if (firstItem) setSelectedItem(firstItem);
165
+ }
166
+ }, [visible, loadKeys]);
167
+
168
+ const handleValidate = async (id: string) => {
169
+ const key = keys[id];
170
+ if (!key?.trim()) return;
171
+
172
+ setLoading(l => ({ ...l, [id]: true }));
173
+ const result = id === 'google_maps'
174
+ ? await validateGoogleMapsKey(key)
175
+ : id === 'android_remote'
176
+ ? await saveApiKey(id, key)
177
+ : await validateApiKey(id, key);
178
+ setLoading(l => ({ ...l, [id]: false }));
179
+
180
+ if (result.isValid) {
181
+ setValidKeys(v => ({ ...v, [id]: true }));
182
+ if (result.models) setModels(m => ({ ...m, [id]: result.models! }));
183
+ }
184
+ };
185
+
186
+ const handleRemove = async (id: string) => {
187
+ await removeApiKey(id);
188
+ setKeys(k => ({ ...k, [id]: '' }));
189
+ setValidKeys(v => ({ ...v, [id]: false }));
190
+ setModels(m => ({ ...m, [id]: [] }));
191
+ };
192
+
193
+ // Get status for special items
194
+ const getSpecialStatus = (item: CredentialItem) => {
195
+ if (item.panelType === 'whatsapp') {
196
+ return { connected: whatsappStatus.connected, label: whatsappStatus.connected ? 'Connected' : 'Not Connected' };
197
+ }
198
+ if (item.panelType === 'android') {
199
+ return { connected: androidStatus.paired, label: androidStatus.paired ? 'Paired' : 'Not Paired' };
200
+ }
201
+ return null;
202
+ };
203
+
204
+ // Count configured items
205
+ const totalConfigured = Object.values(validKeys).filter(Boolean).length;
206
+
207
+ // Header actions
208
+ const headerActions = (
209
+ <div style={{
210
+ display: 'flex',
211
+ gap: theme.spacing.lg,
212
+ alignItems: 'center',
213
+ }}>
214
+ <div style={{
215
+ display: 'flex',
216
+ alignItems: 'center',
217
+ gap: theme.spacing.sm,
218
+ fontSize: theme.fontSize.base,
219
+ fontWeight: theme.fontWeight.semibold,
220
+ color: theme.colors.text,
221
+ }}>
222
+ <SafetyOutlined style={{ fontSize: 18, color: theme.dracula.yellow }} />
223
+ <span>API Credentials</span>
224
+ </div>
225
+ <Tag
226
+ style={{
227
+ margin: 0,
228
+ fontSize: theme.fontSize.xs,
229
+ ...(totalConfigured > 0 ? getTagStyle('success') : {}),
230
+ }}
231
+ >
232
+ {totalConfigured} configured
233
+ </Tag>
234
+ </div>
235
+ );
236
+
237
+ // Render left sidebar item
238
+ const renderSidebarItem = (item: CredentialItem) => {
239
+ const isSelected = selectedItem?.id === item.id;
240
+ const isValid = validKeys[item.id];
241
+ const specialStatus = item.isSpecial ? getSpecialStatus(item) : null;
242
+ const Icon = item.Icon;
243
+ const CustomIcon = item.CustomIcon;
244
+
245
+ return (
246
+ <div
247
+ key={item.id}
248
+ onClick={() => setSelectedItem(item)}
249
+ style={{
250
+ display: 'flex',
251
+ alignItems: 'center',
252
+ gap: theme.spacing.md,
253
+ padding: `${theme.spacing.md} ${theme.spacing.lg}`,
254
+ cursor: 'pointer',
255
+ backgroundColor: isSelected ? `${item.color}15` : 'transparent',
256
+ borderLeft: isSelected ? `3px solid ${item.color}` : '3px solid transparent',
257
+ transition: 'all 0.15s ease',
258
+ }}
259
+ onMouseEnter={(e) => {
260
+ if (!isSelected) {
261
+ e.currentTarget.style.backgroundColor = theme.colors.backgroundHover;
262
+ }
263
+ }}
264
+ onMouseLeave={(e) => {
265
+ if (!isSelected) {
266
+ e.currentTarget.style.backgroundColor = 'transparent';
267
+ }
268
+ }}
269
+ >
270
+ {/* Icon */}
271
+ <div style={{
272
+ width: 32,
273
+ height: 32,
274
+ borderRadius: theme.borderRadius.md,
275
+ backgroundColor: `${item.color}15`,
276
+ display: 'flex',
277
+ alignItems: 'center',
278
+ justifyContent: 'center',
279
+ flexShrink: 0,
280
+ }}>
281
+ {CustomIcon ? <CustomIcon /> : Icon ? <Icon size={18} /> : null}
282
+ </div>
283
+
284
+ {/* Name */}
285
+ <div style={{ flex: 1, minWidth: 0 }}>
286
+ <div style={{
287
+ fontSize: theme.fontSize.sm,
288
+ fontWeight: theme.fontWeight.medium,
289
+ color: theme.colors.text,
290
+ overflow: 'hidden',
291
+ textOverflow: 'ellipsis',
292
+ whiteSpace: 'nowrap',
293
+ }}>
294
+ {item.name}
295
+ </div>
296
+ </div>
297
+
298
+ {/* Status dot */}
299
+ {(isValid || specialStatus?.connected) && (
300
+ <div style={{
301
+ width: 8,
302
+ height: 8,
303
+ borderRadius: '50%',
304
+ backgroundColor: theme.dracula.green,
305
+ flexShrink: 0,
306
+ }} />
307
+ )}
308
+ </div>
309
+ );
310
+ };
311
+
312
+ // WhatsApp actions
313
+ const { getStatus: getWhatsAppStatus, startConnection, restartConnection } = useWhatsApp();
314
+ const { sendRequest, getWhatsAppRateLimitConfig, setWhatsAppRateLimitConfig, unpauseWhatsAppRateLimit } = useWebSocket();
315
+
316
+ // Android state
317
+ const [androidApiKey, setAndroidApiKey] = useState('');
318
+ const [androidApiKeyStored, setAndroidApiKeyStored] = useState<boolean | null>(null);
319
+ const [androidLoading, setAndroidLoading] = useState<string | null>(null);
320
+ const [androidError, setAndroidError] = useState<string | null>(null);
321
+ const [whatsappLoading, setWhatsappLoading] = useState<string | null>(null);
322
+ const [whatsappError, setWhatsappError] = useState<string | null>(null);
323
+
324
+ // Rate limit state
325
+ const [rateLimitConfig, setRateLimitConfig] = useState<RateLimitConfig | null>(null);
326
+ const [rateLimitStats, setRateLimitStats] = useState<RateLimitStats | null>(null);
327
+ const [rateLimitExpanded, setRateLimitExpanded] = useState(false);
328
+ const [rateLimitLoading, setRateLimitLoading] = useState(false);
329
+ const [rateLimitDirty, setRateLimitDirty] = useState(false);
330
+
331
+ // Load Android API key on mount
332
+ useEffect(() => {
333
+ if (visible) {
334
+ hasStoredKey('android_remote').then(async (has) => {
335
+ setAndroidApiKeyStored(has);
336
+ if (has) {
337
+ const key = await getStoredApiKey('android_remote');
338
+ if (key) setAndroidApiKey(key);
339
+ }
340
+ });
341
+ }
342
+ }, [visible, hasStoredKey, getStoredApiKey]);
343
+
344
+ // Android handlers
345
+ const handleAndroidSaveKey = async () => {
346
+ if (!androidApiKey.trim()) return;
347
+ setAndroidLoading('save');
348
+ try {
349
+ await saveApiKey('android_remote', androidApiKey);
350
+ setAndroidApiKeyStored(true);
351
+ } catch (err: any) {
352
+ setAndroidError(err.message || 'Failed to save API key');
353
+ } finally {
354
+ setAndroidLoading(null);
355
+ }
356
+ };
357
+
358
+ const handleAndroidConnect = async () => {
359
+ setAndroidLoading('connect');
360
+ setAndroidError(null);
361
+ try {
362
+ const key = await getStoredApiKey('android_remote');
363
+ if (!key) {
364
+ setAndroidError('No API key configured');
365
+ setAndroidLoading(null);
366
+ return;
367
+ }
368
+ const response = await sendRequest('android_relay_connect', {
369
+ url: import.meta.env.VITE_ANDROID_RELAY_URL || '',
370
+ api_key: key,
371
+ });
372
+ if (!response.success) {
373
+ setAndroidError(response.error || 'Failed to connect');
374
+ }
375
+ } catch (err: any) {
376
+ setAndroidError(err.message || 'Failed to connect');
377
+ } finally {
378
+ setAndroidLoading(null);
379
+ }
380
+ };
381
+
382
+ const handleAndroidDisconnect = async () => {
383
+ setAndroidLoading('disconnect');
384
+ setAndroidError(null);
385
+ try {
386
+ const response = await sendRequest('android_relay_disconnect', {});
387
+ if (!response.success) {
388
+ setAndroidError(response.error || 'Failed to disconnect');
389
+ }
390
+ } catch (err: any) {
391
+ setAndroidError(err.message || 'Failed to disconnect');
392
+ } finally {
393
+ setAndroidLoading(null);
394
+ }
395
+ };
396
+
397
+ // WhatsApp handlers
398
+ const handleWhatsAppStart = async () => {
399
+ setWhatsappLoading('start');
400
+ setWhatsappError(null);
401
+ try {
402
+ const result = await startConnection();
403
+ if (!result.success && result.message) {
404
+ setWhatsappError(result.message);
405
+ }
406
+ } catch (err: any) {
407
+ setWhatsappError(err.message || 'Failed to start');
408
+ } finally {
409
+ setWhatsappLoading(null);
410
+ }
411
+ };
412
+
413
+ const handleWhatsAppRestart = async () => {
414
+ setWhatsappLoading('restart');
415
+ setWhatsappError(null);
416
+ try {
417
+ const result = await restartConnection();
418
+ if (!result.success && result.message) {
419
+ setWhatsappError(result.message);
420
+ }
421
+ } catch (err: any) {
422
+ setWhatsappError(err.message || 'Failed to restart');
423
+ } finally {
424
+ setWhatsappLoading(null);
425
+ }
426
+ };
427
+
428
+ const handleWhatsAppRefresh = async () => {
429
+ setWhatsappLoading('refresh');
430
+ setWhatsappError(null);
431
+ try {
432
+ await getWhatsAppStatus();
433
+ } catch (err: any) {
434
+ setWhatsappError(err.message || 'Failed to refresh');
435
+ } finally {
436
+ setWhatsappLoading(null);
437
+ }
438
+ };
439
+
440
+ // Rate limit handlers
441
+ const loadRateLimits = useCallback(async () => {
442
+ setRateLimitLoading(true);
443
+ try {
444
+ const result = await getWhatsAppRateLimitConfig();
445
+ if (result.success) {
446
+ setRateLimitConfig(result.config || null);
447
+ setRateLimitStats(result.stats || null);
448
+ setRateLimitDirty(false);
449
+ }
450
+ } catch (err) {
451
+ console.error('Failed to load rate limits:', err);
452
+ } finally {
453
+ setRateLimitLoading(false);
454
+ }
455
+ }, [getWhatsAppRateLimitConfig]);
456
+
457
+ const handleRateLimitSave = async () => {
458
+ if (!rateLimitConfig) return;
459
+ setRateLimitLoading(true);
460
+ try {
461
+ const result = await setWhatsAppRateLimitConfig(rateLimitConfig);
462
+ if (result.success && result.config) {
463
+ setRateLimitConfig(result.config);
464
+ setRateLimitDirty(false);
465
+ }
466
+ } catch (err) {
467
+ console.error('Failed to save rate limits:', err);
468
+ } finally {
469
+ setRateLimitLoading(false);
470
+ }
471
+ };
472
+
473
+ const handleRateLimitUnpause = async () => {
474
+ setRateLimitLoading(true);
475
+ try {
476
+ const result = await unpauseWhatsAppRateLimit();
477
+ if (result.success && result.stats) {
478
+ setRateLimitStats(result.stats);
479
+ }
480
+ } catch (err) {
481
+ console.error('Failed to unpause rate limit:', err);
482
+ } finally {
483
+ setRateLimitLoading(false);
484
+ }
485
+ };
486
+
487
+ const updateRateLimitConfig = (key: keyof RateLimitConfig, value: any) => {
488
+ if (!rateLimitConfig) return;
489
+ setRateLimitConfig({ ...rateLimitConfig, [key]: value });
490
+ setRateLimitDirty(true);
491
+ };
492
+
493
+ // Load rate limits when WhatsApp panel is selected and connected
494
+ useEffect(() => {
495
+ if (selectedItem?.panelType === 'whatsapp' && whatsappStatus.connected && rateLimitExpanded) {
496
+ loadRateLimits();
497
+ }
498
+ }, [selectedItem, whatsappStatus.connected, rateLimitExpanded, loadRateLimits]);
499
+
500
+ // Render detail panel for selected item
501
+ const renderDetailPanel = () => {
502
+ if (!selectedItem) {
503
+ return (
504
+ <div style={{
505
+ display: 'flex',
506
+ alignItems: 'center',
507
+ justifyContent: 'center',
508
+ height: '100%',
509
+ color: theme.colors.textMuted,
510
+ fontSize: theme.fontSize.sm,
511
+ }}>
512
+ Select a credential to configure
513
+ </div>
514
+ );
515
+ }
516
+
517
+ // WhatsApp panel
518
+ if (selectedItem.panelType === 'whatsapp') {
519
+ return (
520
+ <div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
521
+ {/* Scrollable content area */}
522
+ <div style={{ flex: 1, overflow: 'auto', padding: theme.spacing.xl }}>
523
+ <Descriptions
524
+ title={<Space><WhatsAppIcon /> WhatsApp Personal</Space>}
525
+ bordered
526
+ column={1}
527
+ size="small"
528
+ style={{
529
+ marginBottom: theme.spacing.lg,
530
+ background: theme.colors.backgroundAlt,
531
+ borderRadius: theme.borderRadius.md,
532
+ }}
533
+ styles={{
534
+ label: {
535
+ backgroundColor: theme.colors.backgroundPanel,
536
+ color: theme.colors.textSecondary,
537
+ fontWeight: theme.fontWeight.medium,
538
+ },
539
+ content: {
540
+ backgroundColor: theme.colors.background,
541
+ color: theme.colors.text,
542
+ },
543
+ }}
544
+ >
545
+ <Descriptions.Item label="Status">
546
+ <Tag style={getTagStyle(whatsappStatus.connected ? 'success' : 'error')}>
547
+ {whatsappStatus.connected ? 'Connected' : 'Disconnected'}
548
+ </Tag>
549
+ </Descriptions.Item>
550
+ <Descriptions.Item label="Session">
551
+ <Tag style={getTagStyle(whatsappStatus.has_session ? 'success' : 'error')}>
552
+ {whatsappStatus.has_session ? 'Active' : 'Inactive'}
553
+ </Tag>
554
+ </Descriptions.Item>
555
+ <Descriptions.Item label="Service">
556
+ <Tag style={getTagStyle(whatsappStatus.running ? 'success' : 'error')}>
557
+ {whatsappStatus.running ? 'Running' : 'Stopped'}
558
+ </Tag>
559
+ </Descriptions.Item>
560
+ {whatsappStatus.device_id && (
561
+ <Descriptions.Item label="Device">
562
+ <span style={{ fontFamily: 'monospace', fontSize: theme.fontSize.xs }}>
563
+ {whatsappStatus.device_id.slice(0, 24)}...
564
+ </span>
565
+ </Descriptions.Item>
566
+ )}
567
+ </Descriptions>
568
+
569
+ {/* QR Code */}
570
+ <div style={{
571
+ display: 'flex',
572
+ flexDirection: 'column',
573
+ alignItems: 'center',
574
+ justifyContent: 'center',
575
+ backgroundColor: theme.colors.backgroundAlt,
576
+ borderRadius: theme.borderRadius.lg,
577
+ marginBottom: theme.spacing.lg,
578
+ padding: theme.spacing.xl,
579
+ }}>
580
+ <QRCodeDisplay
581
+ value={whatsappStatus.qr}
582
+ isConnected={whatsappStatus.connected}
583
+ size={200}
584
+ connectedTitle="Already Connected!"
585
+ connectedSubtitle="No QR code needed"
586
+ loading={whatsappStatus.running && !whatsappStatus.qr && !whatsappStatus.connected}
587
+ emptyText={whatsappStatus.running ? 'Waiting for QR code...' : 'Start service to get QR code'}
588
+ />
589
+ {!whatsappStatus.connected && whatsappStatus.qr && (
590
+ <div style={{ marginTop: theme.spacing.md, color: theme.colors.textSecondary, fontSize: theme.fontSize.sm }}>
591
+ Scan with WhatsApp mobile app
592
+ </div>
593
+ )}
594
+ </div>
595
+
596
+ {/* Rate Limits Section */}
597
+ {whatsappStatus.connected && (
598
+ <div style={{
599
+ marginBottom: theme.spacing.lg,
600
+ border: `1px solid ${theme.colors.border}`,
601
+ borderRadius: theme.borderRadius.md,
602
+ overflow: 'hidden'
603
+ }}>
604
+ <div
605
+ onClick={() => setRateLimitExpanded(!rateLimitExpanded)}
606
+ style={{
607
+ display: 'flex',
608
+ alignItems: 'center',
609
+ justifyContent: 'space-between',
610
+ padding: theme.spacing.sm,
611
+ backgroundColor: theme.colors.backgroundAlt,
612
+ cursor: 'pointer',
613
+ }}
614
+ >
615
+ <span style={{ fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.medium, color: theme.colors.text }}>
616
+ {rateLimitExpanded ? '▼' : '▶'} Rate Limits
617
+ </span>
618
+ <span onClick={(e) => e.stopPropagation()}>
619
+ <Switch
620
+ size="small"
621
+ checked={rateLimitConfig?.enabled ?? true}
622
+ onChange={(checked) => updateRateLimitConfig('enabled', checked)}
623
+ />
624
+ </span>
625
+ </div>
626
+ {rateLimitExpanded && (
627
+ <div style={{ padding: theme.spacing.md }}>
628
+ {rateLimitLoading && !rateLimitConfig ? (
629
+ <div style={{ textAlign: 'center', color: theme.colors.textSecondary, fontSize: theme.fontSize.sm }}>Loading...</div>
630
+ ) : rateLimitConfig ? (
631
+ <>
632
+ {/* Stats Section */}
633
+ {rateLimitStats && (
634
+ <div style={{
635
+ display: 'grid',
636
+ gridTemplateColumns: 'repeat(3, 1fr)',
637
+ gap: theme.spacing.sm,
638
+ marginBottom: theme.spacing.md,
639
+ padding: theme.spacing.sm,
640
+ backgroundColor: theme.colors.backgroundPanel,
641
+ borderRadius: theme.borderRadius.sm
642
+ }}>
643
+ <div style={{ textAlign: 'center' }}>
644
+ <div style={{ fontSize: theme.fontSize.xs, color: theme.colors.textSecondary }}>Last Minute</div>
645
+ <div style={{ fontSize: theme.fontSize.lg, color: theme.colors.text, fontWeight: theme.fontWeight.semibold }}>{rateLimitStats.messages_sent_last_minute}</div>
646
+ </div>
647
+ <div style={{ textAlign: 'center' }}>
648
+ <div style={{ fontSize: theme.fontSize.xs, color: theme.colors.textSecondary }}>Last Hour</div>
649
+ <div style={{ fontSize: theme.fontSize.lg, color: theme.colors.text, fontWeight: theme.fontWeight.semibold }}>{rateLimitStats.messages_sent_last_hour}</div>
650
+ </div>
651
+ <div style={{ textAlign: 'center' }}>
652
+ <div style={{ fontSize: theme.fontSize.xs, color: theme.colors.textSecondary }}>Today</div>
653
+ <div style={{ fontSize: theme.fontSize.lg, color: theme.colors.text, fontWeight: theme.fontWeight.semibold }}>{rateLimitStats.messages_sent_today}</div>
654
+ </div>
655
+ <div style={{ textAlign: 'center' }}>
656
+ <div style={{ fontSize: theme.fontSize.xs, color: theme.colors.textSecondary }}>New Contacts</div>
657
+ <div style={{ fontSize: theme.fontSize.lg, color: theme.colors.text, fontWeight: theme.fontWeight.semibold }}>{rateLimitStats.new_contacts_today}</div>
658
+ </div>
659
+ <div style={{ textAlign: 'center' }}>
660
+ <div style={{ fontSize: theme.fontSize.xs, color: theme.colors.textSecondary }}>Responses</div>
661
+ <div style={{ fontSize: theme.fontSize.lg, color: theme.colors.text, fontWeight: theme.fontWeight.semibold }}>{rateLimitStats.responses_received}</div>
662
+ </div>
663
+ <div style={{ textAlign: 'center' }}>
664
+ <div style={{ fontSize: theme.fontSize.xs, color: theme.colors.textSecondary }}>Response Rate</div>
665
+ <div style={{ fontSize: theme.fontSize.lg, color: theme.colors.text, fontWeight: theme.fontWeight.semibold }}>{Math.round((rateLimitStats.response_rate || 0) * 100)}%</div>
666
+ </div>
667
+ </div>
668
+ )}
669
+ {rateLimitStats?.is_paused && (
670
+ <Alert type="warning" message={rateLimitStats.pause_reason || 'Paused'} action={<Button size="small" onClick={handleRateLimitUnpause}>Unpause</Button>} style={{ marginBottom: theme.spacing.md }} />
671
+ )}
672
+
673
+ {/* Delays Section */}
674
+ <div style={{ fontSize: theme.fontSize.xs, color: theme.colors.textSecondary, marginBottom: theme.spacing.xs, fontWeight: theme.fontWeight.medium }}>
675
+ Message Delays (milliseconds)
676
+ </div>
677
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: theme.spacing.sm, marginBottom: theme.spacing.md }}>
678
+ <div>
679
+ <div style={{ fontSize: theme.fontSize.xs, color: theme.colors.textSecondary, marginBottom: theme.spacing.xs }}>Min Delay</div>
680
+ <InputNumber size="small" value={rateLimitConfig.min_delay_ms} onChange={(v) => updateRateLimitConfig('min_delay_ms', v ?? 3000)} style={{ width: '100%' }} />
681
+ </div>
682
+ <div>
683
+ <div style={{ fontSize: theme.fontSize.xs, color: theme.colors.textSecondary, marginBottom: theme.spacing.xs }}>Max Delay</div>
684
+ <InputNumber size="small" value={rateLimitConfig.max_delay_ms} onChange={(v) => updateRateLimitConfig('max_delay_ms', v ?? 8000)} style={{ width: '100%' }} />
685
+ </div>
686
+ <div>
687
+ <div style={{ fontSize: theme.fontSize.xs, color: theme.colors.textSecondary, marginBottom: theme.spacing.xs }}>Typing Duration</div>
688
+ <InputNumber size="small" value={rateLimitConfig.typing_delay_ms} onChange={(v) => updateRateLimitConfig('typing_delay_ms', v ?? 2000)} style={{ width: '100%' }} />
689
+ </div>
690
+ <div>
691
+ <div style={{ fontSize: theme.fontSize.xs, color: theme.colors.textSecondary, marginBottom: theme.spacing.xs }}>Link Extra Delay</div>
692
+ <InputNumber size="small" value={rateLimitConfig.link_extra_delay_ms} onChange={(v) => updateRateLimitConfig('link_extra_delay_ms', v ?? 5000)} style={{ width: '100%' }} />
693
+ </div>
694
+ </div>
695
+
696
+ {/* Limits Section */}
697
+ <div style={{ fontSize: theme.fontSize.xs, color: theme.colors.textSecondary, marginBottom: theme.spacing.xs, fontWeight: theme.fontWeight.medium }}>
698
+ Message Limits
699
+ </div>
700
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: theme.spacing.sm, marginBottom: theme.spacing.md }}>
701
+ <div>
702
+ <div style={{ fontSize: theme.fontSize.xs, color: theme.colors.textSecondary, marginBottom: theme.spacing.xs }}>Per Minute</div>
703
+ <InputNumber size="small" value={rateLimitConfig.max_messages_per_minute} onChange={(v) => updateRateLimitConfig('max_messages_per_minute', v ?? 10)} style={{ width: '100%' }} />
704
+ </div>
705
+ <div>
706
+ <div style={{ fontSize: theme.fontSize.xs, color: theme.colors.textSecondary, marginBottom: theme.spacing.xs }}>Per Hour</div>
707
+ <InputNumber size="small" value={rateLimitConfig.max_messages_per_hour} onChange={(v) => updateRateLimitConfig('max_messages_per_hour', v ?? 60)} style={{ width: '100%' }} />
708
+ </div>
709
+ <div>
710
+ <div style={{ fontSize: theme.fontSize.xs, color: theme.colors.textSecondary, marginBottom: theme.spacing.xs }}>New Contacts/Day</div>
711
+ <InputNumber size="small" value={rateLimitConfig.max_new_contacts_per_day} onChange={(v) => updateRateLimitConfig('max_new_contacts_per_day', v ?? 20)} style={{ width: '100%' }} />
712
+ </div>
713
+ </div>
714
+
715
+ {/* Behavior Section */}
716
+ <div style={{ fontSize: theme.fontSize.xs, color: theme.colors.textSecondary, marginBottom: theme.spacing.xs, fontWeight: theme.fontWeight.medium }}>
717
+ Behavior
718
+ </div>
719
+ <div style={{ display: 'flex', flexDirection: 'column', gap: theme.spacing.sm, marginBottom: theme.spacing.md }}>
720
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
721
+ <div>
722
+ <div style={{ fontSize: theme.fontSize.sm, color: theme.colors.text }}>Simulate Typing</div>
723
+ <div style={{ fontSize: theme.fontSize.xs, color: theme.colors.textSecondary }}>Show typing indicator before sending</div>
724
+ </div>
725
+ <Switch size="small" checked={rateLimitConfig.simulate_typing} onChange={(v) => updateRateLimitConfig('simulate_typing', v)} />
726
+ </div>
727
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
728
+ <div>
729
+ <div style={{ fontSize: theme.fontSize.sm, color: theme.colors.text }}>Randomize Delays</div>
730
+ <div style={{ fontSize: theme.fontSize.xs, color: theme.colors.textSecondary }}>Add variance between min/max delay</div>
731
+ </div>
732
+ <Switch size="small" checked={rateLimitConfig.randomize_delays} onChange={(v) => updateRateLimitConfig('randomize_delays', v)} />
733
+ </div>
734
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
735
+ <div>
736
+ <div style={{ fontSize: theme.fontSize.sm, color: theme.colors.text }}>Pause on Low Response</div>
737
+ <div style={{ fontSize: theme.fontSize.xs, color: theme.colors.textSecondary }}>Auto-pause if response rate drops below threshold</div>
738
+ </div>
739
+ <Switch size="small" checked={rateLimitConfig.pause_on_low_response} onChange={(v) => updateRateLimitConfig('pause_on_low_response', v)} />
740
+ </div>
741
+ {rateLimitConfig.pause_on_low_response && (
742
+ <div>
743
+ <div style={{ fontSize: theme.fontSize.xs, color: theme.colors.textSecondary, marginBottom: theme.spacing.xs }}>Response Rate Threshold (%)</div>
744
+ <InputNumber
745
+ size="small"
746
+ value={Math.round((rateLimitConfig.response_rate_threshold || 0.3) * 100)}
747
+ onChange={(v) => updateRateLimitConfig('response_rate_threshold', (v ?? 30) / 100)}
748
+ min={0}
749
+ max={100}
750
+ style={{ width: '100%' }}
751
+ />
752
+ </div>
753
+ )}
754
+ </div>
755
+
756
+ <Button
757
+ size="small"
758
+ onClick={handleRateLimitSave}
759
+ loading={rateLimitLoading}
760
+ disabled={!rateLimitDirty}
761
+ block
762
+ style={{
763
+ backgroundColor: rateLimitDirty ? `${theme.dracula.green}25` : undefined,
764
+ borderColor: rateLimitDirty ? `${theme.dracula.green}60` : undefined,
765
+ color: rateLimitDirty ? theme.dracula.green : undefined,
766
+ }}
767
+ >
768
+ Save Changes
769
+ </Button>
770
+ </>
771
+ ) : (
772
+ <div style={{ textAlign: 'center', color: theme.colors.textSecondary, fontSize: theme.fontSize.sm }}>Failed to load</div>
773
+ )}
774
+ </div>
775
+ )}
776
+ </div>
777
+ )}
778
+
779
+ {whatsappError && (
780
+ <Alert type="error" message={whatsappError} showIcon style={{ marginBottom: theme.spacing.lg }} />
781
+ )}
782
+ </div>
783
+
784
+ {/* Actions - Fixed at bottom */}
785
+ <div style={{
786
+ display: 'flex',
787
+ gap: theme.spacing.sm,
788
+ justifyContent: 'center',
789
+ padding: theme.spacing.md,
790
+ borderTop: `1px solid ${theme.colors.border}`,
791
+ backgroundColor: theme.colors.background,
792
+ flexShrink: 0,
793
+ }}>
794
+ <Button
795
+ onClick={handleWhatsAppStart}
796
+ loading={whatsappLoading === 'start'}
797
+ style={{
798
+ backgroundColor: `${theme.dracula.green}25`,
799
+ borderColor: `${theme.dracula.green}60`,
800
+ color: theme.dracula.green,
801
+ }}
802
+ >
803
+ Start
804
+ </Button>
805
+ <Button
806
+ onClick={handleWhatsAppRestart}
807
+ loading={whatsappLoading === 'restart'}
808
+ style={{
809
+ backgroundColor: `${theme.dracula.orange}25`,
810
+ borderColor: `${theme.dracula.orange}60`,
811
+ color: theme.dracula.orange,
812
+ }}
813
+ >
814
+ Restart
815
+ </Button>
816
+ <Button
817
+ onClick={handleWhatsAppRefresh}
818
+ loading={whatsappLoading === 'refresh'}
819
+ style={{
820
+ backgroundColor: `${theme.dracula.cyan}25`,
821
+ borderColor: `${theme.dracula.cyan}60`,
822
+ color: theme.dracula.cyan,
823
+ }}
824
+ >
825
+ Refresh
826
+ </Button>
827
+ </div>
828
+ </div>
829
+ );
830
+ }
831
+
832
+ // Android panel
833
+ if (selectedItem.panelType === 'android') {
834
+ return (
835
+ <div style={{ padding: theme.spacing.xl, display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
836
+ {/* API Key Input */}
837
+ <div style={{ marginBottom: theme.spacing.lg }}>
838
+ <ApiKeyInput
839
+ value={androidApiKey}
840
+ onChange={setAndroidApiKey}
841
+ onSave={handleAndroidSaveKey}
842
+ onDelete={async () => {
843
+ await removeApiKey('android_remote');
844
+ setAndroidApiKey('');
845
+ setAndroidApiKeyStored(false);
846
+ }}
847
+ placeholder="Enter Android Relay API key..."
848
+ loading={androidLoading === 'save'}
849
+ isStored={androidApiKeyStored}
850
+ />
851
+ </div>
852
+
853
+ <Descriptions
854
+ title={<Space><AndroidIcon /> Android Device</Space>}
855
+ bordered
856
+ column={1}
857
+ size="small"
858
+ style={{
859
+ marginBottom: theme.spacing.xl,
860
+ background: theme.colors.backgroundAlt,
861
+ borderRadius: theme.borderRadius.md,
862
+ }}
863
+ styles={{
864
+ label: {
865
+ backgroundColor: theme.colors.backgroundPanel,
866
+ color: theme.colors.textSecondary,
867
+ fontWeight: theme.fontWeight.medium,
868
+ },
869
+ content: {
870
+ backgroundColor: theme.colors.background,
871
+ color: theme.colors.text,
872
+ },
873
+ }}
874
+ >
875
+ <Descriptions.Item label="API Key">
876
+ <Tag style={getTagStyle((androidApiKeyStored || androidStatus.connected) ? 'success' : 'error')}>
877
+ {androidApiKeyStored === null ? 'Checking...' : (androidApiKeyStored || androidStatus.connected) ? 'Configured' : 'Not configured'}
878
+ </Tag>
879
+ </Descriptions.Item>
880
+ <Descriptions.Item label="Relay">
881
+ <Tag style={getTagStyle(androidStatus.connected ? 'success' : 'error')}>
882
+ {androidStatus.connected ? 'Connected' : 'Disconnected'}
883
+ </Tag>
884
+ </Descriptions.Item>
885
+ <Descriptions.Item label="Device">
886
+ <Tag style={getTagStyle(androidStatus.paired ? 'success' : (androidStatus.connected ? 'warning' : 'error'))}>
887
+ {androidStatus.paired ? 'Paired' : (androidStatus.connected ? 'Waiting for pairing' : 'Not connected')}
888
+ </Tag>
889
+ </Descriptions.Item>
890
+ {androidStatus.device_name && (
891
+ <Descriptions.Item label="Device Name">
892
+ {androidStatus.device_name}
893
+ </Descriptions.Item>
894
+ )}
895
+ </Descriptions>
896
+
897
+ {/* QR Code */}
898
+ <div style={{
899
+ flex: 1,
900
+ display: 'flex',
901
+ flexDirection: 'column',
902
+ alignItems: 'center',
903
+ justifyContent: 'center',
904
+ backgroundColor: theme.colors.backgroundAlt,
905
+ borderRadius: theme.borderRadius.lg,
906
+ marginBottom: theme.spacing.xl,
907
+ minHeight: 200,
908
+ }}>
909
+ <QRCodeDisplay
910
+ value={androidStatus.qr_data}
911
+ isConnected={androidStatus.paired}
912
+ size={260}
913
+ connectedTitle="Device Paired!"
914
+ connectedSubtitle={androidStatus.device_name || androidStatus.device_id || 'Android Device'}
915
+ loading={androidStatus.connected && !androidStatus.qr_data && !androidStatus.paired}
916
+ emptyText={androidApiKeyStored ? (androidStatus.connected ? 'Waiting for QR code...' : 'Click Connect to start') : 'Add API key first'}
917
+ />
918
+ {!androidStatus.paired && androidStatus.qr_data && (
919
+ <div style={{ marginTop: theme.spacing.md, color: theme.colors.textSecondary }}>
920
+ Scan with Android companion app
921
+ </div>
922
+ )}
923
+ </div>
924
+
925
+ {!androidApiKeyStored && !androidStatus.connected && (
926
+ <Alert
927
+ type="warning"
928
+ message="API key required"
929
+ description="Configure your Android Relay API key above before connecting."
930
+ showIcon
931
+ style={{ marginBottom: theme.spacing.lg }}
932
+ />
933
+ )}
934
+
935
+ {androidError && (
936
+ <Alert type="error" message={androidError} showIcon style={{ marginBottom: theme.spacing.lg }} />
937
+ )}
938
+
939
+ {/* Actions - Fixed at bottom */}
940
+ <div style={{
941
+ display: 'flex',
942
+ gap: theme.spacing.sm,
943
+ justifyContent: 'center',
944
+ paddingTop: theme.spacing.md,
945
+ borderTop: `1px solid ${theme.colors.border}`,
946
+ }}>
947
+ <Button
948
+ onClick={handleAndroidConnect}
949
+ loading={androidLoading === 'connect'}
950
+ disabled={androidStatus.connected || !androidApiKeyStored}
951
+ style={{
952
+ backgroundColor: `${theme.dracula.green}25`,
953
+ borderColor: `${theme.dracula.green}60`,
954
+ color: theme.dracula.green,
955
+ }}
956
+ >
957
+ Connect
958
+ </Button>
959
+ <Button
960
+ onClick={async () => {
961
+ setAndroidLoading('reconnect');
962
+ setAndroidError(null);
963
+ try {
964
+ const key = await getStoredApiKey('android_remote');
965
+ if (!key) {
966
+ setAndroidError('No API key configured');
967
+ setAndroidLoading(null);
968
+ return;
969
+ }
970
+ const response = await sendRequest('android_relay_reconnect', {
971
+ url: import.meta.env.VITE_ANDROID_RELAY_URL || '',
972
+ api_key: key,
973
+ });
974
+ if (!response.success) {
975
+ setAndroidError(response.error || 'Failed to reconnect');
976
+ }
977
+ } catch (err: any) {
978
+ setAndroidError(err.message || 'Failed to reconnect');
979
+ } finally {
980
+ setAndroidLoading(null);
981
+ }
982
+ }}
983
+ loading={androidLoading === 'reconnect'}
984
+ disabled={!androidStatus.connected || !androidApiKeyStored}
985
+ style={{
986
+ backgroundColor: `${theme.dracula.orange}25`,
987
+ borderColor: `${theme.dracula.orange}60`,
988
+ color: theme.dracula.orange,
989
+ }}
990
+ >
991
+ Reconnect
992
+ </Button>
993
+ <Button
994
+ onClick={handleAndroidDisconnect}
995
+ loading={androidLoading === 'disconnect'}
996
+ disabled={!androidStatus.connected}
997
+ style={{
998
+ backgroundColor: `${theme.dracula.pink}25`,
999
+ borderColor: `${theme.dracula.pink}60`,
1000
+ color: theme.dracula.pink,
1001
+ }}
1002
+ >
1003
+ Disconnect
1004
+ </Button>
1005
+ </div>
1006
+
1007
+ {androidStatus.connected && (
1008
+ <div style={{ marginTop: theme.spacing.sm, fontSize: theme.fontSize.xs, color: theme.colors.textMuted, textAlign: 'center' }}>
1009
+ Use Reconnect to get a new QR code if pairing fails
1010
+ </div>
1011
+ )}
1012
+ </div>
1013
+ );
1014
+ }
1015
+
1016
+ const item = selectedItem;
1017
+ const isValid = validKeys[item.id];
1018
+ const Icon = item.Icon;
1019
+ const CustomIcon = item.CustomIcon;
1020
+
1021
+ return (
1022
+ <div style={{ padding: theme.spacing.xl }}>
1023
+ {/* Header */}
1024
+ <div style={{
1025
+ display: 'flex',
1026
+ alignItems: 'center',
1027
+ gap: theme.spacing.lg,
1028
+ marginBottom: theme.spacing.xl,
1029
+ paddingBottom: theme.spacing.lg,
1030
+ borderBottom: `1px solid ${theme.colors.border}`,
1031
+ }}>
1032
+ <div style={{
1033
+ width: 48,
1034
+ height: 48,
1035
+ borderRadius: theme.borderRadius.lg,
1036
+ backgroundColor: `${item.color}15`,
1037
+ display: 'flex',
1038
+ alignItems: 'center',
1039
+ justifyContent: 'center',
1040
+ }}>
1041
+ {CustomIcon ? <CustomIcon /> : Icon ? <Icon size={24} /> : null}
1042
+ </div>
1043
+ <div>
1044
+ <div style={{
1045
+ fontSize: theme.fontSize.lg,
1046
+ fontWeight: theme.fontWeight.semibold,
1047
+ color: theme.colors.text,
1048
+ marginBottom: 4,
1049
+ }}>
1050
+ {item.name}
1051
+ </div>
1052
+ <div style={{
1053
+ fontSize: theme.fontSize.sm,
1054
+ color: theme.colors.textSecondary,
1055
+ }}>
1056
+ {item.desc}
1057
+ </div>
1058
+ </div>
1059
+ {isValid && (
1060
+ <Tag
1061
+ icon={<CheckCircleOutlined />}
1062
+ style={{
1063
+ marginLeft: 'auto',
1064
+ backgroundColor: `${theme.dracula.green}25`,
1065
+ borderColor: `${theme.dracula.green}60`,
1066
+ color: theme.dracula.green,
1067
+ }}
1068
+ >
1069
+ Connected
1070
+ </Tag>
1071
+ )}
1072
+ </div>
1073
+
1074
+ {/* API Key Input */}
1075
+ <div style={{ marginBottom: theme.spacing.xl }}>
1076
+ <label style={{
1077
+ display: 'block',
1078
+ fontSize: theme.fontSize.sm,
1079
+ fontWeight: theme.fontWeight.medium,
1080
+ color: theme.colors.text,
1081
+ marginBottom: theme.spacing.sm,
1082
+ }}>
1083
+ API Key
1084
+ </label>
1085
+ <ApiKeyInput
1086
+ value={keys[item.id] || ''}
1087
+ onChange={(value) => {
1088
+ setKeys(k => ({ ...k, [item.id]: value }));
1089
+ setValidKeys(v => ({ ...v, [item.id]: false }));
1090
+ }}
1091
+ onSave={() => handleValidate(item.id)}
1092
+ onDelete={isValid ? () => handleRemove(item.id) : undefined}
1093
+ placeholder={item.placeholder}
1094
+ loading={loading[item.id]}
1095
+ isStored={isValid}
1096
+ />
1097
+ </div>
1098
+
1099
+ {/* Models list */}
1100
+ {models[item.id]?.length > 0 && (
1101
+ <div style={{ marginBottom: theme.spacing.xl }}>
1102
+ <label style={{
1103
+ display: 'block',
1104
+ fontSize: theme.fontSize.sm,
1105
+ fontWeight: theme.fontWeight.medium,
1106
+ color: theme.colors.text,
1107
+ marginBottom: theme.spacing.sm,
1108
+ }}>
1109
+ Available Models
1110
+ </label>
1111
+ <div style={{
1112
+ fontSize: theme.fontSize.sm,
1113
+ color: theme.colors.textSecondary,
1114
+ padding: theme.spacing.md,
1115
+ backgroundColor: theme.colors.backgroundAlt,
1116
+ borderRadius: theme.borderRadius.md,
1117
+ maxHeight: 150,
1118
+ overflow: 'auto',
1119
+ }}>
1120
+ {models[item.id].map((model, idx) => (
1121
+ <div key={idx} style={{ padding: '4px 0' }}>{model}</div>
1122
+ ))}
1123
+ </div>
1124
+ </div>
1125
+ )}
1126
+
1127
+ {/* Info box */}
1128
+ <div style={{
1129
+ marginTop: theme.spacing.xl,
1130
+ padding: theme.spacing.md,
1131
+ borderRadius: theme.borderRadius.md,
1132
+ backgroundColor: `${theme.dracula.cyan}10`,
1133
+ border: `1px solid ${theme.dracula.cyan}30`,
1134
+ }}>
1135
+ <div style={{
1136
+ fontSize: theme.fontSize.sm,
1137
+ color: theme.colors.textSecondary,
1138
+ lineHeight: 1.5,
1139
+ }}>
1140
+ Your API key is stored securely and will be automatically injected when using {item.name} nodes in your workflows.
1141
+ </div>
1142
+ </div>
1143
+ </div>
1144
+ );
1145
+ };
1146
+
1147
+ return (
1148
+ <Modal isOpen={visible} onClose={onClose} maxWidth="95vw" maxHeight="95vh" headerActions={headerActions}>
1149
+ <div style={{
1150
+ display: 'flex',
1151
+ height: '100%',
1152
+ overflow: 'hidden',
1153
+ }}>
1154
+ {/* Left sidebar */}
1155
+ <div style={{
1156
+ width: 280,
1157
+ borderRight: `1px solid ${theme.colors.border}`,
1158
+ overflow: 'auto',
1159
+ flexShrink: 0,
1160
+ }}>
1161
+ {CATEGORIES.map(category => (
1162
+ <div key={category.key}>
1163
+ {/* Category header */}
1164
+ <div style={{
1165
+ padding: `${theme.spacing.md} ${theme.spacing.lg}`,
1166
+ fontSize: theme.fontSize.xs,
1167
+ fontWeight: theme.fontWeight.semibold,
1168
+ color: theme.colors.textMuted,
1169
+ textTransform: 'uppercase',
1170
+ letterSpacing: '0.05em',
1171
+ backgroundColor: theme.colors.backgroundPanel,
1172
+ borderBottom: `1px solid ${theme.colors.border}`,
1173
+ position: 'sticky',
1174
+ top: 0,
1175
+ zIndex: 1,
1176
+ }}>
1177
+ {category.label}
1178
+ </div>
1179
+ {/* Category items */}
1180
+ {category.items.map(renderSidebarItem)}
1181
+ </div>
1182
+ ))}
1183
+ </div>
1184
+
1185
+ {/* Right detail panel */}
1186
+ <div style={{
1187
+ flex: 1,
1188
+ overflow: 'auto',
1189
+ backgroundColor: theme.colors.background,
1190
+ display: 'flex',
1191
+ flexDirection: 'column',
1192
+ }}>
1193
+ {renderDetailPanel()}
1194
+ </div>
1195
+ </div>
1196
+ </Modal>
1197
+ );
1198
+ };
1199
+
1200
+ export default CredentialsModal;