@graph8/sdk 0.362.0 → 0.373.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -10
- package/blocks/_foundation/README.md +78 -1
- package/blocks/_foundation/block.json +123 -6
- package/blocks/_foundation/components/api-error.tsx +9 -7
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/confirmed-action.tsx +90 -0
- package/blocks/_foundation/components/connect-graph8.tsx +280 -0
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +2 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-brand.ts +52 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/connect-route.ts +15 -0
- package/blocks/_foundation/server/connect.ts +127 -0
- package/blocks/_foundation/server/local-key.ts +70 -0
- package/blocks/_foundation/server/proxy.ts +103 -3
- package/blocks/_foundation/server/route.ts +16 -4
- package/blocks/agents/README.md +93 -2
- package/blocks/agents/block.json +343 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +29 -10
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +119 -0
- package/blocks/agents/components/agent-runs.tsx +19 -10
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +226 -0
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +222 -0
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +38 -11
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +33 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +4 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +38 -18
- package/blocks/appointments/components/meetings-screen.tsx +7 -4
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/appointments/components/scheduling-screen.tsx +2 -1
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +17 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +17 -6
- package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +163 -0
- package/blocks/campaigns/components/icp-editor.tsx +22 -17
- package/blocks/campaigns/components/persona-detail.tsx +181 -0
- package/blocks/campaigns/components/persona-editor.tsx +19 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
- package/blocks/coach/README.md +10 -0
- package/blocks/coach/block.json +16 -1
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/coach/components/coach-screen.tsx +222 -0
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +161 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +14 -5
- package/blocks/copilot/components/copilot-screen.tsx +148 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
- package/blocks/crm/README.md +34 -0
- package/blocks/crm/block.json +107 -2
- package/blocks/crm/components/companies-screen.tsx +359 -0
- package/blocks/crm/components/company-detail.tsx +10 -8
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +10 -7
- package/blocks/crm/components/contact-lookup-link.tsx +44 -0
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +377 -0
- package/blocks/crm/components/dedupe-review.tsx +13 -6
- package/blocks/crm/components/import-wizard.tsx +7 -9
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +6 -4
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +399 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +9 -1
- package/blocks/crm/lib/crm-types.ts +26 -2
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +24 -6
- package/blocks/deals/block.json +160 -5
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +25 -13
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +31 -10
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +66 -26
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +5 -5
- package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +45 -4
- package/blocks/dialer/block.json +84 -3
- package/blocks/dialer/components/call-contact-button.tsx +269 -0
- package/blocks/dialer/components/call-log-screen.tsx +208 -0
- package/blocks/dialer/components/call-log.tsx +23 -7
- package/blocks/dialer/components/call-queue.tsx +4 -4
- package/blocks/dialer/components/dialer-screen.tsx +361 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +61 -12
- package/blocks/dialer/components/softphone.tsx +4 -2
- package/blocks/enrichment/README.md +29 -7
- package/blocks/enrichment/block.json +49 -9
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
- package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
- package/blocks/enrichment/components/enrich-button.tsx +32 -9
- package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
- package/blocks/enrichment/components/waterfall-config.tsx +66 -17
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +8 -0
- package/blocks/forms/block.json +139 -18
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +4 -2
- package/blocks/forms/components/form-embed.tsx +4 -2
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +56 -19
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +17 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +32 -3
- package/blocks/inbox/block.json +29 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
- package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
- package/blocks/inbox/lib/inbox-types.ts +47 -1
- package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
- package/blocks/integrations/README.md +63 -1
- package/blocks/integrations/block.json +234 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +119 -0
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +13 -8
- package/blocks/integrations/components/integrations-screen.tsx +152 -0
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +11 -9
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +179 -0
- package/blocks/integrations/components/webhook-manager.tsx +43 -25
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +11 -1
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +13 -10
- package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/block.json +13 -8
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +20 -1
- package/blocks/lists/block.json +69 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +20 -9
- package/blocks/lists/components/list-picker.tsx +17 -3
- package/blocks/lists/components/list-table.tsx +12 -6
- package/blocks/lists/components/lists-screen.tsx +346 -0
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +33 -8
- package/blocks/lists/components/suppressions-screen.tsx +264 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +69 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
- package/blocks/mailboxes/components/domain-detail.tsx +148 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
- package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
- package/blocks/mailboxes/components/warmup-health.tsx +24 -22
- package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +157 -24
- package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/block.json +28 -14
- package/blocks/nurture/components/nurture-builder.tsx +13 -3
- package/blocks/nurture/components/nurture-enroll.tsx +12 -10
- package/blocks/nurture/components/nurture-form.tsx +27 -10
- package/blocks/nurture/components/nurture-screen.tsx +26 -11
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +18 -1
- package/blocks/objects/block.json +42 -21
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +10 -2
- package/blocks/objects/components/object-schema-editor.tsx +4 -2
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +140 -23
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +9 -4
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-list.tsx +2 -1
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/quotes/README.md +13 -0
- package/blocks/quotes/block.json +65 -13
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +5 -3
- package/blocks/quotes/components/quote-related.tsx +8 -4
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-report.tsx +4 -2
- package/blocks/radar/components/radar-screen.tsx +214 -0
- package/blocks/radar/components/report-detail.tsx +104 -0
- package/blocks/search/README.md +10 -0
- package/blocks/search/block.json +16 -1
- package/blocks/search/components/filter-builder.tsx +3 -3
- package/blocks/search/components/prospect-results.tsx +5 -3
- package/blocks/search/components/prospect-search-screen.tsx +235 -0
- package/blocks/search/components/saved-searches.tsx +4 -1
- package/blocks/search/hooks/use-filter-options.ts +20 -6
- package/blocks/search/lib/search-types.ts +2 -0
- package/blocks/sequencer/README.md +55 -9
- package/blocks/sequencer/block.json +126 -19
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +224 -60
- package/blocks/sequencer/components/sequence-detail.tsx +261 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
- package/blocks/sequencer/components/sequence-list.tsx +34 -9
- package/blocks/sequencer/components/sequence-stats.tsx +11 -10
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +162 -0
- package/blocks/sequencer/components/step-editor.tsx +14 -12
- package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
- package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
- package/blocks/social-ads/README.md +19 -0
- package/blocks/social-ads/block.json +233 -1
- package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
- package/blocks/social-ads/components/ads-screen.tsx +152 -0
- package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
- package/blocks/social-ads/components/audience-sync.tsx +17 -9
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +138 -0
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +155 -0
- package/blocks/social-ads/components/creative-gallery.tsx +35 -7
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +7 -3
- package/blocks/social-ads/components/post-detail.tsx +163 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +187 -0
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +21 -10
- package/blocks/visitors/components/traffic-chart.tsx +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +26 -16
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +16 -7
- package/blocks/visitors/components/visitors-screen.tsx +166 -0
- package/blocks/webchat/README.md +10 -0
- package/blocks/webchat/block.json +21 -7
- package/blocks/webchat/components/chat-widget-config.tsx +8 -5
- package/blocks/webchat/components/conversation-list.tsx +8 -4
- package/blocks/webchat/components/webchat-screen.tsx +245 -0
- package/blocks/workbench/README.md +11 -0
- package/blocks/workbench/block.json +48 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +10 -4
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +202 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workflows/README.md +13 -0
- package/blocks/workflows/block.json +28 -9
- package/blocks/workflows/components/run-history.tsx +31 -9
- package/blocks/workflows/components/trigger-config.tsx +3 -1
- package/blocks/workflows/components/workflow-editor.tsx +4 -2
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +3109 -1977
- package/dist/errors.d.mts +34 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +21 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.browser.js +11 -5
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +11 -5
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +164 -25
- package/dist/index.d.ts +164 -25
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +10 -4
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +10 -4
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +142 -5
- package/dist/react.d.ts +142 -5
- package/dist/react.js +10 -4
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +10 -4
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/add.d.ts +11 -4
- package/dist/types/blocks/brand.d.ts +122 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +27 -20
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/blocks/paths.d.ts +4 -0
- package/dist/types/blocks/prefixes.d.ts +37 -0
- package/dist/types/blocks/registry.d.ts +17 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +5 -0
- package/dist/types/errors.d.ts +33 -0
- package/dist/types/http.d.ts +2 -18
- package/dist/types/version.d.ts +2 -2
- package/docs/agents.md +15 -4
- package/docs/api/README.md +2 -2
- package/docs/api/by-path.md +1 -0
- package/docs/api/desk.d.ts +98 -2
- package/docs/api/desk.md +123 -1
- package/docs/api/inbox.d.ts +10 -2
- package/docs/api/inbox.md +10 -2
- package/docs/api/seo.d.ts +19 -1
- package/docs/api/seo.md +24 -1
- package/package.json +16 -2
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +55 -17
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +147 -37
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +64 -0
- package/stacks/clay/pages/companies.tsx.tmpl +31 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +73 -27
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +98 -35
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +104 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +181 -64
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +52 -20
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +68 -34
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +106 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +117 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +85 -31
- package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +115 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +40 -9
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/app/layout.tsx.tmpl +7 -2
- package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CallContactButton: call ONE contact from any screen (a contact page, a table row), through the same
|
|
5
|
+
* Twilio-backed dialer as CallQueue, with the rep's softphone joining the call.
|
|
6
|
+
*
|
|
7
|
+
* Why manual-dial (and not next-batch's force_next_contact): `force_next_contact` only picks a contact that is IN
|
|
8
|
+
* the session's list, and next-batch also dials the rest of the batch. `manual-dial` places exactly one call to the
|
|
9
|
+
* number you pass, attaches it to an existing session (same analytics and call log), and answers with the
|
|
10
|
+
* `conference_name` the softphone joins. It needs a PAUSED or COMPLETED session (an idle ACTIVE one is re-paused by
|
|
11
|
+
* voice; one with live calls is refused) and refuses DRAFT, so the picker offers paused and completed sessions.
|
|
12
|
+
*
|
|
13
|
+
* SDK operations, in order:
|
|
14
|
+
* 0. GET /team-members/me (teamMembers.getMyTeamMember) read - only without `userId`: the rep is the person who
|
|
15
|
+
* minted a PERSONAL key (`propelauth_user_id`, `email`). An organisation key is refused there (403), so the
|
|
16
|
+
* panel then asks for `userId`: an org key is not a rep.
|
|
17
|
+
* 1. GET /voice/dialer/sessions?status=PAUSED|COMPLETED (voice.listDialerSessions) read - the session to log under
|
|
18
|
+
* 2. GET /voice/dialer/numbers (voice.listDialerNumbers) read - caller ID
|
|
19
|
+
* 3. GET /dialer/sessions/token?user_id= (Softphone, EXTERNAL, confirmed inside Softphone)
|
|
20
|
+
* 4. POST /dialer/parallel-calls/manual-dial (dialer.manualDial) EXTERNAL -> ConfirmSpendDialog
|
|
21
|
+
* CALLS A REAL PERSON and spends dialer credits. An unknown number is matched to a contact (or one is created).
|
|
22
|
+
* The response's `conference_name` goes to the Softphone, which joins with audio.
|
|
23
|
+
*
|
|
24
|
+
* The call is only offered once the softphone is ready, so the rep is never late to their own call (the response
|
|
25
|
+
* is the first moment the conference name is known; a softphone that registers after it would join late).
|
|
26
|
+
*/
|
|
27
|
+
import { useEffect, useRef, useState } from "react";
|
|
28
|
+
import { Phone } from "lucide-react";
|
|
29
|
+
import { Button } from "@graph8/sdk/ui";
|
|
30
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
31
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
32
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
33
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
34
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
35
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
36
|
+
import { useCurrentMember } from "@/g8/pickers/hooks/use-current-member";
|
|
37
|
+
import { useDialerNumbers } from "../hooks/use-dialer-numbers";
|
|
38
|
+
import { dataOf } from "../lib/dialer-types";
|
|
39
|
+
import type { SoftphoneTransport } from "../lib/softphone-transport";
|
|
40
|
+
import { Softphone, type SoftphoneState } from "./softphone";
|
|
41
|
+
|
|
42
|
+
export interface CallableContact {
|
|
43
|
+
id?: string | number | null;
|
|
44
|
+
first_name?: string | null;
|
|
45
|
+
last_name?: string | null;
|
|
46
|
+
full_name?: string | null;
|
|
47
|
+
name?: string | null;
|
|
48
|
+
email?: string | null;
|
|
49
|
+
company?: string | null;
|
|
50
|
+
company_name?: string | null;
|
|
51
|
+
phone?: string | null;
|
|
52
|
+
mobile_phone?: string | null;
|
|
53
|
+
direct_phone?: string | null;
|
|
54
|
+
work_phone?: string | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface CallContactButtonProps {
|
|
58
|
+
contact: CallableContact;
|
|
59
|
+
/** The rep's user id: the softphone credential and the call are minted for it. Without it the button explains. */
|
|
60
|
+
userId?: string;
|
|
61
|
+
/** The rep's email. Only used together with `userId`; without `userId` both come from the personal key's user. */
|
|
62
|
+
userEmail?: string;
|
|
63
|
+
/** Log the call under this session (PAUSED or COMPLETED). Omit to pick one. */
|
|
64
|
+
sessionId?: string;
|
|
65
|
+
/** Caller ID. Defaults to the session's number, else the first dialer number. */
|
|
66
|
+
fromPhone?: string;
|
|
67
|
+
/** Your audio client (`createTwilioTransport()`); by default the Twilio adapter loads on demand. */
|
|
68
|
+
transport?: SoftphoneTransport;
|
|
69
|
+
label?: string;
|
|
70
|
+
onCalled?: (result: ManualDialResult) => void;
|
|
71
|
+
className?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ManualDialResult {
|
|
75
|
+
session_id?: string;
|
|
76
|
+
status?: string;
|
|
77
|
+
conference_name?: string | null;
|
|
78
|
+
calls?: Array<{ call_id?: string; to_phone?: string; status?: string }>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface SessionOption {
|
|
82
|
+
session_id: string;
|
|
83
|
+
name?: string | null;
|
|
84
|
+
status?: string | null;
|
|
85
|
+
from_phone?: string | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const CALLABLE = ["PAUSED", "COMPLETED"];
|
|
89
|
+
|
|
90
|
+
/** Every distinct phone on the contact, labelled by field. */
|
|
91
|
+
export function contactPhones(c: CallableContact): Array<{ field: string; phone: string }> {
|
|
92
|
+
const out: Array<{ field: string; phone: string }> = [];
|
|
93
|
+
for (const field of ["mobile_phone", "direct_phone", "work_phone", "phone"] as const) {
|
|
94
|
+
const phone = (c[field] ?? "").trim();
|
|
95
|
+
// A locked field arrives masked as "***" (search results before saving): never offer it as a number.
|
|
96
|
+
if (phone && !phone.includes("*") && !out.some((p) => p.phone === phone)) out.push({ field, phone });
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function displayName(c: CallableContact): string {
|
|
102
|
+
return c.full_name || c.name || [c.first_name, c.last_name].filter(Boolean).join(" ") || c.email || "this contact";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Dialer sessions per read and the most pages read per status; beyond that the picker says so. */
|
|
106
|
+
const SESSION_PAGE = 25;
|
|
107
|
+
const SESSION_PAGES = 8;
|
|
108
|
+
|
|
109
|
+
export function CallContactButton(props: CallContactButtonProps) {
|
|
110
|
+
const { contact, label = "Call", className } = props;
|
|
111
|
+
const [open, setOpen] = useState(false);
|
|
112
|
+
const phones = contactPhones(contact);
|
|
113
|
+
if (open) return <CallContactPanel {...props} onClose={() => setOpen(false)} />;
|
|
114
|
+
const name = displayName(contact);
|
|
115
|
+
// Closed: nothing is loaded, so a table of rows with this button makes no requests until one is clicked.
|
|
116
|
+
return (
|
|
117
|
+
<Button size="sm" variant="outline" className={className} disabled={phones.length === 0} title={phones.length === 0 ? `${name} has no phone number` : undefined} onClick={() => setOpen(true)}>
|
|
118
|
+
<Phone className="mr-1 h-4 w-4" aria-hidden /> {label}
|
|
119
|
+
</Button>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function CallContactPanel(props: CallContactButtonProps & { onClose: () => void }) {
|
|
124
|
+
const { contact, sessionId, fromPhone, transport, onCalled, onClose, className } = props;
|
|
125
|
+
// The shared resolver (foundation): the same one DialerScreen uses, so the two cannot drift apart.
|
|
126
|
+
const me = useCurrentMember({ enabled: !props.userId });
|
|
127
|
+
const userId = props.userId ?? me.userId;
|
|
128
|
+
// user_id and user_email must describe the same person: a userEmail without a userId (e.g. a history filtered to
|
|
129
|
+
// another rep) is ignored, and both come from /team-members/me.
|
|
130
|
+
const userEmail = props.userId ? props.userEmail : me.email;
|
|
131
|
+
const phones = contactPhones(contact);
|
|
132
|
+
const [phone, setPhone] = useState(phones[0]?.phone ?? "");
|
|
133
|
+
const [session, setSession] = useState(sessionId ?? "");
|
|
134
|
+
const [caller, setCaller] = useState(fromPhone ?? "");
|
|
135
|
+
const [softphone, setSoftphone] = useState<SoftphoneState>("off");
|
|
136
|
+
const [confirming, setConfirming] = useState(false);
|
|
137
|
+
const [conference, setConference] = useState<string | null>(null);
|
|
138
|
+
const audio = useLazyTransport(transport, true);
|
|
139
|
+
|
|
140
|
+
// Every page of each status is read (25 per page) up to SESSION_PAGES pages; `capped` says the cap cut a status short.
|
|
141
|
+
const capped = useRef(false);
|
|
142
|
+
const sessions = useG8Query(
|
|
143
|
+
async (signal) => {
|
|
144
|
+
let cut = false;
|
|
145
|
+
const lists = await Promise.all(
|
|
146
|
+
CALLABLE.map(async (status) => {
|
|
147
|
+
const rows: SessionOption[] = [];
|
|
148
|
+
for (let page = 1; page <= SESSION_PAGES; page++) {
|
|
149
|
+
const got = dataOf<{ sessions?: SessionOption[] }>(await g8Get("/voice/dialer/sessions", { status, page, page_size: SESSION_PAGE, user_email: userEmail }, { signal })).sessions ?? [];
|
|
150
|
+
rows.push(...got);
|
|
151
|
+
if (got.length < SESSION_PAGE) return rows;
|
|
152
|
+
}
|
|
153
|
+
cut = true;
|
|
154
|
+
return rows;
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
capped.current = cut;
|
|
158
|
+
return lists.flat();
|
|
159
|
+
},
|
|
160
|
+
[userEmail],
|
|
161
|
+
{ enabled: !sessionId },
|
|
162
|
+
);
|
|
163
|
+
const numbers = useDialerNumbers();
|
|
164
|
+
const chosenSession = (sessions.data ?? []).find((s) => s.session_id === session);
|
|
165
|
+
const from = caller || chosenSession?.from_phone || numbers.numbers[0]?.phone_number || "";
|
|
166
|
+
|
|
167
|
+
const dial = useG8Mutation(async () => {
|
|
168
|
+
const env = await g8Post<ManualDialResult>("/dialer/parallel-calls/manual-dial", {
|
|
169
|
+
session_id: session,
|
|
170
|
+
user_id: userId,
|
|
171
|
+
user_email: userEmail,
|
|
172
|
+
to_phone: phone,
|
|
173
|
+
from_phone: from,
|
|
174
|
+
first_name: contact.first_name ?? undefined,
|
|
175
|
+
last_name: contact.last_name ?? undefined,
|
|
176
|
+
company: contact.company ?? contact.company_name ?? undefined,
|
|
177
|
+
contact_id: contact.id != null ? String(contact.id) : undefined,
|
|
178
|
+
email: contact.email ?? undefined,
|
|
179
|
+
});
|
|
180
|
+
return dataOf<ManualDialResult>(env);
|
|
181
|
+
}, { invalidates: ["dialer"] });
|
|
182
|
+
|
|
183
|
+
const confirm = async () => {
|
|
184
|
+
try {
|
|
185
|
+
const result = await dial.mutateAsync(undefined);
|
|
186
|
+
setConference(result.conference_name ?? null);
|
|
187
|
+
onCalled?.(result);
|
|
188
|
+
} catch {
|
|
189
|
+
// ApiError below shows why; nothing was dialed on a refusal.
|
|
190
|
+
} finally {
|
|
191
|
+
setConfirming(false);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const name = displayName(contact);
|
|
196
|
+
const missing = !userId
|
|
197
|
+
? me.loading
|
|
198
|
+
? "Finding your user..."
|
|
199
|
+
: "Calling from the browser needs the rep: pass `userId`, or use a personal API key (an organisation key is not a rep)."
|
|
200
|
+
: phones.length === 0
|
|
201
|
+
? `${name} has no phone number.`
|
|
202
|
+
: !session
|
|
203
|
+
? "Pick a paused or completed dialer session to log the call under."
|
|
204
|
+
: !from
|
|
205
|
+
? "Pick the number to call from."
|
|
206
|
+
: softphone !== "ready" && softphone !== "in-call"
|
|
207
|
+
? "Turn on the softphone first, so you are on the line when they answer."
|
|
208
|
+
: null;
|
|
209
|
+
|
|
210
|
+
const select = "h-9 w-full rounded-md border border-border bg-background px-2 text-sm";
|
|
211
|
+
return (
|
|
212
|
+
<div className={`space-y-3 rounded-lg border border-border p-3 text-sm ${className ?? ""}`} aria-label={`Call ${name}`}>
|
|
213
|
+
<div className="flex items-center justify-between">
|
|
214
|
+
<p className="font-medium">Call {name}</p>
|
|
215
|
+
<Button size="sm" variant="ghost" onClick={onClose}>Close</Button>
|
|
216
|
+
</div>
|
|
217
|
+
<select aria-label="Number to call" className={select} value={phone} onChange={(e) => setPhone(e.target.value)}>
|
|
218
|
+
{phones.map((p) => <option key={p.phone} value={p.phone}>{p.phone} ({p.field.replace("_", " ")})</option>)}
|
|
219
|
+
</select>
|
|
220
|
+
{!sessionId ? (
|
|
221
|
+
<select aria-label="Dialer session" className={select} value={session} onChange={(e) => setSession(e.target.value)}>
|
|
222
|
+
<option value="">{sessions.loading ? "Loading sessions..." : "Log the call under session..."}</option>
|
|
223
|
+
{(sessions.data ?? []).map((s) => <option key={s.session_id} value={s.session_id}>{s.name || s.session_id} ({(s.status ?? "").toLowerCase()})</option>)}
|
|
224
|
+
</select>
|
|
225
|
+
) : null}
|
|
226
|
+
{!sessionId ? <FirstNotice force={capped.current} shown={(sessions.data ?? []).length} limit={0} noun="sessions" /> : null}
|
|
227
|
+
{!sessionId && sessions.data?.length === 0 ? (
|
|
228
|
+
<p className="text-muted-foreground">No paused or completed session yet. Start one on the Dialer screen and pause it; the call is logged under it.</p>
|
|
229
|
+
) : null}
|
|
230
|
+
<select aria-label="Call from" className={select} value={from} onChange={(e) => setCaller(e.target.value)}>
|
|
231
|
+
{numbers.numbers.map((n) => <option key={n.phone_number} value={n.phone_number}>{n.friendly_name || n.phone_number}</option>)}
|
|
232
|
+
{from && !numbers.numbers.some((n) => n.phone_number === from) ? <option value={from}>{from}</option> : null}
|
|
233
|
+
</select>
|
|
234
|
+
{userId && audio ? (
|
|
235
|
+
<Softphone userId={userId} transport={audio} conferenceName={conference} onStateChange={setSoftphone} />
|
|
236
|
+
) : null}
|
|
237
|
+
{sessions.error || numbers.error ? <ApiError error={sessions.error ?? numbers.error} title="Could not load the dialer" onRetry={() => { void sessions.refetch(); void numbers.refetch(); }} /> : null}
|
|
238
|
+
{dial.error ? <ApiError error={dial.error} title="The call was not placed" /> : null}
|
|
239
|
+
{conference ? <p role="status">Calling {name} at {phone}. You join the call through the softphone.</p> : null}
|
|
240
|
+
{missing ? <p className="text-muted-foreground">{missing}</p> : null}
|
|
241
|
+
<Button disabled={Boolean(missing) || dial.loading} onClick={() => setConfirming(true)}>
|
|
242
|
+
<Phone className="mr-1 h-4 w-4" aria-hidden /> Call {phone}
|
|
243
|
+
</Button>
|
|
244
|
+
<ConfirmSpendDialog
|
|
245
|
+
open={confirming}
|
|
246
|
+
tier="external"
|
|
247
|
+
title={`Call ${name}?`}
|
|
248
|
+
description={`This calls ${phone} now from ${from}, through your softphone, and spends dialer credits per attempt and per connected minute.`}
|
|
249
|
+
recipients={1}
|
|
250
|
+
confirmLabel="Call now"
|
|
251
|
+
confirming={dial.loading}
|
|
252
|
+
onConfirm={() => void confirm()}
|
|
253
|
+
onCancel={() => setConfirming(false)}
|
|
254
|
+
/>
|
|
255
|
+
</div>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** The caller's transport, else the Twilio adapter loaded on demand once the panel opens. */
|
|
260
|
+
function useLazyTransport(provided: SoftphoneTransport | undefined, active: boolean): SoftphoneTransport | null {
|
|
261
|
+
const [loaded, setLoaded] = useState<SoftphoneTransport | null>(null);
|
|
262
|
+
useEffect(() => {
|
|
263
|
+
if (!active || provided || loaded) return;
|
|
264
|
+
let live = true;
|
|
265
|
+
import("../lib/twilio-transport").then((m) => { if (live) setLoaded(m.createTwilioTransport()); }).catch(() => undefined);
|
|
266
|
+
return () => { live = false; };
|
|
267
|
+
}, [active, provided, loaded]);
|
|
268
|
+
return provided ?? loaded;
|
|
269
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CallLogScreen: the dialer's call history as a finished page. Pick whose calls to show (a rep's email, a session or a
|
|
5
|
+
* contact; the API never lists the whole organisation at once), then filter by outcome, search numbers and summaries,
|
|
6
|
+
* and open a call to hear the recording, read the transcript and AI grade (deep link `?id=<room_name>`).
|
|
7
|
+
*
|
|
8
|
+
* SDK operations (through the composed parts): GET /voice/dialer/calls (read, CallLog's query), transcript, grading and
|
|
9
|
+
* recording (read, via TranscriptViewer). The page itself dials, spends and deletes nothing. A call's detail offers
|
|
10
|
+
* "Call back" (CallContactButton), which dials only through its own external confirm dialog and loads nothing until opened.
|
|
11
|
+
* `CallHistory` is exported so DialerScreen can embed the same body under its own header.
|
|
12
|
+
*/
|
|
13
|
+
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
14
|
+
import { Phone } from "lucide-react";
|
|
15
|
+
import { Button, Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
16
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
17
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
18
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
19
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
20
|
+
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
21
|
+
import { StatStrip } from "@/g8/components/screen/stat-strip";
|
|
22
|
+
import { SplitView } from "@/g8/components/screen/split-view";
|
|
23
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
24
|
+
import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
25
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
26
|
+
import { useCurrentMember } from "@/g8/pickers/hooks/use-current-member";
|
|
27
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
28
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
29
|
+
import { dataOf, formatDuration, type CallSummary } from "../lib/dialer-types";
|
|
30
|
+
import { TranscriptViewer } from "./transcript-viewer";
|
|
31
|
+
import { CallContactButton } from "./call-contact-button";
|
|
32
|
+
import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
33
|
+
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
34
|
+
|
|
35
|
+
export interface CallLogScreenProps {
|
|
36
|
+
contactId?: string;
|
|
37
|
+
userEmail?: string;
|
|
38
|
+
sessionId?: string;
|
|
39
|
+
/** Rows loaded at first; "Load more" adds this many again. Default 25. */
|
|
40
|
+
limit?: number;
|
|
41
|
+
toolbarActions?: ReactNode;
|
|
42
|
+
renderRowActions?: (call: CallSummary) => ReactNode;
|
|
43
|
+
renderDetailActions?: (call: CallSummary) => ReactNode;
|
|
44
|
+
extraDetailTabs?: Array<{ id: string; label: string; render: (call: CallSummary) => ReactNode }>;
|
|
45
|
+
className?: string;
|
|
46
|
+
/** URL query key that holds the open call (a room name). Default "id"; give a screen-specific key when another list on the page shares `id`. */
|
|
47
|
+
idParam?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function useDebounced<T>(value: T, ms = 300): T {
|
|
51
|
+
const [v, setV] = useState(value);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const t = setTimeout(() => setV(value), ms);
|
|
54
|
+
return () => clearTimeout(t);
|
|
55
|
+
}, [value, ms]);
|
|
56
|
+
return v;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const label = (d?: string | null) => (d ? d.replace(/_/g, " ") : "none");
|
|
60
|
+
export function outcomeTone(d?: string | null): "neutral" | "info" | "positive" | "warning" | "negative" {
|
|
61
|
+
if (!d) return "neutral";
|
|
62
|
+
if (/wrong|do_not|dnc|bad|invalid|fail/.test(d)) return "negative";
|
|
63
|
+
if (/call_?back|follow|meeting|interested|connected|booked/.test(d)) return /connected|interested|meeting|booked/.test(d) ? "positive" : "info";
|
|
64
|
+
if (/voicemail|no_answer|busy/.test(d)) return "warning";
|
|
65
|
+
return "neutral";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function when(iso?: string | null): string {
|
|
69
|
+
const d = iso ? new Date(iso) : null;
|
|
70
|
+
return d && !Number.isNaN(d.getTime()) ? d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : "";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function CallHistory({ contactId, userEmail, sessionId, limit = 25, idParam = "id", renderRowActions, renderDetailActions, extraDetailTabs }: CallLogScreenProps) {
|
|
74
|
+
const [typedEmail, setTypedEmail] = useState("");
|
|
75
|
+
const [rows, setRows] = useState(limit);
|
|
76
|
+
const [search, setSearch] = useState("");
|
|
77
|
+
const [outcome, setOutcome] = useState("");
|
|
78
|
+
const [openId, setOpenId] = useUrlState(idParam);
|
|
79
|
+
const [view, setView] = useUrlState("view");
|
|
80
|
+
// Unscoped history defaults to YOUR calls (the signed-in member); typing another rep's email still overrides.
|
|
81
|
+
const me = useCurrentMember({ enabled: !userEmail && !contactId && !sessionId });
|
|
82
|
+
const rep = useDebounced(userEmail || typedEmail.trim() || me.email || "");
|
|
83
|
+
const scoped = Boolean(contactId || rep || sessionId);
|
|
84
|
+
const q = useG8Query(
|
|
85
|
+
async (signal) => {
|
|
86
|
+
const env = await g8Get("/voice/dialer/calls", { contact_id: contactId, user_email: rep || undefined, parallel_session_id: sessionId, limit: rows }, { signal });
|
|
87
|
+
return dataOf<{ calls?: CallSummary[] }>(env).calls ?? [];
|
|
88
|
+
},
|
|
89
|
+
[contactId, rep, sessionId, rows],
|
|
90
|
+
{ enabled: scoped },
|
|
91
|
+
);
|
|
92
|
+
// A different rep, session or contact is a different list.
|
|
93
|
+
const scope = `${contactId}|${rep}|${sessionId}|${limit}`;
|
|
94
|
+
const lastScope = useRef(scope);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (lastScope.current === scope) return; // keep a deep-linked ?id= on first render
|
|
97
|
+
lastScope.current = scope;
|
|
98
|
+
setRows(limit);
|
|
99
|
+
setOutcome("");
|
|
100
|
+
setOpenId(null);
|
|
101
|
+
}, [scope]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
102
|
+
|
|
103
|
+
const all = q.data ?? [];
|
|
104
|
+
const needle = search.trim().toLowerCase();
|
|
105
|
+
const calls = useMemo(
|
|
106
|
+
() => all.filter((c) => (!outcome || (c.disposition ?? "none") === outcome) && (!needle || `${c.to ?? ""} ${c.from_ ?? ""} ${c.summary ?? ""}`.toLowerCase().includes(needle))),
|
|
107
|
+
[all, outcome, needle],
|
|
108
|
+
);
|
|
109
|
+
const outcomes = [...new Set(all.map((c) => c.disposition ?? "none"))];
|
|
110
|
+
const selected = openId ? all.find((c) => c.room_name === openId) : undefined;
|
|
111
|
+
const expanded = view === "full" && Boolean(selected);
|
|
112
|
+
const totalSecs = all.reduce((n, c) => n + (c.call_duration ?? 0), 0);
|
|
113
|
+
|
|
114
|
+
if (!scoped) {
|
|
115
|
+
return (
|
|
116
|
+
<div className="max-w-md space-y-3">
|
|
117
|
+
<EmptyState icon={<Phone className="size-6" />} title="Choose whose calls to show" description="Type a rep's email. The API will not list the whole organisation's calls at once." />
|
|
118
|
+
<Input aria-label="Rep email" type="email" placeholder="rep@company.com" value={typedEmail} onChange={(e) => setTypedEmail(e.target.value)} />
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const list = (
|
|
123
|
+
<div className="space-y-3">
|
|
124
|
+
<Toolbar
|
|
125
|
+
search={{ value: search, onChange: setSearch, placeholder: "Search number or summary" }}
|
|
126
|
+
filters={
|
|
127
|
+
<select aria-label="Outcome" className="h-8 rounded-md border border-border bg-background px-2 text-sm" value={outcome} onChange={(e) => setOutcome(e.target.value)}>
|
|
128
|
+
<option value="">Any outcome</option>
|
|
129
|
+
{outcomes.map((o) => <option key={o} value={o}>{label(o)}</option>)}
|
|
130
|
+
</select>
|
|
131
|
+
}
|
|
132
|
+
/>
|
|
133
|
+
{q.error ? (
|
|
134
|
+
<ApiError error={q.error} title="Could not load calls" onRetry={() => void q.refetch()} />
|
|
135
|
+
) : q.loading && !q.data ? (
|
|
136
|
+
<Skeleton className="h-32 w-full" aria-label="Loading calls" />
|
|
137
|
+
) : calls.length === 0 ? (
|
|
138
|
+
all.length === 0 ? (
|
|
139
|
+
<EmptyState icon={<Phone className="size-6" />} title="No calls yet" description="Calls appear here after the dialer places them." />
|
|
140
|
+
) : (
|
|
141
|
+
<EmptyState title="No calls match" description="Nothing fits this search or outcome." action={<Button size="sm" variant="outline" onClick={() => { setSearch(""); setOutcome(""); }}>Clear filters</Button>} />
|
|
142
|
+
)
|
|
143
|
+
) : (
|
|
144
|
+
<Table aria-label="Call history">
|
|
145
|
+
<TableHeader><TableRow><TableHead>When</TableHead><TableHead>Number</TableHead><TableHead>Outcome</TableHead><TableHead>Length</TableHead><TableHead>Summary</TableHead>{renderRowActions ? <TableHead /> : null}</TableRow></TableHeader>
|
|
146
|
+
<TableBody>
|
|
147
|
+
{calls.map((c) => (
|
|
148
|
+
<TableRow key={c.room_name} data-state={openId === c.room_name ? "selected" : undefined} className="cursor-pointer" {...rowActivationProps(() => setOpenId(c.room_name))}>
|
|
149
|
+
<TableCell className="whitespace-nowrap"><RecordLink param={idParam} id={c.room_name} onOpen={() => setOpenId(c.room_name)}>{when(c.created_at)}</RecordLink></TableCell>
|
|
150
|
+
<TableCell>{c.to}</TableCell>
|
|
151
|
+
<TableCell><StatusPill tone={outcomeTone(c.disposition)}>{label(c.disposition)}</StatusPill></TableCell>
|
|
152
|
+
<TableCell>{formatDuration(c.call_duration)}</TableCell>
|
|
153
|
+
<TableCell className="max-w-64 truncate text-muted-foreground">{c.summary ?? (c.has_transcript ? "Transcript available" : "")}</TableCell>
|
|
154
|
+
{renderRowActions ? <TableCell onClick={(e) => e.stopPropagation()}>{renderRowActions(c)}</TableCell> : null}
|
|
155
|
+
</TableRow>
|
|
156
|
+
))}
|
|
157
|
+
</TableBody>
|
|
158
|
+
</Table>
|
|
159
|
+
)}
|
|
160
|
+
{all.length >= rows ? <Button size="sm" variant="outline" disabled={q.loading} onClick={() => setRows(rows + limit)}>Load more</Button> : null}
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
const detail = selected ? (
|
|
164
|
+
<DetailPanel
|
|
165
|
+
title={selected.to ?? "Call"}
|
|
166
|
+
subtitle={when(selected.created_at)}
|
|
167
|
+
badges={<StatusPill tone={outcomeTone(selected.disposition)}>{label(selected.disposition)}</StatusPill>}
|
|
168
|
+
actions={
|
|
169
|
+
<>
|
|
170
|
+
{renderDetailActions?.(selected)}
|
|
171
|
+
{/* Call the same number again: CallContactButton (manual-dial, confirmed; loads nothing until opened). */}
|
|
172
|
+
{selected.to ? <CallContactButton contact={{ id: selected.contact_id, phone: selected.to }} userEmail={userEmail} label="Call back" /> : null}
|
|
173
|
+
</>
|
|
174
|
+
}
|
|
175
|
+
onClose={() => { setOpenId(null); setView(null); }}
|
|
176
|
+
onExpand={expanded ? undefined : () => setView("full")}
|
|
177
|
+
onCollapse={expanded ? () => setView(null) : undefined}
|
|
178
|
+
tabs={[
|
|
179
|
+
{ id: "recording", label: "Recording and transcript", content: <TranscriptViewer key={selected.room_name} roomName={selected.room_name} /> },
|
|
180
|
+
{ id: "details", label: "Details", content: <KeyValueList columns={1} items={[{ label: "From", value: selected.from_ ?? "" }, { label: "To", value: selected.to ?? "" }, { label: "Length", value: formatDuration(selected.call_duration) }, { label: "Contact id", value: selected.contact_id ?? "" }, { label: "Room", value: selected.room_name }]} /> },
|
|
181
|
+
...(extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(selected) })),
|
|
182
|
+
]}
|
|
183
|
+
/>
|
|
184
|
+
) : null;
|
|
185
|
+
return (
|
|
186
|
+
<div className="space-y-4">
|
|
187
|
+
{all.length > 0 ? (
|
|
188
|
+
<StatStrip
|
|
189
|
+
items={[
|
|
190
|
+
{ label: "Calls loaded", value: all.length },
|
|
191
|
+
{ label: "Talk time", value: formatDuration(totalSecs) },
|
|
192
|
+
{ label: "With transcript", value: all.filter((c) => c.has_transcript).length },
|
|
193
|
+
{ label: "No outcome yet", value: all.filter((c) => !c.disposition).length, tone: all.some((c) => !c.disposition) ? "warning" : "neutral" },
|
|
194
|
+
]}
|
|
195
|
+
/>
|
|
196
|
+
) : null}
|
|
197
|
+
<SplitView list={list} detail={detail} detailOpen={Boolean(selected)} onCloseDetail={() => { setOpenId(null); setView(null); }} expanded={expanded} onExpandedChange={(next) => setView(next ? "full" : null)} />
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function CallLogScreen(props: CallLogScreenProps) {
|
|
203
|
+
return (
|
|
204
|
+
<ScreenLayout className={props.className} title="Call log" description="Every dialer call with its outcome, recording, transcript and AI grade." icon={<Phone className="size-5" />} actions={props.toolbarActions}>
|
|
205
|
+
<CallHistory {...props} />
|
|
206
|
+
</ScreenLayout>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
@@ -9,14 +9,18 @@
|
|
|
9
9
|
* Org-wide history across all reps: use POST /voice/call-results/search (voice.searchCallResults) from a server recipe.
|
|
10
10
|
*/
|
|
11
11
|
import { useState } from "react";
|
|
12
|
-
import { Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
12
|
+
import { Button, Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
13
13
|
import { ApiError } from "@/g8/components/api-error";
|
|
14
14
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
15
15
|
import { g8Get } from "@/g8/lib/g8-client";
|
|
16
16
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
17
17
|
import { dataOf, formatDuration, type CallSummary } from "../lib/dialer-types";
|
|
18
|
+
import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
19
|
+
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
18
20
|
|
|
19
21
|
export interface CallLogProps {
|
|
22
|
+
/** URL param the host screen reads for this record type. With it the name is a real link (Cmd-click opens it in a new tab); without it a plain button. */
|
|
23
|
+
linkParam?: string;
|
|
20
24
|
contactId?: string;
|
|
21
25
|
userEmail?: string;
|
|
22
26
|
sessionId?: string;
|
|
@@ -31,16 +35,20 @@ function when(iso: string | null | undefined): string {
|
|
|
31
35
|
return Number.isNaN(d.getTime()) ? "" : d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
export function CallLog({ contactId, userEmail, sessionId, limit = 25, onSelect, className }: CallLogProps) {
|
|
38
|
+
export function CallLog({ linkParam, contactId, userEmail, sessionId, limit = 25, onSelect, className }: CallLogProps) {
|
|
35
39
|
const [typedEmail, setTypedEmail] = useState("");
|
|
36
40
|
const repEmail = userEmail || typedEmail.trim();
|
|
37
41
|
const scoped = Boolean(contactId || repEmail || sessionId);
|
|
42
|
+
// The calls API pages by a growing `limit`: "Load more" asks for another page-worth and keeps the earlier calls.
|
|
43
|
+
const scope = `${contactId}|${repEmail}|${sessionId}|${limit}`;
|
|
44
|
+
const [size, setSize] = useState({ scope, rows: limit });
|
|
45
|
+
const rowsWanted = size.scope === scope ? size.rows : limit;
|
|
38
46
|
const q = useG8Query(
|
|
39
47
|
async (signal) => {
|
|
40
|
-
const env = await g8Get("/voice/dialer/calls", { contact_id: contactId, user_email: repEmail || undefined, parallel_session_id: sessionId, limit }, { signal });
|
|
48
|
+
const env = await g8Get("/voice/dialer/calls", { contact_id: contactId, user_email: repEmail || undefined, parallel_session_id: sessionId, limit: rowsWanted }, { signal });
|
|
41
49
|
return dataOf<{ calls?: CallSummary[] }>(env).calls ?? [];
|
|
42
50
|
},
|
|
43
|
-
[contactId, repEmail, sessionId,
|
|
51
|
+
[contactId, repEmail, sessionId, rowsWanted],
|
|
44
52
|
{ enabled: scoped },
|
|
45
53
|
);
|
|
46
54
|
|
|
@@ -58,14 +66,15 @@ export function CallLog({ contactId, userEmail, sessionId, limit = 25, onSelect,
|
|
|
58
66
|
if (calls.length === 0) return <EmptyState className={className} title="No calls yet" description="Calls appear here after the dialer places them." />;
|
|
59
67
|
|
|
60
68
|
return (
|
|
61
|
-
<
|
|
69
|
+
<div className={className}>
|
|
70
|
+
<Table aria-label="Call log">
|
|
62
71
|
<TableHeader>
|
|
63
72
|
<TableRow><TableHead>When</TableHead><TableHead>Number</TableHead><TableHead>Outcome</TableHead><TableHead>Length</TableHead><TableHead>Summary</TableHead></TableRow>
|
|
64
73
|
</TableHeader>
|
|
65
74
|
<TableBody>
|
|
66
75
|
{calls.map((c) => (
|
|
67
|
-
<TableRow key={c.room_name} className={onSelect ? "cursor-pointer" : undefined}
|
|
68
|
-
<TableCell className="whitespace-nowrap">{when(c.created_at)}</TableCell>
|
|
76
|
+
<TableRow key={c.room_name} className={onSelect ? "cursor-pointer" : undefined} {...(onSelect ? rowActivationProps(() => onSelect(c)) : {})}>
|
|
77
|
+
<TableCell className="whitespace-nowrap">{onSelect ? <RecordLink param={linkParam} id={c.room_name} onOpen={() => onSelect(c)}>{when(c.created_at)}</RecordLink> : when(c.created_at)}</TableCell>
|
|
69
78
|
<TableCell>{c.to}</TableCell>
|
|
70
79
|
<TableCell className="capitalize">{c.disposition?.replace(/_/g, " ") ?? "none"}</TableCell>
|
|
71
80
|
<TableCell>{formatDuration(c.call_duration)}</TableCell>
|
|
@@ -74,5 +83,12 @@ export function CallLog({ contactId, userEmail, sessionId, limit = 25, onSelect,
|
|
|
74
83
|
))}
|
|
75
84
|
</TableBody>
|
|
76
85
|
</Table>
|
|
86
|
+
{calls.length >= rowsWanted ? (
|
|
87
|
+
<div className="flex items-center justify-between gap-3 py-2 text-sm text-muted-foreground">
|
|
88
|
+
<span role="status">Showing {calls.length.toLocaleString()} calls</span>
|
|
89
|
+
<Button size="sm" variant="outline" loading={q.loading} onClick={() => setSize({ scope, rows: rowsWanted + limit })}>Load more</Button>
|
|
90
|
+
</div>
|
|
91
|
+
) : null}
|
|
92
|
+
</div>
|
|
77
93
|
);
|
|
78
94
|
}
|
|
@@ -50,12 +50,12 @@ function Stat({ label, value }: { label: string; value: string | number }) {
|
|
|
50
50
|
return (
|
|
51
51
|
<div className="rounded-lg border p-3">
|
|
52
52
|
<div className="text-xs uppercase text-muted-foreground">{label}</div>
|
|
53
|
-
<div className="text-lg font-
|
|
53
|
+
<div className="text-lg font-medium capitalize">{value}</div>
|
|
54
54
|
</div>
|
|
55
55
|
);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function WaitingContacts({ sessionId, version, onSkipped }: { sessionId: string; version: number; onSkipped: () => void }) {
|
|
58
|
+
export function WaitingContacts({ sessionId, version, onSkipped }: { sessionId: string; version: number; onSkipped: () => void }) {
|
|
59
59
|
const grid = useG8Query(
|
|
60
60
|
async (signal) => {
|
|
61
61
|
const env = await g8Post(`/dialer/sessions/${encodeURIComponent(sessionId)}/grid`, { limit: 10, offset: 0, include_contact_data: true }, { signal });
|
|
@@ -71,7 +71,7 @@ function WaitingContacts({ sessionId, version, onSkipped }: { sessionId: string;
|
|
|
71
71
|
const rows = (grid.data ?? []).filter((r) => !r.callresult?.disposition);
|
|
72
72
|
return (
|
|
73
73
|
<div className="space-y-2">
|
|
74
|
-
<h3 className="text-sm font-
|
|
74
|
+
<h3 className="text-sm font-medium">Up next</h3>
|
|
75
75
|
{skip.error ? <ApiError error={skip.error} title="Could not skip that contact" /> : null}
|
|
76
76
|
<Table aria-label="Contacts waiting">
|
|
77
77
|
<TableHeader>
|
|
@@ -141,7 +141,7 @@ export function CallQueue({ sessionId: initialId, userId, userEmail, listId, lis
|
|
|
141
141
|
return (
|
|
142
142
|
<div className={`space-y-4 ${className ?? ""}`}>
|
|
143
143
|
<header className="flex flex-wrap items-center justify-between gap-2">
|
|
144
|
-
<h2 className="text-base font-
|
|
144
|
+
<h2 className="text-base font-medium">{session.detail?.name ?? "Dialing session"}</h2>
|
|
145
145
|
<div className="flex gap-2">
|
|
146
146
|
<Button disabled={!stats || over || nextBatch.loading} onClick={() => setConfirming(true)}>Dial next batch</Button>
|
|
147
147
|
<Button variant="outline" disabled={!stats || over} onClick={() => void pause.mutate(undefined)}>Pause</Button>
|