@graph8/sdk 0.370.0 → 0.374.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 +22 -3
- package/blocks/_foundation/README.md +61 -0
- package/blocks/_foundation/block.json +57 -0
- package/blocks/_foundation/components/auth-gate.tsx +363 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- 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 +1 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/auth.ts +815 -0
- package/blocks/agents/README.md +81 -4
- package/blocks/agents/block.json +311 -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 +17 -3
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +20 -3
- package/blocks/agents/components/agent-runs.tsx +3 -3
- 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 +53 -11
- 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/components/analytics-screen.tsx +16 -3
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/report-builder.tsx +15 -2
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +27 -0
- package/blocks/appointments/components/booking-page.tsx +2 -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 +29 -13
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/campaigns/README.md +1 -1
- package/blocks/campaigns/block.json +34 -1
- package/blocks/campaigns/components/campaign-assets.tsx +1 -1
- package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
- package/blocks/campaigns/components/campaign-links.tsx +180 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
- package/blocks/campaigns/components/icp-detail.tsx +11 -2
- package/blocks/campaigns/components/icp-editor.tsx +8 -3
- package/blocks/campaigns/components/persona-detail.tsx +11 -2
- package/blocks/campaigns/components/persona-editor.tsx +6 -1
- package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- 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 +165 -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/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +1 -1
- package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
- package/blocks/crm/README.md +25 -0
- package/blocks/crm/block.json +257 -9
- package/blocks/crm/components/bulk-update.tsx +119 -0
- package/blocks/crm/components/companies-screen.tsx +23 -9
- package/blocks/crm/components/company-detail.tsx +2 -2
- package/blocks/crm/components/contact-detail.tsx +4 -4
- package/blocks/crm/components/contact-documents-tab.tsx +144 -0
- package/blocks/crm/components/contact-insights-tab.tsx +118 -0
- package/blocks/crm/components/contact-lookup-link.tsx +85 -0
- package/blocks/crm/components/contact-row-actions.tsx +43 -0
- package/blocks/crm/components/contact-tag-chips.tsx +113 -0
- package/blocks/crm/components/contacts-screen.tsx +77 -18
- package/blocks/crm/components/dedupe-review.tsx +6 -1
- package/blocks/crm/components/import-wizard.tsx +3 -7
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/owner-assign.tsx +132 -0
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +1 -1
- package/blocks/crm/components/record-open-links.tsx +79 -0
- package/blocks/crm/components/tag-assign.tsx +112 -0
- package/blocks/crm/components/tag-manager.tsx +178 -0
- package/blocks/crm/components/tasks-screen.tsx +35 -15
- package/blocks/crm/components/unlock-info-button.tsx +82 -0
- package/blocks/crm/components/verify-email-button.tsx +93 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-contact-tags.ts +29 -0
- package/blocks/crm/hooks/use-page-query.ts +3 -1
- package/blocks/crm/lib/crm-types.ts +14 -0
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/record-actions.ts +102 -0
- package/blocks/deals/README.md +25 -6
- package/blocks/deals/block.json +168 -2
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/create-deal-button.tsx +109 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +7 -5
- 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 +30 -9
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +57 -22
- 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 +1 -1
- package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +14 -5
- package/blocks/dialer/block.json +17 -2
- package/blocks/dialer/components/call-contact-button.tsx +26 -9
- package/blocks/dialer/components/call-log-screen.tsx +10 -2
- package/blocks/dialer/components/call-log.tsx +16 -4
- package/blocks/dialer/components/call-queue.tsx +3 -3
- package/blocks/dialer/components/dialer-screen.tsx +69 -27
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +58 -11
- package/blocks/dialer/components/softphone.tsx +1 -1
- package/blocks/documents/README.md +86 -0
- package/blocks/documents/block.json +378 -0
- package/blocks/documents/components/account-bar.tsx +48 -0
- package/blocks/documents/components/correspondence-form.tsx +95 -0
- package/blocks/documents/components/correspondence-import.tsx +106 -0
- package/blocks/documents/components/correspondence-panel.tsx +191 -0
- package/blocks/documents/components/documents-screen.tsx +76 -0
- package/blocks/documents/components/library-document-detail.tsx +175 -0
- package/blocks/documents/components/library-document-form.tsx +105 -0
- package/blocks/documents/components/library-folder-form.tsx +74 -0
- package/blocks/documents/components/library-panel.tsx +235 -0
- package/blocks/documents/components/library-upload.tsx +160 -0
- package/blocks/documents/components/question-row.tsx +130 -0
- package/blocks/documents/components/questionnaire-detail.tsx +197 -0
- package/blocks/documents/components/questionnaires-panel.tsx +173 -0
- package/blocks/documents/components/record-documents-tab.tsx +109 -0
- package/blocks/documents/lib/documents-types.ts +252 -0
- package/blocks/documents/lib/download.ts +64 -0
- package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
- package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
- package/blocks/documents/recipes/organize-the-library.ts +25 -0
- package/blocks/email-signatures/README.md +123 -0
- package/blocks/email-signatures/block.json +341 -0
- package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
- package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
- package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
- package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
- package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
- package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
- package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
- package/blocks/email-signatures/components/signature-policy.tsx +130 -0
- package/blocks/email-signatures/components/signature-preview.tsx +119 -0
- package/blocks/email-signatures/components/signature-report.tsx +77 -0
- package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
- package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
- package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
- package/blocks/email-signatures/lib/signatures-types.ts +392 -0
- package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
- package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
- package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
- package/blocks/enrichment/README.md +38 -10
- package/blocks/enrichment/block.json +164 -12
- package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
- package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
- package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
- package/blocks/enrichment/components/enrich-button.tsx +1 -1
- package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
- package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
- package/blocks/enrichment/components/list-pipelines.tsx +97 -0
- package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
- package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
- package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
- package/blocks/enrichment/components/waterfall-config.tsx +27 -7
- package/blocks/enrichment/lib/enrichment-types.ts +51 -0
- package/blocks/enrichment/lib/pipeline-types.ts +234 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/forms/README.md +10 -0
- package/blocks/forms/block.json +107 -0
- 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 +1 -1
- package/blocks/forms/components/form-embed.tsx +1 -1
- 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 +57 -20
- 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 +15 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +45 -4
- package/blocks/inbox/block.json +145 -8
- package/blocks/inbox/components/blocked-senders.tsx +103 -0
- package/blocks/inbox/components/compose-message.tsx +193 -0
- package/blocks/inbox/components/inbox-screen.tsx +54 -6
- package/blocks/inbox/components/inbox-search.tsx +106 -0
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/tag-manager.tsx +136 -0
- package/blocks/inbox/components/thread-actions.tsx +133 -0
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +3 -1
- package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
- package/blocks/inbox/lib/inbox-manage.ts +227 -0
- package/blocks/integrations/README.md +54 -1
- package/blocks/integrations/block.json +189 -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 +20 -1
- 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 +4 -4
- package/blocks/integrations/components/integrations-screen.tsx +20 -3
- 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 +6 -6
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +26 -7
- package/blocks/integrations/components/webhook-manager.tsx +30 -17
- 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 +5 -0
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +8 -8
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
- package/blocks/intent/lib/intent-utils.ts +4 -4
- 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/lists/README.md +7 -1
- package/blocks/lists/block.json +31 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +17 -7
- package/blocks/lists/components/list-picker.tsx +13 -3
- package/blocks/lists/components/list-table.tsx +1 -1
- package/blocks/lists/components/lists-screen.tsx +4 -2
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +27 -6
- package/blocks/lists/components/suppressions-screen.tsx +21 -12
- package/blocks/mailboxes/block.json +4 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
- package/blocks/mailboxes/components/domain-detail.tsx +10 -4
- package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
- package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
- package/blocks/mailboxes/components/warmup-health.tsx +20 -20
- package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +117 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
- 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 +38 -13
- 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/components/nurture-builder.tsx +10 -2
- package/blocks/nurture/components/nurture-enroll.tsx +6 -7
- package/blocks/nurture/components/nurture-form.tsx +5 -8
- package/blocks/nurture/components/nurture-screen.tsx +16 -5
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/objects/README.md +1 -1
- package/blocks/objects/block.json +13 -5
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-record-detail.tsx +8 -1
- package/blocks/objects/components/object-schema-editor.tsx +1 -1
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +123 -2
- 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-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/hooks/use-landing-page-paths.ts +60 -0
- 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/programmatic-pages/README.md +82 -0
- package/blocks/programmatic-pages/block.json +292 -0
- package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
- package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
- package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
- package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
- package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
- package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
- package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
- package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
- package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
- package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
- package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
- package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
- package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
- package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
- package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
- package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
- package/blocks/quotes/README.md +38 -1
- package/blocks/quotes/block.json +170 -6
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/manual-products.tsx +144 -0
- package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-documents.tsx +94 -0
- package/blocks/quotes/components/quote-evidence.tsx +95 -0
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +28 -8
- package/blocks/quotes/components/quote-related.tsx +3 -2
- package/blocks/quotes/components/quote-settings.tsx +184 -0
- package/blocks/quotes/components/quote-templates.tsx +189 -0
- package/blocks/quotes/components/quote-viewer.tsx +23 -5
- package/blocks/quotes/components/quotes-screen.tsx +15 -5
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/quotes/lib/quote-admin.ts +264 -0
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-screen.tsx +2 -2
- package/blocks/sequencer/README.md +61 -0
- package/blocks/sequencer/block.json +200 -6
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
- package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +32 -23
- package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
- package/blocks/sequencer/components/sequence-detail.tsx +10 -2
- package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
- package/blocks/sequencer/components/sequence-list.tsx +1 -1
- package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
- package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
- package/blocks/sequencer/components/sequence-stats.tsx +8 -8
- package/blocks/sequencer/components/sequence-templates.tsx +160 -0
- package/blocks/sequencer/components/sequences-screen.tsx +39 -6
- package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
- package/blocks/sequencer/components/step-editor.tsx +21 -12
- package/blocks/sequencer/components/step-templates.tsx +224 -0
- package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
- package/blocks/signals/README.md +39 -0
- package/blocks/signals/block.json +287 -0
- package/blocks/signals/components/auto-seed-panel.tsx +111 -0
- package/blocks/signals/components/listener-form.tsx +184 -0
- package/blocks/signals/components/matched-posts.tsx +195 -0
- package/blocks/signals/components/post-engagements.tsx +114 -0
- package/blocks/signals/components/record-social-tab.tsx +49 -0
- package/blocks/signals/components/social-listeners-screen.tsx +250 -0
- package/blocks/signals/components/social-settings.tsx +128 -0
- package/blocks/signals/components/source-lists.tsx +83 -0
- package/blocks/signals/components/top-engagers.tsx +68 -0
- package/blocks/signals/components/x-integration.tsx +129 -0
- package/blocks/signals/lib/signals-types.ts +228 -0
- package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
- package/blocks/signals/recipes/spend-against-caps.ts +14 -0
- package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
- package/blocks/social-ads/README.md +8 -0
- package/blocks/social-ads/block.json +137 -4
- package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
- package/blocks/social-ads/components/audience-sync.tsx +6 -2
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +6 -1
- 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 +6 -1
- package/blocks/social-ads/components/creative-gallery.tsx +20 -2
- 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 +4 -2
- package/blocks/social-ads/components/post-detail.tsx +3 -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-screen.tsx +33 -6
- 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/components/snippet-install.tsx +18 -9
- package/blocks/visitors/components/traffic-chart.tsx +7 -6
- package/blocks/visitors/components/visitor-company-card.tsx +24 -14
- package/blocks/visitors/components/visitor-feed.tsx +5 -5
- package/blocks/web-analytics/README.md +102 -0
- package/blocks/web-analytics/block.json +292 -0
- package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
- package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
- package/blocks/web-analytics/components/audience-tab.tsx +135 -0
- package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
- package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
- package/blocks/web-analytics/components/overview-tab.tsx +102 -0
- package/blocks/web-analytics/components/pages-tab.tsx +148 -0
- package/blocks/web-analytics/components/ranked-table.tsx +103 -0
- package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
- package/blocks/web-analytics/components/report-card.tsx +71 -0
- package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
- package/blocks/web-analytics/hooks/use-report.ts +48 -0
- package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
- package/blocks/web-analytics/lib/page-path.ts +14 -0
- package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
- package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
- package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
- package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
- package/blocks/webchat/components/chat-widget-config.tsx +1 -1
- package/blocks/webchat/components/conversation-list.tsx +1 -1
- package/blocks/workbench/README.md +54 -2
- package/blocks/workbench/block.json +234 -5
- package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
- package/blocks/workbench/components/workbench-automation.tsx +36 -0
- package/blocks/workbench/components/workbench-connections.tsx +136 -0
- package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
- package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
- package/blocks/workbench/components/workbench-grid.tsx +71 -8
- package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
- package/blocks/workbench/components/workbench-schedule.tsx +191 -0
- package/blocks/workbench/components/workbench-screen.tsx +65 -22
- package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
- package/blocks/workbench/components/workbench-triggers.tsx +178 -0
- package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
- package/blocks/workbench/lib/api-status.ts +8 -0
- package/blocks/workbench/lib/automation-types.ts +257 -0
- package/blocks/workflows/components/workflow-editor.tsx +1 -1
- package/dist/cli.js +744 -394
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +1 -1
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +1 -1
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/auth.d.ts +45 -0
- package/dist/types/blocks/brand.d.ts +11 -2
- package/dist/types/blocks/create.d.ts +11 -4
- package/dist/types/blocks/doctor.d.ts +1 -0
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/version.d.ts +2 -2
- package/docs/api/desk.d.ts +2 -2
- package/docs/api/desk.md +3 -3
- package/docs/api/forms.md +1 -1
- package/package.json +1 -1
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +37 -4
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
- package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
- 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 +24 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +68 -9
- 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 +8 -4
- package/stacks/clay/pages/companies.tsx.tmpl +17 -1
- package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
- package/stacks/clay/pages/lists.tsx.tmpl +24 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +16 -3
- 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 +34 -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 +23 -6
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +19 -4
- package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
- package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
- package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
- package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
- 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/sequences.tsx.tmpl +18 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
- package/stacks/hubspot.json +66 -7
- 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 +18 -4
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
- package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +22 -6
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
- 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 +18 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +38 -9
- package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
- package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +14 -2
- package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
- package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
- package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +17 -3
- package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +36 -3
- package/styles/styles.css +1 -1
- package/templates/auth/auth-route.ts.tmpl +16 -0
- package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
- package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
- package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
- package/templates/auth/g8.auth.password.ts.tmpl +18 -0
- package/templates/auth/g8.auth.session.ts.tmpl +23 -0
- package/templates/auth/middleware.ts.tmpl +14 -0
- package/templates/next-app/README.md.tmpl +5 -0
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
|
@@ -50,9 +50,9 @@ const STATES = ["", "waiting", "paused", "completed", "replied", "bounced", "ski
|
|
|
50
50
|
|
|
51
51
|
function Tile({ label, value }: { label: string; value: string | number }) {
|
|
52
52
|
return (
|
|
53
|
-
<div className="rounded border border-
|
|
54
|
-
<div className="text-xs capitalize text-
|
|
55
|
-
<div className="text-xl font-
|
|
53
|
+
<div className="rounded border border-border p-3">
|
|
54
|
+
<div className="text-xs capitalize text-muted-foreground">{label}</div>
|
|
55
|
+
<div className="text-xl font-medium">{value}</div>
|
|
56
56
|
</div>
|
|
57
57
|
);
|
|
58
58
|
}
|
|
@@ -145,7 +145,7 @@ export function SequenceStats({ sequenceId, pageSize = 25, refreshSeconds, class
|
|
|
145
145
|
<TableCell>{st.step_order}</TableCell>
|
|
146
146
|
<TableCell>{st.step_type}</TableCell>
|
|
147
147
|
<TableCell>{st.total_contacts ?? 0}</TableCell>
|
|
148
|
-
<TableCell className="text-xs text-
|
|
148
|
+
<TableCell className="text-xs text-muted-foreground">
|
|
149
149
|
{Object.entries(st.counts ?? {})
|
|
150
150
|
.filter(([, n]) => n > 0)
|
|
151
151
|
.map(([k, n]) => `${k}: ${n}`)
|
|
@@ -156,11 +156,11 @@ export function SequenceStats({ sequenceId, pageSize = 25, refreshSeconds, class
|
|
|
156
156
|
</TableBody>
|
|
157
157
|
</Table>
|
|
158
158
|
) : (
|
|
159
|
-
<p className="text-sm text-
|
|
159
|
+
<p className="text-sm text-muted-foreground">No step activity yet.</p>
|
|
160
160
|
)}
|
|
161
161
|
|
|
162
162
|
{skips.length > 0 ? (
|
|
163
|
-
<p className="text-sm text-
|
|
163
|
+
<p className="text-sm text-muted-foreground" data-testid="skip-reasons">
|
|
164
164
|
Skipped contacts, counted once each: {skips.map(([k, n]) => `${k.replace(/^skipped_/, "").replace(/_/g, " ")} ${n}`).join(", ")}
|
|
165
165
|
</p>
|
|
166
166
|
) : null}
|
|
@@ -171,7 +171,7 @@ export function SequenceStats({ sequenceId, pageSize = 25, refreshSeconds, class
|
|
|
171
171
|
Contacts in
|
|
172
172
|
<select
|
|
173
173
|
aria-label="Contact state"
|
|
174
|
-
className="ml-2 h-8 rounded border border-
|
|
174
|
+
className="ml-2 h-8 rounded border border-input px-1"
|
|
175
175
|
value={state}
|
|
176
176
|
onChange={(e) => {
|
|
177
177
|
setState(e.target.value);
|
|
@@ -201,7 +201,7 @@ export function SequenceStats({ sequenceId, pageSize = 25, refreshSeconds, class
|
|
|
201
201
|
<TableBody>
|
|
202
202
|
{rows.length === 0 ? (
|
|
203
203
|
<TableRow>
|
|
204
|
-
<TableCell colSpan={4} className="text-sm text-
|
|
204
|
+
<TableCell colSpan={4} className="text-sm text-muted-foreground">
|
|
205
205
|
{contacts.loading ? "Loading..." : "No contacts in this state."}
|
|
206
206
|
</TableCell>
|
|
207
207
|
</TableRow>
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SequenceTemplates: reusable sequence skeletons (a named list of steps). List, create, edit, archive, restore and USE
|
|
5
|
+
* them: "Use template" hands the template's steps to `onUse`, which SequencesScreen opens in the builder so you only add
|
|
6
|
+
* the audience list and the senders.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. GET /sequencer/templates/sequences (sequences.listSequenceTemplates) read (archived ones included; filtered here)
|
|
10
|
+
* 2. POST /sequencer/templates/sequences (sequences.createSequenceTemplate) write
|
|
11
|
+
* 3. PATCH /sequencer/templates/sequences/{id} (sequences.updateSequenceTemplate) write - ONLY the changed fields; the steps
|
|
12
|
+
* are sent (as the whole list, which replaces the old one) only when they changed
|
|
13
|
+
* 4. PATCH /sequencer/templates/sequences/{id}/archive (sequences.archiveSequenceTemplate) DESTRUCTIVE to archive -> ConfirmSpendDialog; restoring is a plain write
|
|
14
|
+
* A template is a COPY: using it, or changing it later, never touches a sequence built from it. The form is seeded from a
|
|
15
|
+
* LOADED template and Save stays disabled until something changed. A template can also be made from a finished sequence
|
|
16
|
+
* ("Save as template" in SequenceManageActions).
|
|
17
|
+
*/
|
|
18
|
+
import { useState } from "react";
|
|
19
|
+
import { Archive, ArchiveRestore, FilePlus2, Pencil, Plus } from "lucide-react";
|
|
20
|
+
import { Button, Input, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
21
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
23
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
24
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
25
|
+
import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
26
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
27
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
28
|
+
import { sequenceTemplates, templateBodyFromSteps, templateToDraftSteps } from "../lib/sequencer-manage";
|
|
29
|
+
import type { SequenceTemplate } from "../lib/sequencer-manage";
|
|
30
|
+
import { KIND_LABEL, KINDS, newStep, stepIssues, toApiStep } from "../lib/sequencer-utils";
|
|
31
|
+
import type { DraftStep, StepKind } from "../lib/sequencer-utils";
|
|
32
|
+
import { StepEditor } from "./step-editor";
|
|
33
|
+
|
|
34
|
+
export interface SequenceTemplatesProps {
|
|
35
|
+
/** Called when the person chooses "Use template": the template's name and an unsaved copy of its steps. */
|
|
36
|
+
onUse?: (template: { id: string; name: string; description: string; steps: DraftStep[] }) => void;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const stepsKey = (steps: DraftStep[]) => JSON.stringify(steps.map((s, i) => toApiStep(s, i + 1)));
|
|
41
|
+
|
|
42
|
+
function TemplateForm({ loaded, onDone }: { loaded: SequenceTemplate | null; onDone: (changed: boolean) => void }) {
|
|
43
|
+
const [name, setName] = useState(loaded?.name ?? "");
|
|
44
|
+
const [description, setDescription] = useState(loaded?.description ?? "");
|
|
45
|
+
const [steps, setSteps] = useState<DraftStep[]>(() => (loaded ? templateToDraftSteps(loaded) : []));
|
|
46
|
+
const stepsChanged = loaded ? stepsKey(steps) !== stepsKey(templateToDraftSteps(loaded)) : steps.length > 0;
|
|
47
|
+
const save = useG8Mutation(async () => {
|
|
48
|
+
// A loaded step keeps its own name (same position and same step type); only new or retyped steps get the automatic one.
|
|
49
|
+
const api = steps.map((s, i) => {
|
|
50
|
+
const was = loaded?.steps[i];
|
|
51
|
+
const a = toApiStep(s, i + 1);
|
|
52
|
+
return { ...a, id: undefined, name: was && was.step_type === a.step_type ? was.name : undefined };
|
|
53
|
+
});
|
|
54
|
+
const body = templateBodyFromSteps(name, description, api);
|
|
55
|
+
if (!loaded) {
|
|
56
|
+
await g8Post("/sequencer/templates/sequences", body);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const changes: Record<string, unknown> = {};
|
|
60
|
+
if (name.trim() !== loaded.name) changes.name = name.trim();
|
|
61
|
+
if (description.trim() !== loaded.description) changes.description = description.trim();
|
|
62
|
+
if (stepsChanged) changes.steps = body.steps;
|
|
63
|
+
await g8Patch(`/sequencer/templates/sequences/${encodeURIComponent(loaded.id)}`, changes);
|
|
64
|
+
}, { invalidates: ["sequences"] });
|
|
65
|
+
const dirty = loaded ? name.trim() !== loaded.name || description.trim() !== loaded.description || stepsChanged : true;
|
|
66
|
+
const valid = name.trim().length > 0 && dirty && steps.every((s) => stepIssues(s).length === 0);
|
|
67
|
+
const submit = async () => {
|
|
68
|
+
try {
|
|
69
|
+
await save.mutateAsync(undefined);
|
|
70
|
+
onDone(true);
|
|
71
|
+
} catch {
|
|
72
|
+
/* shown below */
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const add = (kind: StepKind) => setSteps((all) => [...all, newStep(kind)]);
|
|
76
|
+
return (
|
|
77
|
+
<form className="space-y-3 rounded-md border border-border p-3" aria-label={loaded ? `Edit ${loaded.name}` : "New sequence template"} onSubmit={(e) => { e.preventDefault(); if (valid) void submit(); }}>
|
|
78
|
+
<Input aria-label="Template name" placeholder="Template name" value={name} onChange={(e) => setName(e.target.value)} />
|
|
79
|
+
<Textarea aria-label="Description" rows={2} placeholder="What this sequence is for (optional)" value={description} onChange={(e) => setDescription(e.target.value)} />
|
|
80
|
+
{steps.map((s, i) => (
|
|
81
|
+
<StepEditor key={s.key} step={s} index={i + 1} onChange={(next) => setSteps((all) => all.map((x) => (x.key === s.key ? next : x)))} onRemove={() => setSteps((all) => all.filter((x) => x.key !== s.key))} />
|
|
82
|
+
))}
|
|
83
|
+
<div className="flex flex-wrap gap-2">
|
|
84
|
+
{KINDS.map((k) => <Button key={k} type="button" size="sm" variant="outline" onClick={() => add(k)}>Add {KIND_LABEL[k].toLowerCase()} step</Button>)}
|
|
85
|
+
</div>
|
|
86
|
+
{save.error ? <ApiError error={save.error} title="Could not save the template" /> : null}
|
|
87
|
+
<div className="flex justify-end gap-2">
|
|
88
|
+
<Button type="button" variant="outline" size="sm" onClick={() => onDone(false)}>Cancel</Button>
|
|
89
|
+
<Button type="submit" size="sm" disabled={!valid || save.loading}>{loaded ? "Save changes" : "Create template"}</Button>
|
|
90
|
+
</div>
|
|
91
|
+
</form>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function SequenceTemplates({ onUse, className }: SequenceTemplatesProps) {
|
|
96
|
+
const [editing, setEditing] = useState<SequenceTemplate | "new" | null>(null);
|
|
97
|
+
const [archiving, setArchiving] = useState<SequenceTemplate | null>(null);
|
|
98
|
+
const [showArchived, setShowArchived] = useState(false);
|
|
99
|
+
const [restoringId, setRestoringId] = useState<string | null>(null);
|
|
100
|
+
const list = useG8Query(async (signal) => sequenceTemplates(await g8Get("/sequencer/templates/sequences", undefined, { signal })), [], { key: ["sequences", "templates"] });
|
|
101
|
+
const archive = useG8Mutation(async (v: { template: SequenceTemplate; archived: boolean }) => {
|
|
102
|
+
await g8Patch(`/sequencer/templates/sequences/${encodeURIComponent(v.template.id)}/archive`, { is_archived: v.archived });
|
|
103
|
+
setArchiving(null);
|
|
104
|
+
}, { invalidates: ["sequences"], onError: () => setArchiving(null) });
|
|
105
|
+
|
|
106
|
+
if (list.error) return <ApiError error={list.error} title="Could not load the sequence templates" onRetry={() => void list.refetch()} className={className} />;
|
|
107
|
+
if (list.loading && !list.data) return <div aria-busy="true" className={`space-y-2 ${className ?? ""}`}><Skeleton className="h-12 w-full" /><Skeleton className="h-12 w-full" /></div>;
|
|
108
|
+
const rows = (list.data ?? []).filter((t) => showArchived || !t.archived);
|
|
109
|
+
const done = (changed: boolean) => { setEditing(null); if (changed) void list.refetch(); };
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className={`space-y-3 ${className ?? ""}`}>
|
|
113
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
114
|
+
<h3 className="font-medium">Sequence templates</h3>
|
|
115
|
+
<div className="flex items-center gap-2">
|
|
116
|
+
<label className="flex items-center gap-1 text-xs"><input type="checkbox" checked={showArchived} onChange={(e) => setShowArchived(e.target.checked)} />Show archived</label>
|
|
117
|
+
<Button size="sm" disabled={editing != null} onClick={() => setEditing("new")}><Plus className="mr-1 size-4" aria-hidden />New template</Button>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
{editing === "new" ? <TemplateForm loaded={null} onDone={done} /> : null}
|
|
121
|
+
{rows.length === 0 && editing !== "new" ? (
|
|
122
|
+
<EmptyState compact title="No sequence templates yet" description="Save the steps of a sequence that works, then start new ones from it." action={<Button size="sm" onClick={() => setEditing("new")}>Create a template</Button>} />
|
|
123
|
+
) : (
|
|
124
|
+
<ul aria-label="Sequence templates" className="divide-y rounded-md border border-border">
|
|
125
|
+
{rows.map((t) => (
|
|
126
|
+
<li key={t.id} className="p-2 text-sm">
|
|
127
|
+
{editing !== "new" && editing?.id === t.id ? (
|
|
128
|
+
<TemplateForm loaded={t} onDone={done} />
|
|
129
|
+
) : (
|
|
130
|
+
<div className="flex items-center gap-2">
|
|
131
|
+
<span className="min-w-0 flex-1"><span className="font-medium">{t.name}</span><span className="block truncate text-xs text-muted-foreground">{t.steps.length} {t.steps.length === 1 ? "step" : "steps"}{t.description ? ` - ${t.description}` : ""}</span></span>
|
|
132
|
+
{t.archived ? <StatusPill tone="warning">archived</StatusPill> : null}
|
|
133
|
+
{!t.archived && onUse ? <Button size="sm" variant="outline" onClick={() => onUse({ id: t.id, name: t.name, description: t.description, steps: templateToDraftSteps(t) })}><FilePlus2 className="mr-1 size-4" aria-hidden />Use template</Button> : null}
|
|
134
|
+
<Button size="sm" variant="ghost" aria-label={`Edit ${t.name}`} disabled={editing != null} onClick={() => setEditing(t)}><Pencil className="size-4" aria-hidden /></Button>
|
|
135
|
+
{t.archived ? (
|
|
136
|
+
<Button size="sm" variant="ghost" aria-label={`Restore ${t.name}`} loading={archive.loading && restoringId === t.id} onClick={() => { setRestoringId(t.id); void archive.mutate({ template: t, archived: false }); }}><ArchiveRestore className="size-4" aria-hidden /></Button>
|
|
137
|
+
) : (
|
|
138
|
+
<Button size="sm" variant="ghost" aria-label={`Archive ${t.name}`} onClick={() => setArchiving(t)}><Archive className="size-4" aria-hidden /></Button>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
</li>
|
|
143
|
+
))}
|
|
144
|
+
</ul>
|
|
145
|
+
)}
|
|
146
|
+
{archive.error ? <ApiError error={archive.error} title="Could not update the template" /> : null}
|
|
147
|
+
<ConfirmSpendDialog
|
|
148
|
+
open={archiving != null}
|
|
149
|
+
tier="destructive"
|
|
150
|
+
title={`Archive "${archiving?.name ?? ""}"?`}
|
|
151
|
+
description="It leaves the template list. Sequences already built from it are not changed. You can restore it from Show archived."
|
|
152
|
+
confirmLabel="Archive template"
|
|
153
|
+
affected={1}
|
|
154
|
+
confirming={archive.loading}
|
|
155
|
+
onConfirm={() => archiving && void archive.mutate({ template: archiving, archived: true })}
|
|
156
|
+
onCancel={() => setArchiving(null)}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
* - Open a sequence (deep link `?id=`): Results (stats, contacts, pause/resume one), Steps (edit copy), and
|
|
20
20
|
* "Enroll contacts": pick a list, tick contacts, confirm the recipient count.
|
|
21
21
|
*
|
|
22
|
+
* - "Templates" (header): sequence templates (list, create, edit, archive, use) and step templates (the same, plus AI drafts);
|
|
23
|
+
* "Use template" opens the new-sequence sheet with the template's name and steps filled in. In an open sequence: Edit (rename and
|
|
24
|
+
* settings), Sync audience, Save as template, Delete, a Senders tab, and removing a contact from the sequence.
|
|
25
|
+
*
|
|
22
26
|
* SDK operations of its own (both read):
|
|
23
27
|
* 1. listSequences GET /sequences?limit=100 (counts for the stat strip)
|
|
24
28
|
* 2. getSequence GET /sequences/{id} (header of the open sequence; also gives associated_list_id)
|
|
@@ -27,8 +31,8 @@
|
|
|
27
31
|
|
|
28
32
|
import { useState } from "react";
|
|
29
33
|
import type { ReactNode } from "react";
|
|
30
|
-
import { ListOrdered, Plus } from "lucide-react";
|
|
31
|
-
import { Button } from "@graph8/sdk/ui";
|
|
34
|
+
import { LayoutTemplate, ListOrdered, Plus } from "lucide-react";
|
|
35
|
+
import { Button, Tabs, TabsContent, TabsList, TabsTrigger } from "@graph8/sdk/ui";
|
|
32
36
|
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
33
37
|
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
34
38
|
import { SplitView } from "@/g8/components/screen/split-view";
|
|
@@ -38,10 +42,12 @@ import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
|
38
42
|
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
39
43
|
import { g8Get } from "@/g8/lib/g8-client";
|
|
40
44
|
import { listOf, statusGroup } from "../lib/sequencer-utils";
|
|
41
|
-
import type { SequenceRow } from "../lib/sequencer-utils";
|
|
45
|
+
import type { DraftStep, SequenceRow } from "../lib/sequencer-utils";
|
|
42
46
|
import { SequenceBuilder } from "./sequence-builder";
|
|
43
47
|
import { SequenceDetail } from "./sequence-detail";
|
|
44
48
|
import { SequenceList } from "./sequence-list";
|
|
49
|
+
import { SequenceTemplates } from "./sequence-templates";
|
|
50
|
+
import { StepTemplates } from "./step-templates";
|
|
45
51
|
|
|
46
52
|
export interface SequencesScreenProps {
|
|
47
53
|
/** Owner email sent with a new sequence (`user_email`). */
|
|
@@ -52,6 +58,8 @@ export interface SequencesScreenProps {
|
|
|
52
58
|
renderDetailActions?: (sequence: SequenceRow) => ReactNode;
|
|
53
59
|
/** Extra tabs in the open sequence's panel. */
|
|
54
60
|
extraDetailTabs?: { id: string; label: string; render: (sequence: SequenceRow) => ReactNode }[];
|
|
61
|
+
/** Open an enrolled person in your contacts screen; without it the Contacts tab has no such button. */
|
|
62
|
+
onOpenContact?: (contactId: number) => void;
|
|
55
63
|
className?: string;
|
|
56
64
|
}
|
|
57
65
|
|
|
@@ -85,6 +93,8 @@ export function SequencesScreen(props: SequencesScreenProps) {
|
|
|
85
93
|
const [contactId, setContactId] = useUrlState("enrollment", "");
|
|
86
94
|
const [status, setStatus] = useUrlState("status", "");
|
|
87
95
|
const [creating, setCreating] = useState(false);
|
|
96
|
+
const [templatesOpen, setTemplatesOpen] = useState(false);
|
|
97
|
+
const [prefill, setPrefill] = useState<{ name: string; steps: DraftStep[] } | null>(null);
|
|
88
98
|
const [version, setVersion] = useState(0);
|
|
89
99
|
const [listKey, setListKey] = useState(0);
|
|
90
100
|
const summary = useG8Query((signal) => g8Get("/sequences", { limit: SUMMARY_LIMIT }, { signal }), [version], { key: ["sequences", "summary"] });
|
|
@@ -101,9 +111,14 @@ export function SequencesScreen(props: SequencesScreenProps) {
|
|
|
101
111
|
description="Multi-channel outreach that runs on its own: build steps, enroll contacts, watch replies."
|
|
102
112
|
icon={<ListOrdered className="h-5 w-5" aria-hidden />}
|
|
103
113
|
actions={
|
|
104
|
-
|
|
105
|
-
<
|
|
106
|
-
|
|
114
|
+
<>
|
|
115
|
+
<Button variant="outline" onClick={() => setTemplatesOpen(true)}>
|
|
116
|
+
<LayoutTemplate className="mr-1 h-4 w-4" aria-hidden /> Templates
|
|
117
|
+
</Button>
|
|
118
|
+
<Button onClick={() => { setPrefill(null); setCreating(true); }}>
|
|
119
|
+
<Plus className="mr-1 h-4 w-4" aria-hidden /> New sequence
|
|
120
|
+
</Button>
|
|
121
|
+
</>
|
|
107
122
|
}
|
|
108
123
|
stats={stats}
|
|
109
124
|
>
|
|
@@ -135,6 +150,7 @@ export function SequencesScreen(props: SequencesScreenProps) {
|
|
|
135
150
|
onStepChange={(v) => setStepId(v)}
|
|
136
151
|
contactId={contactId || undefined}
|
|
137
152
|
onContactChange={(v) => setContactId(v)}
|
|
153
|
+
onOpenContact={props.onOpenContact}
|
|
138
154
|
onClose={close}
|
|
139
155
|
onChanged={refreshList}
|
|
140
156
|
refreshKey={version}
|
|
@@ -146,9 +162,26 @@ export function SequencesScreen(props: SequencesScreenProps) {
|
|
|
146
162
|
}
|
|
147
163
|
/>
|
|
148
164
|
</div>
|
|
165
|
+
<FormSheet open={templatesOpen} onOpenChange={setTemplatesOpen} width="xl" title="Templates" description="Reusable sequences and steps. A template is a copy: changing one never changes a sequence built from it.">
|
|
166
|
+
{templatesOpen ? (
|
|
167
|
+
<Tabs defaultValue="sequences">
|
|
168
|
+
<TabsList aria-label="Template kind">
|
|
169
|
+
<TabsTrigger value="sequences">Sequence templates</TabsTrigger>
|
|
170
|
+
<TabsTrigger value="steps">Step templates</TabsTrigger>
|
|
171
|
+
</TabsList>
|
|
172
|
+
<TabsContent value="sequences" className="pt-3">
|
|
173
|
+
<SequenceTemplates onUse={(t) => { setPrefill({ name: t.name, steps: t.steps }); setTemplatesOpen(false); setCreating(true); }} />
|
|
174
|
+
</TabsContent>
|
|
175
|
+
<TabsContent value="steps" className="pt-3"><StepTemplates /></TabsContent>
|
|
176
|
+
</Tabs>
|
|
177
|
+
) : null}
|
|
178
|
+
</FormSheet>
|
|
149
179
|
<FormSheet open={creating} onOpenChange={setCreating} width="xl" title="New sequence" description="Saved as a draft. Nobody is contacted until you launch it.">
|
|
150
180
|
{creating ? (
|
|
151
181
|
<SequenceBuilder
|
|
182
|
+
key={prefill ? `tpl:${prefill.name}` : "blank"}
|
|
183
|
+
initialName={prefill?.name}
|
|
184
|
+
initialSteps={prefill?.steps}
|
|
152
185
|
ownerEmail={ownerEmail}
|
|
153
186
|
onSaved={(id) => { setCreating(false); refreshList(); open(id); }}
|
|
154
187
|
/>
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* StepAiAssist: AI help for the copy of ONE email or LinkedIn step. It is collapsed until opened and loads nothing
|
|
5
|
+
* until then. Plug it into StepEditor's `renderAssist` slot (SequenceBuilder does).
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* Improve the instructions (email steps the AI writes)
|
|
9
|
+
* POST /sequencer/content/instructions/improve (sequences.improveStepInstruction) BILLABLE -> ConfirmSpendDialog first.
|
|
10
|
+
* Returns a rewrite of your instruction; it is shown beside yours and only replaces it when you accept.
|
|
11
|
+
* Preview a draft for one contact
|
|
12
|
+
* GET /contacts?name= (contacts.listContacts) read - find the contact
|
|
13
|
+
* GET /sequencer/agents/textual (sequences.listSequencerTextualAgents) read - the agent that writes
|
|
14
|
+
* POST /sequencer/content/email/estimate (sequences.estimateEmailDraft) read, FREE - the credit price, shown in the dialog
|
|
15
|
+
* POST /sequencer/content/email/generate (sequences.draftEmail) BILLABLE -> ConfirmSpendDialog first
|
|
16
|
+
* POST /sequencer/content/linkedin/generate (sequences.draftLinkedinMessage) BILLABLE -> ConfirmSpendDialog first
|
|
17
|
+
* The draft is shown, never sent or saved. "Use as my copy" first warns that the draft is personalised for one contact while the step goes to everyone, and writes it only after a second click.
|
|
18
|
+
* Nothing here reaches a contact: it only drafts text for you to read.
|
|
19
|
+
*/
|
|
20
|
+
import { useEffect, useState } from "react";
|
|
21
|
+
import { Sparkles } from "lucide-react";
|
|
22
|
+
import { Button, Input, Textarea } from "@graph8/sdk/ui";
|
|
23
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
24
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
25
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
26
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
27
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
28
|
+
import { agentOptions, draftContact, readCreditEstimate, readEmailDraft, readImprovedInstruction, readLinkedinDraft, templateRows } from "../lib/sequencer-manage";
|
|
29
|
+
import type { DraftContact } from "../lib/sequencer-manage";
|
|
30
|
+
import type { DraftStep } from "../lib/sequencer-utils";
|
|
31
|
+
|
|
32
|
+
export interface StepAiAssistProps {
|
|
33
|
+
step: DraftStep;
|
|
34
|
+
onChange: (next: DraftStep) => void;
|
|
35
|
+
/** The saved sequence this step belongs to, when there is one (gives the drafter the earlier steps as context). */
|
|
36
|
+
sequenceId?: string;
|
|
37
|
+
/** 1-based position of the step, for the same reason. */
|
|
38
|
+
stepOrder?: number;
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const LINKEDIN_COPY: Record<string, string | undefined> = { CONNECTION_REQUEST: "linkedin_connection_message", MESSAGE: "linkedin_message_text", INMAIL: "linkedin_inmail_body" };
|
|
43
|
+
const text = (v: unknown): string => (typeof v === "string" ? v : "");
|
|
44
|
+
|
|
45
|
+
function useDebounced<T>(value: T, ms = 300): T {
|
|
46
|
+
const [v, setV] = useState(value);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const t = setTimeout(() => setV(value), ms);
|
|
49
|
+
return () => clearTimeout(t);
|
|
50
|
+
}, [value, ms]);
|
|
51
|
+
return v;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type Pending = { kind: "improve" } | { kind: "draft"; credits: number | null; cached: boolean } | null;
|
|
55
|
+
|
|
56
|
+
export function StepAiAssist({ step, onChange, sequenceId, stepOrder, className }: StepAiAssistProps) {
|
|
57
|
+
const [open, setOpen] = useState(false);
|
|
58
|
+
if (step.kind !== "email" && step.kind !== "linkedin") return null;
|
|
59
|
+
if (!open) {
|
|
60
|
+
return <div className={className}><Button type="button" size="sm" variant="ghost" onClick={() => setOpen(true)}><Sparkles className="mr-1 size-4" aria-hidden />AI help for this step</Button></div>;
|
|
61
|
+
}
|
|
62
|
+
return <AssistPanel step={step} onChange={onChange} sequenceId={sequenceId} stepOrder={stepOrder} onClose={() => setOpen(false)} className={className} />;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function AssistPanel({ step, onChange, sequenceId, stepOrder, onClose, className }: StepAiAssistProps & { onClose: () => void }) {
|
|
66
|
+
const isEmail = step.kind === "email";
|
|
67
|
+
const action = text(step.step_data.linkedin_action_type);
|
|
68
|
+
const copyKey = LINKEDIN_COPY[action];
|
|
69
|
+
const aiEmail = isEmail && step.input_type !== "MANUAL_TEMPLATE";
|
|
70
|
+
const [instructions, setInstructions] = useState(text(step.step_data.instructions));
|
|
71
|
+
const [agent, setAgent] = useState("");
|
|
72
|
+
const [search, setSearch] = useState("");
|
|
73
|
+
const [contact, setContact] = useState<DraftContact | null>(null);
|
|
74
|
+
const [pending, setPending] = useState<Pending>(null);
|
|
75
|
+
const [improved, setImproved] = useState<string | null>(null);
|
|
76
|
+
const [draft, setDraft] = useState<{ subject: string; body: string } | null>(null);
|
|
77
|
+
// The draft is written for ONE contact but the step is sent to every contact: ask before it replaces the step copy.
|
|
78
|
+
const [confirmingCopy, setConfirmingCopy] = useState(false);
|
|
79
|
+
const q = useDebounced(search.trim());
|
|
80
|
+
|
|
81
|
+
const agents = useG8Query(async (signal) => agentOptions(await g8Get("/sequencer/agents/textual", undefined, { signal })), []);
|
|
82
|
+
const found = useG8Query(
|
|
83
|
+
async (signal) => templateRows<Record<string, unknown>>(await g8Get("/contacts", { name: q, limit: 8 }, { signal })).map(draftContact).filter((c): c is DraftContact => c != null),
|
|
84
|
+
[q],
|
|
85
|
+
{ enabled: q.length >= 2 },
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const body = () =>
|
|
89
|
+
isEmail
|
|
90
|
+
? { agent_name: agent, contact_id: contact!.id, contact_work_email: contact!.email, first_name: contact!.first_name, last_name: contact!.last_name, instructions, lead_info: contact!.lead_info, sequence_id: sequenceId, step_order: stepOrder }
|
|
91
|
+
: { agent_name: agent, contact_id: contact!.id, first_name: contact!.first_name, last_name: contact!.last_name, linkedin_action_type: action, instructions, lead_info: contact!.lead_info };
|
|
92
|
+
|
|
93
|
+
const prepare = useG8Mutation(async (kind: "improve" | "draft") => {
|
|
94
|
+
if (kind === "improve") return setPending({ kind: "improve" });
|
|
95
|
+
// The estimate is free and exact; if it is unavailable the dialog still asks, without a number.
|
|
96
|
+
const estimate = isEmail ? await g8Post("/sequencer/content/email/estimate", body()).then(readCreditEstimate, () => null) : null;
|
|
97
|
+
setPending({ kind: "draft", credits: estimate?.credits ?? null, cached: estimate?.cached ?? false });
|
|
98
|
+
});
|
|
99
|
+
const run = useG8Mutation(async (p: NonNullable<Pending>) => {
|
|
100
|
+
if (p.kind === "improve") {
|
|
101
|
+
setImproved(readImprovedInstruction(await g8Post("/sequencer/content/instructions/improve", { user_instruction: instructions })) || null);
|
|
102
|
+
} else if (isEmail) {
|
|
103
|
+
setDraft(readEmailDraft(await g8Post("/sequencer/content/email/generate", body())));
|
|
104
|
+
} else {
|
|
105
|
+
const li = readLinkedinDraft(await g8Post("/sequencer/content/linkedin/generate", body()));
|
|
106
|
+
setDraft(li ? { subject: li.subject, body: li.message } : null);
|
|
107
|
+
}
|
|
108
|
+
setPending(null);
|
|
109
|
+
}, { onError: () => setPending(null) });
|
|
110
|
+
|
|
111
|
+
const useCopy = () => {
|
|
112
|
+
if (!draft) return;
|
|
113
|
+
const data = isEmail ? { ...step.step_data, subject: draft.subject, body: draft.body } : { ...step.step_data, ...(copyKey ? { [copyKey]: draft.body } : {}), ...(action === "INMAIL" && draft.subject ? { linkedin_inmail_subject: draft.subject } : {}) };
|
|
114
|
+
onChange({ ...step, dirty: true, input_type: "MANUAL_TEMPLATE", step_data: data });
|
|
115
|
+
setDraft(null);
|
|
116
|
+
setConfirmingCopy(false);
|
|
117
|
+
};
|
|
118
|
+
const acceptInstructions = () => {
|
|
119
|
+
if (improved == null) return;
|
|
120
|
+
setInstructions(improved);
|
|
121
|
+
onChange({ ...step, dirty: true, step_data: { ...step.step_data, instructions: improved } });
|
|
122
|
+
setImproved(null);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const canDraft = Boolean(agent) && contact != null && instructions.trim().length > 0 && (isEmail ? Boolean(contact.email) : Boolean(copyKey));
|
|
126
|
+
const busy = prepare.loading || run.loading;
|
|
127
|
+
const confirmDraft = pending?.kind === "draft" ? pending : null;
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div className={`space-y-3 rounded-md border border-border p-3 text-sm ${className ?? ""}`} aria-label="AI help">
|
|
131
|
+
<div className="flex items-center justify-between"><h4 className="font-medium">AI help</h4><Button type="button" size="sm" variant="ghost" onClick={onClose}>Close</Button></div>
|
|
132
|
+
<Textarea aria-label="What should it say" rows={3} placeholder="What should this message say, and to whom?" value={instructions} onChange={(e) => setInstructions(e.target.value)} />
|
|
133
|
+
{aiEmail ? <Button type="button" size="sm" variant="outline" disabled={!instructions.trim() || busy} onClick={() => void prepare.mutate("improve")}>Improve my instructions</Button> : null}
|
|
134
|
+
{improved != null ? (
|
|
135
|
+
<div role="group" aria-label="Improved instructions" className="space-y-2 rounded-md bg-muted p-2">
|
|
136
|
+
<p className="whitespace-pre-wrap">{improved}</p>
|
|
137
|
+
<div className="flex gap-2"><Button type="button" size="sm" onClick={acceptInstructions}>Use these instructions</Button><Button type="button" size="sm" variant="ghost" onClick={() => setImproved(null)}>Keep mine</Button></div>
|
|
138
|
+
</div>
|
|
139
|
+
) : null}
|
|
140
|
+
|
|
141
|
+
<div className="space-y-2 border-t border-border pt-3">
|
|
142
|
+
<p className="font-medium">Preview a draft for one contact</p>
|
|
143
|
+
<select aria-label="Agent" className="h-9 w-full rounded-md border border-border bg-background px-2" value={agent} disabled={agents.loading} onChange={(e) => setAgent(e.target.value)}>
|
|
144
|
+
<option value="">{agents.loading ? "Loading agents..." : "Choose the agent that writes"}</option>
|
|
145
|
+
{(agents.data ?? []).map((a) => <option key={a.value} value={a.value}>{a.label}</option>)}
|
|
146
|
+
</select>
|
|
147
|
+
{agents.error ? <ApiError error={agents.error} title="Could not load the agents" onRetry={() => void agents.refetch()} /> : null}
|
|
148
|
+
{agents.data && agents.data.length === 0 ? <p className="text-muted-foreground">No agent is set up yet, so there is nobody to write the draft.</p> : null}
|
|
149
|
+
{contact ? (
|
|
150
|
+
<p className="flex items-center justify-between gap-2"><span>Draft for <strong>{contact.label}</strong></span><Button type="button" size="sm" variant="ghost" onClick={() => setContact(null)}>Change</Button></p>
|
|
151
|
+
) : (
|
|
152
|
+
<>
|
|
153
|
+
<Input aria-label="Find a contact" placeholder="Search contacts by name" value={search} onChange={(e) => setSearch(e.target.value)} />
|
|
154
|
+
{found.error ? <ApiError error={found.error} compact onRetry={() => void found.refetch()} /> : null}
|
|
155
|
+
{found.data && found.data.length === 0 ? <p className="text-muted-foreground">No contact matches.</p> : null}
|
|
156
|
+
{found.data && found.data.length > 0 ? (
|
|
157
|
+
<ul aria-label="Contacts" className="divide-y rounded-md border border-border">
|
|
158
|
+
{found.data.map((c) => <li key={c.id}><button type="button" className="w-full p-2 text-left hover:bg-muted/50" onClick={() => setContact(c)}>{c.label}</button></li>)}
|
|
159
|
+
</ul>
|
|
160
|
+
) : null}
|
|
161
|
+
</>
|
|
162
|
+
)}
|
|
163
|
+
{!isEmail && !copyKey ? <p className="text-muted-foreground">This LinkedIn action sends no text, so there is nothing to draft.</p> : null}
|
|
164
|
+
<Button type="button" size="sm" disabled={!canDraft || busy} loading={prepare.loading} onClick={() => void prepare.mutate("draft")}>Draft for this contact</Button>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
{[prepare.error, run.error].map((e, i) => (e ? <ApiError key={i} error={e} title="The AI request failed" /> : null))}
|
|
168
|
+
{draft ? (
|
|
169
|
+
<div role="group" aria-label="Drafted copy" className="space-y-2 rounded-md bg-muted p-2">
|
|
170
|
+
{draft.subject ? <p className="font-medium">{draft.subject}</p> : null}
|
|
171
|
+
<p className="whitespace-pre-wrap">{draft.body}</p>
|
|
172
|
+
<div className="flex gap-2">{confirmingCopy ? (
|
|
173
|
+
<div role="alert" className="space-y-2 rounded-md border border-border bg-background p-2">
|
|
174
|
+
<p>This draft was written for {contact?.label ?? "one contact"}. The step is sent to every contact in the sequence, so names and details in it will go to people they do not fit. Replace the step copy only if you will make it generic (merge tokens such as {"{{first_name}}"}).</p>
|
|
175
|
+
<div className="flex gap-2"><Button type="button" size="sm" onClick={useCopy}>Replace the step copy</Button><Button type="button" size="sm" variant="ghost" onClick={() => setConfirmingCopy(false)}>Keep my copy</Button></div>
|
|
176
|
+
</div>
|
|
177
|
+
) : (
|
|
178
|
+
<div className="flex gap-2"><Button type="button" size="sm" onClick={() => setConfirmingCopy(true)}>Use as my copy</Button><Button type="button" size="sm" variant="ghost" onClick={() => { setDraft(null); setConfirmingCopy(false); }}>Discard</Button></div>
|
|
179
|
+
)}</div>
|
|
180
|
+
</div>
|
|
181
|
+
) : null}
|
|
182
|
+
|
|
183
|
+
<ConfirmSpendDialog
|
|
184
|
+
open={pending != null}
|
|
185
|
+
tier="billable"
|
|
186
|
+
title={pending?.kind === "improve" ? "Improve these instructions with AI?" : `Draft ${isEmail ? "an email" : "a LinkedIn message"} for ${contact?.label ?? "this contact"}?`}
|
|
187
|
+
description={pending?.kind === "improve" ? "The AI rewrites your instructions. It uses credits and nothing is sent to anyone." : confirmDraft?.cached ? "An identical draft is cached, so this one is free. Nothing is sent to anyone." : "The AI writes one draft for you to read. It uses credits each time and nothing is sent to anyone."}
|
|
188
|
+
estimatedCredits={confirmDraft?.credits ?? undefined}
|
|
189
|
+
confirmLabel={pending?.kind === "improve" ? "Improve instructions" : "Write draft"}
|
|
190
|
+
confirming={run.loading}
|
|
191
|
+
onConfirm={() => pending && void run.mutate(pending)}
|
|
192
|
+
onCancel={() => setPending(null)}
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* Problems show inline and `stepIssues(step)` returns the same list for gating a Save button.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import type { ReactNode } from "react";
|
|
18
19
|
import { Input, Textarea } from "@graph8/sdk/ui";
|
|
19
20
|
import { g8Get } from "@/g8/lib/g8-client";
|
|
20
21
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
@@ -28,6 +29,10 @@ export interface StepEditorProps {
|
|
|
28
29
|
index: number;
|
|
29
30
|
onChange: (next: DraftStep) => void;
|
|
30
31
|
onRemove?: () => void;
|
|
32
|
+
/** Optional slot under the copy, for AI help or other tools (SequenceBuilder passes StepAiAssist). Receives the step and its setter. */
|
|
33
|
+
/** Disable the Manual / AI input-type select (a saved step template: the API fixes its input type at creation). */
|
|
34
|
+
lockInputType?: boolean;
|
|
35
|
+
renderAssist?: (step: DraftStep, onChange: (next: DraftStep) => void) => ReactNode;
|
|
31
36
|
disabled?: boolean;
|
|
32
37
|
className?: string;
|
|
33
38
|
}
|
|
@@ -52,15 +57,15 @@ function LinkedinReadiness() {
|
|
|
52
57
|
const c = conn.data?.data;
|
|
53
58
|
if (conn.loading || conn.error || !c) return null;
|
|
54
59
|
if (c.connected && c.is_token_valid !== false && !c.requires_manual_login)
|
|
55
|
-
return <p className="text-xs text-
|
|
60
|
+
return <p className="text-xs text-positive-foreground">LinkedIn sender connected.</p>;
|
|
56
61
|
return (
|
|
57
|
-
<p role="alert" className="text-xs text-
|
|
62
|
+
<p role="alert" className="text-xs text-warning-foreground">
|
|
58
63
|
LinkedIn is not ready to send. Reconnect the LinkedIn account in the {brand.linkLabel} app before launching.
|
|
59
64
|
</p>
|
|
60
65
|
);
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
export function StepEditor({ step, index, onChange, onRemove, disabled, className }: StepEditorProps) {
|
|
68
|
+
export function StepEditor({ step, index, onChange, onRemove, renderAssist, lockInputType, disabled, className }: StepEditorProps) {
|
|
64
69
|
const set = (patch: Record<string, unknown>) => onChange({ ...step, dirty: true, step_data: { ...step.step_data, ...patch } });
|
|
65
70
|
const issues = stepIssues(step);
|
|
66
71
|
const data = step.step_data;
|
|
@@ -72,13 +77,13 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
|
|
|
72
77
|
const phoneOptions = Array.isArray(data.phone_options) ? (data.phone_options as string[]) : [];
|
|
73
78
|
|
|
74
79
|
return (
|
|
75
|
-
<fieldset disabled={disabled} className={`space-y-3 rounded border border-
|
|
80
|
+
<fieldset disabled={disabled} className={`space-y-3 rounded border border-border p-3 ${className ?? ""}`} data-step-kind={step.kind}>
|
|
76
81
|
<div className="flex items-center justify-between">
|
|
77
|
-
<legend className="text-sm font-
|
|
82
|
+
<legend className="text-sm font-medium">
|
|
78
83
|
Step {index}: {KIND_LABEL[step.kind]}
|
|
79
84
|
</legend>
|
|
80
85
|
{onRemove ? (
|
|
81
|
-
<button type="button" className="text-xs text-
|
|
86
|
+
<button type="button" className="text-xs text-error-foreground hover:underline" onClick={onRemove}>
|
|
82
87
|
Remove step
|
|
83
88
|
</button>
|
|
84
89
|
) : null}
|
|
@@ -105,8 +110,9 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
|
|
|
105
110
|
Copy
|
|
106
111
|
<select
|
|
107
112
|
aria-label="Email copy"
|
|
108
|
-
className="ml-2 h-8 rounded border border-
|
|
113
|
+
className="ml-2 h-8 rounded border border-input px-1 text-sm"
|
|
109
114
|
value={step.input_type}
|
|
115
|
+
disabled={lockInputType}
|
|
110
116
|
onChange={(e) => onChange({ ...step, dirty: true, input_type: e.target.value as InputType })}
|
|
111
117
|
>
|
|
112
118
|
<option value="MANUAL_TEMPLATE">I write it</option>
|
|
@@ -114,6 +120,7 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
|
|
|
114
120
|
<option value="ON_DEMAND">AI writes each email</option>
|
|
115
121
|
</select>
|
|
116
122
|
</label>
|
|
123
|
+
{lockInputType ? <p className="text-xs text-muted-foreground">The input type is fixed once a template is created. Create a new template to change it.</p> : null}
|
|
117
124
|
{step.input_type === "MANUAL_TEMPLATE" ? (
|
|
118
125
|
<>
|
|
119
126
|
<Input aria-label="Subject" placeholder="Subject" value={str("subject")} onChange={(e) => set({ subject: e.target.value })} />
|
|
@@ -141,7 +148,7 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
|
|
|
141
148
|
<>
|
|
142
149
|
<select
|
|
143
150
|
aria-label="LinkedIn action"
|
|
144
|
-
className="h-8 rounded border border-
|
|
151
|
+
className="h-8 rounded border border-input px-1 text-sm"
|
|
145
152
|
value={action}
|
|
146
153
|
onChange={(e) => set({ linkedin_action_type: e.target.value })}
|
|
147
154
|
>
|
|
@@ -160,7 +167,7 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
|
|
|
160
167
|
onChange={(e) => set({ [copyKey]: e.target.value })}
|
|
161
168
|
/>
|
|
162
169
|
) : (
|
|
163
|
-
<p className="text-xs text-
|
|
170
|
+
<p className="text-xs text-muted-foreground">This action sends no text.</p>
|
|
164
171
|
)}
|
|
165
172
|
{action === "INMAIL" ? (
|
|
166
173
|
<Input
|
|
@@ -176,7 +183,7 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
|
|
|
176
183
|
|
|
177
184
|
{step.kind === "call" || step.kind === "task" ? (
|
|
178
185
|
<div className="space-y-1 text-sm">
|
|
179
|
-
<p className="text-
|
|
186
|
+
<p className="text-muted-foreground">
|
|
180
187
|
{step.kind === "call" ? "An automatic call attempt." : "Creates a call task for a person to work in the dialer."}
|
|
181
188
|
</p>
|
|
182
189
|
<div className="flex flex-wrap gap-3">
|
|
@@ -198,13 +205,15 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
|
|
|
198
205
|
</div>
|
|
199
206
|
) : null}
|
|
200
207
|
|
|
208
|
+
{renderAssist ? renderAssist(step, onChange) : null}
|
|
209
|
+
|
|
201
210
|
{broken ? (
|
|
202
|
-
<p role="alert" className="text-xs text-
|
|
211
|
+
<p role="alert" className="text-xs text-warning-foreground">
|
|
203
212
|
A {"{{merge token}}"} is not closed. Unclosed tokens are deleted when the email sends.
|
|
204
213
|
</p>
|
|
205
214
|
) : null}
|
|
206
215
|
{issues.length > 0 ? (
|
|
207
|
-
<ul className="text-xs text-
|
|
216
|
+
<ul className="text-xs text-error-foreground" aria-label="Step problems">
|
|
208
217
|
{issues.map((i) => (
|
|
209
218
|
<li key={i}>{i}</li>
|
|
210
219
|
))}
|