@graph8/sdk 0.308.1 → 0.357.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -0
- package/blocks/README.md +32 -0
- package/blocks/_foundation/README.md +78 -0
- package/blocks/_foundation/block.json +79 -0
- package/blocks/_foundation/components/api-error.tsx +62 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +142 -0
- package/blocks/_foundation/components/empty-state.tsx +24 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +88 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +87 -0
- package/blocks/_foundation/hooks/use-job-poll.ts +130 -0
- package/blocks/_foundation/hooks/use-paginated.ts +105 -0
- package/blocks/_foundation/lib/g8-client.ts +128 -0
- package/blocks/_foundation/recipes/verify-proxy-key.ts +18 -0
- package/blocks/_foundation/server/proxy.ts +233 -0
- package/blocks/_foundation/server/route.ts +30 -0
- package/blocks/agents/README.md +133 -0
- package/blocks/agents/block.json +223 -0
- package/blocks/agents/components/agent-builder.tsx +199 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +144 -0
- package/blocks/agents/components/agent-runs.tsx +175 -0
- package/blocks/agents/components/agent-skills-panel.tsx +126 -0
- package/blocks/agents/components/agent-test-console.tsx +175 -0
- package/blocks/agents/lib/agent-types.ts +114 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +50 -0
- package/blocks/agents/recipes/run-and-follow-agent-run.ts +46 -0
- package/blocks/agents/recipes/test-agent-before-activating.ts +36 -0
- package/blocks/analytics/README.md +73 -0
- package/blocks/analytics/block.json +178 -0
- package/blocks/analytics/components/funnel-chart.tsx +79 -0
- package/blocks/analytics/components/kpi-tiles.tsx +74 -0
- package/blocks/analytics/components/leaderboard.tsx +86 -0
- package/blocks/analytics/components/report-builder.tsx +200 -0
- package/blocks/analytics/hooks/use-report.ts +26 -0
- package/blocks/analytics/lib/analytics-types.ts +108 -0
- package/blocks/analytics/lib/use-debounced.ts +16 -0
- package/blocks/analytics/recipes/01-weekly-outbound-report.ts +32 -0
- package/blocks/analytics/recipes/02-revenue-snapshot.ts +22 -0
- package/blocks/analytics/recipes/03-saved-report.ts +30 -0
- package/blocks/appointments/README.md +80 -0
- package/blocks/appointments/block.json +218 -0
- package/blocks/appointments/components/availability-editor.tsx +192 -0
- package/blocks/appointments/components/booking-page.tsx +154 -0
- package/blocks/appointments/components/event-type-editor.tsx +214 -0
- package/blocks/appointments/components/meeting-detail.tsx +147 -0
- package/blocks/appointments/components/meeting-list.tsx +148 -0
- package/blocks/appointments/hooks/use-available-slots.ts +29 -0
- package/blocks/appointments/hooks/use-schedules.ts +18 -0
- package/blocks/appointments/lib/appointments-types.ts +159 -0
- package/blocks/appointments/lib/idempotency.ts +30 -0
- package/blocks/appointments/recipes/01-create-event-type-and-hours.ts +27 -0
- package/blocks/appointments/recipes/02-find-slots-and-book.ts +34 -0
- package/blocks/appointments/recipes/03-bookings-digest.ts +22 -0
- package/blocks/appointments/recipes/04-calendar-invite.ts +21 -0
- package/blocks/block.schema.json +130 -0
- package/blocks/block.types.ts +109 -0
- package/blocks/campaigns/README.md +118 -0
- package/blocks/campaigns/block.json +361 -0
- package/blocks/campaigns/components/campaign-assets.tsx +180 -0
- package/blocks/campaigns/components/campaign-audience-step.tsx +91 -0
- package/blocks/campaigns/components/campaign-launch-step.tsx +147 -0
- package/blocks/campaigns/components/campaign-list.tsx +120 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +93 -0
- package/blocks/campaigns/components/campaign-select.tsx +52 -0
- package/blocks/campaigns/components/campaign-sequence-step.tsx +158 -0
- package/blocks/campaigns/components/campaign-wizard.tsx +238 -0
- package/blocks/campaigns/components/form-field.tsx +29 -0
- package/blocks/campaigns/components/icp-editor.tsx +230 -0
- package/blocks/campaigns/components/persona-editor.tsx +210 -0
- package/blocks/campaigns/hooks/use-company-profile.ts +34 -0
- package/blocks/campaigns/lib/campaigns-utils.ts +155 -0
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +33 -0
- package/blocks/campaigns/recipes/campaign-report.ts +22 -0
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +51 -0
- package/blocks/campaigns/recipes/set-up-icp-and-personas.ts +37 -0
- package/blocks/coach/README.md +93 -0
- package/blocks/coach/block.json +125 -0
- package/blocks/coach/components/call-scorecard.tsx +126 -0
- package/blocks/coach/components/coaching-feed.tsx +140 -0
- package/blocks/coach/lib/coach-types.ts +116 -0
- package/blocks/coach/recipes/read-a-scorecard.ts +27 -0
- package/blocks/coach/recipes/weekly-coaching-digest.ts +53 -0
- package/blocks/copilot/README.md +99 -0
- package/blocks/copilot/block.json +151 -0
- package/blocks/copilot/components/copilot-memory.tsx +87 -0
- package/blocks/copilot/components/copilot-panel.tsx +189 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +111 -0
- package/blocks/copilot/lib/copilot-types.ts +111 -0
- package/blocks/copilot/recipes/ask-the-copilot.ts +35 -0
- package/blocks/copilot/recipes/post-in-a-work-channel.ts +33 -0
- package/blocks/copilot/recipes/remember-and-recall.ts +19 -0
- package/blocks/crm/README.md +122 -0
- package/blocks/crm/block.json +378 -0
- package/blocks/crm/components/activity-timeline.tsx +99 -0
- package/blocks/crm/components/company-detail.tsx +118 -0
- package/blocks/crm/components/company-table.tsx +173 -0
- package/blocks/crm/components/contact-detail.tsx +135 -0
- package/blocks/crm/components/contact-table.tsx +217 -0
- package/blocks/crm/components/dedupe-review.tsx +164 -0
- package/blocks/crm/components/import-wizard.tsx +193 -0
- package/blocks/crm/components/notes-panel.tsx +112 -0
- package/blocks/crm/components/record-form.tsx +191 -0
- package/blocks/crm/components/task-list.tsx +107 -0
- package/blocks/crm/hooks/use-custom-fields.ts +18 -0
- package/blocks/crm/hooks/use-debounced.ts +13 -0
- package/blocks/crm/hooks/use-page-query.ts +52 -0
- package/blocks/crm/lib/crm-types.ts +136 -0
- package/blocks/crm/recipes/custom-fields-and-values.ts +28 -0
- package/blocks/crm/recipes/dedupe-and-merge.ts +30 -0
- package/blocks/crm/recipes/notes-tasks-and-activity.ts +23 -0
- package/blocks/crm/recipes/upsert-contact-with-company.ts +33 -0
- package/blocks/deals/README.md +78 -0
- package/blocks/deals/block.json +198 -0
- package/blocks/deals/components/deal-detail.tsx +134 -0
- package/blocks/deals/components/forecast-view.tsx +112 -0
- package/blocks/deals/components/pipeline-board.tsx +142 -0
- package/blocks/deals/components/stage-editor.tsx +96 -0
- package/blocks/deals/hooks/use-pipeline-deals.ts +67 -0
- package/blocks/deals/hooks/use-pipelines.ts +19 -0
- package/blocks/deals/lib/deals-types.ts +128 -0
- package/blocks/deals/recipes/01-create-deal-and-advance.ts +32 -0
- package/blocks/deals/recipes/02-pipeline-report.ts +35 -0
- package/blocks/deals/recipes/03-stale-deals-sweep.ts +20 -0
- package/blocks/dialer/README.md +114 -0
- package/blocks/dialer/block.json +260 -0
- package/blocks/dialer/components/call-log.tsx +78 -0
- package/blocks/dialer/components/call-queue.tsx +188 -0
- package/blocks/dialer/components/dialer-workspace.tsx +47 -0
- package/blocks/dialer/components/disposition-panel.tsx +75 -0
- package/blocks/dialer/components/live-calls-table.tsx +107 -0
- package/blocks/dialer/components/session-start-form.tsx +111 -0
- package/blocks/dialer/components/softphone.tsx +122 -0
- package/blocks/dialer/components/transcript-viewer.tsx +82 -0
- package/blocks/dialer/hooks/use-dialer-numbers.ts +19 -0
- package/blocks/dialer/hooks/use-dialer-session.ts +68 -0
- package/blocks/dialer/hooks/use-dispositions.ts +29 -0
- package/blocks/dialer/lib/dialer-types.ts +158 -0
- package/blocks/dialer/lib/softphone-transport.ts +67 -0
- package/blocks/dialer/lib/twilio-transport.ts +74 -0
- package/blocks/dialer/recipes/01-create-session-and-dial.ts +39 -0
- package/blocks/dialer/recipes/02-disposition-calls.ts +26 -0
- package/blocks/dialer/recipes/03-review-a-call.ts +23 -0
- package/blocks/enrichment/README.md +105 -0
- package/blocks/enrichment/block.json +204 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +205 -0
- package/blocks/enrichment/components/email-verify-badge.tsx +82 -0
- package/blocks/enrichment/components/enrich-button.tsx +136 -0
- package/blocks/enrichment/components/waterfall-config.tsx +127 -0
- package/blocks/enrichment/hooks/use-providers.ts +18 -0
- package/blocks/enrichment/lib/enrichment-types.ts +104 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +41 -0
- package/blocks/enrichment/recipes/lookup-and-verify.ts +25 -0
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +26 -0
- package/blocks/forms/README.md +112 -0
- package/blocks/forms/block.json +200 -0
- package/blocks/forms/components/form-builder.tsx +150 -0
- package/blocks/forms/components/form-embed.tsx +103 -0
- package/blocks/forms/components/form-mapping-editor.tsx +125 -0
- package/blocks/forms/components/site-audit-panel.tsx +73 -0
- package/blocks/forms/components/submissions-table.tsx +158 -0
- package/blocks/forms/lib/form-types.ts +127 -0
- package/blocks/forms/recipes/install-snippet-and-form.ts +21 -0
- package/blocks/forms/recipes/retry-failed-deliveries.ts +26 -0
- package/blocks/forms/recipes/route-form-to-list.ts +44 -0
- package/blocks/inbox/README.md +72 -0
- package/blocks/inbox/block.json +194 -0
- package/blocks/inbox/components/inbox-list.tsx +156 -0
- package/blocks/inbox/components/inbox-screen.tsx +49 -0
- package/blocks/inbox/components/reply-composer.tsx +183 -0
- package/blocks/inbox/components/tag-and-assign.tsx +189 -0
- package/blocks/inbox/components/thread-badge.tsx +17 -0
- package/blocks/inbox/components/thread-view.tsx +108 -0
- package/blocks/inbox/hooks/use-inbox-thread.ts +24 -0
- package/blocks/inbox/hooks/use-inbox-threads.ts +40 -0
- package/blocks/inbox/lib/idempotency.ts +30 -0
- package/blocks/inbox/lib/inbox-types.ts +100 -0
- package/blocks/inbox/recipes/01-triage-and-reply.ts +43 -0
- package/blocks/inbox/recipes/02-tag-and-assign.ts +31 -0
- package/blocks/inbox/recipes/03-hot-contacts-digest.ts +16 -0
- package/blocks/integrations/README.md +75 -0
- package/blocks/integrations/block.json +172 -0
- package/blocks/integrations/components/integration-list.tsx +165 -0
- package/blocks/integrations/components/sync-status.tsx +223 -0
- package/blocks/integrations/components/webhook-manager.tsx +300 -0
- package/blocks/integrations/lib/integrations-utils.ts +62 -0
- package/blocks/integrations/recipes/crm-sync-health.ts +19 -0
- package/blocks/integrations/recipes/push-contacts-to-crm.ts +20 -0
- package/blocks/integrations/recipes/register-webhook.ts +18 -0
- package/blocks/integrations/recipes/verify-webhook-route.ts +33 -0
- package/blocks/intent/README.md +57 -0
- package/blocks/intent/block.json +138 -0
- package/blocks/intent/components/account-intent-score.tsx +91 -0
- package/blocks/intent/components/intent-feed.tsx +215 -0
- package/blocks/intent/components/intent-keyword-manager.tsx +331 -0
- package/blocks/intent/hooks/use-account-intent.ts +46 -0
- package/blocks/intent/lib/intent-utils.ts +95 -0
- package/blocks/intent/recipes/rank-intent-accounts.ts +36 -0
- package/blocks/intent/recipes/track-competitor-domain.ts +23 -0
- package/blocks/intent/recipes/who-visited-url.ts +18 -0
- package/blocks/lifecycle/README.md +79 -0
- package/blocks/lifecycle/block.json +198 -0
- package/blocks/lifecycle/components/churn-list.tsx +92 -0
- package/blocks/lifecycle/components/customer-table.tsx +119 -0
- package/blocks/lifecycle/components/lead-queue.tsx +127 -0
- package/blocks/lifecycle/components/lifecycle-board.tsx +102 -0
- package/blocks/lifecycle/components/stage-move-dialog.tsx +75 -0
- package/blocks/lifecycle/hooks/use-debounced.ts +13 -0
- package/blocks/lifecycle/hooks/use-lifecycle-stages.ts +27 -0
- package/blocks/lifecycle/lib/lifecycle.ts +99 -0
- package/blocks/lifecycle/recipes/customer-brief.ts +23 -0
- package/blocks/lifecycle/recipes/move-company-stage.ts +17 -0
- package/blocks/lifecycle/recipes/read-lifecycle-board.ts +21 -0
- package/blocks/lifecycle/recipes/unlock-stage-contacts.ts +25 -0
- package/blocks/lists/README.md +96 -0
- package/blocks/lists/block.json +166 -0
- package/blocks/lists/components/add-to-list-button.tsx +111 -0
- package/blocks/lists/components/list-picker.tsx +94 -0
- package/blocks/lists/components/list-table.tsx +160 -0
- package/blocks/lists/components/suppression-manager.tsx +153 -0
- package/blocks/lists/recipes/create-list-and-add-contacts.ts +35 -0
- package/blocks/lists/recipes/refresh-and-check-lists.ts +21 -0
- package/blocks/lists/recipes/suppress-and-reinstate.ts +30 -0
- package/blocks/mailboxes/README.md +71 -0
- package/blocks/mailboxes/block.json +187 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +284 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +271 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +185 -0
- package/blocks/mailboxes/components/warmup-health.tsx +232 -0
- package/blocks/mailboxes/lib/mailboxes-utils.ts +69 -0
- package/blocks/mailboxes/recipes/buy-domain-and-mailboxes.ts +64 -0
- package/blocks/mailboxes/recipes/connect-imap-mailbox.ts +38 -0
- package/blocks/mailboxes/recipes/is-it-safe-to-send.ts +17 -0
- package/blocks/mailboxes/recipes/warmup-report.ts +31 -0
- package/blocks/newsletter/README.md +122 -0
- package/blocks/newsletter/block.json +224 -0
- package/blocks/newsletter/components/issue-form.tsx +124 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +152 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +131 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +93 -0
- package/blocks/newsletter/components/subscriber-table.tsx +173 -0
- package/blocks/newsletter/lib/newsletter-types.ts +97 -0
- package/blocks/newsletter/recipes/create-newsletter-and-draft-issue.ts +27 -0
- package/blocks/newsletter/recipes/import-audience-and-send.ts +43 -0
- package/blocks/newsletter/recipes/issue-performance-report.ts +26 -0
- package/blocks/nurture/README.md +110 -0
- package/blocks/nurture/block.json +191 -0
- package/blocks/nurture/components/nurture-builder.tsx +114 -0
- package/blocks/nurture/components/nurture-enroll.tsx +107 -0
- package/blocks/nurture/components/nurture-form.tsx +120 -0
- package/blocks/nurture/components/nurture-stats.tsx +81 -0
- package/blocks/nurture/lib/nurture-types.ts +109 -0
- package/blocks/nurture/recipes/create-a-nurture.ts +34 -0
- package/blocks/nurture/recipes/enroll-contacts-and-launch.ts +24 -0
- package/blocks/nurture/recipes/nurture-health-check.ts +28 -0
- package/blocks/objects/README.md +120 -0
- package/blocks/objects/block.json +243 -0
- package/blocks/objects/components/attribute-field-input.tsx +82 -0
- package/blocks/objects/components/attribute-form-dialog.tsx +159 -0
- package/blocks/objects/components/object-not-found.tsx +34 -0
- package/blocks/objects/components/object-record-detail.tsx +143 -0
- package/blocks/objects/components/object-schema-editor.tsx +197 -0
- package/blocks/objects/components/object-table.tsx +130 -0
- package/blocks/objects/components/record-form-dialog.tsx +112 -0
- package/blocks/objects/components/record-roles-panel.tsx +106 -0
- package/blocks/objects/hooks/use-object-schema.ts +36 -0
- package/blocks/objects/lib/object-types.ts +214 -0
- package/blocks/objects/recipes/define-object-and-add-records.ts +24 -0
- package/blocks/objects/recipes/update-audit-and-archive-records.ts +39 -0
- package/blocks/objects/recipes/upsert-records-from-your-system.ts +30 -0
- package/blocks/pages/README.md +104 -0
- package/blocks/pages/block.json +223 -0
- package/blocks/pages/components/brand-kit-editor.tsx +152 -0
- package/blocks/pages/components/content-library.tsx +163 -0
- package/blocks/pages/components/page-editor.tsx +253 -0
- package/blocks/pages/components/page-list.tsx +159 -0
- package/blocks/pages/components/seo-report.tsx +159 -0
- package/blocks/pages/lib/studio.ts +77 -0
- package/blocks/pages/recipes/audit-site-seo.ts +28 -0
- package/blocks/pages/recipes/create-and-publish-page.ts +26 -0
- package/blocks/pages/recipes/edit-page-section.ts +25 -0
- package/blocks/pages/recipes/refresh-brand-kit.ts +22 -0
- package/blocks/quotes/README.md +79 -0
- package/blocks/quotes/block.json +183 -0
- package/blocks/quotes/components/product-picker.tsx +80 -0
- package/blocks/quotes/components/quote-builder.tsx +195 -0
- package/blocks/quotes/components/quote-list.tsx +79 -0
- package/blocks/quotes/components/quote-viewer.tsx +135 -0
- package/blocks/quotes/components/signature-status.tsx +77 -0
- package/blocks/quotes/hooks/use-debounced.ts +13 -0
- package/blocks/quotes/lib/quotes.ts +142 -0
- package/blocks/quotes/recipes/create-and-send-quote.ts +44 -0
- package/blocks/quotes/recipes/revise-sent-quote.ts +18 -0
- package/blocks/quotes/recipes/track-quote-signature.ts +22 -0
- package/blocks/radar/README.md +106 -0
- package/blocks/radar/block.json +189 -0
- package/blocks/radar/components/competitor-battle-card.tsx +53 -0
- package/blocks/radar/components/competitor-tracker.tsx +158 -0
- package/blocks/radar/components/radar-overview.tsx +67 -0
- package/blocks/radar/components/radar-report.tsx +86 -0
- package/blocks/radar/hooks/use-competitors.ts +23 -0
- package/blocks/radar/lib/radar-types.ts +124 -0
- package/blocks/radar/recipes/battle-card.ts +28 -0
- package/blocks/radar/recipes/deep-dive-report.ts +32 -0
- package/blocks/radar/recipes/marketing-snapshot.ts +18 -0
- package/blocks/radar/recipes/track-and-scan.ts +39 -0
- package/blocks/search/README.md +129 -0
- package/blocks/search/block.json +185 -0
- package/blocks/search/components/filter-builder.tsx +149 -0
- package/blocks/search/components/prospect-results.tsx +175 -0
- package/blocks/search/components/save-to-list-dialog.tsx +135 -0
- package/blocks/search/components/saved-searches.tsx +95 -0
- package/blocks/search/hooks/use-filter-options.ts +41 -0
- package/blocks/search/lib/search-types.ts +149 -0
- package/blocks/search/recipes/explore-filter-values.ts +27 -0
- package/blocks/search/recipes/saved-searches.ts +34 -0
- package/blocks/search/recipes/search-and-save-to-list.ts +37 -0
- package/blocks/sequencer/README.md +87 -0
- package/blocks/sequencer/block.json +214 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +93 -0
- package/blocks/sequencer/components/sequence-builder.tsx +306 -0
- package/blocks/sequencer/components/sequence-list.tsx +200 -0
- package/blocks/sequencer/components/sequence-stats.tsx +256 -0
- package/blocks/sequencer/components/step-editor.tsx +213 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +168 -0
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +51 -0
- package/blocks/sequencer/recipes/launch-and-enroll.ts +44 -0
- package/blocks/sequencer/recipes/pause-when-unsafe.ts +30 -0
- package/blocks/sequencer/recipes/sequence-report.ts +22 -0
- package/blocks/social-ads/README.md +128 -0
- package/blocks/social-ads/block.json +256 -0
- package/blocks/social-ads/components/ad-campaign-list.tsx +136 -0
- package/blocks/social-ads/components/audience-sync.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +121 -0
- package/blocks/social-ads/components/post-composer.tsx +122 -0
- package/blocks/social-ads/components/social-post-list.tsx +116 -0
- package/blocks/social-ads/hooks/use-social-accounts.ts +16 -0
- package/blocks/social-ads/lib/social-ads-types.ts +167 -0
- package/blocks/social-ads/recipes/creative-to-gallery.ts +41 -0
- package/blocks/social-ads/recipes/schedule-post.ts +51 -0
- package/blocks/social-ads/recipes/sync-audience-and-launch.ts +41 -0
- package/blocks/visitors/README.md +56 -0
- package/blocks/visitors/block.json +139 -0
- package/blocks/visitors/components/snippet-install.tsx +169 -0
- package/blocks/visitors/components/traffic-chart.tsx +156 -0
- package/blocks/visitors/components/visitor-company-card.tsx +92 -0
- package/blocks/visitors/components/visitor-feed.tsx +157 -0
- package/blocks/visitors/lib/visitors-utils.ts +78 -0
- package/blocks/visitors/recipes/check-snippet-installed.ts +24 -0
- package/blocks/visitors/recipes/recent-company-visits.ts +27 -0
- package/blocks/visitors/recipes/traffic-summary.ts +24 -0
- package/blocks/webchat/README.md +87 -0
- package/blocks/webchat/block.json +134 -0
- package/blocks/webchat/components/chat-widget-config.tsx +160 -0
- package/blocks/webchat/components/conversation-list.tsx +74 -0
- package/blocks/webchat/lib/webchat-types.ts +83 -0
- package/blocks/webchat/recipes/create-bot-and-style-widget.ts +24 -0
- package/blocks/webchat/recipes/list-chat-visitors.ts +24 -0
- package/blocks/workbench/README.md +135 -0
- package/blocks/workbench/block.json +259 -0
- package/blocks/workbench/components/column-recipe-editor.tsx +183 -0
- package/blocks/workbench/components/run-progress.tsx +132 -0
- package/blocks/workbench/components/workbench-add-rows.tsx +100 -0
- package/blocks/workbench/components/workbench-grid.tsx +159 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +102 -0
- package/blocks/workbench/components/workbench-table-list.tsx +105 -0
- package/blocks/workbench/hooks/use-workbench-table.ts +30 -0
- package/blocks/workbench/lib/waterfall-presets.ts +119 -0
- package/blocks/workbench/lib/workbench-types.ts +191 -0
- package/blocks/workbench/recipes/build-enriched-table.ts +65 -0
- package/blocks/workbench/recipes/import-from-search.ts +42 -0
- package/blocks/workbench/recipes/scrape-into-table.ts +36 -0
- package/blocks/workbench/recipes/waterfall-column.ts +55 -0
- package/blocks/workflows/README.md +100 -0
- package/blocks/workflows/block.json +188 -0
- package/blocks/workflows/components/run-history.tsx +136 -0
- package/blocks/workflows/components/trigger-config.tsx +134 -0
- package/blocks/workflows/components/workflow-editor.tsx +142 -0
- package/blocks/workflows/components/workflow-list.tsx +153 -0
- package/blocks/workflows/hooks/use-workflow.ts +51 -0
- package/blocks/workflows/lib/graph.ts +187 -0
- package/blocks/workflows/recipes/create-workflow.ts +35 -0
- package/blocks/workflows/recipes/find-failed-runs.ts +15 -0
- package/blocks/workflows/recipes/run-workflow-and-wait.ts +32 -0
- package/blocks/workflows/recipes/set-trigger.ts +22 -0
- package/dist/cli.js +5330 -29
- package/dist/index.browser.js +7740 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.browser.mjs +7717 -0
- package/dist/index.browser.mjs.map +1 -0
- package/dist/index.d.mts +1440 -21
- package/dist/index.d.ts +1440 -21
- package/dist/index.js +42 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +42 -3
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +7598 -0
- package/dist/react.browser.js.map +1 -0
- package/dist/react.browser.mjs +7568 -0
- package/dist/react.browser.mjs.map +1 -0
- package/dist/react.d.mts +1340 -18
- package/dist/react.d.ts +1340 -18
- package/dist/react.js +41 -2
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +41 -2
- package/dist/react.mjs.map +1 -1
- package/dist/types/agentSetup.d.ts +6 -6
- package/dist/types/blocks/add.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +46 -0
- package/dist/types/blocks/doctor.d.ts +26 -0
- package/dist/types/blocks/registry.d.ts +134 -0
- package/dist/types/cli.d.ts +16 -0
- package/dist/types/core.d.ts +1 -1
- package/dist/types/crypto-browser.d.ts +2 -0
- package/dist/types/crypto-node.d.ts +6 -0
- package/dist/types/react.d.ts +1 -1
- package/dist/types/version.d.ts +2 -2
- package/dist/types/webhooks.d.ts +2 -2
- package/dist/ui/index.d.mts +54 -0
- package/dist/ui/index.d.ts +54 -0
- package/dist/ui/index.js +105 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/index.mjs +44 -0
- package/dist/ui/index.mjs.map +1 -0
- package/dist/ui/lib/compose-refs.d.mts +19 -0
- package/dist/ui/lib/compose-refs.d.ts +19 -0
- package/dist/ui/lib/compose-refs.js +76 -0
- package/dist/ui/lib/compose-refs.js.map +1 -0
- package/dist/ui/lib/compose-refs.mjs +41 -0
- package/dist/ui/lib/compose-refs.mjs.map +1 -0
- package/dist/ui/lib/use-mobile.d.mts +4 -0
- package/dist/ui/lib/use-mobile.d.ts +4 -0
- package/dist/ui/lib/use-mobile.js +57 -0
- package/dist/ui/lib/use-mobile.js.map +1 -0
- package/dist/ui/lib/use-mobile.mjs +22 -0
- package/dist/ui/lib/use-mobile.mjs.map +1 -0
- package/dist/ui/lib/utils.d.mts +10 -0
- package/dist/ui/lib/utils.d.ts +10 -0
- package/dist/ui/lib/utils.js +33 -0
- package/dist/ui/lib/utils.js.map +1 -0
- package/dist/ui/lib/utils.mjs +9 -0
- package/dist/ui/lib/utils.mjs.map +1 -0
- package/dist/ui/primitives/alert-dialog.d.mts +22 -0
- package/dist/ui/primitives/alert-dialog.d.ts +22 -0
- package/dist/ui/primitives/alert-dialog.js +163 -0
- package/dist/ui/primitives/alert-dialog.js.map +1 -0
- package/dist/ui/primitives/alert-dialog.mjs +119 -0
- package/dist/ui/primitives/alert-dialog.mjs.map +1 -0
- package/dist/ui/primitives/avatar.d.mts +8 -0
- package/dist/ui/primitives/avatar.d.ts +8 -0
- package/dist/ui/primitives/avatar.js +91 -0
- package/dist/ui/primitives/avatar.js.map +1 -0
- package/dist/ui/primitives/avatar.mjs +55 -0
- package/dist/ui/primitives/avatar.mjs.map +1 -0
- package/dist/ui/primitives/breadcrumb.d.mts +13 -0
- package/dist/ui/primitives/breadcrumb.d.ts +13 -0
- package/dist/ui/primitives/breadcrumb.js +127 -0
- package/dist/ui/primitives/breadcrumb.js.map +1 -0
- package/dist/ui/primitives/breadcrumb.mjs +97 -0
- package/dist/ui/primitives/breadcrumb.mjs.map +1 -0
- package/dist/ui/primitives/button.d.mts +15 -0
- package/dist/ui/primitives/button.d.ts +15 -0
- package/dist/ui/primitives/button.js +91 -0
- package/dist/ui/primitives/button.js.map +1 -0
- package/dist/ui/primitives/button.mjs +56 -0
- package/dist/ui/primitives/button.mjs.map +1 -0
- package/dist/ui/primitives/card.d.mts +10 -0
- package/dist/ui/primitives/card.d.ts +10 -0
- package/dist/ui/primitives/card.js +101 -0
- package/dist/ui/primitives/card.js.map +1 -0
- package/dist/ui/primitives/card.mjs +62 -0
- package/dist/ui/primitives/card.mjs.map +1 -0
- package/dist/ui/primitives/carousel.d.mts +45 -0
- package/dist/ui/primitives/carousel.d.ts +45 -0
- package/dist/ui/primitives/carousel.js +300 -0
- package/dist/ui/primitives/carousel.js.map +1 -0
- package/dist/ui/primitives/carousel.mjs +261 -0
- package/dist/ui/primitives/carousel.mjs.map +1 -0
- package/dist/ui/primitives/chart.d.mts +68 -0
- package/dist/ui/primitives/chart.d.ts +68 -0
- package/dist/ui/primitives/chart.js +280 -0
- package/dist/ui/primitives/chart.js.map +1 -0
- package/dist/ui/primitives/chart.mjs +241 -0
- package/dist/ui/primitives/chart.mjs.map +1 -0
- package/dist/ui/primitives/collapsible.d.mts +8 -0
- package/dist/ui/primitives/collapsible.d.ts +8 -0
- package/dist/ui/primitives/collapsible.js +72 -0
- package/dist/ui/primitives/collapsible.js.map +1 -0
- package/dist/ui/primitives/collapsible.mjs +36 -0
- package/dist/ui/primitives/collapsible.mjs.map +1 -0
- package/dist/ui/primitives/context-menu.d.mts +29 -0
- package/dist/ui/primitives/context-menu.d.ts +29 -0
- package/dist/ui/primitives/context-menu.js +203 -0
- package/dist/ui/primitives/context-menu.js.map +1 -0
- package/dist/ui/primitives/context-menu.mjs +155 -0
- package/dist/ui/primitives/context-menu.mjs.map +1 -0
- package/dist/ui/primitives/dialog.d.mts +26 -0
- package/dist/ui/primitives/dialog.d.ts +26 -0
- package/dist/ui/primitives/dialog.js +192 -0
- package/dist/ui/primitives/dialog.js.map +1 -0
- package/dist/ui/primitives/dialog.mjs +148 -0
- package/dist/ui/primitives/dialog.mjs.map +1 -0
- package/dist/ui/primitives/drawer.d.mts +26 -0
- package/dist/ui/primitives/drawer.d.ts +26 -0
- package/dist/ui/primitives/drawer.js +146 -0
- package/dist/ui/primitives/drawer.js.map +1 -0
- package/dist/ui/primitives/drawer.mjs +103 -0
- package/dist/ui/primitives/drawer.mjs.map +1 -0
- package/dist/ui/primitives/input.d.mts +5 -0
- package/dist/ui/primitives/input.d.ts +5 -0
- package/dist/ui/primitives/input.js +46 -0
- package/dist/ui/primitives/input.js.map +1 -0
- package/dist/ui/primitives/input.mjs +22 -0
- package/dist/ui/primitives/input.mjs.map +1 -0
- package/dist/ui/primitives/item.d.mts +27 -0
- package/dist/ui/primitives/item.d.ts +27 -0
- package/dist/ui/primitives/item.js +221 -0
- package/dist/ui/primitives/item.js.map +1 -0
- package/dist/ui/primitives/item.mjs +188 -0
- package/dist/ui/primitives/item.mjs.map +1 -0
- package/dist/ui/primitives/kbd.d.mts +15 -0
- package/dist/ui/primitives/kbd.d.ts +15 -0
- package/dist/ui/primitives/kbd.js +45 -0
- package/dist/ui/primitives/kbd.js.map +1 -0
- package/dist/ui/primitives/kbd.mjs +21 -0
- package/dist/ui/primitives/kbd.mjs.map +1 -0
- package/dist/ui/primitives/label.d.mts +6 -0
- package/dist/ui/primitives/label.d.ts +6 -0
- package/dist/ui/primitives/label.js +55 -0
- package/dist/ui/primitives/label.js.map +1 -0
- package/dist/ui/primitives/label.mjs +21 -0
- package/dist/ui/primitives/label.mjs.map +1 -0
- package/dist/ui/primitives/resizable.d.mts +10 -0
- package/dist/ui/primitives/resizable.d.ts +10 -0
- package/dist/ui/primitives/resizable.js +68 -0
- package/dist/ui/primitives/resizable.js.map +1 -0
- package/dist/ui/primitives/resizable.mjs +42 -0
- package/dist/ui/primitives/resizable.mjs.map +1 -0
- package/dist/ui/primitives/save-status.d.mts +31 -0
- package/dist/ui/primitives/save-status.d.ts +31 -0
- package/dist/ui/primitives/save-status.js +150 -0
- package/dist/ui/primitives/save-status.js.map +1 -0
- package/dist/ui/primitives/save-status.mjs +126 -0
- package/dist/ui/primitives/save-status.mjs.map +1 -0
- package/dist/ui/primitives/separator.d.mts +6 -0
- package/dist/ui/primitives/separator.d.ts +6 -0
- package/dist/ui/primitives/separator.js +62 -0
- package/dist/ui/primitives/separator.js.map +1 -0
- package/dist/ui/primitives/separator.mjs +28 -0
- package/dist/ui/primitives/separator.mjs.map +1 -0
- package/dist/ui/primitives/sheet.d.mts +16 -0
- package/dist/ui/primitives/sheet.d.ts +16 -0
- package/dist/ui/primitives/sheet.js +170 -0
- package/dist/ui/primitives/sheet.js.map +1 -0
- package/dist/ui/primitives/sheet.mjs +129 -0
- package/dist/ui/primitives/sheet.mjs.map +1 -0
- package/dist/ui/primitives/sidebar.d.mts +74 -0
- package/dist/ui/primitives/sidebar.d.ts +74 -0
- package/dist/ui/primitives/sidebar.js +697 -0
- package/dist/ui/primitives/sidebar.js.map +1 -0
- package/dist/ui/primitives/sidebar.mjs +651 -0
- package/dist/ui/primitives/sidebar.mjs.map +1 -0
- package/dist/ui/primitives/skeleton.d.mts +5 -0
- package/dist/ui/primitives/skeleton.d.ts +5 -0
- package/dist/ui/primitives/skeleton.js +40 -0
- package/dist/ui/primitives/skeleton.js.map +1 -0
- package/dist/ui/primitives/skeleton.mjs +16 -0
- package/dist/ui/primitives/skeleton.mjs.map +1 -0
- package/dist/ui/primitives/slider.d.mts +20 -0
- package/dist/ui/primitives/slider.d.ts +20 -0
- package/dist/ui/primitives/slider.js +112 -0
- package/dist/ui/primitives/slider.js.map +1 -0
- package/dist/ui/primitives/slider.mjs +78 -0
- package/dist/ui/primitives/slider.mjs.map +1 -0
- package/dist/ui/primitives/sortable.d.mts +50 -0
- package/dist/ui/primitives/sortable.d.ts +50 -0
- package/dist/ui/primitives/sortable.js +456 -0
- package/dist/ui/primitives/sortable.js.map +1 -0
- package/dist/ui/primitives/sortable.mjs +435 -0
- package/dist/ui/primitives/sortable.mjs.map +1 -0
- package/dist/ui/primitives/switch.d.mts +6 -0
- package/dist/ui/primitives/switch.d.ts +6 -0
- package/dist/ui/primitives/switch.js +67 -0
- package/dist/ui/primitives/switch.js.map +1 -0
- package/dist/ui/primitives/switch.mjs +33 -0
- package/dist/ui/primitives/switch.mjs.map +1 -0
- package/dist/ui/primitives/table.d.mts +12 -0
- package/dist/ui/primitives/table.d.ts +12 -0
- package/dist/ui/primitives/table.js +130 -0
- package/dist/ui/primitives/table.js.map +1 -0
- package/dist/ui/primitives/table.mjs +89 -0
- package/dist/ui/primitives/table.mjs.map +1 -0
- package/dist/ui/primitives/tabs.d.mts +9 -0
- package/dist/ui/primitives/tabs.d.ts +9 -0
- package/dist/ui/primitives/tabs.js +86 -0
- package/dist/ui/primitives/tabs.js.map +1 -0
- package/dist/ui/primitives/tabs.mjs +49 -0
- package/dist/ui/primitives/tabs.mjs.map +1 -0
- package/dist/ui/primitives/textarea.d.mts +5 -0
- package/dist/ui/primitives/textarea.d.ts +5 -0
- package/dist/ui/primitives/textarea.js +43 -0
- package/dist/ui/primitives/textarea.js.map +1 -0
- package/dist/ui/primitives/textarea.mjs +19 -0
- package/dist/ui/primitives/textarea.mjs.map +1 -0
- package/dist/ui/primitives/tooltip.d.mts +9 -0
- package/dist/ui/primitives/tooltip.d.ts +9 -0
- package/dist/ui/primitives/tooltip.js +94 -0
- package/dist/ui/primitives/tooltip.js.map +1 -0
- package/dist/ui/primitives/tooltip.mjs +57 -0
- package/dist/ui/primitives/tooltip.mjs.map +1 -0
- package/docs/agents.md +72 -29
- package/docs/api/README.md +6 -3
- package/docs/api/appObjects.d.ts +1 -1
- package/docs/api/appObjects.md +1 -1
- package/docs/api/by-path.md +29 -0
- package/docs/api/objects.d.ts +72 -5
- package/docs/api/objects.md +84 -7
- package/docs/api/processLists.d.ts +247 -0
- package/docs/api/processLists.md +409 -0
- package/docs/api/reports.d.ts +476 -0
- package/docs/api/reports.md +539 -0
- package/docs/api/scheduleBuy.d.ts +12 -8
- package/docs/api/scheduleBuy.md +13 -10
- package/docs/api/seo.d.ts +406 -3
- package/docs/api/seo.md +505 -1
- package/docs/api/workspaceBackup.d.ts +324 -0
- package/docs/api/workspaceBackup.md +427 -0
- package/package.json +174 -5
- package/stacks/6sense.json +46 -0
- package/stacks/README.md +20 -0
- package/stacks/apollo.json +128 -0
- package/stacks/clay.json +59 -0
- package/stacks/gong.json +72 -0
- package/stacks/hubspot.json +162 -0
- package/stacks/intercom.json +53 -0
- package/stacks/mailchimp.json +71 -0
- package/stacks/outreach.json +86 -0
- package/stacks/salesforce.json +94 -0
- package/stacks/stack.schema.json +32 -0
- package/stacks/starter.json +22 -0
- package/styles/styles.css +2 -0
- package/styles/theme.css +181 -0
- package/styles/tokens.css +284 -0
- package/templates/next-app/README.md.tmpl +35 -0
- package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
- package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
- package/templates/next-app/app/globals.css.tmpl +8 -0
- package/templates/next-app/app/layout.tsx.tmpl +19 -0
- package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
- package/templates/next-app/dot-env.local.example.tmpl +6 -0
- package/templates/next-app/dot-gitignore.tmpl +7 -0
- package/templates/next-app/lib/nav.ts.tmpl +2 -0
- package/templates/next-app/next.config.mjs.tmpl +4 -0
- package/templates/next-app/package.json.tmpl +25 -0
- package/templates/next-app/postcss.config.mjs.tmpl +4 -0
- package/templates/next-app/tsconfig.json.tmpl +21 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# dialer: outbound calling
|
|
2
|
+
|
|
3
|
+
Run a calling session for a rep: pick a list, dial batch by batch, watch the queue, take the calls in the
|
|
4
|
+
browser, record outcomes, and review transcripts. Copy `DialerWorkspace` for the whole screen, or compose
|
|
5
|
+
`CallQueue`, `Softphone`, `DispositionPanel`, `CallLog` and `TranscriptViewer` yourself. Install with
|
|
6
|
+
`npx @graph8/sdk add dialer` (depends on `lists`).
|
|
7
|
+
|
|
8
|
+
> Dialing reaches real people on real phones and spends credits. Nothing in this block dials, leaves a
|
|
9
|
+
> voicemail, or mints a softphone credential without a confirm dialog first, and the block never auto-advances to
|
|
10
|
+
> the next batch.
|
|
11
|
+
|
|
12
|
+
## How it works (read this before building)
|
|
13
|
+
|
|
14
|
+
- graph8 places the calls **from its servers** into a conference. The rep's browser only **joins that
|
|
15
|
+
conference** with audio. So the Softphone does not dial; "Dial next batch" does.
|
|
16
|
+
- The API has **no live event stream**: no WebSocket or SSE for sessions, call state or transcripts. The block
|
|
17
|
+
**polls** (`useDialerSession`, every 5 seconds, via `useJobPoll`). `queue-stats` is the cheap poll; the
|
|
18
|
+
session read carries the live call list.
|
|
19
|
+
- The browser audio client is **not part of the API**. The Softphone talks to a `SoftphoneTransport`
|
|
20
|
+
(`lib/softphone-transport.ts`). `lib/twilio-transport.ts` implements it with `@twilio/voice-sdk` (graph8's
|
|
21
|
+
dialer runs on Twilio conferences). Another vendor (LiveKit, your own SIP stack) = write a transport and pass
|
|
22
|
+
it as `transport`; delete `twilio-transport.ts` and the peer dependency if you do.
|
|
23
|
+
|
|
24
|
+
## The flow, in order
|
|
25
|
+
|
|
26
|
+
1. **Caller ID**: `GET /voice/dialer/numbers` (`SessionStartForm`). Use the `number` field.
|
|
27
|
+
2. **Draft** (write, dials nobody): `POST /dialer/sessions/draft` with `{ user_id, list_id, dialing_mode,
|
|
28
|
+
parallel_count, from_phone }` -> `session_id`. An organisation API key has no user of its own, so you pass
|
|
29
|
+
the rep's `user_id` (and `user_email`). The same list returns the existing live session.
|
|
30
|
+
3. **Watch** (read, polled): `GET /dialer/sessions/{session_id}/queue-stats` -> `{ status, batch_status,
|
|
31
|
+
current_batch_id, queue_remaining }` and `GET /dialer/sessions/{session_id}` -> `live_state.calls`.
|
|
32
|
+
`batch_status` is `in_flight`, `complete_awaiting_disposition` or `complete_auto_advance_eligible`.
|
|
33
|
+
4. **Enable audio** (`Softphone`, **external**, confirm first): `GET /dialer/sessions/token?user_id=` returns a
|
|
34
|
+
one-hour bearer credential `{ token, identity }`. Treat it as a secret (the block keeps it in memory and
|
|
35
|
+
renders nothing).
|
|
36
|
+
5. **Dial** (`CallQueue`, **external**, confirm first): `POST /dialer/sessions/{id}/next-batch` with
|
|
37
|
+
`{ previous_batch_id: current_batch_id, dialing_mode, parallel_count, from_phone }`. The response has
|
|
38
|
+
`conference_name`, `queue_remaining`, `queue_exhausted`, `all_dials_failed` and `error_message`. Hand
|
|
39
|
+
`conference_name` to the Softphone (`DialerWorkspace` does this) so the rep's audio joins.
|
|
40
|
+
6. **Work the calls**: hang up (`POST /dialer/sessions/calls/hangup`), mute (`.../calls/mute`), drop a voicemail
|
|
41
|
+
(`.../calls/drop-voicemail`, **external**, confirm first), skip a contact (`.../{id}/calls/skip-contact`).
|
|
42
|
+
7. **Outcome** (`DispositionPanel`, write): `GET /voice/dispositions` for the accepted values, then
|
|
43
|
+
`PATCH /dialer/parallel-calls/calls/{room_name}/disposition` with `{ disposition, notes?, callback_at? }`.
|
|
44
|
+
`room_name` is the call's `call_id`. Setting `callback_at` schedules a follow-up; it does not place the call.
|
|
45
|
+
8. **Review** (`CallLog`, `TranscriptViewer`): `GET /voice/dialer/calls?contact_id=|user_email=|
|
|
46
|
+
parallel_session_id=` (one of the three is required), then `.../calls/{room_name}/transcript` and
|
|
47
|
+
`.../grading` (status `empty` / `pending` until ready), and the recording through your proxy at
|
|
48
|
+
`/voice/calls/{call_id}/recording` (425 while it finalises).
|
|
49
|
+
9. **Stop** (`pause` / `stop`, write): both hang up live calls. Pause, then `next-batch` again to continue.
|
|
50
|
+
|
|
51
|
+
IDs passed between steps: `session_id` (draft -> poll -> next-batch/pause/stop/skip/hangup), `current_batch_id`
|
|
52
|
+
(queue-stats -> next-batch's `previous_batch_id`, makes a retry idempotent), `conference_name` (next-batch ->
|
|
53
|
+
Softphone), `call_id` = `room_name` (live call -> disposition -> transcript -> recording), `contact_id`.
|
|
54
|
+
|
|
55
|
+
## Prerequisites a human must do
|
|
56
|
+
|
|
57
|
+
- A phone number for the org (`where: "app"`): buying, porting or verifying a caller ID is a decision made in
|
|
58
|
+
the graph8 app. `GET /voice/dialer/numbers` must return one.
|
|
59
|
+
- Telephony active and credits available (HTTP 402 otherwise).
|
|
60
|
+
- `npm i @twilio/voice-sdk` if you keep the Twilio adapter, and the browser must grant microphone access.
|
|
61
|
+
- Key scopes: `voice:read`, `voice:write`, `voice:run`.
|
|
62
|
+
|
|
63
|
+
## Defaults (the agent does not need to ask)
|
|
64
|
+
|
|
65
|
+
- Power mode (one call at a time); parallel is opt-in (2 to 10 at once).
|
|
66
|
+
- Each batch is confirmed separately. Do not add auto-advance without asking the human.
|
|
67
|
+
- Poll every 5 seconds.
|
|
68
|
+
- Attribute calls to the signed-in rep: pass `userId` / `userEmail` from your own session.
|
|
69
|
+
- Ask the human only about the **audio vendor** (Twilio adapter by default).
|
|
70
|
+
|
|
71
|
+
## Stack nav pages
|
|
72
|
+
|
|
73
|
+
`CallQueue` and `CallLog` render with no props (a stack uses them as pages): the start form asks for the rep's user id
|
|
74
|
+
and the call log for a rep email. Pass `userId` / `userEmail` from your session to skip those fields.
|
|
75
|
+
|
|
76
|
+
## Gotchas
|
|
77
|
+
|
|
78
|
+
- Parallel dialing: the rep joins the conference **muted** (the server unmutes when someone answers). The
|
|
79
|
+
workspace passes `joinMuted` when the batch has more than one call.
|
|
80
|
+
- `all_dials_failed` pauses the session (bad caller ID, no telephony). Show `error_message`.
|
|
81
|
+
- Voicemail drop plays the rep's cloned voice to a stranger's phone. The confirm dialog names the contact.
|
|
82
|
+
- Another tenant's session id is a 404, the same as a missing one.
|
|
83
|
+
- Manual dial (`POST /dialer/parallel-calls/manual-dial`, external) needs a PAUSED or COMPLETED session and can
|
|
84
|
+
create a contact; it is a server recipe here, not a component.
|
|
85
|
+
|
|
86
|
+
## Operations
|
|
87
|
+
|
|
88
|
+
| Operation | Method and path | Tier | Used by |
|
|
89
|
+
|---|---|---|---|
|
|
90
|
+
| `voice.listDialerNumbers` | GET /voice/dialer/numbers | read | SessionStartForm |
|
|
91
|
+
| `dialer.createSessionDraft` | POST /dialer/sessions/draft | write | SessionStartForm |
|
|
92
|
+
| `dialer.readQueueStats` | GET /dialer/sessions/{id}/queue-stats | read | useDialerSession |
|
|
93
|
+
| `voice.getDialerSession` | GET /dialer/sessions/{id} | read | useDialerSession |
|
|
94
|
+
| `dialer.readSessionGrid` | POST /dialer/sessions/{id}/grid | read | CallQueue |
|
|
95
|
+
| `dialer.dialNextBatch` | POST /dialer/sessions/{id}/next-batch | external | CallQueue |
|
|
96
|
+
| `voice.pauseDialerSession` | POST /dialer/sessions/{id}/pause | write | CallQueue |
|
|
97
|
+
| `voice.stopDialerSession` | POST /dialer/sessions/{id}/stop | write | CallQueue |
|
|
98
|
+
| `dialer.skipContact` | POST /dialer/sessions/{id}/calls/skip-contact | write | CallQueue |
|
|
99
|
+
| `dialer.hangupSessionCall` | POST /dialer/sessions/calls/hangup | write | LiveCallsTable |
|
|
100
|
+
| `dialer.muteCall` | POST /dialer/sessions/calls/mute | write | LiveCallsTable |
|
|
101
|
+
| `dialer.dropSessionVoicemail` | POST /dialer/sessions/calls/drop-voicemail | external | LiveCallsTable |
|
|
102
|
+
| `dialer.mintSoftphoneToken` | GET /dialer/sessions/token | external | Softphone |
|
|
103
|
+
| `voice.listVoiceDispositions` | GET /voice/dispositions | read | DispositionPanel |
|
|
104
|
+
| `dialer.setCallDisposition` | PATCH /dialer/parallel-calls/calls/{room}/disposition | write | DispositionPanel |
|
|
105
|
+
| `voice.listCallsForContact` | GET /voice/dialer/calls | read | CallLog |
|
|
106
|
+
| `voice.getCallTranscript` | GET /voice/dialer/calls/{room}/transcript | read | TranscriptViewer |
|
|
107
|
+
| `voice.getCallGrading` | GET /voice/dialer/calls/{room}/grading | read | TranscriptViewer |
|
|
108
|
+
| `voice.downloadCallRecording` | GET /voice/calls/{id}/recording | read | TranscriptViewer |
|
|
109
|
+
|
|
110
|
+
## Recipes (server side)
|
|
111
|
+
|
|
112
|
+
- `01-create-session-and-dial.ts`: draft, one confirmed batch, poll queue-stats.
|
|
113
|
+
- `02-disposition-calls.ts`: record outcomes from the org's accepted list.
|
|
114
|
+
- `03-review-a-call.ts`: transcript, AI grade, rep performance (`dialerAnalytics`).
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../block.schema.json",
|
|
3
|
+
"id": "dialer",
|
|
4
|
+
"title": "Dialer",
|
|
5
|
+
"summary": "Outbound calling: dialing sessions with a polled queue, a pluggable browser softphone, outcomes, call log and transcripts.",
|
|
6
|
+
"features": [
|
|
7
|
+
"draft a session from a list, then dial batch by batch (each batch confirmed)",
|
|
8
|
+
"queue, live calls and counters by polling (the API has no event stream)",
|
|
9
|
+
"softphone on a pluggable audio transport with a ready Twilio adapter",
|
|
10
|
+
"dispositions with callback scheduling",
|
|
11
|
+
"call log, transcript, AI grading and recording per call"
|
|
12
|
+
],
|
|
13
|
+
"dependsOn": [
|
|
14
|
+
"lists"
|
|
15
|
+
],
|
|
16
|
+
"operations": [
|
|
17
|
+
"create_session_draft_dialer_sessions_draft_post",
|
|
18
|
+
"dial_next_batch_dialer_sessions__session_id__next_batch_post",
|
|
19
|
+
"download_call_recording_voice_calls__call_id__recording_get",
|
|
20
|
+
"drop_session_voicemail_dialer_sessions_calls_drop_voicemail_post",
|
|
21
|
+
"get_call_grading_voice_dialer_calls__room_name__grading_get",
|
|
22
|
+
"get_call_transcript_voice_dialer_calls__room_name__transcript_get",
|
|
23
|
+
"get_dialer_session_dialer_sessions__session_id__get",
|
|
24
|
+
"hangup_session_call_dialer_sessions_calls_hangup_post",
|
|
25
|
+
"list_calls_for_contact_voice_dialer_calls_get",
|
|
26
|
+
"list_dialer_numbers_voice_dialer_numbers_get",
|
|
27
|
+
"list_voice_dispositions_voice_dispositions_get",
|
|
28
|
+
"mint_softphone_token_dialer_sessions_token_get",
|
|
29
|
+
"mute_call_dialer_sessions_calls_mute_post",
|
|
30
|
+
"pause_dialer_session_dialer_sessions__session_id__pause_post",
|
|
31
|
+
"read_queue_stats_dialer_sessions__session_id__queue_stats_get",
|
|
32
|
+
"read_session_grid_dialer_sessions__session_id__grid_post",
|
|
33
|
+
"set_call_disposition_dialer_parallel_calls_calls__livekit_room_name__disposition_patch",
|
|
34
|
+
"skip_contact_dialer_sessions__session_id__calls_skip_contact_post",
|
|
35
|
+
"stop_dialer_session_dialer_sessions__session_id__stop_post"
|
|
36
|
+
],
|
|
37
|
+
"components": [
|
|
38
|
+
{
|
|
39
|
+
"name": "CallQueue",
|
|
40
|
+
"file": "components/call-queue.tsx",
|
|
41
|
+
"purpose": "Run one dialing session: polled counters, calls in flight, waiting contacts, and confirmed Dial next batch, pause, stop and skip.",
|
|
42
|
+
"operations": [
|
|
43
|
+
"dial_next_batch_dialer_sessions__session_id__next_batch_post",
|
|
44
|
+
"get_dialer_session_dialer_sessions__session_id__get",
|
|
45
|
+
"pause_dialer_session_dialer_sessions__session_id__pause_post",
|
|
46
|
+
"read_queue_stats_dialer_sessions__session_id__queue_stats_get",
|
|
47
|
+
"read_session_grid_dialer_sessions__session_id__grid_post",
|
|
48
|
+
"skip_contact_dialer_sessions__session_id__calls_skip_contact_post",
|
|
49
|
+
"stop_dialer_session_dialer_sessions__session_id__stop_post"
|
|
50
|
+
],
|
|
51
|
+
"tiers": [
|
|
52
|
+
"read",
|
|
53
|
+
"write",
|
|
54
|
+
"external"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "SessionStartForm",
|
|
59
|
+
"file": "components/session-start-form.tsx",
|
|
60
|
+
"purpose": "Create a draft session (dials nobody) for a list, caller ID and mode.",
|
|
61
|
+
"operations": [
|
|
62
|
+
"create_session_draft_dialer_sessions_draft_post",
|
|
63
|
+
"list_dialer_numbers_voice_dialer_numbers_get"
|
|
64
|
+
],
|
|
65
|
+
"tiers": [
|
|
66
|
+
"read",
|
|
67
|
+
"write"
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"name": "LiveCallsTable",
|
|
72
|
+
"file": "components/live-calls-table.tsx",
|
|
73
|
+
"purpose": "Calls in the session with hang up, mute and a confirmed voicemail drop.",
|
|
74
|
+
"operations": [
|
|
75
|
+
"drop_session_voicemail_dialer_sessions_calls_drop_voicemail_post",
|
|
76
|
+
"hangup_session_call_dialer_sessions_calls_hangup_post",
|
|
77
|
+
"mute_call_dialer_sessions_calls_mute_post"
|
|
78
|
+
],
|
|
79
|
+
"tiers": [
|
|
80
|
+
"write",
|
|
81
|
+
"external"
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"name": "Softphone",
|
|
86
|
+
"file": "components/softphone.tsx",
|
|
87
|
+
"purpose": "The rep's browser audio endpoint on a pluggable transport; mints the softphone credential behind a confirm dialog.",
|
|
88
|
+
"operations": [
|
|
89
|
+
"mint_softphone_token_dialer_sessions_token_get"
|
|
90
|
+
],
|
|
91
|
+
"tiers": [
|
|
92
|
+
"external"
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"name": "DispositionPanel",
|
|
97
|
+
"file": "components/disposition-panel.tsx",
|
|
98
|
+
"purpose": "Record the outcome of a finished call, with notes and a scheduled callback.",
|
|
99
|
+
"operations": [
|
|
100
|
+
"list_voice_dispositions_voice_dispositions_get",
|
|
101
|
+
"set_call_disposition_dialer_parallel_calls_calls__livekit_room_name__disposition_patch"
|
|
102
|
+
],
|
|
103
|
+
"tiers": [
|
|
104
|
+
"read",
|
|
105
|
+
"write"
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"name": "CallLog",
|
|
110
|
+
"file": "components/call-log.tsx",
|
|
111
|
+
"purpose": "Recent calls for a contact, rep or session with outcome, length and summary.",
|
|
112
|
+
"operations": [
|
|
113
|
+
"list_calls_for_contact_voice_dialer_calls_get"
|
|
114
|
+
],
|
|
115
|
+
"tiers": [
|
|
116
|
+
"read"
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"name": "TranscriptViewer",
|
|
121
|
+
"file": "components/transcript-viewer.tsx",
|
|
122
|
+
"purpose": "Speaker-segmented transcript, summary, AI grading and recording for one call.",
|
|
123
|
+
"operations": [
|
|
124
|
+
"download_call_recording_voice_calls__call_id__recording_get",
|
|
125
|
+
"get_call_grading_voice_dialer_calls__room_name__grading_get",
|
|
126
|
+
"get_call_transcript_voice_dialer_calls__room_name__transcript_get"
|
|
127
|
+
],
|
|
128
|
+
"tiers": [
|
|
129
|
+
"read"
|
|
130
|
+
]
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"name": "DialerWorkspace",
|
|
134
|
+
"file": "components/dialer-workspace.tsx",
|
|
135
|
+
"purpose": "The whole rep screen wiring queue, softphone, outcome panel and transcript together.",
|
|
136
|
+
"operations": [],
|
|
137
|
+
"tiers": []
|
|
138
|
+
}
|
|
139
|
+
],
|
|
140
|
+
"hooks": [
|
|
141
|
+
{
|
|
142
|
+
"name": "useDialerSession",
|
|
143
|
+
"file": "hooks/use-dialer-session.ts",
|
|
144
|
+
"purpose": "Poll a session's queue-stats and live call list every 5 seconds until it is over.",
|
|
145
|
+
"operations": [
|
|
146
|
+
"get_dialer_session_dialer_sessions__session_id__get",
|
|
147
|
+
"read_queue_stats_dialer_sessions__session_id__queue_stats_get"
|
|
148
|
+
]
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"name": "useDispositions",
|
|
152
|
+
"file": "hooks/use-dispositions.ts",
|
|
153
|
+
"purpose": "The call outcomes this org accepts (system and custom).",
|
|
154
|
+
"operations": [
|
|
155
|
+
"list_voice_dispositions_voice_dispositions_get"
|
|
156
|
+
]
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"name": "useDialerNumbers",
|
|
160
|
+
"file": "hooks/use-dialer-numbers.ts",
|
|
161
|
+
"purpose": "Caller IDs available to the dialer with today's usage.",
|
|
162
|
+
"operations": [
|
|
163
|
+
"list_dialer_numbers_voice_dialer_numbers_get"
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
"lib": [
|
|
168
|
+
{
|
|
169
|
+
"file": "lib/dialer-types.ts",
|
|
170
|
+
"purpose": "Session, call, queue and disposition types plus status helpers for the voice service's JSON."
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"file": "lib/softphone-transport.ts",
|
|
174
|
+
"purpose": "The SoftphoneTransport interface (the vendor seam) and a no-op transport."
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"file": "lib/twilio-transport.ts",
|
|
178
|
+
"purpose": "Ready Twilio adapter for the transport interface, using the optional peer @twilio/voice-sdk."
|
|
179
|
+
}
|
|
180
|
+
],
|
|
181
|
+
"recipes": [
|
|
182
|
+
{
|
|
183
|
+
"file": "recipes/01-create-session-and-dial.ts",
|
|
184
|
+
"title": "Create a session, dial a batch and poll it",
|
|
185
|
+
"when": "An agent or job running calls for a rep; the dial step reaches real people and needs approval."
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"file": "recipes/02-disposition-calls.ts",
|
|
189
|
+
"title": "Record outcomes for a session's finished calls",
|
|
190
|
+
"when": "After a batch completes; uses the org's accepted disposition list."
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"file": "recipes/03-review-a-call.ts",
|
|
194
|
+
"title": "Pull a call's transcript, AI grade and rep performance",
|
|
195
|
+
"when": "Coaching and QA; read-only."
|
|
196
|
+
}
|
|
197
|
+
],
|
|
198
|
+
"prerequisites": [
|
|
199
|
+
{
|
|
200
|
+
"id": "phone-number",
|
|
201
|
+
"description": "At least one dialer phone number for the org (provisioned or verified caller ID). Buying or porting a number is a human decision in the graph8 app.",
|
|
202
|
+
"where": "app",
|
|
203
|
+
"verify": "GET /voice/dialer/numbers returns at least one number with is_valid true."
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"id": "telephony-and-credits",
|
|
207
|
+
"description": "Telephony active for the org and credits available: every dial attempt reserves credits and connected minutes charge more (402 when none).",
|
|
208
|
+
"where": "api",
|
|
209
|
+
"verify": "A power-mode next-batch on a one-contact test list returns no error_code."
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"id": "voice-scopes",
|
|
213
|
+
"description": "Key scopes voice:read and voice:write for the screens, voice:run for dialing, the softphone credential and voicemail drop, analytics:read for rep performance. The softphone credential is a bearer secret.",
|
|
214
|
+
"where": "api",
|
|
215
|
+
"verify": "`g8.api.me.describeCurrentKey()` lists voice:read, voice:write and voice:run."
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"id": "audio-client",
|
|
219
|
+
"description": "A browser audio client package for the Softphone: the default adapter needs `npm i @twilio/voice-sdk`. Microphone permission must be granted in the browser.",
|
|
220
|
+
"where": "app",
|
|
221
|
+
"verify": "`import(\"@twilio/voice-sdk\")` resolves and the browser prompts for the microphone."
|
|
222
|
+
}
|
|
223
|
+
],
|
|
224
|
+
"scopes": [
|
|
225
|
+
"voice:read",
|
|
226
|
+
"voice:run",
|
|
227
|
+
"voice:write"
|
|
228
|
+
],
|
|
229
|
+
"defaults": [
|
|
230
|
+
{
|
|
231
|
+
"decision": "Dialing mode",
|
|
232
|
+
"default": "power (one call at a time); parallel is opt-in with 2 to 10 calls at once",
|
|
233
|
+
"askHuman": false
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"decision": "Does the next batch dial automatically after a batch ends",
|
|
237
|
+
"default": "No. Every batch needs its own confirmation; auto-advance is never enabled by the block",
|
|
238
|
+
"askHuman": false
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"decision": "How session state reaches the screen",
|
|
242
|
+
"default": "Polling queue-stats and the session every 5 seconds (the API has no event stream)",
|
|
243
|
+
"askHuman": false
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"decision": "Which rep the calls are attributed to (an organisation API key has no user)",
|
|
247
|
+
"default": "The signed-in user of your app: pass userId (and userEmail) from your session",
|
|
248
|
+
"askHuman": false
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"decision": "Browser audio vendor for the Softphone",
|
|
252
|
+
"default": "Twilio via lib/twilio-transport.ts; graph8's dialer runs on Twilio conferences. For another vendor write a SoftphoneTransport and delete twilio-transport.ts",
|
|
253
|
+
"askHuman": true
|
|
254
|
+
}
|
|
255
|
+
],
|
|
256
|
+
"peerDependencies": {
|
|
257
|
+
"react": ">=18",
|
|
258
|
+
"@twilio/voice-sdk": "^2.18.5"
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CallLog: recent dialer calls for one contact, one rep, or one session, with outcome, length and summary.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations: GET /voice/dialer/calls?contact_id=|user_email=|parallel_session_id=&limit= (voice.listCallsForContact) read
|
|
7
|
+
* The API REQUIRES at least one of contact_id, user_email or parallel_session_id (org alone would dump the whole
|
|
8
|
+
* history) and returns summaries without transcripts; open one in TranscriptViewer via `onSelect`.
|
|
9
|
+
* Org-wide history across all reps: use POST /voice/call-results/search (voice.searchCallResults) from a server recipe.
|
|
10
|
+
*/
|
|
11
|
+
import { useState } from "react";
|
|
12
|
+
import { Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
13
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
14
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
15
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
16
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
17
|
+
import { dataOf, formatDuration, type CallSummary } from "../lib/dialer-types";
|
|
18
|
+
|
|
19
|
+
export interface CallLogProps {
|
|
20
|
+
contactId?: string;
|
|
21
|
+
userEmail?: string;
|
|
22
|
+
sessionId?: string;
|
|
23
|
+
limit?: number;
|
|
24
|
+
onSelect?: (call: CallSummary) => void;
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function when(iso: string | null | undefined): string {
|
|
29
|
+
if (!iso) return "";
|
|
30
|
+
const d = new Date(iso);
|
|
31
|
+
return Number.isNaN(d.getTime()) ? "" : d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function CallLog({ contactId, userEmail, sessionId, limit = 25, onSelect, className }: CallLogProps) {
|
|
35
|
+
const [typedEmail, setTypedEmail] = useState("");
|
|
36
|
+
const repEmail = userEmail || typedEmail.trim();
|
|
37
|
+
const scoped = Boolean(contactId || repEmail || sessionId);
|
|
38
|
+
const q = useG8Query(
|
|
39
|
+
async (signal) => {
|
|
40
|
+
const env = await g8Get("/voice/dialer/calls", { contact_id: contactId, user_email: repEmail || undefined, parallel_session_id: sessionId, limit }, { signal });
|
|
41
|
+
return dataOf<{ calls?: CallSummary[] }>(env).calls ?? [];
|
|
42
|
+
},
|
|
43
|
+
[contactId, repEmail, sessionId, limit],
|
|
44
|
+
{ enabled: scoped },
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (!scoped) {
|
|
48
|
+
return (
|
|
49
|
+
<div className={`space-y-3 ${className ?? ""}`}>
|
|
50
|
+
<EmptyState title="Choose whose calls to show" description="Pass a contactId, userEmail or sessionId, or type a rep's email. The API will not list the whole organisation's calls at once." />
|
|
51
|
+
<Input aria-label="Rep email" type="email" placeholder="rep@company.com" value={typedEmail} onChange={(e) => setTypedEmail(e.target.value)} />
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (q.error) return <ApiError error={q.error} title="Could not load calls" onRetry={() => void q.refetch()} />;
|
|
56
|
+
if (q.loading && !q.data) return <Skeleton className="h-32 w-full" aria-label="Loading calls" />;
|
|
57
|
+
const calls = q.data ?? [];
|
|
58
|
+
if (calls.length === 0) return <EmptyState className={className} title="No calls yet" description="Calls appear here after the dialer places them." />;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Table className={className} aria-label="Call log">
|
|
62
|
+
<TableHeader>
|
|
63
|
+
<TableRow><TableHead>When</TableHead><TableHead>Number</TableHead><TableHead>Outcome</TableHead><TableHead>Length</TableHead><TableHead>Summary</TableHead></TableRow>
|
|
64
|
+
</TableHeader>
|
|
65
|
+
<TableBody>
|
|
66
|
+
{calls.map((c) => (
|
|
67
|
+
<TableRow key={c.room_name} className={onSelect ? "cursor-pointer" : undefined} onClick={() => onSelect?.(c)}>
|
|
68
|
+
<TableCell className="whitespace-nowrap">{when(c.created_at)}</TableCell>
|
|
69
|
+
<TableCell>{c.to}</TableCell>
|
|
70
|
+
<TableCell className="capitalize">{c.disposition?.replace(/_/g, " ") ?? "none"}</TableCell>
|
|
71
|
+
<TableCell>{formatDuration(c.call_duration)}</TableCell>
|
|
72
|
+
<TableCell className="max-w-72 truncate text-muted-foreground">{c.summary ?? (c.has_transcript ? "Transcript available" : "")}</TableCell>
|
|
73
|
+
</TableRow>
|
|
74
|
+
))}
|
|
75
|
+
</TableBody>
|
|
76
|
+
</Table>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CallQueue: run one dialing session. Live counters, the calls in flight, the contacts still waiting, and the
|
|
5
|
+
* controls that start, pause and stop dialing. It POLLS (no event stream exists), see useDialerSession.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. (no session yet) SessionStartForm: GET /voice/dialer/numbers, POST /dialer/sessions/draft write, dials no one
|
|
9
|
+
* 2. every 5s: GET /dialer/sessions/{id}/queue-stats + GET /dialer/sessions/{id} read
|
|
10
|
+
* 3. POST /dialer/sessions/{id}/grid (dialer.readSessionGrid) read - the waiting contacts
|
|
11
|
+
* 4. POST /dialer/sessions/{id}/next-batch (dialer.dialNextBatch) EXTERNAL -> ConfirmSpendDialog first
|
|
12
|
+
* CALLS REAL PEOPLE and reserves credits. Returns { conference_name, twilio_token, ... }: pass them to
|
|
13
|
+
* <Softphone/> through `onBatch` so the rep's browser can join the conference.
|
|
14
|
+
* 5. POST /dialer/sessions/{id}/pause, /stop (voice.pauseDialerSession / stopDialerSession) write - hang up live calls
|
|
15
|
+
* 6. POST /dialer/sessions/{id}/calls/skip-contact (dialer.skipContact) write - reversible
|
|
16
|
+
*
|
|
17
|
+
* Each batch needs its own confirmation by design: dialing is never automatic.
|
|
18
|
+
*/
|
|
19
|
+
import { useEffect, useState } from "react";
|
|
20
|
+
import { Button, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
21
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
23
|
+
import { g8Post } from "@/g8/lib/g8-client";
|
|
24
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
25
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
26
|
+
import { useDialerSession } from "../hooks/use-dialer-session";
|
|
27
|
+
import { contactName, dataOf, firstPhone, isLiveCall, isSessionOver, normalizeStatus, type DialerMode, type LiveCall, type NextBatchResult, type QueueRow } from "../lib/dialer-types";
|
|
28
|
+
import { LiveCallsTable } from "./live-calls-table";
|
|
29
|
+
import { SessionStartForm } from "./session-start-form";
|
|
30
|
+
|
|
31
|
+
export interface CallQueueProps {
|
|
32
|
+
/** Resume an existing session; omit to show the start form. */
|
|
33
|
+
sessionId?: string;
|
|
34
|
+
/** Optional: when omitted the start form asks for it, so CallQueue renders as a stack nav page with no props. */
|
|
35
|
+
userId?: string;
|
|
36
|
+
userEmail?: string;
|
|
37
|
+
listId?: string;
|
|
38
|
+
listTitle?: string;
|
|
39
|
+
/** Fires after each confirmed "Dial next batch" with the conference the rep must join. */
|
|
40
|
+
onBatch?: (batch: NextBatchResult) => void;
|
|
41
|
+
onSessionCreated?: (sessionId: string) => void;
|
|
42
|
+
/** A finished call was clicked: open DispositionPanel for it. */
|
|
43
|
+
onSelectCall?: (call: LiveCall) => void;
|
|
44
|
+
/** Called with every poll result, for sibling components. */
|
|
45
|
+
onCalls?: (calls: LiveCall[]) => void;
|
|
46
|
+
className?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function Stat({ label, value }: { label: string; value: string | number }) {
|
|
50
|
+
return (
|
|
51
|
+
<div className="rounded-lg border p-3">
|
|
52
|
+
<div className="text-xs uppercase text-muted-foreground">{label}</div>
|
|
53
|
+
<div className="text-lg font-semibold capitalize">{value}</div>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function WaitingContacts({ sessionId, version, onSkipped }: { sessionId: string; version: number; onSkipped: () => void }) {
|
|
59
|
+
const grid = useG8Query(
|
|
60
|
+
async (signal) => {
|
|
61
|
+
const env = await g8Post(`/dialer/sessions/${encodeURIComponent(sessionId)}/grid`, { limit: 10, offset: 0, include_contact_data: true }, { signal });
|
|
62
|
+
const data = dataOf<{ items?: QueueRow[]; rows?: QueueRow[] }>(env);
|
|
63
|
+
return data.items ?? data.rows ?? [];
|
|
64
|
+
},
|
|
65
|
+
[sessionId, version],
|
|
66
|
+
);
|
|
67
|
+
const [skipping, setSkipping] = useState<string | null>(null);
|
|
68
|
+
const skip = useG8Mutation(async (row: QueueRow) => { setSkipping(row.contact_id); try { return await g8Post(`/dialer/sessions/${encodeURIComponent(sessionId)}/calls/skip-contact`, { contact_id: row.contact_id }); } finally { setSkipping(null); } }, { onSuccess: onSkipped });
|
|
69
|
+
if (grid.error) return <ApiError error={grid.error} title="Could not load the queue" onRetry={() => void grid.refetch()} />;
|
|
70
|
+
if (grid.loading && !grid.data) return <Skeleton className="h-24 w-full" aria-label="Loading queue" />;
|
|
71
|
+
const rows = (grid.data ?? []).filter((r) => !r.callresult?.disposition);
|
|
72
|
+
return (
|
|
73
|
+
<div className="space-y-2">
|
|
74
|
+
<h3 className="text-sm font-semibold">Up next</h3>
|
|
75
|
+
{skip.error ? <ApiError error={skip.error} title="Could not skip that contact" /> : null}
|
|
76
|
+
<Table aria-label="Contacts waiting">
|
|
77
|
+
<TableHeader>
|
|
78
|
+
<TableRow><TableHead>Contact</TableHead><TableHead>Company</TableHead><TableHead>Number</TableHead><TableHead /></TableRow>
|
|
79
|
+
</TableHeader>
|
|
80
|
+
<TableBody>
|
|
81
|
+
{rows.map((r) => (
|
|
82
|
+
<TableRow key={r.contact_id}>
|
|
83
|
+
<TableCell className="font-medium">{contactName(r)}{r.is_next_to_dial ? " (next)" : ""}</TableCell>
|
|
84
|
+
<TableCell>{r.company}</TableCell>
|
|
85
|
+
<TableCell>{firstPhone(r) || (r.blocked_reason ? `blocked: ${r.blocked_reason}` : "no number")}</TableCell>
|
|
86
|
+
<TableCell className="text-right"><Button size="sm" variant="ghost" disabled={skipping === r.contact_id} onClick={() => void skip.mutate(r)}>Skip</Button></TableCell>
|
|
87
|
+
</TableRow>
|
|
88
|
+
))}
|
|
89
|
+
</TableBody>
|
|
90
|
+
</Table>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function CallQueue({ sessionId: initialId, userId, userEmail, listId, listTitle, onBatch, onSessionCreated, onSelectCall, onCalls, className }: CallQueueProps) {
|
|
96
|
+
const [sessionId, setSessionId] = useState(initialId);
|
|
97
|
+
useEffect(() => { setSessionId(initialId); }, [initialId]); // follow the prop when the caller switches sessions
|
|
98
|
+
const [stopping, setStopping] = useState(false);
|
|
99
|
+
const [dial, setDial] = useState({ mode: "power" as DialerMode, parallelCount: 1, fromPhone: "" });
|
|
100
|
+
const [confirming, setConfirming] = useState(false);
|
|
101
|
+
const [version, setVersion] = useState(0);
|
|
102
|
+
const session = useDialerSession(sessionId);
|
|
103
|
+
const bump = () => { setVersion((v) => v + 1); session.refresh(); };
|
|
104
|
+
const mode = session.detail?.dialing_mode ?? dial.mode;
|
|
105
|
+
const parallelCount = mode === "parallel" ? (session.detail?.parallel_count ?? dial.parallelCount) : 1;
|
|
106
|
+
useEffect(() => { onCalls?.(session.calls); }, [session.calls]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
107
|
+
|
|
108
|
+
const nextBatch = useG8Mutation(
|
|
109
|
+
async () => {
|
|
110
|
+
const env = await g8Post(`/dialer/sessions/${encodeURIComponent(sessionId as string)}/next-batch`, {
|
|
111
|
+
previous_batch_id: session.stats?.current_batch_id ?? null,
|
|
112
|
+
from_phone: dial.fromPhone || session.detail?.from_phone || undefined,
|
|
113
|
+
dialing_mode: mode,
|
|
114
|
+
parallel_count: parallelCount,
|
|
115
|
+
user_email: userEmail,
|
|
116
|
+
});
|
|
117
|
+
return dataOf<NextBatchResult>(env);
|
|
118
|
+
},
|
|
119
|
+
{ onSuccess: (batch) => { setConfirming(false); onBatch?.(batch); bump(); }, onError: () => setConfirming(false) },
|
|
120
|
+
);
|
|
121
|
+
const pause = useG8Mutation(() => g8Post(`/dialer/sessions/${encodeURIComponent(sessionId as string)}/pause`, undefined, { query: { user_email: userEmail } }), { onSuccess: bump });
|
|
122
|
+
const stop = useG8Mutation(() => g8Post(`/dialer/sessions/${encodeURIComponent(sessionId as string)}/stop`), { onSuccess: () => { setStopping(false); bump(); }, onError: () => setStopping(false) });
|
|
123
|
+
|
|
124
|
+
if (!sessionId) {
|
|
125
|
+
return (
|
|
126
|
+
<SessionStartForm
|
|
127
|
+
className={className}
|
|
128
|
+
userId={userId}
|
|
129
|
+
userEmail={userEmail}
|
|
130
|
+
listId={listId}
|
|
131
|
+
listTitle={listTitle}
|
|
132
|
+
onCreated={(s) => { setDial({ mode: s.mode, parallelCount: s.parallelCount, fromPhone: s.fromPhone }); setSessionId(s.sessionId); onSessionCreated?.(s.sessionId); }}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
const stats = session.stats;
|
|
137
|
+
const over = isSessionOver(stats?.status);
|
|
138
|
+
const batchResult = nextBatch.data;
|
|
139
|
+
const failure = nextBatch.error ?? pause.error ?? stop.error ?? session.error;
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div className={`space-y-4 ${className ?? ""}`}>
|
|
143
|
+
<header className="flex flex-wrap items-center justify-between gap-2">
|
|
144
|
+
<h2 className="text-base font-semibold">{session.detail?.name ?? "Dialing session"}</h2>
|
|
145
|
+
<div className="flex gap-2">
|
|
146
|
+
<Button disabled={!stats || over || nextBatch.loading} onClick={() => setConfirming(true)}>Dial next batch</Button>
|
|
147
|
+
<Button variant="outline" disabled={!stats || over} onClick={() => void pause.mutate(undefined)}>Pause</Button>
|
|
148
|
+
<Button variant="outline" disabled={!stats || over} onClick={() => setStopping(true)}>Stop</Button>
|
|
149
|
+
</div>
|
|
150
|
+
</header>
|
|
151
|
+
{failure ? <ApiError error={failure} title="Session problem" onRetry={() => session.refresh()} /> : null}
|
|
152
|
+
{batchResult?.all_dials_failed ? <p role="alert" className="text-sm text-destructive">{batchResult.error_message ?? "Every call in that batch failed to connect. The session was paused."}</p> : null}
|
|
153
|
+
{batchResult?.queue_exhausted ? <p className="text-sm text-muted-foreground">The queue is empty. Everyone on the list has been called.</p> : null}
|
|
154
|
+
{!stats ? <Skeleton className="h-20 w-full" aria-label="Loading session" /> : (
|
|
155
|
+
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
156
|
+
<Stat label="Status" value={normalizeStatus(stats.status)} />
|
|
157
|
+
<Stat label="Batch" value={(stats.batch_status ?? "idle").replace(/_/g, " ")} />
|
|
158
|
+
<Stat label="Left in queue" value={stats.queue_remaining} />
|
|
159
|
+
<Stat label="Retries pending" value={stats.retry_pending ?? 0} />
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
162
|
+
<LiveCallsTable sessionId={sessionId} calls={session.calls} onChanged={bump} onSelect={onSelectCall} />
|
|
163
|
+
<WaitingContacts sessionId={sessionId} version={version} onSkipped={bump} />
|
|
164
|
+
<ConfirmSpendDialog
|
|
165
|
+
open={stopping}
|
|
166
|
+
tier="destructive"
|
|
167
|
+
title="Stop this session?"
|
|
168
|
+
description="Every live call is hung up and the session is completed. It cannot be resumed; start a new session to keep calling."
|
|
169
|
+
confirmLabel="Stop session"
|
|
170
|
+
affected={session.calls.filter((c) => isLiveCall(c)).length}
|
|
171
|
+
confirming={stop.loading}
|
|
172
|
+
onConfirm={() => void stop.mutate(undefined)}
|
|
173
|
+
onCancel={() => setStopping(false)}
|
|
174
|
+
/>
|
|
175
|
+
<ConfirmSpendDialog
|
|
176
|
+
open={confirming}
|
|
177
|
+
tier="external"
|
|
178
|
+
title={mode === "parallel" ? `Dial the next ${parallelCount} contacts at once?` : "Dial the next contact?"}
|
|
179
|
+
description="This places real phone calls to people on your list and reserves credits for each attempt. Connected minutes charge more."
|
|
180
|
+
confirmLabel="Dial now"
|
|
181
|
+
recipients={parallelCount}
|
|
182
|
+
confirming={nextBatch.loading}
|
|
183
|
+
onConfirm={() => void nextBatch.mutate(undefined)}
|
|
184
|
+
onCancel={() => setConfirming(false)}
|
|
185
|
+
/>
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
}
|