@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.
- package/.env.paperclip +18 -0
- package/.env.wopr +18 -0
- package/README.md +36 -0
- package/biome.json +52 -0
- package/next.config.ts +45 -0
- package/package.json +84 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/window.svg +1 -0
- package/src/__tests__/__snapshots__/layout-snapshots.test.tsx.snap +741 -0
- package/src/__tests__/account-page-redirect.test.tsx +73 -0
- package/src/__tests__/account-switcher.test.tsx +85 -0
- package/src/__tests__/activity-page.test.tsx +176 -0
- package/src/__tests__/add-payment-method-dialog.test.tsx +160 -0
- package/src/__tests__/admin-api.test.ts +244 -0
- package/src/__tests__/admin-gpu-api.test.ts +188 -0
- package/src/__tests__/admin-guard.test.tsx +79 -0
- package/src/__tests__/admin-marketplace-api.test.ts +179 -0
- package/src/__tests__/admin-middleware.test.ts +157 -0
- package/src/__tests__/admin-tenant-table.test.tsx +95 -0
- package/src/__tests__/affiliate-dashboard.test.tsx +178 -0
- package/src/__tests__/api-401-redirect.test.ts +78 -0
- package/src/__tests__/api-client.test.ts +316 -0
- package/src/__tests__/api-config.test.ts +89 -0
- package/src/__tests__/api-control-instance.test.ts +69 -0
- package/src/__tests__/api-fleet-resources.test.ts +52 -0
- package/src/__tests__/api-fleet-trpc.test.ts +252 -0
- package/src/__tests__/api-get-instance-config.test.ts +41 -0
- package/src/__tests__/api-null-guards.test.ts +244 -0
- package/src/__tests__/api-rename-instance.test.ts +60 -0
- package/src/__tests__/api-update-instance-config.test.ts +60 -0
- package/src/__tests__/audit-log-table-pagination.test.tsx +136 -0
- package/src/__tests__/auth-client.test.ts +87 -0
- package/src/__tests__/auth-password-reset.test.tsx +435 -0
- package/src/__tests__/auth-redirect.test.tsx +60 -0
- package/src/__tests__/auth.test.tsx +269 -0
- package/src/__tests__/auto-topup-card.test.tsx +257 -0
- package/src/__tests__/backups-tab.test.tsx +221 -0
- package/src/__tests__/billing-byok-callout.test.tsx +76 -0
- package/src/__tests__/billing-layout-nav-hidden.test.tsx +47 -0
- package/src/__tests__/billing-payment-org-invoices.test.tsx +123 -0
- package/src/__tests__/billing.test.tsx +509 -0
- package/src/__tests__/bot-settings/resources-tab.test.tsx +119 -0
- package/src/__tests__/bot-settings/storage-tab.test.tsx +80 -0
- package/src/__tests__/bot-settings/vps-info-panel.test.tsx +108 -0
- package/src/__tests__/bot-settings/vps-upgrade-card.test.tsx +52 -0
- package/src/__tests__/bot-settings-data-control.test.ts +49 -0
- package/src/__tests__/bot-settings-restart.test.tsx +149 -0
- package/src/__tests__/bot-settings.test.tsx +678 -0
- package/src/__tests__/brand.test.ts +335 -0
- package/src/__tests__/buy-credits-panel.test.tsx +249 -0
- package/src/__tests__/buy-crypto-credits-panel.test.tsx +178 -0
- package/src/__tests__/capability-conflicts.test.ts +88 -0
- package/src/__tests__/capability-resolver.test.tsx +173 -0
- package/src/__tests__/changeset-detail.test.tsx +156 -0
- package/src/__tests__/channel-setup-logger.test.ts +22 -0
- package/src/__tests__/channel-setup-toast.test.tsx +60 -0
- package/src/__tests__/channel-wizard.test.tsx +505 -0
- package/src/__tests__/chat/ambient-dot.test.tsx +35 -0
- package/src/__tests__/chat/chat-input.test.tsx +78 -0
- package/src/__tests__/chat/chat-message.test.tsx +45 -0
- package/src/__tests__/chat/chat-panel.test.tsx +111 -0
- package/src/__tests__/chat/chat-widget.test.tsx +82 -0
- package/src/__tests__/chat-store.test.ts +87 -0
- package/src/__tests__/command-center.test.tsx +246 -0
- package/src/__tests__/compliance-retention-edit.test.tsx +134 -0
- package/src/__tests__/coupon-input.test.tsx +119 -0
- package/src/__tests__/create-instance.test.tsx +96 -0
- package/src/__tests__/create-org-wizard.test.tsx +200 -0
- package/src/__tests__/credit-balance.test.tsx +103 -0
- package/src/__tests__/credits.test.tsx +376 -0
- package/src/__tests__/csrf-middleware.test.ts +198 -0
- package/src/__tests__/degraded-state-banner.test.tsx +130 -0
- package/src/__tests__/dividend-calculator.test.tsx +20 -0
- package/src/__tests__/dividend-stats.test.tsx +64 -0
- package/src/__tests__/dividend.test.tsx +169 -0
- package/src/__tests__/dockerfile.test.ts +110 -0
- package/src/__tests__/email-verification-banner.test.tsx +64 -0
- package/src/__tests__/env-example.test.ts +25 -0
- package/src/__tests__/error-boundaries.test.tsx +64 -0
- package/src/__tests__/fetch-pricing.test.ts +121 -0
- package/src/__tests__/field-oauth.test.tsx +302 -0
- package/src/__tests__/fixtures/mock-manifests-data.js +372 -0
- package/src/__tests__/fixtures/mock-manifests.ts +24 -0
- package/src/__tests__/fleet-health-timestamp.test.tsx +101 -0
- package/src/__tests__/fleet-health-update.test.tsx +83 -0
- package/src/__tests__/format-credit.test.ts +58 -0
- package/src/__tests__/gpu-dashboard.test.tsx +236 -0
- package/src/__tests__/hosted-usage-date-range.test.tsx +54 -0
- package/src/__tests__/instance-detail.test.tsx +571 -0
- package/src/__tests__/instance-list.test.tsx +230 -0
- package/src/__tests__/landing-hero.test.tsx +27 -0
- package/src/__tests__/landing-nav.test.tsx +24 -0
- package/src/__tests__/layout-snapshots.test.tsx +167 -0
- package/src/__tests__/logger.test.ts +54 -0
- package/src/__tests__/login-page-redirect.test.tsx +142 -0
- package/src/__tests__/manifest-validation.test.ts +126 -0
- package/src/__tests__/marketplace-admin.test.tsx +151 -0
- package/src/__tests__/marketplace.test.tsx +609 -0
- package/src/__tests__/merge-api-rates.test.ts +70 -0
- package/src/__tests__/middleware.test.ts +690 -0
- package/src/__tests__/network-page.test.tsx +100 -0
- package/src/__tests__/next-config-headers.test.ts +28 -0
- package/src/__tests__/not-found.test.tsx +26 -0
- package/src/__tests__/notifications.test.tsx +128 -0
- package/src/__tests__/oauth-buttons.test.tsx +101 -0
- package/src/__tests__/oauth-error-mapping.test.tsx +97 -0
- package/src/__tests__/observability.test.tsx +541 -0
- package/src/__tests__/onboarding-data.test.ts +363 -0
- package/src/__tests__/onboarding-page.test.tsx +113 -0
- package/src/__tests__/onboarding-store.test.ts +121 -0
- package/src/__tests__/org-billing-api.test.tsx +70 -0
- package/src/__tests__/org-billing-null-guards.test.ts +64 -0
- package/src/__tests__/org-billing-page.test.tsx +124 -0
- package/src/__tests__/plugin-definition.test.ts +43 -0
- package/src/__tests__/plugin-install-flow.test.tsx +535 -0
- package/src/__tests__/plugin-registry.test.tsx +475 -0
- package/src/__tests__/plugin-setup/setup-chat-panel.test.ts +142 -0
- package/src/__tests__/plugin-setup/use-plugin-setup-chat.test.ts +49 -0
- package/src/__tests__/plugin-tool-definitions.test.ts +51 -0
- package/src/__tests__/plugin-tool-sync.test.ts +59 -0
- package/src/__tests__/portfolio-chart.test.tsx +24 -0
- package/src/__tests__/pricing.test.tsx +107 -0
- package/src/__tests__/promotion-form.test.tsx +180 -0
- package/src/__tests__/promotions-list.test.tsx +194 -0
- package/src/__tests__/provider-key-api.test.ts +134 -0
- package/src/__tests__/resend-verification-button.test.tsx +104 -0
- package/src/__tests__/sanitize-redirect-url.test.ts +47 -0
- package/src/__tests__/secrets-audit-pagination.test.tsx +139 -0
- package/src/__tests__/settings.test.tsx +937 -0
- package/src/__tests__/setup-checklist.test.tsx +274 -0
- package/src/__tests__/setup.ts +82 -0
- package/src/__tests__/smoke.test.tsx +10 -0
- package/src/__tests__/snapshot-api.test.ts +104 -0
- package/src/__tests__/status-api.test.ts +46 -0
- package/src/__tests__/status-badge.test.tsx +33 -0
- package/src/__tests__/status-colors.test.ts +83 -0
- package/src/__tests__/status-page.test.tsx +86 -0
- package/src/__tests__/step-superpowers.test.tsx +218 -0
- package/src/__tests__/story-sections.test.tsx +24 -0
- package/src/__tests__/superpower-content-sanitize.test.tsx +87 -0
- package/src/__tests__/superpower-content.test.tsx +44 -0
- package/src/__tests__/suspension-banner.test.tsx +140 -0
- package/src/__tests__/tenant-context.test.tsx +146 -0
- package/src/__tests__/tenant-keys-api.test.ts +114 -0
- package/src/__tests__/tenant-table-pagination.test.tsx +124 -0
- package/src/__tests__/terminal-log-cleanup.test.tsx +51 -0
- package/src/__tests__/terminal-sequence.test.tsx +28 -0
- package/src/__tests__/transaction-history.test.tsx +325 -0
- package/src/__tests__/trpc-types.test.ts +102 -0
- package/src/__tests__/use-capability-meta.test.ts +161 -0
- package/src/__tests__/use-chat.test.ts +616 -0
- package/src/__tests__/use-has-org.test.ts +44 -0
- package/src/__tests__/use-image-status.test.ts +77 -0
- package/src/__tests__/use-pagination-params.test.ts +88 -0
- package/src/__tests__/use-plugin-setup-chat-stale-closure.test.ts +53 -0
- package/src/__tests__/use-webmcp.test.ts +119 -0
- package/src/__tests__/validate-elevenlabs-key.test.ts +95 -0
- package/src/__tests__/validate-redirect-url.test.ts +61 -0
- package/src/__tests__/verify-page.test.tsx +140 -0
- package/src/__tests__/verify-redirect.test.tsx +41 -0
- package/src/__tests__/verify-result-banner.test.tsx +66 -0
- package/src/__tests__/webmcp-feature-detect.test.ts +54 -0
- package/src/__tests__/webmcp-hook.test.tsx +72 -0
- package/src/__tests__/webmcp-marketplace-onboarding-tools.test.ts +185 -0
- package/src/__tests__/webmcp-register.test.ts +103 -0
- package/src/__tests__/webmcp-set-provider.test.ts +47 -0
- package/src/__tests__/webmcp-tools.test.ts +348 -0
- package/src/app/(auth)/error.tsx +72 -0
- package/src/app/(auth)/forgot-password/page.tsx +137 -0
- package/src/app/(auth)/layout.tsx +14 -0
- package/src/app/(auth)/loading.tsx +26 -0
- package/src/app/(auth)/login/page.tsx +188 -0
- package/src/app/(auth)/reset-password/page.tsx +169 -0
- package/src/app/(auth)/signup/page.tsx +309 -0
- package/src/app/(dashboard)/billing/credits/page.tsx +209 -0
- package/src/app/(dashboard)/billing/error.tsx +72 -0
- package/src/app/(dashboard)/billing/layout.tsx +73 -0
- package/src/app/(dashboard)/billing/loading.tsx +41 -0
- package/src/app/(dashboard)/billing/payment/page.tsx +639 -0
- package/src/app/(dashboard)/billing/plans/page.tsx +58 -0
- package/src/app/(dashboard)/billing/referrals/page.tsx +7 -0
- package/src/app/(dashboard)/billing/usage/hosted/page.tsx +348 -0
- package/src/app/(dashboard)/billing/usage/page.tsx +663 -0
- package/src/app/(dashboard)/changesets/[id]/changeset-detail-client.tsx +400 -0
- package/src/app/(dashboard)/changesets/[id]/error.tsx +57 -0
- package/src/app/(dashboard)/changesets/[id]/loading.tsx +23 -0
- package/src/app/(dashboard)/changesets/[id]/page.tsx +10 -0
- package/src/app/(dashboard)/changesets/error.tsx +72 -0
- package/src/app/(dashboard)/changesets/page.tsx +10 -0
- package/src/app/(dashboard)/chat/page.tsx +74 -0
- package/src/app/(dashboard)/dashboard/bots/[id]/settings/page.tsx +10 -0
- package/src/app/(dashboard)/dashboard/network/page.tsx +97 -0
- package/src/app/(dashboard)/dashboard/page.tsx +13 -0
- package/src/app/(dashboard)/error.tsx +72 -0
- package/src/app/(dashboard)/layout.tsx +113 -0
- package/src/app/(dashboard)/loading.tsx +27 -0
- package/src/app/(dashboard)/marketplace/[plugin]/page.tsx +548 -0
- package/src/app/(dashboard)/marketplace/error.tsx +72 -0
- package/src/app/(dashboard)/marketplace/loading.tsx +27 -0
- package/src/app/(dashboard)/marketplace/page.tsx +268 -0
- package/src/app/(dashboard)/not-found.tsx +46 -0
- package/src/app/(dashboard)/onboarding/page.tsx +267 -0
- package/src/app/(dashboard)/settings/account/page.tsx +132 -0
- package/src/app/(dashboard)/settings/activity/page.tsx +280 -0
- package/src/app/(dashboard)/settings/api-keys/page.tsx +530 -0
- package/src/app/(dashboard)/settings/brain/page.tsx +412 -0
- package/src/app/(dashboard)/settings/error.tsx +72 -0
- package/src/app/(dashboard)/settings/layout.tsx +114 -0
- package/src/app/(dashboard)/settings/loading.tsx +31 -0
- package/src/app/(dashboard)/settings/notifications/page.tsx +216 -0
- package/src/app/(dashboard)/settings/org/page.tsx +617 -0
- package/src/app/(dashboard)/settings/profile/page.tsx +510 -0
- package/src/app/(dashboard)/settings/providers/page.tsx +842 -0
- package/src/app/(dashboard)/settings/secrets/page.tsx +658 -0
- package/src/app/(dashboard)/settings/security/page.tsx +1133 -0
- package/src/app/admin/accounting/loading.tsx +32 -0
- package/src/app/admin/accounting/page.tsx +5 -0
- package/src/app/admin/affiliates/loading.tsx +32 -0
- package/src/app/admin/affiliates/page.tsx +5 -0
- package/src/app/admin/audit/loading.tsx +32 -0
- package/src/app/admin/audit/page.tsx +5 -0
- package/src/app/admin/billing-health/loading.tsx +17 -0
- package/src/app/admin/billing-health/page.tsx +10 -0
- package/src/app/admin/compliance/page.tsx +5 -0
- package/src/app/admin/error.tsx +72 -0
- package/src/app/admin/gpu/loading.tsx +38 -0
- package/src/app/admin/gpu/page.tsx +5 -0
- package/src/app/admin/incidents/page.tsx +10 -0
- package/src/app/admin/inference/loading.tsx +32 -0
- package/src/app/admin/inference/page.tsx +5 -0
- package/src/app/admin/layout.tsx +44 -0
- package/src/app/admin/loading.tsx +32 -0
- package/src/app/admin/marketplace/loading.tsx +32 -0
- package/src/app/admin/marketplace/page.tsx +5 -0
- package/src/app/admin/migrations/loading.tsx +22 -0
- package/src/app/admin/migrations/page.tsx +5 -0
- package/src/app/admin/onboarding/loading.tsx +18 -0
- package/src/app/admin/onboarding/page.tsx +5 -0
- package/src/app/admin/promotions/[id]/edit/loading.tsx +16 -0
- package/src/app/admin/promotions/[id]/edit/page.tsx +56 -0
- package/src/app/admin/promotions/[id]/loading.tsx +15 -0
- package/src/app/admin/promotions/[id]/page.tsx +311 -0
- package/src/app/admin/promotions/loading.tsx +21 -0
- package/src/app/admin/promotions/new/loading.tsx +16 -0
- package/src/app/admin/promotions/new/page.tsx +12 -0
- package/src/app/admin/promotions/page.tsx +266 -0
- package/src/app/admin/rate-overrides/loading.tsx +17 -0
- package/src/app/admin/rate-overrides/page.tsx +290 -0
- package/src/app/admin/roles/loading.tsx +27 -0
- package/src/app/admin/roles/page.tsx +5 -0
- package/src/app/admin/tenants/loading.tsx +32 -0
- package/src/app/admin/tenants/page.tsx +5 -0
- package/src/app/apple-icon.tsx +32 -0
- package/src/app/auth/callback/[provider]/page.tsx +104 -0
- package/src/app/auth/verify/page.tsx +224 -0
- package/src/app/channels/error.tsx +72 -0
- package/src/app/channels/loading.tsx +29 -0
- package/src/app/channels/page.tsx +262 -0
- package/src/app/channels/setup/[plugin]/page.tsx +136 -0
- package/src/app/error.tsx +72 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/fleet/error.tsx +72 -0
- package/src/app/fleet/health/page.tsx +9 -0
- package/src/app/fleet/layout.tsx +14 -0
- package/src/app/fleet/loading.tsx +33 -0
- package/src/app/fleet/page.tsx +5 -0
- package/src/app/global-error.tsx +96 -0
- package/src/app/globals.css +251 -0
- package/src/app/icon.svg +4 -0
- package/src/app/instances/[id]/instance-detail-client.tsx +1298 -0
- package/src/app/instances/[id]/page.tsx +10 -0
- package/src/app/instances/error.tsx +72 -0
- package/src/app/instances/instance-list-client.tsx +540 -0
- package/src/app/instances/loading.tsx +33 -0
- package/src/app/instances/new/create-instance-client.tsx +377 -0
- package/src/app/instances/new/page.tsx +9 -0
- package/src/app/instances/page.tsx +9 -0
- package/src/app/layout.tsx +83 -0
- package/src/app/not-found.tsx +38 -0
- package/src/app/og/route.tsx +50 -0
- package/src/app/page.tsx +39 -0
- package/src/app/plugins/error.tsx +72 -0
- package/src/app/plugins/layout.tsx +14 -0
- package/src/app/plugins/loading.tsx +30 -0
- package/src/app/plugins/page.tsx +555 -0
- package/src/app/pricing/error.tsx +72 -0
- package/src/app/pricing/loading.tsx +25 -0
- package/src/app/pricing/page.tsx +20 -0
- package/src/app/privacy/page.tsx +406 -0
- package/src/app/robots.ts +9 -0
- package/src/app/sitemap.ts +11 -0
- package/src/app/status/error.tsx +72 -0
- package/src/app/status/loading.tsx +21 -0
- package/src/app/status/page.tsx +20 -0
- package/src/app/terms/page.tsx +414 -0
- package/src/components/account-switcher.tsx +82 -0
- package/src/components/admin/accounting-dashboard.tsx +190 -0
- package/src/components/admin/admin-guard.tsx +36 -0
- package/src/components/admin/admin-nav.tsx +71 -0
- package/src/components/admin/affiliate-dashboard.tsx +564 -0
- package/src/components/admin/audit-log-table.tsx +336 -0
- package/src/components/admin/billing-health-dashboard.test.tsx +40 -0
- package/src/components/admin/billing-health-dashboard.tsx +416 -0
- package/src/components/admin/bulk-actions-bar.test.tsx +92 -0
- package/src/components/admin/bulk-actions-bar.tsx +80 -0
- package/src/components/admin/bulk-export-dialog.test.tsx +75 -0
- package/src/components/admin/bulk-export-dialog.tsx +189 -0
- package/src/components/admin/bulk-grant-dialog.test.tsx +81 -0
- package/src/components/admin/bulk-grant-dialog.tsx +147 -0
- package/src/components/admin/bulk-preview-dialog.test.tsx +72 -0
- package/src/components/admin/bulk-preview-dialog.tsx +106 -0
- package/src/components/admin/bulk-reactivate-dialog.test.tsx +51 -0
- package/src/components/admin/bulk-reactivate-dialog.tsx +55 -0
- package/src/components/admin/bulk-select-all-banner.test.tsx +36 -0
- package/src/components/admin/bulk-select-all-banner.tsx +44 -0
- package/src/components/admin/bulk-suspend-dialog.test.tsx +77 -0
- package/src/components/admin/bulk-suspend-dialog.tsx +129 -0
- package/src/components/admin/bulk-undo-toast.test.tsx +66 -0
- package/src/components/admin/bulk-undo-toast.tsx +121 -0
- package/src/components/admin/compliance-dashboard.tsx +1341 -0
- package/src/components/admin/gpu-dashboard.tsx +552 -0
- package/src/components/admin/grant-credits-dialog.tsx +121 -0
- package/src/components/admin/incident-dashboard.test.tsx +44 -0
- package/src/components/admin/incident-dashboard.tsx +717 -0
- package/src/components/admin/inference-dashboard.tsx +415 -0
- package/src/components/admin/marketplace-admin.tsx +765 -0
- package/src/components/admin/migrations-dashboard.tsx +404 -0
- package/src/components/admin/onboarding-dashboard.tsx +422 -0
- package/src/components/admin/promotions/promotion-form.tsx +440 -0
- package/src/components/admin/roles-dashboard.tsx +278 -0
- package/src/components/admin/suspend-dialog.tsx +98 -0
- package/src/components/admin/tenant-notes-panel.tsx +134 -0
- package/src/components/admin/tenant-row-actions.tsx +78 -0
- package/src/components/admin/tenant-table.tsx +339 -0
- package/src/components/auth/auth-error.tsx +22 -0
- package/src/components/auth/auth-redirect.tsx +18 -0
- package/src/components/auth/auth-shell.tsx +25 -0
- package/src/components/auth/email-verification-banner.tsx +25 -0
- package/src/components/auth/email-verification-result-banner.tsx +70 -0
- package/src/components/auth/resend-verification-button.tsx +94 -0
- package/src/components/auth/wopr-wordmark.tsx +19 -0
- package/src/components/billing/add-payment-method-dialog.tsx +267 -0
- package/src/components/billing/affiliate-dashboard.tsx +300 -0
- package/src/components/billing/auto-topup-card.tsx +432 -0
- package/src/components/billing/buy-credits-panel.tsx +180 -0
- package/src/components/billing/buy-crypto-credits-panel.tsx +96 -0
- package/src/components/billing/byok-callout.tsx +87 -0
- package/src/components/billing/coupon-input.tsx +86 -0
- package/src/components/billing/credit-balance.tsx +79 -0
- package/src/components/billing/degraded-state-banner.tsx +95 -0
- package/src/components/billing/dividend-banner.tsx +97 -0
- package/src/components/billing/dividend-eligibility.tsx +86 -0
- package/src/components/billing/dividend-pool-stats.tsx +86 -0
- package/src/components/billing/first-dividend-dialog.tsx +109 -0
- package/src/components/billing/low-balance-banner.tsx +50 -0
- package/src/components/billing/org-billing-page.tsx +360 -0
- package/src/components/billing/suspension-banner.tsx +53 -0
- package/src/components/billing/transaction-history.tsx +239 -0
- package/src/components/bot-settings/__tests__/bot-settings-client.test.tsx +205 -0
- package/src/components/bot-settings/backups-tab.tsx +377 -0
- package/src/components/bot-settings/bot-settings-client.tsx +1712 -0
- package/src/components/bot-settings/resources-tab.tsx +203 -0
- package/src/components/bot-settings/storage-tab.tsx +248 -0
- package/src/components/bot-settings/vps-info-panel.tsx +132 -0
- package/src/components/bot-settings/vps-upgrade-card.tsx +110 -0
- package/src/components/capability/CapabilityResolver.tsx +113 -0
- package/src/components/channel-wizard/field-interactive.tsx +48 -0
- package/src/components/channel-wizard/field-oauth.tsx +181 -0
- package/src/components/channel-wizard/field-paste.tsx +47 -0
- package/src/components/channel-wizard/field-qr.tsx +302 -0
- package/src/components/channel-wizard/index.ts +6 -0
- package/src/components/channel-wizard/step-renderer.tsx +103 -0
- package/src/components/channel-wizard/wizard.tsx +200 -0
- package/src/components/chat/ambient-dot.tsx +32 -0
- package/src/components/chat/chat-input.tsx +56 -0
- package/src/components/chat/chat-message.tsx +36 -0
- package/src/components/chat/chat-panel.tsx +138 -0
- package/src/components/chat/chat-widget.tsx +41 -0
- package/src/components/chat/index.ts +5 -0
- package/src/components/dashboard/command-center.tsx +614 -0
- package/src/components/instances/friends-tab.test.tsx +265 -0
- package/src/components/instances/friends-tab.tsx +721 -0
- package/src/components/landing/hero.tsx +53 -0
- package/src/components/landing/landing-nav.tsx +21 -0
- package/src/components/landing/landing-page.tsx +71 -0
- package/src/components/landing/portfolio-chart.tsx +349 -0
- package/src/components/landing/story-sections.tsx +50 -0
- package/src/components/landing/terminal-lines.ts +99 -0
- package/src/components/landing/terminal-sequence.tsx +453 -0
- package/src/components/landing/typing-effect.tsx +43 -0
- package/src/components/marketplace/category-filter.tsx +61 -0
- package/src/components/marketplace/empty-state.tsx +61 -0
- package/src/components/marketplace/featured-heroes.tsx +84 -0
- package/src/components/marketplace/first-visit-hero.tsx +110 -0
- package/src/components/marketplace/index.ts +9 -0
- package/src/components/marketplace/install-wizard.tsx +782 -0
- package/src/components/marketplace/marketplace-tabs.tsx +54 -0
- package/src/components/marketplace/plugin-card.tsx +129 -0
- package/src/components/marketplace/superpower-card.tsx +104 -0
- package/src/components/marketplace/superpower-content.tsx +117 -0
- package/src/components/marketplace/terminal-search.tsx +67 -0
- package/src/components/oauth-buttons.tsx +75 -0
- package/src/components/observability/fleet-health.tsx +370 -0
- package/src/components/observability/health-overview.tsx +246 -0
- package/src/components/observability/logs-viewer.tsx +215 -0
- package/src/components/observability/metrics-dashboard.tsx +288 -0
- package/src/components/onboarding/fallback-setup.tsx +137 -0
- package/src/components/onboarding/index.ts +3 -0
- package/src/components/onboarding/setup-checklist.tsx +333 -0
- package/src/components/onboarding/step-superpowers.tsx +122 -0
- package/src/components/plugin-setup/index.ts +1 -0
- package/src/components/plugin-setup/setup-chat-panel.tsx +188 -0
- package/src/components/pricing/dividend-calculator.tsx +47 -0
- package/src/components/pricing/dividend-stats.tsx +117 -0
- package/src/components/pricing/pricing-page.tsx +229 -0
- package/src/components/settings/create-org-wizard.tsx +225 -0
- package/src/components/sidebar.tsx +202 -0
- package/src/components/status/status-page.tsx +209 -0
- package/src/components/status-badge.tsx +28 -0
- package/src/components/theme-provider.tsx +8 -0
- package/src/components/ui/alert-dialog.tsx +141 -0
- package/src/components/ui/badge.tsx +47 -0
- package/src/components/ui/banner.tsx +36 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +75 -0
- package/src/components/ui/checkbox.tsx +52 -0
- package/src/components/ui/collapsible.tsx +31 -0
- package/src/components/ui/credit-detailed.tsx +33 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/dropdown-menu.tsx +228 -0
- package/src/components/ui/form.tsx +151 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +21 -0
- package/src/components/ui/popover.tsx +74 -0
- package/src/components/ui/progress.tsx +28 -0
- package/src/components/ui/radio-group.tsx +45 -0
- package/src/components/ui/select.tsx +175 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +125 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/switch.tsx +35 -0
- package/src/components/ui/table.tsx +92 -0
- package/src/components/ui/tabs.tsx +81 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +44 -0
- package/src/config/provider-docs.ts +17 -0
- package/src/hooks/__tests__/use-async.test.ts +127 -0
- package/src/hooks/__tests__/use-count-up.test.ts +129 -0
- package/src/hooks/__tests__/use-debounce.test.ts +105 -0
- package/src/hooks/__tests__/use-fleet-sse.test.ts +216 -0
- package/src/hooks/__tests__/use-local-storage.test.ts +74 -0
- package/src/hooks/__tests__/use-mobile.test.ts +86 -0
- package/src/hooks/__tests__/use-save-queue.test.ts +159 -0
- package/src/hooks/use-async.ts +54 -0
- package/src/hooks/use-capability-meta.ts +99 -0
- package/src/hooks/use-count-up.ts +23 -0
- package/src/hooks/use-debounce.ts +12 -0
- package/src/hooks/use-fleet-sse.ts +47 -0
- package/src/hooks/use-has-org.ts +18 -0
- package/src/hooks/use-image-status.ts +36 -0
- package/src/hooks/use-local-storage.ts +36 -0
- package/src/hooks/use-mobile.ts +17 -0
- package/src/hooks/use-page-context.ts +24 -0
- package/src/hooks/use-pagination-params.ts +30 -0
- package/src/hooks/use-plugin-registry.ts +247 -0
- package/src/hooks/use-plugin-setup-chat.ts +211 -0
- package/src/hooks/use-save-queue.ts +54 -0
- package/src/hooks/use-webmcp.ts +40 -0
- package/src/lib/__tests__/__snapshots__/pricing-data.test.ts.snap +112 -0
- package/src/lib/__tests__/admin-api.test.ts +487 -0
- package/src/lib/__tests__/api-bot-crud.test.ts +391 -0
- package/src/lib/__tests__/api-fetch.test.ts +196 -0
- package/src/lib/__tests__/bot-settings-data.test.ts +352 -0
- package/src/lib/__tests__/org-api.test.ts +281 -0
- package/src/lib/__tests__/org-billing-api.test.ts +242 -0
- package/src/lib/__tests__/pricing-data.test.ts +32 -0
- package/src/lib/__tests__/settings-api.test.ts +272 -0
- package/src/lib/admin-affiliate-api.ts +51 -0
- package/src/lib/admin-api.ts +325 -0
- package/src/lib/admin-compliance-api.ts +127 -0
- package/src/lib/admin-gpu-api.ts +82 -0
- package/src/lib/admin-incident-api.ts +121 -0
- package/src/lib/admin-inference-api.ts +47 -0
- package/src/lib/admin-marketplace-api.ts +97 -0
- package/src/lib/api-config.test.ts +111 -0
- package/src/lib/api-config.ts +65 -0
- package/src/lib/api-errors.test.ts +43 -0
- package/src/lib/api.ts +2011 -0
- package/src/lib/auth-client.ts +11 -0
- package/src/lib/bot-settings-data.ts +342 -0
- package/src/lib/brand-config.ts +145 -0
- package/src/lib/brand.ts +669 -0
- package/src/lib/changeset-api.ts +29 -0
- package/src/lib/changeset-types.ts +56 -0
- package/src/lib/channel-manifests.ts +50 -0
- package/src/lib/chat/chat-context.tsx +70 -0
- package/src/lib/chat/chat-store.ts +62 -0
- package/src/lib/chat/types.ts +35 -0
- package/src/lib/chat/use-chat.ts +255 -0
- package/src/lib/cost-comparison-data.test.ts +95 -0
- package/src/lib/cost-comparison-data.ts +54 -0
- package/src/lib/errors.test.ts +64 -0
- package/src/lib/errors.ts +52 -0
- package/src/lib/fetch-utils.test.ts +57 -0
- package/src/lib/fetch-utils.ts +25 -0
- package/src/lib/format-credit.test.ts +66 -0
- package/src/lib/format-credit.ts +24 -0
- package/src/lib/format.test.ts +62 -0
- package/src/lib/format.ts +17 -0
- package/src/lib/logger.ts +28 -0
- package/src/lib/marketplace-data.ts +346 -0
- package/src/lib/oauth-errors.ts +19 -0
- package/src/lib/onboarding-data.ts +1265 -0
- package/src/lib/onboarding-store.ts +233 -0
- package/src/lib/org-api.ts +74 -0
- package/src/lib/org-billing-api.ts +81 -0
- package/src/lib/page-prompts.test.ts +32 -0
- package/src/lib/page-prompts.ts +23 -0
- package/src/lib/plugin/index.ts +32 -0
- package/src/lib/plugin/tool-definitions.ts +306 -0
- package/src/lib/pricing-data.ts +115 -0
- package/src/lib/promotions-types.ts +58 -0
- package/src/lib/settings-api.ts +63 -0
- package/src/lib/status-colors.ts +38 -0
- package/src/lib/tenant-context.tsx +134 -0
- package/src/lib/trpc-types.ts +173 -0
- package/src/lib/trpc.tsx +86 -0
- package/src/lib/utils.test.ts +55 -0
- package/src/lib/utils.ts +18 -0
- package/src/lib/validate-redirect-url.ts +39 -0
- package/src/lib/webmcp/feature-detect.ts +13 -0
- package/src/lib/webmcp/marketplace-onboarding-tools.ts +202 -0
- package/src/lib/webmcp/register.ts +44 -0
- package/src/lib/webmcp/tools.ts +422 -0
- package/src/proxy.ts +258 -0
- package/src/types/missing-deps.d.ts +160 -0
- package/src/types/motion-dom.d.ts +162 -0
- package/src/types/vitest-matchers.d.ts +40 -0
- package/src/types/web-mcp.d.ts +22 -0
- package/tsconfig.json +34 -0
- package/vitest.config.ts +26 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Badge } from "@/components/ui/badge";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { useCapabilityMeta } from "@/hooks/use-capability-meta";
|
|
6
|
+
import { brandName } from "@/lib/brand-config";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Renders a capability label from the registry. Falls back to auto-formatted name.
|
|
10
|
+
*/
|
|
11
|
+
export function CapabilityLabel({ capability }: { capability: string }) {
|
|
12
|
+
const { getMeta } = useCapabilityMeta();
|
|
13
|
+
const meta = getMeta(capability);
|
|
14
|
+
return <span>{meta.label}</span>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Renders the pricing badge for a capability. Returns null if no pricing info.
|
|
19
|
+
*/
|
|
20
|
+
export function CapabilityPricing({ capability }: { capability: string }) {
|
|
21
|
+
const { getMeta } = useCapabilityMeta();
|
|
22
|
+
const meta = getMeta(capability);
|
|
23
|
+
if (!meta.pricing) return null;
|
|
24
|
+
return <Badge variant="outline">{meta.pricing}</Badge>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Renders the description for a capability. Returns null if no description.
|
|
29
|
+
*/
|
|
30
|
+
export function CapabilityDescription({ capability }: { capability: string }) {
|
|
31
|
+
const { getMeta } = useCapabilityMeta();
|
|
32
|
+
const meta = getMeta(capability);
|
|
33
|
+
if (!meta.description) return null;
|
|
34
|
+
return <p className="text-sm text-muted-foreground">{meta.description}</p>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Generic provider picker for capabilities. Given a list of capability strings,
|
|
39
|
+
* renders hosted-vs-BYOK choice cards for each capability that has a hostedProvider
|
|
40
|
+
* in the capability registry. Capabilities without a hostedProvider are omitted
|
|
41
|
+
* (they are BYOK-only, no choice needed).
|
|
42
|
+
*
|
|
43
|
+
* Drop-in replacement for the old hardcoded ProviderSelector in install-wizard.
|
|
44
|
+
*/
|
|
45
|
+
export function CapabilityProviderPicker({
|
|
46
|
+
capabilities,
|
|
47
|
+
choices,
|
|
48
|
+
onChoose,
|
|
49
|
+
}: {
|
|
50
|
+
capabilities: string[];
|
|
51
|
+
choices: Record<string, "byok" | "hosted">;
|
|
52
|
+
onChoose: (capability: string, choice: "byok" | "hosted") => void;
|
|
53
|
+
}) {
|
|
54
|
+
const { meta } = useCapabilityMeta();
|
|
55
|
+
|
|
56
|
+
const hostedCapabilities = capabilities
|
|
57
|
+
.map((cap) => meta.find((m) => m.capability === cap))
|
|
58
|
+
.filter((m): m is NonNullable<typeof m> => !!m && !!m.hostedProvider);
|
|
59
|
+
|
|
60
|
+
if (hostedCapabilities.length === 0) {
|
|
61
|
+
return (
|
|
62
|
+
<div className="py-4 text-center">
|
|
63
|
+
<p className="text-sm text-muted-foreground">
|
|
64
|
+
No hosted provider options available for this plugin's capabilities.
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="space-y-4">
|
|
72
|
+
<p className="text-sm text-muted-foreground">
|
|
73
|
+
Some capabilities can be provided by {brandName()} Hosted services. Choose for each:
|
|
74
|
+
</p>
|
|
75
|
+
{hostedCapabilities.map((capMeta) => {
|
|
76
|
+
const choice = choices[capMeta.capability] ?? "hosted";
|
|
77
|
+
return (
|
|
78
|
+
<div key={capMeta.capability} className="rounded-sm border p-4">
|
|
79
|
+
<div className="flex items-center justify-between">
|
|
80
|
+
<div>
|
|
81
|
+
<p className="text-sm font-medium">{capMeta.label}</p>
|
|
82
|
+
<p className="text-xs text-muted-foreground">{capMeta.description}</p>
|
|
83
|
+
</div>
|
|
84
|
+
{capMeta.pricing && (
|
|
85
|
+
<Badge variant="outline" className="text-[10px]">
|
|
86
|
+
{capMeta.pricing}
|
|
87
|
+
</Badge>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
<div className="mt-3 flex gap-2">
|
|
91
|
+
<Button
|
|
92
|
+
data-onboarding-id={`marketplace.wizard.provider.hosted.${capMeta.capability}`}
|
|
93
|
+
variant={choice === "hosted" ? "default" : "outline"}
|
|
94
|
+
size="sm"
|
|
95
|
+
onClick={() => onChoose(capMeta.capability, "hosted")}
|
|
96
|
+
>
|
|
97
|
+
{brandName()} Hosted
|
|
98
|
+
</Button>
|
|
99
|
+
<Button
|
|
100
|
+
data-onboarding-id={`marketplace.wizard.provider.byok.${capMeta.capability}`}
|
|
101
|
+
variant={choice === "byok" ? "default" : "outline"}
|
|
102
|
+
size="sm"
|
|
103
|
+
onClick={() => onChoose(capMeta.capability, "byok")}
|
|
104
|
+
>
|
|
105
|
+
Use your key
|
|
106
|
+
</Button>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
})}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { Label } from "@/components/ui/label";
|
|
5
|
+
import type { ConfigField } from "@/lib/channel-manifests";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
interface FieldInteractiveProps {
|
|
9
|
+
field: ConfigField;
|
|
10
|
+
value: string;
|
|
11
|
+
onChange: (key: string, value: string) => void;
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function FieldInteractive({ field, value, onChange, error }: FieldInteractiveProps) {
|
|
16
|
+
if (field.type === "select" && field.options) {
|
|
17
|
+
return (
|
|
18
|
+
<div className="space-y-2">
|
|
19
|
+
<Label htmlFor={field.key}>{field.label}</Label>
|
|
20
|
+
{field.description && <p className="text-sm text-muted-foreground">{field.description}</p>}
|
|
21
|
+
<div className="space-y-2">
|
|
22
|
+
{field.options.map((option) => (
|
|
23
|
+
<Button
|
|
24
|
+
key={option.value}
|
|
25
|
+
type="button"
|
|
26
|
+
variant="ghost"
|
|
27
|
+
onClick={() => onChange(field.key, option.value)}
|
|
28
|
+
className={cn(
|
|
29
|
+
"flex w-full items-center rounded-sm border px-4 py-3 text-left text-sm h-auto transition-colors hover:bg-transparent",
|
|
30
|
+
value === option.value
|
|
31
|
+
? "border-primary bg-primary/10 text-foreground"
|
|
32
|
+
: "border-border hover:border-primary/50 hover:bg-accent",
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
<span className="flex-1">{option.label}</span>
|
|
36
|
+
{value === option.value && (
|
|
37
|
+
<span className="text-primary text-xs font-medium">Selected</span>
|
|
38
|
+
)}
|
|
39
|
+
</Button>
|
|
40
|
+
))}
|
|
41
|
+
</div>
|
|
42
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { Label } from "@/components/ui/label";
|
|
6
|
+
import { initiateChannelOAuth, pollChannelOAuth } from "@/lib/api";
|
|
7
|
+
import { eventName } from "@/lib/brand-config";
|
|
8
|
+
import type { ConfigField } from "@/lib/channel-manifests";
|
|
9
|
+
|
|
10
|
+
interface FieldOAuthProps {
|
|
11
|
+
field: ConfigField;
|
|
12
|
+
value: string;
|
|
13
|
+
onChange: (key: string, value: string) => void;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type OAuthStatus = "idle" | "authorizing" | "exchanging" | "authorized" | "error";
|
|
18
|
+
|
|
19
|
+
export function FieldOAuth({ field, value, onChange, error }: FieldOAuthProps) {
|
|
20
|
+
const [status, setStatus] = useState<OAuthStatus>(value ? "authorized" : "idle");
|
|
21
|
+
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
|
22
|
+
const popupRef = useRef<Window | null>(null);
|
|
23
|
+
const stateRef = useRef<string | null>(null);
|
|
24
|
+
const statusRef = useRef<OAuthStatus>(value ? "authorized" : "idle");
|
|
25
|
+
|
|
26
|
+
// Keep statusRef in sync so the popup close monitor can read current status
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
statusRef.current = status;
|
|
29
|
+
}, [status]);
|
|
30
|
+
|
|
31
|
+
const pollForToken = useCallback(
|
|
32
|
+
async (state: string) => {
|
|
33
|
+
const maxAttempts = 30; // 30 seconds at 1s interval
|
|
34
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
35
|
+
try {
|
|
36
|
+
const data = await pollChannelOAuth(state);
|
|
37
|
+
|
|
38
|
+
if (data.status === "completed" && data.token) {
|
|
39
|
+
onChange(field.key, data.token);
|
|
40
|
+
setStatus("authorized");
|
|
41
|
+
setErrorMsg(null);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (data.status === "expired") {
|
|
45
|
+
setStatus("error");
|
|
46
|
+
setErrorMsg("OAuth session expired. Please try again.");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Still pending — wait and retry
|
|
50
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
51
|
+
} catch {
|
|
52
|
+
setStatus("error");
|
|
53
|
+
setErrorMsg("Network error while checking authorization status");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
setStatus("error");
|
|
58
|
+
setErrorMsg("Authorization timed out. Please try again.");
|
|
59
|
+
},
|
|
60
|
+
[field.key, onChange],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Listen for postMessage from the OAuth popup
|
|
64
|
+
const handleMessage = useCallback(
|
|
65
|
+
(event: MessageEvent) => {
|
|
66
|
+
// Validate origin
|
|
67
|
+
if (event.origin !== window.location.origin) return;
|
|
68
|
+
const data = event.data as Record<string, unknown>;
|
|
69
|
+
if (!data || data.type !== eventName("oauth-callback")) return;
|
|
70
|
+
|
|
71
|
+
if (data.status === "error") {
|
|
72
|
+
setStatus("error");
|
|
73
|
+
setErrorMsg((data.error as string) || "OAuth authorization failed");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (data.status === "success" && data.state) {
|
|
78
|
+
// Poll for the token
|
|
79
|
+
setStatus("exchanging");
|
|
80
|
+
pollForToken(data.state as string);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
[pollForToken],
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
window.addEventListener("message", handleMessage);
|
|
88
|
+
return () => window.removeEventListener("message", handleMessage);
|
|
89
|
+
}, [handleMessage]);
|
|
90
|
+
|
|
91
|
+
async function handleAuthorize() {
|
|
92
|
+
setStatus("authorizing");
|
|
93
|
+
setErrorMsg(null);
|
|
94
|
+
|
|
95
|
+
const provider = field.oauthProvider;
|
|
96
|
+
if (!provider) {
|
|
97
|
+
setStatus("error");
|
|
98
|
+
setErrorMsg("No OAuth provider configured for this field");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Open a blank popup synchronously (before the async fetch) to avoid
|
|
103
|
+
// popup blockers, which only allow window.open() in synchronous event handlers.
|
|
104
|
+
const width = 600;
|
|
105
|
+
const height = 700;
|
|
106
|
+
const left = window.screenX + (window.outerWidth - width) / 2;
|
|
107
|
+
const top = window.screenY + (window.outerHeight - height) / 2;
|
|
108
|
+
const popup = window.open(
|
|
109
|
+
"about:blank",
|
|
110
|
+
eventName("oauth-popup"),
|
|
111
|
+
`width=${width},height=${height},left=${left},top=${top},popup=yes`,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
if (!popup) {
|
|
115
|
+
setStatus("error");
|
|
116
|
+
setErrorMsg("Popup blocked. Please allow popups for this site and try again.");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
popupRef.current = popup;
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const res = await initiateChannelOAuth(provider);
|
|
124
|
+
|
|
125
|
+
if (!res.ok) {
|
|
126
|
+
popup.close();
|
|
127
|
+
const data = (await res.json()) as { error?: string };
|
|
128
|
+
setStatus("error");
|
|
129
|
+
setErrorMsg(data.error || "Failed to start OAuth flow");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const { authorizeUrl, state } = (await res.json()) as {
|
|
134
|
+
authorizeUrl: string;
|
|
135
|
+
state: string;
|
|
136
|
+
};
|
|
137
|
+
stateRef.current = state;
|
|
138
|
+
|
|
139
|
+
// Navigate the already-open popup to the authorization URL
|
|
140
|
+
popup.location.href = authorizeUrl;
|
|
141
|
+
|
|
142
|
+
// Monitor popup close (user might close it manually)
|
|
143
|
+
const interval = setInterval(() => {
|
|
144
|
+
if (popup.closed) {
|
|
145
|
+
clearInterval(interval);
|
|
146
|
+
// If we haven't received a success/error message yet, return to idle
|
|
147
|
+
if (statusRef.current === "authorizing") {
|
|
148
|
+
setStatus("idle");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}, 500);
|
|
152
|
+
} catch {
|
|
153
|
+
popup.close();
|
|
154
|
+
setStatus("error");
|
|
155
|
+
setErrorMsg("Network error. Please check your connection and try again.");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div className="space-y-2">
|
|
161
|
+
<Label>{field.label}</Label>
|
|
162
|
+
{field.description && <p className="text-sm text-muted-foreground">{field.description}</p>}
|
|
163
|
+
<div className="flex items-center gap-3">
|
|
164
|
+
<Button
|
|
165
|
+
type="button"
|
|
166
|
+
variant={status === "authorized" ? "secondary" : "default"}
|
|
167
|
+
onClick={handleAuthorize}
|
|
168
|
+
disabled={status === "authorizing" || status === "exchanging"}
|
|
169
|
+
>
|
|
170
|
+
{status === "idle" && "Authorize"}
|
|
171
|
+
{status === "authorizing" && "Authorizing..."}
|
|
172
|
+
{status === "exchanging" && "Completing..."}
|
|
173
|
+
{status === "authorized" && "Re-authorize"}
|
|
174
|
+
{status === "error" && "Retry"}
|
|
175
|
+
</Button>
|
|
176
|
+
{status === "authorized" && <span className="text-sm text-emerald-500">Connected</span>}
|
|
177
|
+
</div>
|
|
178
|
+
{(errorMsg || error) && <p className="text-sm text-destructive">{errorMsg || error}</p>}
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { Input } from "@/components/ui/input";
|
|
6
|
+
import { Label } from "@/components/ui/label";
|
|
7
|
+
import type { ConfigField } from "@/lib/channel-manifests";
|
|
8
|
+
|
|
9
|
+
interface FieldPasteProps {
|
|
10
|
+
field: ConfigField;
|
|
11
|
+
value: string;
|
|
12
|
+
onChange: (key: string, value: string) => void;
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function FieldPaste({ field, value, onChange, error }: FieldPasteProps) {
|
|
17
|
+
const [revealed, setRevealed] = useState(false);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="space-y-2">
|
|
21
|
+
<Label htmlFor={field.key}>{field.label}</Label>
|
|
22
|
+
{field.description && <p className="text-sm text-muted-foreground">{field.description}</p>}
|
|
23
|
+
<div className="relative">
|
|
24
|
+
<Input
|
|
25
|
+
id={field.key}
|
|
26
|
+
type={field.secret && !revealed ? "password" : "text"}
|
|
27
|
+
placeholder={field.placeholder}
|
|
28
|
+
value={value}
|
|
29
|
+
onChange={(e) => onChange(field.key, e.target.value)}
|
|
30
|
+
className={error ? "border-destructive" : ""}
|
|
31
|
+
/>
|
|
32
|
+
{field.secret && (
|
|
33
|
+
<Button
|
|
34
|
+
type="button"
|
|
35
|
+
variant="ghost"
|
|
36
|
+
size="xs"
|
|
37
|
+
onClick={() => setRevealed(!revealed)}
|
|
38
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-muted-foreground hover:text-foreground hover:bg-transparent h-auto px-1"
|
|
39
|
+
>
|
|
40
|
+
{revealed ? "Hide" : "Show"}
|
|
41
|
+
</Button>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { AlertTriangle, Check, RefreshCw, WifiOff } from "lucide-react";
|
|
4
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Label } from "@/components/ui/label";
|
|
7
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
8
|
+
import { pollChannelQr } from "@/lib/api";
|
|
9
|
+
import type { ConfigField } from "@/lib/channel-manifests";
|
|
10
|
+
|
|
11
|
+
interface FieldQRProps {
|
|
12
|
+
field: ConfigField;
|
|
13
|
+
value: string;
|
|
14
|
+
onChange: (key: string, value: string) => void;
|
|
15
|
+
error?: string;
|
|
16
|
+
botId?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type QrState = "loading" | "qr" | "expired" | "connected" | "error" | "offline";
|
|
20
|
+
|
|
21
|
+
const POLL_INTERVAL = 2000;
|
|
22
|
+
const QR_TTL_SECONDS = 90;
|
|
23
|
+
|
|
24
|
+
export function FieldQR({ field, value: _value, onChange, error, botId }: FieldQRProps) {
|
|
25
|
+
const [state, setState] = useState<QrState>("loading");
|
|
26
|
+
const [qrPng, setQrPng] = useState<string | null>(null);
|
|
27
|
+
const [secondsLeft, setSecondsLeft] = useState(QR_TTL_SECONDS);
|
|
28
|
+
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
|
29
|
+
const expiresAtRef = useRef<number | null>(null);
|
|
30
|
+
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
31
|
+
const countdownRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
32
|
+
const stateRef = useRef<QrState>("loading");
|
|
33
|
+
|
|
34
|
+
// Keep stateRef in sync so poll callback can read current state
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
stateRef.current = state;
|
|
37
|
+
}, [state]);
|
|
38
|
+
|
|
39
|
+
const cleanup = useCallback(() => {
|
|
40
|
+
if (intervalRef.current) {
|
|
41
|
+
clearInterval(intervalRef.current);
|
|
42
|
+
intervalRef.current = null;
|
|
43
|
+
}
|
|
44
|
+
if (countdownRef.current) {
|
|
45
|
+
clearInterval(countdownRef.current);
|
|
46
|
+
countdownRef.current = null;
|
|
47
|
+
}
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const startCountdown = useCallback(() => {
|
|
51
|
+
if (countdownRef.current) clearInterval(countdownRef.current);
|
|
52
|
+
countdownRef.current = setInterval(() => {
|
|
53
|
+
if (expiresAtRef.current === null) return;
|
|
54
|
+
const remaining = Math.max(0, Math.ceil((expiresAtRef.current - Date.now()) / 1000));
|
|
55
|
+
setSecondsLeft(remaining);
|
|
56
|
+
if (remaining <= 0) {
|
|
57
|
+
setState("expired");
|
|
58
|
+
if (countdownRef.current) clearInterval(countdownRef.current);
|
|
59
|
+
}
|
|
60
|
+
}, 1000);
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
const poll = useCallback(async () => {
|
|
64
|
+
if (!botId) {
|
|
65
|
+
setState("offline");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const res = await pollChannelQr(botId);
|
|
70
|
+
const currentState = stateRef.current;
|
|
71
|
+
|
|
72
|
+
switch (res.status) {
|
|
73
|
+
case "pending":
|
|
74
|
+
if (res.qrPng) {
|
|
75
|
+
setQrPng(res.qrPng);
|
|
76
|
+
if (currentState === "loading" || currentState === "expired") {
|
|
77
|
+
expiresAtRef.current = Date.now() + QR_TTL_SECONDS * 1000;
|
|
78
|
+
setSecondsLeft(QR_TTL_SECONDS);
|
|
79
|
+
startCountdown();
|
|
80
|
+
}
|
|
81
|
+
setState("qr");
|
|
82
|
+
} else {
|
|
83
|
+
setState("loading");
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
case "connected":
|
|
87
|
+
setState("connected");
|
|
88
|
+
if (intervalRef.current) {
|
|
89
|
+
clearInterval(intervalRef.current);
|
|
90
|
+
intervalRef.current = null;
|
|
91
|
+
}
|
|
92
|
+
if (countdownRef.current) {
|
|
93
|
+
clearInterval(countdownRef.current);
|
|
94
|
+
countdownRef.current = null;
|
|
95
|
+
}
|
|
96
|
+
onChange(field.key, "connected");
|
|
97
|
+
break;
|
|
98
|
+
case "expired":
|
|
99
|
+
setState("expired");
|
|
100
|
+
if (countdownRef.current) clearInterval(countdownRef.current);
|
|
101
|
+
break;
|
|
102
|
+
case "no-session":
|
|
103
|
+
setState("offline");
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
cleanup();
|
|
108
|
+
setState("error");
|
|
109
|
+
setErrorMsg("Could not reach the server. Check that your bot is running and try again.");
|
|
110
|
+
}
|
|
111
|
+
}, [botId, field.key, onChange, startCountdown, cleanup]);
|
|
112
|
+
|
|
113
|
+
const startPolling = useCallback(() => {
|
|
114
|
+
cleanup();
|
|
115
|
+
setState("loading");
|
|
116
|
+
setErrorMsg(null);
|
|
117
|
+
poll();
|
|
118
|
+
intervalRef.current = setInterval(poll, POLL_INTERVAL);
|
|
119
|
+
}, [cleanup, poll]);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
startPolling();
|
|
123
|
+
return cleanup;
|
|
124
|
+
}, [startPolling, cleanup]);
|
|
125
|
+
|
|
126
|
+
function handleRefresh() {
|
|
127
|
+
expiresAtRef.current = null;
|
|
128
|
+
startPolling();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function countdownColor(): string {
|
|
132
|
+
if (secondsLeft <= 5) return "text-destructive motion-safe:animate-pulse";
|
|
133
|
+
if (secondsLeft <= 15) return "text-amber-500";
|
|
134
|
+
return "text-terminal-dim";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div className="space-y-2">
|
|
139
|
+
<Label>{field.label}</Label>
|
|
140
|
+
{field.description && <p className="text-sm text-muted-foreground">{field.description}</p>}
|
|
141
|
+
|
|
142
|
+
<div className="flex flex-col items-center gap-5 rounded-sm border border-dashed p-8 animate-in fade-in duration-300">
|
|
143
|
+
{/* --- Loading State --- */}
|
|
144
|
+
{state === "loading" && (
|
|
145
|
+
<div className="flex flex-col items-center gap-4">
|
|
146
|
+
<Skeleton className="h-40 w-40 rounded-sm min-[375px]:h-48 min-[375px]:w-48" />
|
|
147
|
+
<p className="text-sm text-muted-foreground">
|
|
148
|
+
Generating secure QR code
|
|
149
|
+
<span className="animate-ellipsis" />
|
|
150
|
+
</p>
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{/* --- QR Displayed State --- */}
|
|
155
|
+
{state === "qr" && qrPng && (
|
|
156
|
+
<div className="flex flex-col items-center gap-4">
|
|
157
|
+
<div className="relative rounded-sm transition-shadow duration-1000 shadow-[0_0_24px_rgba(0,255,65,0.15)]">
|
|
158
|
+
{/* Corner accents */}
|
|
159
|
+
<div className="absolute top-0 left-0 h-3 w-3 border-t-2 border-l-2 border-terminal" />
|
|
160
|
+
<div className="absolute top-0 right-0 h-3 w-3 border-t-2 border-r-2 border-terminal" />
|
|
161
|
+
<div className="absolute bottom-0 left-0 h-3 w-3 border-b-2 border-l-2 border-terminal" />
|
|
162
|
+
<div className="absolute bottom-0 right-0 h-3 w-3 border-b-2 border-r-2 border-terminal" />
|
|
163
|
+
|
|
164
|
+
{/* bg-white is intentional -- QR codes require white background for scanability */}
|
|
165
|
+
<div className="h-40 w-40 rounded-sm bg-white p-3 min-[375px]:h-48 min-[375px]:w-48">
|
|
166
|
+
{/* biome-ignore lint/performance/noImgElement: QR code is a base64 data URL, not optimizable by next/image */}
|
|
167
|
+
<img
|
|
168
|
+
src={qrPng}
|
|
169
|
+
alt="Scan this QR code with your phone"
|
|
170
|
+
className="h-full w-full"
|
|
171
|
+
/>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
{/* Scanning indicator */}
|
|
176
|
+
<div className="flex items-center gap-2">
|
|
177
|
+
<div className="h-1.5 w-1.5 rounded-full bg-terminal motion-safe:animate-[pulse-dot_2s_ease-in-out_infinite]" />
|
|
178
|
+
<span className="text-xs font-medium uppercase tracking-wider text-terminal">
|
|
179
|
+
WAITING FOR SCAN
|
|
180
|
+
</span>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
{/* Instructions */}
|
|
184
|
+
<p className="max-w-[280px] text-center text-sm text-muted-foreground">
|
|
185
|
+
{field.description ?? "Scan this QR code with your mobile app to link your account"}
|
|
186
|
+
</p>
|
|
187
|
+
|
|
188
|
+
{/* Countdown */}
|
|
189
|
+
<span className={`text-xs tabular-nums ${countdownColor()}`}>
|
|
190
|
+
Expires in {secondsLeft}s
|
|
191
|
+
</span>
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
|
|
195
|
+
{/* --- Expired State --- */}
|
|
196
|
+
{state === "expired" && (
|
|
197
|
+
<div className="flex flex-col items-center gap-4">
|
|
198
|
+
<div className="relative rounded-sm">
|
|
199
|
+
{/* Corner accents (dimmed) */}
|
|
200
|
+
<div className="absolute top-0 left-0 h-3 w-3 border-t-2 border-l-2 border-terminal/30" />
|
|
201
|
+
<div className="absolute top-0 right-0 h-3 w-3 border-t-2 border-r-2 border-terminal/30" />
|
|
202
|
+
<div className="absolute bottom-0 left-0 h-3 w-3 border-b-2 border-l-2 border-terminal/30" />
|
|
203
|
+
<div className="absolute bottom-0 right-0 h-3 w-3 border-b-2 border-r-2 border-terminal/30" />
|
|
204
|
+
|
|
205
|
+
{/* bg-white is intentional -- QR codes require white background for scanability */}
|
|
206
|
+
<div className="relative h-40 w-40 rounded-sm bg-white p-3 min-[375px]:h-48 min-[375px]:w-48">
|
|
207
|
+
{qrPng && (
|
|
208
|
+
// biome-ignore lint/performance/noImgElement: QR code is a base64 data URL
|
|
209
|
+
<img src={qrPng} alt="Expired QR code" className="h-full w-full opacity-40" />
|
|
210
|
+
)}
|
|
211
|
+
{/* Dark overlay */}
|
|
212
|
+
<div className="absolute inset-0 flex items-center justify-center rounded-sm bg-black/60 animate-in fade-in duration-300">
|
|
213
|
+
<RefreshCw className="size-8 text-white" />
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<span className="text-xs font-medium uppercase tracking-wider text-amber-500">
|
|
219
|
+
QR CODE EXPIRED
|
|
220
|
+
</span>
|
|
221
|
+
|
|
222
|
+
<p className="text-center text-sm text-muted-foreground">
|
|
223
|
+
The code expired. Tap below to generate a fresh one.
|
|
224
|
+
</p>
|
|
225
|
+
|
|
226
|
+
<Button
|
|
227
|
+
type="button"
|
|
228
|
+
variant="terminal"
|
|
229
|
+
size="sm"
|
|
230
|
+
className="h-10 px-4"
|
|
231
|
+
onClick={handleRefresh}
|
|
232
|
+
>
|
|
233
|
+
<RefreshCw className="size-3.5" />
|
|
234
|
+
Generate New Code
|
|
235
|
+
</Button>
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{/* --- Connected / Success State --- */}
|
|
240
|
+
{state === "connected" && (
|
|
241
|
+
<div className="flex flex-col items-center gap-4 animate-in fade-in duration-300">
|
|
242
|
+
<div className="flex h-40 w-40 items-center justify-center rounded-full border border-emerald-500/30 bg-emerald-500/10 min-[375px]:h-48 min-[375px]:w-48 animate-in zoom-in-90 duration-300">
|
|
243
|
+
<Check className="size-16 text-emerald-500" />
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
<span className="text-xs font-medium uppercase tracking-wider text-emerald-500">
|
|
247
|
+
LINKED SUCCESSFULLY
|
|
248
|
+
</span>
|
|
249
|
+
|
|
250
|
+
<p className="text-center text-sm text-muted-foreground">
|
|
251
|
+
{field.label ? `${field.label} connected` : "Connected"}. You can continue setup.
|
|
252
|
+
</p>
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
|
|
256
|
+
{/* --- Error State --- */}
|
|
257
|
+
{state === "error" && (
|
|
258
|
+
<div className="flex flex-col items-center gap-4">
|
|
259
|
+
<AlertTriangle className="size-10 text-destructive" />
|
|
260
|
+
|
|
261
|
+
<span className="text-xs font-medium uppercase tracking-wider text-destructive">
|
|
262
|
+
CONNECTION ERROR
|
|
263
|
+
</span>
|
|
264
|
+
|
|
265
|
+
<p className="max-w-[280px] text-center text-sm text-muted-foreground">
|
|
266
|
+
{errorMsg ||
|
|
267
|
+
"Could not reach the server. Check that your bot is running and try again."}
|
|
268
|
+
</p>
|
|
269
|
+
|
|
270
|
+
<Button
|
|
271
|
+
type="button"
|
|
272
|
+
variant="terminal"
|
|
273
|
+
size="sm"
|
|
274
|
+
className="h-10 px-4"
|
|
275
|
+
onClick={handleRefresh}
|
|
276
|
+
>
|
|
277
|
+
<RefreshCw className="size-3.5" />
|
|
278
|
+
Try Again
|
|
279
|
+
</Button>
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
|
|
283
|
+
{/* --- Bot Offline State --- */}
|
|
284
|
+
{state === "offline" && (
|
|
285
|
+
<div className="flex flex-col items-center gap-4">
|
|
286
|
+
<WifiOff className="size-10 text-muted-foreground" />
|
|
287
|
+
|
|
288
|
+
<span className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
|
289
|
+
BOT OFFLINE
|
|
290
|
+
</span>
|
|
291
|
+
|
|
292
|
+
<p className="max-w-[280px] text-center text-sm text-muted-foreground">
|
|
293
|
+
Your bot is currently offline. Start it from the fleet dashboard to link your account.
|
|
294
|
+
</p>
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
297
|
+
</div>
|
|
298
|
+
|
|
299
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
300
|
+
</div>
|
|
301
|
+
);
|
|
302
|
+
}
|