@graph8/sdk 0.361.0 → 0.370.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/README.md +14 -10
- package/blocks/_foundation/README.md +172 -1
- package/blocks/_foundation/block.json +234 -13
- package/blocks/_foundation/components/api-error.tsx +71 -25
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirmed-action.tsx +90 -0
- package/blocks/_foundation/components/connect-graph8.tsx +280 -0
- package/blocks/_foundation/components/empty-state.tsx +39 -7
- package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
- package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
- package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
- package/blocks/_foundation/components/screen/index.ts +22 -0
- package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
- package/blocks/_foundation/components/screen/section-card.tsx +47 -0
- package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
- package/blocks/_foundation/components/screen/split-view.tsx +125 -0
- package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
- package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +32 -0
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/components/screen/toolbar.tsx +144 -0
- package/blocks/_foundation/hooks/use-brand.ts +52 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
- package/blocks/_foundation/hooks/use-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/hooks/use-url-state.ts +74 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/server/connect-route.ts +15 -0
- package/blocks/_foundation/server/connect.ts +127 -0
- package/blocks/_foundation/server/local-key.ts +70 -0
- package/blocks/_foundation/server/proxy.ts +103 -3
- package/blocks/_foundation/server/route.ts +16 -4
- package/blocks/agents/README.md +14 -0
- package/blocks/agents/block.json +33 -1
- package/blocks/agents/components/agent-builder.tsx +13 -8
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
- package/blocks/agents/components/agent-run-detail.tsx +102 -0
- package/blocks/agents/components/agent-runs.tsx +16 -7
- package/blocks/agents/components/agents-screen.tsx +184 -0
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +209 -0
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +23 -9
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +21 -1
- package/blocks/appointments/block.json +43 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +77 -0
- package/blocks/appointments/components/meeting-list.tsx +14 -6
- package/blocks/appointments/components/meetings-screen.tsx +157 -0
- package/blocks/appointments/components/scheduling-screen.tsx +122 -0
- package/blocks/appointments/hooks/use-event-types.ts +17 -0
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +26 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +16 -5
- package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +154 -0
- package/blocks/campaigns/components/icp-editor.tsx +15 -15
- package/blocks/campaigns/components/persona-detail.tsx +172 -0
- package/blocks/campaigns/components/persona-editor.tsx +14 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
- package/blocks/coach/README.md +10 -0
- package/blocks/coach/block.json +16 -1
- package/blocks/coach/components/coach-screen.tsx +222 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-panel.tsx +14 -5
- package/blocks/copilot/components/copilot-screen.tsx +148 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
- package/blocks/crm/README.md +28 -0
- package/blocks/crm/block.json +58 -2
- package/blocks/crm/components/companies-screen.tsx +345 -0
- package/blocks/crm/components/company-detail.tsx +8 -6
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +6 -3
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +359 -0
- package/blocks/crm/components/dedupe-review.tsx +7 -5
- package/blocks/crm/components/import-wizard.tsx +4 -2
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-form.tsx +5 -3
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +379 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +7 -1
- package/blocks/crm/lib/crm-types.ts +12 -2
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +34 -3
- package/blocks/deals/block.json +45 -4
- package/blocks/deals/components/deal-detail.tsx +27 -13
- package/blocks/deals/components/deal-related.tsx +87 -0
- package/blocks/deals/components/deals-screen.tsx +298 -0
- package/blocks/deals/components/pipeline-board.tsx +4 -2
- package/blocks/deals/components/stage-editor.tsx +4 -4
- package/blocks/deals/hooks/use-default-owner.ts +60 -0
- package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deals-types.ts +14 -0
- package/blocks/dialer/README.md +34 -2
- package/blocks/dialer/block.json +67 -1
- package/blocks/dialer/components/call-contact-button.tsx +252 -0
- package/blocks/dialer/components/call-log-screen.tsx +205 -0
- package/blocks/dialer/components/call-log.tsx +7 -3
- package/blocks/dialer/components/call-queue.tsx +1 -1
- package/blocks/dialer/components/dialer-screen.tsx +319 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/session-start-form.tsx +4 -2
- package/blocks/dialer/components/softphone.tsx +3 -1
- package/blocks/enrichment/README.md +14 -2
- package/blocks/enrichment/block.json +32 -4
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
- package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
- package/blocks/enrichment/components/enrich-button.tsx +32 -9
- package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
- package/blocks/enrichment/components/waterfall-config.tsx +39 -10
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +12 -0
- package/blocks/forms/block.json +39 -17
- package/blocks/forms/components/form-builder.tsx +3 -1
- package/blocks/forms/components/form-embed.tsx +3 -1
- package/blocks/forms/components/forms-screen.tsx +353 -0
- package/blocks/forms/components/submissions-table.tsx +16 -6
- package/blocks/inbox/README.md +27 -3
- package/blocks/inbox/block.json +14 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
- package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
- package/blocks/inbox/lib/inbox-types.ts +47 -1
- package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
- package/blocks/integrations/README.md +9 -0
- package/blocks/integrations/block.json +46 -1
- package/blocks/integrations/components/crm-detail.tsx +100 -0
- package/blocks/integrations/components/integration-list.tsx +9 -4
- package/blocks/integrations/components/integrations-screen.tsx +135 -0
- package/blocks/integrations/components/sync-status.tsx +5 -3
- package/blocks/integrations/components/webhook-detail.tsx +160 -0
- package/blocks/integrations/components/webhook-manager.tsx +13 -8
- package/blocks/integrations/lib/integrations-utils.ts +6 -1
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/intent-feed.tsx +5 -2
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/lifecycle/README.md +16 -0
- package/blocks/lifecycle/block.json +34 -7
- package/blocks/lifecycle/components/account-related.tsx +66 -0
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +149 -0
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +13 -0
- package/blocks/lists/block.json +39 -1
- package/blocks/lists/components/add-to-list-button.tsx +3 -2
- package/blocks/lists/components/list-picker.tsx +5 -1
- package/blocks/lists/components/list-table.tsx +11 -5
- package/blocks/lists/components/lists-screen.tsx +344 -0
- package/blocks/lists/components/suppression-manager.tsx +6 -2
- package/blocks/lists/components/suppressions-screen.tsx +255 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +65 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
- package/blocks/mailboxes/components/domain-detail.tsx +142 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
- package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
- package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
- package/blocks/mailboxes/components/warmup-health.tsx +4 -2
- package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
- package/blocks/newsletter/README.md +13 -0
- package/blocks/newsletter/block.json +47 -23
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
- package/blocks/newsletter/components/newsletter-screen.tsx +404 -0
- package/blocks/newsletter/components/subscriber-table.tsx +12 -6
- package/blocks/nurture/README.md +12 -0
- package/blocks/nurture/block.json +35 -13
- package/blocks/nurture/components/nurture-builder.tsx +3 -1
- package/blocks/nurture/components/nurture-enroll.tsx +6 -3
- package/blocks/nurture/components/nurture-form.tsx +23 -3
- package/blocks/nurture/components/nurture-screen.tsx +283 -0
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +17 -0
- package/blocks/objects/block.json +29 -16
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +2 -1
- package/blocks/objects/components/object-schema-editor.tsx +3 -1
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +12 -0
- package/blocks/pages/block.json +35 -22
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +46 -11
- package/blocks/pages/components/page-list.tsx +6 -3
- package/blocks/pages/components/pages-screen.tsx +154 -0
- package/blocks/quotes/README.md +17 -0
- package/blocks/quotes/block.json +38 -9
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-list.tsx +7 -3
- package/blocks/quotes/components/quote-related.tsx +123 -0
- package/blocks/quotes/components/quotes-screen.tsx +169 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-report.tsx +4 -2
- package/blocks/radar/components/radar-screen.tsx +214 -0
- package/blocks/radar/components/report-detail.tsx +104 -0
- package/blocks/search/README.md +10 -0
- package/blocks/search/block.json +16 -1
- package/blocks/search/components/filter-builder.tsx +3 -3
- package/blocks/search/components/prospect-results.tsx +5 -3
- package/blocks/search/components/prospect-search-screen.tsx +235 -0
- package/blocks/search/components/saved-searches.tsx +4 -1
- package/blocks/search/hooks/use-filter-options.ts +20 -6
- package/blocks/search/lib/search-types.ts +2 -0
- package/blocks/sequencer/README.md +49 -9
- package/blocks/sequencer/block.json +99 -19
- package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
- package/blocks/sequencer/components/sequence-builder.tsx +211 -46
- package/blocks/sequencer/components/sequence-detail.tsx +259 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
- package/blocks/sequencer/components/sequence-list.tsx +33 -8
- package/blocks/sequencer/components/sequence-stats.tsx +3 -2
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +159 -0
- package/blocks/sequencer/components/step-editor.tsx +3 -1
- package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
- package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
- package/blocks/social-ads/README.md +11 -0
- package/blocks/social-ads/block.json +101 -2
- package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
- package/blocks/social-ads/components/ads-screen.tsx +152 -0
- package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
- package/blocks/social-ads/components/audience-sync.tsx +11 -7
- package/blocks/social-ads/components/campaign-detail.tsx +133 -0
- package/blocks/social-ads/components/creative-detail.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +15 -5
- package/blocks/social-ads/components/post-composer.tsx +3 -1
- package/blocks/social-ads/components/post-detail.tsx +160 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +160 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +3 -1
- package/blocks/visitors/components/visitor-company-card.tsx +2 -2
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +11 -2
- package/blocks/visitors/components/visitors-screen.tsx +166 -0
- package/blocks/webchat/README.md +10 -0
- package/blocks/webchat/block.json +21 -7
- package/blocks/webchat/components/chat-widget-config.tsx +7 -4
- package/blocks/webchat/components/conversation-list.tsx +7 -3
- package/blocks/webchat/components/webchat-screen.tsx +245 -0
- package/blocks/workbench/README.md +11 -0
- package/blocks/workbench/block.json +48 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
- package/blocks/workbench/components/workbench-grid.tsx +9 -3
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +192 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workflows/README.md +13 -0
- package/blocks/workflows/block.json +28 -9
- package/blocks/workflows/components/run-history.tsx +31 -9
- package/blocks/workflows/components/trigger-config.tsx +3 -1
- package/blocks/workflows/components/workflow-editor.tsx +3 -1
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +2785 -1637
- package/dist/errors.d.mts +34 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +21 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.browser.js +11 -5
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +11 -5
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +164 -25
- package/dist/index.d.ts +164 -25
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +10 -4
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +10 -4
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +142 -5
- package/dist/react.d.ts +142 -5
- package/dist/react.js +10 -4
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +10 -4
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/add.d.ts +11 -4
- package/dist/types/blocks/brand.d.ts +113 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +25 -8
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +20 -0
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/paths.d.ts +4 -0
- package/dist/types/blocks/prefixes.d.ts +37 -0
- package/dist/types/blocks/registry.d.ts +32 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/errors.d.ts +33 -0
- package/dist/types/http.d.ts +2 -18
- package/dist/types/version.d.ts +2 -2
- package/docs/agents.md +15 -4
- package/docs/api/README.md +2 -2
- package/docs/api/by-path.md +1 -0
- package/docs/api/desk.d.ts +98 -2
- package/docs/api/desk.md +123 -1
- package/docs/api/inbox.d.ts +10 -2
- package/docs/api/inbox.md +10 -2
- package/docs/api/seo.d.ts +19 -1
- package/docs/api/seo.md +24 -1
- package/package.json +16 -2
- package/stacks/6sense.json +41 -16
- package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
- package/stacks/apollo.json +118 -36
- package/stacks/calendly.json +60 -0
- package/stacks/clay/pages/companies.tsx.tmpl +15 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
- package/stacks/clay.json +59 -26
- package/stacks/gong.json +81 -35
- package/stacks/hootsuite.json +99 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot.json +167 -66
- package/stacks/intercom.json +48 -20
- package/stacks/mailchimp.json +52 -33
- package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
- package/stacks/outreach.json +88 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive.json +115 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce.json +81 -31
- package/stacks/stack.schema.json +6 -1
- package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +102 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +55 -8
- package/templates/next-app/app/globals.css.tmpl +105 -0
- package/templates/next-app/app/layout.tsx.tmpl +18 -4
- package/templates/next-app/components/app-shell.tsx.tmpl +248 -23
- package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
- package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
- package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/lib/nav.ts.tmpl +1 -1
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -15,45 +15,51 @@
|
|
|
15
15
|
"mailboxes"
|
|
16
16
|
],
|
|
17
17
|
"operations": [
|
|
18
|
-
"get_company_profile_company_profile_get",
|
|
19
|
-
"update_company_profile_company_profile_put",
|
|
20
|
-
"generate_company_profile_company_profile_generate_post",
|
|
21
|
-
"get_company_profile_workflow_status_company_profile_workflow_status_get",
|
|
22
18
|
"approve_company_profile_company_profile_approve_post",
|
|
23
|
-
"list_icps_icps_get",
|
|
24
|
-
"create_icp_icps_post",
|
|
25
|
-
"update_icp_icps__icp_id__patch",
|
|
26
19
|
"archive_icp_icps__icp_id__archive_post",
|
|
27
|
-
"list_personas_personas_get",
|
|
28
|
-
"create_persona_personas_post",
|
|
29
|
-
"update_persona_personas__persona_id__patch",
|
|
30
20
|
"archive_persona_personas__persona_id__archive_post",
|
|
31
|
-
"recommend_campaign_personas_campaigns_recommend_personas_post",
|
|
32
|
-
"generate_campaign_ideas_async_campaigns_generate_ideas_async_post",
|
|
33
|
-
"get_campaign_ideas_task_status_campaigns_ideas_status__task_id__get",
|
|
34
|
-
"create_campaign_campaigns_post",
|
|
35
|
-
"update_campaign_campaigns__campaign_id__patch",
|
|
36
|
-
"get_campaign_generation_status_campaigns__campaign_id__status_get",
|
|
37
|
-
"get_campaign_full_campaigns__campaign_id__full_get",
|
|
38
21
|
"attach_audience_to_campaign_campaigns__campaign_id__audience_put",
|
|
39
|
-
"
|
|
22
|
+
"create_campaign_campaigns_post",
|
|
23
|
+
"create_campaign_document_campaigns__campaign_id__documents_post",
|
|
40
24
|
"create_campaign_step_campaigns__campaign_id__sequence_steps_post",
|
|
41
|
-
"
|
|
25
|
+
"create_icp_icps_post",
|
|
26
|
+
"create_persona_personas_post",
|
|
27
|
+
"delete_campaign_campaigns__campaign_id__delete",
|
|
28
|
+
"delete_campaign_document_campaigns__campaign_id__documents__document_id__delete",
|
|
42
29
|
"delete_campaign_step_campaigns__campaign_id__sequence_steps__step_id__delete",
|
|
30
|
+
"generate_campaign_ideas_async_campaigns_generate_ideas_async_post",
|
|
43
31
|
"generate_campaign_step_content_campaigns__campaign_id__sequence_steps__step_id__generate_post",
|
|
44
|
-
"
|
|
32
|
+
"generate_company_profile_company_profile_generate_post",
|
|
33
|
+
"get_campaign_campaigns__campaign_id__get",
|
|
34
|
+
"get_campaign_document_campaigns__campaign_id__documents__document_id__get",
|
|
35
|
+
"get_campaign_full_campaigns__campaign_id__full_get",
|
|
36
|
+
"get_campaign_generation_status_campaigns__campaign_id__status_get",
|
|
37
|
+
"get_campaign_ideas_task_status_campaigns_ideas_status__task_id__get",
|
|
38
|
+
"get_campaign_metrics_campaigns__campaign_id__metrics_get",
|
|
39
|
+
"get_campaign_sequence_campaigns__campaign_id__sequence_get",
|
|
40
|
+
"get_company_profile_company_profile_get",
|
|
41
|
+
"get_company_profile_workflow_status_company_profile_workflow_status_get",
|
|
42
|
+
"get_persona_personas__persona_id__get",
|
|
45
43
|
"get_safe_to_send_deliverability_safe_to_send_get",
|
|
46
44
|
"launch_campaign_campaigns__campaign_id__launch_post",
|
|
45
|
+
"list_campaign_documents_campaigns__campaign_id__documents_get",
|
|
47
46
|
"list_campaigns_campaigns_get",
|
|
48
|
-
"delete_campaign_campaigns__campaign_id__delete",
|
|
49
47
|
"list_campaigns_for_picker_campaigns_picker_get",
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
48
|
+
"list_icps_icps_get",
|
|
49
|
+
"list_mailboxes_mailboxes_get",
|
|
50
|
+
"list_personas_personas_get",
|
|
51
|
+
"pin_icp_icps__icp_id__pin_post",
|
|
52
|
+
"pin_persona_personas__persona_id__pin_post",
|
|
53
|
+
"recommend_campaign_personas_campaigns_recommend_personas_post",
|
|
54
|
+
"regenerate_campaign_document_campaigns__campaign_id__documents__document_id__regenerate_post",
|
|
55
|
+
"unpin_icp_icps__icp_id__unpin_post",
|
|
56
|
+
"unpin_persona_personas__persona_id__unpin_post",
|
|
57
|
+
"update_campaign_campaigns__campaign_id__patch",
|
|
54
58
|
"update_campaign_document_campaigns__campaign_id__documents__document_id__put",
|
|
55
|
-
"
|
|
56
|
-
"
|
|
59
|
+
"update_campaign_step_campaigns__campaign_id__sequence_steps__step_id__put",
|
|
60
|
+
"update_company_profile_company_profile_put",
|
|
61
|
+
"update_icp_icps__icp_id__patch",
|
|
62
|
+
"update_persona_personas__persona_id__patch"
|
|
57
63
|
],
|
|
58
64
|
"components": [
|
|
59
65
|
{
|
|
@@ -102,23 +108,26 @@
|
|
|
102
108
|
"file": "components/campaign-wizard.tsx",
|
|
103
109
|
"purpose": "Idea -> create campaign -> attach audience -> steps and copy -> launch. Every billable or external step is confirmed; ideas are polled.",
|
|
104
110
|
"operations": [
|
|
105
|
-
"get_company_profile_company_profile_get",
|
|
106
|
-
"list_personas_personas_get",
|
|
107
|
-
"generate_campaign_ideas_async_campaigns_generate_ideas_async_post",
|
|
108
|
-
"get_campaign_ideas_task_status_campaigns_ideas_status__task_id__get",
|
|
109
|
-
"create_campaign_campaigns_post",
|
|
110
|
-
"update_campaign_campaigns__campaign_id__patch",
|
|
111
|
-
"get_campaign_generation_status_campaigns__campaign_id__status_get",
|
|
112
|
-
"get_campaign_full_campaigns__campaign_id__full_get",
|
|
113
111
|
"attach_audience_to_campaign_campaigns__campaign_id__audience_put",
|
|
114
|
-
"
|
|
112
|
+
"create_campaign_campaigns_post",
|
|
113
|
+
"create_campaign_document_campaigns__campaign_id__documents_post",
|
|
115
114
|
"create_campaign_step_campaigns__campaign_id__sequence_steps_post",
|
|
116
|
-
"update_campaign_step_campaigns__campaign_id__sequence_steps__step_id__put",
|
|
117
115
|
"delete_campaign_step_campaigns__campaign_id__sequence_steps__step_id__delete",
|
|
116
|
+
"generate_campaign_ideas_async_campaigns_generate_ideas_async_post",
|
|
118
117
|
"generate_campaign_step_content_campaigns__campaign_id__sequence_steps__step_id__generate_post",
|
|
119
|
-
"
|
|
118
|
+
"get_campaign_campaigns__campaign_id__get",
|
|
119
|
+
"get_campaign_full_campaigns__campaign_id__full_get",
|
|
120
|
+
"get_campaign_generation_status_campaigns__campaign_id__status_get",
|
|
121
|
+
"get_campaign_ideas_task_status_campaigns_ideas_status__task_id__get",
|
|
122
|
+
"get_campaign_sequence_campaigns__campaign_id__sequence_get",
|
|
123
|
+
"get_company_profile_company_profile_get",
|
|
120
124
|
"get_safe_to_send_deliverability_safe_to_send_get",
|
|
121
|
-
"launch_campaign_campaigns__campaign_id__launch_post"
|
|
125
|
+
"launch_campaign_campaigns__campaign_id__launch_post",
|
|
126
|
+
"list_mailboxes_mailboxes_get",
|
|
127
|
+
"list_personas_personas_get",
|
|
128
|
+
"regenerate_campaign_document_campaigns__campaign_id__documents__document_id__regenerate_post",
|
|
129
|
+
"update_campaign_campaigns__campaign_id__patch",
|
|
130
|
+
"update_campaign_step_campaigns__campaign_id__sequence_steps__step_id__put"
|
|
122
131
|
],
|
|
123
132
|
"tiers": [
|
|
124
133
|
"read",
|
|
@@ -161,15 +170,19 @@
|
|
|
161
170
|
{
|
|
162
171
|
"name": "CampaignLaunchStep",
|
|
163
172
|
"file": "components/campaign-launch-step.tsx",
|
|
164
|
-
"purpose": "Readiness checklist, sender
|
|
173
|
+
"purpose": "Readiness checklist (audience, steps, written copy, sender), deliverability and warm-up hold warnings, writes missing email copy with AI, then a confirmed launch.",
|
|
165
174
|
"operations": [
|
|
166
175
|
"get_campaign_full_campaigns__campaign_id__full_get",
|
|
167
176
|
"list_mailboxes_mailboxes_get",
|
|
168
177
|
"get_safe_to_send_deliverability_safe_to_send_get",
|
|
169
|
-
"launch_campaign_campaigns__campaign_id__launch_post"
|
|
178
|
+
"launch_campaign_campaigns__campaign_id__launch_post",
|
|
179
|
+
"get_campaign_generation_status_campaigns__campaign_id__status_get",
|
|
180
|
+
"create_campaign_document_campaigns__campaign_id__documents_post",
|
|
181
|
+
"regenerate_campaign_document_campaigns__campaign_id__documents__document_id__regenerate_post"
|
|
170
182
|
],
|
|
171
183
|
"tiers": [
|
|
172
184
|
"read",
|
|
185
|
+
"billable",
|
|
173
186
|
"external"
|
|
174
187
|
]
|
|
175
188
|
},
|
|
@@ -245,6 +258,55 @@
|
|
|
245
258
|
"purpose": "A label around a control; the layout piece the editors share.",
|
|
246
259
|
"operations": [],
|
|
247
260
|
"tiers": []
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"name": "CampaignsScreen",
|
|
264
|
+
"file": "components/campaigns-screen.tsx",
|
|
265
|
+
"purpose": "Finished campaigns page: list, wizard sheet, detail tabs (details, metrics, documents, audience, sequence, launch), company profile and personas.",
|
|
266
|
+
"operations": [
|
|
267
|
+
"get_campaign_campaigns__campaign_id__get",
|
|
268
|
+
"get_campaign_full_campaigns__campaign_id__full_get",
|
|
269
|
+
"update_campaign_campaigns__campaign_id__patch"
|
|
270
|
+
],
|
|
271
|
+
"tiers": [
|
|
272
|
+
"read",
|
|
273
|
+
"external"
|
|
274
|
+
]
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
"name": "IcpDetail",
|
|
278
|
+
"file": "components/icp-detail.tsx",
|
|
279
|
+
"purpose": "One ideal customer profile, complete: every field; edit, pin or unpin, archive (confirmed).",
|
|
280
|
+
"operations": [
|
|
281
|
+
"list_icps_icps_get",
|
|
282
|
+
"update_icp_icps__icp_id__patch",
|
|
283
|
+
"pin_icp_icps__icp_id__pin_post",
|
|
284
|
+
"unpin_icp_icps__icp_id__unpin_post",
|
|
285
|
+
"archive_icp_icps__icp_id__archive_post"
|
|
286
|
+
],
|
|
287
|
+
"tiers": [
|
|
288
|
+
"read",
|
|
289
|
+
"write",
|
|
290
|
+
"destructive"
|
|
291
|
+
]
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
"name": "PersonaDetail",
|
|
295
|
+
"file": "components/persona-detail.tsx",
|
|
296
|
+
"purpose": "One buyer persona, complete: every field and the campaigns targeting it; edit, pin or unpin, archive (confirmed).",
|
|
297
|
+
"operations": [
|
|
298
|
+
"get_persona_personas__persona_id__get",
|
|
299
|
+
"list_campaigns_campaigns_get",
|
|
300
|
+
"update_persona_personas__persona_id__patch",
|
|
301
|
+
"pin_persona_personas__persona_id__pin_post",
|
|
302
|
+
"unpin_persona_personas__persona_id__unpin_post",
|
|
303
|
+
"archive_persona_personas__persona_id__archive_post"
|
|
304
|
+
],
|
|
305
|
+
"tiers": [
|
|
306
|
+
"read",
|
|
307
|
+
"write",
|
|
308
|
+
"destructive"
|
|
309
|
+
]
|
|
248
310
|
}
|
|
249
311
|
],
|
|
250
312
|
"hooks": [
|
|
@@ -356,6 +418,7 @@
|
|
|
356
418
|
}
|
|
357
419
|
],
|
|
358
420
|
"peerDependencies": {
|
|
359
|
-
"react": ">=18"
|
|
421
|
+
"react": ">=18",
|
|
422
|
+
"lucide-react": ">=0.400.0"
|
|
360
423
|
}
|
|
361
424
|
}
|
|
@@ -27,13 +27,17 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
|
27
27
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
28
28
|
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
29
29
|
import { g8Delete, g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
|
|
30
|
-
import {
|
|
30
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
31
|
+
import { CREDIT_ESTIMATES, MAX_BODY_BYTES, byteLength, fieldItems, listOf, objectOf, statusClass, text } from "../lib/campaigns-utils";
|
|
31
32
|
import type { CampaignDocRow } from "../lib/campaigns-utils";
|
|
32
33
|
import { CampaignSelect } from "./campaign-select";
|
|
33
34
|
import { FormField } from "./form-field";
|
|
34
35
|
|
|
35
36
|
export interface CampaignAssetsProps {
|
|
36
37
|
campaignId?: string;
|
|
38
|
+
/** Controlled open document (a screen deep-links it). Omit to let the component keep it. */
|
|
39
|
+
documentId?: string | null;
|
|
40
|
+
onDocumentChange?: (documentId: string | null) => void;
|
|
37
41
|
className?: string;
|
|
38
42
|
}
|
|
39
43
|
|
|
@@ -74,6 +78,7 @@ function Editor({ campaignId, doc, onChanged, onClosed }: { campaignId: string;
|
|
|
74
78
|
<h4 className="font-medium">{doc.display_name}</h4>
|
|
75
79
|
<Button size="sm" variant="outline" onClick={onClosed}>Close</Button>
|
|
76
80
|
</div>
|
|
81
|
+
<KeyValueList columns={2} items={fieldItems({ type: doc.file_type, status: text(objectOf(full.data).status) || doc.status, folder: doc.folder_path, updated_at: doc.updated_at, size: `${byteLength(text(objectOf(full.data).content)).toLocaleString()} bytes` })} />
|
|
77
82
|
{full.error ? <ApiError error={full.error} title="Could not load the document" onRetry={() => void full.refetch()} /> : null}
|
|
78
83
|
{full.loading && !full.data ? <Skeleton className="h-32 w-full" /> : null}
|
|
79
84
|
{run.error ? <ApiError error={run.error} /> : null}
|
|
@@ -105,9 +110,15 @@ function Editor({ campaignId, doc, onChanged, onClosed }: { campaignId: string;
|
|
|
105
110
|
);
|
|
106
111
|
}
|
|
107
112
|
|
|
108
|
-
function Documents({ campaignId, className }: { campaignId: string; className?: string }) {
|
|
113
|
+
function Documents({ campaignId, documentId, onDocumentChange, className }: { campaignId: string; documentId?: string | null; onDocumentChange?: (id: string | null) => void; className?: string }) {
|
|
109
114
|
const docs = useG8Query((signal) => g8Get(`/campaigns/${campaignId}/documents`, undefined, { signal }), [campaignId]);
|
|
110
|
-
const [
|
|
115
|
+
const [localId, setLocalId] = useState<string | null>(null);
|
|
116
|
+
const openId = documentId !== undefined ? documentId : localId;
|
|
117
|
+
const setOpenId = (next: string | null | ((cur: string | null) => string | null)) => {
|
|
118
|
+
const value = typeof next === "function" ? next(openId) : next;
|
|
119
|
+
setLocalId(value);
|
|
120
|
+
onDocumentChange?.(value);
|
|
121
|
+
};
|
|
111
122
|
const [creating, setCreating] = useState(false);
|
|
112
123
|
const [form, setForm] = useState({ display_name: "", file_type: FILE_TYPES[0], content: "" });
|
|
113
124
|
const [confirmCreate, setConfirmCreate] = useState(false);
|
|
@@ -168,13 +179,13 @@ function Documents({ campaignId, className }: { campaignId: string; className?:
|
|
|
168
179
|
);
|
|
169
180
|
}
|
|
170
181
|
|
|
171
|
-
export function CampaignAssets({ campaignId, className }: CampaignAssetsProps) {
|
|
182
|
+
export function CampaignAssets({ campaignId, documentId, onDocumentChange, className }: CampaignAssetsProps) {
|
|
172
183
|
const [picked, setPicked] = useState<string | null>(null);
|
|
173
184
|
const id = campaignId ?? picked;
|
|
174
185
|
return (
|
|
175
186
|
<div className={`space-y-3 ${className ?? ""}`}>
|
|
176
187
|
{campaignId ? null : <CampaignSelect value={picked} onChange={setPicked} />}
|
|
177
|
-
{id ? <Documents key={id} campaignId={id} /> : null}
|
|
188
|
+
{id ? <Documents key={id} campaignId={id} documentId={documentId} onDocumentChange={onDocumentChange} /> : null}
|
|
178
189
|
</div>
|
|
179
190
|
);
|
|
180
191
|
}
|
|
@@ -2,31 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* CampaignLaunchStep: the last check before a campaign starts messaging real people. Shows what is missing
|
|
5
|
-
* (audience, steps, a sender), warns when deliverability is poor
|
|
6
|
-
*
|
|
5
|
+
* (audience, steps, written copy, a sender), warns when deliverability is poor or a chosen mailbox is still in
|
|
6
|
+
* its warm-up hold, lets the person write missing copy with AI, choose the sending mailboxes, and launches
|
|
7
|
+
* behind a confirmation that states the recipient count.
|
|
7
8
|
*
|
|
8
9
|
* SDK operations, in order:
|
|
9
|
-
* 1. getCampaignFull
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* 1. getCampaignFull GET /campaigns/{campaign_id}/full (read; audience and size, steps, documents, is_launched;
|
|
11
|
+
* POLLED while copy is being written)
|
|
12
|
+
* 2. getCampaignGenerationStatus GET /campaigns/{campaign_id}/status (read; `generation_error` when document generation failed)
|
|
13
|
+
* 3. listMailboxes GET /mailboxes (read; active senders to choose from, `created_at` for the hold)
|
|
14
|
+
* 4. getSafeToSend GET /deliverability/safe-to-send (read; a warning, never a block)
|
|
15
|
+
* 5. createCampaignDocument POST /campaigns/{campaign_id}/documents
|
|
16
|
+
* (external; only when no email copy document exists, part of "Write the email copy")
|
|
17
|
+
* regenerateCampaignDocument POST /campaigns/{campaign_id}/documents/{document_id}/regenerate
|
|
18
|
+
* (billable, ConfirmSpendDialog; writes the email copy with AI)
|
|
19
|
+
* 6. launchCampaign POST /campaigns/{campaign_id}/launch { sender_mailbox_ids }
|
|
20
|
+
* (external, ConfirmSpendDialog with the recipient count)
|
|
14
21
|
*
|
|
15
|
-
*
|
|
22
|
+
* Copy: the launch is refused (400 "no completed copy documents") unless a document other than the sequence and
|
|
23
|
+
* step catalog is completed, so Launch stays disabled until one is. A campaign created without AI documents has
|
|
24
|
+
* none; "Write the email copy with AI" creates the email document (once: its id is kept, and an existing one is
|
|
25
|
+
* reused, so a retry never makes a second) and has AI write it.
|
|
26
|
+
*
|
|
27
|
+
* Tiers: 6 begins REAL outreach and cannot be recalled. The result is kept in state, so the button is
|
|
16
28
|
* gone after a success and a double click cannot launch twice. After an error, do not blindly retry: a 502
|
|
17
29
|
* `draft_created_activation_failed` or `activation_state_uncertain` means a sequence may exist; check the
|
|
18
|
-
* campaign in the sequencer block first. `is_launched` from /full
|
|
30
|
+
* campaign in the sequencer block first. `is_launched` from /full (true only for a live launch, not a failed
|
|
31
|
+
* attempt) also refuses a second launch after a reload. Google and Microsoft mailboxes are connected in the graph8 app.
|
|
19
32
|
*/
|
|
20
33
|
|
|
21
|
-
import { useState } from "react";
|
|
34
|
+
import { useEffect, useState } from "react";
|
|
22
35
|
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
23
36
|
import { ApiError } from "@/g8/components/api-error";
|
|
24
37
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
25
38
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
26
39
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
40
|
+
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
27
41
|
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
28
|
-
import {
|
|
29
|
-
import { audienceOf, listOf, objectOf, stepsOf } from "../lib/campaigns-utils";
|
|
42
|
+
import { GRAPH8_APP_MAILBOXES_PATH } from "@/g8/mailboxes/lib/mailboxes-utils";
|
|
43
|
+
import { CREDIT_ESTIMATES, EMAILS_DOCUMENT, WARMUP_HOLD_DAYS, audienceOf, copyStateOf, listOf, objectOf, stepsOf, text, warmupHoldUntil } from "../lib/campaigns-utils";
|
|
44
|
+
import { brandUrl, useBrand } from "@/g8/hooks/use-brand";
|
|
30
45
|
|
|
31
46
|
export interface CampaignLaunchStepProps {
|
|
32
47
|
campaignId: string;
|
|
@@ -36,24 +51,43 @@ export interface CampaignLaunchStepProps {
|
|
|
36
51
|
className?: string;
|
|
37
52
|
}
|
|
38
53
|
|
|
39
|
-
interface Mailbox { id: string | number; email?: string | null; connection_status?: string | null; is_archived?: boolean | null }
|
|
54
|
+
interface Mailbox { id: string | number; email?: string | null; connection_status?: string | null; is_archived?: boolean | null; created_at?: string | null }
|
|
40
55
|
|
|
41
|
-
function Check({ ok, children }: { ok: boolean | undefined; children: string }) {
|
|
56
|
+
function Check({ ok, children, detail }: { ok: boolean | undefined; children: string; detail?: string }) {
|
|
42
57
|
return (
|
|
43
58
|
<li className={ok ? "text-green-700" : "text-amber-800"}>
|
|
44
59
|
<span aria-hidden>{ok ? "Ready: " : "Missing: "}</span>{children}
|
|
60
|
+
{!ok && detail ? <span className="block pl-4 text-xs text-muted-foreground">{detail}</span> : null}
|
|
45
61
|
</li>
|
|
46
62
|
);
|
|
47
63
|
}
|
|
48
64
|
|
|
65
|
+
const day = (d: Date) => d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" });
|
|
66
|
+
|
|
49
67
|
export function CampaignLaunchStep({ campaignId, audienceSize, onLaunched, className }: CampaignLaunchStepProps) {
|
|
68
|
+
const brand = useBrand();
|
|
50
69
|
const campaign = useG8Query((signal) => g8Get(`/campaigns/${campaignId}/full`, undefined, { signal }), [campaignId]);
|
|
70
|
+
const generation = useG8Query((signal) => g8Get(`/campaigns/${campaignId}/status`, undefined, { signal }), [campaignId]);
|
|
51
71
|
const mailboxes = useG8Query((signal) => g8Get("/mailboxes", { limit: 200 }, { signal }), []);
|
|
52
72
|
const verdict = useG8Query((signal) => g8Get("/deliverability/safe-to-send", undefined, { signal }), []);
|
|
53
73
|
const [chosen, setChosen] = useState<Set<string> | null>(null);
|
|
54
74
|
const [confirm, setConfirm] = useState(false);
|
|
75
|
+
const [confirmCopy, setConfirmCopy] = useState(false);
|
|
76
|
+
const [emailsDocId, setEmailsDocId] = useState<string | null>(null);
|
|
55
77
|
const [result, setResult] = useState<Record<string, unknown> | null>(null);
|
|
56
78
|
|
|
79
|
+
// Copy being written (by the create-time generator or by "Write the email copy"): re-read /full until it settles.
|
|
80
|
+
const copyPoll = useJobPoll((signal) => g8Get(`/campaigns/${campaignId}/full`, undefined, { signal }), {
|
|
81
|
+
enabled: false, isTerminal: (s) => !copyStateOf(s).generating, intervalMs: 3000, maxAttempts: 60,
|
|
82
|
+
});
|
|
83
|
+
const full = copyPoll.status ?? campaign.data;
|
|
84
|
+
const copy = copyStateOf(full);
|
|
85
|
+
const genError = text(objectOf(generation.data).generation_error);
|
|
86
|
+
// Auto-follow only once: after the poll gives up, "Still writing" asks the person to check back.
|
|
87
|
+
const shouldPoll = copy.generating && !genError && copyPoll.attempts === 0 && !copyPoll.polling;
|
|
88
|
+
useEffect(() => { if (shouldPoll) copyPoll.start(); // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
89
|
+
}, [shouldPoll]);
|
|
90
|
+
|
|
57
91
|
const active = listOf<Mailbox>(mailboxes.data).filter((m) => m.connection_status === "active" && !m.is_archived);
|
|
58
92
|
const senders = chosen ?? new Set(active.slice(0, 1).map((m) => String(m.id)));
|
|
59
93
|
const launch = useG8Mutation(
|
|
@@ -63,21 +97,41 @@ export function CampaignLaunchStep({ campaignId, audienceSize, onLaunched, class
|
|
|
63
97
|
onError: () => setConfirm(false),
|
|
64
98
|
},
|
|
65
99
|
);
|
|
100
|
+
const writeCopy = useG8Mutation(async () => {
|
|
101
|
+
let docId = emailsDocId ?? copy.emailsDocId;
|
|
102
|
+
if (!docId) {
|
|
103
|
+
const created = await g8Post(`/campaigns/${campaignId}/documents`, { ...EMAILS_DOCUMENT, content: "", status: "draft" });
|
|
104
|
+
docId = text(objectOf(created).id);
|
|
105
|
+
setEmailsDocId(docId); // stored at once: a failed regenerate is retried on this document, never a second one
|
|
106
|
+
}
|
|
107
|
+
return g8Post(`/campaigns/${campaignId}/documents/${docId}/regenerate`, {});
|
|
108
|
+
}, {
|
|
109
|
+
onSuccess: () => { setConfirmCopy(false); copyPoll.start(); },
|
|
110
|
+
onError: () => setConfirmCopy(false),
|
|
111
|
+
});
|
|
66
112
|
|
|
67
113
|
const loading = [campaign, mailboxes].some((q) => q.loading && !q.data);
|
|
68
114
|
const failedQuery = [campaign, mailboxes].find((q) => q.error);
|
|
69
115
|
if (failedQuery) return <ApiError error={failedQuery.error} title="Could not check launch readiness" onRetry={() => { void campaign.refetch(); void mailboxes.refetch(); }} />;
|
|
70
116
|
if (loading) return <Skeleton className="h-40 w-full" />;
|
|
71
117
|
|
|
72
|
-
const c = objectOf(
|
|
73
|
-
const audience = audienceOf(
|
|
118
|
+
const c = objectOf(full);
|
|
119
|
+
const audience = audienceOf(full);
|
|
74
120
|
const hasAudience = audience.listId != null;
|
|
75
|
-
const hasSteps = stepsOf(
|
|
121
|
+
const hasSteps = stepsOf(full).length > 0;
|
|
122
|
+
// An older API without `documents` cannot be checked here: the server still checks at launch.
|
|
123
|
+
const copyOk = !copy.known || copy.ready;
|
|
76
124
|
const alreadyLaunched = c.is_launched === true;
|
|
77
125
|
const recipients = audienceSize ?? audience.size;
|
|
78
126
|
const v = objectOf(verdict.data);
|
|
79
127
|
const unsafe = !verdict.error && (v.safe_to_send ?? v.safe) === false;
|
|
80
|
-
const ready = hasAudience && hasSteps && senders.size > 0 && !result && !alreadyLaunched;
|
|
128
|
+
const ready = hasAudience && hasSteps && copyOk && senders.size > 0 && !result && !alreadyLaunched;
|
|
129
|
+
// Once the follow-up poll gives up, the person may write the copy again rather than wait forever.
|
|
130
|
+
const writingCopy = writeCopy.loading || copyPoll.polling || (copy.generating && !genError && copyPoll.attempts === 0);
|
|
131
|
+
const held = active.filter((m) => senders.has(String(m.id))).map((m) => ({ m, until: warmupHoldUntil(m.created_at) })).filter((h) => h.until);
|
|
132
|
+
const copyDetail = writingCopy
|
|
133
|
+
? "The copy is being written. This usually takes a minute or two."
|
|
134
|
+
: "The launch is refused until the campaign has written copy. Write it with AI below, or add it in Campaign assets.";
|
|
81
135
|
|
|
82
136
|
if (result) {
|
|
83
137
|
const channels = (result.channels_attached as Array<{ email?: string }> | undefined) ?? [];
|
|
@@ -99,18 +153,36 @@ export function CampaignLaunchStep({ campaignId, audienceSize, onLaunched, class
|
|
|
99
153
|
<ul className="space-y-1 text-sm">
|
|
100
154
|
<Check ok={hasAudience}>An audience list is attached</Check>
|
|
101
155
|
<Check ok={hasSteps}>The sequence has at least one step</Check>
|
|
156
|
+
{copy.known ? <Check ok={copy.ready} detail={copyDetail}>The campaign copy is written</Check> : null}
|
|
102
157
|
<Check ok={senders.size > 0}>At least one active sending mailbox is selected</Check>
|
|
103
158
|
</ul>
|
|
159
|
+
{genError && !copy.ready ? (
|
|
160
|
+
<p role="alert" className="rounded bg-red-50 p-2 text-sm text-red-800">Writing the campaign documents failed: {genError}</p>
|
|
161
|
+
) : null}
|
|
162
|
+
{copy.known && !copy.ready ? (
|
|
163
|
+
<div className="space-y-1">
|
|
164
|
+
{writeCopy.error ? <ApiError error={writeCopy.error} title="Could not write the copy" /> : null}
|
|
165
|
+
{copyPoll.done && !copyStateOf(copyPoll.status).ready && !writeCopy.loading ? <p role="status" className="text-sm text-amber-900">The copy did not finish. Try again, or write it in Campaign assets.</p> : null}
|
|
166
|
+
{!copyPoll.polling && !copyPoll.done && copyPoll.attempts > 0 ? <p role="status" className="text-sm text-amber-900">Still writing. Check back in a minute.</p> : null}
|
|
167
|
+
<Button variant="outline" loading={writeCopy.loading} disabled={writingCopy} onClick={() => setConfirmCopy(true)}>Write the email copy with AI</Button>
|
|
168
|
+
</div>
|
|
169
|
+
) : null}
|
|
104
170
|
{unsafe ? (
|
|
105
171
|
<p role="alert" className="rounded bg-amber-50 p-2 text-sm text-amber-900">
|
|
106
172
|
Deliverability warning: {typeof v.reason === "string" ? v.reason : "sending is not recommended right now."}
|
|
107
173
|
</p>
|
|
108
174
|
) : null}
|
|
175
|
+
{held.length > 0 ? (
|
|
176
|
+
<div role="status" className="rounded bg-amber-50 p-2 text-sm text-amber-900">
|
|
177
|
+
<p>New mailboxes send nothing for their first {WARMUP_HOLD_DAYS} days while they warm up. The campaign goes live, but no email goes out from:</p>
|
|
178
|
+
<ul className="list-disc pl-5">{held.map(({ m, until }) => <li key={m.id}>{m.email ?? `Mailbox ${m.id}`} until {day(until as Date)}</li>)}</ul>
|
|
179
|
+
</div>
|
|
180
|
+
) : null}
|
|
109
181
|
<fieldset className="space-y-1">
|
|
110
182
|
<legend className="text-sm font-medium">Send from</legend>
|
|
111
183
|
{active.length === 0 ? (
|
|
112
184
|
<p className="text-sm text-muted-foreground">
|
|
113
|
-
No active mailbox. <a className="underline" href={
|
|
185
|
+
No active mailbox. <a className="underline" href={brandUrl(brand, GRAPH8_APP_MAILBOXES_PATH)} target="_blank" rel="noreferrer">Connect one in {brand.linkLabel}</a>, or use the mailboxes block.
|
|
114
186
|
</p>
|
|
115
187
|
) : null}
|
|
116
188
|
{active.map((m) => (
|
|
@@ -142,6 +214,17 @@ export function CampaignLaunchStep({ campaignId, audienceSize, onLaunched, class
|
|
|
142
214
|
onConfirm={() => void launch.mutate(undefined)}
|
|
143
215
|
onCancel={() => setConfirm(false)}
|
|
144
216
|
/>
|
|
217
|
+
<ConfirmSpendDialog
|
|
218
|
+
open={confirmCopy}
|
|
219
|
+
tier="billable"
|
|
220
|
+
title={`Write the email copy for "${String(c.name ?? "this campaign")}" with AI`}
|
|
221
|
+
description="AI writes the emails from the campaign brief and its steps. Nothing is sent. Estimate only."
|
|
222
|
+
estimatedCredits={CREDIT_ESTIMATES.documentRegenerate}
|
|
223
|
+
confirmLabel="Write copy"
|
|
224
|
+
confirming={writeCopy.loading}
|
|
225
|
+
onConfirm={() => void writeCopy.mutate(undefined)}
|
|
226
|
+
onCancel={() => setConfirmCopy(false)}
|
|
227
|
+
/>
|
|
145
228
|
</section>
|
|
146
229
|
);
|
|
147
230
|
}
|
|
@@ -25,8 +25,12 @@ import { formatDate, listOf, statusClass } from "../lib/campaigns-utils";
|
|
|
25
25
|
import type { CampaignRow } from "../lib/campaigns-utils";
|
|
26
26
|
import { CampaignAssets } from "./campaign-assets";
|
|
27
27
|
import { CampaignMetrics } from "./campaign-metrics";
|
|
28
|
+
import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
29
|
+
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
28
30
|
|
|
29
31
|
export interface CampaignListProps {
|
|
32
|
+
/** URL param the host screen reads for this record type. With it the name is a real link (Cmd-click opens it in a new tab); without it a plain button. */
|
|
33
|
+
linkParam?: string;
|
|
30
34
|
pageSize?: number;
|
|
31
35
|
/** Called when a row is opened. Without it the row opens metrics and documents below the table. */
|
|
32
36
|
onOpen?: (campaign: CampaignRow) => void;
|
|
@@ -37,7 +41,7 @@ export interface CampaignListProps {
|
|
|
37
41
|
|
|
38
42
|
const COLUMNS = 6;
|
|
39
43
|
|
|
40
|
-
export function CampaignList({ pageSize = 25, onOpen, emptyAction, className }: CampaignListProps) {
|
|
44
|
+
export function CampaignList({ linkParam, pageSize = 25, onOpen, emptyAction, className }: CampaignListProps) {
|
|
41
45
|
const [page, setPage] = useState(1);
|
|
42
46
|
const [openId, setOpenId] = useState<string | null>(null);
|
|
43
47
|
const [deleting, setDeleting] = useState<CampaignRow | null>(null);
|
|
@@ -74,8 +78,8 @@ export function CampaignList({ pageSize = 25, onOpen, emptyAction, className }:
|
|
|
74
78
|
<TableBody>
|
|
75
79
|
{list.loading && !list.data ? <TableRow><TableCell colSpan={COLUMNS}><Skeleton className="h-6 w-full" /></TableCell></TableRow> : null}
|
|
76
80
|
{rows.map((c) => (
|
|
77
|
-
<TableRow key={c.id} data-state={c.id === openId ? "selected" : undefined}>
|
|
78
|
-
<TableCell className="font-medium">{c.name}</TableCell>
|
|
81
|
+
<TableRow key={c.id} data-state={c.id === openId ? "selected" : undefined} {...rowActivationProps(() => open(c))}>
|
|
82
|
+
<TableCell className="font-medium"><RecordLink param={linkParam} id={c.id} onOpen={() => open(c)}>{c.name}</RecordLink></TableCell>
|
|
79
83
|
<TableCell><span className={`rounded px-2 py-0.5 text-xs ${statusClass(c.status)}`}>{c.status}</span></TableCell>
|
|
80
84
|
<TableCell>{c.category}</TableCell>
|
|
81
85
|
<TableCell className="max-w-xs truncate text-muted-foreground">{c.goal}</TableCell>
|
|
@@ -13,13 +13,18 @@
|
|
|
13
13
|
* getCampaignGenerationStatus GET /campaigns/{campaign_id}/status (read; POLLED while documents generate)
|
|
14
14
|
* 4. attach audience, steps/content, launch: see the three step components.
|
|
15
15
|
*
|
|
16
|
+
* Copy: AI writes the campaign documents by default (`autoGenerateDocuments`, default true). The launch is refused
|
|
17
|
+
* without written copy, so a campaign created without them needs its copy written before launch; the Launch step
|
|
18
|
+
* offers that. If the server could not queue the generation (`generation_status: "failed_to_dispatch"`), the
|
|
19
|
+
* wizard says so and the Launch step offers to write the copy.
|
|
20
|
+
*
|
|
16
21
|
* Retry safety: the campaign id is stored the moment create succeeds, so a failure in a later step never
|
|
17
22
|
* creates a second campaign; "Create" turns into "Save changes". "Start over" clears every field, the idea
|
|
18
23
|
* results and the campaign id (the campaign already created stays in graph8; delete it in CampaignList).
|
|
19
24
|
*/
|
|
20
25
|
|
|
21
26
|
import { useEffect, useState } from "react";
|
|
22
|
-
import { Button, Input, Textarea } from "@graph8/sdk/ui";
|
|
27
|
+
import { Button, Input, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
23
28
|
import { ApiError } from "@/g8/components/api-error";
|
|
24
29
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
25
30
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
@@ -40,6 +45,11 @@ export interface CampaignWizardProps {
|
|
|
40
45
|
/** Overrides the website from the saved profile. */
|
|
41
46
|
websiteUrl?: string;
|
|
42
47
|
onLaunched?: (result: Record<string, unknown>) => void;
|
|
48
|
+
/**
|
|
49
|
+
* Whether "Write the campaign documents with AI" starts checked. Default true: a campaign without written copy
|
|
50
|
+
* cannot launch. Pass false to start with it unchecked (the person writes the copy, or the Launch step does).
|
|
51
|
+
*/
|
|
52
|
+
autoGenerateDocuments?: boolean;
|
|
43
53
|
className?: string;
|
|
44
54
|
}
|
|
45
55
|
|
|
@@ -48,7 +58,7 @@ const GOALS = ["awareness", "consideration", "decision"];
|
|
|
48
58
|
const CHANNELS = ["email", "linkedin", "phone"];
|
|
49
59
|
|
|
50
60
|
interface Draft { name: string; category: string; goal: string; brief: string; core_concept: string; primary_hook: string; target_persona: string; channels: string[]; autoGenerate: boolean }
|
|
51
|
-
const emptyDraft: Draft = { name: "", category: "Outbound", goal: "", brief: "", core_concept: "", primary_hook: "", target_persona: "", channels: ["email"], autoGenerate:
|
|
61
|
+
const emptyDraft: Draft = { name: "", category: "Outbound", goal: "", brief: "", core_concept: "", primary_hook: "", target_persona: "", channels: ["email"], autoGenerate: true };
|
|
52
62
|
|
|
53
63
|
interface Idea { id?: string; campaign_name?: string; campaign_category?: string; core_concept?: string; why_this_works?: string; primary_hook?: string; target_channels?: string[] }
|
|
54
64
|
|
|
@@ -63,7 +73,7 @@ function IdeaStep({ websiteUrl, onPick, onSkip }: { websiteUrl: string; onPick:
|
|
|
63
73
|
const start = useG8Mutation(
|
|
64
74
|
() => g8Post("/campaigns/generate-ideas-async", {
|
|
65
75
|
organization_id: "current", website_url: websiteUrl, campaign_goal: goal, num_ideas: count, user_direction: direction || null,
|
|
66
|
-
target_personas: selected, persona_mode: selected.length ? "selected" : "
|
|
76
|
+
target_personas: selected, persona_mode: selected.length ? "selected" : "ai_pick", // the server's AI-picks-the-persona mode
|
|
67
77
|
}),
|
|
68
78
|
{ onSuccess: (res) => { setConfirm(false); setTaskId(text(objectOf(res).task_id) || null); }, onError: () => setConfirm(false) },
|
|
69
79
|
);
|
|
@@ -132,19 +142,32 @@ function IdeaStep({ websiteUrl, onPick, onSkip }: { websiteUrl: string; onPick:
|
|
|
132
142
|
}
|
|
133
143
|
|
|
134
144
|
function GenerationStatus({ campaignId }: { campaignId: string }) {
|
|
135
|
-
const poll = useJobPoll(async (signal) => objectOf(await g8Get(`/campaigns/${campaignId}/status`, undefined, { signal })), { isTerminal: settled, maxAttempts: 90 });
|
|
136
|
-
const
|
|
137
|
-
|
|
145
|
+
const poll = useJobPoll(async (signal) => objectOf(await g8Get(`/campaigns/${campaignId}/status`, undefined, { signal })), { isTerminal: (s) => Boolean(text(s.generation_error)) || settled(s), maxAttempts: 90 });
|
|
146
|
+
const failed = text(poll.status?.generation_error);
|
|
147
|
+
const label = failed ? "failed" : poll.polling ? "generating documents" : text(poll.status?.status) || "ready";
|
|
148
|
+
return <span role="status" title={failed || undefined} className={`rounded px-2 py-0.5 text-xs ${statusClass(failed ? "failed" : poll.polling ? "generating" : "completed")}`}>Documents: {label}</span>;
|
|
138
149
|
}
|
|
139
150
|
|
|
140
|
-
export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, className }: CampaignWizardProps) {
|
|
151
|
+
export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, autoGenerateDocuments = true, className }: CampaignWizardProps) {
|
|
141
152
|
const profile = useCompanyProfile(websiteUrl);
|
|
153
|
+
const initialDraft: Draft = { ...emptyDraft, autoGenerate: autoGenerateDocuments };
|
|
142
154
|
const [step, setStep] = useState(initialCampaignId ? 2 : 0);
|
|
143
|
-
const [draft, setDraft] = useState<Draft>(
|
|
155
|
+
const [draft, setDraft] = useState<Draft>(initialDraft);
|
|
156
|
+
const [dispatchFailed, setDispatchFailed] = useState(false);
|
|
144
157
|
const [campaignId, setCampaignId] = useState<string | null>(initialCampaignId ?? null);
|
|
145
158
|
const [audience, setAudience] = useState<ListSummary | null>(null);
|
|
146
159
|
const [confirm, setConfirm] = useState(false);
|
|
147
160
|
const [resetKey, setResetKey] = useState(0);
|
|
161
|
+
// Resuming a saved campaign: the form shows (and can save) nothing until the campaign itself has loaded, otherwise
|
|
162
|
+
// Save would PATCH the blank defaults over the real name, goal and brief.
|
|
163
|
+
const resume = useG8Query((signal) => g8Get(`/campaigns/${initialCampaignId}`, undefined, { signal }), [initialCampaignId], { enabled: Boolean(initialCampaignId) });
|
|
164
|
+
const [seeded, setSeeded] = useState(!initialCampaignId);
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (seeded || !resume.data) return;
|
|
167
|
+
const c = objectOf(resume.data);
|
|
168
|
+
setDraft((d) => ({ ...d, name: text(c.name), category: text(c.category) || d.category, goal: text(c.goal), brief: text(c.brief), core_concept: text(c.core_concept), primary_hook: text(c.primary_hook), target_persona: text(c.target_persona) }));
|
|
169
|
+
setSeeded(true);
|
|
170
|
+
}, [resume.data, seeded]);
|
|
148
171
|
|
|
149
172
|
const save = useG8Mutation(async () => {
|
|
150
173
|
const fields = { name: draft.name.trim(), category: draft.category, goal: draft.goal || null, brief: draft.brief || null, core_concept: draft.core_concept || null, primary_hook: draft.primary_hook || null, target_persona: draft.target_persona || null };
|
|
@@ -155,6 +178,7 @@ export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, clas
|
|
|
155
178
|
setConfirm(false);
|
|
156
179
|
const id = text(objectOf(res).id);
|
|
157
180
|
if (!campaignId && id) setCampaignId(id); // stored at once: a later failure never re-creates the campaign
|
|
181
|
+
if (!campaignId && objectOf(res).generation_status === "failed_to_dispatch") setDispatchFailed(true);
|
|
158
182
|
setStep(2);
|
|
159
183
|
},
|
|
160
184
|
onError: () => setConfirm(false),
|
|
@@ -165,7 +189,7 @@ export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, clas
|
|
|
165
189
|
setDraft({ ...draft, name: i.campaign_name ?? "", category: i.campaign_category ?? draft.category, core_concept: i.core_concept ?? "", primary_hook: i.primary_hook ?? "", brief: i.why_this_works ?? "", channels: i.target_channels?.length ? i.target_channels : draft.channels });
|
|
166
190
|
setStep(1);
|
|
167
191
|
};
|
|
168
|
-
const reset = () => { setStep(0); setDraft(
|
|
192
|
+
const reset = () => { setSeeded(true); setStep(0); setDraft(initialDraft); setDispatchFailed(false); setCampaignId(null); setAudience(null); setConfirm(false); save.reset(); setResetKey((k) => k + 1); };
|
|
169
193
|
const autoGen = !campaignId && draft.autoGenerate;
|
|
170
194
|
|
|
171
195
|
return (
|
|
@@ -181,8 +205,14 @@ export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, clas
|
|
|
181
205
|
<Button size="sm" variant="outline" className="ml-auto" onClick={reset}>Start over</Button>
|
|
182
206
|
</nav>
|
|
183
207
|
|
|
208
|
+
{dispatchFailed ? (
|
|
209
|
+
<p role="alert" className="rounded bg-amber-50 p-2 text-sm text-amber-900">
|
|
210
|
+
The campaign was created, but writing its documents could not be started. Write the email copy from the Launch step before launching.
|
|
211
|
+
</p>
|
|
212
|
+
) : null}
|
|
184
213
|
{step === 0 ? <IdeaStep key={resetKey} websiteUrl={profile.websiteUrl} onPick={pickIdea} onSkip={() => setStep(1)} /> : null}
|
|
185
|
-
{step === 1 ? (
|
|
214
|
+
{step === 1 && !seeded ? (resume.error ? <ApiError error={resume.error} title="Could not load this campaign" onRetry={() => void resume.refetch()} /> : <Skeleton className="h-48 w-full" aria-label="Loading campaign" />) : null}
|
|
215
|
+
{step === 1 && seeded ? (
|
|
186
216
|
<section aria-label="Campaign" className="space-y-3">
|
|
187
217
|
<FormField label="Campaign name" htmlFor="cw-name"><Input id="cw-name" value={draft.name} onChange={set("name")} /></FormField>
|
|
188
218
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
@@ -206,6 +236,9 @@ export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, clas
|
|
|
206
236
|
<input type="checkbox" checked={draft.autoGenerate} onChange={(e) => setDraft({ ...draft, autoGenerate: e.target.checked })} />
|
|
207
237
|
Write the campaign documents with AI after creating it (uses credits)
|
|
208
238
|
</label>
|
|
239
|
+
{!draft.autoGenerate && draft.channels.includes("email") ? (
|
|
240
|
+
<p className="text-sm text-muted-foreground">Without them the campaign has no email copy, and it cannot launch until the copy is written. The Launch step can write it with AI.</p>
|
|
241
|
+
) : null}
|
|
209
242
|
</>
|
|
210
243
|
) : null}
|
|
211
244
|
{save.error ? <ApiError error={save.error} title={campaignId ? "Could not save the campaign" : "Could not create the campaign"} /> : null}
|