@wopr-network/platform-ui-core 1.0.0

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 (543) hide show
  1. package/.env.paperclip +18 -0
  2. package/.env.wopr +18 -0
  3. package/README.md +36 -0
  4. package/biome.json +52 -0
  5. package/next.config.ts +45 -0
  6. package/package.json +84 -0
  7. package/postcss.config.mjs +7 -0
  8. package/public/file.svg +1 -0
  9. package/public/globe.svg +1 -0
  10. package/public/window.svg +1 -0
  11. package/src/__tests__/__snapshots__/layout-snapshots.test.tsx.snap +741 -0
  12. package/src/__tests__/account-page-redirect.test.tsx +73 -0
  13. package/src/__tests__/account-switcher.test.tsx +85 -0
  14. package/src/__tests__/activity-page.test.tsx +176 -0
  15. package/src/__tests__/add-payment-method-dialog.test.tsx +160 -0
  16. package/src/__tests__/admin-api.test.ts +244 -0
  17. package/src/__tests__/admin-gpu-api.test.ts +188 -0
  18. package/src/__tests__/admin-guard.test.tsx +79 -0
  19. package/src/__tests__/admin-marketplace-api.test.ts +179 -0
  20. package/src/__tests__/admin-middleware.test.ts +157 -0
  21. package/src/__tests__/admin-tenant-table.test.tsx +95 -0
  22. package/src/__tests__/affiliate-dashboard.test.tsx +178 -0
  23. package/src/__tests__/api-401-redirect.test.ts +78 -0
  24. package/src/__tests__/api-client.test.ts +316 -0
  25. package/src/__tests__/api-config.test.ts +89 -0
  26. package/src/__tests__/api-control-instance.test.ts +69 -0
  27. package/src/__tests__/api-fleet-resources.test.ts +52 -0
  28. package/src/__tests__/api-fleet-trpc.test.ts +252 -0
  29. package/src/__tests__/api-get-instance-config.test.ts +41 -0
  30. package/src/__tests__/api-null-guards.test.ts +244 -0
  31. package/src/__tests__/api-rename-instance.test.ts +60 -0
  32. package/src/__tests__/api-update-instance-config.test.ts +60 -0
  33. package/src/__tests__/audit-log-table-pagination.test.tsx +136 -0
  34. package/src/__tests__/auth-client.test.ts +87 -0
  35. package/src/__tests__/auth-password-reset.test.tsx +435 -0
  36. package/src/__tests__/auth-redirect.test.tsx +60 -0
  37. package/src/__tests__/auth.test.tsx +269 -0
  38. package/src/__tests__/auto-topup-card.test.tsx +257 -0
  39. package/src/__tests__/backups-tab.test.tsx +221 -0
  40. package/src/__tests__/billing-byok-callout.test.tsx +76 -0
  41. package/src/__tests__/billing-layout-nav-hidden.test.tsx +47 -0
  42. package/src/__tests__/billing-payment-org-invoices.test.tsx +123 -0
  43. package/src/__tests__/billing.test.tsx +509 -0
  44. package/src/__tests__/bot-settings/resources-tab.test.tsx +119 -0
  45. package/src/__tests__/bot-settings/storage-tab.test.tsx +80 -0
  46. package/src/__tests__/bot-settings/vps-info-panel.test.tsx +108 -0
  47. package/src/__tests__/bot-settings/vps-upgrade-card.test.tsx +52 -0
  48. package/src/__tests__/bot-settings-data-control.test.ts +49 -0
  49. package/src/__tests__/bot-settings-restart.test.tsx +149 -0
  50. package/src/__tests__/bot-settings.test.tsx +678 -0
  51. package/src/__tests__/brand.test.ts +335 -0
  52. package/src/__tests__/buy-credits-panel.test.tsx +249 -0
  53. package/src/__tests__/buy-crypto-credits-panel.test.tsx +178 -0
  54. package/src/__tests__/capability-conflicts.test.ts +88 -0
  55. package/src/__tests__/capability-resolver.test.tsx +173 -0
  56. package/src/__tests__/changeset-detail.test.tsx +156 -0
  57. package/src/__tests__/channel-setup-logger.test.ts +22 -0
  58. package/src/__tests__/channel-setup-toast.test.tsx +60 -0
  59. package/src/__tests__/channel-wizard.test.tsx +505 -0
  60. package/src/__tests__/chat/ambient-dot.test.tsx +35 -0
  61. package/src/__tests__/chat/chat-input.test.tsx +78 -0
  62. package/src/__tests__/chat/chat-message.test.tsx +45 -0
  63. package/src/__tests__/chat/chat-panel.test.tsx +111 -0
  64. package/src/__tests__/chat/chat-widget.test.tsx +82 -0
  65. package/src/__tests__/chat-store.test.ts +87 -0
  66. package/src/__tests__/command-center.test.tsx +246 -0
  67. package/src/__tests__/compliance-retention-edit.test.tsx +134 -0
  68. package/src/__tests__/coupon-input.test.tsx +119 -0
  69. package/src/__tests__/create-instance.test.tsx +96 -0
  70. package/src/__tests__/create-org-wizard.test.tsx +200 -0
  71. package/src/__tests__/credit-balance.test.tsx +103 -0
  72. package/src/__tests__/credits.test.tsx +376 -0
  73. package/src/__tests__/csrf-middleware.test.ts +198 -0
  74. package/src/__tests__/degraded-state-banner.test.tsx +130 -0
  75. package/src/__tests__/dividend-calculator.test.tsx +20 -0
  76. package/src/__tests__/dividend-stats.test.tsx +64 -0
  77. package/src/__tests__/dividend.test.tsx +169 -0
  78. package/src/__tests__/dockerfile.test.ts +110 -0
  79. package/src/__tests__/email-verification-banner.test.tsx +64 -0
  80. package/src/__tests__/env-example.test.ts +25 -0
  81. package/src/__tests__/error-boundaries.test.tsx +64 -0
  82. package/src/__tests__/fetch-pricing.test.ts +121 -0
  83. package/src/__tests__/field-oauth.test.tsx +302 -0
  84. package/src/__tests__/fixtures/mock-manifests-data.js +372 -0
  85. package/src/__tests__/fixtures/mock-manifests.ts +24 -0
  86. package/src/__tests__/fleet-health-timestamp.test.tsx +101 -0
  87. package/src/__tests__/fleet-health-update.test.tsx +83 -0
  88. package/src/__tests__/format-credit.test.ts +58 -0
  89. package/src/__tests__/gpu-dashboard.test.tsx +236 -0
  90. package/src/__tests__/hosted-usage-date-range.test.tsx +54 -0
  91. package/src/__tests__/instance-detail.test.tsx +571 -0
  92. package/src/__tests__/instance-list.test.tsx +230 -0
  93. package/src/__tests__/landing-hero.test.tsx +27 -0
  94. package/src/__tests__/landing-nav.test.tsx +24 -0
  95. package/src/__tests__/layout-snapshots.test.tsx +167 -0
  96. package/src/__tests__/logger.test.ts +54 -0
  97. package/src/__tests__/login-page-redirect.test.tsx +142 -0
  98. package/src/__tests__/manifest-validation.test.ts +126 -0
  99. package/src/__tests__/marketplace-admin.test.tsx +151 -0
  100. package/src/__tests__/marketplace.test.tsx +609 -0
  101. package/src/__tests__/merge-api-rates.test.ts +70 -0
  102. package/src/__tests__/middleware.test.ts +690 -0
  103. package/src/__tests__/network-page.test.tsx +100 -0
  104. package/src/__tests__/next-config-headers.test.ts +28 -0
  105. package/src/__tests__/not-found.test.tsx +26 -0
  106. package/src/__tests__/notifications.test.tsx +128 -0
  107. package/src/__tests__/oauth-buttons.test.tsx +101 -0
  108. package/src/__tests__/oauth-error-mapping.test.tsx +97 -0
  109. package/src/__tests__/observability.test.tsx +541 -0
  110. package/src/__tests__/onboarding-data.test.ts +363 -0
  111. package/src/__tests__/onboarding-page.test.tsx +113 -0
  112. package/src/__tests__/onboarding-store.test.ts +121 -0
  113. package/src/__tests__/org-billing-api.test.tsx +70 -0
  114. package/src/__tests__/org-billing-null-guards.test.ts +64 -0
  115. package/src/__tests__/org-billing-page.test.tsx +124 -0
  116. package/src/__tests__/plugin-definition.test.ts +43 -0
  117. package/src/__tests__/plugin-install-flow.test.tsx +535 -0
  118. package/src/__tests__/plugin-registry.test.tsx +475 -0
  119. package/src/__tests__/plugin-setup/setup-chat-panel.test.ts +142 -0
  120. package/src/__tests__/plugin-setup/use-plugin-setup-chat.test.ts +49 -0
  121. package/src/__tests__/plugin-tool-definitions.test.ts +51 -0
  122. package/src/__tests__/plugin-tool-sync.test.ts +59 -0
  123. package/src/__tests__/portfolio-chart.test.tsx +24 -0
  124. package/src/__tests__/pricing.test.tsx +107 -0
  125. package/src/__tests__/promotion-form.test.tsx +180 -0
  126. package/src/__tests__/promotions-list.test.tsx +194 -0
  127. package/src/__tests__/provider-key-api.test.ts +134 -0
  128. package/src/__tests__/resend-verification-button.test.tsx +104 -0
  129. package/src/__tests__/sanitize-redirect-url.test.ts +47 -0
  130. package/src/__tests__/secrets-audit-pagination.test.tsx +139 -0
  131. package/src/__tests__/settings.test.tsx +937 -0
  132. package/src/__tests__/setup-checklist.test.tsx +274 -0
  133. package/src/__tests__/setup.ts +82 -0
  134. package/src/__tests__/smoke.test.tsx +10 -0
  135. package/src/__tests__/snapshot-api.test.ts +104 -0
  136. package/src/__tests__/status-api.test.ts +46 -0
  137. package/src/__tests__/status-badge.test.tsx +33 -0
  138. package/src/__tests__/status-colors.test.ts +83 -0
  139. package/src/__tests__/status-page.test.tsx +86 -0
  140. package/src/__tests__/step-superpowers.test.tsx +218 -0
  141. package/src/__tests__/story-sections.test.tsx +24 -0
  142. package/src/__tests__/superpower-content-sanitize.test.tsx +87 -0
  143. package/src/__tests__/superpower-content.test.tsx +44 -0
  144. package/src/__tests__/suspension-banner.test.tsx +140 -0
  145. package/src/__tests__/tenant-context.test.tsx +146 -0
  146. package/src/__tests__/tenant-keys-api.test.ts +114 -0
  147. package/src/__tests__/tenant-table-pagination.test.tsx +124 -0
  148. package/src/__tests__/terminal-log-cleanup.test.tsx +51 -0
  149. package/src/__tests__/terminal-sequence.test.tsx +28 -0
  150. package/src/__tests__/transaction-history.test.tsx +325 -0
  151. package/src/__tests__/trpc-types.test.ts +102 -0
  152. package/src/__tests__/use-capability-meta.test.ts +161 -0
  153. package/src/__tests__/use-chat.test.ts +616 -0
  154. package/src/__tests__/use-has-org.test.ts +44 -0
  155. package/src/__tests__/use-image-status.test.ts +77 -0
  156. package/src/__tests__/use-pagination-params.test.ts +88 -0
  157. package/src/__tests__/use-plugin-setup-chat-stale-closure.test.ts +53 -0
  158. package/src/__tests__/use-webmcp.test.ts +119 -0
  159. package/src/__tests__/validate-elevenlabs-key.test.ts +95 -0
  160. package/src/__tests__/validate-redirect-url.test.ts +61 -0
  161. package/src/__tests__/verify-page.test.tsx +140 -0
  162. package/src/__tests__/verify-redirect.test.tsx +41 -0
  163. package/src/__tests__/verify-result-banner.test.tsx +66 -0
  164. package/src/__tests__/webmcp-feature-detect.test.ts +54 -0
  165. package/src/__tests__/webmcp-hook.test.tsx +72 -0
  166. package/src/__tests__/webmcp-marketplace-onboarding-tools.test.ts +185 -0
  167. package/src/__tests__/webmcp-register.test.ts +103 -0
  168. package/src/__tests__/webmcp-set-provider.test.ts +47 -0
  169. package/src/__tests__/webmcp-tools.test.ts +348 -0
  170. package/src/app/(auth)/error.tsx +72 -0
  171. package/src/app/(auth)/forgot-password/page.tsx +137 -0
  172. package/src/app/(auth)/layout.tsx +14 -0
  173. package/src/app/(auth)/loading.tsx +26 -0
  174. package/src/app/(auth)/login/page.tsx +188 -0
  175. package/src/app/(auth)/reset-password/page.tsx +169 -0
  176. package/src/app/(auth)/signup/page.tsx +309 -0
  177. package/src/app/(dashboard)/billing/credits/page.tsx +209 -0
  178. package/src/app/(dashboard)/billing/error.tsx +72 -0
  179. package/src/app/(dashboard)/billing/layout.tsx +73 -0
  180. package/src/app/(dashboard)/billing/loading.tsx +41 -0
  181. package/src/app/(dashboard)/billing/payment/page.tsx +639 -0
  182. package/src/app/(dashboard)/billing/plans/page.tsx +58 -0
  183. package/src/app/(dashboard)/billing/referrals/page.tsx +7 -0
  184. package/src/app/(dashboard)/billing/usage/hosted/page.tsx +348 -0
  185. package/src/app/(dashboard)/billing/usage/page.tsx +663 -0
  186. package/src/app/(dashboard)/changesets/[id]/changeset-detail-client.tsx +400 -0
  187. package/src/app/(dashboard)/changesets/[id]/error.tsx +57 -0
  188. package/src/app/(dashboard)/changesets/[id]/loading.tsx +23 -0
  189. package/src/app/(dashboard)/changesets/[id]/page.tsx +10 -0
  190. package/src/app/(dashboard)/changesets/error.tsx +72 -0
  191. package/src/app/(dashboard)/changesets/page.tsx +10 -0
  192. package/src/app/(dashboard)/chat/page.tsx +74 -0
  193. package/src/app/(dashboard)/dashboard/bots/[id]/settings/page.tsx +10 -0
  194. package/src/app/(dashboard)/dashboard/network/page.tsx +97 -0
  195. package/src/app/(dashboard)/dashboard/page.tsx +13 -0
  196. package/src/app/(dashboard)/error.tsx +72 -0
  197. package/src/app/(dashboard)/layout.tsx +113 -0
  198. package/src/app/(dashboard)/loading.tsx +27 -0
  199. package/src/app/(dashboard)/marketplace/[plugin]/page.tsx +548 -0
  200. package/src/app/(dashboard)/marketplace/error.tsx +72 -0
  201. package/src/app/(dashboard)/marketplace/loading.tsx +27 -0
  202. package/src/app/(dashboard)/marketplace/page.tsx +268 -0
  203. package/src/app/(dashboard)/not-found.tsx +46 -0
  204. package/src/app/(dashboard)/onboarding/page.tsx +267 -0
  205. package/src/app/(dashboard)/settings/account/page.tsx +132 -0
  206. package/src/app/(dashboard)/settings/activity/page.tsx +280 -0
  207. package/src/app/(dashboard)/settings/api-keys/page.tsx +530 -0
  208. package/src/app/(dashboard)/settings/brain/page.tsx +412 -0
  209. package/src/app/(dashboard)/settings/error.tsx +72 -0
  210. package/src/app/(dashboard)/settings/layout.tsx +114 -0
  211. package/src/app/(dashboard)/settings/loading.tsx +31 -0
  212. package/src/app/(dashboard)/settings/notifications/page.tsx +216 -0
  213. package/src/app/(dashboard)/settings/org/page.tsx +617 -0
  214. package/src/app/(dashboard)/settings/profile/page.tsx +510 -0
  215. package/src/app/(dashboard)/settings/providers/page.tsx +842 -0
  216. package/src/app/(dashboard)/settings/secrets/page.tsx +658 -0
  217. package/src/app/(dashboard)/settings/security/page.tsx +1133 -0
  218. package/src/app/admin/accounting/loading.tsx +32 -0
  219. package/src/app/admin/accounting/page.tsx +5 -0
  220. package/src/app/admin/affiliates/loading.tsx +32 -0
  221. package/src/app/admin/affiliates/page.tsx +5 -0
  222. package/src/app/admin/audit/loading.tsx +32 -0
  223. package/src/app/admin/audit/page.tsx +5 -0
  224. package/src/app/admin/billing-health/loading.tsx +17 -0
  225. package/src/app/admin/billing-health/page.tsx +10 -0
  226. package/src/app/admin/compliance/page.tsx +5 -0
  227. package/src/app/admin/error.tsx +72 -0
  228. package/src/app/admin/gpu/loading.tsx +38 -0
  229. package/src/app/admin/gpu/page.tsx +5 -0
  230. package/src/app/admin/incidents/page.tsx +10 -0
  231. package/src/app/admin/inference/loading.tsx +32 -0
  232. package/src/app/admin/inference/page.tsx +5 -0
  233. package/src/app/admin/layout.tsx +44 -0
  234. package/src/app/admin/loading.tsx +32 -0
  235. package/src/app/admin/marketplace/loading.tsx +32 -0
  236. package/src/app/admin/marketplace/page.tsx +5 -0
  237. package/src/app/admin/migrations/loading.tsx +22 -0
  238. package/src/app/admin/migrations/page.tsx +5 -0
  239. package/src/app/admin/onboarding/loading.tsx +18 -0
  240. package/src/app/admin/onboarding/page.tsx +5 -0
  241. package/src/app/admin/promotions/[id]/edit/loading.tsx +16 -0
  242. package/src/app/admin/promotions/[id]/edit/page.tsx +56 -0
  243. package/src/app/admin/promotions/[id]/loading.tsx +15 -0
  244. package/src/app/admin/promotions/[id]/page.tsx +311 -0
  245. package/src/app/admin/promotions/loading.tsx +21 -0
  246. package/src/app/admin/promotions/new/loading.tsx +16 -0
  247. package/src/app/admin/promotions/new/page.tsx +12 -0
  248. package/src/app/admin/promotions/page.tsx +266 -0
  249. package/src/app/admin/rate-overrides/loading.tsx +17 -0
  250. package/src/app/admin/rate-overrides/page.tsx +290 -0
  251. package/src/app/admin/roles/loading.tsx +27 -0
  252. package/src/app/admin/roles/page.tsx +5 -0
  253. package/src/app/admin/tenants/loading.tsx +32 -0
  254. package/src/app/admin/tenants/page.tsx +5 -0
  255. package/src/app/apple-icon.tsx +32 -0
  256. package/src/app/auth/callback/[provider]/page.tsx +104 -0
  257. package/src/app/auth/verify/page.tsx +224 -0
  258. package/src/app/channels/error.tsx +72 -0
  259. package/src/app/channels/loading.tsx +29 -0
  260. package/src/app/channels/page.tsx +262 -0
  261. package/src/app/channels/setup/[plugin]/page.tsx +136 -0
  262. package/src/app/error.tsx +72 -0
  263. package/src/app/favicon.ico +0 -0
  264. package/src/app/fleet/error.tsx +72 -0
  265. package/src/app/fleet/health/page.tsx +9 -0
  266. package/src/app/fleet/layout.tsx +14 -0
  267. package/src/app/fleet/loading.tsx +33 -0
  268. package/src/app/fleet/page.tsx +5 -0
  269. package/src/app/global-error.tsx +96 -0
  270. package/src/app/globals.css +251 -0
  271. package/src/app/icon.svg +4 -0
  272. package/src/app/instances/[id]/instance-detail-client.tsx +1298 -0
  273. package/src/app/instances/[id]/page.tsx +10 -0
  274. package/src/app/instances/error.tsx +72 -0
  275. package/src/app/instances/instance-list-client.tsx +540 -0
  276. package/src/app/instances/loading.tsx +33 -0
  277. package/src/app/instances/new/create-instance-client.tsx +377 -0
  278. package/src/app/instances/new/page.tsx +9 -0
  279. package/src/app/instances/page.tsx +9 -0
  280. package/src/app/layout.tsx +83 -0
  281. package/src/app/not-found.tsx +38 -0
  282. package/src/app/og/route.tsx +50 -0
  283. package/src/app/page.tsx +39 -0
  284. package/src/app/plugins/error.tsx +72 -0
  285. package/src/app/plugins/layout.tsx +14 -0
  286. package/src/app/plugins/loading.tsx +30 -0
  287. package/src/app/plugins/page.tsx +555 -0
  288. package/src/app/pricing/error.tsx +72 -0
  289. package/src/app/pricing/loading.tsx +25 -0
  290. package/src/app/pricing/page.tsx +20 -0
  291. package/src/app/privacy/page.tsx +406 -0
  292. package/src/app/robots.ts +9 -0
  293. package/src/app/sitemap.ts +11 -0
  294. package/src/app/status/error.tsx +72 -0
  295. package/src/app/status/loading.tsx +21 -0
  296. package/src/app/status/page.tsx +20 -0
  297. package/src/app/terms/page.tsx +414 -0
  298. package/src/components/account-switcher.tsx +82 -0
  299. package/src/components/admin/accounting-dashboard.tsx +190 -0
  300. package/src/components/admin/admin-guard.tsx +36 -0
  301. package/src/components/admin/admin-nav.tsx +71 -0
  302. package/src/components/admin/affiliate-dashboard.tsx +564 -0
  303. package/src/components/admin/audit-log-table.tsx +336 -0
  304. package/src/components/admin/billing-health-dashboard.test.tsx +40 -0
  305. package/src/components/admin/billing-health-dashboard.tsx +416 -0
  306. package/src/components/admin/bulk-actions-bar.test.tsx +92 -0
  307. package/src/components/admin/bulk-actions-bar.tsx +80 -0
  308. package/src/components/admin/bulk-export-dialog.test.tsx +75 -0
  309. package/src/components/admin/bulk-export-dialog.tsx +189 -0
  310. package/src/components/admin/bulk-grant-dialog.test.tsx +81 -0
  311. package/src/components/admin/bulk-grant-dialog.tsx +147 -0
  312. package/src/components/admin/bulk-preview-dialog.test.tsx +72 -0
  313. package/src/components/admin/bulk-preview-dialog.tsx +106 -0
  314. package/src/components/admin/bulk-reactivate-dialog.test.tsx +51 -0
  315. package/src/components/admin/bulk-reactivate-dialog.tsx +55 -0
  316. package/src/components/admin/bulk-select-all-banner.test.tsx +36 -0
  317. package/src/components/admin/bulk-select-all-banner.tsx +44 -0
  318. package/src/components/admin/bulk-suspend-dialog.test.tsx +77 -0
  319. package/src/components/admin/bulk-suspend-dialog.tsx +129 -0
  320. package/src/components/admin/bulk-undo-toast.test.tsx +66 -0
  321. package/src/components/admin/bulk-undo-toast.tsx +121 -0
  322. package/src/components/admin/compliance-dashboard.tsx +1341 -0
  323. package/src/components/admin/gpu-dashboard.tsx +552 -0
  324. package/src/components/admin/grant-credits-dialog.tsx +121 -0
  325. package/src/components/admin/incident-dashboard.test.tsx +44 -0
  326. package/src/components/admin/incident-dashboard.tsx +717 -0
  327. package/src/components/admin/inference-dashboard.tsx +415 -0
  328. package/src/components/admin/marketplace-admin.tsx +765 -0
  329. package/src/components/admin/migrations-dashboard.tsx +404 -0
  330. package/src/components/admin/onboarding-dashboard.tsx +422 -0
  331. package/src/components/admin/promotions/promotion-form.tsx +440 -0
  332. package/src/components/admin/roles-dashboard.tsx +278 -0
  333. package/src/components/admin/suspend-dialog.tsx +98 -0
  334. package/src/components/admin/tenant-notes-panel.tsx +134 -0
  335. package/src/components/admin/tenant-row-actions.tsx +78 -0
  336. package/src/components/admin/tenant-table.tsx +339 -0
  337. package/src/components/auth/auth-error.tsx +22 -0
  338. package/src/components/auth/auth-redirect.tsx +18 -0
  339. package/src/components/auth/auth-shell.tsx +25 -0
  340. package/src/components/auth/email-verification-banner.tsx +25 -0
  341. package/src/components/auth/email-verification-result-banner.tsx +70 -0
  342. package/src/components/auth/resend-verification-button.tsx +94 -0
  343. package/src/components/auth/wopr-wordmark.tsx +19 -0
  344. package/src/components/billing/add-payment-method-dialog.tsx +267 -0
  345. package/src/components/billing/affiliate-dashboard.tsx +300 -0
  346. package/src/components/billing/auto-topup-card.tsx +432 -0
  347. package/src/components/billing/buy-credits-panel.tsx +180 -0
  348. package/src/components/billing/buy-crypto-credits-panel.tsx +96 -0
  349. package/src/components/billing/byok-callout.tsx +87 -0
  350. package/src/components/billing/coupon-input.tsx +86 -0
  351. package/src/components/billing/credit-balance.tsx +79 -0
  352. package/src/components/billing/degraded-state-banner.tsx +95 -0
  353. package/src/components/billing/dividend-banner.tsx +97 -0
  354. package/src/components/billing/dividend-eligibility.tsx +86 -0
  355. package/src/components/billing/dividend-pool-stats.tsx +86 -0
  356. package/src/components/billing/first-dividend-dialog.tsx +109 -0
  357. package/src/components/billing/low-balance-banner.tsx +50 -0
  358. package/src/components/billing/org-billing-page.tsx +360 -0
  359. package/src/components/billing/suspension-banner.tsx +53 -0
  360. package/src/components/billing/transaction-history.tsx +239 -0
  361. package/src/components/bot-settings/__tests__/bot-settings-client.test.tsx +205 -0
  362. package/src/components/bot-settings/backups-tab.tsx +377 -0
  363. package/src/components/bot-settings/bot-settings-client.tsx +1712 -0
  364. package/src/components/bot-settings/resources-tab.tsx +203 -0
  365. package/src/components/bot-settings/storage-tab.tsx +248 -0
  366. package/src/components/bot-settings/vps-info-panel.tsx +132 -0
  367. package/src/components/bot-settings/vps-upgrade-card.tsx +110 -0
  368. package/src/components/capability/CapabilityResolver.tsx +113 -0
  369. package/src/components/channel-wizard/field-interactive.tsx +48 -0
  370. package/src/components/channel-wizard/field-oauth.tsx +181 -0
  371. package/src/components/channel-wizard/field-paste.tsx +47 -0
  372. package/src/components/channel-wizard/field-qr.tsx +302 -0
  373. package/src/components/channel-wizard/index.ts +6 -0
  374. package/src/components/channel-wizard/step-renderer.tsx +103 -0
  375. package/src/components/channel-wizard/wizard.tsx +200 -0
  376. package/src/components/chat/ambient-dot.tsx +32 -0
  377. package/src/components/chat/chat-input.tsx +56 -0
  378. package/src/components/chat/chat-message.tsx +36 -0
  379. package/src/components/chat/chat-panel.tsx +138 -0
  380. package/src/components/chat/chat-widget.tsx +41 -0
  381. package/src/components/chat/index.ts +5 -0
  382. package/src/components/dashboard/command-center.tsx +614 -0
  383. package/src/components/instances/friends-tab.test.tsx +265 -0
  384. package/src/components/instances/friends-tab.tsx +721 -0
  385. package/src/components/landing/hero.tsx +53 -0
  386. package/src/components/landing/landing-nav.tsx +21 -0
  387. package/src/components/landing/landing-page.tsx +71 -0
  388. package/src/components/landing/portfolio-chart.tsx +349 -0
  389. package/src/components/landing/story-sections.tsx +50 -0
  390. package/src/components/landing/terminal-lines.ts +99 -0
  391. package/src/components/landing/terminal-sequence.tsx +453 -0
  392. package/src/components/landing/typing-effect.tsx +43 -0
  393. package/src/components/marketplace/category-filter.tsx +61 -0
  394. package/src/components/marketplace/empty-state.tsx +61 -0
  395. package/src/components/marketplace/featured-heroes.tsx +84 -0
  396. package/src/components/marketplace/first-visit-hero.tsx +110 -0
  397. package/src/components/marketplace/index.ts +9 -0
  398. package/src/components/marketplace/install-wizard.tsx +782 -0
  399. package/src/components/marketplace/marketplace-tabs.tsx +54 -0
  400. package/src/components/marketplace/plugin-card.tsx +129 -0
  401. package/src/components/marketplace/superpower-card.tsx +104 -0
  402. package/src/components/marketplace/superpower-content.tsx +117 -0
  403. package/src/components/marketplace/terminal-search.tsx +67 -0
  404. package/src/components/oauth-buttons.tsx +75 -0
  405. package/src/components/observability/fleet-health.tsx +370 -0
  406. package/src/components/observability/health-overview.tsx +246 -0
  407. package/src/components/observability/logs-viewer.tsx +215 -0
  408. package/src/components/observability/metrics-dashboard.tsx +288 -0
  409. package/src/components/onboarding/fallback-setup.tsx +137 -0
  410. package/src/components/onboarding/index.ts +3 -0
  411. package/src/components/onboarding/setup-checklist.tsx +333 -0
  412. package/src/components/onboarding/step-superpowers.tsx +122 -0
  413. package/src/components/plugin-setup/index.ts +1 -0
  414. package/src/components/plugin-setup/setup-chat-panel.tsx +188 -0
  415. package/src/components/pricing/dividend-calculator.tsx +47 -0
  416. package/src/components/pricing/dividend-stats.tsx +117 -0
  417. package/src/components/pricing/pricing-page.tsx +229 -0
  418. package/src/components/settings/create-org-wizard.tsx +225 -0
  419. package/src/components/sidebar.tsx +202 -0
  420. package/src/components/status/status-page.tsx +209 -0
  421. package/src/components/status-badge.tsx +28 -0
  422. package/src/components/theme-provider.tsx +8 -0
  423. package/src/components/ui/alert-dialog.tsx +141 -0
  424. package/src/components/ui/badge.tsx +47 -0
  425. package/src/components/ui/banner.tsx +36 -0
  426. package/src/components/ui/button.tsx +64 -0
  427. package/src/components/ui/card.tsx +75 -0
  428. package/src/components/ui/checkbox.tsx +52 -0
  429. package/src/components/ui/collapsible.tsx +31 -0
  430. package/src/components/ui/credit-detailed.tsx +33 -0
  431. package/src/components/ui/dialog.tsx +143 -0
  432. package/src/components/ui/dropdown-menu.tsx +228 -0
  433. package/src/components/ui/form.tsx +151 -0
  434. package/src/components/ui/input.tsx +21 -0
  435. package/src/components/ui/label.tsx +21 -0
  436. package/src/components/ui/popover.tsx +74 -0
  437. package/src/components/ui/progress.tsx +28 -0
  438. package/src/components/ui/radio-group.tsx +45 -0
  439. package/src/components/ui/select.tsx +175 -0
  440. package/src/components/ui/separator.tsx +28 -0
  441. package/src/components/ui/sheet.tsx +125 -0
  442. package/src/components/ui/skeleton.tsx +15 -0
  443. package/src/components/ui/switch.tsx +35 -0
  444. package/src/components/ui/table.tsx +92 -0
  445. package/src/components/ui/tabs.tsx +81 -0
  446. package/src/components/ui/textarea.tsx +18 -0
  447. package/src/components/ui/tooltip.tsx +44 -0
  448. package/src/config/provider-docs.ts +17 -0
  449. package/src/hooks/__tests__/use-async.test.ts +127 -0
  450. package/src/hooks/__tests__/use-count-up.test.ts +129 -0
  451. package/src/hooks/__tests__/use-debounce.test.ts +105 -0
  452. package/src/hooks/__tests__/use-fleet-sse.test.ts +216 -0
  453. package/src/hooks/__tests__/use-local-storage.test.ts +74 -0
  454. package/src/hooks/__tests__/use-mobile.test.ts +86 -0
  455. package/src/hooks/__tests__/use-save-queue.test.ts +159 -0
  456. package/src/hooks/use-async.ts +54 -0
  457. package/src/hooks/use-capability-meta.ts +99 -0
  458. package/src/hooks/use-count-up.ts +23 -0
  459. package/src/hooks/use-debounce.ts +12 -0
  460. package/src/hooks/use-fleet-sse.ts +47 -0
  461. package/src/hooks/use-has-org.ts +18 -0
  462. package/src/hooks/use-image-status.ts +36 -0
  463. package/src/hooks/use-local-storage.ts +36 -0
  464. package/src/hooks/use-mobile.ts +17 -0
  465. package/src/hooks/use-page-context.ts +24 -0
  466. package/src/hooks/use-pagination-params.ts +30 -0
  467. package/src/hooks/use-plugin-registry.ts +247 -0
  468. package/src/hooks/use-plugin-setup-chat.ts +211 -0
  469. package/src/hooks/use-save-queue.ts +54 -0
  470. package/src/hooks/use-webmcp.ts +40 -0
  471. package/src/lib/__tests__/__snapshots__/pricing-data.test.ts.snap +112 -0
  472. package/src/lib/__tests__/admin-api.test.ts +487 -0
  473. package/src/lib/__tests__/api-bot-crud.test.ts +391 -0
  474. package/src/lib/__tests__/api-fetch.test.ts +196 -0
  475. package/src/lib/__tests__/bot-settings-data.test.ts +352 -0
  476. package/src/lib/__tests__/org-api.test.ts +281 -0
  477. package/src/lib/__tests__/org-billing-api.test.ts +242 -0
  478. package/src/lib/__tests__/pricing-data.test.ts +32 -0
  479. package/src/lib/__tests__/settings-api.test.ts +272 -0
  480. package/src/lib/admin-affiliate-api.ts +51 -0
  481. package/src/lib/admin-api.ts +325 -0
  482. package/src/lib/admin-compliance-api.ts +127 -0
  483. package/src/lib/admin-gpu-api.ts +82 -0
  484. package/src/lib/admin-incident-api.ts +121 -0
  485. package/src/lib/admin-inference-api.ts +47 -0
  486. package/src/lib/admin-marketplace-api.ts +97 -0
  487. package/src/lib/api-config.test.ts +111 -0
  488. package/src/lib/api-config.ts +65 -0
  489. package/src/lib/api-errors.test.ts +43 -0
  490. package/src/lib/api.ts +2011 -0
  491. package/src/lib/auth-client.ts +11 -0
  492. package/src/lib/bot-settings-data.ts +342 -0
  493. package/src/lib/brand-config.ts +145 -0
  494. package/src/lib/brand.ts +669 -0
  495. package/src/lib/changeset-api.ts +29 -0
  496. package/src/lib/changeset-types.ts +56 -0
  497. package/src/lib/channel-manifests.ts +50 -0
  498. package/src/lib/chat/chat-context.tsx +70 -0
  499. package/src/lib/chat/chat-store.ts +62 -0
  500. package/src/lib/chat/types.ts +35 -0
  501. package/src/lib/chat/use-chat.ts +255 -0
  502. package/src/lib/cost-comparison-data.test.ts +95 -0
  503. package/src/lib/cost-comparison-data.ts +54 -0
  504. package/src/lib/errors.test.ts +64 -0
  505. package/src/lib/errors.ts +52 -0
  506. package/src/lib/fetch-utils.test.ts +57 -0
  507. package/src/lib/fetch-utils.ts +25 -0
  508. package/src/lib/format-credit.test.ts +66 -0
  509. package/src/lib/format-credit.ts +24 -0
  510. package/src/lib/format.test.ts +62 -0
  511. package/src/lib/format.ts +17 -0
  512. package/src/lib/logger.ts +28 -0
  513. package/src/lib/marketplace-data.ts +346 -0
  514. package/src/lib/oauth-errors.ts +19 -0
  515. package/src/lib/onboarding-data.ts +1265 -0
  516. package/src/lib/onboarding-store.ts +233 -0
  517. package/src/lib/org-api.ts +74 -0
  518. package/src/lib/org-billing-api.ts +81 -0
  519. package/src/lib/page-prompts.test.ts +32 -0
  520. package/src/lib/page-prompts.ts +23 -0
  521. package/src/lib/plugin/index.ts +32 -0
  522. package/src/lib/plugin/tool-definitions.ts +306 -0
  523. package/src/lib/pricing-data.ts +115 -0
  524. package/src/lib/promotions-types.ts +58 -0
  525. package/src/lib/settings-api.ts +63 -0
  526. package/src/lib/status-colors.ts +38 -0
  527. package/src/lib/tenant-context.tsx +134 -0
  528. package/src/lib/trpc-types.ts +173 -0
  529. package/src/lib/trpc.tsx +86 -0
  530. package/src/lib/utils.test.ts +55 -0
  531. package/src/lib/utils.ts +18 -0
  532. package/src/lib/validate-redirect-url.ts +39 -0
  533. package/src/lib/webmcp/feature-detect.ts +13 -0
  534. package/src/lib/webmcp/marketplace-onboarding-tools.ts +202 -0
  535. package/src/lib/webmcp/register.ts +44 -0
  536. package/src/lib/webmcp/tools.ts +422 -0
  537. package/src/proxy.ts +258 -0
  538. package/src/types/missing-deps.d.ts +160 -0
  539. package/src/types/motion-dom.d.ts +162 -0
  540. package/src/types/vitest-matchers.d.ts +40 -0
  541. package/src/types/web-mcp.d.ts +22 -0
  542. package/tsconfig.json +34 -0
  543. package/vitest.config.ts +26 -0
