@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
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RegisterOwnedNumber: wire a number you already own (bought outside, or whose voice half failed) into the voice platform.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations:
|
|
7
|
+
* provisionVoiceNumber POST /voice/numbers/provision (write; BUYS NOTHING and charges nothing: it creates SIP trunks
|
|
8
|
+
* and dispatch rules for a number you already hold. Idempotent: a number already registered
|
|
9
|
+
* comes back as a success.)
|
|
10
|
+
*
|
|
11
|
+
* The person comes from MemberPicker, not a typed email. A personal work phone needs someone assigned, or no one can
|
|
12
|
+
* reach it by name.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useState } from "react";
|
|
16
|
+
import { Button, Input, Label } from "@graph8/sdk/ui";
|
|
17
|
+
import { g8Post } from "@/g8/lib/g8-client";
|
|
18
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
19
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
20
|
+
import { MemberPicker } from "@/g8/pickers/components/member-picker";
|
|
21
|
+
import type { MemberOption } from "@/g8/pickers/components/member-picker";
|
|
22
|
+
import { INBOUND_LABEL, INBOUND_TYPES, PROVIDERS, formatNumber, isE164 } from "../lib/phone-numbers-utils";
|
|
23
|
+
import type { InboundType, Provider } from "../lib/phone-numbers-utils";
|
|
24
|
+
|
|
25
|
+
export interface RegisterOwnedNumberProps {
|
|
26
|
+
onRegistered?: (phoneNumber: string) => void;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const SELECT = "h-9 w-full rounded-md border border-border bg-background px-2 text-sm";
|
|
31
|
+
|
|
32
|
+
export function RegisterOwnedNumber({ onRegistered, className }: RegisterOwnedNumberProps) {
|
|
33
|
+
const [number, setNumber] = useState("");
|
|
34
|
+
const [provider, setProvider] = useState<Provider>("twilio");
|
|
35
|
+
const [mode, setMode] = useState<InboundType>("ai");
|
|
36
|
+
const [assignee, setAssignee] = useState<MemberOption | null>(null);
|
|
37
|
+
const [note, setNote] = useState<string | null>(null);
|
|
38
|
+
const valid = isE164(number);
|
|
39
|
+
const needsPerson = mode === "work" && !assignee?.email;
|
|
40
|
+
|
|
41
|
+
const register = useG8Mutation(async () => {
|
|
42
|
+
const n = number.trim();
|
|
43
|
+
await g8Post("/voice/numbers/provision", {
|
|
44
|
+
number: n,
|
|
45
|
+
provider,
|
|
46
|
+
inbound_type: mode,
|
|
47
|
+
dialer: mode === "dialer",
|
|
48
|
+
work: mode === "work",
|
|
49
|
+
...(assignee?.email ? { assigned_to: assignee.email } : {}),
|
|
50
|
+
});
|
|
51
|
+
setNote(`${formatNumber(n)} is registered with the voice platform.`);
|
|
52
|
+
setNumber("");
|
|
53
|
+
onRegistered?.(n);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<form
|
|
58
|
+
className={`space-y-4 ${className ?? ""}`}
|
|
59
|
+
onSubmit={(e) => {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
if (valid && !needsPerson) void register.mutate(undefined);
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
<p className="text-sm text-muted-foreground">Use this for a number you already own. Nothing is bought and no credits are spent.</p>
|
|
65
|
+
<div className="space-y-1">
|
|
66
|
+
<Label htmlFor="ro-number">Phone number (E.164)</Label>
|
|
67
|
+
<Input id="ro-number" placeholder="+14155550123" value={number} onChange={(e) => setNumber(e.target.value)} aria-invalid={number !== "" && !valid} />
|
|
68
|
+
{number !== "" && !valid ? <p className="text-xs text-error">Use the international format: a plus sign, the country code, then the number.</p> : null}
|
|
69
|
+
</div>
|
|
70
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
71
|
+
<div className="space-y-1">
|
|
72
|
+
<Label htmlFor="ro-provider">Carrier that holds it</Label>
|
|
73
|
+
<select id="ro-provider" className={SELECT} value={provider} onChange={(e) => setProvider(e.target.value as Provider)}>
|
|
74
|
+
{PROVIDERS.map((p) => (
|
|
75
|
+
<option key={p} value={p}>{p === "twilio" ? "Twilio" : "Telnyx"}</option>
|
|
76
|
+
))}
|
|
77
|
+
</select>
|
|
78
|
+
</div>
|
|
79
|
+
<div className="space-y-1">
|
|
80
|
+
<Label htmlFor="ro-mode">How it answers</Label>
|
|
81
|
+
<select id="ro-mode" className={SELECT} value={mode} onChange={(e) => setMode(e.target.value as InboundType)}>
|
|
82
|
+
{INBOUND_TYPES.map((t) => (
|
|
83
|
+
<option key={t} value={t}>{INBOUND_LABEL[t]}</option>
|
|
84
|
+
))}
|
|
85
|
+
</select>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
<div className="space-y-1">
|
|
89
|
+
<Label htmlFor="ro-assignee">Assign to {mode === "work" ? "" : "(optional)"}</Label>
|
|
90
|
+
<MemberPicker id="ro-assignee" label="Assign to" value={assignee?.id ?? null} selectedName={assignee?.name} onChange={setAssignee} />
|
|
91
|
+
{needsPerson ? <p className="text-xs text-muted-foreground">A personal work phone needs a person, or no one can reach it by name.</p> : null}
|
|
92
|
+
</div>
|
|
93
|
+
{register.error ? <ApiError error={register.error} title="The number was not registered" onRetry={register.reset} /> : null}
|
|
94
|
+
{note ? <p role="status" className="text-sm">{note}</p> : null}
|
|
95
|
+
<Button type="submit" loading={register.loading} disabled={!valid || needsPerson}>Register number</Button>
|
|
96
|
+
</form>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ReleaseNumber: give a number back, or take it off the voice platform without giving it back.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations:
|
|
7
|
+
* releasePhoneNumber POST /phone-numbers/release (destructive: IRREVERSIBLE. The number returns to the carrier's pool
|
|
8
|
+
* and anyone can buy it, including a competitor; calls and messages stop at once. Confirmed with
|
|
9
|
+
* ConfirmSpendDialog after the person types the number.)
|
|
10
|
+
* deleteVoiceNumber DELETE /voice/numbers (destructive: tears down the number's SIP trunks, dispatch rules and
|
|
11
|
+
* carrier routing. It does NOT release the number: you keep owning it and the carrier keeps billing
|
|
12
|
+
* it. Confirmed with ConfirmSpendDialog.)
|
|
13
|
+
*
|
|
14
|
+
* The carrier must match where the number was bought: releasing a Telnyx number as Twilio reaches the wrong provider. The
|
|
15
|
+
* list knows it for every dialer number; when it does not, the person chooses and the form says why.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useState } from "react";
|
|
19
|
+
import { Button, Input, Label } from "@graph8/sdk/ui";
|
|
20
|
+
import { g8Delete, g8Post } from "@/g8/lib/g8-client";
|
|
21
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
22
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
23
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
24
|
+
import { PROVIDERS, dataOf, digits, formatNumber } from "../lib/phone-numbers-utils";
|
|
25
|
+
import type { NumberRow, Provider } from "../lib/phone-numbers-utils";
|
|
26
|
+
|
|
27
|
+
export interface ReleaseNumberProps {
|
|
28
|
+
row: NumberRow;
|
|
29
|
+
/** Called after the number was released (the record is gone: close its detail). */
|
|
30
|
+
onReleased?: () => void;
|
|
31
|
+
/** Called after the voice registration was removed (the number is still owned). */
|
|
32
|
+
onChanged?: () => void;
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type Pending = "release" | "deregister";
|
|
37
|
+
|
|
38
|
+
export function ReleaseNumber({ row, onReleased, onChanged, className }: ReleaseNumberProps) {
|
|
39
|
+
const known = (PROVIDERS as readonly string[]).includes(row.provider);
|
|
40
|
+
const [provider, setProvider] = useState<Provider | "">(known ? (row.provider as Provider) : "");
|
|
41
|
+
const [typed, setTyped] = useState("");
|
|
42
|
+
const [pending, setPending] = useState<Pending | null>(null);
|
|
43
|
+
const [note, setNote] = useState<string | null>(null);
|
|
44
|
+
|
|
45
|
+
const release = useG8Mutation(async () => {
|
|
46
|
+
if (!provider) return;
|
|
47
|
+
const out = dataOf(await g8Post("/phone-numbers/release", { phone_number: row.number, provider }));
|
|
48
|
+
const detail = typeof out.message === "string" ? ` ${out.message}` : "";
|
|
49
|
+
setNote(`${formatNumber(row.number)} was released.${detail}`);
|
|
50
|
+
setPending(null);
|
|
51
|
+
onReleased?.();
|
|
52
|
+
});
|
|
53
|
+
const deregister = useG8Mutation(async () => {
|
|
54
|
+
await g8Delete("/voice/numbers", { body: { number: row.number, ...(provider ? { provider } : {}) } });
|
|
55
|
+
setNote(`${formatNumber(row.number)} was removed from the voice platform. You still own it.`);
|
|
56
|
+
setPending(null);
|
|
57
|
+
onChanged?.();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const matches = digits(typed) === digits(row.number);
|
|
61
|
+
const assignedNote = row.agentName ? ` The voice agent ${row.agentName} stops answering on it.` : "";
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className={`space-y-6 ${className ?? ""}`}>
|
|
65
|
+
{!known ? (
|
|
66
|
+
<div className="space-y-1">
|
|
67
|
+
<Label htmlFor="rn-provider">Carrier it was bought from</Label>
|
|
68
|
+
<select id="rn-provider" className="h-9 rounded-md border border-border bg-background px-2 text-sm" value={provider} onChange={(e) => setProvider(e.target.value as Provider | "")}>
|
|
69
|
+
<option value="">Choose the carrier</option>
|
|
70
|
+
{PROVIDERS.map((p) => (
|
|
71
|
+
<option key={p} value={p}>{p === "twilio" ? "Twilio" : "Telnyx"}</option>
|
|
72
|
+
))}
|
|
73
|
+
</select>
|
|
74
|
+
<p className="text-xs text-muted-foreground">The carrier is not known for this number. It must match, or the wrong provider is asked to release it.</p>
|
|
75
|
+
</div>
|
|
76
|
+
) : null}
|
|
77
|
+
|
|
78
|
+
<section className="space-y-2" aria-label="Release this number">
|
|
79
|
+
<h3 className="text-sm font-medium">Release this number</h3>
|
|
80
|
+
<p className="text-sm text-muted-foreground">
|
|
81
|
+
Gives the number back to the carrier. It returns to the public pool and anyone can buy it, including a competitor. Calls and messages to it stop
|
|
82
|
+
at once, and monthly rental billing for it ends. There is no undo.
|
|
83
|
+
</p>
|
|
84
|
+
<div className="flex flex-wrap items-end gap-2">
|
|
85
|
+
<div className="space-y-1">
|
|
86
|
+
<Label htmlFor="rn-confirm">Type {row.number} to enable release</Label>
|
|
87
|
+
<Input id="rn-confirm" value={typed} placeholder={row.number} onChange={(e) => setTyped(e.target.value)} />
|
|
88
|
+
</div>
|
|
89
|
+
<Button variant="destructive" disabled={!matches || !provider} onClick={() => setPending("release")}>
|
|
90
|
+
Release number
|
|
91
|
+
</Button>
|
|
92
|
+
</div>
|
|
93
|
+
{release.error ? <ApiError error={release.error} title="The number was not released" onRetry={release.reset} /> : null}
|
|
94
|
+
</section>
|
|
95
|
+
|
|
96
|
+
<section className="space-y-2" aria-label="Remove from voice">
|
|
97
|
+
<h3 className="text-sm font-medium">Remove from the voice platform</h3>
|
|
98
|
+
<p className="text-sm text-muted-foreground">
|
|
99
|
+
Tears down the number's call routing so nothing answers it. You still own the number and the carrier still bills for it. Use Release to give it up.
|
|
100
|
+
</p>
|
|
101
|
+
<Button variant="outline" onClick={() => setPending("deregister")}>Remove voice routing</Button>
|
|
102
|
+
{deregister.error ? <ApiError error={deregister.error} title="The voice routing was not removed" onRetry={deregister.reset} /> : null}
|
|
103
|
+
</section>
|
|
104
|
+
|
|
105
|
+
{note ? <p role="status" className="text-sm">{note}</p> : null}
|
|
106
|
+
|
|
107
|
+
{pending ? (
|
|
108
|
+
<ConfirmSpendDialog
|
|
109
|
+
open
|
|
110
|
+
tier="destructive"
|
|
111
|
+
title={pending === "release" ? `Release ${formatNumber(row.number)}` : `Remove voice routing for ${formatNumber(row.number)}`}
|
|
112
|
+
description={
|
|
113
|
+
pending === "release"
|
|
114
|
+
? `The number goes back to the carrier and can be bought by anyone, including a competitor. Inbound calls and messages stop at once.${assignedNote}`
|
|
115
|
+
: `Nothing will answer this number until it is set up again. You keep owning it and the carrier keeps billing it.${assignedNote}`
|
|
116
|
+
}
|
|
117
|
+
confirmLabel={pending === "release" ? "Release number" : "Remove routing"}
|
|
118
|
+
affected={1}
|
|
119
|
+
confirming={release.loading || deregister.loading}
|
|
120
|
+
onCancel={() => setPending(null)}
|
|
121
|
+
onConfirm={() => void (pending === "release" ? release.mutate(undefined) : deregister.mutate(undefined)).then(() => setPending(null))}
|
|
122
|
+
/>
|
|
123
|
+
) : null}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* usePhoneNumbers: every phone number of the workspace, in full.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order (both read, in parallel):
|
|
7
|
+
* 1. listDialerNumbers GET /voice/dialer/numbers every number except the ones a voice AI agent holds, with
|
|
8
|
+
* usage (calls today / 7 days, connect rate) and the daily limit
|
|
9
|
+
* 2. listAgentPhoneNumbers GET /voice/agent-phone-numbers the AI-capable numbers and which agent holds each
|
|
10
|
+
*
|
|
11
|
+
* Neither endpoint pages or caps: each answers the whole inventory, so nothing is cut off and the screen pages the list
|
|
12
|
+
* on the client ("Load more"). A key that cannot read agents (403) still gets the dialer list; `agentsUnavailable` says
|
|
13
|
+
* so, and the AI-held numbers are then missing and the screen says that rather than hiding it.
|
|
14
|
+
*/
|
|
15
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
16
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
17
|
+
import { agentNumbers, dialerRows, mergeInventory } from "../lib/phone-numbers-utils";
|
|
18
|
+
import type { NumberRow } from "../lib/phone-numbers-utils";
|
|
19
|
+
|
|
20
|
+
export interface UsePhoneNumbers {
|
|
21
|
+
rows: NumberRow[];
|
|
22
|
+
loading: boolean;
|
|
23
|
+
/** The first read has finished. */
|
|
24
|
+
loaded: boolean;
|
|
25
|
+
/** The dialer list failed (the agents list failing alone is not an error, see `agentsUnavailable`). */
|
|
26
|
+
error: unknown;
|
|
27
|
+
/** Numbers held by voice AI agents could not be read, so they may be missing from `rows`. */
|
|
28
|
+
agentsUnavailable: boolean;
|
|
29
|
+
refetch: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function usePhoneNumbers(deps: readonly unknown[] = []): UsePhoneNumbers {
|
|
33
|
+
const q = useG8Query(
|
|
34
|
+
async (signal) => {
|
|
35
|
+
const [dialer, agents] = await Promise.allSettled([
|
|
36
|
+
g8Get("/voice/dialer/numbers", undefined, { signal }),
|
|
37
|
+
g8Get("/voice/agent-phone-numbers", undefined, { signal }),
|
|
38
|
+
]);
|
|
39
|
+
if (dialer.status === "rejected") throw dialer.reason;
|
|
40
|
+
const agentsOk = agents.status === "fulfilled";
|
|
41
|
+
return {
|
|
42
|
+
rows: mergeInventory(dialerRows(dialer.value), agentsOk ? agentNumbers(agents.value) : { available: [], booked: [] }),
|
|
43
|
+
agentsUnavailable: !agentsOk,
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
[...deps],
|
|
47
|
+
);
|
|
48
|
+
return {
|
|
49
|
+
rows: q.data?.rows ?? EMPTY,
|
|
50
|
+
loading: q.loading,
|
|
51
|
+
loaded: q.data !== undefined,
|
|
52
|
+
error: q.error,
|
|
53
|
+
agentsUnavailable: q.data?.agentsUnavailable ?? false,
|
|
54
|
+
refetch: () => void q.refetch(),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const EMPTY: NumberRow[] = [];
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Types and helpers shared by the phone-numbers components. The voice endpoints answer open-shaped payloads, so every
|
|
5
|
+
* field is read defensively (a missing field is "unknown", never a crash and never a made-up default).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** The carriers a number can be bought from (the contract's enum on purchase and release). */
|
|
9
|
+
export const PROVIDERS = ["twilio", "telnyx"] as const;
|
|
10
|
+
export type Provider = (typeof PROVIDERS)[number];
|
|
11
|
+
|
|
12
|
+
/** How a number answers inbound calls (the contract's enum on `inbound-type`). */
|
|
13
|
+
export const INBOUND_TYPES = ["ai", "sdr", "dialer", "work"] as const;
|
|
14
|
+
export type InboundType = (typeof INBOUND_TYPES)[number];
|
|
15
|
+
|
|
16
|
+
export const INBOUND_LABEL: Record<InboundType, string> = {
|
|
17
|
+
ai: "Voice AI agent",
|
|
18
|
+
sdr: "Rep answers",
|
|
19
|
+
dialer: "Outbound dialer",
|
|
20
|
+
work: "Personal work phone",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* What the platform holds when you buy, and charges each month after (credits). These are the platform's current
|
|
25
|
+
* rates (250 credits held on purchase, 250 per monthly rental); the API has no price read, so they are ESTIMATES the
|
|
26
|
+
* dialogs say so. Numbers the platform provisioned itself (the onboarding receptionist line) are never rental-billed.
|
|
27
|
+
*/
|
|
28
|
+
export const PURCHASE_CREDITS = 250;
|
|
29
|
+
export const MONTHLY_RENTAL_CREDITS = 250;
|
|
30
|
+
|
|
31
|
+
export interface NumberRow {
|
|
32
|
+
/** E.164, the number's id everywhere in the API. */
|
|
33
|
+
number: string;
|
|
34
|
+
/** Empty when the list does not say (never guessed). */
|
|
35
|
+
inboundType: string;
|
|
36
|
+
/** `twilio` or `telnyx`; empty when no list says (a number only the agents list knows). */
|
|
37
|
+
provider: string;
|
|
38
|
+
assignedTo: string | null;
|
|
39
|
+
callsToday: number | null;
|
|
40
|
+
calls7d: number | null;
|
|
41
|
+
connectRate7d: number | null;
|
|
42
|
+
dailyLimit: number | null;
|
|
43
|
+
createdAt: string | null;
|
|
44
|
+
/** False when the carrier would refuse to dial from it (for example an unverified Telnyx caller ID). */
|
|
45
|
+
isValid: boolean | null;
|
|
46
|
+
callerIdVerified: boolean | null;
|
|
47
|
+
/** Set when a voice AI agent has this number booked. */
|
|
48
|
+
agentName: string | null;
|
|
49
|
+
agentId: string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AvailableNumber {
|
|
53
|
+
phone_number: string;
|
|
54
|
+
capabilities?: { voice?: boolean; sms?: boolean; mms?: boolean; fax?: boolean };
|
|
55
|
+
region?: string | null;
|
|
56
|
+
locality?: string | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const asObject = (v: unknown): Record<string, unknown> => (v && typeof v === "object" && !Array.isArray(v) ? (v as Record<string, unknown>) : {});
|
|
60
|
+
const str = (v: unknown): string | null => (typeof v === "string" && v.trim() ? v : null);
|
|
61
|
+
const num = (v: unknown): number | null => (typeof v === "number" && Number.isFinite(v) ? v : null);
|
|
62
|
+
const bool = (v: unknown): boolean | null => (typeof v === "boolean" ? v : null);
|
|
63
|
+
|
|
64
|
+
/** The envelope's `data`, or the value itself. */
|
|
65
|
+
export function dataOf(envelope: unknown): Record<string, unknown> {
|
|
66
|
+
const o = asObject(envelope);
|
|
67
|
+
return "data" in o ? asObject(o.data) : o;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Rows of `data.<key>`, `data.items` or a bare `data` array. */
|
|
71
|
+
function listAt(envelope: unknown, key: string): unknown[] {
|
|
72
|
+
const o = asObject(envelope);
|
|
73
|
+
const data = "data" in o ? o.data : envelope;
|
|
74
|
+
if (Array.isArray(data)) return data;
|
|
75
|
+
const inner = asObject(data);
|
|
76
|
+
const rows = inner[key] ?? inner.items;
|
|
77
|
+
return Array.isArray(rows) ? rows : [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** GET /voice/dialer/numbers rows (`data.numbers`). */
|
|
81
|
+
export function dialerRows(envelope: unknown): NumberRow[] {
|
|
82
|
+
return listAt(envelope, "numbers")
|
|
83
|
+
.map((raw) => asObject(raw))
|
|
84
|
+
.filter((r) => str(r.number))
|
|
85
|
+
.map((r) => ({
|
|
86
|
+
number: String(r.number),
|
|
87
|
+
inboundType: str(r.inbound_type) ?? "",
|
|
88
|
+
provider: str(r.telephony_provider) ?? "",
|
|
89
|
+
assignedTo: str(r.assigned_to),
|
|
90
|
+
callsToday: num(r.calls_today),
|
|
91
|
+
calls7d: num(r.calls_7d),
|
|
92
|
+
connectRate7d: num(r.connect_rate_7d),
|
|
93
|
+
dailyLimit: num(r.daily_limit),
|
|
94
|
+
createdAt: str(r.created_at),
|
|
95
|
+
isValid: bool(r.is_valid),
|
|
96
|
+
callerIdVerified: bool(r.caller_id_verified),
|
|
97
|
+
agentName: null,
|
|
98
|
+
agentId: null,
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface AgentNumbers {
|
|
103
|
+
/** AI-capable numbers no agent holds. They also appear in the dialer list, so they only mark "free for an agent". */
|
|
104
|
+
available: string[];
|
|
105
|
+
booked: Array<{ number: string; agentName: string | null; agentId: string | null }>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** GET /voice/agent-phone-numbers (`available_numbers` strings, `booked_numbers` objects). */
|
|
109
|
+
export function agentNumbers(envelope: unknown): AgentNumbers {
|
|
110
|
+
const d = dataOf(envelope);
|
|
111
|
+
const available = Array.isArray(d.available_numbers) ? d.available_numbers.filter((n): n is string => typeof n === "string") : [];
|
|
112
|
+
const booked = (Array.isArray(d.booked_numbers) ? d.booked_numbers : [])
|
|
113
|
+
.map((b) => asObject(b))
|
|
114
|
+
.filter((b) => str(b.number))
|
|
115
|
+
.map((b) => ({ number: String(b.number), agentName: str(b.agent_name), agentId: str(b.agent_id) }));
|
|
116
|
+
return { available, booked };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The whole inventory. The dialer list leaves out the numbers a voice AI agent holds, and the agent list leaves out
|
|
121
|
+
* dialer-only numbers, so the union is every number. A number in both keeps the dialer row (it has the usage stats) and
|
|
122
|
+
* gains the agent's name.
|
|
123
|
+
*/
|
|
124
|
+
export function mergeInventory(dialer: NumberRow[], agents: AgentNumbers): NumberRow[] {
|
|
125
|
+
const byKey = new Map<string, NumberRow>();
|
|
126
|
+
for (const r of dialer) byKey.set(digits(r.number), r);
|
|
127
|
+
for (const b of agents.booked) {
|
|
128
|
+
const hit = byKey.get(digits(b.number));
|
|
129
|
+
if (hit) byKey.set(digits(b.number), { ...hit, agentName: b.agentName, agentId: b.agentId });
|
|
130
|
+
else {
|
|
131
|
+
byKey.set(digits(b.number), {
|
|
132
|
+
number: b.number, inboundType: "ai", provider: "", assignedTo: null, callsToday: null, calls7d: null,
|
|
133
|
+
connectRate7d: null, dailyLimit: null, createdAt: null, isValid: null, callerIdVerified: null, agentName: b.agentName, agentId: b.agentId,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return [...byKey.values()].sort((a, b) => (b.createdAt ?? "").localeCompare(a.createdAt ?? "") || a.number.localeCompare(b.number));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Digits only, so `+1 (415) 555-0123` and `+14155550123` compare equal (stored formats drift). */
|
|
141
|
+
export function digits(value: string): string {
|
|
142
|
+
return value.replace(/\D/g, "");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** True for a valid E.164 number: `+`, then 8 to 15 digits, no leading zero. */
|
|
146
|
+
export function isE164(value: string): boolean {
|
|
147
|
+
return /^\+[1-9]\d{7,14}$/.test(value.trim());
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** `+14155550123` -> `+1 (415) 555-0123`; any other shape is shown as it is. */
|
|
151
|
+
export function formatNumber(value: string): string {
|
|
152
|
+
const m = /^\+1(\d{3})(\d{3})(\d{4})$/.exec(value);
|
|
153
|
+
return m ? `+1 (${m[1]}) ${m[2]}-${m[3]}` : value;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function typeLabel(inboundType: string): string {
|
|
157
|
+
return INBOUND_LABEL[inboundType as InboundType] ?? (inboundType || "-");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export type Tone = "neutral" | "info" | "positive" | "warning" | "negative";
|
|
161
|
+
|
|
162
|
+
/** Health of a number for the pill: unusable, near its daily cap, or fine. */
|
|
163
|
+
export function numberHealth(row: NumberRow): { tone: Tone; label: string } {
|
|
164
|
+
if (row.isValid === false) return { tone: "negative", label: "Cannot dial" };
|
|
165
|
+
if (row.dailyLimit && row.callsToday != null && row.callsToday >= row.dailyLimit) return { tone: "warning", label: "At daily limit" };
|
|
166
|
+
if (row.dailyLimit && row.callsToday != null && row.callsToday >= row.dailyLimit * 0.8) return { tone: "warning", label: "Near limit" };
|
|
167
|
+
return { tone: "positive", label: "Ready" };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** A fresh idempotency key for one confirmed purchase (reuse it when the same click is retried). */
|
|
171
|
+
export function newIdempotencyKey(): string {
|
|
172
|
+
return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `g8-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function percent(rate: number | null): string {
|
|
176
|
+
if (rate == null) return "-";
|
|
177
|
+
return `${Math.round((rate <= 1 ? rate * 100 : rate) * 10) / 10}%`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function shortDate(value: string | null): string {
|
|
181
|
+
if (!value) return "-";
|
|
182
|
+
const d = new Date(value);
|
|
183
|
+
return Number.isNaN(d.getTime()) ? "-" : d.toLocaleDateString("en-US", { dateStyle: "medium", timeZone: "UTC" });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** A2P registration states the status payload can carry (carrier vocabulary): the pill tone for a status string. */
|
|
187
|
+
export function a2pTone(status: string | null | undefined): Tone {
|
|
188
|
+
const s = (status ?? "").toUpperCase();
|
|
189
|
+
if (["VERIFIED", "APPROVED", "REGISTERED"].includes(s)) return "positive";
|
|
190
|
+
if (["FAILED", "TIMEOUT", "REJECTED"].includes(s)) return "negative";
|
|
191
|
+
if (s) return "warning";
|
|
192
|
+
return "neutral";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const HIDDEN_FIELDS = /password|secret|token|credential/i;
|
|
196
|
+
|
|
197
|
+
/** Scalar fields of an open-shaped payload as label/value pairs (secrets are never shown). */
|
|
198
|
+
export function scalarItems(obj: Record<string, unknown>): Array<{ label: string; value: string }> {
|
|
199
|
+
return Object.entries(obj)
|
|
200
|
+
.filter(([k, v]) => !HIDDEN_FIELDS.test(k) && ["string", "number", "boolean"].includes(typeof v) && v !== "")
|
|
201
|
+
.map(([k, v]) => ({ label: k.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase()), value: typeof v === "boolean" ? (v ? "Yes" : "No") : String(v) }));
|
|
202
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Search for a number and buy it
|
|
3
|
+
* @when An agent setting up calling for a workspace. The search is free. THE PURCHASE SPENDS REAL CREDITS (about 250
|
|
4
|
+
* held on purchase, then about 250 a month in rental; estimates, the API has no price read), so a human must approve the
|
|
5
|
+
* exact number first. The purchase uses an Idempotency-Key DERIVED from the number, so re-running after a timeout cannot
|
|
6
|
+
* buy it twice. 402 = not enough credits (nothing bought, nothing charged); 409 = the number is already taken.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { g8 } from "@graph8/sdk";
|
|
10
|
+
|
|
11
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
12
|
+
|
|
13
|
+
interface Available {
|
|
14
|
+
phone_number: string;
|
|
15
|
+
locality?: string | null;
|
|
16
|
+
region?: string | null;
|
|
17
|
+
capabilities?: { voice?: boolean; sms?: boolean };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function findNumbers(areaCode: string, provider: "twilio" | "telnyx" = "twilio"): Promise<Available[]> {
|
|
21
|
+
// READ, free: nothing is held or charged by searching.
|
|
22
|
+
const res = await g8.api.phoneNumbers.searchAvailablePhoneNumbers({
|
|
23
|
+
body: { provider, country: "US", number_type: "local", area_code: areaCode },
|
|
24
|
+
});
|
|
25
|
+
return (res.data?.items ?? []) as Available[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function buyNumber(
|
|
29
|
+
phoneNumber: string,
|
|
30
|
+
humanApproved: boolean,
|
|
31
|
+
options: { provider?: "twilio" | "telnyx"; use?: "dialer" | "voice-ai"; assigneeEmail?: string } = {},
|
|
32
|
+
) {
|
|
33
|
+
if (!humanApproved) throw new Error("Refusing to spend credits without approval");
|
|
34
|
+
const provider = options.provider ?? "twilio";
|
|
35
|
+
const key = createHash("sha256").update(["number", phoneNumber, provider].join("|")).digest("hex").slice(0, 40);
|
|
36
|
+
// BILLABLE. `dialer: true` makes an outbound dialer number; `dialer: false` a voice-AI number. Leave `assignee` out to
|
|
37
|
+
// own it as the identity behind the key.
|
|
38
|
+
return g8.api.phoneNumbers.purchasePhoneNumber(
|
|
39
|
+
{
|
|
40
|
+
body: {
|
|
41
|
+
phone_number: phoneNumber,
|
|
42
|
+
provider,
|
|
43
|
+
dialer: (options.use ?? "dialer") === "dialer",
|
|
44
|
+
...(options.assigneeEmail ? { assignee: options.assigneeEmail } : {}),
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{ idempotencyKey: key },
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Assign a number to a rep, or clear it
|
|
3
|
+
* @when Onboarding a rep (give them a caller ID) or offboarding one (take their number back). Reassigning is ADMIN-ONLY
|
|
4
|
+
* through the API: an organisation key can never be the current assignee or the creator, so a non-admin key gets a 403.
|
|
5
|
+
* The assignee is not a label: it drives who the number rings, who hears about a missed call and whose outbound caller-ID
|
|
6
|
+
* rotation pool includes it.
|
|
7
|
+
*/
|
|
8
|
+
import { g8 } from "@graph8/sdk";
|
|
9
|
+
|
|
10
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
11
|
+
|
|
12
|
+
/** Move a number to a person (their email): updates the CRM record and tells voice. */
|
|
13
|
+
export async function assignNumber(phoneNumber: string, repEmail: string) {
|
|
14
|
+
return g8.api.phoneNumbers.updatePhoneAssignee({ body: { phone_number: phoneNumber, assignee: repEmail } });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Clear the assignment on the voice side (`assigned_to: ""` is how voice spells "nobody"). */
|
|
18
|
+
export async function unassignNumber(phoneNumber: string) {
|
|
19
|
+
return g8.api.phoneNumbers.updateVoiceAssignee({ body: { number: phoneNumber, assigned_to: "" } });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The CRM and voice records can disagree (voice is told best-effort): send the same assignee again to reconcile voice. */
|
|
23
|
+
export async function resyncVoice(phoneNumber: string, repEmail: string) {
|
|
24
|
+
return g8.api.phoneNumbers.updateVoiceAssignee({ body: { number: phoneNumber, assigned_to: repEmail } });
|
|
25
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Audit the inventory: usage, unassigned numbers and text registration
|
|
3
|
+
* @when A health check before a calling or texting campaign. READ-ONLY: it changes nothing. The dialer list leaves out the
|
|
4
|
+
* numbers a voice AI agent holds and the agent list leaves out dialer-only numbers, so both are read and merged.
|
|
5
|
+
*/
|
|
6
|
+
import { g8 } from "@graph8/sdk";
|
|
7
|
+
|
|
8
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
9
|
+
|
|
10
|
+
interface DialerNumber {
|
|
11
|
+
number: string;
|
|
12
|
+
assigned_to?: string | null;
|
|
13
|
+
calls_today?: number;
|
|
14
|
+
daily_limit?: number;
|
|
15
|
+
is_valid?: boolean;
|
|
16
|
+
inbound_type?: string;
|
|
17
|
+
telephony_provider?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function auditNumbers() {
|
|
21
|
+
const [dialer, agents, a2p] = await Promise.all([
|
|
22
|
+
g8.api.voice.listDialerNumbers({}),
|
|
23
|
+
g8.api.voice.listAgentPhoneNumbers({}),
|
|
24
|
+
g8.api.phoneNumbers.getPhoneA2pStatus({}),
|
|
25
|
+
]);
|
|
26
|
+
const numbers = (dialer.data?.numbers ?? []) as DialerNumber[];
|
|
27
|
+
const booked = ((agents.data as { booked_numbers?: unknown[] } | undefined)?.booked_numbers ?? []).length;
|
|
28
|
+
return {
|
|
29
|
+
total: numbers.length + booked,
|
|
30
|
+
heldByAgents: booked,
|
|
31
|
+
unassigned: numbers.filter((n) => !n.assigned_to).map((n) => n.number),
|
|
32
|
+
cannotDial: numbers.filter((n) => n.is_valid === false).map((n) => n.number),
|
|
33
|
+
atDailyLimit: numbers.filter((n) => n.daily_limit && (n.calls_today ?? 0) >= n.daily_limit).map((n) => n.number),
|
|
34
|
+
textRegistration: a2p.data,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Save A2P details and check registration
|
|
3
|
+
* @when Before sending texts to US numbers: carriers filter business texts from numbers whose business is not registered
|
|
4
|
+
* (A2P 10DLC). SAVING a draft is safe (nothing reaches a carrier, nothing is charged) and REPLACES what was saved, so send
|
|
5
|
+
* the whole object. SUBMITTING reaches the carrier registry, carries carrier fees, takes days and can be rejected: a human
|
|
6
|
+
* must approve it. Submitting is idempotent per workspace.
|
|
7
|
+
*/
|
|
8
|
+
import { g8 } from "@graph8/sdk";
|
|
9
|
+
|
|
10
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
11
|
+
|
|
12
|
+
export interface A2pDetails {
|
|
13
|
+
brand_inputs: Record<string, unknown>;
|
|
14
|
+
campaign_inputs: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function saveDraft(details: A2pDetails) {
|
|
18
|
+
// Merge over what is saved so a field this call does not know about (an address, say) is not wiped.
|
|
19
|
+
const saved = (await g8.api.phoneNumbers.getPhoneA2pStatus({})).data as { brand_inputs?: object; campaign_inputs?: object } | undefined;
|
|
20
|
+
await g8.api.phoneNumbers.saveA2pInputs({
|
|
21
|
+
body: {
|
|
22
|
+
brand_inputs: { ...saved?.brand_inputs, ...details.brand_inputs },
|
|
23
|
+
campaign_inputs: { ...saved?.campaign_inputs, ...details.campaign_inputs },
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
return (await g8.api.phoneNumbers.getPhoneA2pStatus({})).data as { inputs_complete?: boolean; missing_inputs?: string[] } | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function submit(details: A2pDetails, humanApproved: boolean) {
|
|
30
|
+
if (!humanApproved) throw new Error("Refusing to submit a carrier registration without approval");
|
|
31
|
+
const status = await saveDraft(details);
|
|
32
|
+
if (!status?.inputs_complete) throw new Error(`Still missing: ${(status?.missing_inputs ?? []).join(", ")}`);
|
|
33
|
+
return g8.api.phoneNumbers.startA2pRegistration({ body: details });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Re-attach pending or failed numbers (a write, safe to repeat). A worker also does this every six hours. */
|
|
37
|
+
export async function retryStuckNumbers() {
|
|
38
|
+
return g8.api.phoneNumbers.reconcileA2pRegistration({});
|
|
39
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# pickers
|
|
2
|
+
|
|
3
|
+
No raw ids: nobody should type an internal id they cannot discover. This opt-in block holds the signed-in-member hook and the searchable pickers. It depends on the foundation only; the blocks that use it (dialer, lists, enrichment, objects) depend on it, so an app with none of those gets no extra proxy prefixes or scopes.
|
|
4
|
+
|
|
5
|
+
## What you get
|
|
6
|
+
|
|
7
|
+
| File | What it is |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `hooks/use-current-member.ts` | `useCurrentMember({ enabled? })` -> `{ member, userId, email, name, loading, forbidden }`. The signed-in team member (`GET /team-members/me`, a personal key). An organisation key has no person: `forbidden` is true, so ask with a `MemberPicker`. |
|
|
10
|
+
| `components/member-picker.tsx` | `MemberPicker`: searchable team-member select (`GET /team-members`, fetched on open, "Show more"). Reports the user id operations take; people without a user id are left out. A 403 shows a plain note. |
|
|
11
|
+
| `components/contact-picker.tsx` | `ContactPicker`: single, or `multiple` with chips. Search by name, or by work email for text with `@`. Paged; locked `***` values are never shown. |
|
|
12
|
+
| `components/company-picker.tsx` | `CompanyPicker`: search by name or domain, paged. |
|
|
13
|
+
| `components/search-picker.tsx`, `hooks/use-picker-pages.ts` | The dropdown shell and the paging/search engine the pickers share, to build another picker. Import them as `@/g8/pickers/components/search-picker` and `@/g8/pickers/hooks/use-picker-pages` from a block that `dependsOn` pickers. |
|
|
14
|
+
| `lib/use-debounced.ts` | `useDebounced(value, ms)`. |
|
|
15
|
+
|
|
16
|
+
## Operations
|
|
17
|
+
|
|
18
|
+
| Operation | Endpoint | Tier | Scope | Used by |
|
|
19
|
+
|---|---|---|---|---|
|
|
20
|
+
| `teamMembers.getMyTeamMember` | GET /team-members/me | read | `team:read` | useCurrentMember |
|
|
21
|
+
| `teamMembers.listTeamMembers` | GET /team-members | read | `team:read` | MemberPicker |
|
|
22
|
+
| `contacts.listContacts` | GET /contacts | read | `contacts:read` | ContactPicker |
|
|
23
|
+
| `companies.listCompanies` | GET /companies | read | `companies:read` | CompanyPicker |
|