@graph8/sdk 0.362.0 → 0.373.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -10
- package/blocks/_foundation/README.md +78 -1
- package/blocks/_foundation/block.json +123 -6
- package/blocks/_foundation/components/api-error.tsx +9 -7
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/confirmed-action.tsx +90 -0
- package/blocks/_foundation/components/connect-graph8.tsx +280 -0
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +2 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-brand.ts +52 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/connect-route.ts +15 -0
- package/blocks/_foundation/server/connect.ts +127 -0
- package/blocks/_foundation/server/local-key.ts +70 -0
- package/blocks/_foundation/server/proxy.ts +103 -3
- package/blocks/_foundation/server/route.ts +16 -4
- package/blocks/agents/README.md +93 -2
- package/blocks/agents/block.json +343 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +29 -10
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +119 -0
- package/blocks/agents/components/agent-runs.tsx +19 -10
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +226 -0
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +222 -0
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +38 -11
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +33 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +4 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +38 -18
- package/blocks/appointments/components/meetings-screen.tsx +7 -4
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/appointments/components/scheduling-screen.tsx +2 -1
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +17 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +17 -6
- package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +163 -0
- package/blocks/campaigns/components/icp-editor.tsx +22 -17
- package/blocks/campaigns/components/persona-detail.tsx +181 -0
- package/blocks/campaigns/components/persona-editor.tsx +19 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
- package/blocks/coach/README.md +10 -0
- package/blocks/coach/block.json +16 -1
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/coach/components/coach-screen.tsx +222 -0
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +161 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +14 -5
- package/blocks/copilot/components/copilot-screen.tsx +148 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
- package/blocks/crm/README.md +34 -0
- package/blocks/crm/block.json +107 -2
- package/blocks/crm/components/companies-screen.tsx +359 -0
- package/blocks/crm/components/company-detail.tsx +10 -8
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +10 -7
- package/blocks/crm/components/contact-lookup-link.tsx +44 -0
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +377 -0
- package/blocks/crm/components/dedupe-review.tsx +13 -6
- package/blocks/crm/components/import-wizard.tsx +7 -9
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +6 -4
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +399 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +9 -1
- package/blocks/crm/lib/crm-types.ts +26 -2
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +24 -6
- package/blocks/deals/block.json +160 -5
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +25 -13
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +31 -10
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +66 -26
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +5 -5
- package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +45 -4
- package/blocks/dialer/block.json +84 -3
- package/blocks/dialer/components/call-contact-button.tsx +269 -0
- package/blocks/dialer/components/call-log-screen.tsx +208 -0
- package/blocks/dialer/components/call-log.tsx +23 -7
- package/blocks/dialer/components/call-queue.tsx +4 -4
- package/blocks/dialer/components/dialer-screen.tsx +361 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +61 -12
- package/blocks/dialer/components/softphone.tsx +4 -2
- package/blocks/enrichment/README.md +29 -7
- package/blocks/enrichment/block.json +49 -9
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
- package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
- package/blocks/enrichment/components/enrich-button.tsx +32 -9
- package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
- package/blocks/enrichment/components/waterfall-config.tsx +66 -17
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +8 -0
- package/blocks/forms/block.json +139 -18
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +4 -2
- package/blocks/forms/components/form-embed.tsx +4 -2
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +56 -19
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +17 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +32 -3
- package/blocks/inbox/block.json +29 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
- package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
- package/blocks/inbox/lib/inbox-types.ts +47 -1
- package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
- package/blocks/integrations/README.md +63 -1
- package/blocks/integrations/block.json +234 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +119 -0
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +13 -8
- package/blocks/integrations/components/integrations-screen.tsx +152 -0
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +11 -9
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +179 -0
- package/blocks/integrations/components/webhook-manager.tsx +43 -25
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +11 -1
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +13 -10
- package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/block.json +13 -8
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +20 -1
- package/blocks/lists/block.json +69 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +20 -9
- package/blocks/lists/components/list-picker.tsx +17 -3
- package/blocks/lists/components/list-table.tsx +12 -6
- package/blocks/lists/components/lists-screen.tsx +346 -0
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +33 -8
- package/blocks/lists/components/suppressions-screen.tsx +264 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +69 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
- package/blocks/mailboxes/components/domain-detail.tsx +148 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
- package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
- package/blocks/mailboxes/components/warmup-health.tsx +24 -22
- package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +157 -24
- package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/block.json +28 -14
- package/blocks/nurture/components/nurture-builder.tsx +13 -3
- package/blocks/nurture/components/nurture-enroll.tsx +12 -10
- package/blocks/nurture/components/nurture-form.tsx +27 -10
- package/blocks/nurture/components/nurture-screen.tsx +26 -11
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +18 -1
- package/blocks/objects/block.json +42 -21
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +10 -2
- package/blocks/objects/components/object-schema-editor.tsx +4 -2
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +140 -23
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +9 -4
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-list.tsx +2 -1
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/quotes/README.md +13 -0
- package/blocks/quotes/block.json +65 -13
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +5 -3
- package/blocks/quotes/components/quote-related.tsx +8 -4
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-report.tsx +4 -2
- package/blocks/radar/components/radar-screen.tsx +214 -0
- package/blocks/radar/components/report-detail.tsx +104 -0
- package/blocks/search/README.md +10 -0
- package/blocks/search/block.json +16 -1
- package/blocks/search/components/filter-builder.tsx +3 -3
- package/blocks/search/components/prospect-results.tsx +5 -3
- package/blocks/search/components/prospect-search-screen.tsx +235 -0
- package/blocks/search/components/saved-searches.tsx +4 -1
- package/blocks/search/hooks/use-filter-options.ts +20 -6
- package/blocks/search/lib/search-types.ts +2 -0
- package/blocks/sequencer/README.md +55 -9
- package/blocks/sequencer/block.json +126 -19
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +224 -60
- package/blocks/sequencer/components/sequence-detail.tsx +261 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
- package/blocks/sequencer/components/sequence-list.tsx +34 -9
- package/blocks/sequencer/components/sequence-stats.tsx +11 -10
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +162 -0
- package/blocks/sequencer/components/step-editor.tsx +14 -12
- package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
- package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
- package/blocks/social-ads/README.md +19 -0
- package/blocks/social-ads/block.json +233 -1
- package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
- package/blocks/social-ads/components/ads-screen.tsx +152 -0
- package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
- package/blocks/social-ads/components/audience-sync.tsx +17 -9
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +138 -0
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +155 -0
- package/blocks/social-ads/components/creative-gallery.tsx +35 -7
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +7 -3
- package/blocks/social-ads/components/post-detail.tsx +163 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +187 -0
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +21 -10
- package/blocks/visitors/components/traffic-chart.tsx +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +26 -16
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +16 -7
- package/blocks/visitors/components/visitors-screen.tsx +166 -0
- package/blocks/webchat/README.md +10 -0
- package/blocks/webchat/block.json +21 -7
- package/blocks/webchat/components/chat-widget-config.tsx +8 -5
- package/blocks/webchat/components/conversation-list.tsx +8 -4
- package/blocks/webchat/components/webchat-screen.tsx +245 -0
- package/blocks/workbench/README.md +11 -0
- package/blocks/workbench/block.json +48 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +10 -4
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +202 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workflows/README.md +13 -0
- package/blocks/workflows/block.json +28 -9
- package/blocks/workflows/components/run-history.tsx +31 -9
- package/blocks/workflows/components/trigger-config.tsx +3 -1
- package/blocks/workflows/components/workflow-editor.tsx +4 -2
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +3109 -1977
- package/dist/errors.d.mts +34 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +21 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.browser.js +11 -5
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +11 -5
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +164 -25
- package/dist/index.d.ts +164 -25
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +10 -4
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +10 -4
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +142 -5
- package/dist/react.d.ts +142 -5
- package/dist/react.js +10 -4
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +10 -4
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/add.d.ts +11 -4
- package/dist/types/blocks/brand.d.ts +122 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +27 -20
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/blocks/paths.d.ts +4 -0
- package/dist/types/blocks/prefixes.d.ts +37 -0
- package/dist/types/blocks/registry.d.ts +17 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +5 -0
- package/dist/types/errors.d.ts +33 -0
- package/dist/types/http.d.ts +2 -18
- package/dist/types/version.d.ts +2 -2
- package/docs/agents.md +15 -4
- package/docs/api/README.md +2 -2
- package/docs/api/by-path.md +1 -0
- package/docs/api/desk.d.ts +98 -2
- package/docs/api/desk.md +123 -1
- package/docs/api/inbox.d.ts +10 -2
- package/docs/api/inbox.md +10 -2
- package/docs/api/seo.d.ts +19 -1
- package/docs/api/seo.md +24 -1
- package/package.json +16 -2
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +55 -17
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +147 -37
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +64 -0
- package/stacks/clay/pages/companies.tsx.tmpl +31 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +73 -27
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +98 -35
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +104 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +181 -64
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +52 -20
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +68 -34
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +106 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +117 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +85 -31
- package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +115 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +40 -9
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/app/layout.tsx.tmpl +7 -2
- package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SequenceStepsPanel: the steps of one sequence as records. A list with each step's channel, wait and
|
|
5
|
+
* progress; open one (deep link via `stepId`) for every field, how many contacts are at each state on it, and
|
|
6
|
+
* the editor (change the copy, delete it, add steps).
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. listSequenceSteps GET /sequences/{id}/steps (read; the steps in order)
|
|
10
|
+
* 2. getSequenceStats GET /sequences/{id}/stats (read; per-step contact counts)
|
|
11
|
+
* Editing opens SequenceBuilder, which owns addSequenceSteps (write), updateSequenceStep (external:
|
|
12
|
+
* confirmed) and deleteSequenceStep (destructive: confirmed).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useState } from "react";
|
|
16
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
17
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
18
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
19
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
20
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
21
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
22
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
23
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
24
|
+
import { fieldItems, fromApiStep, KIND_LABEL, listOf, objectOf, stepSummary } from "../lib/sequencer-utils";
|
|
25
|
+
import type { ApiStep } from "../lib/sequencer-utils";
|
|
26
|
+
import { SequenceBuilder } from "./sequence-builder";
|
|
27
|
+
|
|
28
|
+
export interface SequenceStepsPanelProps {
|
|
29
|
+
sequenceId: string;
|
|
30
|
+
/** The open step (the API step id). Controlled so a screen can deep-link it. */
|
|
31
|
+
stepId?: string;
|
|
32
|
+
onStepChange?: (stepId: string | null) => void;
|
|
33
|
+
/** Called after any edit so the sequence header can refresh. */
|
|
34
|
+
onChanged?: () => void;
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface StepStat { step_id?: string; step_order: number; total_contacts?: number; counts?: Record<string, number> }
|
|
39
|
+
|
|
40
|
+
export function SequenceStepsPanel({ sequenceId, stepId, onStepChange, onChanged, className }: SequenceStepsPanelProps) {
|
|
41
|
+
const [editing, setEditing] = useState(false);
|
|
42
|
+
const [version, setVersion] = useState(0);
|
|
43
|
+
const steps = useG8Query((signal) => g8Get(`/sequences/${sequenceId}/steps`, undefined, { signal }), [sequenceId, version], { key: ["sequences", "steps", sequenceId] });
|
|
44
|
+
const stats = useG8Query((signal) => g8Get(`/sequences/${sequenceId}/stats`, undefined, { signal }), [sequenceId, version], { key: ["sequences", "stats", sequenceId] });
|
|
45
|
+
const rows = listOf<ApiStep>(steps.data).sort((a, b) => a.step_order - b.step_order);
|
|
46
|
+
const statRows = (objectOf(stats.data).step_stats as StepStat[] | undefined) ?? [];
|
|
47
|
+
const statFor = (s: ApiStep) => statRows.find((x) => (x.step_id && x.step_id === s.id) || x.step_order === s.step_order);
|
|
48
|
+
const open = rows.find((s) => s.id === stepId);
|
|
49
|
+
const refresh = () => { setVersion((v) => v + 1); onChanged?.(); };
|
|
50
|
+
|
|
51
|
+
if (steps.error) return <ApiError error={steps.error} title="Could not load the steps" onRetry={() => void steps.refetch()} className={className} />;
|
|
52
|
+
if (steps.loading && !steps.data) return <Skeleton className={`h-40 w-full ${className ?? ""}`} />;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className={`space-y-4 ${className ?? ""}`}>
|
|
56
|
+
<div className="flex items-center justify-between">
|
|
57
|
+
<h3 className="font-medium">Steps</h3>
|
|
58
|
+
<Button size="sm" variant="outline" onClick={() => setEditing((e) => !e)}>{editing ? "Done editing" : "Edit or add steps"}</Button>
|
|
59
|
+
</div>
|
|
60
|
+
{editing ? (
|
|
61
|
+
<SequenceBuilder sequenceId={sequenceId} onSaved={refresh} />
|
|
62
|
+
) : rows.length === 0 ? (
|
|
63
|
+
<EmptyState title="No steps yet" description="Add the first email, LinkedIn touch or call." action={<Button onClick={() => setEditing(true)}>Add a step</Button>} />
|
|
64
|
+
) : (
|
|
65
|
+
<ol aria-label="Steps" className="divide-y rounded-md border">
|
|
66
|
+
{rows.map((s) => {
|
|
67
|
+
const draft = fromApiStep(s);
|
|
68
|
+
const st = statFor(s);
|
|
69
|
+
return (
|
|
70
|
+
<li key={s.id ?? s.step_order} className={`flex items-center gap-3 px-3 py-2 text-sm ${s.id && s.id === stepId ? "bg-muted" : ""}`}>
|
|
71
|
+
<span className="w-6 text-muted-foreground tabular-nums">{s.step_order}</span>
|
|
72
|
+
<StatusPill tone="neutral">{KIND_LABEL[draft.kind]}</StatusPill>
|
|
73
|
+
<button type="button" className="min-w-0 flex-1 truncate text-left font-medium hover:underline" aria-label={`Open step ${s.step_order}`} onClick={() => onStepChange?.(s.id ?? null)}>
|
|
74
|
+
{stepSummary(draft)}
|
|
75
|
+
</button>
|
|
76
|
+
<span className="text-xs text-muted-foreground">after {draft.wait_days} d</span>
|
|
77
|
+
{st?.total_contacts != null ? <span className="text-xs tabular-nums">{st.total_contacts} contacts</span> : null}
|
|
78
|
+
</li>
|
|
79
|
+
);
|
|
80
|
+
})}
|
|
81
|
+
</ol>
|
|
82
|
+
)}
|
|
83
|
+
{!editing && open ? (
|
|
84
|
+
<SectionCard
|
|
85
|
+
title={`Step ${open.step_order}: ${stepSummary(fromApiStep(open))}`}
|
|
86
|
+
actions={<Button size="sm" variant="ghost" onClick={() => onStepChange?.(null)}>Close step</Button>}
|
|
87
|
+
>
|
|
88
|
+
<KeyValueList
|
|
89
|
+
columns={2}
|
|
90
|
+
items={[
|
|
91
|
+
...fieldItems({ step_type: open.step_type, input_type: open.input_type, wait_days: fromApiStep(open).wait_days }),
|
|
92
|
+
...fieldItems((open.step_data ?? {}) as Record<string, unknown>),
|
|
93
|
+
]}
|
|
94
|
+
/>
|
|
95
|
+
{statFor(open)?.counts ? (
|
|
96
|
+
<KeyValueList columns={2} items={Object.entries(statFor(open)!.counts!).map(([k, v]) => ({ label: `Contacts ${k.replace(/_/g, " ")}`, value: String(v) }))} />
|
|
97
|
+
) : null}
|
|
98
|
+
<Button size="sm" onClick={() => setEditing(true)}>Edit this sequence's steps</Button>
|
|
99
|
+
</SectionCard>
|
|
100
|
+
) : null}
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SequencesScreen: the finished "Sequences" page (the Apollo / Outreach sequencer).
|
|
5
|
+
*
|
|
6
|
+
* Composes: SequenceList (status, launch, pause, resume, duplicate, archive), SequenceBuilder (create a draft,
|
|
7
|
+
* edit steps), SequenceStats (totals, per-step progress, per-contact pause/resume), EnrollDialog, plus the
|
|
8
|
+
* lists block ListPicker and the crm block ContactTable to choose who to enroll.
|
|
9
|
+
*
|
|
10
|
+
* Every record has a complete detail, beside the list or as a full page (`?view=full`): the sequence
|
|
11
|
+
* (SequenceDetail), each step (`?step=`) and each enrollment (`?enrollment=`).
|
|
12
|
+
*
|
|
13
|
+
* User flows:
|
|
14
|
+
* - Browse sequences, filter by status, see live/paused/draft counts at a glance.
|
|
15
|
+
* - "New sequence" opens a sheet with the builder (sender, audience list, email/LinkedIn/call steps); the new
|
|
16
|
+
* draft opens right away. Nothing is sent until it is launched.
|
|
17
|
+
* - Launch (after a preview that checks each step's copy and sender; a launch that would 400 is refused with the
|
|
18
|
+
* reasons), pause, resume, duplicate, archive from the list (all confirmed).
|
|
19
|
+
* - Open a sequence (deep link `?id=`): Results (stats, contacts, pause/resume one), Steps (edit copy), and
|
|
20
|
+
* "Enroll contacts": pick a list, tick contacts, confirm the recipient count.
|
|
21
|
+
*
|
|
22
|
+
* SDK operations of its own (both read):
|
|
23
|
+
* 1. listSequences GET /sequences?limit=100 (counts for the stat strip)
|
|
24
|
+
* 2. getSequence GET /sequences/{id} (header of the open sequence; also gives associated_list_id)
|
|
25
|
+
* Everything that reaches real people is confirmed in the composed parts.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { useState } from "react";
|
|
29
|
+
import type { ReactNode } from "react";
|
|
30
|
+
import { ListOrdered, Plus } from "lucide-react";
|
|
31
|
+
import { Button } from "@graph8/sdk/ui";
|
|
32
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
33
|
+
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
34
|
+
import { SplitView } from "@/g8/components/screen/split-view";
|
|
35
|
+
import type { StatItem } from "@/g8/components/screen/stat-strip";
|
|
36
|
+
import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
37
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
38
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
39
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
40
|
+
import { listOf, statusGroup } from "../lib/sequencer-utils";
|
|
41
|
+
import type { SequenceRow } from "../lib/sequencer-utils";
|
|
42
|
+
import { SequenceBuilder } from "./sequence-builder";
|
|
43
|
+
import { SequenceDetail } from "./sequence-detail";
|
|
44
|
+
import { SequenceList } from "./sequence-list";
|
|
45
|
+
|
|
46
|
+
export interface SequencesScreenProps {
|
|
47
|
+
/** Owner email sent with a new sequence (`user_email`). */
|
|
48
|
+
ownerEmail?: string;
|
|
49
|
+
/** Extra buttons in the toolbar. */
|
|
50
|
+
toolbarActions?: ReactNode;
|
|
51
|
+
/** Extra buttons in the open sequence's header. */
|
|
52
|
+
renderDetailActions?: (sequence: SequenceRow) => ReactNode;
|
|
53
|
+
/** Extra tabs in the open sequence's panel. */
|
|
54
|
+
extraDetailTabs?: { id: string; label: string; render: (sequence: SequenceRow) => ReactNode }[];
|
|
55
|
+
/** Open an enrolled person in your contacts screen; without it the Contacts tab has no such button. */
|
|
56
|
+
onOpenContact?: (contactId: number) => void;
|
|
57
|
+
className?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const FILTERS = [
|
|
61
|
+
{ value: "", label: "All statuses" },
|
|
62
|
+
{ value: "drafted", label: "Drafts" },
|
|
63
|
+
{ value: "live", label: "Live" },
|
|
64
|
+
{ value: "paused", label: "Paused" },
|
|
65
|
+
];
|
|
66
|
+
const SUMMARY_LIMIT = 100;
|
|
67
|
+
|
|
68
|
+
/** Counts by status group for the stat strip. Exported for tests. */
|
|
69
|
+
export function sequenceStats(rows: SequenceRow[]): StatItem[] {
|
|
70
|
+
const n = (g: ReturnType<typeof statusGroup>) => rows.filter((r) => statusGroup(r.status) === g).length;
|
|
71
|
+
const hint = rows.length >= SUMMARY_LIMIT ? `First ${SUMMARY_LIMIT} sequences` : undefined;
|
|
72
|
+
const live = rows.filter((r) => statusGroup(r.status) === "live");
|
|
73
|
+
return [
|
|
74
|
+
{ label: "Live", value: n("live"), tone: "positive", hint },
|
|
75
|
+
{ label: "Paused", value: n("paused"), tone: "warning", hint },
|
|
76
|
+
{ label: "Drafts", value: n("draft"), hint },
|
|
77
|
+
{ label: "Contacts in live", value: live.reduce((a, r) => a + (r.contact_count ?? 0), 0).toLocaleString(), hint },
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function SequencesScreen(props: SequencesScreenProps) {
|
|
82
|
+
const { ownerEmail, toolbarActions, className } = props;
|
|
83
|
+
const [selectedId, setSelectedId] = useUrlState("id", "");
|
|
84
|
+
const [detailTab, setDetailTab] = useUrlState("dtab", "");
|
|
85
|
+
const [view, setView] = useUrlState("view", "");
|
|
86
|
+
const [stepId, setStepId] = useUrlState("step", "");
|
|
87
|
+
const [contactId, setContactId] = useUrlState("enrollment", "");
|
|
88
|
+
const [status, setStatus] = useUrlState("status", "");
|
|
89
|
+
const [creating, setCreating] = useState(false);
|
|
90
|
+
const [version, setVersion] = useState(0);
|
|
91
|
+
const [listKey, setListKey] = useState(0);
|
|
92
|
+
const summary = useG8Query((signal) => g8Get("/sequences", { limit: SUMMARY_LIMIT }, { signal }), [version], { key: ["sequences", "summary"] });
|
|
93
|
+
const stats = summary.data ? sequenceStats(listOf<SequenceRow>(summary.data)) : undefined;
|
|
94
|
+
const bump = () => setVersion((v) => v + 1);
|
|
95
|
+
const open = (id: string) => { setSelectedId(id); setDetailTab(null); setStepId(null); setContactId(null); };
|
|
96
|
+
const close = () => { setSelectedId(null); setDetailTab(null); setStepId(null); setContactId(null); setView(null); };
|
|
97
|
+
const refreshList = () => { setListKey((k) => k + 1); bump(); };
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<ScreenLayout
|
|
101
|
+
className={className}
|
|
102
|
+
title="Sequences"
|
|
103
|
+
description="Multi-channel outreach that runs on its own: build steps, enroll contacts, watch replies."
|
|
104
|
+
icon={<ListOrdered className="h-5 w-5" aria-hidden />}
|
|
105
|
+
actions={
|
|
106
|
+
<Button onClick={() => setCreating(true)}>
|
|
107
|
+
<Plus className="mr-1 h-4 w-4" aria-hidden /> New sequence
|
|
108
|
+
</Button>
|
|
109
|
+
}
|
|
110
|
+
stats={stats}
|
|
111
|
+
>
|
|
112
|
+
<div className="space-y-3">
|
|
113
|
+
<Toolbar
|
|
114
|
+
actions={toolbarActions}
|
|
115
|
+
filters={
|
|
116
|
+
<select aria-label="Status" className="h-9 rounded-md border bg-background px-2 text-sm" value={status} onChange={(e) => setStatus(e.target.value || null)}>
|
|
117
|
+
{FILTERS.map((f) => <option key={f.value} value={f.value}>{f.label}</option>)}
|
|
118
|
+
</select>
|
|
119
|
+
}
|
|
120
|
+
/>
|
|
121
|
+
<SplitView
|
|
122
|
+
expanded={view === "full"} onExpandedChange={(e) => setView(e ? "full" : null)}
|
|
123
|
+
detailOpen={Boolean(selectedId)}
|
|
124
|
+
onCloseDetail={close}
|
|
125
|
+
list={
|
|
126
|
+
<SequenceList key={`${listKey}:${status}`} status={status || undefined} onOpen={(s) => open(s.id)} onCreate={() => setCreating(true)} onChanged={bump} />
|
|
127
|
+
}
|
|
128
|
+
detail={
|
|
129
|
+
selectedId ? (
|
|
130
|
+
<SequenceDetail
|
|
131
|
+
key={selectedId}
|
|
132
|
+
sequenceId={selectedId}
|
|
133
|
+
onExpand={view === "full" ? undefined : () => setView("full")} onCollapse={view === "full" ? () => setView(null) : undefined}
|
|
134
|
+
tab={detailTab || undefined}
|
|
135
|
+
onTabChange={(t) => setDetailTab(t === "details" ? null : t)}
|
|
136
|
+
stepId={stepId || undefined}
|
|
137
|
+
onStepChange={(v) => setStepId(v)}
|
|
138
|
+
contactId={contactId || undefined}
|
|
139
|
+
onContactChange={(v) => setContactId(v)}
|
|
140
|
+
onOpenContact={props.onOpenContact}
|
|
141
|
+
onClose={close}
|
|
142
|
+
onChanged={refreshList}
|
|
143
|
+
refreshKey={version}
|
|
144
|
+
onOpenSequence={open}
|
|
145
|
+
renderActions={props.renderDetailActions}
|
|
146
|
+
extraTabs={props.extraDetailTabs}
|
|
147
|
+
/>
|
|
148
|
+
) : null
|
|
149
|
+
}
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
<FormSheet open={creating} onOpenChange={setCreating} width="xl" title="New sequence" description="Saved as a draft. Nobody is contacted until you launch it.">
|
|
153
|
+
{creating ? (
|
|
154
|
+
<SequenceBuilder
|
|
155
|
+
ownerEmail={ownerEmail}
|
|
156
|
+
onSaved={(id) => { setCreating(false); refreshList(); open(id); }}
|
|
157
|
+
/>
|
|
158
|
+
) : null}
|
|
159
|
+
</FormSheet>
|
|
160
|
+
</ScreenLayout>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -20,6 +20,7 @@ import { g8Get } from "@/g8/lib/g8-client";
|
|
|
20
20
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
21
21
|
import { hasBrokenTokens, KIND_LABEL, LINKEDIN_ACTIONS, stepIssues } from "../lib/sequencer-utils";
|
|
22
22
|
import type { DraftStep, InputType } from "../lib/sequencer-utils";
|
|
23
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
23
24
|
|
|
24
25
|
export interface StepEditorProps {
|
|
25
26
|
step: DraftStep;
|
|
@@ -38,6 +39,7 @@ const COPY_KEY: Record<string, string | undefined> = {
|
|
|
38
39
|
};
|
|
39
40
|
|
|
40
41
|
function LinkedinReadiness() {
|
|
42
|
+
const brand = useBrand();
|
|
41
43
|
const conn = useG8Query(
|
|
42
44
|
(signal) =>
|
|
43
45
|
g8Get<{ connected?: boolean; is_token_valid?: boolean; requires_manual_login?: boolean }>(
|
|
@@ -50,10 +52,10 @@ function LinkedinReadiness() {
|
|
|
50
52
|
const c = conn.data?.data;
|
|
51
53
|
if (conn.loading || conn.error || !c) return null;
|
|
52
54
|
if (c.connected && c.is_token_valid !== false && !c.requires_manual_login)
|
|
53
|
-
return <p className="text-xs text-
|
|
55
|
+
return <p className="text-xs text-positive-foreground">LinkedIn sender connected.</p>;
|
|
54
56
|
return (
|
|
55
|
-
<p role="alert" className="text-xs text-
|
|
56
|
-
LinkedIn is not ready to send. Reconnect the LinkedIn account in the
|
|
57
|
+
<p role="alert" className="text-xs text-warning-foreground">
|
|
58
|
+
LinkedIn is not ready to send. Reconnect the LinkedIn account in the {brand.linkLabel} app before launching.
|
|
57
59
|
</p>
|
|
58
60
|
);
|
|
59
61
|
}
|
|
@@ -70,13 +72,13 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
|
|
|
70
72
|
const phoneOptions = Array.isArray(data.phone_options) ? (data.phone_options as string[]) : [];
|
|
71
73
|
|
|
72
74
|
return (
|
|
73
|
-
<fieldset disabled={disabled} className={`space-y-3 rounded border border-
|
|
75
|
+
<fieldset disabled={disabled} className={`space-y-3 rounded border border-border p-3 ${className ?? ""}`} data-step-kind={step.kind}>
|
|
74
76
|
<div className="flex items-center justify-between">
|
|
75
|
-
<legend className="text-sm font-
|
|
77
|
+
<legend className="text-sm font-medium">
|
|
76
78
|
Step {index}: {KIND_LABEL[step.kind]}
|
|
77
79
|
</legend>
|
|
78
80
|
{onRemove ? (
|
|
79
|
-
<button type="button" className="text-xs text-
|
|
81
|
+
<button type="button" className="text-xs text-error-foreground hover:underline" onClick={onRemove}>
|
|
80
82
|
Remove step
|
|
81
83
|
</button>
|
|
82
84
|
) : null}
|
|
@@ -103,7 +105,7 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
|
|
|
103
105
|
Copy
|
|
104
106
|
<select
|
|
105
107
|
aria-label="Email copy"
|
|
106
|
-
className="ml-2 h-8 rounded border border-
|
|
108
|
+
className="ml-2 h-8 rounded border border-input px-1 text-sm"
|
|
107
109
|
value={step.input_type}
|
|
108
110
|
onChange={(e) => onChange({ ...step, dirty: true, input_type: e.target.value as InputType })}
|
|
109
111
|
>
|
|
@@ -139,7 +141,7 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
|
|
|
139
141
|
<>
|
|
140
142
|
<select
|
|
141
143
|
aria-label="LinkedIn action"
|
|
142
|
-
className="h-8 rounded border border-
|
|
144
|
+
className="h-8 rounded border border-input px-1 text-sm"
|
|
143
145
|
value={action}
|
|
144
146
|
onChange={(e) => set({ linkedin_action_type: e.target.value })}
|
|
145
147
|
>
|
|
@@ -158,7 +160,7 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
|
|
|
158
160
|
onChange={(e) => set({ [copyKey]: e.target.value })}
|
|
159
161
|
/>
|
|
160
162
|
) : (
|
|
161
|
-
<p className="text-xs text-
|
|
163
|
+
<p className="text-xs text-muted-foreground">This action sends no text.</p>
|
|
162
164
|
)}
|
|
163
165
|
{action === "INMAIL" ? (
|
|
164
166
|
<Input
|
|
@@ -174,7 +176,7 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
|
|
|
174
176
|
|
|
175
177
|
{step.kind === "call" || step.kind === "task" ? (
|
|
176
178
|
<div className="space-y-1 text-sm">
|
|
177
|
-
<p className="text-
|
|
179
|
+
<p className="text-muted-foreground">
|
|
178
180
|
{step.kind === "call" ? "An automatic call attempt." : "Creates a call task for a person to work in the dialer."}
|
|
179
181
|
</p>
|
|
180
182
|
<div className="flex flex-wrap gap-3">
|
|
@@ -197,12 +199,12 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
|
|
|
197
199
|
) : null}
|
|
198
200
|
|
|
199
201
|
{broken ? (
|
|
200
|
-
<p role="alert" className="text-xs text-
|
|
202
|
+
<p role="alert" className="text-xs text-warning-foreground">
|
|
201
203
|
A {"{{merge token}}"} is not closed. Unclosed tokens are deleted when the email sends.
|
|
202
204
|
</p>
|
|
203
205
|
) : null}
|
|
204
206
|
{issues.length > 0 ? (
|
|
205
|
-
<ul className="text-xs text-
|
|
207
|
+
<ul className="text-xs text-error-foreground" aria-label="Step problems">
|
|
206
208
|
{issues.map((i) => (
|
|
207
209
|
<li key={i}>{i}</li>
|
|
208
210
|
))}
|
|
@@ -10,7 +10,8 @@ export type StepKind = "email" | "linkedin" | "call" | "task";
|
|
|
10
10
|
|
|
11
11
|
/** UI step kind -> the API `step_type`. `linkedin` is HeyReach; `task` is a manual dialer task. */
|
|
12
12
|
export const STEP_TYPE: Record<StepKind, string> = { email: "EMAIL", linkedin: "HEYREACH", call: "PHONE", task: "MANUAL_DIALER" };
|
|
13
|
-
|
|
13
|
+
/** `call` is an AI voice-agent call (PHONE); `task` is a call a rep makes from the dialer (MANUAL_DIALER). */
|
|
14
|
+
export const KIND_LABEL: Record<StepKind, string> = { email: "Email", linkedin: "LinkedIn", call: "AI call", task: "Call task" };
|
|
14
15
|
export const KINDS = Object.keys(STEP_TYPE) as StepKind[];
|
|
15
16
|
|
|
16
17
|
export const LINKEDIN_ACTIONS = ["CONNECTION_REQUEST", "MESSAGE", "INMAIL", "FOLLOW", "VIEW_PROFILE", "LIKE_POST"] as const;
|
|
@@ -29,6 +30,8 @@ export interface DraftStep {
|
|
|
29
30
|
step_data: Record<string, unknown>;
|
|
30
31
|
/** True once changed since it was loaded or saved. */
|
|
31
32
|
dirty?: boolean;
|
|
33
|
+
/** The API `step_type` a loaded step had (for example NETRION), so saving it never rewrites the provider. */
|
|
34
|
+
step_type?: string;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
export interface SequenceRow {
|
|
@@ -77,17 +80,23 @@ export function fromApiStep(s: ApiStep): DraftStep {
|
|
|
77
80
|
key: s.id ?? newKey(),
|
|
78
81
|
id: s.id,
|
|
79
82
|
kind: kindOf(s.step_type),
|
|
83
|
+
step_type: s.step_type,
|
|
80
84
|
input_type: (s.input_type as InputType) ?? "MANUAL_TEMPLATE",
|
|
81
85
|
wait_days: (s.time_interval ?? 0) / 86400,
|
|
82
86
|
step_data: { ...(s.step_data ?? {}) },
|
|
83
87
|
};
|
|
84
88
|
}
|
|
85
89
|
|
|
86
|
-
/**
|
|
87
|
-
|
|
90
|
+
/**
|
|
91
|
+
* The request shape for create, update and add-steps. `order` is 1-based. `linkedinStepType` is the LinkedIn
|
|
92
|
+
* provider step for NEW LinkedIn steps (`HEYREACH`, or `NETRION`); a loaded step keeps its own `step_type`.
|
|
93
|
+
*/
|
|
94
|
+
export function toApiStep(step: DraftStep, order: number, linkedinStepType?: string) {
|
|
95
|
+
// A loaded step keeps its own type (a NETRION step stays NETRION) as long as its kind did not change.
|
|
96
|
+
const own = step.step_type && kindOf(step.step_type) === step.kind ? step.step_type : undefined;
|
|
88
97
|
return {
|
|
89
98
|
step_order: order,
|
|
90
|
-
step_type: STEP_TYPE[step.kind],
|
|
99
|
+
step_type: own ?? (step.kind === "linkedin" && linkedinStepType ? linkedinStepType : STEP_TYPE[step.kind]),
|
|
91
100
|
input_type: step.input_type,
|
|
92
101
|
time_interval: Math.max(0, Math.round(step.wait_days * 86400)),
|
|
93
102
|
step_data: step.step_data,
|
|
@@ -133,11 +142,11 @@ export function statusGroup(status: string | null | undefined): "draft" | "live"
|
|
|
133
142
|
}
|
|
134
143
|
|
|
135
144
|
export const STATUS_CLASS: Record<ReturnType<typeof statusGroup>, string> = {
|
|
136
|
-
draft: "bg-
|
|
137
|
-
live: "bg-
|
|
138
|
-
paused: "bg-
|
|
139
|
-
ended: "bg-
|
|
140
|
-
busy: "bg-
|
|
145
|
+
draft: "bg-muted text-foreground",
|
|
146
|
+
live: "bg-positive text-positive-foreground",
|
|
147
|
+
paused: "bg-warning text-warning-foreground",
|
|
148
|
+
ended: "bg-muted text-muted-foreground",
|
|
149
|
+
busy: "bg-info text-info-foreground",
|
|
141
150
|
};
|
|
142
151
|
|
|
143
152
|
export function listOf<T>(envelope: unknown): T[] {
|
|
@@ -153,7 +162,96 @@ export function objectOf(envelope: unknown): Record<string, unknown> {
|
|
|
153
162
|
return data && typeof data === "object" && !Array.isArray(data) ? (data as Record<string, unknown>) : {};
|
|
154
163
|
}
|
|
155
164
|
|
|
156
|
-
|
|
165
|
+
export type EmailChannelType = "GMAIL" | "INBOXKIT" | "SMTP";
|
|
166
|
+
|
|
167
|
+
export interface MailboxLike {
|
|
168
|
+
provider?: string | null;
|
|
169
|
+
/** inboxkit | manual_google | manual_microsoft | manual_smtp | opensrs | purchased (GET /mailboxes). */
|
|
170
|
+
source?: string | null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The sequence channel type for a mailbox, decided by `source` first, as the graph8 app does: an InboxKit mailbox
|
|
175
|
+
* is INBOXKIT whatever its provider (a Google-backed InboxKit mailbox attached as GMAIL fails at send time),
|
|
176
|
+
* Google is GMAIL, SMTP is SMTP. `null` = this mailbox cannot send from a sequence (see `mailboxUnsupportedReason`).
|
|
177
|
+
*/
|
|
178
|
+
export function mailboxChannelType(mailbox: MailboxLike): EmailChannelType | null {
|
|
179
|
+
const source = (mailbox.source ?? "").toLowerCase();
|
|
180
|
+
const provider = (mailbox.provider ?? "").toLowerCase();
|
|
181
|
+
if (source === "inboxkit") return "INBOXKIT";
|
|
182
|
+
if (source === "manual_microsoft" || provider === "microsoft") return null;
|
|
183
|
+
if (provider === "google_mailbox" || source === "manual_google") return "GMAIL";
|
|
184
|
+
if (provider === "smtp" || source === "manual_smtp") return "SMTP";
|
|
185
|
+
// An API without `source` (older graph8): fall back to the provider guess.
|
|
186
|
+
return source ? null : channelTypeFor(provider);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Why a mailbox cannot be picked as a sequence sender, or null when it can. */
|
|
190
|
+
export function mailboxUnsupportedReason(mailbox: MailboxLike): string | null {
|
|
191
|
+
if (mailboxChannelType(mailbox)) return null;
|
|
192
|
+
const microsoft = (mailbox.source ?? "").toLowerCase() === "manual_microsoft" || (mailbox.provider ?? "").toLowerCase() === "microsoft";
|
|
193
|
+
return microsoft
|
|
194
|
+
? "Directly connected Microsoft mailboxes cannot send from sequences yet"
|
|
195
|
+
: "This mailbox type cannot send from sequences yet";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The senders a set of steps needs. `call` (AI) needs an AI number, `task` a dialer number. */
|
|
199
|
+
export function requiredSenders(kinds: Iterable<StepKind>): { email: boolean; aiPhone: boolean; dialerPhone: boolean; linkedin: boolean } {
|
|
200
|
+
const set = new Set(kinds);
|
|
201
|
+
return { email: set.has("email"), aiPhone: set.has("call"), dialerPhone: set.has("task"), linkedin: set.has("linkedin") };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const EMAIL_TYPES = new Set(["SMTP", "GMAIL", "INBOXKIT"]);
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* What stops a sequence from launching, read from GET /sequences/{id}/preview (`steps[].step_type` and
|
|
208
|
+
* `channels[].channel_type`): the API refuses `/run` with 400 when a step type has no sender channel. Checked
|
|
209
|
+
* client-side so a launch is never offered that can only fail.
|
|
210
|
+
*/
|
|
211
|
+
export function senderGaps(steps: Array<{ step_type?: string | null }>, channels: Array<{ channel_type?: string | null }>): string[] {
|
|
212
|
+
const types = new Set(steps.map((s) => (s.step_type ?? "").toUpperCase()));
|
|
213
|
+
const have = new Set(channels.map((c) => (c.channel_type ?? "").toUpperCase()));
|
|
214
|
+
const gaps: string[] = [];
|
|
215
|
+
if (types.has("EMAIL") && ![...have].some((t) => EMAIL_TYPES.has(t))) gaps.push("Email steps have no mailbox to send from.");
|
|
216
|
+
if ((types.has("PHONE") || types.has("MANUAL_DIALER")) && !have.has("PHONE"))
|
|
217
|
+
gaps.push("Call steps have no phone number to call from.");
|
|
218
|
+
if (types.has("HEYREACH") && !have.has("LINKEDIN")) gaps.push("LinkedIn steps have no LinkedIn account to send from.");
|
|
219
|
+
if (types.has("NETRION") && !have.has("LINKEDIN_NETRION")) gaps.push("LinkedIn steps have no LinkedIn account to send from.");
|
|
220
|
+
if (types.has("SMS") && !have.has("SMS")) gaps.push("SMS steps have no number to text from.");
|
|
221
|
+
return gaps;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
interface PreviewIssue {
|
|
225
|
+
message?: string;
|
|
226
|
+
severity?: string;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Launch check from GET /sequences/{id}/preview. The verdict lives on each step: `steps[].rendered.will_send_as_shown`
|
|
231
|
+
* and `steps[].rendered.content_issues` (a top-level copy is read too, for older APIs). An error-severity issue
|
|
232
|
+
* makes `/run` answer 400, so it blocks; anything else is a warning. A missing sender also blocks.
|
|
233
|
+
*/
|
|
234
|
+
export function launchCheck(preview: Record<string, unknown>): { warnings: string[]; blockers: string[] } {
|
|
235
|
+
const steps = (Array.isArray(preview.steps) ? preview.steps : []) as Array<Record<string, unknown>>;
|
|
236
|
+
const channels = (Array.isArray(preview.channels) ? preview.channels : []) as Array<{ channel_type?: string | null }>;
|
|
237
|
+
const warnings: string[] = [];
|
|
238
|
+
const blockers: string[] = [];
|
|
239
|
+
const read = (verdict: Record<string, unknown> | null | undefined, where: string) => {
|
|
240
|
+
if (!verdict) return;
|
|
241
|
+
const issues = (Array.isArray(verdict.content_issues) ? verdict.content_issues : []) as PreviewIssue[];
|
|
242
|
+
for (const issue of issues) {
|
|
243
|
+
const text = `${where}${issue.message ?? "The copy will not send as shown."}`;
|
|
244
|
+
((issue.severity ?? "").toLowerCase() === "error" ? blockers : warnings).push(text);
|
|
245
|
+
}
|
|
246
|
+
if (verdict.will_send_as_shown === false && issues.length === 0) warnings.push(`${where}The copy will not send as shown.`);
|
|
247
|
+
};
|
|
248
|
+
steps.forEach((step, i) => read(step.rendered as Record<string, unknown> | undefined, `Step ${Number(step.step_order ?? i + 1)}: `));
|
|
249
|
+
read(preview, "");
|
|
250
|
+
blockers.push(...senderGaps(steps as Array<{ step_type?: string | null }>, channels));
|
|
251
|
+
return { warnings: [...new Set(warnings)], blockers: [...new Set(blockers)] };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Map a mailbox's provider to the sequence channel type the API takes. Prefer `mailboxChannelType`, which reads `source`. */
|
|
157
255
|
export function channelTypeFor(provider: string | null | undefined): "GMAIL" | "INBOXKIT" | "SMTP" {
|
|
158
256
|
const p = (provider ?? "").toLowerCase();
|
|
159
257
|
if (p.includes("gmail") || p.includes("google")) return "GMAIL";
|
|
@@ -166,3 +264,28 @@ export function newIdempotencyKey(): string {
|
|
|
166
264
|
? crypto.randomUUID()
|
|
167
265
|
: `g8-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
168
266
|
}
|
|
267
|
+
|
|
268
|
+
const HIDDEN_FIELDS = /password|secret|token|credential/i;
|
|
269
|
+
|
|
270
|
+
/** Every readable field of an open-shaped record as label/value pairs (secrets are never shown). */
|
|
271
|
+
export function fieldItems(obj: Record<string, unknown>): Array<{ label: string; value: string }> {
|
|
272
|
+
return Object.entries(obj)
|
|
273
|
+
.filter(([k, v]) => !HIDDEN_FIELDS.test(k) && v !== null && v !== undefined && v !== "")
|
|
274
|
+
.map(([k, v]) => {
|
|
275
|
+
const label = k.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
|
|
276
|
+
if (typeof v === "string" && /_at$|date$/.test(k) && !Number.isNaN(new Date(v).getTime()))
|
|
277
|
+
return { label, value: new Date(v).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short", timeZone: "UTC" }) + " UTC" };
|
|
278
|
+
if (typeof v === "boolean") return { label, value: v ? "Yes" : "No" };
|
|
279
|
+
if (typeof v === "object") return { label, value: JSON.stringify(v) };
|
|
280
|
+
return { label, value: String(v) };
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** One line that says what a step does, for lists. */
|
|
285
|
+
export function stepSummary(step: DraftStep): string {
|
|
286
|
+
const d = step.step_data;
|
|
287
|
+
const str = (v: unknown) => (typeof v === "string" ? v.trim() : "");
|
|
288
|
+
if (step.kind === "email") return str(d.subject) || str(d.instructions) || "Email";
|
|
289
|
+
if (step.kind === "linkedin") return str(d.linkedin_action_type).replace(/_/g, " ").toLowerCase() || "LinkedIn";
|
|
290
|
+
return KIND_LABEL[step.kind];
|
|
291
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @title Create a draft sequence with email, LinkedIn and
|
|
2
|
+
* @title Create a draft sequence with email, LinkedIn and an AI call
|
|
3
3
|
* @when Building outreach. Creating the draft is tier `external` in the contract but contacts nobody; the
|
|
4
|
-
* LAUNCH is what reaches people (see launch-and-enroll.ts).
|
|
4
|
+
* LAUNCH is what reaches people (see launch-and-enroll.ts). Rules the API only enforces at send or launch time:
|
|
5
5
|
* 1) an EMAIL step needs `input_type: "MANUAL_TEMPLATE"` or its subject/body are ignored; 2) a LinkedIn
|
|
6
6
|
* (HEYREACH) step needs `linkedin_action_type` and the action's own copy key, never `body`; 3) `time_interval`
|
|
7
|
-
* is in SECONDS
|
|
7
|
+
* is in SECONDS; 4) EVERY step type needs its own sender channel or POST /sequences/{id}/run answers 400: the
|
|
8
|
+
* mailbox (channel type from its `source`), a Voice AI number for PHONE (an AI voice-agent call; a rep's call is
|
|
9
|
+
* MANUAL_DIALER with a dialer number), a LinkedIn account for HEYREACH; 5) `associated_list_id` is required to
|
|
10
|
+
* launch and cannot be set later.
|
|
8
11
|
*/
|
|
9
12
|
import { g8 } from "@graph8/sdk";
|
|
10
13
|
|
|
@@ -12,7 +15,28 @@ g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
|
12
15
|
|
|
13
16
|
const DAY = 86400;
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
/** Sequence channel type for a mailbox from GET /mailboxes: by `source` first, as the graph8 app does. */
|
|
19
|
+
function mailboxChannelType(m: { source?: string | null; provider?: string | null }): "INBOXKIT" | "GMAIL" | "SMTP" {
|
|
20
|
+
if (m.source === "inboxkit") return "INBOXKIT";
|
|
21
|
+
if (m.source === "manual_microsoft" || m.provider === "microsoft")
|
|
22
|
+
throw new Error("Directly connected Microsoft mailboxes cannot send from sequences yet: pick another mailbox");
|
|
23
|
+
if (m.provider === "google_mailbox" || m.source === "manual_google") return "GMAIL";
|
|
24
|
+
return "SMTP";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function createDraftSequence(name: string, listId: number, sender: { id: number; email: string }) {
|
|
28
|
+
// The senders, one per step type. Rows are open-shaped (no response schema in the contract): read loosely.
|
|
29
|
+
const mailboxes = (await g8.api.gtmContext.listMailboxes()).data as Array<{ id: string; email: string; source?: string; provider?: string }>;
|
|
30
|
+
const mailbox = mailboxes.find((m) => Number(m.id) === sender.id);
|
|
31
|
+
if (!mailbox) throw new Error(`Mailbox ${sender.id} not found`);
|
|
32
|
+
const mailboxId = sender.id;
|
|
33
|
+
const phones = (await g8.api.sequences.listSequencerPhoneNumbers()).data as { phone_numbers?: Array<{ id: number; phone_number: string; inbound_type?: string }> };
|
|
34
|
+
const aiNumber = (phones.phone_numbers ?? []).find((p) => p.inbound_type === "ai");
|
|
35
|
+
const linkedin = (await g8.api.sequences.listLinkedinAccounts()).data as { linkedin_accounts?: Array<{ id: number; name?: string }>; provider?: string };
|
|
36
|
+
const account = linkedin.linkedin_accounts?.[0];
|
|
37
|
+
if (!aiNumber || !account || linkedin.provider !== "HEYREACH")
|
|
38
|
+
throw new Error("This recipe needs a Voice AI number and a HeyReach LinkedIn account (connect them in the graph8 app)");
|
|
39
|
+
|
|
16
40
|
const created = await g8.api.sequenceLifecycle.createSequence({
|
|
17
41
|
body: {
|
|
18
42
|
name,
|
|
@@ -44,7 +68,11 @@ export async function createDraftSequence(name: string, listId: number, mailbox:
|
|
|
44
68
|
step_data: { dial_dnc: false, phone_options: ["mobile", "direct"] },
|
|
45
69
|
},
|
|
46
70
|
],
|
|
47
|
-
channels: [
|
|
71
|
+
channels: [
|
|
72
|
+
{ channel_id: mailboxId, channel_value: mailbox.email, channel_type: mailboxChannelType(mailbox) },
|
|
73
|
+
{ channel_id: aiNumber.id, channel_value: aiNumber.phone_number, channel_type: "PHONE" },
|
|
74
|
+
{ channel_id: account.id, channel_value: account.name ?? String(account.id), channel_type: "LINKEDIN" },
|
|
75
|
+
],
|
|
48
76
|
},
|
|
49
77
|
});
|
|
50
78
|
return created.data?.id;
|
|
@@ -19,9 +19,26 @@ export async function launchAndEnroll(sequenceId: string, listId: number, contac
|
|
|
19
19
|
if ((verdict.data as { safe_to_send?: boolean } | undefined)?.safe_to_send === false)
|
|
20
20
|
throw new Error("Deliverability says not safe to send right now");
|
|
21
21
|
|
|
22
|
+
// The copy verdict is PER STEP (`steps[].rendered`), not on the preview itself. An error-severity issue makes
|
|
23
|
+
// /run answer 400; anything else is a warning to show the human. Every step type also needs a sender channel.
|
|
22
24
|
const preview = await g8.api.sequenceLifecycle.previewSequence({ path: { sequence_id: sequenceId } });
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
+
const steps = preview.data?.steps ?? [];
|
|
26
|
+
const issues = steps.flatMap((s) =>
|
|
27
|
+
(s.rendered?.content_issues ?? []).map((i) => ({ step: s.step_order, ...(i as { severity?: string; message?: string }) })),
|
|
28
|
+
);
|
|
29
|
+
const errors = issues.filter((i) => (i.severity ?? "").toLowerCase() === "error");
|
|
30
|
+
if (errors.length) throw new Error(`Fix the copy first: ${errors.map((e) => `step ${e.step}: ${e.message}`).join("; ")}`);
|
|
31
|
+
if (steps.some((s) => s.rendered?.will_send_as_shown === false))
|
|
32
|
+
console.warn("Some copy will not send exactly as previewed:", JSON.stringify(issues));
|
|
33
|
+
const types = new Set(steps.map((s) => s.step_type.toUpperCase()));
|
|
34
|
+
const senders = new Set((preview.data?.channels ?? []).map((c) => (c.channel_type ?? "").toUpperCase()));
|
|
35
|
+
const missing = [
|
|
36
|
+
types.has("EMAIL") && !["SMTP", "GMAIL", "INBOXKIT"].some((t) => senders.has(t)) && "a mailbox",
|
|
37
|
+
(types.has("PHONE") || types.has("MANUAL_DIALER")) && !senders.has("PHONE") && "a phone number",
|
|
38
|
+
types.has("HEYREACH") && !senders.has("LINKEDIN") && "a LinkedIn account",
|
|
39
|
+
types.has("NETRION") && !senders.has("LINKEDIN_NETRION") && "a LinkedIn account",
|
|
40
|
+
].filter(Boolean);
|
|
41
|
+
if (missing.length) throw new Error(`Attach ${missing.join(", ")} first (POST /sequences/{id}/channels): /run refuses it`);
|
|
25
42
|
|
|
26
43
|
const current = await g8.api.sequences.getSequence({ path: { sequence_id: sequenceId } });
|
|
27
44
|
const status = (current.data as { status?: string } | undefined)?.status;
|