@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
|
@@ -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`, or 2 credits x contacts when the preflight could not price it;
|
|
16
|
+
* there is no balance read, so this is an estimate of 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,54 +39,90 @@ 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";
|
|
45
|
+
import { plural } from "@/g8/lib/plural";
|
|
34
46
|
import { useProviders } from "../hooks/use-providers";
|
|
35
|
-
import { CREDITS_PER_RECORD_ESTIMATE, TARGET_FIELDS, estimateCredits, isJobTerminal } from "../lib/enrichment-types";
|
|
36
|
-
import type { EnrichJob, Preflight, TargetField } from "../lib/enrichment-types";
|
|
47
|
+
import { CREDITS_PER_RECORD_ESTIMATE, FIELD_PROVIDERS, TARGET_FIELDS, estimateCredits, isJobTerminal, providersForField } from "../lib/enrichment-types";
|
|
48
|
+
import type { EnrichJob, Preflight, TargetField, WaterfallConfigSummary, WaterfallPipelineResult } from "../lib/enrichment-types";
|
|
49
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
37
50
|
|
|
38
51
|
export interface BulkEnrichPanelProps {
|
|
39
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. */
|
|
40
53
|
listId?: number;
|
|
41
54
|
contactIds?: number[];
|
|
42
|
-
/**
|
|
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
|
+
*/
|
|
43
59
|
defaultProviders?: string[];
|
|
44
60
|
defaultField?: TargetField;
|
|
45
61
|
/** Called when the job reached a final state. */
|
|
46
62
|
onDone?: (job: EnrichJob) => void;
|
|
63
|
+
/** Called with the job id as soon as a run starts (to list it in a job history). */
|
|
64
|
+
onStarted?: (jobId: string) => void;
|
|
47
65
|
className?: string;
|
|
48
66
|
}
|
|
49
67
|
|
|
50
|
-
|
|
68
|
+
type StartedRun = { job: EnrichJob; pipeline: WaterfallPipelineResult; asked: string[] };
|
|
51
69
|
|
|
52
|
-
function BulkEnrichCore({ listId, contactIds, defaultProviders
|
|
70
|
+
function BulkEnrichCore({ listId, contactIds, defaultProviders, defaultField = "work_email", onDone, onStarted, className }: BulkEnrichPanelProps & { listId: number; contactIds: number[] }) {
|
|
71
|
+
const brand = useBrand();
|
|
53
72
|
const [field, setField] = useState<TargetField>(defaultField);
|
|
54
73
|
const [chosen, setChosen] = useState<string[] | null>(null);
|
|
55
74
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
56
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);
|
|
57
79
|
const catalog = useProviders();
|
|
58
80
|
|
|
59
81
|
const known = new Set(catalog.providers.map((p) => p.provider));
|
|
60
|
-
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));
|
|
61
84
|
const column = TARGET_FIELDS.find((f) => f.value === field)!;
|
|
62
85
|
const ids = [...new Set(contactIds)];
|
|
63
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
|
+
|
|
64
96
|
const preflight = useG8Query(
|
|
65
|
-
async (signal) => (await g8Post<Preflight>("/enrichment/preflight", { list_id: listId, contact_ids: ids, mode: "waterfall", providers, target_field: column.target }, { signal })).data,
|
|
66
|
-
[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],
|
|
67
99
|
{ enabled: ids.length > 0 && !jobId },
|
|
68
100
|
);
|
|
69
101
|
const quote = preflight.data;
|
|
70
102
|
const credits = estimateCredits(quote, ids.length);
|
|
71
103
|
const runnable = quote ? quote.contact_count - quote.not_in_list.length : ids.length;
|
|
72
104
|
|
|
73
|
-
const start = useG8Mutation(()
|
|
74
|
-
|
|
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); },
|
|
75
117
|
});
|
|
76
118
|
const job = useJobPoll<EnrichJob>(
|
|
77
119
|
async (signal) => (await g8Get<EnrichJob>(`/enrichment/jobs/${jobId}`, undefined, { signal })).data,
|
|
78
|
-
{
|
|
120
|
+
{
|
|
121
|
+
enabled: jobId != null, intervalMs: 2000, isTerminal: (j) => isJobTerminal(j.status),
|
|
122
|
+
onDone: (j) => { void g8QueryCache.invalidate(["contacts"]); onDone?.(j); },
|
|
123
|
+
},
|
|
79
124
|
);
|
|
125
|
+
const reused = run && !run.pipeline.created && run.pipeline.providers?.length && run.pipeline.providers.join() !== run.asked.join() ? run.pipeline.providers : null;
|
|
80
126
|
const cancel = useG8Mutation(() => g8Post(`/enrichment/jobs/${jobId}/cancel`, {}));
|
|
81
127
|
const finished = job.status;
|
|
82
128
|
const nothingWritten = job.done && ((finished?.successful_enrichments ?? 0) === 0 || (finished?.failed_enrichments ?? 0) > 0);
|
|
@@ -101,9 +147,10 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
|
|
|
101
147
|
const pct = total ? Math.round(((j?.completed ?? 0) / total) * 100) : 0;
|
|
102
148
|
return (
|
|
103
149
|
<div className={`flex flex-col gap-3 ${className ?? ""}`} role="status">
|
|
104
|
-
<h3 className="text-base font-
|
|
150
|
+
<h3 className="text-base font-medium">{job.done ? (j?.status === "completed" ? "Enrichment finished" : `Enrichment ${j?.status}`) : "Enriching..."}</h3>
|
|
105
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>
|
|
106
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}
|
|
107
154
|
{j?.warnings?.map((w) => <p key={w} className="text-xs text-muted-foreground">{w}</p>)}
|
|
108
155
|
{job.error ? <ApiError error={job.error} title="Could not read the job" onRetry={() => job.start()} /> : null}
|
|
109
156
|
{errors.data?.length ? (
|
|
@@ -114,7 +161,7 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
|
|
|
114
161
|
{cancel.error ? <ApiError error={cancel.error} title="Could not cancel" /> : null}
|
|
115
162
|
<div className="flex gap-2">
|
|
116
163
|
{!job.done ? <Button variant="outline" size="sm" loading={cancel.loading} onClick={() => void cancel.mutate(undefined)}>Cancel</Button> : null}
|
|
117
|
-
{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}
|
|
118
165
|
</div>
|
|
119
166
|
</div>
|
|
120
167
|
);
|
|
@@ -124,17 +171,21 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
|
|
|
124
171
|
<div className={`flex flex-col gap-4 ${className ?? ""}`}>
|
|
125
172
|
<div className="flex flex-wrap items-center gap-3">
|
|
126
173
|
<label className="flex items-center gap-2 text-sm">Fill
|
|
127
|
-
<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); }}>
|
|
128
175
|
{TARGET_FIELDS.map((f) => <option key={f.value} value={f.value}>{f.label}</option>)}
|
|
129
176
|
</select>
|
|
130
177
|
</label>
|
|
131
|
-
<span className="text-sm text-muted-foreground">for {
|
|
178
|
+
<span className="text-sm text-muted-foreground">for {plural(ids.length, "contact")}</span>
|
|
132
179
|
</div>
|
|
133
180
|
|
|
134
181
|
<fieldset className="flex flex-col gap-1">
|
|
135
182
|
<legend className="mb-1 text-sm font-medium">Providers, tried in this order</legend>
|
|
136
183
|
{catalog.error ? <ApiError error={catalog.error} title="Could not load providers" onRetry={() => void catalog.refetch()} /> : null}
|
|
137
|
-
{
|
|
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) => (
|
|
138
189
|
<div key={p} className="flex items-center gap-2 text-sm">
|
|
139
190
|
<span className="w-6 text-muted-foreground">{i + 1}.</span><span className="w-32 font-medium">{p}</span>
|
|
140
191
|
<Button variant="ghost" size="sm" aria-label={`Move ${p} up`} disabled={i === 0} onClick={() => move(p, -1)}>Up</Button>
|
|
@@ -143,7 +194,7 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
|
|
|
143
194
|
</div>
|
|
144
195
|
))}
|
|
145
196
|
<div className="flex flex-wrap gap-1">
|
|
146
|
-
{
|
|
197
|
+
{(existing ? [] : offered).map((p) => (
|
|
147
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>
|
|
148
199
|
))}
|
|
149
200
|
</div>
|
|
@@ -154,15 +205,15 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
|
|
|
154
205
|
<div className="rounded-md border p-3 text-sm" aria-label="Preflight">
|
|
155
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>
|
|
156
207
|
<p className="mt-1">{quote.max_total_credits != null ? `At most ${quote.max_total_credits.toLocaleString()} credits (${quote.credits_per_record} per record).` : `About ${credits.toLocaleString()} credits (${CREDITS_PER_RECORD_ESTIMATE} per record, the API's flat estimate).`} Records that already have a value or find nothing cost less.</p>
|
|
157
|
-
{quote.missing_credentials.length ? <p role="alert" className="mt-1 text-
|
|
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}
|
|
158
209
|
{quote.warnings.map((w) => <p key={w} className="mt-1 text-xs text-muted-foreground">{w}</p>)}
|
|
159
210
|
</div>
|
|
160
211
|
) : null}
|
|
161
212
|
{start.error ? <ApiError error={start.error} title="Could not start the enrichment" /> : null}
|
|
162
213
|
|
|
163
214
|
<div className="flex justify-end">
|
|
164
|
-
<Button disabled={ids.length === 0 ||
|
|
165
|
-
Enrich {
|
|
215
|
+
<Button disabled={ids.length === 0 || runProviders.length === 0 || preflight.loading || runnable === 0 || Boolean(preflight.error)} onClick={() => setConfirmOpen(true)}>
|
|
216
|
+
Enrich {plural(runnable, "contact")}
|
|
166
217
|
</Button>
|
|
167
218
|
</div>
|
|
168
219
|
|
|
@@ -170,7 +221,7 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
|
|
|
170
221
|
open={confirmOpen}
|
|
171
222
|
tier="billable"
|
|
172
223
|
title={`Enrich ${runnable.toLocaleString()} contacts: ${column.label.toLowerCase()}`}
|
|
173
|
-
description={`Providers in order: ${
|
|
224
|
+
description={`Providers in order: ${runProviders.join(", ")}${existing ? ` (the list's saved ${column.label.toLowerCase()} pipeline)` : ""}. The estimate is the most this run can charge; your balance is not checked here.`}
|
|
174
225
|
estimatedCredits={credits}
|
|
175
226
|
affected={runnable}
|
|
176
227
|
confirmLabel="Start enrichment"
|
|
@@ -189,15 +240,18 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
|
|
|
189
240
|
export function BulkEnrichPanel(props: BulkEnrichPanelProps) {
|
|
190
241
|
const [listId, setListId] = useState<number | undefined>(undefined);
|
|
191
242
|
const [picked, setPicked] = useState<number[]>([]);
|
|
243
|
+
const [tableVersion, setTableVersion] = useState(0);
|
|
192
244
|
if (props.listId != null && props.contactIds) return <BulkEnrichCore {...props} listId={props.listId} contactIds={props.contactIds} />;
|
|
193
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); };
|
|
194
248
|
return (
|
|
195
249
|
<div className={`flex flex-col gap-4 ${props.className ?? ""}`}>
|
|
196
250
|
<ListPicker value={listId} allowCreate={false} onChange={(l) => { setListId(l.id); setPicked([]); }} />
|
|
197
251
|
{listId != null ? (
|
|
198
252
|
<>
|
|
199
|
-
<ContactTable listId={listId} selectable pageSize={10} onSelectionChange={(ids) => setPicked(ids)} />
|
|
200
|
-
<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} />
|
|
201
255
|
</>
|
|
202
256
|
) : <p className="text-sm text-muted-foreground">Choose a list to enrich its contacts.</p>}
|
|
203
257
|
</div>
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
* EmailVerifyBadge: a small status chip next to an email address (Unverified, Valid, Risky, Invalid) with
|
|
5
5
|
* a "Verify" action.
|
|
6
6
|
*
|
|
7
|
+
* The verdict comes from the verifier's own result code (EmailListVerify and friends; the backend's vocabulary is
|
|
8
|
+
* shared/services/email_verification_classifier.py): `ok`/`valid` is Valid; catch-all codes (`ok_for_all`,
|
|
9
|
+
* `accept_all`) are Risky; hard failures (`email_disabled`, `invalid_mx`, `dead_server`, `no_mx`, ...) are Invalid, so a
|
|
10
|
+
* known-bad address is never offered a second paid Verify; `unknown`/`error` (the verifier was inconclusive or the call
|
|
11
|
+
* failed) is "Couldn't verify", which can be retried.
|
|
12
|
+
*
|
|
7
13
|
* SDK operations, in order:
|
|
8
14
|
* 1. verifyEmail POST /enrichment/verify-email { email } (billable)
|
|
9
15
|
* Goes through ConfirmSpendDialog first: 1 credit per verification.
|
|
@@ -20,7 +26,7 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
|
20
26
|
import { g8Post } from "@/g8/lib/g8-client";
|
|
21
27
|
import { CREDITS_PER_EMAIL_VERIFICATION, isMaskedValue } from "../lib/enrichment-types";
|
|
22
28
|
|
|
23
|
-
export type VerifyState = "unverified" | "valid" | "risky" | "invalid";
|
|
29
|
+
export type VerifyState = "unverified" | "valid" | "risky" | "invalid" | "unverifiable";
|
|
24
30
|
|
|
25
31
|
export interface EmailVerifyBadgeProps {
|
|
26
32
|
email?: string | null;
|
|
@@ -32,23 +38,37 @@ export interface EmailVerifyBadgeProps {
|
|
|
32
38
|
className?: string;
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
|
|
41
|
+
// The verifier's result codes, normalised to lowercase with "-" (shared/services/email_verification_classifier.py).
|
|
42
|
+
// `error` is EmailListVerify's "delivery failed" and the classifier counts it NOT_VALID, so it is Invalid here; a
|
|
43
|
+
// vendor failure reaches the client as `unknown` (the endpoint catches it and charges nothing). `role-based` and
|
|
44
|
+
// `timeout` are not in the backend vocabulary; they are kept as harmless aliases from other verifiers.
|
|
45
|
+
const VALID = new Set(["valid", "ok", "deliverable", "ultra-sure", "sure"]);
|
|
46
|
+
const RISKY = new Set(["catch-all", "accept-all", "ok-for-all", "role-based"]);
|
|
47
|
+
const INVALID = new Set([
|
|
48
|
+
"invalid", "fail", "undeliverable", "unlikely", "unsure", "email-disabled", "dead-server", "invalid-mx", "invalid-syntax",
|
|
49
|
+
"bad-mx", "no-mx", "abuse", "do-not-mail", "spamtrap", "disposable", "bounced", "error",
|
|
50
|
+
]);
|
|
51
|
+
const UNVERIFIABLE = new Set(["unknown", "unverifiable", "smtp-protocol", "antispam-system", "timeout"]);
|
|
52
|
+
|
|
53
|
+
/** Collapse the verifier's vocabulary into the states the chip shows. */
|
|
36
54
|
export function classifyVerification(status: string | null | undefined, isValid?: boolean): VerifyState {
|
|
37
|
-
const s = String(status ?? "").toLowerCase().replace(/_
|
|
55
|
+
const s = String(status ?? "").trim().toLowerCase().replace(/[_\s]+/g, "-");
|
|
38
56
|
if (!s) return "unverified";
|
|
39
|
-
if (
|
|
40
|
-
if (s
|
|
41
|
-
if (
|
|
57
|
+
if (RISKY.has(s)) return "risky";
|
|
58
|
+
if (VALID.has(s)) return "valid";
|
|
59
|
+
if (INVALID.has(s)) return "invalid";
|
|
60
|
+
if (UNVERIFIABLE.has(s)) return "unverifiable";
|
|
42
61
|
return isValid === true ? "valid" : "unverified"; // an unrecognised status is not proof of a bad address
|
|
43
62
|
}
|
|
44
63
|
|
|
45
64
|
const STYLE: Record<VerifyState, string> = {
|
|
46
65
|
unverified: "bg-muted text-muted-foreground",
|
|
47
|
-
valid: "bg-
|
|
48
|
-
risky: "bg-
|
|
49
|
-
invalid: "bg-
|
|
66
|
+
valid: "bg-positive text-positive-foreground",
|
|
67
|
+
risky: "bg-warning text-warning-foreground",
|
|
68
|
+
invalid: "bg-error text-error-foreground",
|
|
69
|
+
unverifiable: "bg-muted text-muted-foreground",
|
|
50
70
|
};
|
|
51
|
-
const LABEL: Record<VerifyState, string> = { unverified: "Unverified", valid: "Valid", risky: "Risky", invalid: "Invalid" };
|
|
71
|
+
const LABEL: Record<VerifyState, string> = { unverified: "Unverified", valid: "Valid", risky: "Risky", invalid: "Invalid", unverifiable: "Couldn't verify" };
|
|
52
72
|
|
|
53
73
|
export function EmailVerifyBadge({ email, initialStatus, onVerified, readOnly, className }: EmailVerifyBadgeProps) {
|
|
54
74
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
@@ -72,7 +92,7 @@ export function EmailVerifyBadge({ email, initialStatus, onVerified, readOnly, c
|
|
|
72
92
|
return (
|
|
73
93
|
<span className={`inline-flex items-center gap-1 ${className ?? ""}`}>
|
|
74
94
|
<span data-state={state} title={detail ?? undefined} className={`rounded-full px-2 py-0.5 text-xs font-medium ${STYLE[state]}`}>{LABEL[state]}</span>
|
|
75
|
-
{!readOnly && state === "unverified" ? <Button variant="link" size="sm" className="h-auto p-0 text-xs" onClick={() => setConfirmOpen(true)}>Verify</Button> : null}
|
|
95
|
+
{!readOnly && (state === "unverified" || state === "unverifiable") ? <Button variant="link" size="sm" className="h-auto p-0 text-xs" onClick={() => setConfirmOpen(true)}>{state === "unverifiable" ? "Try again" : "Verify"}</Button> : null}
|
|
76
96
|
{verify.error ? <ApiError error={verify.error} title="Could not verify" /> : null}
|
|
77
97
|
<ConfirmSpendDialog open={confirmOpen} tier="billable" title={`Verify ${email}`} description="Checks whether the address can receive mail."
|
|
78
98
|
estimatedCredits={CREDITS_PER_EMAIL_VERIFICATION} confirmLabel="Verify" confirming={verify.loading}
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Apply is opt-in per field: fields the record already has are shown unticked so a lookup never silently
|
|
15
15
|
* overwrites good data. A locked ("***") value on the record counts as empty.
|
|
16
|
+
*
|
|
17
|
+
* "Already has" is judged on the FULL record. A list row can omit fields the record has, so unless the caller says
|
|
18
|
+
* `recordLoaded` (it passed the full record from GET /contacts/{id} or /companies/{id}), the button reads the full
|
|
19
|
+
* record itself right after the lookup (GET /contacts/{id} | /companies/{id}, read). If that read fails nothing is
|
|
20
|
+
* pre-ticked and the dialog says existing values were not checked. The record id is captured when the lookup runs.
|
|
16
21
|
*/
|
|
17
22
|
|
|
18
23
|
import { useState } from "react";
|
|
@@ -20,7 +25,8 @@ import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogH
|
|
|
20
25
|
import { ApiError } from "@/g8/components/api-error";
|
|
21
26
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
22
27
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
23
|
-
import { g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
28
|
+
import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
29
|
+
import { plural } from "@/g8/lib/plural";
|
|
24
30
|
import { CREDITS_PER_FOUND_LOOKUP, isMaskedValue } from "../lib/enrichment-types";
|
|
25
31
|
|
|
26
32
|
export interface PersonLookup { email?: string; linkedin_url?: string; first_name?: string; last_name?: string; company_domain?: string }
|
|
@@ -32,6 +38,8 @@ export interface EnrichButtonProps {
|
|
|
32
38
|
lookup: PersonLookup | CompanyLookup;
|
|
33
39
|
/** The CRM record the result can be applied to (its current values decide what is pre-ticked). */
|
|
34
40
|
record?: { id: number } & Record<string, unknown>;
|
|
41
|
+
/** True when `record` is the full record (not a list row). Default false: the button then reads the full record itself before pre-ticking. */
|
|
42
|
+
recordLoaded?: boolean;
|
|
35
43
|
label?: string;
|
|
36
44
|
variant?: "default" | "outline" | "secondary" | "ghost";
|
|
37
45
|
size?: "default" | "sm";
|
|
@@ -60,25 +68,39 @@ function hasIdentity(kind: EnrichButtonProps["kind"], l: PersonLookup & CompanyL
|
|
|
60
68
|
return kind === "person" ? Boolean(l.email || l.linkedin_url || (l.first_name && l.last_name && l.company_domain)) : Boolean(l.domain || l.name);
|
|
61
69
|
}
|
|
62
70
|
|
|
63
|
-
export function EnrichButton({ kind, lookup, record, label, variant = "outline", size = "sm", onResult, onApplied, className }: EnrichButtonProps) {
|
|
71
|
+
export function EnrichButton({ kind, lookup, record, recordLoaded = false, label, variant = "outline", size = "sm", onResult, onApplied, className }: EnrichButtonProps) {
|
|
64
72
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
65
73
|
const [result, setResult] = useState<LookupResult | null>(null);
|
|
66
74
|
const [ticked, setTicked] = useState<Set<string>>(new Set());
|
|
67
75
|
const [applied, setApplied] = useState(false);
|
|
76
|
+
/** What the record held when the lookup ran: its id, its full values, and whether those values could be read. */
|
|
77
|
+
const [target, setTarget] = useState<{ id: number; existing: Record<string, unknown>; checked: boolean } | null>(null);
|
|
68
78
|
const clean = Object.fromEntries(Object.entries(lookup).filter(([, v]) => typeof v === "string" && v.trim() && !isMaskedValue(v)));
|
|
69
79
|
const ready = hasIdentity(kind, clean);
|
|
80
|
+
const path = (id: number): string => `/${kind === "person" ? "contacts" : "companies"}/${id}`;
|
|
70
81
|
|
|
71
|
-
const run = useG8Mutation(() =>
|
|
72
|
-
|
|
82
|
+
const run = useG8Mutation(async () => {
|
|
83
|
+
const res = await g8Post<LookupResult>(`/enrichment/lookup/${kind}`, clean);
|
|
84
|
+
if (!record) return { res, target: null };
|
|
85
|
+
if (recordLoaded) return { res, target: { id: record.id, existing: record, checked: true } };
|
|
86
|
+
try {
|
|
87
|
+
const full = await g8Get<Record<string, unknown>>(path(record.id));
|
|
88
|
+
return { res, target: { id: record.id, existing: full.data ?? {}, checked: true } };
|
|
89
|
+
} catch {
|
|
90
|
+
return { res, target: { id: record.id, existing: {}, checked: false } };
|
|
91
|
+
}
|
|
92
|
+
}, {
|
|
93
|
+
onSuccess: ({ res, target: t }) => {
|
|
73
94
|
setConfirmOpen(false);
|
|
74
95
|
setResult(res.data);
|
|
75
96
|
setApplied(false);
|
|
97
|
+
setTarget(t);
|
|
76
98
|
const fields = res.data.data ? normalise(kind, res.data.data) : {};
|
|
77
|
-
setTicked(new Set(Object.keys(fields).filter((k) => !
|
|
99
|
+
setTicked(new Set(t?.checked ? Object.keys(fields).filter((k) => !t.existing[k] || isMaskedValue(t.existing[k])) : []));
|
|
78
100
|
onResult?.(res.data);
|
|
79
101
|
},
|
|
80
102
|
});
|
|
81
|
-
const apply = useG8Mutation((values: Record<string, string>) => g8Patch(
|
|
103
|
+
const apply = useG8Mutation((values: Record<string, string>) => g8Patch(path(target!.id), values), {
|
|
82
104
|
onSuccess: (_r, values) => { setApplied(true); onApplied?.(values); },
|
|
83
105
|
});
|
|
84
106
|
|
|
@@ -114,19 +136,20 @@ export function EnrichButton({ kind, lookup, record, label, variant = "outline",
|
|
|
114
136
|
<ul className="flex flex-col gap-1 text-sm" aria-label="Found fields">
|
|
115
137
|
{Object.entries(fields).map(([k, v]) => (
|
|
116
138
|
<li key={k} className="flex items-center gap-2">
|
|
117
|
-
{
|
|
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}
|
|
118
140
|
<span className="w-36 text-muted-foreground">{k.replace(/_/g, " ")}</span><span className="truncate">{v}</span>
|
|
119
|
-
{
|
|
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}
|
|
120
142
|
</li>
|
|
121
143
|
))}
|
|
122
144
|
{Object.keys(fields).length === 0 ? <li className="text-muted-foreground">The match had no fields to show.</li> : null}
|
|
123
145
|
</ul>
|
|
124
146
|
) : null}
|
|
147
|
+
{target && !target.checked && result?.found ? <p role="status" className="text-sm text-muted-foreground">Existing values were not checked, so nothing is ticked. Tick only what you want to write.</p> : null}
|
|
125
148
|
{apply.error ? <ApiError error={apply.error} title="Could not update the record" /> : null}
|
|
126
149
|
{applied ? <p role="status" className="text-sm">Updated the record.</p> : null}
|
|
127
150
|
<DialogFooter>
|
|
128
151
|
<Button variant="ghost" onClick={() => setResult(null)}>Close</Button>
|
|
129
|
-
{result?.found &&
|
|
152
|
+
{result?.found && target && !applied ? <Button loading={apply.loading} disabled={Object.keys(chosen).length === 0} onClick={() => void apply.mutate(chosen)}>Apply {plural(Object.keys(chosen).length, "field")}</Button> : null}
|
|
130
153
|
</DialogFooter>
|
|
131
154
|
</DialogContent>
|
|
132
155
|
</Dialog>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* EnrichmentJobDetail: one bulk enrichment job in full. Live status (polled with backoff, there are no streams),
|
|
5
|
+
* progress, how many records were enriched or failed, credits used, warnings, the provider errors that explain a
|
|
6
|
+
* failure, and Cancel while it runs.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. getEnrichmentJob GET /enrichment/jobs/{job_id} (read) polled with useJobPoll until a final status;
|
|
10
|
+
* read once more with ?include_provider_errors=true when nothing was enriched or some records failed
|
|
11
|
+
* -> { status, total, completed, successful_enrichments, failed_enrichments, total_credits_used, warnings, provider_errors }
|
|
12
|
+
* 2. cancelEnrichmentJob POST /enrichment/jobs/{job_id}/cancel (write, free; records already done keep their data and charge)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Button } from "@graph8/sdk/ui";
|
|
16
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
17
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
18
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
19
|
+
import { StatusPill, humanizeStatus, toneForStatus } from "@/g8/components/screen/status-pill";
|
|
20
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
21
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
22
|
+
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
23
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
24
|
+
import { isJobTerminal } from "../lib/enrichment-types";
|
|
25
|
+
import type { EnrichJob } from "../lib/enrichment-types";
|
|
26
|
+
|
|
27
|
+
export interface EnrichmentJobDetailProps {
|
|
28
|
+
jobId: string;
|
|
29
|
+
/** Called when the job reached a final state. */
|
|
30
|
+
onDone?: (job: EnrichJob) => void;
|
|
31
|
+
className?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function EnrichmentJobDetail({ jobId, onDone, className }: EnrichmentJobDetailProps) {
|
|
35
|
+
const job = useJobPoll<EnrichJob>(
|
|
36
|
+
async (signal) => (await g8Get<EnrichJob>(`/enrichment/jobs/${jobId}`, undefined, { signal })).data,
|
|
37
|
+
{ intervalMs: 2000, isTerminal: (j) => isJobTerminal(j.status), onDone },
|
|
38
|
+
);
|
|
39
|
+
const cancel = useG8Mutation(() => g8Post(`/enrichment/jobs/${jobId}/cancel`, {}));
|
|
40
|
+
const j = job.status;
|
|
41
|
+
const problems = job.done && j != null && ((j.successful_enrichments ?? 0) === 0 || (j.failed_enrichments ?? 0) > 0);
|
|
42
|
+
const errors = useG8Query(
|
|
43
|
+
async (signal) => (await g8Get<EnrichJob>(`/enrichment/jobs/${jobId}`, { include_provider_errors: true }, { signal })).data.provider_errors ?? [],
|
|
44
|
+
[jobId, job.done],
|
|
45
|
+
{ enabled: problems },
|
|
46
|
+
);
|
|
47
|
+
const total = j?.total ?? 0;
|
|
48
|
+
const pct = total ? Math.min(100, Math.round(((j?.completed ?? 0) / total) * 100)) : 0;
|
|
49
|
+
|
|
50
|
+
if (job.error && !j) return <ApiError error={job.error} title="Could not read the job" onRetry={() => job.start()} />;
|
|
51
|
+
return (
|
|
52
|
+
<div className={`flex flex-col gap-4 ${className ?? ""}`} role="status">
|
|
53
|
+
<div className="flex items-center gap-2">
|
|
54
|
+
{j ? <StatusPill tone={toneForStatus(j.status)} dot>{humanizeStatus(j.status)}</StatusPill> : <span className="text-sm text-muted-foreground">Loading...</span>}
|
|
55
|
+
{!job.done && j ? <Button size="sm" variant="outline" loading={cancel.loading} onClick={() => void cancel.mutate(undefined)}>Cancel job</Button> : null}
|
|
56
|
+
</div>
|
|
57
|
+
<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>
|
|
58
|
+
{cancel.error ? <ApiError error={cancel.error} title="Could not cancel" /> : null}
|
|
59
|
+
<SectionCard title="Result">
|
|
60
|
+
<KeyValueList columns={2} items={[
|
|
61
|
+
{ label: "Job id", value: jobId, mono: true, wide: true },
|
|
62
|
+
{ label: "Processed", value: j ? `${(j.completed ?? 0).toLocaleString()} of ${total.toLocaleString()}` : "" },
|
|
63
|
+
{ label: "Enriched", value: j?.successful_enrichments != null ? j.successful_enrichments.toLocaleString() : "" },
|
|
64
|
+
{ label: "Failed", value: j?.failed_enrichments != null ? j.failed_enrichments.toLocaleString() : "" },
|
|
65
|
+
{ label: "Credits used", value: j?.total_credits_used != null ? j.total_credits_used.toLocaleString() : "" },
|
|
66
|
+
]} />
|
|
67
|
+
</SectionCard>
|
|
68
|
+
{j?.warnings?.length ? <SectionCard title="Warnings"><ul className="list-disc pl-5 text-sm">{j.warnings.map((w) => <li key={w}>{w}</li>)}</ul></SectionCard> : null}
|
|
69
|
+
{errors.data?.length ? (
|
|
70
|
+
<SectionCard title="Provider errors" description="Usually a missing or invalid provider key.">
|
|
71
|
+
<ul className="list-disc pl-5 text-sm text-destructive" aria-label="Provider errors">
|
|
72
|
+
{errors.data.slice(0, 20).map((e, i) => <li key={i}>{e.provider}: {e.error}</li>)}
|
|
73
|
+
</ul>
|
|
74
|
+
</SectionCard>
|
|
75
|
+
) : null}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|