@@ -0,0 +1,1712 @@
1
+ "use client";
2
+
3
+ import { Loader2, Play, RotateCw, Square, Trash2 } from "lucide-react";
4
+ import Link from "next/link";
5
+ import { useRouter } from "next/navigation";
6
+ import { useCallback, useEffect, useRef, useState } from "react";
7
+ import { LogsViewer } from "@/components/observability/logs-viewer";
8
+ import { StatusBadge } from "@/components/status-badge";
9
+ import { Badge } from "@/components/ui/badge";
10
+ import { Button } from "@/components/ui/button";
11
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
12
+ import { CreditDetailed } from "@/components/ui/credit-detailed";
13
+ import {
14
+ Dialog,
15
+ DialogContent,
16
+ DialogDescription,
17
+ DialogFooter,
18
+ DialogHeader,
19
+ DialogTitle,
20
+ } from "@/components/ui/dialog";
21
+ import { Input } from "@/components/ui/input";
22
+ import { Label } from "@/components/ui/label";
23
+ import { Progress } from "@/components/ui/progress";
24
+ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
25
+ import { Separator } from "@/components/ui/separator";
26
+ import { Skeleton } from "@/components/ui/skeleton";
27
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
28
+ import { Textarea } from "@/components/ui/textarea";
29
+ import { useFleetSSE } from "@/hooks/use-fleet-sse";
30
+ import { useSaveQueue } from "@/hooks/use-save-queue";
31
+ import type { InstanceHealth } from "@/lib/api";
32
+ import { getInstanceHealth } from "@/lib/api";
33
+ import type {
34
+ ActiveSuperpower,
35
+ AvailableSuperpower,
36
+ BotChannel,
37
+ BotSettings,
38
+ DiscoverPlugin,
39
+ InstalledPlugin,
40
+ } from "@/lib/bot-settings-data";
41
+ import {
42
+ activateSuperpower,
43
+ controlBot,
44
+ disconnectChannel,
45
+ getBotSettings,
46
+ getBotStatus,
47
+ getChannelConfig,
48
+ getPluginConfig,
49
+ getSuperpowerConfig,
50
+ installPlugin,
51
+ PERSONALITY_TEMPLATES,
52
+ togglePlugin,
53
+ uninstallPlugin,
54
+ updateBotBrain,
55
+ updateBotIdentity,
56
+ updateChannelConfig,
57
+ updatePluginConfig,
58
+ updateSuperpowerConfig,
59
+ } from "@/lib/bot-settings-data";
60
+ import { brandName, productName } from "@/lib/brand-config";
61
+ import { toUserMessage } from "@/lib/errors";
62
+ import { formatCreditDetailed, formatCreditStandard } from "@/lib/format-credit";
63
+ import { DEFAULT_STATUS_STYLE, PLUGIN_STATUS_STYLES } from "@/lib/status-colors";
64
+ import { BackupsTab } from "./backups-tab";
65
+ import { ResourcesTab } from "./resources-tab";
66
+ import { StorageTab } from "./storage-tab";
67
+ import { VpsInfoPanel } from "./vps-info-panel";
68
+ import { VpsUpgradeCard } from "./vps-upgrade-card";
69
+
70
+ export function BotSettingsClient({ botId }: { botId: string }) {
71
+ const [settings, setSettings] = useState<BotSettings | null>(null);
72
+ const [loading, setLoading] = useState(true);
73
+ const [error, setError] = useState<string | null>(null);
74
+ const [actionPending, setActionPending] = useState(false);
75
+ const [actionError, setActionError] = useState<string | null>(null);
76
+ const [pendingAction, setPendingAction] = useState<string | null>(null);
77
+ const [health, setHealth] = useState<InstanceHealth | null>(null);
78
+ const [statusChanged, setStatusChanged] = useState(false);
79
+ const prevStatusRef = useRef<string | null>(null);
80
+
81
+ const load = useCallback(async () => {
82
+ setLoading(true);
83
+ setError(null);
84
+ try {
85
+ const data = await getBotSettings(botId);
86
+ setSettings(data);
87
+ } catch (err) {
88
+ setError(toUserMessage(err, "Failed to load bot settings"));
89
+ } finally {
90
+ setLoading(false);
91
+ }
92
+ }, [botId]);
93
+
94
+ useEffect(() => {
95
+ load();
96
+ }, [load]);
97
+
98
+ const settingsLoaded = settings !== null;
99
+
100
+ // Fetch status and health once on mount (after settings loaded)
101
+ useEffect(() => {
102
+ if (!settingsLoaded) return;
103
+ let cancelled = false;
104
+ Promise.all([getBotStatus(botId), getInstanceHealth(botId)])
105
+ .then(([{ status }, h]) => {
106
+ if (!cancelled) {
107
+ setSettings((prev) => (prev ? { ...prev, status } : prev));
108
+ setHealth(h);
109
+ }
110
+ })
111
+ .catch((_err) => {
112
+ // Silently ignore — same as old polling behavior
113
+ });
114
+ return () => {
115
+ cancelled = true;
116
+ };
117
+ }, [botId, settingsLoaded]);
118
+
119
+ // Real-time bot status via SSE — re-fetch on events for this bot
120
+ const mountedRef = useRef(true);
121
+ useEffect(() => {
122
+ mountedRef.current = true;
123
+ return () => {
124
+ mountedRef.current = false;
125
+ };
126
+ }, []);
127
+ useFleetSSE((event) => {
128
+ if (event.botId !== botId) return;
129
+ Promise.all([getBotStatus(botId), getInstanceHealth(botId)])
130
+ .then(([{ status }, h]) => {
131
+ if (mountedRef.current) {
132
+ setSettings((prev) => (prev ? { ...prev, status } : prev));
133
+ setHealth(h);
134
+ if (status === "running") {
135
+ setPendingAction((prev) => {
136
+ if (prev === "restart") {
137
+ setActionPending(false);
138
+ return null;
139
+ }
140
+ return prev;
141
+ });
142
+ }
143
+ }
144
+ })
145
+ .catch((_err) => {
146
+ // Silently ignore — same as old polling behavior
147
+ });
148
+ });
149
+
150
+ // Detect status changes for badge glow animation
151
+ useEffect(() => {
152
+ if (!settings) return;
153
+ const currentStatus = settings.status;
154
+ const changed = prevStatusRef.current !== null && prevStatusRef.current !== currentStatus;
155
+ prevStatusRef.current = currentStatus;
156
+ if (changed) {
157
+ setStatusChanged(true);
158
+ const timeout = setTimeout(() => setStatusChanged(false), 1000);
159
+ return () => clearTimeout(timeout);
160
+ }
161
+ }, [settings]);
162
+
163
+ // Auto-dismiss action error after 5 seconds
164
+ useEffect(() => {
165
+ if (!actionError) return;
166
+ const timeout = setTimeout(() => setActionError(null), 5000);
167
+ return () => clearTimeout(timeout);
168
+ }, [actionError]);
169
+
170
+ async function handleAction(action: "start" | "stop" | "restart") {
171
+ setActionPending(true);
172
+ setPendingAction(action);
173
+ setActionError(null);
174
+ try {
175
+ await controlBot(botId, action);
176
+ if (action === "start") {
177
+ setSettings((prev) => (prev ? { ...prev, status: "running" } : prev));
178
+ } else if (action === "stop") {
179
+ setSettings((prev) => (prev ? { ...prev, status: "stopped" } : prev));
180
+ }
181
+ // Refetch full settings to pick up any server-side state changes
182
+ load();
183
+ } catch {
184
+ setActionError(`Failed to ${action} bot`);
185
+ } finally {
186
+ if (action !== "restart") {
187
+ setActionPending(false);
188
+ setPendingAction(null);
189
+ }
190
+ }
191
+ }
192
+
193
+ if (loading) {
194
+ return (
195
+ <div className="space-y-6">
196
+ <div className="flex items-center gap-3">
197
+ <Skeleton className="h-8 w-20" />
198
+ <Skeleton className="h-2 w-2 rounded-full" />
199
+ <Skeleton className="h-7 w-48" />
200
+ </div>
201
+ <div className="flex gap-3">
202
+ {Array.from({ length: 5 }, (_, n) => `sk-tab-${n}`).map((skId) => (
203
+ <Skeleton key={skId} className="h-9 w-24" />
204
+ ))}
205
+ </div>
206
+ <div className="space-y-4 rounded-md border p-6">
207
+ <Skeleton className="h-5 w-32" />
208
+ <div className="space-y-2">
209
+ <Skeleton className="h-4 w-24" />
210
+ <Skeleton className="h-9 w-full" />
211
+ </div>
212
+ <div className="space-y-2">
213
+ <Skeleton className="h-4 w-24" />
214
+ <Skeleton className="h-20 w-full" />
215
+ </div>
216
+ <Skeleton className="h-9 w-28" />
217
+ </div>
218
+ </div>
219
+ );
220
+ }
221
+
222
+ if (error || !settings) {
223
+ return (
224
+ <div className="flex h-[60vh] flex-col items-center justify-center gap-4">
225
+ <p className="text-muted-foreground">{error ?? "Bot not found"}</p>
226
+ <Button variant="outline" asChild>
227
+ <Link href="/dashboard">Back to Dashboard</Link>
228
+ </Button>
229
+ </div>
230
+ );
231
+ }
232
+
233
+ return (
234
+ <div className="space-y-6">
235
+ {/* Header */}
236
+ <div className="flex flex-wrap items-center gap-3">
237
+ <Button variant="ghost" size="sm" asChild>
238
+ <Link href="/dashboard">&larr; Back to Fleet</Link>
239
+ </Button>
240
+ <div
241
+ className={`transition-all duration-500 ${statusChanged ? "ring-2 ring-terminal/30 rounded-full" : ""}`}
242
+ >
243
+ {actionPending && pendingAction === "restart" ? (
244
+ <Badge
245
+ variant="outline"
246
+ className="gap-1.5 bg-yellow-500/15 text-yellow-500 border-yellow-500/25"
247
+ >
248
+ <Loader2 className="size-3 animate-spin" />
249
+ Restarting
250
+ </Badge>
251
+ ) : (
252
+ <StatusBadge status={settings.status === "archived" ? "stopped" : settings.status} />
253
+ )}
254
+ </div>
255
+ <h1 className="text-2xl font-bold tracking-tight">{settings.identity.name}</h1>
256
+ <div className="flex gap-2 ml-auto">
257
+ {(settings.status === "stopped" || settings.status === "archived") && (
258
+ <Button
259
+ size="sm"
260
+ variant="terminal"
261
+ disabled={actionPending}
262
+ onClick={() => handleAction("start")}
263
+ >
264
+ {actionPending && pendingAction === "start" ? (
265
+ <Loader2 className="size-4 animate-spin" />
266
+ ) : (
267
+ <Play className="size-4" />
268
+ )}
269
+ Start
270
+ </Button>
271
+ )}
272
+ {settings.status === "running" && !(actionPending && pendingAction === "restart") && (
273
+ <>
274
+ <Button
275
+ size="sm"
276
+ variant="outline"
277
+ disabled={actionPending}
278
+ className="border-red-500/40 text-red-400 hover:bg-red-500/10 hover:border-red-500/60"
279
+ onClick={() => handleAction("stop")}
280
+ >
281
+ {actionPending && pendingAction === "stop" ? (
282
+ <Loader2 className="size-3.5 animate-spin" />
283
+ ) : (
284
+ <Square className="size-3.5" />
285
+ )}
286
+ Stop
287
+ </Button>
288
+ <Button
289
+ size="sm"
290
+ variant="outline"
291
+ disabled={actionPending}
292
+ onClick={() => handleAction("restart")}
293
+ >
294
+ <RotateCw
295
+ className={`size-3.5 ${actionPending && pendingAction === "restart" ? "animate-spin" : ""}`}
296
+ />
297
+ Restart
298
+ </Button>
299
+ </>
300
+ )}
301
+ </div>
302
+ </div>
303
+
304
+ {/* Health info bar */}
305
+ {health && health.uptime > 0 && (
306
+ <div className="flex items-center gap-4 text-xs text-muted-foreground">
307
+ <span>
308
+ Uptime: {Math.floor(health.uptime / 3600)}h {Math.floor((health.uptime % 3600) / 60)}m
309
+ </span>
310
+ <span>
311
+ Sessions: {health.activeSessions} active / {health.totalSessions} total
312
+ </span>
313
+ </div>
314
+ )}
315
+
316
+ {/* Action error banner */}
317
+ {actionError && (
318
+ <div className="rounded-sm border border-red-500/25 bg-red-500/10 px-4 py-3 text-sm text-red-400 animate-in fade-in duration-200">
319
+ {actionError}
320
+ </div>
321
+ )}
322
+
323
+ <Separator />
324
+
325
+ {/* Tabs */}
326
+ <Tabs defaultValue="identity">
327
+ <TabsList className="flex-wrap">
328
+ <TabsTrigger value="identity">Identity</TabsTrigger>
329
+ <TabsTrigger value="brain">Brain</TabsTrigger>
330
+ <TabsTrigger value="channels">Channels</TabsTrigger>
331
+ <TabsTrigger value="superpowers">Superpowers</TabsTrigger>
332
+ <TabsTrigger value="plugins">Plugins</TabsTrigger>
333
+ <TabsTrigger value="storage">Storage</TabsTrigger>
334
+ <TabsTrigger value="backups">Backups</TabsTrigger>
335
+ <TabsTrigger value="usage">Usage</TabsTrigger>
336
+ <TabsTrigger value="resources">Resources</TabsTrigger>
337
+ <TabsTrigger value="logs">Logs</TabsTrigger>
338
+ <TabsTrigger value="vps">VPS</TabsTrigger>
339
+ <TabsTrigger value="danger">Danger Zone</TabsTrigger>
340
+ </TabsList>
341
+
342
+ <TabsContent value="identity" className="mt-4">
343
+ <IdentityTab settings={settings} botId={botId} onUpdate={setSettings} />
344
+ </TabsContent>
345
+
346
+ <TabsContent value="brain" className="mt-4">
347
+ <BrainTab settings={settings} botId={botId} onUpdate={setSettings} />
348
+ </TabsContent>
349
+
350
+ <TabsContent value="channels" className="mt-4">
351
+ <ChannelsTab settings={settings} botId={botId} onUpdate={load} />
352
+ </TabsContent>
353
+
354
+ <TabsContent value="superpowers" className="mt-4">
355
+ <SuperpowersTab settings={settings} botId={botId} onUpdate={load} />
356
+ </TabsContent>
357
+
358
+ <TabsContent value="plugins" className="mt-4">
359
+ <PluginsTab settings={settings} botId={botId} onUpdate={load} />
360
+ </TabsContent>
361
+
362
+ <TabsContent value="storage" className="mt-4">
363
+ <StorageTab botId={botId} />
364
+ </TabsContent>
365
+
366
+ <TabsContent value="backups" className="mt-4">
367
+ <BackupsTab botId={botId} onRestore={load} />
368
+ </TabsContent>
369
+
370
+ <TabsContent value="usage" className="mt-4">
371
+ <UsageTab settings={settings} />
372
+ </TabsContent>
373
+
374
+ <TabsContent value="resources" className="mt-4">
375
+ <ResourcesTab botId={botId} />
376
+ </TabsContent>
377
+
378
+ <TabsContent value="logs" className="mt-6">
379
+ <LogsViewer instanceId={botId} />
380
+ </TabsContent>
381
+
382
+ <TabsContent value="vps" className="mt-4">
383
+ <div className="flex flex-col gap-4">
384
+ <VpsInfoPanel botId={botId} />
385
+ <VpsUpgradeCard botId={botId} />
386
+ </div>
387
+ </TabsContent>
388
+
389
+ <TabsContent value="danger" className="mt-4">
390
+ <DangerZoneTab settings={settings} botId={botId} onUpdate={load} />
391
+ </TabsContent>
392
+ </Tabs>
393
+ </div>
394
+ );
395
+ }
396
+
397
+ // --- Tab 1: Identity ---
398
+
399
+ function IdentityTab({
400
+ settings,
401
+ botId,
402
+ onUpdate,
403
+ }: {
404
+ settings: BotSettings;
405
+ botId: string;
406
+ onUpdate: (s: BotSettings) => void;
407
+ }) {
408
+ const [name, setName] = useState(settings.identity.name);
409
+ const [personality, setPersonality] = useState(settings.identity.personality);
410
+ const [saved, setSaved] = useState(false);
411
+ const savedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
412
+
413
+ useEffect(() => {
414
+ return () => {
415
+ if (savedTimerRef.current !== null) {
416
+ clearTimeout(savedTimerRef.current);
417
+ }
418
+ };
419
+ }, []);
420
+
421
+ const saveFn = useCallback(
422
+ async (payload: { name: string; avatar: string; personality: string }) => {
423
+ try {
424
+ const updated = await updateBotIdentity(botId, payload);
425
+ onUpdate({ ...settings, identity: updated });
426
+ setSaved(true);
427
+ clearTimeout(savedTimerRef.current ?? undefined);
428
+ savedTimerRef.current = setTimeout(() => setSaved(false), 2000);
429
+ } catch {
430
+ throw new Error("Failed to save \u2014 please try again.");
431
+ }
432
+ },
433
+ [botId, settings, onUpdate],
434
+ );
435
+
436
+ const { enqueue: enqueueSave, saving, error: saveError } = useSaveQueue(saveFn);
437
+
438
+ function handleSave() {
439
+ enqueueSave({ name, avatar: settings.identity.avatar, personality });
440
+ }
441
+
442
+ return (
443
+ <div className="max-w-2xl space-y-6">
444
+ <div>
445
+ <h2 className="text-xl font-bold">Identity</h2>
446
+ <p className="text-sm text-muted-foreground">Customize your bot's name and personality.</p>
447
+ </div>
448
+
449
+ <div className="space-y-4">
450
+ <div className="space-y-2">
451
+ <label htmlFor="bot-name" className="text-sm font-medium">
452
+ Name
453
+ </label>
454
+ <Input id="bot-name" value={name} onChange={(e) => setName(e.target.value)} />
455
+ </div>
456
+
457
+ <div className="space-y-2">
458
+ <label htmlFor="bot-personality" className="text-sm font-medium">
459
+ Personality
460
+ </label>
461
+ <Textarea
462
+ id="bot-personality"
463
+ className="min-h-[120px]"
464
+ value={personality}
465
+ onChange={(e) => setPersonality(e.target.value)}
466
+ />
467
+ </div>
468
+
469
+ <div className="space-y-2">
470
+ <span className="text-sm font-medium">Personality templates</span>
471
+ <div className="flex flex-wrap gap-2">
472
+ {PERSONALITY_TEMPLATES.map((t) => (
473
+ <Button
474
+ key={t.id}
475
+ variant="outline"
476
+ size="sm"
477
+ onClick={() => {
478
+ if (t.text) setPersonality(t.text);
479
+ }}
480
+ >
481
+ {t.label}
482
+ </Button>
483
+ ))}
484
+ </div>
485
+ </div>
486
+ </div>
487
+
488
+ <div className="flex items-center gap-3">
489
+ <Button onClick={handleSave} disabled={saving || !name.trim()}>
490
+ {saving ? "Saving..." : "Save changes"}
491
+ </Button>
492
+ {saved && <span className="text-sm text-emerald-500">Saved!</span>}
493
+ {saveError && <p className="text-sm text-destructive">{saveError}</p>}
494
+ </div>
495
+ </div>
496
+ );
497
+ }
498
+
499
+ // --- Tab 2: Brain ---
500
+
501
+ function BrainTab({
502
+ settings,
503
+ botId,
504
+ onUpdate,
505
+ }: {
506
+ settings: BotSettings;
507
+ botId: string;
508
+ onUpdate: (s: BotSettings) => void;
509
+ }) {
510
+ const { brain } = settings;
511
+ const [modelInput, setModelInput] = useState(brain.model);
512
+ const [changingMode, setChangingMode] = useState(false);
513
+ const [brainError, setBrainError] = useState<string | null>(null);
514
+
515
+ const saveModelFn = useCallback(
516
+ async (payload: { model: string }) => {
517
+ await updateBotBrain(botId, payload);
518
+ onUpdate({
519
+ ...settings,
520
+ brain: { ...settings.brain, model: payload.model },
521
+ });
522
+ },
523
+ [botId, settings, onUpdate],
524
+ );
525
+
526
+ const {
527
+ enqueue: enqueueModelSave,
528
+ saving: savingModel,
529
+ error: modelSaveError,
530
+ } = useSaveQueue(saveModelFn);
531
+
532
+ useEffect(() => {
533
+ if (modelSaveError) setBrainError(modelSaveError);
534
+ }, [modelSaveError]);
535
+
536
+ function handleSaveModel() {
537
+ if (modelInput === brain.model) return;
538
+ setBrainError(null);
539
+ enqueueModelSave({ model: modelInput });
540
+ }
541
+
542
+ async function handleModeChange(mode: "hosted" | "byok") {
543
+ if (mode === brain.mode) return;
544
+ setChangingMode(true);
545
+ setBrainError(null);
546
+ try {
547
+ await updateBotBrain(botId, { mode });
548
+ onUpdate({
549
+ ...settings,
550
+ brain: { ...settings.brain, mode },
551
+ });
552
+ } catch {
553
+ setBrainError("Failed to switch provider mode -- please try again.");
554
+ } finally {
555
+ setChangingMode(false);
556
+ }
557
+ }
558
+
559
+ return (
560
+ <div className="max-w-2xl space-y-6">
561
+ <div>
562
+ <h2 className="text-xl font-bold">Brain</h2>
563
+ <p className="text-sm text-muted-foreground">Model and provider configuration.</p>
564
+ </div>
565
+
566
+ <Card>
567
+ <CardHeader>
568
+ <CardTitle>Model</CardTitle>
569
+ <CardDescription>
570
+ LLM model ID (e.g. claude-sonnet-4, gpt-4o). Used via{" "}
571
+ {brain.mode === "hosted" ? `${brandName()} Hosted` : "BYOK"}.
572
+ </CardDescription>
573
+ </CardHeader>
574
+ <CardContent className="space-y-3">
575
+ <div className="flex items-center gap-2">
576
+ <Input
577
+ value={modelInput}
578
+ onChange={(e) => setModelInput(e.target.value)}
579
+ placeholder="e.g. claude-sonnet-4"
580
+ className="max-w-sm"
581
+ />
582
+ <Button
583
+ variant="outline"
584
+ size="sm"
585
+ onClick={handleSaveModel}
586
+ disabled={savingModel || modelInput === brain.model}
587
+ >
588
+ {savingModel ? "Saving..." : "Save"}
589
+ </Button>
590
+ </div>
591
+ </CardContent>
592
+ </Card>
593
+
594
+ <Card>
595
+ <CardHeader>
596
+ <CardTitle>Provider Mode</CardTitle>
597
+ </CardHeader>
598
+ <CardContent className="space-y-4">
599
+ <RadioGroup
600
+ value={brain.mode}
601
+ onValueChange={(v) => handleModeChange(v as "hosted" | "byok")}
602
+ disabled={changingMode}
603
+ className="space-y-2"
604
+ >
605
+ <div className="flex cursor-pointer items-start gap-3 rounded-md border p-3 transition-colors hover:bg-accent/50">
606
+ <RadioGroupItem value="hosted" id="mode-hosted" className="mt-1" />
607
+ <div className="flex-1">
608
+ <div className="flex items-center gap-2">
609
+ <Label htmlFor="mode-hosted" className="text-sm font-medium cursor-pointer">
610
+ {brandName()} Hosted
611
+ </Label>
612
+ {brain.mode === "hosted" && <Badge variant="default">Active</Badge>}
613
+ </div>
614
+ <p className="text-sm text-muted-foreground">
615
+ Everything routed through {brandName()}. Uses credits. No API keys needed.
616
+ </p>
617
+ </div>
618
+ </div>
619
+
620
+ <div className="flex cursor-pointer items-start gap-3 rounded-md border p-3 transition-colors hover:bg-accent/50">
621
+ <RadioGroupItem value="byok" id="mode-byok" className="mt-1" />
622
+ <div className="flex-1">
623
+ <Label htmlFor="mode-byok" className="text-sm font-medium cursor-pointer">
624
+ Bring Your Own Key
625
+ </Label>
626
+ <p className="text-sm text-muted-foreground">
627
+ Use your own Anthropic/OpenAI key. You pay the provider directly.
628
+ </p>
629
+ {brain.mode !== "byok" && (
630
+ <Button
631
+ variant="outline"
632
+ size="sm"
633
+ className="mt-2"
634
+ onClick={() => handleModeChange("byok")}
635
+ disabled={changingMode}
636
+ >
637
+ {changingMode ? "Switching..." : "Switch to BYOK"}
638
+ </Button>
639
+ )}
640
+ </div>
641
+ </div>
642
+ </RadioGroup>
643
+ </CardContent>
644
+ </Card>
645
+
646
+ {brainError && <p className="text-sm text-destructive">{brainError}</p>}
647
+ </div>
648
+ );
649
+ }
650
+
651
+ // --- Tab 3: Channels ---
652
+
653
+ function ChannelsTab({
654
+ settings,
655
+ botId,
656
+ onUpdate,
657
+ }: {
658
+ settings: BotSettings;
659
+ botId: string;
660
+ onUpdate: () => void;
661
+ }) {
662
+ const router = useRouter();
663
+ const [disconnecting, setDisconnecting] = useState<string | null>(null);
664
+ const [channelError, setChannelError] = useState<string | null>(null);
665
+ const [confirmDisconnect, setConfirmDisconnect] = useState<string | null>(null);
666
+ const [configuringChannel, setConfiguringChannel] = useState<BotChannel | null>(null);
667
+
668
+ async function handleDisconnect(channelId: string) {
669
+ setDisconnecting(channelId);
670
+ setChannelError(null);
671
+ try {
672
+ await disconnectChannel(botId, channelId);
673
+ setConfirmDisconnect(null);
674
+ onUpdate();
675
+ } catch {
676
+ setChannelError("Failed to disconnect channel -- please try again.");
677
+ } finally {
678
+ setDisconnecting(null);
679
+ }
680
+ }
681
+
682
+ return (
683
+ <div className="max-w-2xl space-y-6">
684
+ <div>
685
+ <h2 className="text-xl font-bold">Channels</h2>
686
+ <p className="text-sm text-muted-foreground">
687
+ Connected channels and available integrations.
688
+ </p>
689
+ </div>
690
+
691
+ <div className="space-y-3">
692
+ {(settings.channels ?? []).map((ch) => (
693
+ <Card key={ch.id}>
694
+ <CardContent className="flex items-center justify-between p-4">
695
+ <div className="space-y-1">
696
+ <div className="flex items-center gap-2">
697
+ <span className="font-medium">{ch.type}</span>
698
+ <ChannelStatusBadge status={ch.status} />
699
+ </div>
700
+ <p className="text-sm text-muted-foreground">
701
+ {ch.name} &middot; {ch.stats}
702
+ </p>
703
+ </div>
704
+ <div className="flex gap-2">
705
+ <Button variant="outline" size="sm" onClick={() => setConfiguringChannel(ch)}>
706
+ Configure
707
+ </Button>
708
+ {ch.status === "connected" && (
709
+ <Button variant="ghost" size="sm" onClick={() => setConfirmDisconnect(ch.id)}>
710
+ Disconnect
711
+ </Button>
712
+ )}
713
+ </div>
714
+ </CardContent>
715
+ </Card>
716
+ ))}
717
+ </div>
718
+
719
+ <Card>
720
+ <CardHeader>
721
+ <CardTitle>Add More Channels</CardTitle>
722
+ <CardDescription>
723
+ Your {productName()} works everywhere you do. All channels are free.
724
+ </CardDescription>
725
+ </CardHeader>
726
+ <CardContent>
727
+ <div className="flex flex-wrap gap-3">
728
+ {(settings.availableChannels ?? []).map((ch) => (
729
+ <Button
730
+ key={ch.type}
731
+ variant="outline"
732
+ onClick={() => router.push(`/channels/setup/${ch.type.toLowerCase()}?bot=${botId}`)}
733
+ >
734
+ + Add {ch.label}
735
+ </Button>
736
+ ))}
737
+ </div>
738
+ </CardContent>
739
+ </Card>
740
+
741
+ {channelError && <p className="text-sm text-destructive">{channelError}</p>}
742
+
743
+ {configuringChannel && (
744
+ <ConfigureChannelDialog
745
+ channel={configuringChannel}
746
+ botId={botId}
747
+ open={configuringChannel !== null}
748
+ onOpenChange={(open) => {
749
+ if (!open) setConfiguringChannel(null);
750
+ }}
751
+ onSaved={onUpdate}
752
+ />
753
+ )}
754
+
755
+ <Dialog open={confirmDisconnect !== null} onOpenChange={() => setConfirmDisconnect(null)}>
756
+ <DialogContent>
757
+ <DialogHeader>
758
+ <DialogTitle>Disconnect channel?</DialogTitle>
759
+ <DialogDescription>
760
+ This will remove the channel from your bot. You can reconnect it later.
761
+ </DialogDescription>
762
+ </DialogHeader>
763
+ <DialogFooter>
764
+ <Button variant="outline" onClick={() => setConfirmDisconnect(null)}>
765
+ Cancel
766
+ </Button>
767
+ <Button
768
+ variant="destructive"
769
+ onClick={() => confirmDisconnect && handleDisconnect(confirmDisconnect)}
770
+ disabled={disconnecting !== null}
771
+ >
772
+ {disconnecting ? "Disconnecting..." : "Disconnect"}
773
+ </Button>
774
+ </DialogFooter>
775
+ </DialogContent>
776
+ </Dialog>
777
+ </div>
778
+ );
779
+ }
780
+
781
+ // --- Channel status badge ---
782
+
783
+ function ChannelStatusBadge({ status }: { status: string }) {
784
+ const config: Record<string, { label: string; className: string }> = {
785
+ connected: {
786
+ label: "Connected",
787
+ className: "bg-emerald-500/15 text-emerald-500 border-emerald-500/25",
788
+ },
789
+ disconnected: {
790
+ label: "Disconnected",
791
+ className: "bg-zinc-500/15 text-zinc-400 border-zinc-500/25",
792
+ },
793
+ "always-on": {
794
+ label: "Always On",
795
+ className: "bg-blue-500/15 text-blue-500 border-blue-500/25",
796
+ },
797
+ };
798
+ const c = config[status] ?? config.disconnected;
799
+ return (
800
+ <Badge variant="outline" className={c.className}>
801
+ {c.label}
802
+ </Badge>
803
+ );
804
+ }
805
+
806
+ // --- Channel config dialog ---
807
+
808
+ function ConfigureChannelDialog({
809
+ channel,
810
+ botId,
811
+ open,
812
+ onOpenChange,
813
+ onSaved,
814
+ }: {
815
+ channel: BotChannel;
816
+ botId: string;
817
+ open: boolean;
818
+ onOpenChange: (open: boolean) => void;
819
+ onSaved: () => void;
820
+ }) {
821
+ const [config, setConfig] = useState<Record<string, string>>({});
822
+ const [loadingConfig, setLoadingConfig] = useState(false);
823
+ const [saving, setSaving] = useState(false);
824
+ const [error, setError] = useState<string | null>(null);
825
+
826
+ useEffect(() => {
827
+ if (!open) return;
828
+ setLoadingConfig(true);
829
+ setError(null);
830
+ getChannelConfig(botId, channel.id)
831
+ .then(setConfig)
832
+ .catch(() => setError("Failed to load channel configuration."))
833
+ .finally(() => setLoadingConfig(false));
834
+ }, [open, botId, channel.id]);
835
+
836
+ async function handleSave() {
837
+ setSaving(true);
838
+ setError(null);
839
+ try {
840
+ await updateChannelConfig(botId, channel.id, config);
841
+ onSaved();
842
+ onOpenChange(false);
843
+ } catch {
844
+ setError("Failed to save — please try again.");
845
+ } finally {
846
+ setSaving(false);
847
+ }
848
+ }
849
+
850
+ function updateField(key: string, value: string) {
851
+ setConfig((prev) => ({ ...prev, [key]: value }));
852
+ }
853
+
854
+ return (
855
+ <Dialog open={open} onOpenChange={onOpenChange}>
856
+ <DialogContent>
857
+ <DialogHeader>
858
+ <DialogTitle>Configure {channel.type}</DialogTitle>
859
+ <DialogDescription>Settings for {channel.name}</DialogDescription>
860
+ </DialogHeader>
861
+ {loadingConfig ? (
862
+ <p className="text-sm text-muted-foreground">Loading configuration...</p>
863
+ ) : (
864
+ <div className="space-y-4">
865
+ {Object.entries(config).map(([key, value]) => (
866
+ <div key={key} className="space-y-2">
867
+ <Label htmlFor={`channel-cfg-${key}`}>{key}</Label>
868
+ <Input
869
+ id={`channel-cfg-${key}`}
870
+ value={value}
871
+ onChange={(e) => updateField(key, e.target.value)}
872
+ />
873
+ </div>
874
+ ))}
875
+ {Object.keys(config).length === 0 && !error && (
876
+ <p className="text-sm text-muted-foreground">
877
+ No configurable settings for this channel.
878
+ </p>
879
+ )}
880
+ </div>
881
+ )}
882
+ {error && <p className="text-sm text-destructive">{error}</p>}
883
+ <DialogFooter>
884
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
885
+ Cancel
886
+ </Button>
887
+ <Button onClick={handleSave} disabled={saving || loadingConfig}>
888
+ {saving ? "Saving..." : "Save"}
889
+ </Button>
890
+ </DialogFooter>
891
+ </DialogContent>
892
+ </Dialog>
893
+ );
894
+ }
895
+
896
+ // --- Tab 4: Superpowers ---
897
+
898
+ function SuperpowersTab({
899
+ settings,
900
+ botId,
901
+ onUpdate,
902
+ }: {
903
+ settings: BotSettings;
904
+ botId: string;
905
+ onUpdate: () => void;
906
+ }) {
907
+ const [activating, setActivating] = useState<string | null>(null);
908
+ const [activateError, setActivateError] = useState<string | null>(null);
909
+ const [configuringSuperpower, setConfiguringSuperpower] = useState<ActiveSuperpower | null>(null);
910
+
911
+ async function handleActivate(superpowerId: string) {
912
+ setActivating(superpowerId);
913
+ setActivateError(null);
914
+ try {
915
+ await activateSuperpower(botId, superpowerId);
916
+ onUpdate();
917
+ } catch {
918
+ setActivateError("Failed to activate capability \u2014 please try again.");
919
+ } finally {
920
+ setActivating(null);
921
+ }
922
+ }
923
+
924
+ return (
925
+ <div className="space-y-6">
926
+ <div>
927
+ <h2 className="text-xl font-bold">Superpowers</h2>
928
+ <p className="text-sm text-muted-foreground">
929
+ Capabilities that make your bot extraordinary.
930
+ </p>
931
+ </div>
932
+
933
+ {/* Active superpowers */}
934
+ <div className="space-y-3">
935
+ <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">
936
+ Active
937
+ </h3>
938
+ {(settings.activeSuperpowers ?? []).map((sp) => (
939
+ <ActiveSuperpowerCard
940
+ key={sp.id}
941
+ superpower={sp}
942
+ onConfigure={() => setConfiguringSuperpower(sp)}
943
+ />
944
+ ))}
945
+ </div>
946
+
947
+ <Separator />
948
+
949
+ {/* Available superpowers */}
950
+ <div className="space-y-3">
951
+ <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">
952
+ Your {productName()} can do more
953
+ </h3>
954
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
955
+ {(settings.availableSuperpowers ?? []).map((sp) => (
956
+ <AvailableSuperpowerCard
957
+ key={sp.id}
958
+ superpower={sp}
959
+ activating={activating === sp.id}
960
+ onActivate={() => handleActivate(sp.id)}
961
+ />
962
+ ))}
963
+ </div>
964
+ <p className="text-sm text-muted-foreground">One click to activate. Uses your credits.</p>
965
+ {activateError && <p className="text-sm text-destructive">{activateError}</p>}
966
+ </div>
967
+
968
+ {configuringSuperpower && (
969
+ <ConfigureSuperpowerDialog
970
+ superpower={configuringSuperpower}
971
+ botId={botId}
972
+ open={configuringSuperpower !== null}
973
+ onOpenChange={(open) => {
974
+ if (!open) setConfiguringSuperpower(null);
975
+ }}
976
+ onSaved={onUpdate}
977
+ />
978
+ )}
979
+ </div>
980
+ );
981
+ }
982
+
983
+ function ConfigureSuperpowerDialog({
984
+ superpower,
985
+ botId,
986
+ open,
987
+ onOpenChange,
988
+ onSaved,
989
+ }: {
990
+ superpower: ActiveSuperpower;
991
+ botId: string;
992
+ open: boolean;
993
+ onOpenChange: (open: boolean) => void;
994
+ onSaved: () => void;
995
+ }) {
996
+ const [config, setConfig] = useState<Record<string, string>>({});
997
+ const [loadingConfig, setLoadingConfig] = useState(false);
998
+ const [saving, setSaving] = useState(false);
999
+ const [error, setError] = useState<string | null>(null);
1000
+
1001
+ useEffect(() => {
1002
+ if (!open) return;
1003
+ setLoadingConfig(true);
1004
+ setError(null);
1005
+ getSuperpowerConfig(botId, superpower.id)
1006
+ .then(setConfig)
1007
+ .catch(() => setError("Failed to load superpower configuration."))
1008
+ .finally(() => setLoadingConfig(false));
1009
+ }, [open, botId, superpower.id]);
1010
+
1011
+ async function handleSave() {
1012
+ setSaving(true);
1013
+ setError(null);
1014
+ try {
1015
+ await updateSuperpowerConfig(botId, superpower.id, config);
1016
+ onSaved();
1017
+ onOpenChange(false);
1018
+ } catch {
1019
+ setError("Failed to save — please try again.");
1020
+ } finally {
1021
+ setSaving(false);
1022
+ }
1023
+ }
1024
+
1025
+ function updateField(key: string, value: string) {
1026
+ setConfig((prev) => ({ ...prev, [key]: value }));
1027
+ }
1028
+
1029
+ return (
1030
+ <Dialog open={open} onOpenChange={onOpenChange}>
1031
+ <DialogContent>
1032
+ <DialogHeader>
1033
+ <DialogTitle>Configure {superpower.name}</DialogTitle>
1034
+ <DialogDescription>
1035
+ Provider: {superpower.provider} · Model: {superpower.model}
1036
+ </DialogDescription>
1037
+ </DialogHeader>
1038
+ {loadingConfig ? (
1039
+ <p className="text-sm text-muted-foreground">Loading configuration...</p>
1040
+ ) : (
1041
+ <div className="space-y-4">
1042
+ {Object.entries(config).map(([key, value]) => (
1043
+ <div key={key} className="space-y-2">
1044
+ <Label htmlFor={`sp-cfg-${key}`}>{key}</Label>
1045
+ <Input
1046
+ id={`sp-cfg-${key}`}
1047
+ value={value}
1048
+ onChange={(e) => updateField(key, e.target.value)}
1049
+ />
1050
+ </div>
1051
+ ))}
1052
+ {Object.keys(config).length === 0 && !error && (
1053
+ <p className="text-sm text-muted-foreground">
1054
+ No configurable settings for this superpower.
1055
+ </p>
1056
+ )}
1057
+ </div>
1058
+ )}
1059
+ {error && <p className="text-sm text-destructive">{error}</p>}
1060
+ <DialogFooter>
1061
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
1062
+ Cancel
1063
+ </Button>
1064
+ <Button onClick={handleSave} disabled={saving || loadingConfig}>
1065
+ {saving ? "Saving..." : "Save"}
1066
+ </Button>
1067
+ </DialogFooter>
1068
+ </DialogContent>
1069
+ </Dialog>
1070
+ );
1071
+ }
1072
+
1073
+ function ActiveSuperpowerCard({
1074
+ superpower,
1075
+ onConfigure,
1076
+ }: {
1077
+ superpower: ActiveSuperpower;
1078
+ onConfigure: () => void;
1079
+ }) {
1080
+ return (
1081
+ <Card>
1082
+ <CardContent className="flex items-center justify-between p-4">
1083
+ <div className="space-y-1">
1084
+ <div className="flex items-center gap-2">
1085
+ <span className="font-medium">{superpower.name}</span>
1086
+ <Badge variant="outline">{superpower.mode === "hosted" ? "Hosted" : "BYOK"}</Badge>
1087
+ </div>
1088
+ <p className="text-sm text-muted-foreground">
1089
+ {superpower.usageCount} {superpower.usageLabel} &middot;{" "}
1090
+ <CreditDetailed value={superpower.spend} /> spent
1091
+ </p>
1092
+ <p className="text-xs text-muted-foreground">
1093
+ Provider: {superpower.provider} &middot; {superpower.model}
1094
+ </p>
1095
+ </div>
1096
+ <div className="flex gap-2">
1097
+ <Button variant="outline" size="sm" onClick={onConfigure}>
1098
+ Configure
1099
+ </Button>
1100
+ </div>
1101
+ </CardContent>
1102
+ </Card>
1103
+ );
1104
+ }
1105
+
1106
+ function AvailableSuperpowerCard({
1107
+ superpower,
1108
+ activating,
1109
+ onActivate,
1110
+ }: {
1111
+ superpower: AvailableSuperpower;
1112
+ activating: boolean;
1113
+ onActivate: () => void;
1114
+ }) {
1115
+ return (
1116
+ <Card className="flex flex-col justify-between">
1117
+ <CardHeader className="pb-2">
1118
+ <CardTitle className="text-base">{superpower.name}</CardTitle>
1119
+ </CardHeader>
1120
+ <CardContent className="flex flex-1 flex-col justify-between gap-3">
1121
+ <p className="text-sm text-muted-foreground">{superpower.description}</p>
1122
+ <div className="flex items-center justify-between">
1123
+ <span className="text-sm font-medium text-muted-foreground">{superpower.pricing}</span>
1124
+ <Button size="sm" onClick={onActivate} disabled={activating}>
1125
+ {activating ? "Adding..." : "+ Add"}
1126
+ </Button>
1127
+ </div>
1128
+ </CardContent>
1129
+ </Card>
1130
+ );
1131
+ }
1132
+
1133
+ // --- Tab 5: Plugins ---
1134
+
1135
+ function PluginsTab({
1136
+ settings,
1137
+ botId,
1138
+ onUpdate,
1139
+ }: {
1140
+ settings: BotSettings;
1141
+ botId: string;
1142
+ onUpdate: () => void;
1143
+ }) {
1144
+ const [togglingPlugin, setTogglingPlugin] = useState<string | null>(null);
1145
+ const [installingPlugin, setInstallingPlugin] = useState<string | null>(null);
1146
+ const [pluginError, setPluginError] = useState<string | null>(null);
1147
+ const [configuringPlugin, setConfiguringPlugin] = useState<InstalledPlugin | null>(null);
1148
+ const [uninstallingPlugin, setUninstallingPlugin] = useState<string | null>(null);
1149
+ const [confirmUninstall, setConfirmUninstall] = useState<InstalledPlugin | null>(null);
1150
+
1151
+ async function handleUninstall(pluginId: string) {
1152
+ setUninstallingPlugin(pluginId);
1153
+ setPluginError(null);
1154
+ try {
1155
+ await uninstallPlugin(botId, pluginId);
1156
+ onUpdate();
1157
+ } catch (err) {
1158
+ setPluginError(toUserMessage(err, "Failed to uninstall plugin -- please try again."));
1159
+ } finally {
1160
+ setUninstallingPlugin(null);
1161
+ setConfirmUninstall(null);
1162
+ }
1163
+ }
1164
+
1165
+ async function handleToggle(pluginId: string, enabled: boolean) {
1166
+ setTogglingPlugin(pluginId);
1167
+ setPluginError(null);
1168
+ try {
1169
+ await togglePlugin(botId, pluginId, enabled);
1170
+ onUpdate();
1171
+ } catch {
1172
+ setPluginError(`Failed to ${enabled ? "enable" : "disable"} plugin -- please try again.`);
1173
+ } finally {
1174
+ setTogglingPlugin(null);
1175
+ }
1176
+ }
1177
+
1178
+ async function handleInstall(pluginId: string) {
1179
+ setInstallingPlugin(pluginId);
1180
+ setPluginError(null);
1181
+ try {
1182
+ await installPlugin(botId, pluginId);
1183
+ onUpdate();
1184
+ } catch {
1185
+ setPluginError("Failed to install plugin -- please try again.");
1186
+ } finally {
1187
+ setInstallingPlugin(null);
1188
+ }
1189
+ }
1190
+
1191
+ return (
1192
+ <div className="space-y-6">
1193
+ <div>
1194
+ <h2 className="text-xl font-bold">Plugins</h2>
1195
+ <p className="text-sm text-muted-foreground">Installed plugins and plugin discovery.</p>
1196
+ </div>
1197
+
1198
+ {/* Installed */}
1199
+ <div className="space-y-3">
1200
+ <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">
1201
+ Installed
1202
+ </h3>
1203
+ {(settings.installedPlugins ?? []).map((plugin) => (
1204
+ <InstalledPluginCard
1205
+ key={plugin.id}
1206
+ plugin={plugin}
1207
+ onToggle={handleToggle}
1208
+ toggling={togglingPlugin === plugin.id}
1209
+ onConfigure={() => setConfiguringPlugin(plugin)}
1210
+ onUninstall={() => setConfirmUninstall(plugin)}
1211
+ />
1212
+ ))}
1213
+ </div>
1214
+
1215
+ <Separator />
1216
+
1217
+ {/* Discover */}
1218
+ <div className="space-y-3">
1219
+ <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">
1220
+ Discover Plugins
1221
+ </h3>
1222
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
1223
+ {(settings.discoverPlugins ?? []).map((plugin) => (
1224
+ <DiscoverPluginCard
1225
+ key={plugin.id}
1226
+ plugin={plugin}
1227
+ onInstall={handleInstall}
1228
+ installing={installingPlugin === plugin.id}
1229
+ />
1230
+ ))}
1231
+ </div>
1232
+ <Button variant="outline" asChild>
1233
+ <Link href="/marketplace">Browse all plugins</Link>
1234
+ </Button>
1235
+ </div>
1236
+
1237
+ {pluginError && <p className="text-sm text-destructive">{pluginError}</p>}
1238
+
1239
+ {confirmUninstall && (
1240
+ <Dialog
1241
+ open={confirmUninstall !== null}
1242
+ onOpenChange={(open) => {
1243
+ if (!open) setConfirmUninstall(null);
1244
+ }}
1245
+ >
1246
+ <DialogContent>
1247
+ <DialogHeader>
1248
+ <DialogTitle>Uninstall {confirmUninstall.name}?</DialogTitle>
1249
+ <DialogDescription>
1250
+ This will remove the plugin and its configuration from your bot. This action cannot
1251
+ be undone.
1252
+ </DialogDescription>
1253
+ </DialogHeader>
1254
+ <DialogFooter>
1255
+ <Button variant="outline" onClick={() => setConfirmUninstall(null)}>
1256
+ Cancel
1257
+ </Button>
1258
+ <Button
1259
+ variant="destructive"
1260
+ onClick={() => handleUninstall(confirmUninstall.id)}
1261
+ disabled={uninstallingPlugin === confirmUninstall.id}
1262
+ >
1263
+ {uninstallingPlugin === confirmUninstall.id ? (
1264
+ <>
1265
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" /> Uninstalling...
1266
+ </>
1267
+ ) : (
1268
+ "Uninstall"
1269
+ )}
1270
+ </Button>
1271
+ </DialogFooter>
1272
+ </DialogContent>
1273
+ </Dialog>
1274
+ )}
1275
+
1276
+ {configuringPlugin && (
1277
+ <ConfigurePluginDialog
1278
+ plugin={configuringPlugin}
1279
+ botId={botId}
1280
+ open={configuringPlugin !== null}
1281
+ onOpenChange={(open) => {
1282
+ if (!open) setConfiguringPlugin(null);
1283
+ }}
1284
+ onSaved={onUpdate}
1285
+ />
1286
+ )}
1287
+ </div>
1288
+ );
1289
+ }
1290
+
1291
+ function ConfigurePluginDialog({
1292
+ plugin,
1293
+ botId,
1294
+ open,
1295
+ onOpenChange,
1296
+ onSaved,
1297
+ }: {
1298
+ plugin: InstalledPlugin;
1299
+ botId: string;
1300
+ open: boolean;
1301
+ onOpenChange: (open: boolean) => void;
1302
+ onSaved: () => void;
1303
+ }) {
1304
+ const [config, setConfig] = useState<Record<string, string>>({});
1305
+ const [loadingConfig, setLoadingConfig] = useState(false);
1306
+ const [saving, setSaving] = useState(false);
1307
+ const [error, setError] = useState<string | null>(null);
1308
+
1309
+ useEffect(() => {
1310
+ if (!open) return;
1311
+ setLoadingConfig(true);
1312
+ setError(null);
1313
+ getPluginConfig(botId, plugin.id)
1314
+ .then(setConfig)
1315
+ .catch(() => setError("Failed to load plugin configuration."))
1316
+ .finally(() => setLoadingConfig(false));
1317
+ }, [open, botId, plugin.id]);
1318
+
1319
+ async function handleSave() {
1320
+ setSaving(true);
1321
+ setError(null);
1322
+ try {
1323
+ await updatePluginConfig(botId, plugin.id, config);
1324
+ onSaved();
1325
+ onOpenChange(false);
1326
+ } catch {
1327
+ setError("Failed to save — please try again.");
1328
+ } finally {
1329
+ setSaving(false);
1330
+ }
1331
+ }
1332
+
1333
+ function updateField(key: string, value: string) {
1334
+ setConfig((prev) => ({ ...prev, [key]: value }));
1335
+ }
1336
+
1337
+ return (
1338
+ <Dialog open={open} onOpenChange={onOpenChange}>
1339
+ <DialogContent>
1340
+ <DialogHeader>
1341
+ <DialogTitle>Configure {plugin.name}</DialogTitle>
1342
+ <DialogDescription>{plugin.description}</DialogDescription>
1343
+ </DialogHeader>
1344
+ {loadingConfig ? (
1345
+ <p className="text-sm text-muted-foreground">Loading configuration...</p>
1346
+ ) : (
1347
+ <div className="space-y-4">
1348
+ {Object.entries(config).map(([key, value]) => (
1349
+ <div key={key} className="space-y-2">
1350
+ <Label htmlFor={`plugin-cfg-${key}`}>{key}</Label>
1351
+ <Input
1352
+ id={`plugin-cfg-${key}`}
1353
+ value={value}
1354
+ onChange={(e) => updateField(key, e.target.value)}
1355
+ />
1356
+ </div>
1357
+ ))}
1358
+ {Object.keys(config).length === 0 && !error && (
1359
+ <p className="text-sm text-muted-foreground">
1360
+ No configurable settings for this plugin.
1361
+ </p>
1362
+ )}
1363
+ </div>
1364
+ )}
1365
+ {error && <p className="text-sm text-destructive">{error}</p>}
1366
+ <DialogFooter>
1367
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
1368
+ Cancel
1369
+ </Button>
1370
+ <Button onClick={handleSave} disabled={saving || loadingConfig}>
1371
+ {saving ? "Saving..." : "Save"}
1372
+ </Button>
1373
+ </DialogFooter>
1374
+ </DialogContent>
1375
+ </Dialog>
1376
+ );
1377
+ }
1378
+
1379
+ function InstalledPluginCard({
1380
+ plugin,
1381
+ onToggle,
1382
+ toggling,
1383
+ onConfigure,
1384
+ onUninstall,
1385
+ }: {
1386
+ plugin: InstalledPlugin;
1387
+ onToggle: (pluginId: string, enabled: boolean) => void;
1388
+ toggling: boolean;
1389
+ onConfigure: () => void;
1390
+ onUninstall: () => void;
1391
+ }) {
1392
+ return (
1393
+ <Card>
1394
+ <CardContent className="flex items-center justify-between p-4">
1395
+ <div className="space-y-1">
1396
+ <div className="flex items-center gap-2">
1397
+ <span className="font-medium">{plugin.name}</span>
1398
+ <Badge
1399
+ variant="outline"
1400
+ className={PLUGIN_STATUS_STYLES[plugin.status] ?? DEFAULT_STATUS_STYLE}
1401
+ >
1402
+ {plugin.status === "active" ? "Active" : "Disabled"}
1403
+ </Badge>
1404
+ </div>
1405
+ <p className="text-sm text-muted-foreground">{plugin.description}</p>
1406
+ <p className="text-xs text-muted-foreground">Uses: {plugin.capabilities.join(", ")}</p>
1407
+ </div>
1408
+ <div className="flex gap-2">
1409
+ <Button variant="outline" size="sm" onClick={onConfigure}>
1410
+ Configure
1411
+ </Button>
1412
+ <Button
1413
+ variant="ghost"
1414
+ size="icon"
1415
+ onClick={onUninstall}
1416
+ title="Uninstall plugin"
1417
+ aria-label="Uninstall plugin"
1418
+ >
1419
+ <Trash2 className="h-4 w-4 text-destructive" />
1420
+ </Button>
1421
+ <Button
1422
+ variant="ghost"
1423
+ size="sm"
1424
+ onClick={() => onToggle(plugin.id, plugin.status !== "active")}
1425
+ disabled={toggling}
1426
+ >
1427
+ {toggling ? "Updating..." : plugin.status === "active" ? "Disable" : "Enable"}
1428
+ </Button>
1429
+ </div>
1430
+ </CardContent>
1431
+ </Card>
1432
+ );
1433
+ }
1434
+
1435
+ function DiscoverPluginCard({
1436
+ plugin,
1437
+ onInstall,
1438
+ installing,
1439
+ }: {
1440
+ plugin: DiscoverPlugin;
1441
+ onInstall: (pluginId: string) => void;
1442
+ installing: boolean;
1443
+ }) {
1444
+ return (
1445
+ <Card className="flex flex-col justify-between">
1446
+ <CardHeader className="pb-2">
1447
+ <CardTitle className="text-base">{plugin.name}</CardTitle>
1448
+ </CardHeader>
1449
+ <CardContent className="flex flex-1 flex-col justify-between gap-3">
1450
+ <p className="text-sm text-muted-foreground">{plugin.description}</p>
1451
+ {plugin.needs.length > 0 && (
1452
+ <div className="flex flex-wrap gap-1">
1453
+ {plugin.needs.map((n) => (
1454
+ <Badge key={n} variant="secondary" className="text-xs">
1455
+ Needs: {n}
1456
+ </Badge>
1457
+ ))}
1458
+ </div>
1459
+ )}
1460
+ <Button size="sm" onClick={() => onInstall(plugin.id)} disabled={installing}>
1461
+ {installing ? "Installing..." : "Install"}
1462
+ </Button>
1463
+ </CardContent>
1464
+ </Card>
1465
+ );
1466
+ }
1467
+
1468
+ // --- Tab 6: Usage ---
1469
+
1470
+ function UsageTab({ settings }: { settings: BotSettings }) {
1471
+ const { usage } = settings;
1472
+ const denom = usage.totalSpend + usage.creditBalance;
1473
+ const spendPercent = denom > 0 ? (usage.totalSpend / denom) * 100 : 0;
1474
+ const balanceLow = denom > 0 && usage.creditBalance < denom * 0.2;
1475
+
1476
+ return (
1477
+ <div className="max-w-2xl space-y-6">
1478
+ <div>
1479
+ <h2 className="text-xl font-bold">Usage -- {settings.identity.name}</h2>
1480
+ <p className="text-sm text-muted-foreground">Per-bot credit consumption breakdown.</p>
1481
+ </div>
1482
+
1483
+ <Card>
1484
+ <CardContent className="space-y-4 p-4">
1485
+ <div className="text-sm text-muted-foreground">
1486
+ This week: {formatCreditStandard(usage.totalSpend)} of{" "}
1487
+ {formatCreditStandard(usage.creditBalance)} remaining credits
1488
+ </div>
1489
+ <Progress value={spendPercent} className="h-2" />
1490
+ </CardContent>
1491
+ </Card>
1492
+
1493
+ {/* By Capability */}
1494
+ <Card>
1495
+ <CardHeader>
1496
+ <CardTitle>By Capability</CardTitle>
1497
+ </CardHeader>
1498
+ <CardContent className="space-y-3">
1499
+ {(usage?.capabilities ?? []).map((cap) => (
1500
+ <div key={cap.capability} className="flex items-center gap-3">
1501
+ <span className="w-24 text-sm font-medium">{cap.capability}</span>
1502
+ <div className="flex-1">
1503
+ <Progress value={cap.percent} className="h-2" />
1504
+ </div>
1505
+ <span className="w-16 text-right text-sm font-medium min-w-[7rem]">
1506
+ <CreditDetailed value={cap.spend} />
1507
+ </span>
1508
+ <span className="w-12 text-right text-xs text-muted-foreground">{cap.percent}%</span>
1509
+ </div>
1510
+ ))}
1511
+ </CardContent>
1512
+ </Card>
1513
+
1514
+ {/* Trend */}
1515
+ <Card>
1516
+ <CardHeader>
1517
+ <CardTitle>Trend</CardTitle>
1518
+ <CardDescription>Daily spend over the last 14 days</CardDescription>
1519
+ </CardHeader>
1520
+ <CardContent>
1521
+ <div className="flex h-24 items-end gap-1">
1522
+ {(usage?.trend ?? []).map((point) => {
1523
+ const maxSpend = Math.max(0, ...(usage?.trend ?? []).map((p) => p.spend));
1524
+ const height = maxSpend > 0 ? (point.spend / maxSpend) * 100 : 0;
1525
+ return (
1526
+ <div
1527
+ key={point.date}
1528
+ className="flex flex-1 flex-col items-center gap-1"
1529
+ title={`${point.date}: ${formatCreditDetailed(point.spend)}`}
1530
+ >
1531
+ <div
1532
+ className="w-full rounded-t bg-primary/60"
1533
+ style={{ height: `${height}%` }}
1534
+ />
1535
+ <span className="text-[10px] text-muted-foreground">{point.date.slice(-2)}</span>
1536
+ </div>
1537
+ );
1538
+ })}
1539
+ </div>
1540
+ </CardContent>
1541
+ </Card>
1542
+
1543
+ {/* Top-up CTA */}
1544
+ {balanceLow && (
1545
+ <Card className="border-amber-500/25 bg-amber-500/5">
1546
+ <CardContent className="flex items-center justify-between p-4">
1547
+ <span className="text-sm font-medium">Running low on credits?</span>
1548
+ <div className="flex gap-2">
1549
+ <Button size="sm" variant="outline" asChild>
1550
+ <Link href="/billing/credits">Top up -- $10</Link>
1551
+ </Button>
1552
+ <Button size="sm" variant="outline" asChild>
1553
+ <Link href="/billing/credits">$25</Link>
1554
+ </Button>
1555
+ <Button size="sm" asChild>
1556
+ <Link href="/billing/credits">$50</Link>
1557
+ </Button>
1558
+ </div>
1559
+ </CardContent>
1560
+ </Card>
1561
+ )}
1562
+ </div>
1563
+ );
1564
+ }
1565
+
1566
+ // --- Tab 7: Danger Zone ---
1567
+
1568
+ function DangerZoneTab({
1569
+ settings,
1570
+ botId,
1571
+ onUpdate,
1572
+ }: {
1573
+ settings: BotSettings;
1574
+ botId: string;
1575
+ onUpdate: () => void;
1576
+ }) {
1577
+ const router = useRouter();
1578
+ const [confirmAction, setConfirmAction] = useState<"stop" | "archive" | "delete" | null>(null);
1579
+ const [confirmText, setConfirmText] = useState("");
1580
+ const [acting, setActing] = useState(false);
1581
+ const [actionError, setActionError] = useState<string | null>(null);
1582
+
1583
+ const botName = settings.identity.name;
1584
+
1585
+ function handleDialogClose() {
1586
+ setConfirmAction(null);
1587
+ setActionError(null);
1588
+ }
1589
+
1590
+ async function handleConfirm() {
1591
+ if (!confirmAction) return;
1592
+ if (confirmAction === "delete" && confirmText !== botName) return;
1593
+ setActing(true);
1594
+ setActionError(null);
1595
+ try {
1596
+ await controlBot(botId, confirmAction);
1597
+ if (confirmAction === "delete" || confirmAction === "archive") {
1598
+ router.push("/dashboard");
1599
+ return;
1600
+ }
1601
+ setConfirmAction(null);
1602
+ setConfirmText("");
1603
+ onUpdate();
1604
+ } catch {
1605
+ setActionError(`Failed to ${confirmAction} bot \u2014 please try again.`);
1606
+ } finally {
1607
+ setActing(false);
1608
+ }
1609
+ }
1610
+
1611
+ const actions = [
1612
+ {
1613
+ key: "stop" as const,
1614
+ label: `Stop ${botName}`,
1615
+ description: "Pause this bot. Channels go offline. Can restart anytime.",
1616
+ variant: "outline" as const,
1617
+ },
1618
+ {
1619
+ key: "archive" as const,
1620
+ label: `Archive ${botName}`,
1621
+ description: "Remove from fleet. Keeps config and memories for 30 days. Can restore.",
1622
+ variant: "outline" as const,
1623
+ },
1624
+ {
1625
+ key: "delete" as const,
1626
+ label: `Delete ${botName}`,
1627
+ description: "Permanent. All data destroyed. This cannot be undone.",
1628
+ variant: "destructive" as const,
1629
+ },
1630
+ ];
1631
+
1632
+ return (
1633
+ <div className="max-w-2xl space-y-6">
1634
+ <div>
1635
+ <h2 className="text-xl font-bold text-destructive">Danger Zone</h2>
1636
+ <p className="text-sm text-muted-foreground">
1637
+ Irreversible and destructive actions. Proceed with caution.
1638
+ </p>
1639
+ </div>
1640
+
1641
+ <div className="space-y-4">
1642
+ {actions.map((action) => (
1643
+ <Card key={action.key} className={action.key === "delete" ? "border-destructive/50" : ""}>
1644
+ <CardContent className="flex items-center justify-between p-4">
1645
+ <div className="space-y-1">
1646
+ <p className="text-sm font-medium">{action.description}</p>
1647
+ </div>
1648
+ <Button variant={action.variant} onClick={() => setConfirmAction(action.key)}>
1649
+ {action.label}
1650
+ </Button>
1651
+ </CardContent>
1652
+ </Card>
1653
+ ))}
1654
+ </div>
1655
+
1656
+ {/* Confirmation dialog */}
1657
+ <Dialog open={confirmAction !== null} onOpenChange={handleDialogClose}>
1658
+ <DialogContent>
1659
+ <DialogHeader>
1660
+ <DialogTitle>
1661
+ {confirmAction === "stop" && `Stop ${botName}?`}
1662
+ {confirmAction === "archive" && `Archive ${botName}?`}
1663
+ {confirmAction === "delete" && `Delete ${botName} permanently?`}
1664
+ </DialogTitle>
1665
+ <DialogDescription>
1666
+ {confirmAction === "stop" &&
1667
+ "This will pause your bot. All channels will go offline. You can restart it at any time."}
1668
+ {confirmAction === "archive" &&
1669
+ "This will remove your bot from the fleet. Config and memories are kept for 30 days. You can restore it within that window."}
1670
+ {confirmAction === "delete" && (
1671
+ <>
1672
+ This action is permanent and cannot be undone. All data, memories, and
1673
+ configuration will be destroyed. Type{" "}
1674
+ <strong className="text-foreground">{botName}</strong> to confirm.
1675
+ </>
1676
+ )}
1677
+ </DialogDescription>
1678
+ </DialogHeader>
1679
+
1680
+ {actionError && <p className="text-sm text-destructive">{actionError}</p>}
1681
+
1682
+ {confirmAction === "delete" && (
1683
+ <Input
1684
+ placeholder={`Type "${botName}" to confirm`}
1685
+ value={confirmText}
1686
+ onChange={(e) => setConfirmText(e.target.value)}
1687
+ />
1688
+ )}
1689
+
1690
+ <DialogFooter>
1691
+ <Button variant="outline" onClick={handleDialogClose}>
1692
+ Cancel
1693
+ </Button>
1694
+ <Button
1695
+ variant="destructive"
1696
+ onClick={handleConfirm}
1697
+ disabled={acting || (confirmAction === "delete" && confirmText !== botName)}
1698
+ >
1699
+ {acting
1700
+ ? "Processing..."
1701
+ : confirmAction === "stop"
1702
+ ? "Stop bot"
1703
+ : confirmAction === "archive"
1704
+ ? "Archive bot"
1705
+ : "Delete permanently"}
1706
+ </Button>
1707
+ </DialogFooter>
1708
+ </DialogContent>
1709
+ </Dialog>
1710
+ </div>
1711
+ );
1712
+ }