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,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;