@graph8/sdk 0.308.1 → 0.357.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 +81 -0
- package/blocks/README.md +32 -0
- package/blocks/_foundation/README.md +78 -0
- package/blocks/_foundation/block.json +79 -0
- package/blocks/_foundation/components/api-error.tsx +62 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +142 -0
- package/blocks/_foundation/components/empty-state.tsx +24 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +88 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +87 -0
- package/blocks/_foundation/hooks/use-job-poll.ts +130 -0
- package/blocks/_foundation/hooks/use-paginated.ts +105 -0
- package/blocks/_foundation/lib/g8-client.ts +128 -0
- package/blocks/_foundation/recipes/verify-proxy-key.ts +18 -0
- package/blocks/_foundation/server/proxy.ts +233 -0
- package/blocks/_foundation/server/route.ts +30 -0
- package/blocks/agents/README.md +133 -0
- package/blocks/agents/block.json +223 -0
- package/blocks/agents/components/agent-builder.tsx +199 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +144 -0
- package/blocks/agents/components/agent-runs.tsx +175 -0
- package/blocks/agents/components/agent-skills-panel.tsx +126 -0
- package/blocks/agents/components/agent-test-console.tsx +175 -0
- package/blocks/agents/lib/agent-types.ts +114 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +50 -0
- package/blocks/agents/recipes/run-and-follow-agent-run.ts +46 -0
- package/blocks/agents/recipes/test-agent-before-activating.ts +36 -0
- package/blocks/analytics/README.md +73 -0
- package/blocks/analytics/block.json +178 -0
- package/blocks/analytics/components/funnel-chart.tsx +79 -0
- package/blocks/analytics/components/kpi-tiles.tsx +74 -0
- package/blocks/analytics/components/leaderboard.tsx +86 -0
- package/blocks/analytics/components/report-builder.tsx +200 -0
- package/blocks/analytics/hooks/use-report.ts +26 -0
- package/blocks/analytics/lib/analytics-types.ts +108 -0
- package/blocks/analytics/lib/use-debounced.ts +16 -0
- package/blocks/analytics/recipes/01-weekly-outbound-report.ts +32 -0
- package/blocks/analytics/recipes/02-revenue-snapshot.ts +22 -0
- package/blocks/analytics/recipes/03-saved-report.ts +30 -0
- package/blocks/appointments/README.md +80 -0
- package/blocks/appointments/block.json +218 -0
- package/blocks/appointments/components/availability-editor.tsx +192 -0
- package/blocks/appointments/components/booking-page.tsx +154 -0
- package/blocks/appointments/components/event-type-editor.tsx +214 -0
- package/blocks/appointments/components/meeting-detail.tsx +147 -0
- package/blocks/appointments/components/meeting-list.tsx +148 -0
- package/blocks/appointments/hooks/use-available-slots.ts +29 -0
- package/blocks/appointments/hooks/use-schedules.ts +18 -0
- package/blocks/appointments/lib/appointments-types.ts +159 -0
- package/blocks/appointments/lib/idempotency.ts +30 -0
- package/blocks/appointments/recipes/01-create-event-type-and-hours.ts +27 -0
- package/blocks/appointments/recipes/02-find-slots-and-book.ts +34 -0
- package/blocks/appointments/recipes/03-bookings-digest.ts +22 -0
- package/blocks/appointments/recipes/04-calendar-invite.ts +21 -0
- package/blocks/block.schema.json +130 -0
- package/blocks/block.types.ts +109 -0
- package/blocks/campaigns/README.md +118 -0
- package/blocks/campaigns/block.json +361 -0
- package/blocks/campaigns/components/campaign-assets.tsx +180 -0
- package/blocks/campaigns/components/campaign-audience-step.tsx +91 -0
- package/blocks/campaigns/components/campaign-launch-step.tsx +147 -0
- package/blocks/campaigns/components/campaign-list.tsx +120 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +93 -0
- package/blocks/campaigns/components/campaign-select.tsx +52 -0
- package/blocks/campaigns/components/campaign-sequence-step.tsx +158 -0
- package/blocks/campaigns/components/campaign-wizard.tsx +238 -0
- package/blocks/campaigns/components/form-field.tsx +29 -0
- package/blocks/campaigns/components/icp-editor.tsx +230 -0
- package/blocks/campaigns/components/persona-editor.tsx +210 -0
- package/blocks/campaigns/hooks/use-company-profile.ts +34 -0
- package/blocks/campaigns/lib/campaigns-utils.ts +155 -0
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +33 -0
- package/blocks/campaigns/recipes/campaign-report.ts +22 -0
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +51 -0
- package/blocks/campaigns/recipes/set-up-icp-and-personas.ts +37 -0
- package/blocks/coach/README.md +93 -0
- package/blocks/coach/block.json +125 -0
- package/blocks/coach/components/call-scorecard.tsx +126 -0
- package/blocks/coach/components/coaching-feed.tsx +140 -0
- package/blocks/coach/lib/coach-types.ts +116 -0
- package/blocks/coach/recipes/read-a-scorecard.ts +27 -0
- package/blocks/coach/recipes/weekly-coaching-digest.ts +53 -0
- package/blocks/copilot/README.md +99 -0
- package/blocks/copilot/block.json +151 -0
- package/blocks/copilot/components/copilot-memory.tsx +87 -0
- package/blocks/copilot/components/copilot-panel.tsx +189 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +111 -0
- package/blocks/copilot/lib/copilot-types.ts +111 -0
- package/blocks/copilot/recipes/ask-the-copilot.ts +35 -0
- package/blocks/copilot/recipes/post-in-a-work-channel.ts +33 -0
- package/blocks/copilot/recipes/remember-and-recall.ts +19 -0
- package/blocks/crm/README.md +122 -0
- package/blocks/crm/block.json +378 -0
- package/blocks/crm/components/activity-timeline.tsx +99 -0
- package/blocks/crm/components/company-detail.tsx +118 -0
- package/blocks/crm/components/company-table.tsx +173 -0
- package/blocks/crm/components/contact-detail.tsx +135 -0
- package/blocks/crm/components/contact-table.tsx +217 -0
- package/blocks/crm/components/dedupe-review.tsx +164 -0
- package/blocks/crm/components/import-wizard.tsx +193 -0
- package/blocks/crm/components/notes-panel.tsx +112 -0
- package/blocks/crm/components/record-form.tsx +191 -0
- package/blocks/crm/components/task-list.tsx +107 -0
- package/blocks/crm/hooks/use-custom-fields.ts +18 -0
- package/blocks/crm/hooks/use-debounced.ts +13 -0
- package/blocks/crm/hooks/use-page-query.ts +52 -0
- package/blocks/crm/lib/crm-types.ts +136 -0
- package/blocks/crm/recipes/custom-fields-and-values.ts +28 -0
- package/blocks/crm/recipes/dedupe-and-merge.ts +30 -0
- package/blocks/crm/recipes/notes-tasks-and-activity.ts +23 -0
- package/blocks/crm/recipes/upsert-contact-with-company.ts +33 -0
- package/blocks/deals/README.md +78 -0
- package/blocks/deals/block.json +198 -0
- package/blocks/deals/components/deal-detail.tsx +134 -0
- package/blocks/deals/components/forecast-view.tsx +112 -0
- package/blocks/deals/components/pipeline-board.tsx +142 -0
- package/blocks/deals/components/stage-editor.tsx +96 -0
- package/blocks/deals/hooks/use-pipeline-deals.ts +67 -0
- package/blocks/deals/hooks/use-pipelines.ts +19 -0
- package/blocks/deals/lib/deals-types.ts +128 -0
- package/blocks/deals/recipes/01-create-deal-and-advance.ts +32 -0
- package/blocks/deals/recipes/02-pipeline-report.ts +35 -0
- package/blocks/deals/recipes/03-stale-deals-sweep.ts +20 -0
- package/blocks/dialer/README.md +114 -0
- package/blocks/dialer/block.json +260 -0
- package/blocks/dialer/components/call-log.tsx +78 -0
- package/blocks/dialer/components/call-queue.tsx +188 -0
- package/blocks/dialer/components/dialer-workspace.tsx +47 -0
- package/blocks/dialer/components/disposition-panel.tsx +75 -0
- package/blocks/dialer/components/live-calls-table.tsx +107 -0
- package/blocks/dialer/components/session-start-form.tsx +111 -0
- package/blocks/dialer/components/softphone.tsx +122 -0
- package/blocks/dialer/components/transcript-viewer.tsx +82 -0
- package/blocks/dialer/hooks/use-dialer-numbers.ts +19 -0
- package/blocks/dialer/hooks/use-dialer-session.ts +68 -0
- package/blocks/dialer/hooks/use-dispositions.ts +29 -0
- package/blocks/dialer/lib/dialer-types.ts +158 -0
- package/blocks/dialer/lib/softphone-transport.ts +67 -0
- package/blocks/dialer/lib/twilio-transport.ts +74 -0
- package/blocks/dialer/recipes/01-create-session-and-dial.ts +39 -0
- package/blocks/dialer/recipes/02-disposition-calls.ts +26 -0
- package/blocks/dialer/recipes/03-review-a-call.ts +23 -0
- package/blocks/enrichment/README.md +105 -0
- package/blocks/enrichment/block.json +204 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +205 -0
- package/blocks/enrichment/components/email-verify-badge.tsx +82 -0
- package/blocks/enrichment/components/enrich-button.tsx +136 -0
- package/blocks/enrichment/components/waterfall-config.tsx +127 -0
- package/blocks/enrichment/hooks/use-providers.ts +18 -0
- package/blocks/enrichment/lib/enrichment-types.ts +104 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +41 -0
- package/blocks/enrichment/recipes/lookup-and-verify.ts +25 -0
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +26 -0
- package/blocks/forms/README.md +112 -0
- package/blocks/forms/block.json +200 -0
- package/blocks/forms/components/form-builder.tsx +150 -0
- package/blocks/forms/components/form-embed.tsx +103 -0
- package/blocks/forms/components/form-mapping-editor.tsx +125 -0
- package/blocks/forms/components/site-audit-panel.tsx +73 -0
- package/blocks/forms/components/submissions-table.tsx +158 -0
- package/blocks/forms/lib/form-types.ts +127 -0
- package/blocks/forms/recipes/install-snippet-and-form.ts +21 -0
- package/blocks/forms/recipes/retry-failed-deliveries.ts +26 -0
- package/blocks/forms/recipes/route-form-to-list.ts +44 -0
- package/blocks/inbox/README.md +72 -0
- package/blocks/inbox/block.json +194 -0
- package/blocks/inbox/components/inbox-list.tsx +156 -0
- package/blocks/inbox/components/inbox-screen.tsx +49 -0
- package/blocks/inbox/components/reply-composer.tsx +183 -0
- package/blocks/inbox/components/tag-and-assign.tsx +189 -0
- package/blocks/inbox/components/thread-badge.tsx +17 -0
- package/blocks/inbox/components/thread-view.tsx +108 -0
- package/blocks/inbox/hooks/use-inbox-thread.ts +24 -0
- package/blocks/inbox/hooks/use-inbox-threads.ts +40 -0
- package/blocks/inbox/lib/idempotency.ts +30 -0
- package/blocks/inbox/lib/inbox-types.ts +100 -0
- package/blocks/inbox/recipes/01-triage-and-reply.ts +43 -0
- package/blocks/inbox/recipes/02-tag-and-assign.ts +31 -0
- package/blocks/inbox/recipes/03-hot-contacts-digest.ts +16 -0
- package/blocks/integrations/README.md +75 -0
- package/blocks/integrations/block.json +172 -0
- package/blocks/integrations/components/integration-list.tsx +165 -0
- package/blocks/integrations/components/sync-status.tsx +223 -0
- package/blocks/integrations/components/webhook-manager.tsx +300 -0
- package/blocks/integrations/lib/integrations-utils.ts +62 -0
- package/blocks/integrations/recipes/crm-sync-health.ts +19 -0
- package/blocks/integrations/recipes/push-contacts-to-crm.ts +20 -0
- package/blocks/integrations/recipes/register-webhook.ts +18 -0
- package/blocks/integrations/recipes/verify-webhook-route.ts +33 -0
- package/blocks/intent/README.md +57 -0
- package/blocks/intent/block.json +138 -0
- package/blocks/intent/components/account-intent-score.tsx +91 -0
- package/blocks/intent/components/intent-feed.tsx +215 -0
- package/blocks/intent/components/intent-keyword-manager.tsx +331 -0
- package/blocks/intent/hooks/use-account-intent.ts +46 -0
- package/blocks/intent/lib/intent-utils.ts +95 -0
- package/blocks/intent/recipes/rank-intent-accounts.ts +36 -0
- package/blocks/intent/recipes/track-competitor-domain.ts +23 -0
- package/blocks/intent/recipes/who-visited-url.ts +18 -0
- package/blocks/lifecycle/README.md +79 -0
- package/blocks/lifecycle/block.json +198 -0
- package/blocks/lifecycle/components/churn-list.tsx +92 -0
- package/blocks/lifecycle/components/customer-table.tsx +119 -0
- package/blocks/lifecycle/components/lead-queue.tsx +127 -0
- package/blocks/lifecycle/components/lifecycle-board.tsx +102 -0
- package/blocks/lifecycle/components/stage-move-dialog.tsx +75 -0
- package/blocks/lifecycle/hooks/use-debounced.ts +13 -0
- package/blocks/lifecycle/hooks/use-lifecycle-stages.ts +27 -0
- package/blocks/lifecycle/lib/lifecycle.ts +99 -0
- package/blocks/lifecycle/recipes/customer-brief.ts +23 -0
- package/blocks/lifecycle/recipes/move-company-stage.ts +17 -0
- package/blocks/lifecycle/recipes/read-lifecycle-board.ts +21 -0
- package/blocks/lifecycle/recipes/unlock-stage-contacts.ts +25 -0
- package/blocks/lists/README.md +96 -0
- package/blocks/lists/block.json +166 -0
- package/blocks/lists/components/add-to-list-button.tsx +111 -0
- package/blocks/lists/components/list-picker.tsx +94 -0
- package/blocks/lists/components/list-table.tsx +160 -0
- package/blocks/lists/components/suppression-manager.tsx +153 -0
- package/blocks/lists/recipes/create-list-and-add-contacts.ts +35 -0
- package/blocks/lists/recipes/refresh-and-check-lists.ts +21 -0
- package/blocks/lists/recipes/suppress-and-reinstate.ts +30 -0
- package/blocks/mailboxes/README.md +71 -0
- package/blocks/mailboxes/block.json +187 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +284 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +271 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +185 -0
- package/blocks/mailboxes/components/warmup-health.tsx +232 -0
- package/blocks/mailboxes/lib/mailboxes-utils.ts +69 -0
- package/blocks/mailboxes/recipes/buy-domain-and-mailboxes.ts +64 -0
- package/blocks/mailboxes/recipes/connect-imap-mailbox.ts +38 -0
- package/blocks/mailboxes/recipes/is-it-safe-to-send.ts +17 -0
- package/blocks/mailboxes/recipes/warmup-report.ts +31 -0
- package/blocks/newsletter/README.md +122 -0
- package/blocks/newsletter/block.json +224 -0
- package/blocks/newsletter/components/issue-form.tsx +124 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +152 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +131 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +93 -0
- package/blocks/newsletter/components/subscriber-table.tsx +173 -0
- package/blocks/newsletter/lib/newsletter-types.ts +97 -0
- package/blocks/newsletter/recipes/create-newsletter-and-draft-issue.ts +27 -0
- package/blocks/newsletter/recipes/import-audience-and-send.ts +43 -0
- package/blocks/newsletter/recipes/issue-performance-report.ts +26 -0
- package/blocks/nurture/README.md +110 -0
- package/blocks/nurture/block.json +191 -0
- package/blocks/nurture/components/nurture-builder.tsx +114 -0
- package/blocks/nurture/components/nurture-enroll.tsx +107 -0
- package/blocks/nurture/components/nurture-form.tsx +120 -0
- package/blocks/nurture/components/nurture-stats.tsx +81 -0
- package/blocks/nurture/lib/nurture-types.ts +109 -0
- package/blocks/nurture/recipes/create-a-nurture.ts +34 -0
- package/blocks/nurture/recipes/enroll-contacts-and-launch.ts +24 -0
- package/blocks/nurture/recipes/nurture-health-check.ts +28 -0
- package/blocks/objects/README.md +120 -0
- package/blocks/objects/block.json +243 -0
- package/blocks/objects/components/attribute-field-input.tsx +82 -0
- package/blocks/objects/components/attribute-form-dialog.tsx +159 -0
- package/blocks/objects/components/object-not-found.tsx +34 -0
- package/blocks/objects/components/object-record-detail.tsx +143 -0
- package/blocks/objects/components/object-schema-editor.tsx +197 -0
- package/blocks/objects/components/object-table.tsx +130 -0
- package/blocks/objects/components/record-form-dialog.tsx +112 -0
- package/blocks/objects/components/record-roles-panel.tsx +106 -0
- package/blocks/objects/hooks/use-object-schema.ts +36 -0
- package/blocks/objects/lib/object-types.ts +214 -0
- package/blocks/objects/recipes/define-object-and-add-records.ts +24 -0
- package/blocks/objects/recipes/update-audit-and-archive-records.ts +39 -0
- package/blocks/objects/recipes/upsert-records-from-your-system.ts +30 -0
- package/blocks/pages/README.md +104 -0
- package/blocks/pages/block.json +223 -0
- package/blocks/pages/components/brand-kit-editor.tsx +152 -0
- package/blocks/pages/components/content-library.tsx +163 -0
- package/blocks/pages/components/page-editor.tsx +253 -0
- package/blocks/pages/components/page-list.tsx +159 -0
- package/blocks/pages/components/seo-report.tsx +159 -0
- package/blocks/pages/lib/studio.ts +77 -0
- package/blocks/pages/recipes/audit-site-seo.ts +28 -0
- package/blocks/pages/recipes/create-and-publish-page.ts +26 -0
- package/blocks/pages/recipes/edit-page-section.ts +25 -0
- package/blocks/pages/recipes/refresh-brand-kit.ts +22 -0
- package/blocks/quotes/README.md +79 -0
- package/blocks/quotes/block.json +183 -0
- package/blocks/quotes/components/product-picker.tsx +80 -0
- package/blocks/quotes/components/quote-builder.tsx +195 -0
- package/blocks/quotes/components/quote-list.tsx +79 -0
- package/blocks/quotes/components/quote-viewer.tsx +135 -0
- package/blocks/quotes/components/signature-status.tsx +77 -0
- package/blocks/quotes/hooks/use-debounced.ts +13 -0
- package/blocks/quotes/lib/quotes.ts +142 -0
- package/blocks/quotes/recipes/create-and-send-quote.ts +44 -0
- package/blocks/quotes/recipes/revise-sent-quote.ts +18 -0
- package/blocks/quotes/recipes/track-quote-signature.ts +22 -0
- package/blocks/radar/README.md +106 -0
- package/blocks/radar/block.json +189 -0
- package/blocks/radar/components/competitor-battle-card.tsx +53 -0
- package/blocks/radar/components/competitor-tracker.tsx +158 -0
- package/blocks/radar/components/radar-overview.tsx +67 -0
- package/blocks/radar/components/radar-report.tsx +86 -0
- package/blocks/radar/hooks/use-competitors.ts +23 -0
- package/blocks/radar/lib/radar-types.ts +124 -0
- package/blocks/radar/recipes/battle-card.ts +28 -0
- package/blocks/radar/recipes/deep-dive-report.ts +32 -0
- package/blocks/radar/recipes/marketing-snapshot.ts +18 -0
- package/blocks/radar/recipes/track-and-scan.ts +39 -0
- package/blocks/search/README.md +129 -0
- package/blocks/search/block.json +185 -0
- package/blocks/search/components/filter-builder.tsx +149 -0
- package/blocks/search/components/prospect-results.tsx +175 -0
- package/blocks/search/components/save-to-list-dialog.tsx +135 -0
- package/blocks/search/components/saved-searches.tsx +95 -0
- package/blocks/search/hooks/use-filter-options.ts +41 -0
- package/blocks/search/lib/search-types.ts +149 -0
- package/blocks/search/recipes/explore-filter-values.ts +27 -0
- package/blocks/search/recipes/saved-searches.ts +34 -0
- package/blocks/search/recipes/search-and-save-to-list.ts +37 -0
- package/blocks/sequencer/README.md +87 -0
- package/blocks/sequencer/block.json +214 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +93 -0
- package/blocks/sequencer/components/sequence-builder.tsx +306 -0
- package/blocks/sequencer/components/sequence-list.tsx +200 -0
- package/blocks/sequencer/components/sequence-stats.tsx +256 -0
- package/blocks/sequencer/components/step-editor.tsx +213 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +168 -0
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +51 -0
- package/blocks/sequencer/recipes/launch-and-enroll.ts +44 -0
- package/blocks/sequencer/recipes/pause-when-unsafe.ts +30 -0
- package/blocks/sequencer/recipes/sequence-report.ts +22 -0
- package/blocks/social-ads/README.md +128 -0
- package/blocks/social-ads/block.json +256 -0
- package/blocks/social-ads/components/ad-campaign-list.tsx +136 -0
- package/blocks/social-ads/components/audience-sync.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +121 -0
- package/blocks/social-ads/components/post-composer.tsx +122 -0
- package/blocks/social-ads/components/social-post-list.tsx +116 -0
- package/blocks/social-ads/hooks/use-social-accounts.ts +16 -0
- package/blocks/social-ads/lib/social-ads-types.ts +167 -0
- package/blocks/social-ads/recipes/creative-to-gallery.ts +41 -0
- package/blocks/social-ads/recipes/schedule-post.ts +51 -0
- package/blocks/social-ads/recipes/sync-audience-and-launch.ts +41 -0
- package/blocks/visitors/README.md +56 -0
- package/blocks/visitors/block.json +139 -0
- package/blocks/visitors/components/snippet-install.tsx +169 -0
- package/blocks/visitors/components/traffic-chart.tsx +156 -0
- package/blocks/visitors/components/visitor-company-card.tsx +92 -0
- package/blocks/visitors/components/visitor-feed.tsx +157 -0
- package/blocks/visitors/lib/visitors-utils.ts +78 -0
- package/blocks/visitors/recipes/check-snippet-installed.ts +24 -0
- package/blocks/visitors/recipes/recent-company-visits.ts +27 -0
- package/blocks/visitors/recipes/traffic-summary.ts +24 -0
- package/blocks/webchat/README.md +87 -0
- package/blocks/webchat/block.json +134 -0
- package/blocks/webchat/components/chat-widget-config.tsx +160 -0
- package/blocks/webchat/components/conversation-list.tsx +74 -0
- package/blocks/webchat/lib/webchat-types.ts +83 -0
- package/blocks/webchat/recipes/create-bot-and-style-widget.ts +24 -0
- package/blocks/webchat/recipes/list-chat-visitors.ts +24 -0
- package/blocks/workbench/README.md +135 -0
- package/blocks/workbench/block.json +259 -0
- package/blocks/workbench/components/column-recipe-editor.tsx +183 -0
- package/blocks/workbench/components/run-progress.tsx +132 -0
- package/blocks/workbench/components/workbench-add-rows.tsx +100 -0
- package/blocks/workbench/components/workbench-grid.tsx +159 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +102 -0
- package/blocks/workbench/components/workbench-table-list.tsx +105 -0
- package/blocks/workbench/hooks/use-workbench-table.ts +30 -0
- package/blocks/workbench/lib/waterfall-presets.ts +119 -0
- package/blocks/workbench/lib/workbench-types.ts +191 -0
- package/blocks/workbench/recipes/build-enriched-table.ts +65 -0
- package/blocks/workbench/recipes/import-from-search.ts +42 -0
- package/blocks/workbench/recipes/scrape-into-table.ts +36 -0
- package/blocks/workbench/recipes/waterfall-column.ts +55 -0
- package/blocks/workflows/README.md +100 -0
- package/blocks/workflows/block.json +188 -0
- package/blocks/workflows/components/run-history.tsx +136 -0
- package/blocks/workflows/components/trigger-config.tsx +134 -0
- package/blocks/workflows/components/workflow-editor.tsx +142 -0
- package/blocks/workflows/components/workflow-list.tsx +153 -0
- package/blocks/workflows/hooks/use-workflow.ts +51 -0
- package/blocks/workflows/lib/graph.ts +187 -0
- package/blocks/workflows/recipes/create-workflow.ts +35 -0
- package/blocks/workflows/recipes/find-failed-runs.ts +15 -0
- package/blocks/workflows/recipes/run-workflow-and-wait.ts +32 -0
- package/blocks/workflows/recipes/set-trigger.ts +22 -0
- package/dist/cli.js +5330 -29
- package/dist/index.browser.js +7740 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.browser.mjs +7717 -0
- package/dist/index.browser.mjs.map +1 -0
- package/dist/index.d.mts +1440 -21
- package/dist/index.d.ts +1440 -21
- package/dist/index.js +42 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +42 -3
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +7598 -0
- package/dist/react.browser.js.map +1 -0
- package/dist/react.browser.mjs +7568 -0
- package/dist/react.browser.mjs.map +1 -0
- package/dist/react.d.mts +1340 -18
- package/dist/react.d.ts +1340 -18
- package/dist/react.js +41 -2
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +41 -2
- package/dist/react.mjs.map +1 -1
- package/dist/types/agentSetup.d.ts +6 -6
- package/dist/types/blocks/add.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +46 -0
- package/dist/types/blocks/doctor.d.ts +26 -0
- package/dist/types/blocks/registry.d.ts +134 -0
- package/dist/types/cli.d.ts +16 -0
- package/dist/types/core.d.ts +1 -1
- package/dist/types/crypto-browser.d.ts +2 -0
- package/dist/types/crypto-node.d.ts +6 -0
- package/dist/types/react.d.ts +1 -1
- package/dist/types/version.d.ts +2 -2
- package/dist/types/webhooks.d.ts +2 -2
- package/dist/ui/index.d.mts +54 -0
- package/dist/ui/index.d.ts +54 -0
- package/dist/ui/index.js +105 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/index.mjs +44 -0
- package/dist/ui/index.mjs.map +1 -0
- package/dist/ui/lib/compose-refs.d.mts +19 -0
- package/dist/ui/lib/compose-refs.d.ts +19 -0
- package/dist/ui/lib/compose-refs.js +76 -0
- package/dist/ui/lib/compose-refs.js.map +1 -0
- package/dist/ui/lib/compose-refs.mjs +41 -0
- package/dist/ui/lib/compose-refs.mjs.map +1 -0
- package/dist/ui/lib/use-mobile.d.mts +4 -0
- package/dist/ui/lib/use-mobile.d.ts +4 -0
- package/dist/ui/lib/use-mobile.js +57 -0
- package/dist/ui/lib/use-mobile.js.map +1 -0
- package/dist/ui/lib/use-mobile.mjs +22 -0
- package/dist/ui/lib/use-mobile.mjs.map +1 -0
- package/dist/ui/lib/utils.d.mts +10 -0
- package/dist/ui/lib/utils.d.ts +10 -0
- package/dist/ui/lib/utils.js +33 -0
- package/dist/ui/lib/utils.js.map +1 -0
- package/dist/ui/lib/utils.mjs +9 -0
- package/dist/ui/lib/utils.mjs.map +1 -0
- package/dist/ui/primitives/alert-dialog.d.mts +22 -0
- package/dist/ui/primitives/alert-dialog.d.ts +22 -0
- package/dist/ui/primitives/alert-dialog.js +163 -0
- package/dist/ui/primitives/alert-dialog.js.map +1 -0
- package/dist/ui/primitives/alert-dialog.mjs +119 -0
- package/dist/ui/primitives/alert-dialog.mjs.map +1 -0
- package/dist/ui/primitives/avatar.d.mts +8 -0
- package/dist/ui/primitives/avatar.d.ts +8 -0
- package/dist/ui/primitives/avatar.js +91 -0
- package/dist/ui/primitives/avatar.js.map +1 -0
- package/dist/ui/primitives/avatar.mjs +55 -0
- package/dist/ui/primitives/avatar.mjs.map +1 -0
- package/dist/ui/primitives/breadcrumb.d.mts +13 -0
- package/dist/ui/primitives/breadcrumb.d.ts +13 -0
- package/dist/ui/primitives/breadcrumb.js +127 -0
- package/dist/ui/primitives/breadcrumb.js.map +1 -0
- package/dist/ui/primitives/breadcrumb.mjs +97 -0
- package/dist/ui/primitives/breadcrumb.mjs.map +1 -0
- package/dist/ui/primitives/button.d.mts +15 -0
- package/dist/ui/primitives/button.d.ts +15 -0
- package/dist/ui/primitives/button.js +91 -0
- package/dist/ui/primitives/button.js.map +1 -0
- package/dist/ui/primitives/button.mjs +56 -0
- package/dist/ui/primitives/button.mjs.map +1 -0
- package/dist/ui/primitives/card.d.mts +10 -0
- package/dist/ui/primitives/card.d.ts +10 -0
- package/dist/ui/primitives/card.js +101 -0
- package/dist/ui/primitives/card.js.map +1 -0
- package/dist/ui/primitives/card.mjs +62 -0
- package/dist/ui/primitives/card.mjs.map +1 -0
- package/dist/ui/primitives/carousel.d.mts +45 -0
- package/dist/ui/primitives/carousel.d.ts +45 -0
- package/dist/ui/primitives/carousel.js +300 -0
- package/dist/ui/primitives/carousel.js.map +1 -0
- package/dist/ui/primitives/carousel.mjs +261 -0
- package/dist/ui/primitives/carousel.mjs.map +1 -0
- package/dist/ui/primitives/chart.d.mts +68 -0
- package/dist/ui/primitives/chart.d.ts +68 -0
- package/dist/ui/primitives/chart.js +280 -0
- package/dist/ui/primitives/chart.js.map +1 -0
- package/dist/ui/primitives/chart.mjs +241 -0
- package/dist/ui/primitives/chart.mjs.map +1 -0
- package/dist/ui/primitives/collapsible.d.mts +8 -0
- package/dist/ui/primitives/collapsible.d.ts +8 -0
- package/dist/ui/primitives/collapsible.js +72 -0
- package/dist/ui/primitives/collapsible.js.map +1 -0
- package/dist/ui/primitives/collapsible.mjs +36 -0
- package/dist/ui/primitives/collapsible.mjs.map +1 -0
- package/dist/ui/primitives/context-menu.d.mts +29 -0
- package/dist/ui/primitives/context-menu.d.ts +29 -0
- package/dist/ui/primitives/context-menu.js +203 -0
- package/dist/ui/primitives/context-menu.js.map +1 -0
- package/dist/ui/primitives/context-menu.mjs +155 -0
- package/dist/ui/primitives/context-menu.mjs.map +1 -0
- package/dist/ui/primitives/dialog.d.mts +26 -0
- package/dist/ui/primitives/dialog.d.ts +26 -0
- package/dist/ui/primitives/dialog.js +192 -0
- package/dist/ui/primitives/dialog.js.map +1 -0
- package/dist/ui/primitives/dialog.mjs +148 -0
- package/dist/ui/primitives/dialog.mjs.map +1 -0
- package/dist/ui/primitives/drawer.d.mts +26 -0
- package/dist/ui/primitives/drawer.d.ts +26 -0
- package/dist/ui/primitives/drawer.js +146 -0
- package/dist/ui/primitives/drawer.js.map +1 -0
- package/dist/ui/primitives/drawer.mjs +103 -0
- package/dist/ui/primitives/drawer.mjs.map +1 -0
- package/dist/ui/primitives/input.d.mts +5 -0
- package/dist/ui/primitives/input.d.ts +5 -0
- package/dist/ui/primitives/input.js +46 -0
- package/dist/ui/primitives/input.js.map +1 -0
- package/dist/ui/primitives/input.mjs +22 -0
- package/dist/ui/primitives/input.mjs.map +1 -0
- package/dist/ui/primitives/item.d.mts +27 -0
- package/dist/ui/primitives/item.d.ts +27 -0
- package/dist/ui/primitives/item.js +221 -0
- package/dist/ui/primitives/item.js.map +1 -0
- package/dist/ui/primitives/item.mjs +188 -0
- package/dist/ui/primitives/item.mjs.map +1 -0
- package/dist/ui/primitives/kbd.d.mts +15 -0
- package/dist/ui/primitives/kbd.d.ts +15 -0
- package/dist/ui/primitives/kbd.js +45 -0
- package/dist/ui/primitives/kbd.js.map +1 -0
- package/dist/ui/primitives/kbd.mjs +21 -0
- package/dist/ui/primitives/kbd.mjs.map +1 -0
- package/dist/ui/primitives/label.d.mts +6 -0
- package/dist/ui/primitives/label.d.ts +6 -0
- package/dist/ui/primitives/label.js +55 -0
- package/dist/ui/primitives/label.js.map +1 -0
- package/dist/ui/primitives/label.mjs +21 -0
- package/dist/ui/primitives/label.mjs.map +1 -0
- package/dist/ui/primitives/resizable.d.mts +10 -0
- package/dist/ui/primitives/resizable.d.ts +10 -0
- package/dist/ui/primitives/resizable.js +68 -0
- package/dist/ui/primitives/resizable.js.map +1 -0
- package/dist/ui/primitives/resizable.mjs +42 -0
- package/dist/ui/primitives/resizable.mjs.map +1 -0
- package/dist/ui/primitives/save-status.d.mts +31 -0
- package/dist/ui/primitives/save-status.d.ts +31 -0
- package/dist/ui/primitives/save-status.js +150 -0
- package/dist/ui/primitives/save-status.js.map +1 -0
- package/dist/ui/primitives/save-status.mjs +126 -0
- package/dist/ui/primitives/save-status.mjs.map +1 -0
- package/dist/ui/primitives/separator.d.mts +6 -0
- package/dist/ui/primitives/separator.d.ts +6 -0
- package/dist/ui/primitives/separator.js +62 -0
- package/dist/ui/primitives/separator.js.map +1 -0
- package/dist/ui/primitives/separator.mjs +28 -0
- package/dist/ui/primitives/separator.mjs.map +1 -0
- package/dist/ui/primitives/sheet.d.mts +16 -0
- package/dist/ui/primitives/sheet.d.ts +16 -0
- package/dist/ui/primitives/sheet.js +170 -0
- package/dist/ui/primitives/sheet.js.map +1 -0
- package/dist/ui/primitives/sheet.mjs +129 -0
- package/dist/ui/primitives/sheet.mjs.map +1 -0
- package/dist/ui/primitives/sidebar.d.mts +74 -0
- package/dist/ui/primitives/sidebar.d.ts +74 -0
- package/dist/ui/primitives/sidebar.js +697 -0
- package/dist/ui/primitives/sidebar.js.map +1 -0
- package/dist/ui/primitives/sidebar.mjs +651 -0
- package/dist/ui/primitives/sidebar.mjs.map +1 -0
- package/dist/ui/primitives/skeleton.d.mts +5 -0
- package/dist/ui/primitives/skeleton.d.ts +5 -0
- package/dist/ui/primitives/skeleton.js +40 -0
- package/dist/ui/primitives/skeleton.js.map +1 -0
- package/dist/ui/primitives/skeleton.mjs +16 -0
- package/dist/ui/primitives/skeleton.mjs.map +1 -0
- package/dist/ui/primitives/slider.d.mts +20 -0
- package/dist/ui/primitives/slider.d.ts +20 -0
- package/dist/ui/primitives/slider.js +112 -0
- package/dist/ui/primitives/slider.js.map +1 -0
- package/dist/ui/primitives/slider.mjs +78 -0
- package/dist/ui/primitives/slider.mjs.map +1 -0
- package/dist/ui/primitives/sortable.d.mts +50 -0
- package/dist/ui/primitives/sortable.d.ts +50 -0
- package/dist/ui/primitives/sortable.js +456 -0
- package/dist/ui/primitives/sortable.js.map +1 -0
- package/dist/ui/primitives/sortable.mjs +435 -0
- package/dist/ui/primitives/sortable.mjs.map +1 -0
- package/dist/ui/primitives/switch.d.mts +6 -0
- package/dist/ui/primitives/switch.d.ts +6 -0
- package/dist/ui/primitives/switch.js +67 -0
- package/dist/ui/primitives/switch.js.map +1 -0
- package/dist/ui/primitives/switch.mjs +33 -0
- package/dist/ui/primitives/switch.mjs.map +1 -0
- package/dist/ui/primitives/table.d.mts +12 -0
- package/dist/ui/primitives/table.d.ts +12 -0
- package/dist/ui/primitives/table.js +130 -0
- package/dist/ui/primitives/table.js.map +1 -0
- package/dist/ui/primitives/table.mjs +89 -0
- package/dist/ui/primitives/table.mjs.map +1 -0
- package/dist/ui/primitives/tabs.d.mts +9 -0
- package/dist/ui/primitives/tabs.d.ts +9 -0
- package/dist/ui/primitives/tabs.js +86 -0
- package/dist/ui/primitives/tabs.js.map +1 -0
- package/dist/ui/primitives/tabs.mjs +49 -0
- package/dist/ui/primitives/tabs.mjs.map +1 -0
- package/dist/ui/primitives/textarea.d.mts +5 -0
- package/dist/ui/primitives/textarea.d.ts +5 -0
- package/dist/ui/primitives/textarea.js +43 -0
- package/dist/ui/primitives/textarea.js.map +1 -0
- package/dist/ui/primitives/textarea.mjs +19 -0
- package/dist/ui/primitives/textarea.mjs.map +1 -0
- package/dist/ui/primitives/tooltip.d.mts +9 -0
- package/dist/ui/primitives/tooltip.d.ts +9 -0
- package/dist/ui/primitives/tooltip.js +94 -0
- package/dist/ui/primitives/tooltip.js.map +1 -0
- package/dist/ui/primitives/tooltip.mjs +57 -0
- package/dist/ui/primitives/tooltip.mjs.map +1 -0
- package/docs/agents.md +72 -29
- package/docs/api/README.md +6 -3
- package/docs/api/appObjects.d.ts +1 -1
- package/docs/api/appObjects.md +1 -1
- package/docs/api/by-path.md +29 -0
- package/docs/api/objects.d.ts +72 -5
- package/docs/api/objects.md +84 -7
- package/docs/api/processLists.d.ts +247 -0
- package/docs/api/processLists.md +409 -0
- package/docs/api/reports.d.ts +476 -0
- package/docs/api/reports.md +539 -0
- package/docs/api/scheduleBuy.d.ts +12 -8
- package/docs/api/scheduleBuy.md +13 -10
- package/docs/api/seo.d.ts +406 -3
- package/docs/api/seo.md +505 -1
- package/docs/api/workspaceBackup.d.ts +324 -0
- package/docs/api/workspaceBackup.md +427 -0
- package/package.json +174 -5
- package/stacks/6sense.json +46 -0
- package/stacks/README.md +20 -0
- package/stacks/apollo.json +128 -0
- package/stacks/clay.json +59 -0
- package/stacks/gong.json +72 -0
- package/stacks/hubspot.json +162 -0
- package/stacks/intercom.json +53 -0
- package/stacks/mailchimp.json +71 -0
- package/stacks/outreach.json +86 -0
- package/stacks/salesforce.json +94 -0
- package/stacks/stack.schema.json +32 -0
- package/stacks/starter.json +22 -0
- package/styles/styles.css +2 -0
- package/styles/theme.css +181 -0
- package/styles/tokens.css +284 -0
- package/templates/next-app/README.md.tmpl +35 -0
- package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
- package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
- package/templates/next-app/app/globals.css.tmpl +8 -0
- package/templates/next-app/app/layout.tsx.tmpl +19 -0
- package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
- package/templates/next-app/dot-env.local.example.tmpl +6 -0
- package/templates/next-app/dot-gitignore.tmpl +7 -0
- package/templates/next-app/lib/nav.ts.tmpl +2 -0
- package/templates/next-app/next.config.mjs.tmpl +4 -0
- package/templates/next-app/package.json.tmpl +25 -0
- package/templates/next-app/postcss.config.mjs.tmpl +4 -0
- package/templates/next-app/tsconfig.json.tmpl +21 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CampaignWizard: from an idea to a launched campaign in five steps: Idea -> Campaign -> Audience ->
|
|
5
|
+
* Sequence -> Launch. A page with no required props; pass `initialCampaignId` to resume a draft.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order (steps 3-5 are CampaignAudienceStep, CampaignSequenceStep, CampaignLaunchStep):
|
|
8
|
+
* 1. getCompanyProfile / listPersonas GET /company-profile, /personas (read; website and persona choices)
|
|
9
|
+
* 2. generateCampaignIdeasAsync POST /campaigns/generate-ideas-async (billable, ConfirmSpendDialog; returns task_id)
|
|
10
|
+
* getCampaignIdeasTaskStatus GET /campaigns/ideas/status/{task_id} (read; POLLED, there are no streams)
|
|
11
|
+
* 3. createCampaign POST /campaigns (external, ConfirmSpendDialog; billable when documents are generated)
|
|
12
|
+
* updateCampaign PATCH /campaigns/{campaign_id} (external, ConfirmSpendDialog; once the campaign exists)
|
|
13
|
+
* getCampaignGenerationStatus GET /campaigns/{campaign_id}/status (read; POLLED while documents generate)
|
|
14
|
+
* 4. attach audience, steps/content, launch: see the three step components.
|
|
15
|
+
*
|
|
16
|
+
* Retry safety: the campaign id is stored the moment create succeeds, so a failure in a later step never
|
|
17
|
+
* creates a second campaign; "Create" turns into "Save changes". "Start over" clears every field, the idea
|
|
18
|
+
* results and the campaign id (the campaign already created stays in graph8; delete it in CampaignList).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useEffect, useState } from "react";
|
|
22
|
+
import { Button, Input, Textarea } from "@graph8/sdk/ui";
|
|
23
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
24
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
25
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
26
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
27
|
+
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
28
|
+
import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
29
|
+
import type { ListSummary } from "@/g8/lists/components/list-picker";
|
|
30
|
+
import { useCompanyProfile } from "../hooks/use-company-profile";
|
|
31
|
+
import { CREDIT_ESTIMATES, listOf, objectOf, settled, statusClass, text } from "../lib/campaigns-utils";
|
|
32
|
+
import { CampaignAudienceStep } from "./campaign-audience-step";
|
|
33
|
+
import { CampaignLaunchStep } from "./campaign-launch-step";
|
|
34
|
+
import { CampaignSequenceStep } from "./campaign-sequence-step";
|
|
35
|
+
import { FormField } from "./form-field";
|
|
36
|
+
|
|
37
|
+
export interface CampaignWizardProps {
|
|
38
|
+
/** Resume a draft campaign at the Audience step. */
|
|
39
|
+
initialCampaignId?: string;
|
|
40
|
+
/** Overrides the website from the saved profile. */
|
|
41
|
+
websiteUrl?: string;
|
|
42
|
+
onLaunched?: (result: Record<string, unknown>) => void;
|
|
43
|
+
className?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const STEPS = ["Idea", "Campaign", "Audience", "Sequence", "Launch"] as const;
|
|
47
|
+
const GOALS = ["awareness", "consideration", "decision"];
|
|
48
|
+
const CHANNELS = ["email", "linkedin", "phone"];
|
|
49
|
+
|
|
50
|
+
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: false };
|
|
52
|
+
|
|
53
|
+
interface Idea { id?: string; campaign_name?: string; campaign_category?: string; core_concept?: string; why_this_works?: string; primary_hook?: string; target_channels?: string[] }
|
|
54
|
+
|
|
55
|
+
function IdeaStep({ websiteUrl, onPick, onSkip }: { websiteUrl: string; onPick: (i: Idea) => void; onSkip: () => void }) {
|
|
56
|
+
const personas = useG8Query((signal) => g8Get("/personas", { limit: 50 }, { signal }), []);
|
|
57
|
+
const [goal, setGoal] = useState(GOALS[0]);
|
|
58
|
+
const [count, setCount] = useState(5);
|
|
59
|
+
const [direction, setDirection] = useState("");
|
|
60
|
+
const [selected, setSelected] = useState<string[]>([]);
|
|
61
|
+
const [confirm, setConfirm] = useState(false);
|
|
62
|
+
const [taskId, setTaskId] = useState<string | null>(null);
|
|
63
|
+
const start = useG8Mutation(
|
|
64
|
+
() => g8Post("/campaigns/generate-ideas-async", {
|
|
65
|
+
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" : "auto",
|
|
67
|
+
}),
|
|
68
|
+
{ onSuccess: (res) => { setConfirm(false); setTaskId(text(objectOf(res).task_id) || null); }, onError: () => setConfirm(false) },
|
|
69
|
+
);
|
|
70
|
+
const poll = useJobPoll(
|
|
71
|
+
async (signal) => objectOf(await g8Get(`/campaigns/ideas/status/${taskId}`, undefined, { signal })),
|
|
72
|
+
{ enabled: false, isTerminal: (s) => ["completed", "failed"].includes(text(s.status)), maxAttempts: 90 },
|
|
73
|
+
);
|
|
74
|
+
useEffect(() => { if (taskId) poll.start(); // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
75
|
+
}, [taskId]);
|
|
76
|
+
const ideas = (poll.status?.ideas as Idea[] | undefined) ?? [];
|
|
77
|
+
const failed = poll.status?.status === "failed";
|
|
78
|
+
const personaRows = listOf<{ id: string; title?: string }>(personas.data);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<section aria-label="Ideas" className="space-y-3">
|
|
82
|
+
<p className="text-sm text-muted-foreground">Let AI propose campaign ideas for {websiteUrl || "your company"}, or skip and write your own.</p>
|
|
83
|
+
<div className="grid gap-3 sm:grid-cols-3">
|
|
84
|
+
<FormField label="Goal" htmlFor="idea-goal">
|
|
85
|
+
<select id="idea-goal" className="h-9 rounded-md border bg-background px-2" value={goal} onChange={(e) => setGoal(e.target.value)}>{GOALS.map((g) => <option key={g}>{g}</option>)}</select>
|
|
86
|
+
</FormField>
|
|
87
|
+
<FormField label="How many ideas" htmlFor="idea-count"><Input id="idea-count" type="number" min={1} max={10} value={count} onChange={(e) => setCount(Math.min(10, Math.max(1, Number(e.target.value) || 1)))} /></FormField>
|
|
88
|
+
<FormField label="Direction (optional)" htmlFor="idea-direction"><Input id="idea-direction" value={direction} onChange={(e) => setDirection(e.target.value)} /></FormField>
|
|
89
|
+
</div>
|
|
90
|
+
{personaRows.length > 0 ? (
|
|
91
|
+
<fieldset className="flex flex-wrap gap-3"><legend className="text-sm font-medium">Target personas</legend>
|
|
92
|
+
{personaRows.map((p) => (
|
|
93
|
+
<label key={p.id} className="flex items-center gap-1 text-sm">
|
|
94
|
+
<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
|
+
</label>
|
|
96
|
+
))}
|
|
97
|
+
</fieldset>
|
|
98
|
+
) : null}
|
|
99
|
+
{[start.error, poll.error].map((e, i) => (e ? <ApiError key={i} error={e} title="Idea generation failed" /> : null))}
|
|
100
|
+
<div className="flex gap-2">
|
|
101
|
+
<Button disabled={!websiteUrl || poll.polling} loading={start.loading} onClick={() => setConfirm(true)}>Generate ideas</Button>
|
|
102
|
+
<Button variant="outline" onClick={onSkip}>Skip, write my own</Button>
|
|
103
|
+
</div>
|
|
104
|
+
{!websiteUrl ? <p className="text-sm text-amber-800">Save the company website in IcpEditor first; ideas are generated from it.</p> : null}
|
|
105
|
+
{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-amber-800">Still running. Check back in a minute and generate again if nothing appears.</p> : null}
|
|
107
|
+
{failed ? <p role="alert" className="text-sm text-red-700">Generation failed: {text(poll.status?.error) || "try again"}.</p> : null}
|
|
108
|
+
{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
|
+
<ul className="space-y-2">
|
|
110
|
+
{ideas.map((idea, i) => (
|
|
111
|
+
<li key={idea.id ?? i} className="rounded-md border p-3">
|
|
112
|
+
<p className="font-medium">{idea.campaign_name}</p>
|
|
113
|
+
<p className="text-sm text-muted-foreground">{idea.core_concept}</p>
|
|
114
|
+
{idea.primary_hook ? <p className="text-sm">Hook: {idea.primary_hook}</p> : null}
|
|
115
|
+
<Button size="sm" className="mt-2" aria-label={`Use idea ${idea.campaign_name}`} onClick={() => onPick(idea)}>Use this idea</Button>
|
|
116
|
+
</li>
|
|
117
|
+
))}
|
|
118
|
+
</ul>
|
|
119
|
+
<ConfirmSpendDialog
|
|
120
|
+
open={confirm}
|
|
121
|
+
tier="billable"
|
|
122
|
+
title={`Generate ${count} campaign ideas`}
|
|
123
|
+
description="AI writes the ideas from your company profile. Estimate only."
|
|
124
|
+
estimatedCredits={count * CREDIT_ESTIMATES.perIdea}
|
|
125
|
+
confirmLabel="Generate ideas"
|
|
126
|
+
confirming={start.loading}
|
|
127
|
+
onConfirm={() => void start.mutate(undefined)}
|
|
128
|
+
onCancel={() => setConfirm(false)}
|
|
129
|
+
/>
|
|
130
|
+
</section>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
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 label = poll.polling ? "generating documents" : text(poll.status?.status) || "ready";
|
|
137
|
+
return <span role="status" className={`rounded px-2 py-0.5 text-xs ${statusClass(poll.polling ? "generating" : "completed")}`}>Documents: {label}</span>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, className }: CampaignWizardProps) {
|
|
141
|
+
const profile = useCompanyProfile(websiteUrl);
|
|
142
|
+
const [step, setStep] = useState(initialCampaignId ? 2 : 0);
|
|
143
|
+
const [draft, setDraft] = useState<Draft>(emptyDraft);
|
|
144
|
+
const [campaignId, setCampaignId] = useState<string | null>(initialCampaignId ?? null);
|
|
145
|
+
const [audience, setAudience] = useState<ListSummary | null>(null);
|
|
146
|
+
const [confirm, setConfirm] = useState(false);
|
|
147
|
+
const [resetKey, setResetKey] = useState(0);
|
|
148
|
+
|
|
149
|
+
const save = useG8Mutation(async () => {
|
|
150
|
+
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 };
|
|
151
|
+
if (campaignId) return g8Patch(`/campaigns/${campaignId}`, fields);
|
|
152
|
+
return g8Post("/campaigns", { ...fields, target_channels: draft.channels, auto_generate_documents: draft.autoGenerate });
|
|
153
|
+
}, {
|
|
154
|
+
onSuccess: (res) => {
|
|
155
|
+
setConfirm(false);
|
|
156
|
+
const id = text(objectOf(res).id);
|
|
157
|
+
if (!campaignId && id) setCampaignId(id); // stored at once: a later failure never re-creates the campaign
|
|
158
|
+
setStep(2);
|
|
159
|
+
},
|
|
160
|
+
onError: () => setConfirm(false),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const set = (k: keyof Draft) => (e: { target: { value: string } }) => setDraft({ ...draft, [k]: e.target.value });
|
|
164
|
+
const pickIdea = (i: Idea) => {
|
|
165
|
+
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
|
+
setStep(1);
|
|
167
|
+
};
|
|
168
|
+
const reset = () => { setStep(0); setDraft(emptyDraft); setCampaignId(null); setAudience(null); setConfirm(false); save.reset(); setResetKey((k) => k + 1); };
|
|
169
|
+
const autoGen = !campaignId && draft.autoGenerate;
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div className={`space-y-4 ${className ?? ""}`}>
|
|
173
|
+
<nav aria-label="Wizard steps" className="flex flex-wrap items-center gap-2">
|
|
174
|
+
{STEPS.map((label, i) => (
|
|
175
|
+
<button key={label} type="button" aria-current={i === step ? "step" : undefined} disabled={i > 1 && !campaignId}
|
|
176
|
+
className={`rounded-full border px-3 py-1 text-sm ${i === step ? "bg-accent font-medium" : ""}`} onClick={() => setStep(i)}>
|
|
177
|
+
{i + 1}. {label}
|
|
178
|
+
</button>
|
|
179
|
+
))}
|
|
180
|
+
{campaignId ? <GenerationStatus key={campaignId} campaignId={campaignId} /> : null}
|
|
181
|
+
<Button size="sm" variant="outline" className="ml-auto" onClick={reset}>Start over</Button>
|
|
182
|
+
</nav>
|
|
183
|
+
|
|
184
|
+
{step === 0 ? <IdeaStep key={resetKey} websiteUrl={profile.websiteUrl} onPick={pickIdea} onSkip={() => setStep(1)} /> : null}
|
|
185
|
+
{step === 1 ? (
|
|
186
|
+
<section aria-label="Campaign" className="space-y-3">
|
|
187
|
+
<FormField label="Campaign name" htmlFor="cw-name"><Input id="cw-name" value={draft.name} onChange={set("name")} /></FormField>
|
|
188
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
189
|
+
<FormField label="Category" htmlFor="cw-category"><Input id="cw-category" value={draft.category} onChange={set("category")} /></FormField>
|
|
190
|
+
<FormField label="Goal" htmlFor="cw-goal"><Input id="cw-goal" value={draft.goal} onChange={set("goal")} /></FormField>
|
|
191
|
+
</div>
|
|
192
|
+
<FormField label="Target persona" htmlFor="cw-persona"><Input id="cw-persona" value={draft.target_persona} onChange={set("target_persona")} /></FormField>
|
|
193
|
+
<FormField label="Core concept" htmlFor="cw-concept"><Textarea id="cw-concept" value={draft.core_concept} onChange={set("core_concept")} /></FormField>
|
|
194
|
+
<FormField label="Primary hook" htmlFor="cw-hook"><Input id="cw-hook" value={draft.primary_hook} onChange={set("primary_hook")} /></FormField>
|
|
195
|
+
<FormField label="Brief" htmlFor="cw-brief"><Textarea id="cw-brief" value={draft.brief} onChange={set("brief")} /></FormField>
|
|
196
|
+
{!campaignId ? (
|
|
197
|
+
<>
|
|
198
|
+
<fieldset className="flex gap-3"><legend className="text-sm font-medium">Channels</legend>
|
|
199
|
+
{CHANNELS.map((c) => (
|
|
200
|
+
<label key={c} className="flex items-center gap-1 text-sm">
|
|
201
|
+
<input type="checkbox" checked={draft.channels.includes(c)} onChange={(e) => setDraft({ ...draft, channels: e.target.checked ? [...draft.channels, c] : draft.channels.filter((x) => x !== c) })} />{c}
|
|
202
|
+
</label>
|
|
203
|
+
))}
|
|
204
|
+
</fieldset>
|
|
205
|
+
<label className="flex items-center gap-2 text-sm">
|
|
206
|
+
<input type="checkbox" checked={draft.autoGenerate} onChange={(e) => setDraft({ ...draft, autoGenerate: e.target.checked })} />
|
|
207
|
+
Write the campaign documents with AI after creating it (uses credits)
|
|
208
|
+
</label>
|
|
209
|
+
</>
|
|
210
|
+
) : null}
|
|
211
|
+
{save.error ? <ApiError error={save.error} title={campaignId ? "Could not save the campaign" : "Could not create the campaign"} /> : null}
|
|
212
|
+
<Button disabled={!draft.name.trim()} loading={save.loading} onClick={() => setConfirm(true)}>{campaignId ? "Save changes" : "Create campaign"}</Button>
|
|
213
|
+
</section>
|
|
214
|
+
) : null}
|
|
215
|
+
{step === 2 && campaignId ? <CampaignAudienceStep campaignId={campaignId} onAttached={setAudience} /> : null}
|
|
216
|
+
{step === 3 && campaignId ? <CampaignSequenceStep campaignId={campaignId} /> : null}
|
|
217
|
+
{step === 4 && campaignId ? <CampaignLaunchStep campaignId={campaignId} audienceSize={audience?.total ?? audience?.contact_count ?? undefined} onLaunched={onLaunched} /> : null}
|
|
218
|
+
{step >= 2 && campaignId ? (
|
|
219
|
+
<div className="flex justify-between">
|
|
220
|
+
<Button variant="outline" onClick={() => setStep(step - 1)}>Back</Button>
|
|
221
|
+
{step < 4 ? <Button onClick={() => setStep(step + 1)}>Next</Button> : null}
|
|
222
|
+
</div>
|
|
223
|
+
) : null}
|
|
224
|
+
|
|
225
|
+
<ConfirmSpendDialog
|
|
226
|
+
open={confirm}
|
|
227
|
+
tier={autoGen ? "billable" : "external"}
|
|
228
|
+
title={campaignId ? `Save changes to "${draft.name}"` : `Create campaign "${draft.name}"`}
|
|
229
|
+
description={autoGen ? "Creates the campaign and writes its documents with AI. Nothing is sent to contacts until you launch." : "Nothing is sent to contacts until you launch."}
|
|
230
|
+
estimatedCredits={autoGen ? CREDIT_ESTIMATES.documentSet : undefined}
|
|
231
|
+
confirmLabel={campaignId ? "Save changes" : "Create campaign"}
|
|
232
|
+
confirming={save.loading}
|
|
233
|
+
onConfirm={() => void save.mutate(undefined)}
|
|
234
|
+
onCancel={() => setConfirm(false)}
|
|
235
|
+
/>
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FormField: a label wrapped around any control, the one small layout piece the campaign editors share.
|
|
5
|
+
* No SDK operations. Tiers: none.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ReactNode } from "react";
|
|
9
|
+
import { Label } from "@graph8/sdk/ui";
|
|
10
|
+
|
|
11
|
+
export interface FormFieldProps {
|
|
12
|
+
/** Visible label text; also the control's accessible name when `htmlFor` is set. */
|
|
13
|
+
label: string;
|
|
14
|
+
/** The id of the control, so the label is clickable and read out. */
|
|
15
|
+
htmlFor: string;
|
|
16
|
+
hint?: ReactNode;
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function FormField({ label, htmlFor, hint, children, className }: FormFieldProps) {
|
|
22
|
+
return (
|
|
23
|
+
<div className={`flex flex-col gap-1 ${className ?? ""}`}>
|
|
24
|
+
<Label htmlFor={htmlFor}>{label}</Label>
|
|
25
|
+
{children}
|
|
26
|
+
{hint ? <p className="text-xs text-muted-foreground">{hint}</p> : null}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* IcpEditor: the company profile every campaign is generated from, and the ideal customer profiles (ICPs)
|
|
5
|
+
* built on it. Edit by hand for free, or let AI rebuild the profile (confirmed first, it spends credits).
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. getCompanyProfile GET /company-profile (read; via useCompanyProfile)
|
|
9
|
+
* 2. updateCompanyProfile PUT /company-profile (write; replaces the stored field set)
|
|
10
|
+
* 3. generateCompanyProfile POST /company-profile/generate (billable, ConfirmSpendDialog; replaces unapproved data)
|
|
11
|
+
* 4. getCompanyProfileWorkflowStatus GET /company-profile/workflow-status?website_url (read; polled until settled)
|
|
12
|
+
* 5. approveCompanyProfile POST /company-profile/approve (write; locks the profile)
|
|
13
|
+
* 6. listIcps GET /icps (read)
|
|
14
|
+
* 7. createIcp / updateIcp POST /icps, PATCH /icps/{icp_id} (write)
|
|
15
|
+
* 8. archiveIcp POST /icps/{icp_id}/archive (destructive, ConfirmSpendDialog)
|
|
16
|
+
*
|
|
17
|
+
* Tiers: only 3 (billable) and 8 (destructive) open ConfirmSpendDialog. An approved profile is immutable: saving
|
|
18
|
+
* over one needs the explicit "unlock" box, which sends `unapprove_if_approved`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useState } from "react";
|
|
22
|
+
import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Textarea } from "@graph8/sdk/ui";
|
|
23
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
24
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
25
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
26
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
27
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
28
|
+
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
29
|
+
import { g8Get, g8Patch, g8Post, g8Put } from "@/g8/lib/g8-client";
|
|
30
|
+
import { useCompanyProfile } from "../hooks/use-company-profile";
|
|
31
|
+
import { CREDIT_ESTIMATES, commaList, lines, listOf, objectOf, settled, statusClass, text } from "../lib/campaigns-utils";
|
|
32
|
+
import { FormField } from "./form-field";
|
|
33
|
+
|
|
34
|
+
export interface IcpEditorProps {
|
|
35
|
+
/** Overrides the website from the saved profile (needed the first time, before a profile exists). */
|
|
36
|
+
websiteUrl?: string;
|
|
37
|
+
onChange?: () => void;
|
|
38
|
+
className?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface IcpRow {
|
|
42
|
+
id: string;
|
|
43
|
+
name?: string | null;
|
|
44
|
+
description?: string | null;
|
|
45
|
+
firmographics?: Record<string, unknown> | null;
|
|
46
|
+
buying_signals?: unknown[] | null;
|
|
47
|
+
status?: string | null;
|
|
48
|
+
total_score?: number | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const DEFAULT_FIELDS = ["company_name", "industry", "description", "value_proposition"];
|
|
52
|
+
|
|
53
|
+
function ProfileCard({ websiteUrl: override, onChange }: IcpEditorProps) {
|
|
54
|
+
const profile = useCompanyProfile(override);
|
|
55
|
+
const [site, setSite] = useState<string | null>(null);
|
|
56
|
+
const [edits, setEdits] = useState<Record<string, string>>({});
|
|
57
|
+
const [unlock, setUnlock] = useState(false);
|
|
58
|
+
const [confirmGenerate, setConfirmGenerate] = useState(false);
|
|
59
|
+
const [saved, setSaved] = useState(false);
|
|
60
|
+
const website = site ?? profile.websiteUrl;
|
|
61
|
+
const stored = Object.fromEntries(Object.entries(profile.profile?.fields ?? {}).filter(([, v]) => typeof v === "string")) as Record<string, string>;
|
|
62
|
+
const keys = Array.from(new Set([...DEFAULT_FIELDS, ...Object.keys(stored)]));
|
|
63
|
+
const value = (k: string) => edits[k] ?? stored[k] ?? "";
|
|
64
|
+
|
|
65
|
+
const save = useG8Mutation(
|
|
66
|
+
() => g8Put("/company-profile", { website_url: website, fields: { ...stored, ...edits }, unapprove_if_approved: unlock }),
|
|
67
|
+
{ onSuccess: () => { setSaved(true); setEdits({}); void profile.refetch(); onChange?.(); } },
|
|
68
|
+
);
|
|
69
|
+
const generate = useG8Mutation(() => g8Post("/company-profile/generate", { website_url: website, fields: stored }), {
|
|
70
|
+
onSuccess: () => { setConfirmGenerate(false); status.start(); },
|
|
71
|
+
onError: () => setConfirmGenerate(false),
|
|
72
|
+
});
|
|
73
|
+
const approve = useG8Mutation(() => g8Post("/company-profile/approve", { website_url: website }), {
|
|
74
|
+
onSuccess: () => { void profile.refetch(); onChange?.(); },
|
|
75
|
+
});
|
|
76
|
+
const status = useJobPoll(
|
|
77
|
+
async (signal) => (await g8Get("/company-profile/workflow-status", { website_url: website }, { signal })).data,
|
|
78
|
+
{ enabled: false, isTerminal: settled, maxAttempts: 60, onDone: () => { void profile.refetch(); onChange?.(); } },
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (profile.error) return <ApiError error={profile.error} title="Could not load the company profile" onRetry={() => void profile.refetch()} />;
|
|
82
|
+
if (profile.loading && !profile.profile) return <Skeleton className="h-40 w-full" />;
|
|
83
|
+
const approved = Boolean(profile.profile?.approved);
|
|
84
|
+
return (
|
|
85
|
+
<section aria-label="Company profile" className="space-y-3 rounded-md border p-4">
|
|
86
|
+
<div className="flex items-center justify-between">
|
|
87
|
+
<h3 className="font-medium">Company profile</h3>
|
|
88
|
+
<span className={`rounded px-2 py-0.5 text-xs ${statusClass(approved ? "approved" : "draft")}`}>{approved ? "Approved (locked)" : "Not approved"}</span>
|
|
89
|
+
</div>
|
|
90
|
+
<FormField label="Website" htmlFor="profile-website" hint="The real company website. Every ICP, persona and campaign is generated from it.">
|
|
91
|
+
<Input id="profile-website" value={website} placeholder="https://acme.com" onChange={(e) => { setSite(e.target.value); setSaved(false); }} />
|
|
92
|
+
</FormField>
|
|
93
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
94
|
+
{keys.map((k) => (
|
|
95
|
+
<FormField key={k} label={k.replace(/_/g, " ")} htmlFor={`profile-${k}`}>
|
|
96
|
+
<Input id={`profile-${k}`} value={value(k)} onChange={(e) => { setEdits({ ...edits, [k]: e.target.value }); setSaved(false); }} />
|
|
97
|
+
</FormField>
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
{approved ? (
|
|
101
|
+
<label className="flex items-center gap-2 text-sm">
|
|
102
|
+
<input type="checkbox" checked={unlock} onChange={(e) => setUnlock(e.target.checked)} />
|
|
103
|
+
Unlock the approved profile so this save can overwrite it
|
|
104
|
+
</label>
|
|
105
|
+
) : null}
|
|
106
|
+
{status.polling ? <p role="status" className="text-sm text-muted-foreground">Building the profile. This can take a minute.</p> : null}
|
|
107
|
+
{status.done && !status.polling ? <p role="status" className="text-sm text-green-700">Profile build finished.</p> : null}
|
|
108
|
+
{saved ? <p role="status" className="text-sm text-green-700">Saved.</p> : null}
|
|
109
|
+
{[save.error, generate.error, approve.error, status.error].map((e, i) => (e ? <ApiError key={i} error={e} /> : null))}
|
|
110
|
+
<div className="flex flex-wrap gap-2">
|
|
111
|
+
<Button loading={save.loading} disabled={!website.trim() || (approved && !unlock)} onClick={() => void save.mutate(undefined)}>Save profile</Button>
|
|
112
|
+
<Button variant="outline" disabled={!website.trim() || approved || status.polling} onClick={() => setConfirmGenerate(true)}>Build with AI</Button>
|
|
113
|
+
<Button variant="outline" loading={approve.loading} disabled={!profile.profile || approved} onClick={() => void approve.mutate(undefined)}>Approve and lock</Button>
|
|
114
|
+
</div>
|
|
115
|
+
<ConfirmSpendDialog
|
|
116
|
+
open={confirmGenerate}
|
|
117
|
+
tier="billable"
|
|
118
|
+
title="Build the company profile with AI"
|
|
119
|
+
description="Crawls the website and replaces the unapproved profile data. Estimate only."
|
|
120
|
+
estimatedCredits={CREDIT_ESTIMATES.profileGenerate}
|
|
121
|
+
confirmLabel="Build profile"
|
|
122
|
+
confirming={generate.loading}
|
|
123
|
+
onConfirm={() => void generate.mutate(undefined)}
|
|
124
|
+
onCancel={() => setConfirmGenerate(false)}
|
|
125
|
+
/>
|
|
126
|
+
</section>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface IcpForm { name: string; description: string; industries: string; size: string; regions: string; signals: string }
|
|
131
|
+
const emptyForm: IcpForm = { name: "", description: "", industries: "", size: "", regions: "", signals: "" };
|
|
132
|
+
|
|
133
|
+
function formOf(icp: IcpRow): IcpForm {
|
|
134
|
+
const f = (icp.firmographics ?? {}) as Record<string, unknown>;
|
|
135
|
+
const arr = (v: unknown) => (Array.isArray(v) ? v.join(", ") : text(v));
|
|
136
|
+
return {
|
|
137
|
+
name: icp.name ?? "", description: icp.description ?? "", industries: arr(f.industries), size: text(f.company_size), regions: arr(f.regions),
|
|
138
|
+
signals: (icp.buying_signals ?? []).map((s) => text(s)).filter(Boolean).join("\n"),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function IcpEditor(props: IcpEditorProps) {
|
|
143
|
+
const { onChange, className } = props;
|
|
144
|
+
const profile = useCompanyProfile(props.websiteUrl);
|
|
145
|
+
const icps = useG8Query((signal) => g8Get("/icps", { limit: 100 }, { signal }), []);
|
|
146
|
+
const [editing, setEditing] = useState<IcpRow | "new" | null>(null);
|
|
147
|
+
const [form, setForm] = useState<IcpForm>(emptyForm);
|
|
148
|
+
const [archiving, setArchiving] = useState<IcpRow | null>(null);
|
|
149
|
+
const [busyId, setBusyId] = useState<string | null>(null);
|
|
150
|
+
|
|
151
|
+
const done = () => { void icps.refetch(); onChange?.(); };
|
|
152
|
+
const saveIcp = useG8Mutation(async () => {
|
|
153
|
+
const existing = editing && editing !== "new" ? editing : null;
|
|
154
|
+
const firmographics = { ...(existing?.firmographics ?? {}), industries: commaList(form.industries), company_size: form.size, regions: commaList(form.regions) };
|
|
155
|
+
const body = { name: form.name.trim(), description: form.description, firmographics, buying_signals: lines(form.signals) };
|
|
156
|
+
return existing ? g8Patch(`/icps/${existing.id}`, body) : g8Post("/icps", { ...body, website_url: profile.websiteUrl });
|
|
157
|
+
}, { onSuccess: () => { setEditing(null); done(); } });
|
|
158
|
+
const archive = useG8Mutation(async (icp: IcpRow) => {
|
|
159
|
+
setBusyId(icp.id);
|
|
160
|
+
try { await g8Post(`/icps/${icp.id}/archive`, {}); } finally { setBusyId(null); setArchiving(null); }
|
|
161
|
+
}, { onSuccess: done });
|
|
162
|
+
|
|
163
|
+
const rows = listOf<IcpRow>(icps.data);
|
|
164
|
+
const open = (icp: IcpRow | "new") => { setForm(icp === "new" ? emptyForm : formOf(icp)); setEditing(icp); saveIcp.reset(); };
|
|
165
|
+
const set = (k: keyof IcpForm) => (e: { target: { value: string } }) => setForm({ ...form, [k]: e.target.value });
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div className={`space-y-6 ${className ?? ""}`}>
|
|
169
|
+
<ProfileCard {...props} />
|
|
170
|
+
<section aria-label="Ideal customer profiles" className="space-y-3">
|
|
171
|
+
<div className="flex items-center justify-between">
|
|
172
|
+
<h3 className="font-medium">Ideal customer profiles</h3>
|
|
173
|
+
<Button size="sm" disabled={!profile.websiteUrl} onClick={() => open("new")}>Add ICP</Button>
|
|
174
|
+
</div>
|
|
175
|
+
{!profile.websiteUrl ? <p className="text-sm text-muted-foreground">Save the company website above before adding an ICP.</p> : null}
|
|
176
|
+
{icps.error ? <ApiError error={icps.error} title="Could not load ICPs" onRetry={() => void icps.refetch()} /> : null}
|
|
177
|
+
{archive.error ? <ApiError error={archive.error} title="Could not archive the ICP" /> : null}
|
|
178
|
+
<Table>
|
|
179
|
+
<TableHeader><TableRow><TableHead>Name</TableHead><TableHead>Description</TableHead><TableHead>Status</TableHead><TableHead className="w-40" /></TableRow></TableHeader>
|
|
180
|
+
<TableBody>
|
|
181
|
+
{icps.loading && !icps.data ? <TableRow><TableCell colSpan={4}><Skeleton className="h-6 w-full" /></TableCell></TableRow> : null}
|
|
182
|
+
{rows.map((icp) => (
|
|
183
|
+
<TableRow key={icp.id}>
|
|
184
|
+
<TableCell className="font-medium">{icp.name}</TableCell>
|
|
185
|
+
<TableCell className="max-w-md truncate text-muted-foreground">{icp.description}</TableCell>
|
|
186
|
+
<TableCell><span className={`rounded px-2 py-0.5 text-xs ${statusClass(icp.status)}`}>{icp.status ?? "active"}</span></TableCell>
|
|
187
|
+
<TableCell className="space-x-2 text-right">
|
|
188
|
+
<Button size="sm" variant="outline" aria-label={`Edit ${icp.name}`} onClick={() => open(icp)}>Edit</Button>
|
|
189
|
+
<Button size="sm" variant="outline" aria-label={`Archive ${icp.name}`} loading={busyId === icp.id} onClick={() => setArchiving(icp)}>Archive</Button>
|
|
190
|
+
</TableCell>
|
|
191
|
+
</TableRow>
|
|
192
|
+
))}
|
|
193
|
+
</TableBody>
|
|
194
|
+
</Table>
|
|
195
|
+
{!icps.loading && !icps.error && rows.length === 0 ? <EmptyState title="No ICPs yet" description="Describe who you sell to. Campaigns and personas build on it." /> : null}
|
|
196
|
+
</section>
|
|
197
|
+
|
|
198
|
+
<Dialog open={editing !== null} onOpenChange={(o) => { if (!o) setEditing(null); }}>
|
|
199
|
+
<DialogContent>
|
|
200
|
+
<DialogHeader><DialogTitle>{editing === "new" ? "Add ICP" : "Edit ICP"}</DialogTitle></DialogHeader>
|
|
201
|
+
<div className="space-y-3">
|
|
202
|
+
<FormField label="Name" htmlFor="icp-name"><Input id="icp-name" value={form.name} onChange={set("name")} /></FormField>
|
|
203
|
+
<FormField label="Description" htmlFor="icp-description"><Textarea id="icp-description" value={form.description} onChange={set("description")} /></FormField>
|
|
204
|
+
<FormField label="Industries" htmlFor="icp-industries" hint="Comma separated."><Input id="icp-industries" value={form.industries} onChange={set("industries")} /></FormField>
|
|
205
|
+
<FormField label="Company size" htmlFor="icp-size"><Input id="icp-size" value={form.size} placeholder="50-500 employees" onChange={set("size")} /></FormField>
|
|
206
|
+
<FormField label="Regions" htmlFor="icp-regions" hint="Comma separated."><Input id="icp-regions" value={form.regions} onChange={set("regions")} /></FormField>
|
|
207
|
+
<FormField label="Buying signals" htmlFor="icp-signals" hint="One per line."><Textarea id="icp-signals" value={form.signals} onChange={set("signals")} /></FormField>
|
|
208
|
+
{saveIcp.error ? <ApiError error={saveIcp.error} title="Could not save the ICP" /> : null}
|
|
209
|
+
</div>
|
|
210
|
+
<DialogFooter>
|
|
211
|
+
<Button variant="outline" onClick={() => setEditing(null)}>Cancel</Button>
|
|
212
|
+
<Button loading={saveIcp.loading} disabled={!form.name.trim()} onClick={() => void saveIcp.mutate(undefined)}>Save ICP</Button>
|
|
213
|
+
</DialogFooter>
|
|
214
|
+
</DialogContent>
|
|
215
|
+
</Dialog>
|
|
216
|
+
|
|
217
|
+
<ConfirmSpendDialog
|
|
218
|
+
open={archiving !== null}
|
|
219
|
+
tier="destructive"
|
|
220
|
+
title={`Archive ${archiving?.name ?? "this ICP"}`}
|
|
221
|
+
description="It drops out of the default list. Campaigns already created from it are unaffected."
|
|
222
|
+
affected={1}
|
|
223
|
+
confirmLabel="Archive ICP"
|
|
224
|
+
confirming={archive.loading}
|
|
225
|
+
onConfirm={() => archiving && void archive.mutate(archiving)}
|
|
226
|
+
onCancel={() => setArchiving(null)}
|
|
227
|
+
/>
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
}
|