@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,181 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PersonaDetail: one buyer persona, complete. Every field, the campaigns that target it, and every action:
|
|
5
|
+
* edit, pin or unpin, archive.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. getPersona GET /personas/{persona_id} (read; all fields)
|
|
9
|
+
* 2. listCampaigns GET /campaigns (read; campaigns whose target persona is this title)
|
|
10
|
+
* 3. updatePersona PATCH /personas/{persona_id} (write)
|
|
11
|
+
* 4. pinPersona / unpinPersona POST /personas/{persona_id}/pin|unpin (write)
|
|
12
|
+
* 5. archivePersona POST /personas/{persona_id}/archive (destructive: confirmed)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useState } from "react";
|
|
16
|
+
import type { ReactNode } from "react";
|
|
17
|
+
import { Button, Input, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
18
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
19
|
+
import { useAllPages } from "@/g8/hooks/use-all-pages";
|
|
20
|
+
import { hasNextOf } from "@/g8/lib/paging";
|
|
21
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
22
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
23
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
24
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
25
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
26
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
27
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
28
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
29
|
+
import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
30
|
+
import { fieldItems, listOf, objectOf, personaFormOf, personaPatch, text } from "../lib/campaigns-utils";
|
|
31
|
+
import type { CampaignRow } from "../lib/campaigns-utils";
|
|
32
|
+
import { FormField } from "./form-field";
|
|
33
|
+
|
|
34
|
+
export interface PersonaDetailProps {
|
|
35
|
+
personaId: string;
|
|
36
|
+
onClose?: () => void;
|
|
37
|
+
/** Full-page mode: show an "Open full page" (onExpand) or "Back to split view" (onCollapse) button. */
|
|
38
|
+
onExpand?: () => void;
|
|
39
|
+
onCollapse?: () => void;
|
|
40
|
+
onChanged?: () => void;
|
|
41
|
+
/** Open a campaign that targets this persona. */
|
|
42
|
+
onOpenCampaign?: (campaign: CampaignRow) => void;
|
|
43
|
+
renderActions?: (persona: Record<string, unknown>) => ReactNode;
|
|
44
|
+
className?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface Form { title: string; why: string; signals: string; approach: string; goal: string; priority: string; confidence: string }
|
|
48
|
+
|
|
49
|
+
/** The API returns `pinned` (`is_pinned` is accepted too, for older payloads). */
|
|
50
|
+
function isPinned(r: Record<string, unknown> | undefined): boolean {
|
|
51
|
+
return Boolean(r && (r.pinned ?? r.is_pinned));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Campaigns per read: every page is read, so no targeting campaign is missed. */
|
|
55
|
+
const CAMPAIGN_PAGE = 100;
|
|
56
|
+
|
|
57
|
+
export function PersonaDetail({ personaId, onClose, onExpand, onCollapse, onChanged, onOpenCampaign, renderActions, className }: PersonaDetailProps) {
|
|
58
|
+
const [form, setForm] = useState<Form | null>(null);
|
|
59
|
+
const [archiving, setArchiving] = useState(false);
|
|
60
|
+
const q = useG8Query((signal) => g8Get(`/personas/${personaId}`, undefined, { signal }), [personaId]);
|
|
61
|
+
const campaigns = useAllPages<CampaignRow>(async (page, signal) => {
|
|
62
|
+
const env = await g8Get("/campaigns", { page, limit: CAMPAIGN_PAGE }, { signal });
|
|
63
|
+
const rows = listOf<CampaignRow>(env);
|
|
64
|
+
return { rows, hasNext: hasNextOf(env, CAMPAIGN_PAGE, rows.length) };
|
|
65
|
+
}, [personaId]);
|
|
66
|
+
const p = objectOf(q.data) as Record<string, any>;
|
|
67
|
+
|
|
68
|
+
const save = useG8Mutation(async (f: Form) => {
|
|
69
|
+
const patch = personaPatch(p, f);
|
|
70
|
+
if (Object.keys(patch).length > 0) await g8Patch(`/personas/${personaId}`, patch); // nothing changed: no request
|
|
71
|
+
setForm(null);
|
|
72
|
+
await q.refetch();
|
|
73
|
+
onChanged?.();
|
|
74
|
+
});
|
|
75
|
+
const pin = useG8Mutation(async () => {
|
|
76
|
+
await g8Post(`/personas/${personaId}/${isPinned(p) ? "unpin" : "pin"}`, {});
|
|
77
|
+
await q.refetch();
|
|
78
|
+
onChanged?.();
|
|
79
|
+
});
|
|
80
|
+
const archive = useG8Mutation(async () => {
|
|
81
|
+
await g8Post(`/personas/${personaId}/archive`, {});
|
|
82
|
+
setArchiving(false);
|
|
83
|
+
onChanged?.();
|
|
84
|
+
onClose?.();
|
|
85
|
+
}, { onError: () => setArchiving(false) });
|
|
86
|
+
|
|
87
|
+
if (q.error) return <ApiError error={q.error} title="Could not load the persona" onRetry={() => void q.refetch()} className={className} />;
|
|
88
|
+
if (q.loading && !q.data) return <Skeleton className={`h-48 w-full ${className ?? ""}`} />;
|
|
89
|
+
|
|
90
|
+
const title = text(p.title) || `Persona ${personaId}`;
|
|
91
|
+
const targeting = campaigns.items.filter((c) => (c.target_persona ?? "").trim().toLowerCase() === title.trim().toLowerCase());
|
|
92
|
+
const startEdit = () => {
|
|
93
|
+
save.reset();
|
|
94
|
+
setForm(personaFormOf(p));
|
|
95
|
+
};
|
|
96
|
+
const set = (k: keyof Form) => (e: { target: { value: string } }) => setForm((cur) => (cur ? { ...cur, [k]: e.target.value } : cur));
|
|
97
|
+
return (
|
|
98
|
+
<DetailPanel
|
|
99
|
+
className={className}
|
|
100
|
+
title={title}
|
|
101
|
+
subtitle={p.why_target ?? undefined}
|
|
102
|
+
badges={<><StatusPill tone={p.status && p.status !== "active" ? "neutral" : "positive"} dot>{p.status ?? "active"}</StatusPill>{isPinned(p) ? <StatusPill tone="info">pinned</StatusPill> : null}</>}
|
|
103
|
+
actions={
|
|
104
|
+
<>
|
|
105
|
+
<Button size="sm" variant="outline" onClick={startEdit}>Edit persona</Button>
|
|
106
|
+
<Button size="sm" variant="outline" loading={pin.loading} onClick={() => void pin.mutate(undefined)}>{isPinned(p) ? "Unpin persona" : "Pin persona"}</Button>
|
|
107
|
+
<Button size="sm" variant="outline" onClick={() => setArchiving(true)}>Archive persona</Button>
|
|
108
|
+
{renderActions?.(p)}
|
|
109
|
+
<FormSheet
|
|
110
|
+
open={form !== null}
|
|
111
|
+
onOpenChange={(o) => !o && setForm(null)}
|
|
112
|
+
title="Edit persona"
|
|
113
|
+
footer={<><Button variant="outline" onClick={() => setForm(null)}>Cancel</Button><Button loading={save.loading} disabled={!form?.title.trim()} onClick={() => form && void save.mutate(form)}>Save persona</Button></>}
|
|
114
|
+
>
|
|
115
|
+
{form ? (
|
|
116
|
+
<>
|
|
117
|
+
<FormField label="Title" htmlFor="pd-title"><Input id="pd-title" value={form.title} onChange={set("title")} /></FormField>
|
|
118
|
+
<FormField label="Why target" htmlFor="pd-why"><Textarea id="pd-why" value={form.why} onChange={set("why")} /></FormField>
|
|
119
|
+
<FormField label="Key signals" htmlFor="pd-sig" hint="One per line."><Textarea id="pd-sig" value={form.signals} onChange={set("signals")} /></FormField>
|
|
120
|
+
<FormField label="Campaign approach" htmlFor="pd-app"><Textarea id="pd-app" value={form.approach} onChange={set("approach")} /></FormField>
|
|
121
|
+
<FormField label="Recommended goal" htmlFor="pd-goal"><Input id="pd-goal" value={form.goal} onChange={set("goal")} /></FormField>
|
|
122
|
+
<FormField label="Priority" htmlFor="pd-pri"><Input id="pd-pri" type="number" min={1} value={form.priority} onChange={set("priority")} /></FormField>
|
|
123
|
+
<FormField label="Confidence" htmlFor="pd-conf">
|
|
124
|
+
<select id="pd-conf" className="h-9 rounded-md border bg-background px-2" value={form.confidence} onChange={set("confidence")}>{["low", "medium", "high"].map((c) => <option key={c} value={c}>{c}</option>)}</select>
|
|
125
|
+
</FormField>
|
|
126
|
+
{save.error ? <ApiError error={save.error} title="Could not save the persona" /> : null}
|
|
127
|
+
</>
|
|
128
|
+
) : null}
|
|
129
|
+
</FormSheet>
|
|
130
|
+
<ConfirmSpendDialog
|
|
131
|
+
open={archiving}
|
|
132
|
+
tier="destructive"
|
|
133
|
+
title={`Archive ${title}`}
|
|
134
|
+
description="It drops out of the default list. Existing campaigns keep the persona text."
|
|
135
|
+
affected={1}
|
|
136
|
+
confirmLabel="Archive persona"
|
|
137
|
+
confirming={archive.loading}
|
|
138
|
+
onCancel={() => setArchiving(false)}
|
|
139
|
+
onConfirm={() => void archive.mutate(undefined)}
|
|
140
|
+
/>
|
|
141
|
+
</>
|
|
142
|
+
}
|
|
143
|
+
onClose={onClose}
|
|
144
|
+
onExpand={onExpand}
|
|
145
|
+
onCollapse={onCollapse}
|
|
146
|
+
tabs={[
|
|
147
|
+
{
|
|
148
|
+
id: "details",
|
|
149
|
+
label: "Details",
|
|
150
|
+
content: (
|
|
151
|
+
<div className="space-y-4">
|
|
152
|
+
{[pin.error, archive.error].map((e, i) => (e ? <ApiError key={i} error={e} /> : null))}
|
|
153
|
+
<KeyValueList columns={2} items={fieldItems(p)} />
|
|
154
|
+
</div>
|
|
155
|
+
),
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "campaigns",
|
|
159
|
+
label: "Campaigns",
|
|
160
|
+
count: targeting.length,
|
|
161
|
+
content: (
|
|
162
|
+
<SectionCard title="Campaigns targeting this persona">
|
|
163
|
+
{targeting.length === 0 ? (
|
|
164
|
+
<p className="text-sm text-muted-foreground">No campaign targets this persona yet.</p>
|
|
165
|
+
) : (
|
|
166
|
+
<ul className="divide-y">
|
|
167
|
+
{targeting.map((c) => (
|
|
168
|
+
<li key={c.id} className="flex items-center justify-between py-2 text-sm">
|
|
169
|
+
<Button size="sm" variant="ghost" disabled={!onOpenCampaign} onClick={() => onOpenCampaign?.(c)}>{c.name}</Button>
|
|
170
|
+
<StatusPill tone="neutral">{c.status ?? "draft"}</StatusPill>
|
|
171
|
+
</li>
|
|
172
|
+
))}
|
|
173
|
+
</ul>
|
|
174
|
+
)}
|
|
175
|
+
</SectionCard>
|
|
176
|
+
),
|
|
177
|
+
},
|
|
178
|
+
]}
|
|
179
|
+
/>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
@@ -19,13 +19,14 @@
|
|
|
19
19
|
import { useState } from "react";
|
|
20
20
|
import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Textarea } from "@graph8/sdk/ui";
|
|
21
21
|
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
22
23
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
23
24
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
24
25
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
25
26
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
26
27
|
import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
27
28
|
import { useCompanyProfile } from "../hooks/use-company-profile";
|
|
28
|
-
import { CREDIT_ESTIMATES, lines, listOf, statusClass, text } from "../lib/campaigns-utils";
|
|
29
|
+
import { CREDIT_ESTIMATES, lines, listOf, personaFormOf, personaPatch, statusClass, text } from "../lib/campaigns-utils";
|
|
29
30
|
import { FormField } from "./form-field";
|
|
30
31
|
|
|
31
32
|
export interface PersonaEditorProps {
|
|
@@ -33,6 +34,10 @@ export interface PersonaEditorProps {
|
|
|
33
34
|
websiteUrl?: string;
|
|
34
35
|
/** Called after any change, so a wizard can refresh its persona choices. */
|
|
35
36
|
onChange?: () => void;
|
|
37
|
+
/** Called when a persona's title is clicked (a screen opens its detail). Without it the title is plain text. */
|
|
38
|
+
onOpen?: (persona: { id: string; title?: string | null }) => void;
|
|
39
|
+
/** Change it to reload the list (the detail edited or archived a persona). */
|
|
40
|
+
refreshKey?: number;
|
|
36
41
|
className?: string;
|
|
37
42
|
}
|
|
38
43
|
|
|
@@ -61,16 +66,14 @@ interface Suggestion {
|
|
|
61
66
|
interface PersonaForm { title: string; why: string; signals: string; approach: string; goal: string; priority: string; confidence: string }
|
|
62
67
|
const emptyForm: PersonaForm = { title: "", why: "", signals: "", approach: "", goal: "", priority: "1", confidence: "medium" };
|
|
63
68
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
};
|
|
69
|
-
}
|
|
69
|
+
const formOf = (p: PersonaRow): PersonaForm => personaFormOf(p as Record<string, any>);
|
|
70
|
+
|
|
71
|
+
/** The persona API has no paging: a full read is announced under the table. */
|
|
72
|
+
const PERSONA_LIMIT = 100;
|
|
70
73
|
|
|
71
|
-
export function PersonaEditor({ websiteUrl, onChange, className }: PersonaEditorProps) {
|
|
74
|
+
export function PersonaEditor({ websiteUrl, onChange, onOpen, refreshKey, className }: PersonaEditorProps) {
|
|
72
75
|
const profile = useCompanyProfile(websiteUrl);
|
|
73
|
-
const personas = useG8Query((signal) => g8Get("/personas", { limit:
|
|
76
|
+
const personas = useG8Query((signal) => g8Get("/personas", { limit: PERSONA_LIMIT }, { signal }), [refreshKey]);
|
|
74
77
|
const [editing, setEditing] = useState<PersonaRow | "new" | null>(null);
|
|
75
78
|
const [form, setForm] = useState<PersonaForm>(emptyForm);
|
|
76
79
|
const [archiving, setArchiving] = useState<PersonaRow | null>(null);
|
|
@@ -81,11 +84,15 @@ export function PersonaEditor({ websiteUrl, onChange, className }: PersonaEditor
|
|
|
81
84
|
|
|
82
85
|
const done = () => { void personas.refetch(); onChange?.(); };
|
|
83
86
|
const save = useG8Mutation(async () => {
|
|
87
|
+
if (editing && editing !== "new") {
|
|
88
|
+
const patch = personaPatch(editing as Record<string, any>, form);
|
|
89
|
+
return Object.keys(patch).length > 0 ? g8Patch(`/personas/${editing.id}`, patch) : undefined; // nothing changed: no request
|
|
90
|
+
}
|
|
84
91
|
const body = {
|
|
85
92
|
title: form.title.trim(), why_target: form.why, key_signals: lines(form.signals), campaign_approach: form.approach,
|
|
86
93
|
recommended_goal: form.goal, priority: Number(form.priority) || 1, confidence: form.confidence,
|
|
87
94
|
};
|
|
88
|
-
return
|
|
95
|
+
return g8Post("/personas", { ...body, website_url: profile.websiteUrl });
|
|
89
96
|
}, { onSuccess: () => { setEditing(null); done(); } });
|
|
90
97
|
const archive = useG8Mutation(async (p: PersonaRow) => {
|
|
91
98
|
setBusyId(p.id);
|
|
@@ -143,7 +150,7 @@ export function PersonaEditor({ websiteUrl, onChange, className }: PersonaEditor
|
|
|
143
150
|
{personas.loading && !personas.data ? <TableRow><TableCell colSpan={5}><Skeleton className="h-6 w-full" /></TableCell></TableRow> : null}
|
|
144
151
|
{rows.map((p) => (
|
|
145
152
|
<TableRow key={p.id}>
|
|
146
|
-
<TableCell className="font-medium">{p.title}</TableCell>
|
|
153
|
+
<TableCell className="font-medium">{onOpen ? <button type="button" className="text-left hover:underline" aria-label={`Open ${p.title}`} onClick={() => onOpen(p)}>{p.title}</button> : p.title}</TableCell>
|
|
147
154
|
<TableCell className="max-w-md truncate text-muted-foreground">{p.why_target}</TableCell>
|
|
148
155
|
<TableCell>{p.confidence}</TableCell>
|
|
149
156
|
<TableCell><span className={`rounded px-2 py-0.5 text-xs ${statusClass(p.status)}`}>{p.status ?? "active"}</span></TableCell>
|
|
@@ -155,6 +162,7 @@ export function PersonaEditor({ websiteUrl, onChange, className }: PersonaEditor
|
|
|
155
162
|
))}
|
|
156
163
|
</TableBody>
|
|
157
164
|
</Table>
|
|
165
|
+
<FirstNotice shown={rows.length} limit={PERSONA_LIMIT} noun="personas" />
|
|
158
166
|
{!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
167
|
|
|
160
168
|
<Dialog open={editing !== null} onOpenChange={(o) => { if (!o) setEditing(null); }}>
|
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
/** Types and helpers shared by the campaign components. Payloads are open-shaped: every field is optional. */
|
|
4
4
|
|
|
5
|
+
import { DEFAULT_BRAND } from "@/g8/hooks/use-brand";
|
|
6
|
+
|
|
5
7
|
/** Where the person manages mailboxes. Google and Microsoft OAuth consent can only run in the graph8 app. */
|
|
6
|
-
|
|
8
|
+
/** The path on the platform's web app; components resolve it against the brand (`brandUrl(useBrand(), ...)`). */
|
|
9
|
+
export const GRAPH8_APP_MAILBOXES_PATH = "/studio/settings?tab=purchase&view=mailboxes";
|
|
10
|
+
/** The default (graph8) URL. Kept for existing imports; prefer GRAPH8_APP_MAILBOXES_PATH with useBrand. */
|
|
11
|
+
export const GRAPH8_APP_MAILBOXES_URL = `${DEFAULT_BRAND.appUrl}${GRAPH8_APP_MAILBOXES_PATH}`;
|
|
7
12
|
|
|
8
13
|
/**
|
|
9
14
|
* Rough credit estimates shown in ConfirmSpendDialog. The API has no price read and no balance read, so these
|
|
@@ -93,10 +98,10 @@ export function commaList(value: string): string[] {
|
|
|
93
98
|
|
|
94
99
|
export function statusClass(status: string | null | undefined): string {
|
|
95
100
|
const s = (status ?? "").toLowerCase();
|
|
96
|
-
if (["active", "live", "completed", "approved", "complete", "done"].includes(s)) return "bg-
|
|
97
|
-
if (["draft", "pending", "running", "generating", "in_progress", "copy_in_progress", "scheduling"].includes(s)) return "bg-
|
|
98
|
-
if (s) return "bg-
|
|
99
|
-
return "bg-
|
|
101
|
+
if (["active", "live", "completed", "approved", "complete", "done"].includes(s)) return "bg-positive text-positive-foreground";
|
|
102
|
+
if (["draft", "pending", "running", "generating", "in_progress", "copy_in_progress", "scheduling"].includes(s)) return "bg-warning text-warning-foreground";
|
|
103
|
+
if (s) return "bg-error text-error-foreground";
|
|
104
|
+
return "bg-muted text-foreground";
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
/** Timestamps are shown in UTC so the same value reads the same everywhere. */
|
|
@@ -150,6 +155,134 @@ export function audienceOf(envelope: unknown): { listId: string | null; name: st
|
|
|
150
155
|
return { listId: id == null || id === "" ? null : String(id), name: text(a.name) || text(a.title), size };
|
|
151
156
|
}
|
|
152
157
|
|
|
158
|
+
/** Document types that hold the sequence itself, not copy. The launch counts every other completed document as copy. */
|
|
159
|
+
export const NON_COPY_FILE_TYPES = new Set(["sequence", "step_catalog"]);
|
|
160
|
+
|
|
161
|
+
/** The email copy document a launch reads, and where the generator files it. */
|
|
162
|
+
export const EMAILS_DOCUMENT = { file_type: "emails", display_name: "Emails", folder_path: "campaign_copy/channels/email" } as const;
|
|
163
|
+
|
|
164
|
+
const GENERATING = new Set(["generating", "pending", "running", "in_progress", "queued", "processing", "regenerating"]);
|
|
165
|
+
|
|
166
|
+
export interface CopyState {
|
|
167
|
+
/** False when the payload has no `documents` list (an older API): readiness is then unknown, not missing. */
|
|
168
|
+
known: boolean;
|
|
169
|
+
/** At least one completed copy document: the launch's server-side check passes. */
|
|
170
|
+
ready: boolean;
|
|
171
|
+
/** A copy document is still being written, or the campaign is still in `copy_in_progress`. */
|
|
172
|
+
generating: boolean;
|
|
173
|
+
/** The existing email copy document, so writing the copy reuses it instead of creating a second one. */
|
|
174
|
+
emailsDocId: string | null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Copy readiness from a GET /campaigns/{id}/full payload. Mirrors the launch's own check: a launch is refused
|
|
179
|
+
* (400 "no completed copy documents") unless some document other than the sequence and step catalog is completed.
|
|
180
|
+
*/
|
|
181
|
+
export function copyStateOf(envelope: unknown): CopyState {
|
|
182
|
+
const d = objectOf(envelope);
|
|
183
|
+
if (!Array.isArray(d.documents)) return { known: false, ready: false, generating: false, emailsDocId: null };
|
|
184
|
+
const copy = (d.documents as CampaignDocRow[]).filter((doc) => doc && !NON_COPY_FILE_TYPES.has(text(doc.file_type)));
|
|
185
|
+
const ready = copy.some((doc) => text(doc.status).toLowerCase() === "completed");
|
|
186
|
+
const generating = !ready && (copy.some((doc) => GENERATING.has(text(doc.status).toLowerCase())) || text(d.status) === "copy_in_progress");
|
|
187
|
+
const emails = copy.find((doc) => doc.file_type === EMAILS_DOCUMENT.file_type);
|
|
188
|
+
return { known: true, ready, generating, emailsDocId: emails ? String(emails.id) : null };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** New mailboxes send nothing for their first 14 days (the sequencer's warm-up hold, on by default). */
|
|
192
|
+
export const WARMUP_HOLD_DAYS = 14;
|
|
193
|
+
|
|
194
|
+
/** When a mailbox added less than 14 days ago may start sending, or null when it is past the hold (or unknown). */
|
|
195
|
+
export function warmupHoldUntil(createdAt: string | null | undefined, now: Date = new Date()): Date | null {
|
|
196
|
+
if (!createdAt) return null;
|
|
197
|
+
const created = new Date(createdAt.includes("T") ? createdAt : createdAt.replace(" ", "T"));
|
|
198
|
+
if (Number.isNaN(created.getTime())) return null;
|
|
199
|
+
const until = new Date(created.getTime() + WARMUP_HOLD_DAYS * 24 * 60 * 60 * 1000);
|
|
200
|
+
return until > now ? until : null;
|
|
201
|
+
}
|
|
202
|
+
|
|
153
203
|
export function byteLength(s: string): number {
|
|
154
204
|
return typeof Blob !== "undefined" ? new Blob([s]).size : s.length;
|
|
155
205
|
}
|
|
206
|
+
|
|
207
|
+
const HIDDEN_FIELDS = /password|secret|token|credential/i;
|
|
208
|
+
|
|
209
|
+
/** Every readable field of an open-shaped record as label/value pairs (secrets are never shown). */
|
|
210
|
+
export function fieldItems(obj: Record<string, unknown>): Array<{ label: string; value: string }> {
|
|
211
|
+
return Object.entries(obj)
|
|
212
|
+
.filter(([k, v]) => !HIDDEN_FIELDS.test(k) && v !== null && v !== undefined && v !== "")
|
|
213
|
+
.map(([k, v]) => {
|
|
214
|
+
const label = k.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
|
|
215
|
+
if (typeof v === "string" && /_at$|date$/.test(k) && !Number.isNaN(new Date(v).getTime())) return { label, value: formatDate(v) };
|
|
216
|
+
if (typeof v === "boolean") return { label, value: v ? "Yes" : "No" };
|
|
217
|
+
if (Array.isArray(v) && v.every((x) => typeof x === "string")) return { label, value: v.join(", ") };
|
|
218
|
+
if (typeof v === "object") return { label, value: JSON.stringify(v) };
|
|
219
|
+
return { label, value: String(v) };
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const isTextItem = (s: unknown): boolean => typeof s === "string" || typeof s === "number";
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The stored list after the person edited its text lines: items the text box cannot show or edit (objects, nested
|
|
227
|
+
* lists) stay exactly where they were; the text items take the edited lines in order; extra lines go at the end.
|
|
228
|
+
*/
|
|
229
|
+
function mergeList(original: unknown, edited: string[]): unknown[] {
|
|
230
|
+
const out: unknown[] = [];
|
|
231
|
+
let next = 0;
|
|
232
|
+
for (const item of Array.isArray(original) ? original : []) {
|
|
233
|
+
if (!isTextItem(item)) out.push(item);
|
|
234
|
+
else if (next < edited.length) out.push(edited[next++]);
|
|
235
|
+
}
|
|
236
|
+
return [...out, ...edited.slice(next)];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface IcpFormValues { name: string; description: string; industries: string; size: string; regions: string; signals: string }
|
|
240
|
+
export interface PersonaFormValues { title: string; why: string; signals: string; approach: string; goal: string; priority: string; confidence: string }
|
|
241
|
+
|
|
242
|
+
const csv = (v: unknown): string => (Array.isArray(v) ? v.join(", ") : text(v));
|
|
243
|
+
const textLines = (list: unknown): string => (Array.isArray(list) ? list.map((s) => text(s)).filter(Boolean).join("\n") : "");
|
|
244
|
+
|
|
245
|
+
/** The edit form for a stored ICP. */
|
|
246
|
+
export function icpFormOf(icp: Record<string, any>): IcpFormValues {
|
|
247
|
+
const f = (icp.firmographics ?? {}) as Record<string, unknown>;
|
|
248
|
+
return { name: icp.name ?? "", description: icp.description ?? "", industries: csv(f.industries), size: text(f.company_size), regions: csv(f.regions), signals: textLines(icp.buying_signals) };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* PATCH body for an ICP edit: only what the person changed. The API replaces each key it receives, so an untouched
|
|
253
|
+
* list or object is never re-sent (structured buying signals or firmographics the form cannot show survive), and
|
|
254
|
+
* an edited signals list keeps the items the text box could not show.
|
|
255
|
+
*/
|
|
256
|
+
export function icpPatch(icp: Record<string, any>, form: IcpFormValues): Record<string, unknown> {
|
|
257
|
+
const before = icpFormOf(icp);
|
|
258
|
+
const out: Record<string, unknown> = {};
|
|
259
|
+
if (form.name.trim() !== before.name.trim()) out.name = form.name.trim();
|
|
260
|
+
if (form.description !== before.description) out.description = form.description;
|
|
261
|
+
const firmographics: Record<string, unknown> = { ...(icp.firmographics ?? {}) };
|
|
262
|
+
let touched = false;
|
|
263
|
+
if (form.industries !== before.industries) { firmographics.industries = commaList(form.industries); touched = true; }
|
|
264
|
+
if (form.size !== before.size) { firmographics.company_size = form.size; touched = true; }
|
|
265
|
+
if (form.regions !== before.regions) { firmographics.regions = commaList(form.regions); touched = true; }
|
|
266
|
+
if (touched) out.firmographics = firmographics;
|
|
267
|
+
if (form.signals !== before.signals) out.buying_signals = mergeList(icp.buying_signals, lines(form.signals));
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** The edit form for a stored persona. */
|
|
272
|
+
export function personaFormOf(p: Record<string, any>): PersonaFormValues {
|
|
273
|
+
return { title: p.title ?? "", why: p.why_target ?? "", approach: p.campaign_approach ?? "", goal: p.recommended_goal ?? "", priority: String(p.priority ?? 1), confidence: p.confidence ?? "medium", signals: textLines(p.key_signals) };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** PATCH body for a persona edit: only the changed keys (an untouched `key_signals` list is never re-sent). */
|
|
277
|
+
export function personaPatch(p: Record<string, any>, form: PersonaFormValues): Record<string, unknown> {
|
|
278
|
+
const before = personaFormOf(p);
|
|
279
|
+
const out: Record<string, unknown> = {};
|
|
280
|
+
if (form.title.trim() !== before.title.trim()) out.title = form.title.trim();
|
|
281
|
+
if (form.why !== before.why) out.why_target = form.why;
|
|
282
|
+
if (form.signals !== before.signals) out.key_signals = mergeList(p.key_signals, lines(form.signals));
|
|
283
|
+
if (form.approach !== before.approach) out.campaign_approach = form.approach;
|
|
284
|
+
if (form.goal !== before.goal) out.recommended_goal = form.goal;
|
|
285
|
+
if (form.priority !== before.priority) out.priority = Number(form.priority) || 1;
|
|
286
|
+
if (form.confidence !== before.confidence) out.confidence = form.confidence;
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
* first. Readiness needs a non-empty audience, completed copy, at least one complete step and an ACTIVE
|
|
6
6
|
* email mailbox (Google and Microsoft mailboxes are connected by the person in the graph8 app). An empty body
|
|
7
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`
|
|
8
|
+
* `getCampaignFull` (plain `getCampaign` omits the audience): `is_launched` (true only for a live launch, not a
|
|
9
|
+
* failed attempt) means do not launch again; copy is ready when a document other than `sequence` and
|
|
10
|
+
* `step_catalog` has `status: "completed"` (the launch refuses with 400 otherwise). The deliverability verdict is
|
|
11
|
+
* `safe` (older responses: `safe_to_send`). If
|
|
9
12
|
* launch fails with 502 `draft_created_activation_failed` or `activation_state_uncertain`, a sequence may
|
|
10
13
|
* exist: check it before retrying, never repeat blindly.
|
|
11
14
|
*/
|
|
@@ -17,13 +20,23 @@ export async function attachAndLaunch(campaignId: string, listId: number, sender
|
|
|
17
20
|
await g8.api.gtmCampaigns.attachAudienceToCampaign({ path: { campaign_id: campaignId }, body: { list_id: String(listId) } });
|
|
18
21
|
|
|
19
22
|
const verdict = await g8.api.deliverability.getSafeToSend();
|
|
20
|
-
const
|
|
23
|
+
const v = (verdict.data ?? {}) as { safe?: boolean; safe_to_send?: boolean };
|
|
24
|
+
const safe = v.safe ?? v.safe_to_send;
|
|
21
25
|
if (safe === false) return { launched: false as const, reason: "deliverability says do not send" };
|
|
22
26
|
|
|
23
27
|
const full = await g8.api.gtmCampaigns.getCampaignFull({ path: { campaign_id: campaignId } });
|
|
24
|
-
const state = (full.data ?? {}) as {
|
|
28
|
+
const state = (full.data ?? {}) as {
|
|
29
|
+
is_launched?: boolean;
|
|
30
|
+
audience?: { item_count?: number };
|
|
31
|
+
sequence?: { steps?: unknown[] };
|
|
32
|
+
documents?: Array<{ file_type?: string | null; status?: string | null }>;
|
|
33
|
+
};
|
|
25
34
|
if (state.is_launched) return { launched: false as const, reason: "already launched" };
|
|
26
35
|
if (!state.sequence?.steps?.length) return { launched: false as const, reason: "the campaign has no steps" };
|
|
36
|
+
const copyReady = (state.documents ?? []).some(
|
|
37
|
+
(d) => d.status === "completed" && !["sequence", "step_catalog"].includes(String(d.file_type)),
|
|
38
|
+
);
|
|
39
|
+
if (!copyReady) return { launched: false as const, reason: "the campaign has no completed copy yet" };
|
|
27
40
|
|
|
28
41
|
const launched = await g8.api.gtmCampaigns.launchCampaign({
|
|
29
42
|
path: { campaign_id: campaignId },
|
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
* @when Starting a campaign from the company profile. `generateCampaignIdeasAsync` is BILLABLE (it scales with
|
|
4
4
|
* `num_ideas`): a human approves the spend first. It returns a `task_id`; there are no streams, so poll
|
|
5
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
|
|
7
|
-
* writing the documents.
|
|
6
|
+
* `external` in the contract but contacts nobody; with `auto_generate_documents: true` (the default here) it also
|
|
7
|
+
* spends credits writing the documents. Keep it on: a campaign with no completed copy document is refused at launch
|
|
8
|
+
* (400 "no completed copy documents"). Pass `autoGenerateDocuments: false` only when you write the copy yourself
|
|
9
|
+
* (`createCampaignDocument` with `status: "completed"`). `generation_status: "failed_to_dispatch"` in the response
|
|
10
|
+
* means the writing never started: regenerate the documents before launching. Store the returned campaign id at
|
|
11
|
+
* once: a retry after a later failure must reuse it.
|
|
8
12
|
*/
|
|
9
13
|
import { g8 } from "@graph8/sdk";
|
|
10
14
|
|
|
@@ -34,7 +38,7 @@ export async function generateIdeas(websiteUrl: string, goal: "awareness" | "con
|
|
|
34
38
|
throw new Error(`idea task ${taskId} did not finish in time`);
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
export async function createCampaignFromIdea(idea: Idea, listId?: number) {
|
|
41
|
+
export async function createCampaignFromIdea(idea: Idea, listId?: number, autoGenerateDocuments = true) {
|
|
38
42
|
const created = await g8.api.gtmCampaigns.createCampaign({
|
|
39
43
|
body: {
|
|
40
44
|
name: idea.campaign_name ?? "New campaign",
|
|
@@ -44,7 +48,7 @@ export async function createCampaignFromIdea(idea: Idea, listId?: number) {
|
|
|
44
48
|
brief: idea.why_this_works,
|
|
45
49
|
target_channels: ["email"],
|
|
46
50
|
audience_list_id: listId == null ? undefined : String(listId),
|
|
47
|
-
auto_generate_documents:
|
|
51
|
+
auto_generate_documents: autoGenerateDocuments,
|
|
48
52
|
},
|
|
49
53
|
});
|
|
50
54
|
return String(created.data?.id);
|
package/blocks/coach/README.md
CHANGED
|
@@ -5,6 +5,14 @@ the `dialer` block (the optional "Real calls" tab reuses its `CallLog`).
|
|
|
5
5
|
|
|
6
6
|
Install: `npx @graph8/sdk add coach` (installs `foundation`, `lists`, `crm` and `dialer` first).
|
|
7
7
|
|
|
8
|
+
## Screens
|
|
9
|
+
|
|
10
|
+
`CoachScreen` (`components/coach-screen.tsx`) needs no props. **Practice calls**: stats (calls, average and best score,
|
|
11
|
+
weakest stage), a feed with the first tip per call, scorecard / recording / transcript detail (`?id=`), delete (confirmed,
|
|
12
|
+
hide with `allowDelete={false}`), and a New practice call form (adds the row; the live conversation runs in graph8).
|
|
13
|
+
**Leaderboard** ranks reps. **Real calls** shows the rep's dialer calls once a rep email is set; managers type a rep
|
|
14
|
+
email in the toolbar (needs a manager key). Slots: `toolbarActions`, `renderRowActions`, `renderDetailActions`, `extraDetailTabs`.
|
|
15
|
+
|
|
8
16
|
## What you get
|
|
9
17
|
|
|
10
18
|
| Component | Use it for | Embed props |
|
|
@@ -12,6 +20,8 @@ Install: `npx @graph8/sdk add coach` (installs `foundation`, `lists`, `crm` and
|
|
|
12
20
|
| `CallScorecard` | One graded call: overall score, stages with checklist verdicts, strengths, improvements, recording, transcript | `callId`, `allowDelete`, `onDeleted` |
|
|
13
21
|
| `CoachingFeed` | Practice calls newest first with the first tip, a leaderboard tab, optional real-calls tab | `userEmail`, `onOpenCall`, `allowCreate`, `showDialerCalls`, `allowDelete`, `pageSize` |
|
|
14
22
|
|
|
23
|
+
Every detail opens as a full page with `&view=full` (the Open full page button).
|
|
24
|
+
|
|
15
25
|
## The flow, with the ids that pass between steps
|
|
16
26
|
|
|
17
27
|
1. **Feed**: `GET /sales-coach/calls?email=` returns the calls (newest first) with `scores` and transcript
|
package/blocks/coach/block.json
CHANGED
|
@@ -55,6 +55,20 @@
|
|
|
55
55
|
"read",
|
|
56
56
|
"write"
|
|
57
57
|
]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"name": "CoachScreen",
|
|
61
|
+
"file": "components/coach-screen.tsx",
|
|
62
|
+
"purpose": "Finished sales coach page: practice calls with scorecards, leaderboard, real calls and a new-call form.",
|
|
63
|
+
"operations": [
|
|
64
|
+
"create_call_sales_coach_calls_post",
|
|
65
|
+
"get_sales_coach_leaderboard_sales_coach_leaderboard_get",
|
|
66
|
+
"list_calls_sales_coach_calls_get"
|
|
67
|
+
],
|
|
68
|
+
"tiers": [
|
|
69
|
+
"read",
|
|
70
|
+
"write"
|
|
71
|
+
]
|
|
58
72
|
}
|
|
59
73
|
],
|
|
60
74
|
"hooks": [],
|
|
@@ -120,6 +134,7 @@
|
|
|
120
134
|
}
|
|
121
135
|
],
|
|
122
136
|
"peerDependencies": {
|
|
123
|
-
"react": ">=18"
|
|
137
|
+
"react": ">=18",
|
|
138
|
+
"lucide-react": ">=0.400.0"
|
|
124
139
|
}
|
|
125
140
|
}
|
|
@@ -34,7 +34,7 @@ export interface CallScorecardProps {
|
|
|
34
34
|
className?: string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const VERDICT_STYLE: Record<string, string> = { met: "text-
|
|
37
|
+
const VERDICT_STYLE: Record<string, string> = { met: "text-positive-foreground", partial: "text-warning-foreground", missed: "text-destructive" };
|
|
38
38
|
|
|
39
39
|
export function CallScorecard({ callId, onDeleted, allowDelete = true, className }: CallScorecardProps) {
|
|
40
40
|
const [audio, setAudio] = useState<{ callId: string; url: string } | null>(null);
|
|
@@ -67,13 +67,13 @@ export function CallScorecard({ callId, onDeleted, allowDelete = true, className
|
|
|
67
67
|
<div className={`flex flex-col gap-4 ${className ?? ""}`}>
|
|
68
68
|
<header className="flex flex-wrap items-start justify-between gap-3">
|
|
69
69
|
<div>
|
|
70
|
-
<h2 className="text-lg font-
|
|
70
|
+
<h2 className="text-lg font-medium">{row.prospect_name || "Practice call"}{row.prospect_company ? ` at ${row.prospect_company}` : ""}</h2>
|
|
71
71
|
<p className="text-sm text-muted-foreground">
|
|
72
72
|
{[row.user_email, row.call_mode, row.call_status, row.duration_ms ? clock(row.duration_ms) : null, row.date_of_call ? new Date(row.date_of_call).toLocaleDateString() : null].filter(Boolean).join(" - ")}
|
|
73
73
|
</p>
|
|
74
74
|
</div>
|
|
75
75
|
<div className="text-right">
|
|
76
|
-
<div data-testid="overall-score" className="text-3xl font-
|
|
76
|
+
<div data-testid="overall-score" className="text-3xl font-medium">{card?.scorePct != null ? `${Math.round(card.scorePct)}%` : "-"}</div>
|
|
77
77
|
<div className="text-xs text-muted-foreground">{card ? "overall" : "not graded yet"}</div>
|
|
78
78
|
</div>
|
|
79
79
|
</header>
|
|
@@ -94,20 +94,20 @@ export function CallScorecard({ callId, onDeleted, allowDelete = true, className
|
|
|
94
94
|
))}
|
|
95
95
|
</ul>
|
|
96
96
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
97
|
-
<div><h3 className="mb-1 text-sm font-
|
|
98
|
-
<div><h3 className="mb-1 text-sm font-
|
|
97
|
+
<div><h3 className="mb-1 text-sm font-medium">Strengths</h3><ul className="list-disc pl-5 text-sm">{card.strengths.map((t, i) => <li key={i}>{t}</li>)}</ul></div>
|
|
98
|
+
<div><h3 className="mb-1 text-sm font-medium">To improve</h3><ul className="list-disc pl-5 text-sm">{card.improvements.map((t, i) => <li key={i}>{t}</li>)}</ul></div>
|
|
99
99
|
</div>
|
|
100
100
|
</>
|
|
101
101
|
)}
|
|
102
102
|
|
|
103
103
|
<div className="flex flex-col gap-2">
|
|
104
|
-
<h3 className="text-sm font-
|
|
104
|
+
<h3 className="text-sm font-medium">Recording</h3>
|
|
105
105
|
{row.audio_url ? (shownAudio ? <audio controls src={shownAudio} className="w-full" /> : <div><Button size="sm" variant="outline" loading={loadAudio.loading} onClick={() => void loadAudio.mutate(callId)}>Load recording</Button></div>) : <p className="text-sm text-muted-foreground">No recording.</p>}
|
|
106
106
|
{loadAudio.error ? <ApiError error={loadAudio.error} title="Could not load the recording" /> : null}
|
|
107
107
|
</div>
|
|
108
108
|
|
|
109
109
|
<div className="flex flex-col gap-2">
|
|
110
|
-
<h3 className="text-sm font-
|
|
110
|
+
<h3 className="text-sm font-medium">Transcript</h3>
|
|
111
111
|
{transcript.error ? <ApiError error={transcript.error} title="Could not load the transcript" onRetry={() => void transcript.refetch()} /> : null}
|
|
112
112
|
<ul className="flex max-h-96 flex-col gap-1 overflow-y-auto" data-testid="transcript">
|
|
113
113
|
{segments.map((s, i) => (
|