@graph8/sdk 0.370.0 → 0.374.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -3
- package/blocks/_foundation/README.md +61 -0
- package/blocks/_foundation/block.json +57 -0
- package/blocks/_foundation/components/auth-gate.tsx +363 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +1 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/auth.ts +815 -0
- package/blocks/agents/README.md +81 -4
- package/blocks/agents/block.json +311 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +17 -3
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +20 -3
- package/blocks/agents/components/agent-runs.tsx +3 -3
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +53 -11
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/components/analytics-screen.tsx +16 -3
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/report-builder.tsx +15 -2
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +27 -0
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +29 -13
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/campaigns/README.md +1 -1
- package/blocks/campaigns/block.json +34 -1
- package/blocks/campaigns/components/campaign-assets.tsx +1 -1
- package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
- package/blocks/campaigns/components/campaign-links.tsx +180 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
- package/blocks/campaigns/components/icp-detail.tsx +11 -2
- package/blocks/campaigns/components/icp-editor.tsx +8 -3
- package/blocks/campaigns/components/persona-detail.tsx +11 -2
- package/blocks/campaigns/components/persona-editor.tsx +6 -1
- package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +165 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +1 -1
- package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
- package/blocks/crm/README.md +25 -0
- package/blocks/crm/block.json +257 -9
- package/blocks/crm/components/bulk-update.tsx +119 -0
- package/blocks/crm/components/companies-screen.tsx +23 -9
- package/blocks/crm/components/company-detail.tsx +2 -2
- package/blocks/crm/components/contact-detail.tsx +4 -4
- package/blocks/crm/components/contact-documents-tab.tsx +144 -0
- package/blocks/crm/components/contact-insights-tab.tsx +118 -0
- package/blocks/crm/components/contact-lookup-link.tsx +85 -0
- package/blocks/crm/components/contact-row-actions.tsx +43 -0
- package/blocks/crm/components/contact-tag-chips.tsx +113 -0
- package/blocks/crm/components/contacts-screen.tsx +77 -18
- package/blocks/crm/components/dedupe-review.tsx +6 -1
- package/blocks/crm/components/import-wizard.tsx +3 -7
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/owner-assign.tsx +132 -0
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +1 -1
- package/blocks/crm/components/record-open-links.tsx +79 -0
- package/blocks/crm/components/tag-assign.tsx +112 -0
- package/blocks/crm/components/tag-manager.tsx +178 -0
- package/blocks/crm/components/tasks-screen.tsx +35 -15
- package/blocks/crm/components/unlock-info-button.tsx +82 -0
- package/blocks/crm/components/verify-email-button.tsx +93 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-contact-tags.ts +29 -0
- package/blocks/crm/hooks/use-page-query.ts +3 -1
- package/blocks/crm/lib/crm-types.ts +14 -0
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/record-actions.ts +102 -0
- package/blocks/deals/README.md +25 -6
- package/blocks/deals/block.json +168 -2
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/create-deal-button.tsx +109 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +7 -5
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +30 -9
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +57 -22
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +1 -1
- package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +14 -5
- package/blocks/dialer/block.json +17 -2
- package/blocks/dialer/components/call-contact-button.tsx +26 -9
- package/blocks/dialer/components/call-log-screen.tsx +10 -2
- package/blocks/dialer/components/call-log.tsx +16 -4
- package/blocks/dialer/components/call-queue.tsx +3 -3
- package/blocks/dialer/components/dialer-screen.tsx +69 -27
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +58 -11
- package/blocks/dialer/components/softphone.tsx +1 -1
- package/blocks/documents/README.md +86 -0
- package/blocks/documents/block.json +378 -0
- package/blocks/documents/components/account-bar.tsx +48 -0
- package/blocks/documents/components/correspondence-form.tsx +95 -0
- package/blocks/documents/components/correspondence-import.tsx +106 -0
- package/blocks/documents/components/correspondence-panel.tsx +191 -0
- package/blocks/documents/components/documents-screen.tsx +76 -0
- package/blocks/documents/components/library-document-detail.tsx +175 -0
- package/blocks/documents/components/library-document-form.tsx +105 -0
- package/blocks/documents/components/library-folder-form.tsx +74 -0
- package/blocks/documents/components/library-panel.tsx +235 -0
- package/blocks/documents/components/library-upload.tsx +160 -0
- package/blocks/documents/components/question-row.tsx +130 -0
- package/blocks/documents/components/questionnaire-detail.tsx +197 -0
- package/blocks/documents/components/questionnaires-panel.tsx +173 -0
- package/blocks/documents/components/record-documents-tab.tsx +109 -0
- package/blocks/documents/lib/documents-types.ts +252 -0
- package/blocks/documents/lib/download.ts +64 -0
- package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
- package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
- package/blocks/documents/recipes/organize-the-library.ts +25 -0
- package/blocks/email-signatures/README.md +123 -0
- package/blocks/email-signatures/block.json +341 -0
- package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
- package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
- package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
- package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
- package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
- package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
- package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
- package/blocks/email-signatures/components/signature-policy.tsx +130 -0
- package/blocks/email-signatures/components/signature-preview.tsx +119 -0
- package/blocks/email-signatures/components/signature-report.tsx +77 -0
- package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
- package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
- package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
- package/blocks/email-signatures/lib/signatures-types.ts +392 -0
- package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
- package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
- package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
- package/blocks/enrichment/README.md +38 -10
- package/blocks/enrichment/block.json +164 -12
- package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
- package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
- package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
- package/blocks/enrichment/components/enrich-button.tsx +1 -1
- package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
- package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
- package/blocks/enrichment/components/list-pipelines.tsx +97 -0
- package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
- package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
- package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
- package/blocks/enrichment/components/waterfall-config.tsx +27 -7
- package/blocks/enrichment/lib/enrichment-types.ts +51 -0
- package/blocks/enrichment/lib/pipeline-types.ts +234 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/forms/README.md +10 -0
- package/blocks/forms/block.json +107 -0
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +1 -1
- package/blocks/forms/components/form-embed.tsx +1 -1
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +57 -20
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +15 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +45 -4
- package/blocks/inbox/block.json +145 -8
- package/blocks/inbox/components/blocked-senders.tsx +103 -0
- package/blocks/inbox/components/compose-message.tsx +193 -0
- package/blocks/inbox/components/inbox-screen.tsx +54 -6
- package/blocks/inbox/components/inbox-search.tsx +106 -0
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/tag-manager.tsx +136 -0
- package/blocks/inbox/components/thread-actions.tsx +133 -0
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +3 -1
- package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
- package/blocks/inbox/lib/inbox-manage.ts +227 -0
- package/blocks/integrations/README.md +54 -1
- package/blocks/integrations/block.json +189 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +20 -1
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +4 -4
- package/blocks/integrations/components/integrations-screen.tsx +20 -3
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +6 -6
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +26 -7
- package/blocks/integrations/components/webhook-manager.tsx +30 -17
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +5 -0
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +8 -8
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lists/README.md +7 -1
- package/blocks/lists/block.json +31 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +17 -7
- package/blocks/lists/components/list-picker.tsx +13 -3
- package/blocks/lists/components/list-table.tsx +1 -1
- package/blocks/lists/components/lists-screen.tsx +4 -2
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +27 -6
- package/blocks/lists/components/suppressions-screen.tsx +21 -12
- package/blocks/mailboxes/block.json +4 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
- package/blocks/mailboxes/components/domain-detail.tsx +10 -4
- package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
- package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
- package/blocks/mailboxes/components/warmup-health.tsx +20 -20
- package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +117 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +38 -13
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/components/nurture-builder.tsx +10 -2
- package/blocks/nurture/components/nurture-enroll.tsx +6 -7
- package/blocks/nurture/components/nurture-form.tsx +5 -8
- package/blocks/nurture/components/nurture-screen.tsx +16 -5
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/objects/README.md +1 -1
- package/blocks/objects/block.json +13 -5
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-record-detail.tsx +8 -1
- package/blocks/objects/components/object-schema-editor.tsx +1 -1
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +123 -2
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/programmatic-pages/README.md +82 -0
- package/blocks/programmatic-pages/block.json +292 -0
- package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
- package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
- package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
- package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
- package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
- package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
- package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
- package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
- package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
- package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
- package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
- package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
- package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
- package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
- package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
- package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
- package/blocks/quotes/README.md +38 -1
- package/blocks/quotes/block.json +170 -6
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/manual-products.tsx +144 -0
- package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-documents.tsx +94 -0
- package/blocks/quotes/components/quote-evidence.tsx +95 -0
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +28 -8
- package/blocks/quotes/components/quote-related.tsx +3 -2
- package/blocks/quotes/components/quote-settings.tsx +184 -0
- package/blocks/quotes/components/quote-templates.tsx +189 -0
- package/blocks/quotes/components/quote-viewer.tsx +23 -5
- package/blocks/quotes/components/quotes-screen.tsx +15 -5
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/quotes/lib/quote-admin.ts +264 -0
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-screen.tsx +2 -2
- package/blocks/sequencer/README.md +61 -0
- package/blocks/sequencer/block.json +200 -6
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
- package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +32 -23
- package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
- package/blocks/sequencer/components/sequence-detail.tsx +10 -2
- package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
- package/blocks/sequencer/components/sequence-list.tsx +1 -1
- package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
- package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
- package/blocks/sequencer/components/sequence-stats.tsx +8 -8
- package/blocks/sequencer/components/sequence-templates.tsx +160 -0
- package/blocks/sequencer/components/sequences-screen.tsx +39 -6
- package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
- package/blocks/sequencer/components/step-editor.tsx +21 -12
- package/blocks/sequencer/components/step-templates.tsx +224 -0
- package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
- package/blocks/signals/README.md +39 -0
- package/blocks/signals/block.json +287 -0
- package/blocks/signals/components/auto-seed-panel.tsx +111 -0
- package/blocks/signals/components/listener-form.tsx +184 -0
- package/blocks/signals/components/matched-posts.tsx +195 -0
- package/blocks/signals/components/post-engagements.tsx +114 -0
- package/blocks/signals/components/record-social-tab.tsx +49 -0
- package/blocks/signals/components/social-listeners-screen.tsx +250 -0
- package/blocks/signals/components/social-settings.tsx +128 -0
- package/blocks/signals/components/source-lists.tsx +83 -0
- package/blocks/signals/components/top-engagers.tsx +68 -0
- package/blocks/signals/components/x-integration.tsx +129 -0
- package/blocks/signals/lib/signals-types.ts +228 -0
- package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
- package/blocks/signals/recipes/spend-against-caps.ts +14 -0
- package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
- package/blocks/social-ads/README.md +8 -0
- package/blocks/social-ads/block.json +137 -4
- package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
- package/blocks/social-ads/components/audience-sync.tsx +6 -2
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +6 -1
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +6 -1
- package/blocks/social-ads/components/creative-gallery.tsx +20 -2
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +4 -2
- package/blocks/social-ads/components/post-detail.tsx +3 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-screen.tsx +33 -6
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/components/snippet-install.tsx +18 -9
- package/blocks/visitors/components/traffic-chart.tsx +7 -6
- package/blocks/visitors/components/visitor-company-card.tsx +24 -14
- package/blocks/visitors/components/visitor-feed.tsx +5 -5
- package/blocks/web-analytics/README.md +102 -0
- package/blocks/web-analytics/block.json +292 -0
- package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
- package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
- package/blocks/web-analytics/components/audience-tab.tsx +135 -0
- package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
- package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
- package/blocks/web-analytics/components/overview-tab.tsx +102 -0
- package/blocks/web-analytics/components/pages-tab.tsx +148 -0
- package/blocks/web-analytics/components/ranked-table.tsx +103 -0
- package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
- package/blocks/web-analytics/components/report-card.tsx +71 -0
- package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
- package/blocks/web-analytics/hooks/use-report.ts +48 -0
- package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
- package/blocks/web-analytics/lib/page-path.ts +14 -0
- package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
- package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
- package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
- package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
- package/blocks/webchat/components/chat-widget-config.tsx +1 -1
- package/blocks/webchat/components/conversation-list.tsx +1 -1
- package/blocks/workbench/README.md +54 -2
- package/blocks/workbench/block.json +234 -5
- package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
- package/blocks/workbench/components/workbench-automation.tsx +36 -0
- package/blocks/workbench/components/workbench-connections.tsx +136 -0
- package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
- package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
- package/blocks/workbench/components/workbench-grid.tsx +71 -8
- package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
- package/blocks/workbench/components/workbench-schedule.tsx +191 -0
- package/blocks/workbench/components/workbench-screen.tsx +65 -22
- package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
- package/blocks/workbench/components/workbench-triggers.tsx +178 -0
- package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
- package/blocks/workbench/lib/api-status.ts +8 -0
- package/blocks/workbench/lib/automation-types.ts +257 -0
- package/blocks/workflows/components/workflow-editor.tsx +1 -1
- package/dist/cli.js +744 -394
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +1 -1
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +1 -1
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/auth.d.ts +45 -0
- package/dist/types/blocks/brand.d.ts +11 -2
- package/dist/types/blocks/create.d.ts +11 -4
- package/dist/types/blocks/doctor.d.ts +1 -0
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/version.d.ts +2 -2
- package/docs/api/desk.d.ts +2 -2
- package/docs/api/desk.md +3 -3
- package/docs/api/forms.md +1 -1
- package/package.json +1 -1
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +37 -4
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
- package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +24 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +68 -9
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +8 -4
- package/stacks/clay/pages/companies.tsx.tmpl +17 -1
- package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
- package/stacks/clay/pages/lists.tsx.tmpl +24 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +16 -3
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +34 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +23 -6
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +19 -4
- package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
- package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
- package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
- package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +18 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
- package/stacks/hubspot.json +66 -7
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +18 -4
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
- package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +22 -6
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +18 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +38 -9
- package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
- package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +14 -2
- package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
- package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
- package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +17 -3
- package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +36 -3
- package/styles/styles.css +1 -1
- package/templates/auth/auth-route.ts.tmpl +16 -0
- package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
- package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
- package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
- package/templates/auth/g8.auth.password.ts.tmpl +18 -0
- package/templates/auth/g8.auth.session.ts.tmpl +23 -0
- package/templates/auth/middleware.ts.tmpl +14 -0
- package/templates/next-app/README.md.tmpl +5 -0
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
|
@@ -7,21 +7,31 @@
|
|
|
7
7
|
*
|
|
8
8
|
* SDK operations, in order:
|
|
9
9
|
* 1. listEnrichmentProviders GET /enrichment/providers (read) provider choices, graph8-funded vs own key
|
|
10
|
+
* 1b. listWaterfallConfigs GET /enrichment/waterfall/configs?list_id (read) the list's own pipeline for the field, if any:
|
|
11
|
+
* a run reuses it with ITS providers, so the quote and the confirm are priced with those, not the chosen ones.
|
|
10
12
|
* 2. preflightEnrichment POST /enrichment/preflight (read, free; nothing is written or charged)
|
|
11
13
|
* { list_id, contact_ids, mode: "waterfall", providers, target_field } -> contact_count, not_in_list,
|
|
12
14
|
* missing_credentials, warnings, credits_per_record, max_total_credits
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* After ConfirmSpendDialog (with `max_total_credits`; when the preflight could not price the run the confirm shows NO total
|
|
16
|
+
* and says so, never a guess; there is no balance read, so a quote is the most it can charge):
|
|
17
|
+
* 3. createDefaultWaterfallConfig POST /enrichment/waterfall/configs { list_id, name, field, providers } (billable tier, free)
|
|
18
|
+
* Creates the list's pipeline for that field, or reuses the one it already has (`created: false`; it then runs
|
|
19
|
+
* with ITS providers). The column_id is kept, so a retry after step 4 failed does not create it again.
|
|
20
|
+
* 4. runWaterfallPipeline POST /enrichment/waterfall/enrich { column_id, list_id, record_ids } (billable) 202 { job_id, status }
|
|
21
|
+
* A 202 whose status is "error" (or a 502) means the job never started: shown as a failure, never polled.
|
|
22
|
+
* 5. getEnrichmentJob GET /enrichment/jobs/{job_id} (read) polled with useJobPoll (no streams), backoff;
|
|
17
23
|
* read once more with ?include_provider_errors=true when nothing was enriched or some records failed.
|
|
18
|
-
*
|
|
24
|
+
* 6. cancelEnrichmentJob POST /enrichment/jobs/{job_id}/cancel (write, free; records already done keep their data and charge)
|
|
25
|
+
*
|
|
26
|
+
* When the job ends, every cached `contacts` query is refreshed (and the page mode's ContactTable reloads) so the
|
|
27
|
+
* filled values show. The panel does not use POST /enrichment/enrich (startEnrichment): before the API fix of
|
|
28
|
+
* 2026-10-16 that route dispatched the field name instead of a pipeline and silently enriched nothing.
|
|
19
29
|
*
|
|
20
30
|
* The contacts must be on `listId`; ids not on it are skipped and not charged. Locked contacts are enriched
|
|
21
31
|
* like any other once they sit in a list.
|
|
22
32
|
*/
|
|
23
33
|
|
|
24
|
-
import { useState } from "react";
|
|
34
|
+
import { useRef, useState } from "react";
|
|
25
35
|
import { Button } from "@graph8/sdk/ui";
|
|
26
36
|
import { ApiError } from "@/g8/components/api-error";
|
|
27
37
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
@@ -29,19 +39,23 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
|
29
39
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
30
40
|
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
31
41
|
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
42
|
+
import { g8QueryCache } from "@/g8/lib/g8-cache";
|
|
32
43
|
import { ContactTable } from "@/g8/crm/components/contact-table";
|
|
33
44
|
import { ListPicker } from "@/g8/lists/components/list-picker";
|
|
34
45
|
import { plural } from "@/g8/lib/plural";
|
|
35
46
|
import { useProviders } from "../hooks/use-providers";
|
|
36
|
-
import {
|
|
37
|
-
import type { EnrichJob, Preflight, TargetField } from "../lib/enrichment-types";
|
|
47
|
+
import { FIELD_PROVIDERS, TARGET_FIELDS, isJobTerminal, providersForField, quotedCredits } from "../lib/enrichment-types";
|
|
48
|
+
import type { EnrichJob, Preflight, TargetField, WaterfallConfigSummary, WaterfallPipelineResult } from "../lib/enrichment-types";
|
|
38
49
|
import { useBrand } from "@/g8/hooks/use-brand";
|
|
39
50
|
|
|
40
51
|
export interface BulkEnrichPanelProps {
|
|
41
52
|
/** With `listId` and `contactIds` it is just the enrichment panel. Omit them and it is a whole page: pick a list, tick contacts, enrich. */
|
|
42
53
|
listId?: number;
|
|
43
54
|
contactIds?: number[];
|
|
44
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Providers tried, in order, when the person has not chosen. Kept to the providers the field supports. Default: the
|
|
57
|
+
* API's own per field (work email: prospeo, dropcontact; phones: leadmagic, prospeo, apollo).
|
|
58
|
+
*/
|
|
45
59
|
defaultProviders?: string[];
|
|
46
60
|
defaultField?: TargetField;
|
|
47
61
|
/** Called when the job reached a final state. */
|
|
@@ -51,37 +65,64 @@ export interface BulkEnrichPanelProps {
|
|
|
51
65
|
className?: string;
|
|
52
66
|
}
|
|
53
67
|
|
|
54
|
-
|
|
68
|
+
type StartedRun = { job: EnrichJob; pipeline: WaterfallPipelineResult; asked: string[] };
|
|
55
69
|
|
|
56
|
-
function BulkEnrichCore({ listId, contactIds, defaultProviders
|
|
70
|
+
function BulkEnrichCore({ listId, contactIds, defaultProviders, defaultField = "work_email", onDone, onStarted, className }: BulkEnrichPanelProps & { listId: number; contactIds: number[] }) {
|
|
57
71
|
const brand = useBrand();
|
|
58
72
|
const [field, setField] = useState<TargetField>(defaultField);
|
|
59
73
|
const [chosen, setChosen] = useState<string[] | null>(null);
|
|
60
74
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
61
75
|
const [jobId, setJobId] = useState<string | null>(null);
|
|
76
|
+
const [run, setRun] = useState<StartedRun | null>(null);
|
|
77
|
+
// The pipeline step 3 created or reused, kept per list/field/providers so a retry does not create it twice.
|
|
78
|
+
const pipelineRef = useRef<{ key: string; pipeline: WaterfallPipelineResult } | null>(null);
|
|
62
79
|
const catalog = useProviders();
|
|
63
80
|
|
|
64
81
|
const known = new Set(catalog.providers.map((p) => p.provider));
|
|
65
|
-
const providers = chosen ?? defaultProviders.filter((p) => known.size === 0 || known.has(p));
|
|
82
|
+
const providers = chosen ?? providersForField(field, defaultProviders).filter((p) => known.size === 0 || known.has(p));
|
|
83
|
+
const offered = catalog.providers.filter((p) => FIELD_PROVIDERS[field].includes(p.provider) && !providers.includes(p.provider));
|
|
66
84
|
const column = TARGET_FIELDS.find((f) => f.value === field)!;
|
|
67
85
|
const ids = [...new Set(contactIds)];
|
|
68
86
|
|
|
87
|
+
// The list's own pipeline for this field wins over the chosen providers (the API reuses it), so price that one.
|
|
88
|
+
const saved = useG8Query(
|
|
89
|
+
async (signal) => (await g8Get<{ configs: WaterfallConfigSummary[] }>("/enrichment/waterfall/configs", { list_id: listId }, { signal })).data.configs ?? [],
|
|
90
|
+
[listId],
|
|
91
|
+
{ enabled: !jobId },
|
|
92
|
+
);
|
|
93
|
+
const existing = saved.data?.find((c) => c.field_to_enrich === column.target && c.providers.length > 0) ?? null;
|
|
94
|
+
const runProviders = existing ? existing.providers : providers;
|
|
95
|
+
|
|
69
96
|
const preflight = useG8Query(
|
|
70
|
-
async (signal) => (await g8Post<Preflight>("/enrichment/preflight", { list_id: listId, contact_ids: ids, mode: "waterfall", providers, target_field: column.target }, { signal })).data,
|
|
71
|
-
[listId, ids.join(","),
|
|
97
|
+
async (signal) => (await g8Post<Preflight>("/enrichment/preflight", { list_id: listId, contact_ids: ids, mode: "waterfall", providers: runProviders, target_field: column.target }, { signal })).data,
|
|
98
|
+
[listId, ids.join(","), runProviders.join(","), field],
|
|
72
99
|
{ enabled: ids.length > 0 && !jobId },
|
|
73
100
|
);
|
|
74
101
|
const quote = preflight.data;
|
|
75
|
-
const credits =
|
|
102
|
+
const credits = quotedCredits(quote); // null: the API could not price this run, so no total is shown
|
|
76
103
|
const runnable = quote ? quote.contact_count - quote.not_in_list.length : ids.length;
|
|
77
104
|
|
|
78
|
-
const start = useG8Mutation(()
|
|
79
|
-
|
|
105
|
+
const start = useG8Mutation(async (): Promise<StartedRun> => {
|
|
106
|
+
const key = `${listId}|${column.target}|${providers.join(",")}`;
|
|
107
|
+
let pipeline = pipelineRef.current?.key === key ? pipelineRef.current.pipeline : null;
|
|
108
|
+
if (!pipeline) {
|
|
109
|
+
pipeline = (await g8Post<WaterfallPipelineResult>("/enrichment/waterfall/configs", { list_id: listId, name: `${column.label} enrichment`, field: column.target, providers })).data;
|
|
110
|
+
pipelineRef.current = { key, pipeline };
|
|
111
|
+
}
|
|
112
|
+
const job = (await g8Post<EnrichJob>("/enrichment/waterfall/enrich", { column_id: pipeline.column_id, list_id: listId, record_ids: ids })).data;
|
|
113
|
+
if (!job?.job_id || String(job.status).toLowerCase() === "error") throw new Error("The enrichment did not start, so nothing was charged. Try again in a minute.");
|
|
114
|
+
return { job, pipeline, asked: providers };
|
|
115
|
+
}, {
|
|
116
|
+
onSuccess: (res) => { setConfirmOpen(false); setRun(res); setJobId(res.job.job_id); onStarted?.(res.job.job_id); },
|
|
80
117
|
});
|
|
81
118
|
const job = useJobPoll<EnrichJob>(
|
|
82
119
|
async (signal) => (await g8Get<EnrichJob>(`/enrichment/jobs/${jobId}`, undefined, { signal })).data,
|
|
83
|
-
{
|
|
120
|
+
{
|
|
121
|
+
enabled: jobId != null, intervalMs: 2000, isTerminal: (j) => isJobTerminal(j.status),
|
|
122
|
+
onDone: (j) => { void g8QueryCache.invalidate(["contacts"]); onDone?.(j); },
|
|
123
|
+
},
|
|
84
124
|
);
|
|
125
|
+
const reused = run && !run.pipeline.created && run.pipeline.providers?.length && run.pipeline.providers.join() !== run.asked.join() ? run.pipeline.providers : null;
|
|
85
126
|
const cancel = useG8Mutation(() => g8Post(`/enrichment/jobs/${jobId}/cancel`, {}));
|
|
86
127
|
const finished = job.status;
|
|
87
128
|
const nothingWritten = job.done && ((finished?.successful_enrichments ?? 0) === 0 || (finished?.failed_enrichments ?? 0) > 0);
|
|
@@ -106,9 +147,10 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
|
|
|
106
147
|
const pct = total ? Math.round(((j?.completed ?? 0) / total) * 100) : 0;
|
|
107
148
|
return (
|
|
108
149
|
<div className={`flex flex-col gap-3 ${className ?? ""}`} role="status">
|
|
109
|
-
<h3 className="text-base font-
|
|
150
|
+
<h3 className="text-base font-medium">{job.done ? (j?.status === "completed" ? "Enrichment finished" : `Enrichment ${j?.status}`) : "Enriching..."}</h3>
|
|
110
151
|
<div className="h-2 overflow-hidden rounded bg-muted" aria-label="Progress"><div className="h-full bg-primary transition-all" style={{ width: `${job.done ? 100 : pct}%` }} /></div>
|
|
111
152
|
<p className="text-sm">{j?.completed ?? 0} of {total} contacts processed{j?.successful_enrichments != null ? `, ${j.successful_enrichments} enriched, ${j.failed_enrichments ?? 0} failed` : ""}.{j?.total_credits_used != null ? ` ${j.total_credits_used} credits used.` : ""}</p>
|
|
153
|
+
{reused ? <p className="text-xs text-muted-foreground">This list already had a {column.label.toLowerCase()} pipeline, so it ran with its providers: {reused.join(" > ")}.</p> : null}
|
|
112
154
|
{j?.warnings?.map((w) => <p key={w} className="text-xs text-muted-foreground">{w}</p>)}
|
|
113
155
|
{job.error ? <ApiError error={job.error} title="Could not read the job" onRetry={() => job.start()} /> : null}
|
|
114
156
|
{errors.data?.length ? (
|
|
@@ -119,7 +161,7 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
|
|
|
119
161
|
{cancel.error ? <ApiError error={cancel.error} title="Could not cancel" /> : null}
|
|
120
162
|
<div className="flex gap-2">
|
|
121
163
|
{!job.done ? <Button variant="outline" size="sm" loading={cancel.loading} onClick={() => void cancel.mutate(undefined)}>Cancel</Button> : null}
|
|
122
|
-
{job.done ? <Button size="sm" variant="outline" onClick={() => { setJobId(null); job.stop(); }}>Enrich more</Button> : null}
|
|
164
|
+
{job.done ? <Button size="sm" variant="outline" onClick={() => { setJobId(null); setRun(null); job.stop(); }}>Enrich more</Button> : null}
|
|
123
165
|
</div>
|
|
124
166
|
</div>
|
|
125
167
|
);
|
|
@@ -129,7 +171,7 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
|
|
|
129
171
|
<div className={`flex flex-col gap-4 ${className ?? ""}`}>
|
|
130
172
|
<div className="flex flex-wrap items-center gap-3">
|
|
131
173
|
<label className="flex items-center gap-2 text-sm">Fill
|
|
132
|
-
<select aria-label="Field to fill" className="h-9 rounded-md border bg-background px-2" value={field} onChange={(e) => setField(e.target.value as TargetField)}>
|
|
174
|
+
<select aria-label="Field to fill" className="h-9 rounded-md border bg-background px-2" value={field} onChange={(e) => { setField(e.target.value as TargetField); setChosen(null); }}>
|
|
133
175
|
{TARGET_FIELDS.map((f) => <option key={f.value} value={f.value}>{f.label}</option>)}
|
|
134
176
|
</select>
|
|
135
177
|
</label>
|
|
@@ -139,7 +181,11 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
|
|
|
139
181
|
<fieldset className="flex flex-col gap-1">
|
|
140
182
|
<legend className="mb-1 text-sm font-medium">Providers, tried in this order</legend>
|
|
141
183
|
{catalog.error ? <ApiError error={catalog.error} title="Could not load providers" onRetry={() => void catalog.refetch()} /> : null}
|
|
142
|
-
{
|
|
184
|
+
{existing ? (
|
|
185
|
+
<p className="text-sm text-muted-foreground" data-testid="saved-pipeline">
|
|
186
|
+
This list already has a {column.label.toLowerCase()} pipeline ("{existing.name}"), so a run uses its providers: {existing.providers.join(" > ")}. Delete it under Pipelines to choose others.
|
|
187
|
+
</p>
|
|
188
|
+
) : providers.map((p, i) => (
|
|
143
189
|
<div key={p} className="flex items-center gap-2 text-sm">
|
|
144
190
|
<span className="w-6 text-muted-foreground">{i + 1}.</span><span className="w-32 font-medium">{p}</span>
|
|
145
191
|
<Button variant="ghost" size="sm" aria-label={`Move ${p} up`} disabled={i === 0} onClick={() => move(p, -1)}>Up</Button>
|
|
@@ -148,7 +194,7 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
|
|
|
148
194
|
</div>
|
|
149
195
|
))}
|
|
150
196
|
<div className="flex flex-wrap gap-1">
|
|
151
|
-
{
|
|
197
|
+
{(existing ? [] : offered).map((p) => (
|
|
152
198
|
<Button key={p.provider} variant="outline" size="sm" onClick={() => toggleProvider(p.provider)}>Add {p.provider}{p.byok_configured ? " (your key)" : p.system_funded ? "" : " (needs a key)"}</Button>
|
|
153
199
|
))}
|
|
154
200
|
</div>
|
|
@@ -158,15 +204,15 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
|
|
|
158
204
|
{quote ? (
|
|
159
205
|
<div className="rounded-md border p-3 text-sm" aria-label="Preflight">
|
|
160
206
|
<p><strong>{runnable.toLocaleString()}</strong> of {quote.contact_count.toLocaleString()} contacts will be enriched{quote.not_in_list.length ? `; ${quote.not_in_list.length} are not on this list and are skipped (not charged)` : ""}{quote.duplicates_removed ? `; ${quote.duplicates_removed} duplicates removed` : ""}.</p>
|
|
161
|
-
<p className="mt-1">{quote.max_total_credits != null ? `At most ${quote.max_total_credits.toLocaleString()} credits (${quote.credits_per_record} per record).` : `
|
|
162
|
-
{quote.missing_credentials.length ? <p role="alert" className="mt-1 text-
|
|
207
|
+
<p className="mt-1">{quote.max_total_credits != null ? `At most ${quote.max_total_credits.toLocaleString()} credits (${quote.credits_per_record} per record). Records that already have a value or find nothing cost less.` : `The API could not price this run, so no total is shown. Providers on ${brand.linkLabel}'s key spend credits per record; providers on your own key cost none.`}</p>
|
|
208
|
+
{quote.missing_credentials.length ? <p role="alert" className="mt-1 text-warning-foreground">No API key for {quote.missing_credentials.join(", ")}: those steps are skipped. Add the keys in {brand.linkLabel} settings.</p> : null}
|
|
163
209
|
{quote.warnings.map((w) => <p key={w} className="mt-1 text-xs text-muted-foreground">{w}</p>)}
|
|
164
210
|
</div>
|
|
165
211
|
) : null}
|
|
166
212
|
{start.error ? <ApiError error={start.error} title="Could not start the enrichment" /> : null}
|
|
167
213
|
|
|
168
214
|
<div className="flex justify-end">
|
|
169
|
-
<Button disabled={ids.length === 0 ||
|
|
215
|
+
<Button disabled={ids.length === 0 || runProviders.length === 0 || preflight.loading || runnable === 0 || Boolean(preflight.error)} onClick={() => setConfirmOpen(true)}>
|
|
170
216
|
Enrich {plural(runnable, "contact")}
|
|
171
217
|
</Button>
|
|
172
218
|
</div>
|
|
@@ -175,8 +221,8 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
|
|
|
175
221
|
open={confirmOpen}
|
|
176
222
|
tier="billable"
|
|
177
223
|
title={`Enrich ${runnable.toLocaleString()} contacts: ${column.label.toLowerCase()}`}
|
|
178
|
-
description={`Providers in order: ${
|
|
179
|
-
estimatedCredits={credits}
|
|
224
|
+
description={`Providers in order: ${runProviders.join(", ")}${existing ? ` (the list's saved ${column.label.toLowerCase()} pipeline)` : ""}. ${credits != null ? "The estimate is the most this run can charge; your balance is not checked here." : "The API could not price this run, so no total is shown; your balance is not checked here."}`}
|
|
225
|
+
estimatedCredits={credits ?? undefined}
|
|
180
226
|
affected={runnable}
|
|
181
227
|
confirmLabel="Start enrichment"
|
|
182
228
|
confirming={start.loading}
|
|
@@ -194,15 +240,18 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
|
|
|
194
240
|
export function BulkEnrichPanel(props: BulkEnrichPanelProps) {
|
|
195
241
|
const [listId, setListId] = useState<number | undefined>(undefined);
|
|
196
242
|
const [picked, setPicked] = useState<number[]>([]);
|
|
243
|
+
const [tableVersion, setTableVersion] = useState(0);
|
|
197
244
|
if (props.listId != null && props.contactIds) return <BulkEnrichCore {...props} listId={props.listId} contactIds={props.contactIds} />;
|
|
198
245
|
const { listId: _l, contactIds: _c, ...rest } = props;
|
|
246
|
+
// A finished run reloads the table so the filled values show (this also clears the ticked contacts).
|
|
247
|
+
const done = (job: EnrichJob) => { setTableVersion((v) => v + 1); props.onDone?.(job); };
|
|
199
248
|
return (
|
|
200
249
|
<div className={`flex flex-col gap-4 ${props.className ?? ""}`}>
|
|
201
250
|
<ListPicker value={listId} allowCreate={false} onChange={(l) => { setListId(l.id); setPicked([]); }} />
|
|
202
251
|
{listId != null ? (
|
|
203
252
|
<>
|
|
204
|
-
<ContactTable listId={listId} selectable pageSize={10} onSelectionChange={(ids) => setPicked(ids)} />
|
|
205
|
-
<BulkEnrichCore {...rest} className={undefined} listId={listId} contactIds={picked} />
|
|
253
|
+
<ContactTable listId={listId} selectable pageSize={10} refreshKey={tableVersion} onSelectionChange={(ids) => setPicked(ids)} />
|
|
254
|
+
<BulkEnrichCore {...rest} className={undefined} listId={listId} contactIds={picked} onDone={done} />
|
|
206
255
|
</>
|
|
207
256
|
) : <p className="text-sm text-muted-foreground">Choose a list to enrich its contacts.</p>}
|
|
208
257
|
</div>
|
|
@@ -63,9 +63,9 @@ export function classifyVerification(status: string | null | undefined, isValid?
|
|
|
63
63
|
|
|
64
64
|
const STYLE: Record<VerifyState, string> = {
|
|
65
65
|
unverified: "bg-muted text-muted-foreground",
|
|
66
|
-
valid: "bg-
|
|
67
|
-
risky: "bg-
|
|
68
|
-
invalid: "bg-
|
|
66
|
+
valid: "bg-positive text-positive-foreground",
|
|
67
|
+
risky: "bg-warning text-warning-foreground",
|
|
68
|
+
invalid: "bg-error text-error-foreground",
|
|
69
69
|
unverifiable: "bg-muted text-muted-foreground",
|
|
70
70
|
};
|
|
71
71
|
const LABEL: Record<VerifyState, string> = { unverified: "Unverified", valid: "Valid", risky: "Risky", invalid: "Invalid", unverifiable: "Couldn't verify" };
|
|
@@ -138,7 +138,7 @@ export function EnrichButton({ kind, lookup, record, recordLoaded = false, label
|
|
|
138
138
|
<li key={k} className="flex items-center gap-2">
|
|
139
139
|
{target ? <input type="checkbox" aria-label={`Apply ${k}`} checked={ticked.has(k)} onChange={() => setTicked((t) => { const n = new Set(t); if (!n.delete(k)) n.add(k); return n; })} /> : null}
|
|
140
140
|
<span className="w-36 text-muted-foreground">{k.replace(/_/g, " ")}</span><span className="truncate">{v}</span>
|
|
141
|
-
{target?.checked && target.existing[k] && !isMaskedValue(target.existing[k]) && target.existing[k] !== v ? <span className="text-xs text-
|
|
141
|
+
{target?.checked && target.existing[k] && !isMaskedValue(target.existing[k]) && target.existing[k] !== v ? <span className="text-xs text-warning-foreground">replaces {String(target.existing[k])}</span> : null}
|
|
142
142
|
</li>
|
|
143
143
|
))}
|
|
144
144
|
{Object.keys(fields).length === 0 ? <li className="text-muted-foreground">The match had no fields to show.</li> : null}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* EnrichmentActivity: how much enrichment ran, how often it worked, and which fields it filled, for a chosen window.
|
|
5
|
+
* All four reads are free.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations (all read, free):
|
|
8
|
+
* 1. getEnrichmentMetrics GET /enrichment/analytics/metrics?period -> { total_enrichments, success_rate (a percent, 0 to 100) }
|
|
9
|
+
* 2. getEnrichmentTrends GET /enrichment/analytics/trends?period&granularity -> { data_points[{ date, successful_enrichments, failed_enrichments }] }
|
|
10
|
+
* 3. getEnrichmentDailyActivity GET /enrichment/analytics/daily-activity?period -> { activity_points[{ date, enrichments, new_contacts }] }
|
|
11
|
+
* 4. getEnrichmentFieldDistribution GET /enrichment/analytics/field-distribution?period -> { field_types[{ field_type, count, percentage }], total_enrichments }
|
|
12
|
+
*
|
|
13
|
+
* The API counts COMPLETED jobs only (running, paused and failed jobs are not in these numbers), says so here, and the
|
|
14
|
+
* cost it returns is the API's own estimate at a fixed rate rather than credits charged, so no cost is shown.
|
|
15
|
+
* Every panel clears its numbers while a new window loads and when a read fails; a rate with nothing behind it shows
|
|
16
|
+
* a dash, never 0%.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useState } from "react";
|
|
20
|
+
import type { ReactNode } from "react";
|
|
21
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
23
|
+
import { BarList } from "@/g8/components/screen/bar-list";
|
|
24
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
25
|
+
import { StatStrip } from "@/g8/components/screen/stat-strip";
|
|
26
|
+
import { TimeSeriesChart } from "@/g8/components/screen/time-series-chart";
|
|
27
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
28
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
29
|
+
import { GRANULARITIES, PERIODS } from "../lib/pipeline-types";
|
|
30
|
+
import type { Granularity, Period } from "../lib/pipeline-types";
|
|
31
|
+
|
|
32
|
+
interface Metrics { total_enrichments: number; success_rate: number }
|
|
33
|
+
interface Trend { data_points: Array<{ date: string; total_enrichments: number; successful_enrichments: number; failed_enrichments: number }> }
|
|
34
|
+
interface Daily { activity_points: Array<{ date: string; enrichments: number; new_contacts: number }> }
|
|
35
|
+
interface Distribution { field_types: Array<{ field_type: string; count: number; percentage: number }>; total_enrichments: number }
|
|
36
|
+
|
|
37
|
+
const SELECT = "h-9 rounded-md border bg-background px-2 text-sm";
|
|
38
|
+
const humanField = (t: string) => t.replace(/[_-]+/g, " ").replace(/^./, (c) => c.toUpperCase());
|
|
39
|
+
|
|
40
|
+
function Panel({ title, description, q, empty, children }: { title: string; description?: string; q: { error: unknown; loading: boolean; refetch: () => Promise<unknown> }; empty: boolean; children: ReactNode }) {
|
|
41
|
+
return (
|
|
42
|
+
<SectionCard title={title} description={description}>
|
|
43
|
+
{q.error ? <ApiError error={q.error} title={`Could not load ${title.toLowerCase()}`} onRetry={() => void q.refetch()} /> : null}
|
|
44
|
+
{!q.error && q.loading ? <p className="text-sm text-muted-foreground" role="status">Loading...</p> : null}
|
|
45
|
+
{!q.error && !q.loading && empty ? <EmptyState title="Nothing in this window" description="No completed enrichment jobs fall in these dates." /> : null}
|
|
46
|
+
{!q.error && !q.loading && !empty ? children : null}
|
|
47
|
+
</SectionCard>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function EnrichmentActivity({ className }: { className?: string }) {
|
|
52
|
+
const [period, setPeriod] = useState<Period>("last_30_days");
|
|
53
|
+
const [granularity, setGranularity] = useState<Granularity>("day");
|
|
54
|
+
const metrics = useG8Query(async (signal) => (await g8Get<Metrics>("/enrichment/analytics/metrics", { period }, { signal })).data, [period]);
|
|
55
|
+
const trends = useG8Query(async (signal) => (await g8Get<Trend>("/enrichment/analytics/trends", { period, granularity }, { signal })).data, [period, granularity]);
|
|
56
|
+
const daily = useG8Query(async (signal) => (await g8Get<Daily>("/enrichment/analytics/daily-activity", { period }, { signal })).data, [period]);
|
|
57
|
+
const fields = useG8Query(async (signal) => (await g8Get<Distribution>("/enrichment/analytics/field-distribution", { period }, { signal })).data, [period]);
|
|
58
|
+
|
|
59
|
+
const m = metrics.error || metrics.loading ? undefined : metrics.data;
|
|
60
|
+
const points = trends.error || trends.loading ? [] : trends.data?.data_points ?? [];
|
|
61
|
+
const days = daily.error || daily.loading ? [] : daily.data?.activity_points ?? [];
|
|
62
|
+
const kinds = fields.error || fields.loading ? [] : fields.data?.field_types ?? [];
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className={`flex flex-col gap-4 ${className ?? ""}`}>
|
|
66
|
+
<div className="flex flex-wrap items-end gap-3">
|
|
67
|
+
<label className="flex flex-col gap-1 text-sm">Window
|
|
68
|
+
<select className={SELECT} value={period} onChange={(e) => setPeriod(e.target.value as Period)}>{PERIODS.map((p) => <option key={p.value} value={p.value}>{p.label}</option>)}</select>
|
|
69
|
+
</label>
|
|
70
|
+
<label className="flex flex-col gap-1 text-sm">Group trends by
|
|
71
|
+
<select className={SELECT} value={granularity} onChange={(e) => setGranularity(e.target.value as Granularity)}>{GRANULARITIES.map((g) => <option key={g} value={g}>{g === "day" ? "Day" : g === "week" ? "Week" : "Month"}</option>)}</select>
|
|
72
|
+
</label>
|
|
73
|
+
<p className="pb-2 text-xs text-muted-foreground">Counts completed jobs only. Running, paused and failed jobs are not included.</p>
|
|
74
|
+
</div>
|
|
75
|
+
<StatStrip loading={metrics.loading} items={[
|
|
76
|
+
{ label: "Records enriched", value: m ? m.total_enrichments.toLocaleString() : "-" },
|
|
77
|
+
{ label: "Success rate", value: m && m.total_enrichments > 0 ? `${Math.round(m.success_rate)}%` : "-", hint: "Share of records the providers found a value for." },
|
|
78
|
+
]} />
|
|
79
|
+
{metrics.error ? <ApiError error={metrics.error} title="Could not load the totals" onRetry={() => void metrics.refetch()} /> : null}
|
|
80
|
+
<div className="grid gap-4 lg:grid-cols-2">
|
|
81
|
+
<Panel title="Results over time" description="Records enriched and failed in each period." q={trends} empty={points.length === 0}>
|
|
82
|
+
<TimeSeriesChart kind="bar" stacked xKey="date" data={points} series={[{ key: "successful_enrichments", label: "Enriched", tone: "positive" }, { key: "failed_enrichments", label: "Failed", tone: "negative" }]} />
|
|
83
|
+
</Panel>
|
|
84
|
+
<Panel title="Daily activity" description="Records enriched and new contacts each day." q={daily} empty={days.length === 0}>
|
|
85
|
+
<TimeSeriesChart kind="line" xKey="date" data={days} series={[{ key: "enrichments", label: "Enriched" }, { key: "new_contacts", label: "New contacts", tone: "prior" }]} />
|
|
86
|
+
</Panel>
|
|
87
|
+
</div>
|
|
88
|
+
<Panel title="Fields filled" description="Which fields enrichment filled in this window." q={fields} empty={kinds.length === 0}>
|
|
89
|
+
<BarList items={kinds.map((k) => ({ label: humanField(k.field_type), value: k.count, display: `${k.count.toLocaleString()} (${Math.round(k.percentage)}%)` }))} />
|
|
90
|
+
</Panel>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -10,10 +10,15 @@
|
|
|
10
10
|
* read once more with ?include_provider_errors=true when nothing was enriched or some records failed
|
|
11
11
|
* -> { status, total, completed, successful_enrichments, failed_enrichments, total_credits_used, warnings, provider_errors }
|
|
12
12
|
* 2. cancelEnrichmentJob POST /enrichment/jobs/{job_id}/cancel (write, free; records already done keep their data and charge)
|
|
13
|
+
* 3. pauseEnrichmentJob POST /enrichment/jobs/{job_id}/pause (write, free; stops further charges until resumed)
|
|
14
|
+
* 4. resumeEnrichmentJob POST /enrichment/jobs/{job_id}/resume (billable, ConfirmSpendDialog; carries on with, and charges for, the records not yet reached)
|
|
15
|
+
* The state shown is always the server's: after Pause or Resume the job is read again, never assumed.
|
|
13
16
|
*/
|
|
14
17
|
|
|
18
|
+
import { useState } from "react";
|
|
15
19
|
import { Button } from "@graph8/sdk/ui";
|
|
16
20
|
import { ApiError } from "@/g8/components/api-error";
|
|
21
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
17
22
|
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
18
23
|
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
19
24
|
import { StatusPill, humanizeStatus, toneForStatus } from "@/g8/components/screen/status-pill";
|
|
@@ -37,6 +42,9 @@ export function EnrichmentJobDetail({ jobId, onDone, className }: EnrichmentJobD
|
|
|
37
42
|
{ intervalMs: 2000, isTerminal: (j) => isJobTerminal(j.status), onDone },
|
|
38
43
|
);
|
|
39
44
|
const cancel = useG8Mutation(() => g8Post(`/enrichment/jobs/${jobId}/cancel`, {}));
|
|
45
|
+
const [resuming, setResuming] = useState(false);
|
|
46
|
+
const pause = useG8Mutation(() => g8Post(`/enrichment/jobs/${jobId}/pause`, {}), { onSuccess: () => job.start() });
|
|
47
|
+
const resume = useG8Mutation(() => g8Post(`/enrichment/jobs/${jobId}/resume`, {}), { onSuccess: () => { setResuming(false); job.start(); }, onError: () => setResuming(false) });
|
|
40
48
|
const j = job.status;
|
|
41
49
|
const problems = job.done && j != null && ((j.successful_enrichments ?? 0) === 0 || (j.failed_enrichments ?? 0) > 0);
|
|
42
50
|
const errors = useG8Query(
|
|
@@ -45,6 +53,8 @@ export function EnrichmentJobDetail({ jobId, onDone, className }: EnrichmentJobD
|
|
|
45
53
|
{ enabled: problems },
|
|
46
54
|
);
|
|
47
55
|
const total = j?.total ?? 0;
|
|
56
|
+
const state = String(j?.status ?? "").toLowerCase();
|
|
57
|
+
const remaining = Math.max(0, total - (j?.completed ?? 0));
|
|
48
58
|
const pct = total ? Math.min(100, Math.round(((j?.completed ?? 0) / total) * 100)) : 0;
|
|
49
59
|
|
|
50
60
|
if (job.error && !j) return <ApiError error={job.error} title="Could not read the job" onRetry={() => job.start()} />;
|
|
@@ -52,10 +62,15 @@ export function EnrichmentJobDetail({ jobId, onDone, className }: EnrichmentJobD
|
|
|
52
62
|
<div className={`flex flex-col gap-4 ${className ?? ""}`} role="status">
|
|
53
63
|
<div className="flex items-center gap-2">
|
|
54
64
|
{j ? <StatusPill tone={toneForStatus(j.status)} dot>{humanizeStatus(j.status)}</StatusPill> : <span className="text-sm text-muted-foreground">Loading...</span>}
|
|
65
|
+
{!job.done && state === "paused" ? <Button size="sm" onClick={() => setResuming(true)}>Resume job</Button> : null}
|
|
66
|
+
{!job.done && (state === "running" || state === "pending") ? <Button size="sm" variant="outline" loading={pause.loading} onClick={() => void pause.mutate(undefined)}>Pause job</Button> : null}
|
|
55
67
|
{!job.done && j ? <Button size="sm" variant="outline" loading={cancel.loading} onClick={() => void cancel.mutate(undefined)}>Cancel job</Button> : null}
|
|
56
68
|
</div>
|
|
57
69
|
<div className="h-2 overflow-hidden rounded bg-muted" aria-label="Progress"><div className="h-full bg-primary" style={{ width: `${job.done ? 100 : pct}%` }} /></div>
|
|
70
|
+
{j?.paused_at && state === "paused" ? <p className="text-sm text-muted-foreground">Paused {new Date(j.paused_at).toLocaleString()}. Nothing more is enriched or charged until you resume.</p> : null}
|
|
58
71
|
{cancel.error ? <ApiError error={cancel.error} title="Could not cancel" /> : null}
|
|
72
|
+
{pause.error ? <ApiError error={pause.error} title="Could not pause" /> : null}
|
|
73
|
+
{resume.error ? <ApiError error={resume.error} title="Could not resume" /> : null}
|
|
59
74
|
<SectionCard title="Result">
|
|
60
75
|
<KeyValueList columns={2} items={[
|
|
61
76
|
{ label: "Job id", value: jobId, mono: true, wide: true },
|
|
@@ -73,6 +88,9 @@ export function EnrichmentJobDetail({ jobId, onDone, className }: EnrichmentJobD
|
|
|
73
88
|
</ul>
|
|
74
89
|
</SectionCard>
|
|
75
90
|
) : null}
|
|
91
|
+
<ConfirmSpendDialog open={resuming} tier="billable" title="Resume this job" affected={remaining}
|
|
92
|
+
description="The job carries on with the records it had not reached and charges for them as it goes. Records already enriched are not charged again. The API does not quote a total for a resume, so no credit figure is shown."
|
|
93
|
+
confirmLabel="Resume" confirming={resume.loading} onConfirm={() => void resume.mutate(undefined)} onCancel={() => setResuming(false)} />
|
|
76
94
|
</div>
|
|
77
95
|
);
|
|
78
96
|
}
|