@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,256 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../block.schema.json",
|
|
3
|
+
"id": "social-ads",
|
|
4
|
+
"title": "Social & ads",
|
|
5
|
+
"summary": "Compose, schedule and publish social posts; see ad campaigns and their results; generate ad creatives; sync a list to an ad platform and launch a campaign.",
|
|
6
|
+
"features": [
|
|
7
|
+
"post composer with AI drafts, draft/schedule/publish, retry-safe and shown in the viewer's time zone",
|
|
8
|
+
"post list by status with publish, cancel and delete, each with its own spinner",
|
|
9
|
+
"ad campaigns across Meta, LinkedIn, Google and X with results for 7, 30 or 90 days, status refresh and launch retry",
|
|
10
|
+
"AI ad creative gallery: generate, follow, edit copy, regenerate the image, move to the gallery",
|
|
11
|
+
"audience sync from a list to an ad platform, push now, pause, errors, and campaign launch"
|
|
12
|
+
],
|
|
13
|
+
"dependsOn": [
|
|
14
|
+
"lists"
|
|
15
|
+
],
|
|
16
|
+
"operations": [
|
|
17
|
+
"cancel_social_post_social_posts__post_id__cancel_post",
|
|
18
|
+
"create_ad_creative_ad_creatives_post",
|
|
19
|
+
"create_audience_sync_audience_syncs_post",
|
|
20
|
+
"create_social_post_social_posts_post",
|
|
21
|
+
"delete_ad_campaign_ad_campaigns__campaign_id__delete",
|
|
22
|
+
"delete_ad_creative_ad_creatives__creative_id__delete",
|
|
23
|
+
"delete_audience_sync_audience_syncs__config_id__delete",
|
|
24
|
+
"delete_social_post_social_posts__post_id__delete",
|
|
25
|
+
"generate_social_post_social_posts_generate_post",
|
|
26
|
+
"get_ad_creative_specs_ad_creatives_specs_get",
|
|
27
|
+
"get_ad_creative_status_ad_creatives__creative_id__status_get",
|
|
28
|
+
"get_per_campaign_metrics_ad_campaigns_metrics_get",
|
|
29
|
+
"get_social_post_social_posts__post_id__get",
|
|
30
|
+
"launch_ad_campaign_audience_syncs__config_id__launch_post",
|
|
31
|
+
"list_ad_campaigns_ad_campaigns_get",
|
|
32
|
+
"list_ad_creatives_ad_creatives_get",
|
|
33
|
+
"list_audience_syncs_audience_syncs_get",
|
|
34
|
+
"list_social_accounts_social_accounts_get",
|
|
35
|
+
"list_social_posts_social_posts_get",
|
|
36
|
+
"list_sync_errors_audience_syncs__config_id__errors_get",
|
|
37
|
+
"list_sync_runs_audience_syncs__config_id__runs_get",
|
|
38
|
+
"move_ad_creative_to_gallery_ad_creatives__creative_id__move_to_gallery_post",
|
|
39
|
+
"publish_social_post_social_posts__post_id__publish_post",
|
|
40
|
+
"refresh_all_campaign_statuses_ad_campaigns_status_refresh_post",
|
|
41
|
+
"refresh_campaign_status_ad_campaigns__campaign_id__status_refresh_post",
|
|
42
|
+
"regenerate_ad_creative_image_ad_creatives__creative_id__regenerate_image_post",
|
|
43
|
+
"retry_campaign_launch_ad_campaigns__campaign_id__retry_post",
|
|
44
|
+
"schedule_social_post_social_posts__post_id__schedule_post",
|
|
45
|
+
"trigger_audience_sync_audience_syncs__config_id__trigger_post",
|
|
46
|
+
"update_ad_creative_ad_creatives__creative_id__patch",
|
|
47
|
+
"update_audience_sync_audience_syncs__config_id__patch",
|
|
48
|
+
"update_social_post_social_posts__post_id__patch"
|
|
49
|
+
],
|
|
50
|
+
"components": [
|
|
51
|
+
{
|
|
52
|
+
"name": "PostComposer",
|
|
53
|
+
"file": "components/post-composer.tsx",
|
|
54
|
+
"purpose": "Write a post, optionally draft it with AI (billable), save a draft, schedule it for a time in your zone or publish now; scheduling and publishing are confirmed (external).",
|
|
55
|
+
"operations": [
|
|
56
|
+
"list_social_accounts_social_accounts_get",
|
|
57
|
+
"generate_social_post_social_posts_generate_post",
|
|
58
|
+
"create_social_post_social_posts_post",
|
|
59
|
+
"update_social_post_social_posts__post_id__patch",
|
|
60
|
+
"schedule_social_post_social_posts__post_id__schedule_post",
|
|
61
|
+
"publish_social_post_social_posts__post_id__publish_post"
|
|
62
|
+
],
|
|
63
|
+
"tiers": [
|
|
64
|
+
"read",
|
|
65
|
+
"write",
|
|
66
|
+
"billable",
|
|
67
|
+
"external"
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"name": "SocialPostList",
|
|
72
|
+
"file": "components/social-post-list.tsx",
|
|
73
|
+
"purpose": "Posts by status with their times; publish, cancel a scheduled post or delete, each confirmed and each row with its own busy state.",
|
|
74
|
+
"operations": [
|
|
75
|
+
"list_social_posts_social_posts_get",
|
|
76
|
+
"publish_social_post_social_posts__post_id__publish_post",
|
|
77
|
+
"cancel_social_post_social_posts__post_id__cancel_post",
|
|
78
|
+
"delete_social_post_social_posts__post_id__delete"
|
|
79
|
+
],
|
|
80
|
+
"tiers": [
|
|
81
|
+
"read",
|
|
82
|
+
"external",
|
|
83
|
+
"destructive"
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"name": "AdCampaignList",
|
|
88
|
+
"file": "components/ad-campaign-list.tsx",
|
|
89
|
+
"purpose": "Ad campaigns with budget, date-only dates and results; refresh review status, retry a failed launch, delete.",
|
|
90
|
+
"operations": [
|
|
91
|
+
"list_ad_campaigns_ad_campaigns_get",
|
|
92
|
+
"get_per_campaign_metrics_ad_campaigns_metrics_get",
|
|
93
|
+
"refresh_campaign_status_ad_campaigns__campaign_id__status_refresh_post",
|
|
94
|
+
"refresh_all_campaign_statuses_ad_campaigns_status_refresh_post",
|
|
95
|
+
"retry_campaign_launch_ad_campaigns__campaign_id__retry_post",
|
|
96
|
+
"delete_ad_campaign_ad_campaigns__campaign_id__delete"
|
|
97
|
+
],
|
|
98
|
+
"tiers": [
|
|
99
|
+
"read",
|
|
100
|
+
"external",
|
|
101
|
+
"destructive"
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"name": "CreativeGallery",
|
|
106
|
+
"file": "components/creative-gallery.tsx",
|
|
107
|
+
"purpose": "Generate AI ad creatives (billable), follow them while they generate, edit copy, regenerate the image (billable), move to the gallery, delete.",
|
|
108
|
+
"operations": [
|
|
109
|
+
"get_ad_creative_specs_ad_creatives_specs_get",
|
|
110
|
+
"list_ad_creatives_ad_creatives_get",
|
|
111
|
+
"create_ad_creative_ad_creatives_post",
|
|
112
|
+
"update_ad_creative_ad_creatives__creative_id__patch",
|
|
113
|
+
"regenerate_ad_creative_image_ad_creatives__creative_id__regenerate_image_post",
|
|
114
|
+
"move_ad_creative_to_gallery_ad_creatives__creative_id__move_to_gallery_post",
|
|
115
|
+
"delete_ad_creative_ad_creatives__creative_id__delete"
|
|
116
|
+
],
|
|
117
|
+
"tiers": [
|
|
118
|
+
"read",
|
|
119
|
+
"write",
|
|
120
|
+
"billable",
|
|
121
|
+
"destructive"
|
|
122
|
+
]
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"name": "AudienceSync",
|
|
126
|
+
"file": "components/audience-sync.tsx",
|
|
127
|
+
"purpose": "Send a list to an ad platform as an audience, push now, pause or resume, read errors, delete, and launch a campaign on it; every step confirmed with the list size or the daily budget (external).",
|
|
128
|
+
"operations": [
|
|
129
|
+
"list_audience_syncs_audience_syncs_get",
|
|
130
|
+
"create_audience_sync_audience_syncs_post",
|
|
131
|
+
"trigger_audience_sync_audience_syncs__config_id__trigger_post",
|
|
132
|
+
"update_audience_sync_audience_syncs__config_id__patch",
|
|
133
|
+
"list_sync_errors_audience_syncs__config_id__errors_get",
|
|
134
|
+
"launch_ad_campaign_audience_syncs__config_id__launch_post",
|
|
135
|
+
"delete_audience_sync_audience_syncs__config_id__delete"
|
|
136
|
+
],
|
|
137
|
+
"tiers": [
|
|
138
|
+
"read",
|
|
139
|
+
"external",
|
|
140
|
+
"destructive"
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
],
|
|
144
|
+
"hooks": [
|
|
145
|
+
{
|
|
146
|
+
"name": "useSocialAccounts",
|
|
147
|
+
"file": "hooks/use-social-accounts.ts",
|
|
148
|
+
"purpose": "Connected social accounts; the account id is the account_ref posts take.",
|
|
149
|
+
"operations": [
|
|
150
|
+
"list_social_accounts_social_accounts_get"
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
],
|
|
154
|
+
"lib": [
|
|
155
|
+
{
|
|
156
|
+
"file": "lib/social-ads-types.ts",
|
|
157
|
+
"purpose": "Post, account, campaign, creative and sync shapes; time helpers (localInputToIso, formatInstant, formatDateOnly), metricsFor and listOf."
|
|
158
|
+
}
|
|
159
|
+
],
|
|
160
|
+
"recipes": [
|
|
161
|
+
{
|
|
162
|
+
"file": "recipes/schedule-post.ts",
|
|
163
|
+
"title": "Draft a social post with AI, then schedule or publish it",
|
|
164
|
+
"when": "Posting to a connected LinkedIn account. Generating is billable and saves nothing; scheduling and publishing are external, so get approval for the account and the instant. Keep the created post id so a retry reuses it."
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"file": "recipes/creative-to-gallery.ts",
|
|
168
|
+
"title": "Generate an ad creative, wait for it, tweak the copy, move it to the gallery",
|
|
169
|
+
"when": "Producing ad images and copy before a launch. Generation is billable with no advance price. Poll the status until it leaves generating."
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"file": "recipes/sync-audience-and-launch.ts",
|
|
173
|
+
"title": "Sync a list to an ad platform and launch a campaign on it",
|
|
174
|
+
"when": "Retargeting from a list. Both steps are external (data goes to the platform, the campaign spends from the ad account). The ad account must already be connected in graph8; dates are YYYY-MM-DD."
|
|
175
|
+
}
|
|
176
|
+
],
|
|
177
|
+
"prerequisites": [
|
|
178
|
+
{
|
|
179
|
+
"id": "lists-installed",
|
|
180
|
+
"where": "app",
|
|
181
|
+
"description": "The lists block is installed (the CLI does it): audiences come from the list picker.",
|
|
182
|
+
"verify": "components/g8/lists/ exists."
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"id": "proxy-allowlist",
|
|
186
|
+
"where": "app",
|
|
187
|
+
"description": "allowedPrefixes in the /api/g8 route includes `social`, `ad-campaigns`, `ad-creatives`, `audience-syncs` and `lists`.",
|
|
188
|
+
"verify": "GET /api/g8/social/accounts returns 200 when signed in."
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"id": "social-account",
|
|
192
|
+
"where": "api",
|
|
193
|
+
"description": "A LinkedIn account is connected in graph8 (Settings, Channels). Connecting needs a browser sign-in and cannot be done through the API.",
|
|
194
|
+
"verify": "GET /social/accounts returns at least one account."
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"id": "ad-accounts",
|
|
198
|
+
"where": "api",
|
|
199
|
+
"description": "Each ad platform account (Meta, LinkedIn, Google, X) is connected in graph8 through the platform's sign-in. Without it, creating a sync or launching fails.",
|
|
200
|
+
"verify": "Creating a sync for the platform succeeds."
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"id": "credits",
|
|
204
|
+
"where": "api",
|
|
205
|
+
"description": "The workspace has credits for AI drafts and ad creatives. The API has no balance read and no advance price for these, so the UI only says credits are used. Ad spend itself is billed by the ad platform.",
|
|
206
|
+
"verify": "A draft generates."
|
|
207
|
+
}
|
|
208
|
+
],
|
|
209
|
+
"scopes": [
|
|
210
|
+
"ads:delete",
|
|
211
|
+
"ads:read",
|
|
212
|
+
"ads:run",
|
|
213
|
+
"campaigns:delete",
|
|
214
|
+
"campaigns:read",
|
|
215
|
+
"campaigns:run",
|
|
216
|
+
"campaigns:write",
|
|
217
|
+
"integrations:delete",
|
|
218
|
+
"integrations:read",
|
|
219
|
+
"integrations:run"
|
|
220
|
+
],
|
|
221
|
+
"defaults": [
|
|
222
|
+
{
|
|
223
|
+
"decision": "Account a post uses",
|
|
224
|
+
"default": "The first connected account; the person can switch",
|
|
225
|
+
"askHuman": false
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"decision": "AI drafts per request",
|
|
229
|
+
"default": "3",
|
|
230
|
+
"askHuman": false
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"decision": "Schedule time zone",
|
|
234
|
+
"default": "The viewer's own zone, named in the confirmation",
|
|
235
|
+
"askHuman": false
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"decision": "Audience sync mode and cadence",
|
|
239
|
+
"default": "Mirror, refreshed every 24 hours",
|
|
240
|
+
"askHuman": false
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
"decision": "Campaign daily budget, dates and destination URL",
|
|
244
|
+
"default": "None: always typed by the person",
|
|
245
|
+
"askHuman": true
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"decision": "Who may publish, sync or launch",
|
|
249
|
+
"default": "Anyone the proxy's authorize lets through",
|
|
250
|
+
"askHuman": true
|
|
251
|
+
}
|
|
252
|
+
],
|
|
253
|
+
"peerDependencies": {
|
|
254
|
+
"react": ">=18"
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AdCampaignList: your ad campaigns on Meta, LinkedIn, Google and X with their status, budget, dates and the
|
|
5
|
+
* results of the last N days. Refresh a campaign's review status from its platform, retry a failed launch, or delete
|
|
6
|
+
* one. Launching a NEW campaign is AudienceSync (it starts from a synced audience).
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. listAdCampaigns GET /ad-campaigns?platform (read) an array of campaigns; `platform_campaign_id` (else `sync_config_id`) joins them to metrics
|
|
10
|
+
* 2. getPerCampaignMetrics GET /ad-campaigns/metrics?days&platform (read) { days, metrics: [{ sync_config_id, platform_campaign_id, impressions, clicks, spend, currency }] }
|
|
11
|
+
* 3. refreshCampaignStatus POST /ad-campaigns/{id}/status-refresh (external) asks the ad platform; ConfirmSpendDialog first
|
|
12
|
+
* refreshAllCampaignStatuses POST /ad-campaigns/status-refresh (external) every refreshable campaign
|
|
13
|
+
* 4. retryCampaignLaunch POST /ad-campaigns/{id}/retry (external) re-sends a failed launch to the platform
|
|
14
|
+
* 5. deleteAdCampaign DELETE /ad-campaigns/{id} (destructive) STOPS the live campaign on the platform, then removes it.
|
|
15
|
+
* A 502 means the platform refused: the campaign is STILL LIVE and spending, and the list says so.
|
|
16
|
+
* Retry answers HTTP 200 with `status: "error"` in the body when refused: that is shown as a failure, never as success.
|
|
17
|
+
* Start and end dates are date-only: shown in UTC so they never slide a day. Spend is in the campaign's own currency.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { useState } from "react";
|
|
21
|
+
import { Button, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
22
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
23
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
24
|
+
import type { ConfirmTier } from "@/g8/components/confirm-spend-dialog";
|
|
25
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
26
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
27
|
+
import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
28
|
+
import { AD_PLATFORMS, formatDateOnly, formatMoney, launchOutcome, listOf, metricsFor } from "../lib/social-ads-types";
|
|
29
|
+
import type { AdCampaign, CampaignMetric } from "../lib/social-ads-types";
|
|
30
|
+
|
|
31
|
+
export interface AdCampaignListProps {
|
|
32
|
+
/** Change to reload, for example after AudienceSync launched a campaign. */
|
|
33
|
+
refreshKey?: number;
|
|
34
|
+
className?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type Pending = { kind: "refresh" | "retry" | "delete"; campaign: AdCampaign } | { kind: "refreshAll" };
|
|
38
|
+
const COLUMNS = 8;
|
|
39
|
+
|
|
40
|
+
function copyFor(p: Pending | null): { tier: ConfirmTier; title: string; text: string; label: string } {
|
|
41
|
+
if (!p) return { tier: "external", title: "", text: "", label: "Confirm" };
|
|
42
|
+
if (p.kind === "refreshAll") return { tier: "external", title: "Refresh every campaign's status", text: "Asks each ad platform for the current review state of every campaign. Can take a while; campaigns that did refresh keep their new state if it times out.", label: "Refresh all" };
|
|
43
|
+
const name = p.campaign.name;
|
|
44
|
+
if (p.kind === "refresh") return { tier: "external", title: `Refresh "${name}"`, text: `Asks ${p.campaign.platform} for this campaign's current review state.`, label: "Refresh" };
|
|
45
|
+
if (p.kind === "retry") return { tier: "external", title: `Retry launching "${name}"`, text: `Sends the campaign to ${p.campaign.platform} again. It may start spending on that platform once approved.`, label: "Retry launch" };
|
|
46
|
+
return { tier: "destructive", title: `Stop and delete "${name}"`, text: `Stops the campaign on ${p.campaign.platform} first, then removes it from graph8 together with its history. This cannot be undone. If ${p.campaign.platform} refuses to stop it, the campaign stays live and keeps spending.`, label: "Stop and delete" };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function AdCampaignList({ refreshKey = 0, className }: AdCampaignListProps) {
|
|
50
|
+
const [platform, setPlatform] = useState("");
|
|
51
|
+
const [days, setDays] = useState(30);
|
|
52
|
+
const [pending, setPending] = useState<Pending | null>(null);
|
|
53
|
+
const [busy, setBusy] = useState<string | null>(null);
|
|
54
|
+
const [failure, setFailure] = useState<unknown>(null);
|
|
55
|
+
|
|
56
|
+
const campaigns = useG8Query(async (signal) => listOf<AdCampaign>((await g8Get("/ad-campaigns", { platform: platform || undefined }, { signal })).data), [platform, refreshKey]);
|
|
57
|
+
const metrics = useG8Query(
|
|
58
|
+
async (signal) => listOf<CampaignMetric>((await g8Get("/ad-campaigns/metrics", { days, platform: platform || undefined }, { signal })).data, "metrics"),
|
|
59
|
+
[days, platform, refreshKey],
|
|
60
|
+
);
|
|
61
|
+
const rows = campaigns.data ?? [];
|
|
62
|
+
|
|
63
|
+
const act = async () => {
|
|
64
|
+
if (!pending) return;
|
|
65
|
+
const id = pending.kind === "refreshAll" ? "all" : String(pending.campaign.id);
|
|
66
|
+
setBusy(id);
|
|
67
|
+
setFailure(null);
|
|
68
|
+
try {
|
|
69
|
+
if (pending.kind === "refreshAll") await g8Post("/ad-campaigns/status-refresh", {});
|
|
70
|
+
else if (pending.kind === "refresh") await g8Post(`/ad-campaigns/${id}/status-refresh`, {});
|
|
71
|
+
else if (pending.kind === "retry") {
|
|
72
|
+
const outcome = launchOutcome((await g8Post(`/ad-campaigns/${id}/retry`, {})).data);
|
|
73
|
+
if (!outcome.ok) setFailure(new Error(outcome.message));
|
|
74
|
+
}
|
|
75
|
+
else await g8Delete(`/ad-campaigns/${id}`);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
const status = (err as { status?: number })?.status;
|
|
78
|
+
if (pending.kind === "delete" && status === 502) setFailure(new Error(`${pending.campaign.platform} refused to stop "${pending.campaign.name}". It is STILL LIVE on the platform and may keep spending: stop it in the platform's own ads manager.`));
|
|
79
|
+
else if (!(pending.kind === "delete" && status === 404)) setFailure(err);
|
|
80
|
+
} finally {
|
|
81
|
+
setBusy(null);
|
|
82
|
+
setPending(null);
|
|
83
|
+
await Promise.all([campaigns.refetch(), metrics.refetch()]);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const copy = copyFor(pending);
|
|
88
|
+
return (
|
|
89
|
+
<div className={`flex flex-col gap-3 ${className ?? ""}`}>
|
|
90
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
91
|
+
<select aria-label="Platform" className="h-9 rounded-md border bg-background px-2" value={platform} onChange={(e) => setPlatform(e.target.value)}>
|
|
92
|
+
<option value="">All platforms</option>{AD_PLATFORMS.map((p) => <option key={p} value={p}>{p}</option>)}
|
|
93
|
+
</select>
|
|
94
|
+
<select aria-label="Results window" className="h-9 rounded-md border bg-background px-2" value={days} onChange={(e) => setDays(Number(e.target.value))}>
|
|
95
|
+
{[7, 30, 90].map((d) => <option key={d} value={d}>Last {d} days</option>)}
|
|
96
|
+
</select>
|
|
97
|
+
<Button size="sm" variant="outline" loading={busy === "all"} disabled={busy != null || rows.length === 0} onClick={() => setPending({ kind: "refreshAll" })}>Refresh all statuses</Button>
|
|
98
|
+
</div>
|
|
99
|
+
{campaigns.error ? <ApiError error={campaigns.error} title="Could not load campaigns" onRetry={() => void campaigns.refetch()} /> : null}
|
|
100
|
+
{metrics.error ? <p role="status" className="text-xs text-muted-foreground">Results are unavailable right now.</p> : null}
|
|
101
|
+
{failure ? <ApiError error={failure} title="That did not work" /> : null}
|
|
102
|
+
<div className="overflow-x-auto rounded-md border">
|
|
103
|
+
<Table>
|
|
104
|
+
<TableHeader><TableRow>{["Campaign", "Platform", "Status", "Budget / day", "Dates", "Impressions", "Clicks", ""].map((h) => <TableHead key={h || "actions"}>{h}</TableHead>)}</TableRow></TableHeader>
|
|
105
|
+
<TableBody>
|
|
106
|
+
{campaigns.loading && !campaigns.data ? <TableRow><TableCell colSpan={COLUMNS}><Skeleton className="h-8 w-full" /></TableCell></TableRow> : null}
|
|
107
|
+
{!campaigns.loading && !campaigns.error && rows.length === 0 ? <TableRow><TableCell colSpan={COLUMNS}><EmptyState title="No ad campaigns" description="Launch one from a synced audience." /></TableCell></TableRow> : null}
|
|
108
|
+
{rows.map((c) => {
|
|
109
|
+
const m = metricsFor(c, metrics.data ?? []);
|
|
110
|
+
const mine = busy === String(c.id);
|
|
111
|
+
return (
|
|
112
|
+
<TableRow key={c.id} data-testid="campaign">
|
|
113
|
+
<TableCell><div className="font-medium">{c.name}</div>{m ? <div className="text-xs text-muted-foreground">{formatMoney(m.spend, m.currency ?? c.currency)} spent</div> : null}</TableCell>
|
|
114
|
+
<TableCell>{c.platform}</TableCell>
|
|
115
|
+
<TableCell>{c.status}</TableCell>
|
|
116
|
+
<TableCell>{formatMoney(c.daily_budget, c.currency)}</TableCell>
|
|
117
|
+
<TableCell>{formatDateOnly(c.start_date)}{c.end_date ? ` to ${formatDateOnly(c.end_date)}` : ""}</TableCell>
|
|
118
|
+
<TableCell>{m ? m.impressions.toLocaleString() : "-"}</TableCell>
|
|
119
|
+
<TableCell>{m ? m.clicks.toLocaleString() : "-"}</TableCell>
|
|
120
|
+
<TableCell>
|
|
121
|
+
<div className="flex gap-1">
|
|
122
|
+
<Button size="sm" variant="ghost" loading={mine} disabled={busy != null} onClick={() => setPending({ kind: "refresh", campaign: c })}>Refresh</Button>
|
|
123
|
+
{c.status === "failed" ? <Button size="sm" variant="outline" disabled={busy != null} onClick={() => setPending({ kind: "retry", campaign: c })}>Retry</Button> : null}
|
|
124
|
+
<Button size="sm" variant="ghost" disabled={busy != null} onClick={() => setPending({ kind: "delete", campaign: c })}>Delete</Button>
|
|
125
|
+
</div>
|
|
126
|
+
</TableCell>
|
|
127
|
+
</TableRow>
|
|
128
|
+
);
|
|
129
|
+
})}
|
|
130
|
+
</TableBody>
|
|
131
|
+
</Table>
|
|
132
|
+
</div>
|
|
133
|
+
<ConfirmSpendDialog open={pending != null} tier={copy.tier} title={copy.title} description={copy.text} confirmLabel={copy.label} confirming={busy != null} onConfirm={() => void act()} onCancel={() => setPending(null)} />
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AudienceSync: send a graph8 list to an ad platform as a custom audience (Meta, LinkedIn, Google, X), keep it in sync,
|
|
5
|
+
* and launch an ad campaign that targets it. Everything here sends people's data to a third party or can start ad
|
|
6
|
+
* spend, so every action is confirmed with the real scope: which list, how many people, which platform.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. listAudienceSyncs GET /audience-syncs (read) an array of sync configs
|
|
10
|
+
* 2. createAudienceSync POST /audience-syncs { audience_id, platform, mode, refresh_cadence_hours } (external) 409 if the list+platform pair exists;
|
|
11
|
+
* the ad account must already be connected in graph8 (OAuth happens in the app only). ConfirmSpendDialog first with the list size.
|
|
12
|
+
* 3. triggerAudienceSync POST /audience-syncs/{id}/trigger (external) push now; ConfirmSpendDialog first
|
|
13
|
+
* 4. updateAudienceSync PATCH /audience-syncs/{id} { is_active } (external) pause or resume; ConfirmSpendDialog first
|
|
14
|
+
* 5. listSyncErrors GET /audience-syncs/{id}/errors?limit (read) shown under a sync on request
|
|
15
|
+
* 6. launchAdCampaign POST /audience-syncs/{id}/launch { name, objective, daily_budget, ... } (external) creates the campaign on the platform;
|
|
16
|
+
* ConfirmSpendDialog first with the daily budget in its currency. Start and end are date-only (YYYY-MM-DD), sent as typed, and the end may not
|
|
17
|
+
* be before the start. HTTP 200 does NOT mean launched: a refused launch answers 200 with `status: "error"` and a `message` in the body, shown as a failure.
|
|
18
|
+
* Google needs `creative_image_url` (a square image); a CreativeGallery image URL fits.
|
|
19
|
+
* 7. deleteAudienceSync DELETE /audience-syncs/{id} (destructive) stops syncing; the audience stays on the platform
|
|
20
|
+
* Each sync has its own busy state; deleting treats a 404 as already done.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { useState } from "react";
|
|
24
|
+
import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
|
|
25
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
26
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
27
|
+
import type { ConfirmTier } from "@/g8/components/confirm-spend-dialog";
|
|
28
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
29
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
30
|
+
import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
31
|
+
import { ListPicker } from "@/g8/lists/components/list-picker";
|
|
32
|
+
import type { ListSummary } from "@/g8/lists/components/list-picker";
|
|
33
|
+
import { AD_PLATFORMS, formatInstant, formatMoney, launchOutcome, listOf } from "../lib/social-ads-types";
|
|
34
|
+
import type { AudienceSyncConfig } from "../lib/social-ads-types";
|
|
35
|
+
|
|
36
|
+
export interface AudienceSyncProps {
|
|
37
|
+
/** Called after a campaign was launched, for example to refresh an AdCampaignList. */
|
|
38
|
+
onLaunched?: () => void;
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type Pending =
|
|
43
|
+
| { kind: "create"; list: ListSummary }
|
|
44
|
+
| { kind: "trigger" | "toggle" | "delete"; sync: AudienceSyncConfig }
|
|
45
|
+
| { kind: "launch"; sync: AudienceSyncConfig };
|
|
46
|
+
|
|
47
|
+
const listSize = (l: ListSummary) => l.total ?? l.contact_count ?? l.company_count ?? undefined;
|
|
48
|
+
|
|
49
|
+
export function AudienceSync({ onLaunched, className }: AudienceSyncProps) {
|
|
50
|
+
const [list, setList] = useState<ListSummary | null>(null);
|
|
51
|
+
const [platform, setPlatform] = useState<string>("meta");
|
|
52
|
+
const [mode, setMode] = useState("mirror");
|
|
53
|
+
const [cadence, setCadence] = useState(24);
|
|
54
|
+
const [pending, setPending] = useState<Pending | null>(null);
|
|
55
|
+
const [busy, setBusy] = useState<number | "new" | null>(null);
|
|
56
|
+
const [failure, setFailure] = useState<unknown>(null);
|
|
57
|
+
const [errorsFor, setErrorsFor] = useState<number | null>(null);
|
|
58
|
+
const [launch, setLaunch] = useState({ syncId: 0, name: "", objective: "", daily_budget: "", currency: "USD", destination_url: "", creative_image_url: "", headline: "", primary_text: "", start_date: "", end_date: "" });
|
|
59
|
+
const [notice, setNotice] = useState<string | null>(null);
|
|
60
|
+
|
|
61
|
+
const syncs = useG8Query(async (signal) => listOf<AudienceSyncConfig>((await g8Get("/audience-syncs", undefined, { signal })).data), []);
|
|
62
|
+
const errors = useG8Query(async (signal) => listOf<{ id: number; error_message?: string; started_at?: string }>((await g8Get(`/audience-syncs/${errorsFor}/errors`, { limit: 5 }, { signal })).data), [errorsFor], { enabled: errorsFor != null });
|
|
63
|
+
|
|
64
|
+
const work = async (id: number | "new", fn: () => Promise<unknown>, tolerate404 = false) => {
|
|
65
|
+
setBusy(id);
|
|
66
|
+
setFailure(null);
|
|
67
|
+
try { await fn(); } catch (err) { if (!(tolerate404 && (err as { status?: number })?.status === 404)) setFailure(err); }
|
|
68
|
+
finally { setBusy(null); setPending(null); await syncs.refetch(); }
|
|
69
|
+
};
|
|
70
|
+
const confirm = () => {
|
|
71
|
+
if (!pending) return;
|
|
72
|
+
if (pending.kind === "create") return void work("new", async () => { await g8Post("/audience-syncs", { audience_id: pending.list.id, platform, mode, refresh_cadence_hours: cadence }); setList(null); });
|
|
73
|
+
const id = pending.sync.id;
|
|
74
|
+
if (pending.kind === "trigger") return void work(id, async () => { await g8Post(`/audience-syncs/${id}/trigger`, {}); setNotice("Sync started."); });
|
|
75
|
+
if (pending.kind === "toggle") return void work(id, () => g8Patch(`/audience-syncs/${id}`, { is_active: !pending.sync.is_active }));
|
|
76
|
+
if (pending.kind === "delete") return void work(id, () => g8Delete(`/audience-syncs/${id}`), true);
|
|
77
|
+
return void work(id, async () => {
|
|
78
|
+
const res = await g8Post(`/audience-syncs/${id}/launch`, { name: launch.name.trim(), objective: launch.objective.trim() || undefined, daily_budget: Number(launch.daily_budget), currency: launch.currency, destination_url: launch.destination_url.trim(), creative_image_url: launch.creative_image_url.trim() || undefined, headline: launch.headline.trim() || undefined, primary_text: launch.primary_text.trim() || undefined, start_date: launch.start_date || undefined, end_date: launch.end_date || undefined });
|
|
79
|
+
// HTTP 200 does not mean launched: a refused launch answers 200 with status "error" in the body.
|
|
80
|
+
const outcome = launchOutcome(res.data);
|
|
81
|
+
if (!outcome.ok) throw new Error(outcome.message);
|
|
82
|
+
setLaunch({ ...launch, syncId: 0, name: "", daily_budget: "", creative_image_url: "" });
|
|
83
|
+
setNotice(outcome.message ?? "Campaign sent to the platform.");
|
|
84
|
+
onLaunched?.();
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const datesOk = !launch.start_date || !launch.end_date || launch.end_date >= launch.start_date; // YYYY-MM-DD compares as text
|
|
89
|
+
const readyToLaunch = launch.name.trim() && Number(launch.daily_budget) > 0 && /^https?:\/\//.test(launch.destination_url.trim()) && datesOk;
|
|
90
|
+
const meta = describe(pending, platform, formatMoney(Number(launch.daily_budget), launch.currency));
|
|
91
|
+
return (
|
|
92
|
+
<div className={`flex flex-col gap-5 ${className ?? ""}`}>
|
|
93
|
+
<section className="flex flex-col gap-2 rounded-md border p-3" aria-label="New audience sync">
|
|
94
|
+
<h3 className="text-sm font-medium">Send a list to an ad platform</h3>
|
|
95
|
+
<ListPicker value={list?.id ?? null} onChange={setList} />
|
|
96
|
+
<div className="flex flex-wrap items-end gap-2">
|
|
97
|
+
<select aria-label="Platform" className="h-9 rounded-md border bg-background px-2" value={platform} onChange={(e) => setPlatform(e.target.value)}>{AD_PLATFORMS.map((p) => <option key={p} value={p}>{p}</option>)}</select>
|
|
98
|
+
<select aria-label="Mode" className="h-9 rounded-md border bg-background px-2" value={mode} onChange={(e) => setMode(e.target.value)}><option value="mirror">Mirror (remove people who leave the list)</option><option value="append_only">Only add people</option></select>
|
|
99
|
+
<div className="flex flex-col gap-1"><Label htmlFor="as-cadence">Refresh every (hours)</Label><Input id="as-cadence" type="number" min={0} max={720} value={cadence} onChange={(e) => setCadence(Number(e.target.value))} /></div>
|
|
100
|
+
<Button loading={busy === "new"} disabled={!list || busy != null} onClick={() => list && setPending({ kind: "create", list })}>Create sync</Button>
|
|
101
|
+
</div>
|
|
102
|
+
</section>
|
|
103
|
+
{failure ? <ApiError error={failure} title="That did not work" /> : null}
|
|
104
|
+
{notice ? <p role="status" className="text-sm text-muted-foreground">{notice}</p> : null}
|
|
105
|
+
{syncs.error ? <ApiError error={syncs.error} title="Could not load syncs" onRetry={() => void syncs.refetch()} /> : null}
|
|
106
|
+
{syncs.loading && !syncs.data ? <Skeleton className="h-20 w-full" /> : null}
|
|
107
|
+
{!syncs.loading && !syncs.error && (syncs.data ?? []).length === 0 ? <EmptyState title="No audience syncs yet" description="Pick a list above and the platform to send it to. The ad account must be connected in graph8 first." /> : null}
|
|
108
|
+
<ul className="flex flex-col gap-2">
|
|
109
|
+
{(syncs.data ?? []).map((s) => (
|
|
110
|
+
<li key={s.id} className="flex flex-col gap-2 rounded-md border p-3" data-testid="sync" aria-busy={busy === s.id}>
|
|
111
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
112
|
+
<div><p className="text-sm font-medium">{s.platform_audience_name || `List ${s.audience_id}`} on {s.platform}</p>
|
|
113
|
+
<p className="text-xs text-muted-foreground">{s.is_active ? s.status ?? "active" : "paused"} · {s.last_sync_at ? `last synced ${formatInstant(s.last_sync_at)}` : "not synced yet"}{s.last_sync_error ? ` · ${s.last_sync_error}` : ""}</p></div>
|
|
114
|
+
<div className="flex flex-wrap gap-1">
|
|
115
|
+
<Button size="sm" variant="outline" loading={busy === s.id} disabled={busy != null || !s.is_active} onClick={() => setPending({ kind: "trigger", sync: s })}>Sync now</Button>
|
|
116
|
+
<Button size="sm" variant="ghost" disabled={busy != null} onClick={() => setPending({ kind: "toggle", sync: s })}>{s.is_active ? "Pause" : "Resume"}</Button>
|
|
117
|
+
<Button size="sm" variant="ghost" onClick={() => setErrorsFor(errorsFor === s.id ? null : s.id)}>Errors</Button>
|
|
118
|
+
<Button size="sm" variant="ghost" onClick={() => setLaunch({ ...launch, syncId: launch.syncId === s.id ? 0 : s.id })}>Launch campaign</Button>
|
|
119
|
+
<Button size="sm" variant="ghost" disabled={busy != null} onClick={() => setPending({ kind: "delete", sync: s })}>Delete</Button>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
{errorsFor === s.id ? <div className="text-xs">{errors.loading ? "Loading..." : (errors.data ?? []).length === 0 ? "No recent errors." : <ul>{(errors.data ?? []).map((e) => <li key={e.id}>{formatInstant(e.started_at)}: {e.error_message}</li>)}</ul>}</div> : null}
|
|
123
|
+
{launch.syncId === s.id ? (
|
|
124
|
+
<form className="grid gap-2 sm:grid-cols-2" onSubmit={(e) => { e.preventDefault(); if (readyToLaunch) setPending({ kind: "launch", sync: s }); }}>
|
|
125
|
+
{([["name", "Campaign name"], ["objective", "Objective"], ["daily_budget", "Daily budget"], ["destination_url", "Landing page URL"], ["creative_image_url", "Image URL (required on Google)"], ["headline", "Headline"], ["primary_text", "Primary text"]] as const).map(([key, label]) => (
|
|
126
|
+
<div key={key} className="flex flex-col gap-1"><Label htmlFor={`as-${key}`}>{label}</Label><Input id={`as-${key}`} value={launch[key]} onChange={(e) => setLaunch({ ...launch, [key]: e.target.value })} /></div>
|
|
127
|
+
))}
|
|
128
|
+
<div className="flex flex-col gap-1"><Label htmlFor="as-currency">Currency</Label><select id="as-currency" className="h-9 rounded-md border bg-background px-2" value={launch.currency} onChange={(e) => setLaunch({ ...launch, currency: e.target.value })}>{["USD", "EUR", "GBP", "CAD", "AUD"].map((c) => <option key={c} value={c}>{c}</option>)}</select></div>
|
|
129
|
+
<div className="flex flex-col gap-1"><Label htmlFor="as-start">Start date</Label><Input id="as-start" type="date" value={launch.start_date} onChange={(e) => setLaunch({ ...launch, start_date: e.target.value })} /></div>
|
|
130
|
+
<div className="flex flex-col gap-1"><Label htmlFor="as-end">End date</Label><Input id="as-end" type="date" value={launch.end_date} onChange={(e) => setLaunch({ ...launch, end_date: e.target.value })} /></div>
|
|
131
|
+
<div><Button type="submit" disabled={!readyToLaunch || busy != null}>Review launch</Button>{!datesOk ? <p role="alert" className="mt-1 text-xs text-destructive">The end date is before the start date.</p> : null}</div>
|
|
132
|
+
</form>
|
|
133
|
+
) : null}
|
|
134
|
+
</li>
|
|
135
|
+
))}
|
|
136
|
+
</ul>
|
|
137
|
+
<ConfirmSpendDialog open={pending != null} tier={meta.tier} title={meta.title} description={meta.text} recipients={meta.recipients} affected={meta.affected} confirmLabel={meta.label} confirming={busy != null} onConfirm={confirm} onCancel={() => setPending(null)} />
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function describe(p: Pending | null, platform: string, budget: string): { tier: ConfirmTier; title: string; text: string; label: string; recipients?: number; affected?: number } {
|
|
143
|
+
if (!p) return { tier: "external", title: "", text: "", label: "Confirm" };
|
|
144
|
+
if (p.kind === "create") return { tier: "external", title: `Send "${p.list.title}" to ${platform}`, label: "Create sync", recipients: listSize(p.list), text: `Sends the contacts of this list to ${platform} as a custom audience (matched on hashed identifiers) and keeps it refreshed. Anyone matched becomes targetable by your ads there.` };
|
|
145
|
+
const s = p.sync;
|
|
146
|
+
if (p.kind === "trigger") return { tier: "external", title: `Sync "${s.platform_audience_name || s.audience_id}" to ${s.platform} now`, label: "Sync now", text: `Pushes the current members of the list to ${s.platform}.` };
|
|
147
|
+
if (p.kind === "toggle") return { tier: "external", title: `${s.is_active ? "Pause" : "Resume"} the ${s.platform} sync`, label: s.is_active ? "Pause" : "Resume", text: s.is_active ? "The audience stops refreshing; people already there stay." : `Resumes refreshing the audience on ${s.platform}.` };
|
|
148
|
+
if (p.kind === "delete") return { tier: "destructive", title: `Delete the ${s.platform} sync`, label: "Delete", text: "graph8 stops syncing. The audience that already exists on the platform is not removed from here." };
|
|
149
|
+
return { tier: "external", title: `Launch a campaign on ${s.platform}`, label: "Launch", text: `Creates the campaign on ${s.platform}, targeting the synced audience, at ${budget} per day. Once the platform approves it, it spends from your ad account; graph8 credits are not used for that spend.` };
|
|
150
|
+
}
|