@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,210 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PersonaEditor: the buyer personas campaigns speak to. Add and edit them by hand for free, or have AI
|
|
5
|
+
* recommend a set from your company profile (confirmed first, it spends credits).
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. getCompanyProfile GET /company-profile (read; via useCompanyProfile, supplies website_url)
|
|
9
|
+
* 2. listPersonas GET /personas (read)
|
|
10
|
+
* 3. createPersona / updatePersona POST /personas, PATCH /personas/{persona_id} (write)
|
|
11
|
+
* 4. recommendCampaignPersonas POST /campaigns/recommend-personas (billable, ConfirmSpendDialog; returns suggestions and SAVES NOTHING)
|
|
12
|
+
* accept a suggestion POST /personas (write; free, no extra confirmation)
|
|
13
|
+
* 5. archivePersona POST /personas/{persona_id}/archive (destructive, ConfirmSpendDialog)
|
|
14
|
+
*
|
|
15
|
+
* Tiers: 4 spends credits, then you choose which suggestions to keep (each Accept is a free create); 5 removes a persona from the default list, so both are confirmed. Personas are
|
|
16
|
+
* keyed by the workspace website; with none saved, creating is disabled (save it in IcpEditor first).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useState } from "react";
|
|
20
|
+
import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Textarea } from "@graph8/sdk/ui";
|
|
21
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
23
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
24
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
25
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
26
|
+
import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
27
|
+
import { useCompanyProfile } from "../hooks/use-company-profile";
|
|
28
|
+
import { CREDIT_ESTIMATES, lines, listOf, statusClass, text } from "../lib/campaigns-utils";
|
|
29
|
+
import { FormField } from "./form-field";
|
|
30
|
+
|
|
31
|
+
export interface PersonaEditorProps {
|
|
32
|
+
/** Overrides the website from the saved profile. */
|
|
33
|
+
websiteUrl?: string;
|
|
34
|
+
/** Called after any change, so a wizard can refresh its persona choices. */
|
|
35
|
+
onChange?: () => void;
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface PersonaRow {
|
|
40
|
+
id: string;
|
|
41
|
+
title?: string | null;
|
|
42
|
+
why_target?: string | null;
|
|
43
|
+
key_signals?: unknown[] | null;
|
|
44
|
+
campaign_approach?: string | null;
|
|
45
|
+
recommended_goal?: string | null;
|
|
46
|
+
priority?: number | null;
|
|
47
|
+
confidence?: string | null;
|
|
48
|
+
status?: string | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface Suggestion {
|
|
52
|
+
title: string;
|
|
53
|
+
why_target?: string;
|
|
54
|
+
key_signals?: unknown[];
|
|
55
|
+
campaign_approach?: string;
|
|
56
|
+
expected_receptivity?: string;
|
|
57
|
+
priority?: number;
|
|
58
|
+
confidence?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface PersonaForm { title: string; why: string; signals: string; approach: string; goal: string; priority: string; confidence: string }
|
|
62
|
+
const emptyForm: PersonaForm = { title: "", why: "", signals: "", approach: "", goal: "", priority: "1", confidence: "medium" };
|
|
63
|
+
|
|
64
|
+
function formOf(p: PersonaRow): PersonaForm {
|
|
65
|
+
return {
|
|
66
|
+
title: p.title ?? "", why: p.why_target ?? "", approach: p.campaign_approach ?? "", goal: p.recommended_goal ?? "",
|
|
67
|
+
priority: String(p.priority ?? 1), confidence: p.confidence ?? "medium", signals: (p.key_signals ?? []).map((s) => text(s)).filter(Boolean).join("\n"),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function PersonaEditor({ websiteUrl, onChange, className }: PersonaEditorProps) {
|
|
72
|
+
const profile = useCompanyProfile(websiteUrl);
|
|
73
|
+
const personas = useG8Query((signal) => g8Get("/personas", { limit: 100 }, { signal }), []);
|
|
74
|
+
const [editing, setEditing] = useState<PersonaRow | "new" | null>(null);
|
|
75
|
+
const [form, setForm] = useState<PersonaForm>(emptyForm);
|
|
76
|
+
const [archiving, setArchiving] = useState<PersonaRow | null>(null);
|
|
77
|
+
const [confirmRecommend, setConfirmRecommend] = useState(false);
|
|
78
|
+
const [suggestions, setSuggestions] = useState<Suggestion[]>([]);
|
|
79
|
+
const [accepting, setAccepting] = useState<string | null>(null);
|
|
80
|
+
const [busyId, setBusyId] = useState<string | null>(null);
|
|
81
|
+
|
|
82
|
+
const done = () => { void personas.refetch(); onChange?.(); };
|
|
83
|
+
const save = useG8Mutation(async () => {
|
|
84
|
+
const body = {
|
|
85
|
+
title: form.title.trim(), why_target: form.why, key_signals: lines(form.signals), campaign_approach: form.approach,
|
|
86
|
+
recommended_goal: form.goal, priority: Number(form.priority) || 1, confidence: form.confidence,
|
|
87
|
+
};
|
|
88
|
+
return editing && editing !== "new" ? g8Patch(`/personas/${editing.id}`, body) : g8Post("/personas", { ...body, website_url: profile.websiteUrl });
|
|
89
|
+
}, { onSuccess: () => { setEditing(null); done(); } });
|
|
90
|
+
const archive = useG8Mutation(async (p: PersonaRow) => {
|
|
91
|
+
setBusyId(p.id);
|
|
92
|
+
try { await g8Post(`/personas/${p.id}/archive`, {}); } finally { setBusyId(null); setArchiving(null); }
|
|
93
|
+
}, { onSuccess: done });
|
|
94
|
+
const recommend = useG8Mutation(() => g8Post("/campaigns/recommend-personas", { website_url: profile.websiteUrl }), {
|
|
95
|
+
onSuccess: (res) => { setConfirmRecommend(false); setSuggestions(listOf<Suggestion>(res)); },
|
|
96
|
+
onError: () => setConfirmRecommend(false),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const accept = useG8Mutation(async (s: Suggestion) => {
|
|
100
|
+
setAccepting(s.title);
|
|
101
|
+
try {
|
|
102
|
+
await g8Post("/personas", {
|
|
103
|
+
title: s.title, why_target: s.why_target, key_signals: s.key_signals ?? [], campaign_approach: s.campaign_approach,
|
|
104
|
+
expected_receptivity: s.expected_receptivity, priority: s.priority ?? 1, confidence: s.confidence ?? "medium", source: "recommended", website_url: profile.websiteUrl,
|
|
105
|
+
});
|
|
106
|
+
} finally { setAccepting(null); }
|
|
107
|
+
setSuggestions((all) => all.filter((x) => x.title !== s.title));
|
|
108
|
+
}, { onSuccess: done });
|
|
109
|
+
|
|
110
|
+
const rows = listOf<PersonaRow>(personas.data);
|
|
111
|
+
const open = (p: PersonaRow | "new") => { setForm(p === "new" ? emptyForm : formOf(p)); setEditing(p); save.reset(); };
|
|
112
|
+
const set = (k: keyof PersonaForm) => (e: { target: { value: string } }) => setForm({ ...form, [k]: e.target.value });
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<section aria-label="Personas" className={`space-y-3 ${className ?? ""}`}>
|
|
116
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
117
|
+
<h3 className="font-medium">Buyer personas</h3>
|
|
118
|
+
<div className="flex gap-2">
|
|
119
|
+
<Button size="sm" variant="outline" disabled={!profile.websiteUrl} onClick={() => setConfirmRecommend(true)}>Recommend with AI</Button>
|
|
120
|
+
<Button size="sm" disabled={!profile.websiteUrl} onClick={() => open("new")}>Add persona</Button>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
{!profile.loading && !profile.websiteUrl ? <p className="text-sm text-muted-foreground">Save the company website (IcpEditor) before adding personas.</p> : null}
|
|
124
|
+
{suggestions.length > 0 ? (
|
|
125
|
+
<section aria-label="Recommended personas" className="space-y-2 rounded-md border border-dashed p-3">
|
|
126
|
+
<p className="text-sm font-medium">AI recommendations. Nothing is saved until you accept.</p>
|
|
127
|
+
{suggestions.map((s) => (
|
|
128
|
+
<div key={s.title} className="flex items-start justify-between gap-3 text-sm">
|
|
129
|
+
<div><p className="font-medium">{s.title}</p><p className="text-muted-foreground">{s.why_target}</p></div>
|
|
130
|
+
<div className="flex shrink-0 gap-2">
|
|
131
|
+
<Button size="sm" aria-label={`Accept ${s.title}`} loading={accepting === s.title} onClick={() => void accept.mutate(s)}>Accept</Button>
|
|
132
|
+
<Button size="sm" variant="outline" aria-label={`Dismiss ${s.title}`} onClick={() => setSuggestions((all) => all.filter((x) => x.title !== s.title))}>Dismiss</Button>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
))}
|
|
136
|
+
</section>
|
|
137
|
+
) : null}
|
|
138
|
+
{recommend.data && suggestions.length === 0 && !recommend.error ? <p role="status" className="text-sm text-muted-foreground">No recommendations left to review.</p> : null}
|
|
139
|
+
{[personas.error, archive.error, recommend.error, accept.error].map((e, i) => (e ? <ApiError key={i} error={e} onRetry={i === 0 ? () => void personas.refetch() : undefined} /> : null))}
|
|
140
|
+
<Table>
|
|
141
|
+
<TableHeader><TableRow><TableHead>Title</TableHead><TableHead>Why target</TableHead><TableHead>Confidence</TableHead><TableHead>Status</TableHead><TableHead className="w-40" /></TableRow></TableHeader>
|
|
142
|
+
<TableBody>
|
|
143
|
+
{personas.loading && !personas.data ? <TableRow><TableCell colSpan={5}><Skeleton className="h-6 w-full" /></TableCell></TableRow> : null}
|
|
144
|
+
{rows.map((p) => (
|
|
145
|
+
<TableRow key={p.id}>
|
|
146
|
+
<TableCell className="font-medium">{p.title}</TableCell>
|
|
147
|
+
<TableCell className="max-w-md truncate text-muted-foreground">{p.why_target}</TableCell>
|
|
148
|
+
<TableCell>{p.confidence}</TableCell>
|
|
149
|
+
<TableCell><span className={`rounded px-2 py-0.5 text-xs ${statusClass(p.status)}`}>{p.status ?? "active"}</span></TableCell>
|
|
150
|
+
<TableCell className="space-x-2 text-right">
|
|
151
|
+
<Button size="sm" variant="outline" aria-label={`Edit ${p.title}`} onClick={() => open(p)}>Edit</Button>
|
|
152
|
+
<Button size="sm" variant="outline" aria-label={`Archive ${p.title}`} loading={busyId === p.id} onClick={() => setArchiving(p)}>Archive</Button>
|
|
153
|
+
</TableCell>
|
|
154
|
+
</TableRow>
|
|
155
|
+
))}
|
|
156
|
+
</TableBody>
|
|
157
|
+
</Table>
|
|
158
|
+
{!personas.loading && !personas.error && rows.length === 0 ? <EmptyState title="No personas yet" description="Add the roles you sell to, or let AI recommend them." /> : null}
|
|
159
|
+
|
|
160
|
+
<Dialog open={editing !== null} onOpenChange={(o) => { if (!o) setEditing(null); }}>
|
|
161
|
+
<DialogContent>
|
|
162
|
+
<DialogHeader><DialogTitle>{editing === "new" ? "Add persona" : "Edit persona"}</DialogTitle></DialogHeader>
|
|
163
|
+
<div className="space-y-3">
|
|
164
|
+
<FormField label="Title" htmlFor="persona-title"><Input id="persona-title" value={form.title} placeholder="Director of Marketing Operations" onChange={set("title")} /></FormField>
|
|
165
|
+
<FormField label="Why target" htmlFor="persona-why"><Textarea id="persona-why" value={form.why} onChange={set("why")} /></FormField>
|
|
166
|
+
<FormField label="Key signals" htmlFor="persona-signals" hint="One per line."><Textarea id="persona-signals" value={form.signals} onChange={set("signals")} /></FormField>
|
|
167
|
+
<FormField label="Campaign approach" htmlFor="persona-approach"><Textarea id="persona-approach" value={form.approach} onChange={set("approach")} /></FormField>
|
|
168
|
+
<FormField label="Recommended goal" htmlFor="persona-goal"><Input id="persona-goal" value={form.goal} onChange={set("goal")} /></FormField>
|
|
169
|
+
<div className="grid grid-cols-2 gap-3">
|
|
170
|
+
<FormField label="Priority" htmlFor="persona-priority"><Input id="persona-priority" type="number" min={1} value={form.priority} onChange={set("priority")} /></FormField>
|
|
171
|
+
<FormField label="Confidence" htmlFor="persona-confidence">
|
|
172
|
+
<select id="persona-confidence" className="h-9 rounded-md border bg-background px-2" value={form.confidence} onChange={set("confidence")}>
|
|
173
|
+
{["low", "medium", "high"].map((c) => <option key={c} value={c}>{c}</option>)}
|
|
174
|
+
</select>
|
|
175
|
+
</FormField>
|
|
176
|
+
</div>
|
|
177
|
+
{save.error ? <ApiError error={save.error} title="Could not save the persona" /> : null}
|
|
178
|
+
</div>
|
|
179
|
+
<DialogFooter>
|
|
180
|
+
<Button variant="outline" onClick={() => setEditing(null)}>Cancel</Button>
|
|
181
|
+
<Button loading={save.loading} disabled={!form.title.trim()} onClick={() => void save.mutate(undefined)}>Save persona</Button>
|
|
182
|
+
</DialogFooter>
|
|
183
|
+
</DialogContent>
|
|
184
|
+
</Dialog>
|
|
185
|
+
|
|
186
|
+
<ConfirmSpendDialog
|
|
187
|
+
open={confirmRecommend}
|
|
188
|
+
tier="billable"
|
|
189
|
+
title="Recommend personas with AI"
|
|
190
|
+
description="Reads your company profile and suggests personas. Nothing is saved until you accept each one. Estimate only."
|
|
191
|
+
estimatedCredits={CREDIT_ESTIMATES.personaRecommend}
|
|
192
|
+
confirmLabel="Recommend personas"
|
|
193
|
+
confirming={recommend.loading}
|
|
194
|
+
onConfirm={() => void recommend.mutate(undefined)}
|
|
195
|
+
onCancel={() => setConfirmRecommend(false)}
|
|
196
|
+
/>
|
|
197
|
+
<ConfirmSpendDialog
|
|
198
|
+
open={archiving !== null}
|
|
199
|
+
tier="destructive"
|
|
200
|
+
title={`Archive ${archiving?.title ?? "this persona"}`}
|
|
201
|
+
description="It drops out of the default list. Existing campaigns keep the persona text."
|
|
202
|
+
affected={1}
|
|
203
|
+
confirmLabel="Archive persona"
|
|
204
|
+
confirming={archive.loading}
|
|
205
|
+
onConfirm={() => archiving && void archive.mutate(archiving)}
|
|
206
|
+
onCancel={() => setArchiving(null)}
|
|
207
|
+
/>
|
|
208
|
+
</section>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useCompanyProfile: the workspace's company profile and its website, which every ICP, persona and campaign
|
|
5
|
+
* generation call needs.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations:
|
|
8
|
+
* 1. getCompanyProfile GET /company-profile (read; `data.profile` is null until one is saved)
|
|
9
|
+
*
|
|
10
|
+
* Tiers: read only.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
14
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
15
|
+
import { objectOf } from "../lib/campaigns-utils";
|
|
16
|
+
|
|
17
|
+
export interface CompanyProfile {
|
|
18
|
+
website_url?: string;
|
|
19
|
+
fields?: Record<string, unknown> | null;
|
|
20
|
+
approved?: boolean;
|
|
21
|
+
first_run_completed_at?: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useCompanyProfile(websiteOverride?: string) {
|
|
25
|
+
const query = useG8Query((signal) => g8Get("/company-profile", undefined, { signal }), []);
|
|
26
|
+
const profile = (objectOf(query.data).profile ?? null) as CompanyProfile | null;
|
|
27
|
+
return {
|
|
28
|
+
profile,
|
|
29
|
+
websiteUrl: websiteOverride || profile?.website_url || "",
|
|
30
|
+
loading: query.loading,
|
|
31
|
+
error: query.error,
|
|
32
|
+
refetch: query.refetch,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/** Types and helpers shared by the campaign components. Payloads are open-shaped: every field is optional. */
|
|
4
|
+
|
|
5
|
+
/** Where the person manages mailboxes. Google and Microsoft OAuth consent can only run in the graph8 app. */
|
|
6
|
+
export const GRAPH8_APP_MAILBOXES_URL = "https://app.graph8.com/studio/settings?tab=purchase&view=mailboxes";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Rough credit estimates shown in ConfirmSpendDialog. The API has no price read and no balance read, so these
|
|
10
|
+
* are placeholders: tune them to your plan. A 402 from the call means the workspace is out of credits.
|
|
11
|
+
*/
|
|
12
|
+
export const CREDIT_ESTIMATES = {
|
|
13
|
+
profileGenerate: 50,
|
|
14
|
+
personaRecommend: 25,
|
|
15
|
+
perIdea: 10,
|
|
16
|
+
documentSet: 100,
|
|
17
|
+
stepContent: 5,
|
|
18
|
+
documentRegenerate: 10,
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
/** The proxy refuses request bodies over 5 MiB (413). Check before sending. */
|
|
22
|
+
export const MAX_BODY_BYTES = 5 * 1024 * 1024;
|
|
23
|
+
|
|
24
|
+
export interface CampaignRow {
|
|
25
|
+
id: string;
|
|
26
|
+
name?: string | null;
|
|
27
|
+
slug?: string | null;
|
|
28
|
+
status?: string | null;
|
|
29
|
+
category?: string | null;
|
|
30
|
+
goal?: string | null;
|
|
31
|
+
brief?: string | null;
|
|
32
|
+
core_concept?: string | null;
|
|
33
|
+
primary_hook?: string | null;
|
|
34
|
+
target_persona?: string | null;
|
|
35
|
+
audience_list_id?: string | number | null;
|
|
36
|
+
created_at?: string | null;
|
|
37
|
+
updated_at?: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface CampaignDocRow {
|
|
41
|
+
id: string;
|
|
42
|
+
display_name?: string | null;
|
|
43
|
+
file_type?: string | null;
|
|
44
|
+
folder_path?: string | null;
|
|
45
|
+
status?: string | null;
|
|
46
|
+
content?: string | null;
|
|
47
|
+
updated_at?: string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SequenceStep {
|
|
51
|
+
step_id: string;
|
|
52
|
+
name?: string | null;
|
|
53
|
+
channel?: string | null;
|
|
54
|
+
mode?: string | null;
|
|
55
|
+
day?: number | null;
|
|
56
|
+
stop_on_reply?: boolean | null;
|
|
57
|
+
status?: string | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The channel -> mode matrix the step API enforces (docs/developer-api-reference.md, GTM Campaigns). */
|
|
61
|
+
export const CHANNEL_MODES: Record<string, string[]> = {
|
|
62
|
+
email: ["email"],
|
|
63
|
+
phone: ["call", "voicemail"],
|
|
64
|
+
sms: ["sms"],
|
|
65
|
+
social: ["connect_note", "dm", "comment", "reply"],
|
|
66
|
+
voice_ai: ["call"],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export function listOf<T>(envelope: unknown): T[] {
|
|
70
|
+
const data = (envelope as { data?: unknown } | null)?.data;
|
|
71
|
+
if (Array.isArray(data)) return data as T[];
|
|
72
|
+
const named = data as { items?: unknown; documents?: unknown; personas?: unknown } | null;
|
|
73
|
+
for (const rows of [named?.items, named?.documents, named?.personas]) if (Array.isArray(rows)) return rows as T[];
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function objectOf(envelope: unknown): Record<string, unknown> {
|
|
78
|
+
const data = (envelope as { data?: unknown } | null)?.data;
|
|
79
|
+
return data && typeof data === "object" && !Array.isArray(data) ? (data as Record<string, unknown>) : {};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function text(v: unknown): string {
|
|
83
|
+
return typeof v === "string" ? v : typeof v === "number" ? String(v) : "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function lines(value: string): string[] {
|
|
87
|
+
return value.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function commaList(value: string): string[] {
|
|
91
|
+
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function statusClass(status: string | null | undefined): string {
|
|
95
|
+
const s = (status ?? "").toLowerCase();
|
|
96
|
+
if (["active", "live", "completed", "approved", "complete", "done"].includes(s)) return "bg-green-100 text-green-800";
|
|
97
|
+
if (["draft", "pending", "running", "generating", "in_progress", "copy_in_progress", "scheduling"].includes(s)) return "bg-amber-100 text-amber-800";
|
|
98
|
+
if (s) return "bg-red-100 text-red-800";
|
|
99
|
+
return "bg-gray-100 text-gray-700";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Timestamps are shown in UTC so the same value reads the same everywhere. */
|
|
103
|
+
export function formatDate(value: string | null | undefined): string {
|
|
104
|
+
if (!value) return "";
|
|
105
|
+
const d = new Date(value);
|
|
106
|
+
if (Number.isNaN(d.getTime())) return value;
|
|
107
|
+
return d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", timeZone: "UTC" });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Scalar fields of an open-shaped object, for a summary grid. */
|
|
111
|
+
export function scalars(obj: Record<string, unknown>): Array<[string, string]> {
|
|
112
|
+
return Object.entries(obj)
|
|
113
|
+
.filter(([, v]) => ["string", "number", "boolean"].includes(typeof v))
|
|
114
|
+
.map(([k, v]) => [k.replace(/_/g, " "), typeof v === "number" && !Number.isInteger(v) ? v.toFixed(2) : String(v)]);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const RUNNING = new Set(["pending", "running", "in_progress", "processing", "started", "generating", "queued", "copy_in_progress"]);
|
|
118
|
+
|
|
119
|
+
/** True when no string under a `status` or `state` key (at any depth) is still in progress. */
|
|
120
|
+
export function settled(value: unknown): boolean {
|
|
121
|
+
if (Array.isArray(value)) return value.every(settled);
|
|
122
|
+
if (!value || typeof value !== "object") return true;
|
|
123
|
+
return Object.entries(value as Record<string, unknown>).every(([k, v]) => {
|
|
124
|
+
if (typeof v === "string" && /status|state/i.test(k)) return !RUNNING.has(v.toLowerCase());
|
|
125
|
+
return settled(v);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Steps from a GET /campaigns/{id}/sequence (or /full) payload. The ORDER lives in `sequence.steps`
|
|
131
|
+
* ({ step_id, day, condition, stop_on_reply }); the name, channel and mode live in `step_catalog.steps`,
|
|
132
|
+
* keyed by step_id. This joins the two.
|
|
133
|
+
*/
|
|
134
|
+
export function stepsOf(envelope: unknown): SequenceStep[] {
|
|
135
|
+
const data = objectOf(envelope);
|
|
136
|
+
const order = (data.sequence as { steps?: unknown } | null)?.steps;
|
|
137
|
+
const catalog = ((data.step_catalog as { steps?: unknown } | null)?.steps ?? {}) as Record<string, Partial<SequenceStep>>;
|
|
138
|
+
if (!Array.isArray(order)) return [];
|
|
139
|
+
return order
|
|
140
|
+
.filter((s): s is SequenceStep => !!s && typeof s === "object" && typeof (s as SequenceStep).step_id === "string")
|
|
141
|
+
.map((s) => ({ ...catalog[s.step_id], ...s }));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Contacts on the attached list from a /full payload (`audience.item_count`, also read as `total`). */
|
|
145
|
+
export function audienceOf(envelope: unknown): { listId: string | null; name: string; size: number | undefined } {
|
|
146
|
+
const d = objectOf(envelope);
|
|
147
|
+
const a = (d.audience ?? {}) as Record<string, unknown>;
|
|
148
|
+
const id = d.audience_list_id;
|
|
149
|
+
const size = typeof a.item_count === "number" ? a.item_count : typeof a.total === "number" ? a.total : undefined;
|
|
150
|
+
return { listId: id == null || id === "" ? null : String(id), name: text(a.name) || text(a.title), size };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function byteLength(s: string): number {
|
|
154
|
+
return typeof Blob !== "undefined" ? new Blob([s]).size : s.length;
|
|
155
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Attach an audience, check readiness, and launch
|
|
3
|
+
* @when The campaign has steps and copy and a human has approved reaching the list. `launchCampaign` is tier
|
|
4
|
+
* `external`: it BEGINS REAL OUTREACH and cannot be recalled, so show the recipient count and get approval
|
|
5
|
+
* first. Readiness needs a non-empty audience, completed copy, at least one complete step and an ACTIVE
|
|
6
|
+
* email mailbox (Google and Microsoft mailboxes are connected by the person in the graph8 app). An empty body
|
|
7
|
+
* auto-picks the first active mailbox; pass `sender_mailbox_ids` to choose. Read the state with
|
|
8
|
+
* `getCampaignFull` (plain `getCampaign` omits the audience): `is_launched` means do not launch again. If
|
|
9
|
+
* launch fails with 502 `draft_created_activation_failed` or `activation_state_uncertain`, a sequence may
|
|
10
|
+
* exist: check it before retrying, never repeat blindly.
|
|
11
|
+
*/
|
|
12
|
+
import { g8 } from "@graph8/sdk";
|
|
13
|
+
|
|
14
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
15
|
+
|
|
16
|
+
export async function attachAndLaunch(campaignId: string, listId: number, senderMailboxIds?: number[]) {
|
|
17
|
+
await g8.api.gtmCampaigns.attachAudienceToCampaign({ path: { campaign_id: campaignId }, body: { list_id: String(listId) } });
|
|
18
|
+
|
|
19
|
+
const verdict = await g8.api.deliverability.getSafeToSend();
|
|
20
|
+
const safe = ((verdict.data ?? {}) as { safe_to_send?: boolean }).safe_to_send;
|
|
21
|
+
if (safe === false) return { launched: false as const, reason: "deliverability says do not send" };
|
|
22
|
+
|
|
23
|
+
const full = await g8.api.gtmCampaigns.getCampaignFull({ path: { campaign_id: campaignId } });
|
|
24
|
+
const state = (full.data ?? {}) as { is_launched?: boolean; audience?: { item_count?: number }; sequence?: { steps?: unknown[] } };
|
|
25
|
+
if (state.is_launched) return { launched: false as const, reason: "already launched" };
|
|
26
|
+
if (!state.sequence?.steps?.length) return { launched: false as const, reason: "the campaign has no steps" };
|
|
27
|
+
|
|
28
|
+
const launched = await g8.api.gtmCampaigns.launchCampaign({
|
|
29
|
+
path: { campaign_id: campaignId },
|
|
30
|
+
body: senderMailboxIds ? { sender_mailbox_ids: senderMailboxIds } : {},
|
|
31
|
+
});
|
|
32
|
+
return { launched: true as const, recipients: state.audience?.item_count, result: launched.data };
|
|
33
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Report on every campaign
|
|
3
|
+
* @when A daily or weekly summary. Reads only (no spend). `listCampaigns` is page-numbered and returns a bare array; metrics take a
|
|
4
|
+
* window in days (1 to 365). The metrics payload is open-shaped, so this returns it as is.
|
|
5
|
+
*/
|
|
6
|
+
import { g8 } from "@graph8/sdk";
|
|
7
|
+
|
|
8
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
9
|
+
|
|
10
|
+
export async function campaignReport(days = 30) {
|
|
11
|
+
const report: Array<{ id: string; name: string; status: string; metrics: unknown }> = [];
|
|
12
|
+
for (let page = 1; ; page++) {
|
|
13
|
+
const res = await g8.api.gtmCampaigns.listCampaigns({ query: { page, limit: 50 } });
|
|
14
|
+
const rows = (res.data ?? []) as Array<{ id: string; name: string; status: string }>;
|
|
15
|
+
for (const c of rows) {
|
|
16
|
+
const metrics = await g8.api.gtmCampaigns.getCampaignMetrics({ path: { campaign_id: c.id }, query: { days } });
|
|
17
|
+
report.push({ id: c.id, name: c.name, status: c.status, metrics: metrics.data });
|
|
18
|
+
}
|
|
19
|
+
if (rows.length < 50) break; // a bare array with no pagination block: a short page is the last
|
|
20
|
+
}
|
|
21
|
+
return report;
|
|
22
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Generate campaign ideas, then create the campaign from one
|
|
3
|
+
* @when Starting a campaign from the company profile. `generateCampaignIdeasAsync` is BILLABLE (it scales with
|
|
4
|
+
* `num_ideas`): a human approves the spend first. It returns a `task_id`; there are no streams, so poll
|
|
5
|
+
* `getCampaignIdeasTaskStatus` every 2 to 3 seconds until `completed` or `failed`. `createCampaign` is tier
|
|
6
|
+
* `external` in the contract but contacts nobody; with `auto_generate_documents: true` it also spends credits
|
|
7
|
+
* writing the documents. Store the returned campaign id at once: a retry after a later failure must reuse it.
|
|
8
|
+
*/
|
|
9
|
+
import { g8 } from "@graph8/sdk";
|
|
10
|
+
|
|
11
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
12
|
+
|
|
13
|
+
interface Idea {
|
|
14
|
+
campaign_name?: string;
|
|
15
|
+
campaign_category?: string;
|
|
16
|
+
core_concept?: string;
|
|
17
|
+
primary_hook?: string;
|
|
18
|
+
why_this_works?: string;
|
|
19
|
+
target_channels?: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function generateIdeas(websiteUrl: string, goal: "awareness" | "consideration" | "decision", numIdeas = 5) {
|
|
23
|
+
const started = await g8.api.campaigns.generateCampaignIdeasAsync({
|
|
24
|
+
body: { organization_id: "current", website_url: websiteUrl, campaign_goal: goal, num_ideas: numIdeas },
|
|
25
|
+
});
|
|
26
|
+
const taskId = String(started.data?.task_id ?? "");
|
|
27
|
+
for (let attempt = 0; attempt < 60; attempt++) {
|
|
28
|
+
const task = await g8.api.campaigns.getCampaignIdeasTaskStatus({ path: { task_id: taskId } });
|
|
29
|
+
const data = (task.data ?? {}) as { status?: string; ideas?: Idea[]; error?: string };
|
|
30
|
+
if (data.status === "completed") return data.ideas ?? [];
|
|
31
|
+
if (data.status === "failed") throw new Error(data.error ?? "idea generation failed");
|
|
32
|
+
await new Promise((r) => setTimeout(r, 2500));
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`idea task ${taskId} did not finish in time`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function createCampaignFromIdea(idea: Idea, listId?: number) {
|
|
38
|
+
const created = await g8.api.gtmCampaigns.createCampaign({
|
|
39
|
+
body: {
|
|
40
|
+
name: idea.campaign_name ?? "New campaign",
|
|
41
|
+
category: idea.campaign_category ?? "Outbound",
|
|
42
|
+
core_concept: idea.core_concept,
|
|
43
|
+
primary_hook: idea.primary_hook,
|
|
44
|
+
brief: idea.why_this_works,
|
|
45
|
+
target_channels: ["email"],
|
|
46
|
+
audience_list_id: listId == null ? undefined : String(listId),
|
|
47
|
+
auto_generate_documents: false,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
return String(created.data?.id);
|
|
51
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Set the company profile, then create an ICP and personas
|
|
3
|
+
* @when Before any campaign work. Every idea, persona and campaign is generated from the company profile, so
|
|
4
|
+
* a wrong website here makes everything downstream describe the wrong company. `updateCompanyProfile` replaces
|
|
5
|
+
* the stored field set (send all of it) and refuses an APPROVED profile with 409 unless you pass
|
|
6
|
+
* `unapprove_if_approved: true`, which a human should decide. Creating ICPs and personas by hand is free;
|
|
7
|
+
* `recommendCampaignPersonas` and `generateCompanyProfile` spend credits and need approval first.
|
|
8
|
+
*/
|
|
9
|
+
import { g8 } from "@graph8/sdk";
|
|
10
|
+
|
|
11
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
12
|
+
|
|
13
|
+
export async function setUpProfile(websiteUrl: string, fields: Record<string, string>) {
|
|
14
|
+
await g8.api.gtmContext.updateCompanyProfile({ body: { website_url: websiteUrl, fields } });
|
|
15
|
+
|
|
16
|
+
const icp = await g8.api.gtmContext.createIcp({
|
|
17
|
+
body: {
|
|
18
|
+
name: "Mid-market B2B SaaS",
|
|
19
|
+
website_url: websiteUrl,
|
|
20
|
+
description: "Software companies with 50 to 500 employees and a sales team.",
|
|
21
|
+
firmographics: { industries: ["Software"], company_size: "50-500", regions: ["US"] },
|
|
22
|
+
buying_signals: ["Hiring SDRs", "New VP of Sales"],
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const persona = await g8.api.gtmContext.createPersona({
|
|
27
|
+
body: {
|
|
28
|
+
title: "VP of Sales",
|
|
29
|
+
website_url: websiteUrl,
|
|
30
|
+
why_target: "Owns pipeline targets and the outbound budget.",
|
|
31
|
+
key_signals: ["Missed quota", "New sales tooling"],
|
|
32
|
+
campaign_approach: "Lead with the time saved on prospecting.",
|
|
33
|
+
recommended_goal: "consideration",
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
return { icpId: icp.data?.id, personaId: persona.data?.id };
|
|
37
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# coach: sales coach scorecards and a coaching feed
|
|
2
|
+
|
|
3
|
+
Show reps how their practice calls went and what to work on next, and give managers a leaderboard. Builds on
|
|
4
|
+
the `dialer` block (the optional "Real calls" tab reuses its `CallLog`).
|
|
5
|
+
|
|
6
|
+
Install: `npx @graph8/sdk add coach` (installs `foundation`, `lists`, `crm` and `dialer` first).
|
|
7
|
+
|
|
8
|
+
## What you get
|
|
9
|
+
|
|
10
|
+
| Component | Use it for | Embed props |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| `CallScorecard` | One graded call: overall score, stages with checklist verdicts, strengths, improvements, recording, transcript | `callId`, `allowDelete`, `onDeleted` |
|
|
13
|
+
| `CoachingFeed` | Practice calls newest first with the first tip, a leaderboard tab, optional real-calls tab | `userEmail`, `onOpenCall`, `allowCreate`, `showDialerCalls`, `allowDelete`, `pageSize` |
|
|
14
|
+
|
|
15
|
+
## The flow, with the ids that pass between steps
|
|
16
|
+
|
|
17
|
+
1. **Feed**: `GET /sales-coach/calls?email=` returns the calls (newest first) with `scores` and transcript
|
|
18
|
+
segments inline. Without `email` it is the key owner's own; another rep's calls need a manager key (403
|
|
19
|
+
otherwise). Each call's `id` is the `call_id` for everything below.
|
|
20
|
+
2. **Score**: `scores[0].evaluation_data` holds `stages[].criteria[].verdict` (met / partial / missed; a stage
|
|
21
|
+
is the share of judged lines, met = 1, partial = 0.5) or, on older calls, `evaluatorResult` categories
|
|
22
|
+
out of 10. `scorecardOf()` reads both. `evaluatorResult.areas_of_improvement[0]` is the tip the feed shows.
|
|
23
|
+
3. **Scorecard**: `GET /sales-coach/calls/{call_id}`, `.../transcript` (segments with `in_call_timestamp` in
|
|
24
|
+
ms), and on demand `.../audio-url` (a short-lived signed URL; the card fetches it only on "Load recording").
|
|
25
|
+
4. **Leaderboard**: `GET /sales-coach/leaderboard` ranks completed calls per rep (`score` total, `calls`,
|
|
26
|
+
`avgScore`).
|
|
27
|
+
5. **Create** (optional): `POST /sales-coach/calls { call_mode: "assisted" | "compete", prospect_name, ... }`
|
|
28
|
+
creates only the call row. The live practice conversation runs in the graph8 app.
|
|
29
|
+
6. **Delete**: `DELETE /sales-coach/calls/{call_id}` removes the call, its transcript and its scores.
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
"use client";
|
|
33
|
+
import { CoachingFeed } from "@/components/g8/coach/components/coaching-feed";
|
|
34
|
+
|
|
35
|
+
export function MyCoaching() {
|
|
36
|
+
return <CoachingFeed userEmail={me.email} showDialerCalls allowDelete={false} />;
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
(`add` installs under `components/g8/coach/`, or `src/components/g8/coach/`; adjust the alias.)
|
|
41
|
+
|
|
42
|
+
## Tiers and confirmations
|
|
43
|
+
|
|
44
|
+
- Everything read is free. Creating a call row is a write and runs directly.
|
|
45
|
+
- Deleting a call is `destructive` and goes through `ConfirmSpendDialog`; it also drops the score from the
|
|
46
|
+
leaderboard. Turn it off for reps with `allowDelete={false}`.
|
|
47
|
+
|
|
48
|
+
## Prerequisites
|
|
49
|
+
|
|
50
|
+
- `dialer-installed` (app): the CLI installs it as a dependency.
|
|
51
|
+
- `manager-key` (api): reading other reps' calls needs an org admin or sales manager's key.
|
|
52
|
+
- `practice-in-app` (app): practice calls are run in the graph8 app; this block shows the results.
|
|
53
|
+
- Proxy `allowedPrefixes` include `sales-coach` (and `voice` for the Real calls tab). Scopes: `team:read`,
|
|
54
|
+
`team:write`, `team:delete`, `voice:read`.
|
|
55
|
+
|
|
56
|
+
## Defaults the agent can use without asking
|
|
57
|
+
|
|
58
|
+
| Decision | Default |
|
|
59
|
+
|---|---|
|
|
60
|
+
| Whose calls the feed shows | the key owner's own |
|
|
61
|
+
| Delete / create form | delete on, create form off |
|
|
62
|
+
|
|
63
|
+
Ask the person: who may read other reps' calls (graph8 enforces managers-only; decide who gets a manager key).
|
|
64
|
+
|
|
65
|
+
## Known limits
|
|
66
|
+
|
|
67
|
+
- The create form is hidden when `userEmail` is set (it would create a call for the key owner, not that rep). The Real calls tab needs `userEmail` too: the dialer API will not list calls without a rep.
|
|
68
|
+
- Legacy calls store `final_score` out of 30; `scorecardOf()` turns it into a percent. Checklist calls store a percent.
|
|
69
|
+
- Delete is allowed for the call owner or a manager.
|
|
70
|
+
|
|
71
|
+
- The live practice audio session (LiveKit token) is deliberately not in the public API.
|
|
72
|
+
- Dates on a call are datetimes shown in the viewer's locale.
|
|
73
|
+
- Another rep's calls: 403 for a rep's key; the error is shown, not hidden.
|
|
74
|
+
|
|
75
|
+
## Recipes
|
|
76
|
+
|
|
77
|
+
| File | Does |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `recipes/read-a-scorecard.ts` | Latest graded call, its transcript and a signed recording URL |
|
|
80
|
+
| `recipes/weekly-coaching-digest.ts` | Per-rep weakest stage and first tip from the leaderboard and their calls |
|
|
81
|
+
|
|
82
|
+
## Operations
|
|
83
|
+
|
|
84
|
+
| Operation | Method and path | Tier | Used by |
|
|
85
|
+
|---|---|---|---|
|
|
86
|
+
| listCalls | GET /sales-coach/calls | read | CoachingFeed |
|
|
87
|
+
| createCall | POST /sales-coach/calls | write | CoachingFeed |
|
|
88
|
+
| getCall | GET /sales-coach/calls/{call_id} | read | CallScorecard |
|
|
89
|
+
| deleteCall | DELETE /sales-coach/calls/{call_id} | destructive | CallScorecard |
|
|
90
|
+
| getCallAudioUrl | GET /sales-coach/calls/{call_id}/audio-url | read | CallScorecard |
|
|
91
|
+
| getCallTranscript | GET /sales-coach/calls/{call_id}/transcript | read | CallScorecard |
|
|
92
|
+
| getSalesCoachLeaderboard | GET /sales-coach/leaderboard | read | CoachingFeed |
|
|
93
|
+
| listCallsForContact | GET /voice/dialer/calls | read | CoachingFeed |
|