@graph8/sdk 0.362.0 → 0.373.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 +33 -10
- package/blocks/_foundation/README.md +78 -1
- package/blocks/_foundation/block.json +123 -6
- package/blocks/_foundation/components/api-error.tsx +9 -7
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- 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 +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +2 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -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-load-more-list.ts +61 -0
- 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-filters.ts +93 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -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 +93 -2
- package/blocks/agents/block.json +343 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +29 -10
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +119 -0
- package/blocks/agents/components/agent-runs.tsx +19 -10
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +226 -0
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +222 -0
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +38 -11
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +33 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +4 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +38 -18
- package/blocks/appointments/components/meetings-screen.tsx +7 -4
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/appointments/components/scheduling-screen.tsx +2 -1
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +17 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +17 -6
- package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +163 -0
- package/blocks/campaigns/components/icp-editor.tsx +22 -17
- package/blocks/campaigns/components/persona-detail.tsx +181 -0
- package/blocks/campaigns/components/persona-editor.tsx +19 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
- 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/call-scorecard.tsx +7 -7
- package/blocks/coach/components/coach-screen.tsx +222 -0
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +161 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- 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 +13 -6
- package/blocks/crm/README.md +34 -0
- package/blocks/crm/block.json +107 -2
- package/blocks/crm/components/companies-screen.tsx +359 -0
- package/blocks/crm/components/company-detail.tsx +10 -8
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +10 -7
- package/blocks/crm/components/contact-lookup-link.tsx +44 -0
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +377 -0
- package/blocks/crm/components/dedupe-review.tsx +13 -6
- package/blocks/crm/components/import-wizard.tsx +7 -9
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +6 -4
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +399 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +9 -1
- package/blocks/crm/lib/crm-types.ts +26 -2
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +24 -6
- package/blocks/deals/block.json +160 -5
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +25 -13
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +31 -10
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +66 -26
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +5 -5
- package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +45 -4
- package/blocks/dialer/block.json +84 -3
- package/blocks/dialer/components/call-contact-button.tsx +269 -0
- package/blocks/dialer/components/call-log-screen.tsx +208 -0
- package/blocks/dialer/components/call-log.tsx +23 -7
- package/blocks/dialer/components/call-queue.tsx +4 -4
- package/blocks/dialer/components/dialer-screen.tsx +361 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +61 -12
- package/blocks/dialer/components/softphone.tsx +4 -2
- package/blocks/enrichment/README.md +29 -7
- package/blocks/enrichment/block.json +49 -9
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
- package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
- 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 +341 -0
- package/blocks/enrichment/components/waterfall-config.tsx +66 -17
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +8 -0
- package/blocks/forms/block.json +139 -18
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +4 -2
- package/blocks/forms/components/form-embed.tsx +4 -2
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +56 -19
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +17 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +32 -3
- package/blocks/inbox/block.json +29 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- 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 +63 -1
- package/blocks/integrations/block.json +234 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +119 -0
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +13 -8
- package/blocks/integrations/components/integrations-screen.tsx +152 -0
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +11 -9
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +179 -0
- package/blocks/integrations/components/webhook-manager.tsx +43 -25
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +11 -1
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +13 -10
- package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/block.json +13 -8
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +20 -1
- package/blocks/lists/block.json +69 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +20 -9
- package/blocks/lists/components/list-picker.tsx +17 -3
- package/blocks/lists/components/list-table.tsx +12 -6
- package/blocks/lists/components/lists-screen.tsx +346 -0
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +33 -8
- package/blocks/lists/components/suppressions-screen.tsx +264 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +69 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
- package/blocks/mailboxes/components/domain-detail.tsx +148 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
- package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
- package/blocks/mailboxes/components/warmup-health.tsx +24 -22
- package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +157 -24
- package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/block.json +28 -14
- package/blocks/nurture/components/nurture-builder.tsx +13 -3
- package/blocks/nurture/components/nurture-enroll.tsx +12 -10
- package/blocks/nurture/components/nurture-form.tsx +27 -10
- package/blocks/nurture/components/nurture-screen.tsx +26 -11
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +18 -1
- package/blocks/objects/block.json +42 -21
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +10 -2
- package/blocks/objects/components/object-schema-editor.tsx +4 -2
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +140 -23
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +9 -4
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-list.tsx +2 -1
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/quotes/README.md +13 -0
- package/blocks/quotes/block.json +65 -13
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +5 -3
- package/blocks/quotes/components/quote-related.tsx +8 -4
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- 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 +55 -9
- package/blocks/sequencer/block.json +126 -19
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +224 -60
- package/blocks/sequencer/components/sequence-detail.tsx +261 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
- package/blocks/sequencer/components/sequence-list.tsx +34 -9
- package/blocks/sequencer/components/sequence-stats.tsx +11 -10
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +162 -0
- package/blocks/sequencer/components/step-editor.tsx +14 -12
- package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
- 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 +19 -0
- package/blocks/social-ads/block.json +233 -1
- 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 +194 -0
- package/blocks/social-ads/components/audience-sync.tsx +17 -9
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +138 -0
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +155 -0
- package/blocks/social-ads/components/creative-gallery.tsx +35 -7
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +7 -3
- package/blocks/social-ads/components/post-detail.tsx +163 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +187 -0
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +21 -10
- package/blocks/visitors/components/traffic-chart.tsx +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +26 -16
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +16 -7
- 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 +8 -5
- package/blocks/webchat/components/conversation-list.tsx +8 -4
- 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 +5 -3
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +10 -4
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +202 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- 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 +4 -2
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +3109 -1977
- 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 +122 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +27 -20
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/orgBrand.d.ts +34 -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 +17 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +5 -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/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +55 -17
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +147 -37
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +64 -0
- package/stacks/clay/pages/companies.tsx.tmpl +31 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +73 -27
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +98 -35
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +104 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +181 -64
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +52 -20
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +68 -34
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +106 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +117 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +85 -31
- package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +115 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +40 -9
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/app/layout.tsx.tmpl +7 -2
- package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -2,31 +2,47 @@
|
|
|
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";
|
|
37
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
24
38
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
25
39
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
26
40
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
41
|
+
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
27
42
|
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
28
|
-
import {
|
|
29
|
-
import { audienceOf, listOf, objectOf, stepsOf } from "../lib/campaigns-utils";
|
|
43
|
+
import { GRAPH8_APP_MAILBOXES_PATH } from "@/g8/mailboxes/lib/mailboxes-utils";
|
|
44
|
+
import { CREDIT_ESTIMATES, EMAILS_DOCUMENT, WARMUP_HOLD_DAYS, audienceOf, copyStateOf, listOf, objectOf, stepsOf, text, warmupHoldUntil } from "../lib/campaigns-utils";
|
|
45
|
+
import { brandUrl, useBrand } from "@/g8/hooks/use-brand";
|
|
30
46
|
|
|
31
47
|
export interface CampaignLaunchStepProps {
|
|
32
48
|
campaignId: string;
|
|
@@ -36,24 +52,46 @@ export interface CampaignLaunchStepProps {
|
|
|
36
52
|
className?: string;
|
|
37
53
|
}
|
|
38
54
|
|
|
39
|
-
interface Mailbox { id: string | number; email?: string | null; connection_status?: string | null; is_archived?: boolean | null }
|
|
55
|
+
interface Mailbox { id: string | number; email?: string | null; connection_status?: string | null; is_archived?: boolean | null; created_at?: string | null }
|
|
40
56
|
|
|
41
|
-
function Check({ ok, children }: { ok: boolean | undefined; children: string }) {
|
|
57
|
+
function Check({ ok, children, detail }: { ok: boolean | undefined; children: string; detail?: string }) {
|
|
42
58
|
return (
|
|
43
|
-
<li className={ok ? "text-
|
|
59
|
+
<li className={ok ? "text-positive-foreground" : "text-warning-foreground"}>
|
|
44
60
|
<span aria-hidden>{ok ? "Ready: " : "Missing: "}</span>{children}
|
|
61
|
+
{!ok && detail ? <span className="block pl-4 text-xs text-muted-foreground">{detail}</span> : null}
|
|
45
62
|
</li>
|
|
46
63
|
);
|
|
47
64
|
}
|
|
48
65
|
|
|
66
|
+
const day = (d: Date) => d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" });
|
|
67
|
+
|
|
68
|
+
/** The mailboxes API has no paging: a full read is announced under the sender list. */
|
|
69
|
+
const MAILBOX_LIMIT = 200;
|
|
70
|
+
|
|
49
71
|
export function CampaignLaunchStep({ campaignId, audienceSize, onLaunched, className }: CampaignLaunchStepProps) {
|
|
72
|
+
const brand = useBrand();
|
|
50
73
|
const campaign = useG8Query((signal) => g8Get(`/campaigns/${campaignId}/full`, undefined, { signal }), [campaignId]);
|
|
51
|
-
const
|
|
74
|
+
const generation = useG8Query((signal) => g8Get(`/campaigns/${campaignId}/status`, undefined, { signal }), [campaignId]);
|
|
75
|
+
const mailboxes = useG8Query((signal) => g8Get("/mailboxes", { limit: MAILBOX_LIMIT }, { signal }), []);
|
|
52
76
|
const verdict = useG8Query((signal) => g8Get("/deliverability/safe-to-send", undefined, { signal }), []);
|
|
53
77
|
const [chosen, setChosen] = useState<Set<string> | null>(null);
|
|
54
78
|
const [confirm, setConfirm] = useState(false);
|
|
79
|
+
const [confirmCopy, setConfirmCopy] = useState(false);
|
|
80
|
+
const [emailsDocId, setEmailsDocId] = useState<string | null>(null);
|
|
55
81
|
const [result, setResult] = useState<Record<string, unknown> | null>(null);
|
|
56
82
|
|
|
83
|
+
// Copy being written (by the create-time generator or by "Write the email copy"): re-read /full until it settles.
|
|
84
|
+
const copyPoll = useJobPoll((signal) => g8Get(`/campaigns/${campaignId}/full`, undefined, { signal }), {
|
|
85
|
+
enabled: false, isTerminal: (s) => !copyStateOf(s).generating, intervalMs: 3000, maxAttempts: 60,
|
|
86
|
+
});
|
|
87
|
+
const full = copyPoll.status ?? campaign.data;
|
|
88
|
+
const copy = copyStateOf(full);
|
|
89
|
+
const genError = text(objectOf(generation.data).generation_error);
|
|
90
|
+
// Auto-follow only once: after the poll gives up, "Still writing" asks the person to check back.
|
|
91
|
+
const shouldPoll = copy.generating && !genError && copyPoll.attempts === 0 && !copyPoll.polling;
|
|
92
|
+
useEffect(() => { if (shouldPoll) copyPoll.start(); // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
93
|
+
}, [shouldPoll]);
|
|
94
|
+
|
|
57
95
|
const active = listOf<Mailbox>(mailboxes.data).filter((m) => m.connection_status === "active" && !m.is_archived);
|
|
58
96
|
const senders = chosen ?? new Set(active.slice(0, 1).map((m) => String(m.id)));
|
|
59
97
|
const launch = useG8Mutation(
|
|
@@ -63,54 +101,92 @@ export function CampaignLaunchStep({ campaignId, audienceSize, onLaunched, class
|
|
|
63
101
|
onError: () => setConfirm(false),
|
|
64
102
|
},
|
|
65
103
|
);
|
|
104
|
+
const writeCopy = useG8Mutation(async () => {
|
|
105
|
+
let docId = emailsDocId ?? copy.emailsDocId;
|
|
106
|
+
if (!docId) {
|
|
107
|
+
const created = await g8Post(`/campaigns/${campaignId}/documents`, { ...EMAILS_DOCUMENT, content: "", status: "draft" });
|
|
108
|
+
docId = text(objectOf(created).id);
|
|
109
|
+
setEmailsDocId(docId); // stored at once: a failed regenerate is retried on this document, never a second one
|
|
110
|
+
}
|
|
111
|
+
return g8Post(`/campaigns/${campaignId}/documents/${docId}/regenerate`, {});
|
|
112
|
+
}, {
|
|
113
|
+
onSuccess: () => { setConfirmCopy(false); copyPoll.start(); },
|
|
114
|
+
onError: () => setConfirmCopy(false),
|
|
115
|
+
});
|
|
66
116
|
|
|
67
117
|
const loading = [campaign, mailboxes].some((q) => q.loading && !q.data);
|
|
68
118
|
const failedQuery = [campaign, mailboxes].find((q) => q.error);
|
|
69
119
|
if (failedQuery) return <ApiError error={failedQuery.error} title="Could not check launch readiness" onRetry={() => { void campaign.refetch(); void mailboxes.refetch(); }} />;
|
|
70
120
|
if (loading) return <Skeleton className="h-40 w-full" />;
|
|
71
121
|
|
|
72
|
-
const c = objectOf(
|
|
73
|
-
const audience = audienceOf(
|
|
122
|
+
const c = objectOf(full);
|
|
123
|
+
const audience = audienceOf(full);
|
|
74
124
|
const hasAudience = audience.listId != null;
|
|
75
|
-
const hasSteps = stepsOf(
|
|
125
|
+
const hasSteps = stepsOf(full).length > 0;
|
|
126
|
+
// An older API without `documents` cannot be checked here: the server still checks at launch.
|
|
127
|
+
const copyOk = !copy.known || copy.ready;
|
|
76
128
|
const alreadyLaunched = c.is_launched === true;
|
|
77
129
|
const recipients = audienceSize ?? audience.size;
|
|
78
130
|
const v = objectOf(verdict.data);
|
|
79
131
|
const unsafe = !verdict.error && (v.safe_to_send ?? v.safe) === false;
|
|
80
|
-
const ready = hasAudience && hasSteps && senders.size > 0 && !result && !alreadyLaunched;
|
|
132
|
+
const ready = hasAudience && hasSteps && copyOk && senders.size > 0 && !result && !alreadyLaunched;
|
|
133
|
+
// Once the follow-up poll gives up, the person may write the copy again rather than wait forever.
|
|
134
|
+
const writingCopy = writeCopy.loading || copyPoll.polling || (copy.generating && !genError && copyPoll.attempts === 0);
|
|
135
|
+
const held = active.filter((m) => senders.has(String(m.id))).map((m) => ({ m, until: warmupHoldUntil(m.created_at) })).filter((h) => h.until);
|
|
136
|
+
const copyDetail = writingCopy
|
|
137
|
+
? "The copy is being written. This usually takes a minute or two."
|
|
138
|
+
: "The launch is refused until the campaign has written copy. Write it with AI below, or add it in Campaign assets.";
|
|
81
139
|
|
|
82
140
|
if (result) {
|
|
83
141
|
const channels = (result.channels_attached as Array<{ email?: string }> | undefined) ?? [];
|
|
84
142
|
return (
|
|
85
|
-
<section aria-label="Launch result" className={`space-y-2 rounded-md border border-
|
|
143
|
+
<section aria-label="Launch result" className={`space-y-2 rounded-md border border-positive-foreground/25 bg-positive p-4 ${className ?? ""}`}>
|
|
86
144
|
<h3 className="font-medium">Campaign {String(result.status ?? "launched")}</h3>
|
|
87
145
|
<p className="text-sm">{String(result.message ?? "")}</p>
|
|
88
146
|
<p className="text-sm text-muted-foreground">
|
|
89
147
|
Sequence {String(result.sequence_id ?? "")} with {String(result.steps_created ?? 0)} steps, sending from {channels.map((m) => m.email).join(", ") || "the chosen mailboxes"}.
|
|
90
148
|
</p>
|
|
91
|
-
{Array.isArray(result.warnings) && result.warnings.length > 0 ? <ul className="list-disc pl-5 text-sm text-
|
|
149
|
+
{Array.isArray(result.warnings) && result.warnings.length > 0 ? <ul className="list-disc pl-5 text-sm text-warning-foreground">{result.warnings.map((w) => <li key={String(w)}>{String(w)}</li>)}</ul> : null}
|
|
92
150
|
</section>
|
|
93
151
|
);
|
|
94
152
|
}
|
|
95
153
|
return (
|
|
96
154
|
<section aria-label="Launch" className={`space-y-3 ${className ?? ""}`}>
|
|
97
155
|
<h3 className="font-medium">Ready to launch {String(c.name ?? "")}</h3>
|
|
98
|
-
{alreadyLaunched ? <p role="status" className="rounded bg-
|
|
156
|
+
{alreadyLaunched ? <p role="status" className="rounded bg-positive p-2 text-sm text-positive-foreground">This campaign has already been launched. Manage it in the sequencer.</p> : null}
|
|
99
157
|
<ul className="space-y-1 text-sm">
|
|
100
158
|
<Check ok={hasAudience}>An audience list is attached</Check>
|
|
101
159
|
<Check ok={hasSteps}>The sequence has at least one step</Check>
|
|
160
|
+
{copy.known ? <Check ok={copy.ready} detail={copyDetail}>The campaign copy is written</Check> : null}
|
|
102
161
|
<Check ok={senders.size > 0}>At least one active sending mailbox is selected</Check>
|
|
103
162
|
</ul>
|
|
163
|
+
{genError && !copy.ready ? (
|
|
164
|
+
<p role="alert" className="rounded bg-error p-2 text-sm text-error-foreground">Writing the campaign documents failed: {genError}</p>
|
|
165
|
+
) : null}
|
|
166
|
+
{copy.known && !copy.ready ? (
|
|
167
|
+
<div className="space-y-1">
|
|
168
|
+
{writeCopy.error ? <ApiError error={writeCopy.error} title="Could not write the copy" /> : null}
|
|
169
|
+
{copyPoll.done && !copyStateOf(copyPoll.status).ready && !writeCopy.loading ? <p role="status" className="text-sm text-warning-foreground">The copy did not finish. Try again, or write it in Campaign assets.</p> : null}
|
|
170
|
+
{!copyPoll.polling && !copyPoll.done && copyPoll.attempts > 0 ? <p role="status" className="text-sm text-warning-foreground">Still writing. Check back in a minute.</p> : null}
|
|
171
|
+
<Button variant="outline" loading={writeCopy.loading} disabled={writingCopy} onClick={() => setConfirmCopy(true)}>Write the email copy with AI</Button>
|
|
172
|
+
</div>
|
|
173
|
+
) : null}
|
|
104
174
|
{unsafe ? (
|
|
105
|
-
<p role="alert" className="rounded bg-
|
|
175
|
+
<p role="alert" className="rounded bg-warning p-2 text-sm text-warning-foreground">
|
|
106
176
|
Deliverability warning: {typeof v.reason === "string" ? v.reason : "sending is not recommended right now."}
|
|
107
177
|
</p>
|
|
108
178
|
) : null}
|
|
179
|
+
{held.length > 0 ? (
|
|
180
|
+
<div role="status" className="rounded bg-warning p-2 text-sm text-warning-foreground">
|
|
181
|
+
<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>
|
|
182
|
+
<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>
|
|
183
|
+
</div>
|
|
184
|
+
) : null}
|
|
109
185
|
<fieldset className="space-y-1">
|
|
110
186
|
<legend className="text-sm font-medium">Send from</legend>
|
|
111
187
|
{active.length === 0 ? (
|
|
112
188
|
<p className="text-sm text-muted-foreground">
|
|
113
|
-
No active mailbox. <a className="underline" href={
|
|
189
|
+
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
190
|
</p>
|
|
115
191
|
) : null}
|
|
116
192
|
{active.map((m) => (
|
|
@@ -123,11 +199,12 @@ export function CampaignLaunchStep({ campaignId, audienceSize, onLaunched, class
|
|
|
123
199
|
{m.email ?? `Mailbox ${m.id}`}
|
|
124
200
|
</label>
|
|
125
201
|
))}
|
|
202
|
+
<FirstNotice shown={listOf<Mailbox>(mailboxes.data).length} limit={MAILBOX_LIMIT} noun="mailboxes" />
|
|
126
203
|
</fieldset>
|
|
127
204
|
{launch.error ? (
|
|
128
205
|
<div className="space-y-1">
|
|
129
206
|
<ApiError error={launch.error} title="The launch did not complete" />
|
|
130
|
-
<p className="text-sm text-
|
|
207
|
+
<p className="text-sm text-warning-foreground">Do not retry blindly. A sequence may already exist; check the campaign before launching again.</p>
|
|
131
208
|
</div>
|
|
132
209
|
) : null}
|
|
133
210
|
<Button disabled={!ready} loading={launch.loading} onClick={() => setConfirm(true)}>Launch campaign</Button>
|
|
@@ -142,6 +219,17 @@ export function CampaignLaunchStep({ campaignId, audienceSize, onLaunched, class
|
|
|
142
219
|
onConfirm={() => void launch.mutate(undefined)}
|
|
143
220
|
onCancel={() => setConfirm(false)}
|
|
144
221
|
/>
|
|
222
|
+
<ConfirmSpendDialog
|
|
223
|
+
open={confirmCopy}
|
|
224
|
+
tier="billable"
|
|
225
|
+
title={`Write the email copy for "${String(c.name ?? "this campaign")}" with AI`}
|
|
226
|
+
description="AI writes the emails from the campaign brief and its steps. Nothing is sent. Estimate only."
|
|
227
|
+
estimatedCredits={CREDIT_ESTIMATES.documentRegenerate}
|
|
228
|
+
confirmLabel="Write copy"
|
|
229
|
+
confirming={writeCopy.loading}
|
|
230
|
+
onConfirm={() => void writeCopy.mutate(undefined)}
|
|
231
|
+
onCancel={() => setConfirmCopy(false)}
|
|
232
|
+
/>
|
|
145
233
|
</section>
|
|
146
234
|
);
|
|
147
235
|
}
|
|
@@ -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>
|
|
@@ -40,7 +40,7 @@ function Tiles({ data }: { data: Record<string, unknown> }) {
|
|
|
40
40
|
{tiles.map(([k, v]) => (
|
|
41
41
|
<div key={k} className="rounded-md border p-3">
|
|
42
42
|
<dt className="text-xs capitalize text-muted-foreground">{k}</dt>
|
|
43
|
-
<dd className="text-xl font-
|
|
43
|
+
<dd className="text-xl font-medium">{v}</dd>
|
|
44
44
|
</div>
|
|
45
45
|
))}
|
|
46
46
|
</dl>
|
|
@@ -13,14 +13,20 @@
|
|
|
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";
|
|
29
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
24
30
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
25
31
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
26
32
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
@@ -40,6 +46,11 @@ export interface CampaignWizardProps {
|
|
|
40
46
|
/** Overrides the website from the saved profile. */
|
|
41
47
|
websiteUrl?: string;
|
|
42
48
|
onLaunched?: (result: Record<string, unknown>) => void;
|
|
49
|
+
/**
|
|
50
|
+
* Whether "Write the campaign documents with AI" starts checked. Default true: a campaign without written copy
|
|
51
|
+
* cannot launch. Pass false to start with it unchecked (the person writes the copy, or the Launch step does).
|
|
52
|
+
*/
|
|
53
|
+
autoGenerateDocuments?: boolean;
|
|
43
54
|
className?: string;
|
|
44
55
|
}
|
|
45
56
|
|
|
@@ -48,12 +59,15 @@ const GOALS = ["awareness", "consideration", "decision"];
|
|
|
48
59
|
const CHANNELS = ["email", "linkedin", "phone"];
|
|
49
60
|
|
|
50
61
|
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:
|
|
62
|
+
const emptyDraft: Draft = { name: "", category: "Outbound", goal: "", brief: "", core_concept: "", primary_hook: "", target_persona: "", channels: ["email"], autoGenerate: true };
|
|
52
63
|
|
|
53
64
|
interface Idea { id?: string; campaign_name?: string; campaign_category?: string; core_concept?: string; why_this_works?: string; primary_hook?: string; target_channels?: string[] }
|
|
54
65
|
|
|
66
|
+
/** The persona API has no paging: a full read is announced under the checkboxes. */
|
|
67
|
+
const PERSONA_LIMIT = 50;
|
|
68
|
+
|
|
55
69
|
function IdeaStep({ websiteUrl, onPick, onSkip }: { websiteUrl: string; onPick: (i: Idea) => void; onSkip: () => void }) {
|
|
56
|
-
const personas = useG8Query((signal) => g8Get("/personas", { limit:
|
|
70
|
+
const personas = useG8Query((signal) => g8Get("/personas", { limit: PERSONA_LIMIT }, { signal }), []);
|
|
57
71
|
const [goal, setGoal] = useState(GOALS[0]);
|
|
58
72
|
const [count, setCount] = useState(5);
|
|
59
73
|
const [direction, setDirection] = useState("");
|
|
@@ -63,7 +77,7 @@ function IdeaStep({ websiteUrl, onPick, onSkip }: { websiteUrl: string; onPick:
|
|
|
63
77
|
const start = useG8Mutation(
|
|
64
78
|
() => g8Post("/campaigns/generate-ideas-async", {
|
|
65
79
|
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" : "
|
|
80
|
+
target_personas: selected, persona_mode: selected.length ? "selected" : "ai_pick", // the server's AI-picks-the-persona mode
|
|
67
81
|
}),
|
|
68
82
|
{ onSuccess: (res) => { setConfirm(false); setTaskId(text(objectOf(res).task_id) || null); }, onError: () => setConfirm(false) },
|
|
69
83
|
);
|
|
@@ -94,6 +108,7 @@ function IdeaStep({ websiteUrl, onPick, onSkip }: { websiteUrl: string; onPick:
|
|
|
94
108
|
<input type="checkbox" checked={selected.includes(p.title ?? "")} onChange={(e) => setSelected(e.target.checked ? [...selected, p.title ?? ""] : selected.filter((t) => t !== p.title))} />{p.title}
|
|
95
109
|
</label>
|
|
96
110
|
))}
|
|
111
|
+
<FirstNotice shown={personaRows.length} limit={PERSONA_LIMIT} noun="personas" />
|
|
97
112
|
</fieldset>
|
|
98
113
|
) : null}
|
|
99
114
|
{[start.error, poll.error].map((e, i) => (e ? <ApiError key={i} error={e} title="Idea generation failed" /> : null))}
|
|
@@ -101,10 +116,10 @@ function IdeaStep({ websiteUrl, onPick, onSkip }: { websiteUrl: string; onPick:
|
|
|
101
116
|
<Button disabled={!websiteUrl || poll.polling} loading={start.loading} onClick={() => setConfirm(true)}>Generate ideas</Button>
|
|
102
117
|
<Button variant="outline" onClick={onSkip}>Skip, write my own</Button>
|
|
103
118
|
</div>
|
|
104
|
-
{!websiteUrl ? <p className="text-sm text-
|
|
119
|
+
{!websiteUrl ? <p className="text-sm text-warning-foreground">Save the company website in IcpEditor first; ideas are generated from it.</p> : null}
|
|
105
120
|
{poll.polling ? <p role="status" className="text-sm text-muted-foreground">Generating ideas. This takes 10 to 20 seconds.</p> : null}
|
|
106
|
-
{!poll.polling && !poll.done && poll.attempts > 0 && !poll.error ? <p role="status" className="text-sm text-
|
|
107
|
-
{failed ? <p role="alert" className="text-sm text-
|
|
121
|
+
{!poll.polling && !poll.done && poll.attempts > 0 && !poll.error ? <p role="status" className="text-sm text-warning-foreground">Still running. Check back in a minute and generate again if nothing appears.</p> : null}
|
|
122
|
+
{failed ? <p role="alert" className="text-sm text-error-foreground">Generation failed: {text(poll.status?.error) || "try again"}.</p> : null}
|
|
108
123
|
{poll.done && !failed && ideas.length === 0 ? <p className="text-sm text-muted-foreground">No ideas came back. Try a different goal or direction.</p> : null}
|
|
109
124
|
<ul className="space-y-2">
|
|
110
125
|
{ideas.map((idea, i) => (
|
|
@@ -132,19 +147,32 @@ function IdeaStep({ websiteUrl, onPick, onSkip }: { websiteUrl: string; onPick:
|
|
|
132
147
|
}
|
|
133
148
|
|
|
134
149
|
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
|
-
|
|
150
|
+
const poll = useJobPoll(async (signal) => objectOf(await g8Get(`/campaigns/${campaignId}/status`, undefined, { signal })), { isTerminal: (s) => Boolean(text(s.generation_error)) || settled(s), maxAttempts: 90 });
|
|
151
|
+
const failed = text(poll.status?.generation_error);
|
|
152
|
+
const label = failed ? "failed" : poll.polling ? "generating documents" : text(poll.status?.status) || "ready";
|
|
153
|
+
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
154
|
}
|
|
139
155
|
|
|
140
|
-
export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, className }: CampaignWizardProps) {
|
|
156
|
+
export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, autoGenerateDocuments = true, className }: CampaignWizardProps) {
|
|
141
157
|
const profile = useCompanyProfile(websiteUrl);
|
|
158
|
+
const initialDraft: Draft = { ...emptyDraft, autoGenerate: autoGenerateDocuments };
|
|
142
159
|
const [step, setStep] = useState(initialCampaignId ? 2 : 0);
|
|
143
|
-
const [draft, setDraft] = useState<Draft>(
|
|
160
|
+
const [draft, setDraft] = useState<Draft>(initialDraft);
|
|
161
|
+
const [dispatchFailed, setDispatchFailed] = useState(false);
|
|
144
162
|
const [campaignId, setCampaignId] = useState<string | null>(initialCampaignId ?? null);
|
|
145
163
|
const [audience, setAudience] = useState<ListSummary | null>(null);
|
|
146
164
|
const [confirm, setConfirm] = useState(false);
|
|
147
165
|
const [resetKey, setResetKey] = useState(0);
|
|
166
|
+
// Resuming a saved campaign: the form shows (and can save) nothing until the campaign itself has loaded, otherwise
|
|
167
|
+
// Save would PATCH the blank defaults over the real name, goal and brief.
|
|
168
|
+
const resume = useG8Query((signal) => g8Get(`/campaigns/${initialCampaignId}`, undefined, { signal }), [initialCampaignId], { enabled: Boolean(initialCampaignId) });
|
|
169
|
+
const [seeded, setSeeded] = useState(!initialCampaignId);
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (seeded || !resume.data) return;
|
|
172
|
+
const c = objectOf(resume.data);
|
|
173
|
+
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) }));
|
|
174
|
+
setSeeded(true);
|
|
175
|
+
}, [resume.data, seeded]);
|
|
148
176
|
|
|
149
177
|
const save = useG8Mutation(async () => {
|
|
150
178
|
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 +183,7 @@ export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, clas
|
|
|
155
183
|
setConfirm(false);
|
|
156
184
|
const id = text(objectOf(res).id);
|
|
157
185
|
if (!campaignId && id) setCampaignId(id); // stored at once: a later failure never re-creates the campaign
|
|
186
|
+
if (!campaignId && objectOf(res).generation_status === "failed_to_dispatch") setDispatchFailed(true);
|
|
158
187
|
setStep(2);
|
|
159
188
|
},
|
|
160
189
|
onError: () => setConfirm(false),
|
|
@@ -165,7 +194,7 @@ export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, clas
|
|
|
165
194
|
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
195
|
setStep(1);
|
|
167
196
|
};
|
|
168
|
-
const reset = () => { setStep(0); setDraft(
|
|
197
|
+
const reset = () => { setSeeded(true); setStep(0); setDraft(initialDraft); setDispatchFailed(false); setCampaignId(null); setAudience(null); setConfirm(false); save.reset(); setResetKey((k) => k + 1); };
|
|
169
198
|
const autoGen = !campaignId && draft.autoGenerate;
|
|
170
199
|
|
|
171
200
|
return (
|
|
@@ -181,8 +210,14 @@ export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, clas
|
|
|
181
210
|
<Button size="sm" variant="outline" className="ml-auto" onClick={reset}>Start over</Button>
|
|
182
211
|
</nav>
|
|
183
212
|
|
|
213
|
+
{dispatchFailed ? (
|
|
214
|
+
<p role="alert" className="rounded bg-warning p-2 text-sm text-warning-foreground">
|
|
215
|
+
The campaign was created, but writing its documents could not be started. Write the email copy from the Launch step before launching.
|
|
216
|
+
</p>
|
|
217
|
+
) : null}
|
|
184
218
|
{step === 0 ? <IdeaStep key={resetKey} websiteUrl={profile.websiteUrl} onPick={pickIdea} onSkip={() => setStep(1)} /> : null}
|
|
185
|
-
{step === 1 ? (
|
|
219
|
+
{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}
|
|
220
|
+
{step === 1 && seeded ? (
|
|
186
221
|
<section aria-label="Campaign" className="space-y-3">
|
|
187
222
|
<FormField label="Campaign name" htmlFor="cw-name"><Input id="cw-name" value={draft.name} onChange={set("name")} /></FormField>
|
|
188
223
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
@@ -206,6 +241,9 @@ export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, clas
|
|
|
206
241
|
<input type="checkbox" checked={draft.autoGenerate} onChange={(e) => setDraft({ ...draft, autoGenerate: e.target.checked })} />
|
|
207
242
|
Write the campaign documents with AI after creating it (uses credits)
|
|
208
243
|
</label>
|
|
244
|
+
{!draft.autoGenerate && draft.channels.includes("email") ? (
|
|
245
|
+
<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>
|
|
246
|
+
) : null}
|
|
209
247
|
</>
|
|
210
248
|
) : null}
|
|
211
249
|
{save.error ? <ApiError error={save.error} title={campaignId ? "Could not save the campaign" : "Could not create the campaign"} /> : null}
|