@graph8/sdk 0.362.0 → 0.373.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -10
- package/blocks/_foundation/README.md +78 -1
- package/blocks/_foundation/block.json +123 -6
- package/blocks/_foundation/components/api-error.tsx +9 -7
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/confirmed-action.tsx +90 -0
- package/blocks/_foundation/components/connect-graph8.tsx +280 -0
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +2 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-brand.ts +52 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/connect-route.ts +15 -0
- package/blocks/_foundation/server/connect.ts +127 -0
- package/blocks/_foundation/server/local-key.ts +70 -0
- package/blocks/_foundation/server/proxy.ts +103 -3
- package/blocks/_foundation/server/route.ts +16 -4
- package/blocks/agents/README.md +93 -2
- package/blocks/agents/block.json +343 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +29 -10
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +119 -0
- package/blocks/agents/components/agent-runs.tsx +19 -10
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +226 -0
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +222 -0
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +38 -11
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +33 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +4 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +38 -18
- package/blocks/appointments/components/meetings-screen.tsx +7 -4
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/appointments/components/scheduling-screen.tsx +2 -1
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +17 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +17 -6
- package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +163 -0
- package/blocks/campaigns/components/icp-editor.tsx +22 -17
- package/blocks/campaigns/components/persona-detail.tsx +181 -0
- package/blocks/campaigns/components/persona-editor.tsx +19 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
- package/blocks/coach/README.md +10 -0
- package/blocks/coach/block.json +16 -1
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/coach/components/coach-screen.tsx +222 -0
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +161 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +14 -5
- package/blocks/copilot/components/copilot-screen.tsx +148 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
- package/blocks/crm/README.md +34 -0
- package/blocks/crm/block.json +107 -2
- package/blocks/crm/components/companies-screen.tsx +359 -0
- package/blocks/crm/components/company-detail.tsx +10 -8
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +10 -7
- package/blocks/crm/components/contact-lookup-link.tsx +44 -0
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +377 -0
- package/blocks/crm/components/dedupe-review.tsx +13 -6
- package/blocks/crm/components/import-wizard.tsx +7 -9
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +6 -4
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +399 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +9 -1
- package/blocks/crm/lib/crm-types.ts +26 -2
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +24 -6
- package/blocks/deals/block.json +160 -5
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +25 -13
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +31 -10
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +66 -26
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +5 -5
- package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +45 -4
- package/blocks/dialer/block.json +84 -3
- package/blocks/dialer/components/call-contact-button.tsx +269 -0
- package/blocks/dialer/components/call-log-screen.tsx +208 -0
- package/blocks/dialer/components/call-log.tsx +23 -7
- package/blocks/dialer/components/call-queue.tsx +4 -4
- package/blocks/dialer/components/dialer-screen.tsx +361 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +61 -12
- package/blocks/dialer/components/softphone.tsx +4 -2
- package/blocks/enrichment/README.md +29 -7
- package/blocks/enrichment/block.json +49 -9
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
- package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
- package/blocks/enrichment/components/enrich-button.tsx +32 -9
- package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
- package/blocks/enrichment/components/waterfall-config.tsx +66 -17
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +8 -0
- package/blocks/forms/block.json +139 -18
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +4 -2
- package/blocks/forms/components/form-embed.tsx +4 -2
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +56 -19
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +17 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +32 -3
- package/blocks/inbox/block.json +29 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
- package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
- package/blocks/inbox/lib/inbox-types.ts +47 -1
- package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
- package/blocks/integrations/README.md +63 -1
- package/blocks/integrations/block.json +234 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +119 -0
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +13 -8
- package/blocks/integrations/components/integrations-screen.tsx +152 -0
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +11 -9
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +179 -0
- package/blocks/integrations/components/webhook-manager.tsx +43 -25
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +11 -1
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +13 -10
- package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/block.json +13 -8
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +20 -1
- package/blocks/lists/block.json +69 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +20 -9
- package/blocks/lists/components/list-picker.tsx +17 -3
- package/blocks/lists/components/list-table.tsx +12 -6
- package/blocks/lists/components/lists-screen.tsx +346 -0
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +33 -8
- package/blocks/lists/components/suppressions-screen.tsx +264 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +69 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
- package/blocks/mailboxes/components/domain-detail.tsx +148 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
- package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
- package/blocks/mailboxes/components/warmup-health.tsx +24 -22
- package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +157 -24
- package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/block.json +28 -14
- package/blocks/nurture/components/nurture-builder.tsx +13 -3
- package/blocks/nurture/components/nurture-enroll.tsx +12 -10
- package/blocks/nurture/components/nurture-form.tsx +27 -10
- package/blocks/nurture/components/nurture-screen.tsx +26 -11
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +18 -1
- package/blocks/objects/block.json +42 -21
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +10 -2
- package/blocks/objects/components/object-schema-editor.tsx +4 -2
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +140 -23
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +9 -4
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-list.tsx +2 -1
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/quotes/README.md +13 -0
- package/blocks/quotes/block.json +65 -13
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +5 -3
- package/blocks/quotes/components/quote-related.tsx +8 -4
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-report.tsx +4 -2
- package/blocks/radar/components/radar-screen.tsx +214 -0
- package/blocks/radar/components/report-detail.tsx +104 -0
- package/blocks/search/README.md +10 -0
- package/blocks/search/block.json +16 -1
- package/blocks/search/components/filter-builder.tsx +3 -3
- package/blocks/search/components/prospect-results.tsx +5 -3
- package/blocks/search/components/prospect-search-screen.tsx +235 -0
- package/blocks/search/components/saved-searches.tsx +4 -1
- package/blocks/search/hooks/use-filter-options.ts +20 -6
- package/blocks/search/lib/search-types.ts +2 -0
- package/blocks/sequencer/README.md +55 -9
- package/blocks/sequencer/block.json +126 -19
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +224 -60
- package/blocks/sequencer/components/sequence-detail.tsx +261 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
- package/blocks/sequencer/components/sequence-list.tsx +34 -9
- package/blocks/sequencer/components/sequence-stats.tsx +11 -10
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +162 -0
- package/blocks/sequencer/components/step-editor.tsx +14 -12
- package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
- package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
- package/blocks/social-ads/README.md +19 -0
- package/blocks/social-ads/block.json +233 -1
- package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
- package/blocks/social-ads/components/ads-screen.tsx +152 -0
- package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
- package/blocks/social-ads/components/audience-sync.tsx +17 -9
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +138 -0
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +155 -0
- package/blocks/social-ads/components/creative-gallery.tsx +35 -7
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +7 -3
- package/blocks/social-ads/components/post-detail.tsx +163 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +187 -0
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +21 -10
- package/blocks/visitors/components/traffic-chart.tsx +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +26 -16
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +16 -7
- package/blocks/visitors/components/visitors-screen.tsx +166 -0
- package/blocks/webchat/README.md +10 -0
- package/blocks/webchat/block.json +21 -7
- package/blocks/webchat/components/chat-widget-config.tsx +8 -5
- package/blocks/webchat/components/conversation-list.tsx +8 -4
- package/blocks/webchat/components/webchat-screen.tsx +245 -0
- package/blocks/workbench/README.md +11 -0
- package/blocks/workbench/block.json +48 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +10 -4
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +202 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workflows/README.md +13 -0
- package/blocks/workflows/block.json +28 -9
- package/blocks/workflows/components/run-history.tsx +31 -9
- package/blocks/workflows/components/trigger-config.tsx +3 -1
- package/blocks/workflows/components/workflow-editor.tsx +4 -2
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +3109 -1977
- package/dist/errors.d.mts +34 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +21 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.browser.js +11 -5
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +11 -5
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +164 -25
- package/dist/index.d.ts +164 -25
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +10 -4
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +10 -4
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +142 -5
- package/dist/react.d.ts +142 -5
- package/dist/react.js +10 -4
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +10 -4
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/add.d.ts +11 -4
- package/dist/types/blocks/brand.d.ts +122 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +27 -20
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/blocks/paths.d.ts +4 -0
- package/dist/types/blocks/prefixes.d.ts +37 -0
- package/dist/types/blocks/registry.d.ts +17 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +5 -0
- package/dist/types/errors.d.ts +33 -0
- package/dist/types/http.d.ts +2 -18
- package/dist/types/version.d.ts +2 -2
- package/docs/agents.md +15 -4
- package/docs/api/README.md +2 -2
- package/docs/api/by-path.md +1 -0
- package/docs/api/desk.d.ts +98 -2
- package/docs/api/desk.md +123 -1
- package/docs/api/inbox.d.ts +10 -2
- package/docs/api/inbox.md +10 -2
- package/docs/api/seo.d.ts +19 -1
- package/docs/api/seo.md +24 -1
- package/package.json +16 -2
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +55 -17
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +147 -37
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +64 -0
- package/stacks/clay/pages/companies.tsx.tmpl +31 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +73 -27
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +98 -35
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +104 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +181 -64
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +52 -20
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +68 -34
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +106 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +117 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +85 -31
- package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +115 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +40 -9
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/app/layout.tsx.tmpl +7 -2
- package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* EnrichmentScreen: the finished enrichment page, in four tabs (`?tab=`).
|
|
5
|
+
* - Enrich a list: pick a list, tick contacts, run a confirmed bulk enrichment (preflight, price, progress, cancel);
|
|
6
|
+
* open a contact to see its record and look it up or verify its email.
|
|
7
|
+
* - Look up: find one person or company by email, LinkedIn, name and domain (2 credits only when found) and verify
|
|
8
|
+
* an email address (1 credit). Nothing is written to a record without a tick.
|
|
9
|
+
* - Pipelines: the saved provider waterfalls of a list, create and delete them.
|
|
10
|
+
* - Providers: which providers graph8 funds, which need your own key, and what each action costs.
|
|
11
|
+
*
|
|
12
|
+
* Composes: ScreenLayout, StatStrip, SplitView, DetailPanel, SectionCard, KeyValueList, StatusPill (screen kit),
|
|
13
|
+
* BulkEnrichPanel, WaterfallConfig, EnrichButton, EmailVerifyBadge, ContactTable / ContactDetail (crm), ListPicker (lists).
|
|
14
|
+
* Provider keys are added in graph8 settings (no API), so the Providers tab links out for them.
|
|
15
|
+
*
|
|
16
|
+
* SDK operations (inside the parts it composes): listEnrichmentProviders, preflightEnrichment (read),
|
|
17
|
+
* createDefaultWaterfallConfig + runWaterfallPipeline (billable, ConfirmSpendDialog), getEnrichmentJob, cancelEnrichmentJob, lookupPerson / lookupCompany
|
|
18
|
+
* (billable, confirmed), verifyEmail (billable, confirmed), listWaterfallConfigs, createDefaultWaterfallConfig (billable,
|
|
19
|
+
* confirmed), deleteWaterfallConfig (destructive, confirmed), listContacts, getContact.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { useEffect, useState } from "react";
|
|
23
|
+
import type { ReactNode } from "react";
|
|
24
|
+
import { Building2, ExternalLink, KeyRound, Sparkles, UserSearch } from "lucide-react";
|
|
25
|
+
import { Button, Input, Label } from "@graph8/sdk/ui";
|
|
26
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
27
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
28
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
29
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
30
|
+
import { SplitView } from "@/g8/components/screen/split-view";
|
|
31
|
+
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
32
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
33
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
34
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
35
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
36
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
37
|
+
import { CompanyPicker } from "@/g8/pickers/components/company-picker";
|
|
38
|
+
import type { CompanyOption } from "@/g8/pickers/components/company-picker";
|
|
39
|
+
import { ContactPicker } from "@/g8/pickers/components/contact-picker";
|
|
40
|
+
import type { ContactOption } from "@/g8/pickers/components/contact-picker";
|
|
41
|
+
import { ContactDetail } from "@/g8/crm/components/contact-detail";
|
|
42
|
+
import { ContactTable } from "@/g8/crm/components/contact-table";
|
|
43
|
+
import type { ContactRecord } from "@/g8/crm/lib/crm-types";
|
|
44
|
+
import { ListPicker } from "@/g8/lists/components/list-picker";
|
|
45
|
+
import { plural } from "@/g8/lib/plural";
|
|
46
|
+
import { useProviders } from "../hooks/use-providers";
|
|
47
|
+
import { CREDITS_PER_EMAIL_VERIFICATION, CREDITS_PER_FOUND_LOOKUP } from "../lib/enrichment-types";
|
|
48
|
+
import { BulkEnrichPanel } from "./bulk-enrich-panel";
|
|
49
|
+
import { EmailVerifyBadge } from "./email-verify-badge";
|
|
50
|
+
import { EnrichButton } from "./enrich-button";
|
|
51
|
+
import { EnrichmentJobDetail } from "./enrichment-job-detail";
|
|
52
|
+
import { WaterfallConfig } from "./waterfall-config";
|
|
53
|
+
import type { WaterfallConfigSummary } from "../lib/enrichment-types";
|
|
54
|
+
import { useBrand, brandUrl } from "@/g8/hooks/use-brand";
|
|
55
|
+
|
|
56
|
+
export interface EnrichmentScreenProps {
|
|
57
|
+
toolbarActions?: ReactNode;
|
|
58
|
+
/** Extra buttons in the opened contact's header. */
|
|
59
|
+
renderDetailActions?: (contact: ContactRecord) => ReactNode;
|
|
60
|
+
extraDetailTabs?: { id: string; label: string; render: (contact: ContactRecord) => ReactNode }[];
|
|
61
|
+
/** Where provider keys are managed. Default: the graph8 app. */
|
|
62
|
+
providerSettingsUrl?: string;
|
|
63
|
+
className?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const TABS = [{ id: "list", label: "Enrich a list" }, { id: "lookup", label: "Look up" }, { id: "pipelines", label: "Pipelines" }, { id: "jobs", label: "Jobs" }, { id: "providers", label: "Providers" }];
|
|
67
|
+
const FIELD = "flex flex-col gap-1";
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
const JOBS_KEY = "g8.enrichment.jobs";
|
|
71
|
+
function loadJobs(): string[] {
|
|
72
|
+
try { const v = JSON.parse(window.sessionStorage.getItem(JOBS_KEY) ?? "[]"); return Array.isArray(v) ? v.filter((x) => typeof x === "string") : []; } catch { return []; }
|
|
73
|
+
}
|
|
74
|
+
function rememberJobs(ids: string[]): void {
|
|
75
|
+
try { window.sessionStorage.setItem(JOBS_KEY, JSON.stringify(ids.slice(0, 30))); } catch { /* private mode: the list lives for this page only */ }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function PipelineDetail({ config, onClose, onExpand, onCollapse }: { config: WaterfallConfigSummary; onClose: () => void; onExpand?: () => void; onCollapse?: () => void }) {
|
|
79
|
+
const brand = useBrand();
|
|
80
|
+
return (
|
|
81
|
+
<DetailPanel onExpand={onExpand} onCollapse={onCollapse} title={config.name} subtitle={`Fills ${config.field_to_enrich ?? "a field"} in ${plural(config.step_count, "step")}`} onClose={onClose}
|
|
82
|
+
badges={config.is_active === false ? <StatusPill tone="neutral" dot>Inactive</StatusPill> : <StatusPill tone="positive" dot>Active</StatusPill>}>
|
|
83
|
+
<div className="flex flex-col gap-4 p-1">
|
|
84
|
+
{config.missing_credentials.length ? <p role="alert" className="text-sm text-warning-foreground">No key for {config.missing_credentials.join(", ")}: those steps are skipped when it runs. Add the keys in {brand.linkLabel} settings.</p> : null}
|
|
85
|
+
<ol className="flex flex-col gap-2" aria-label="Pipeline steps">
|
|
86
|
+
{(config.providers_detail.length ? config.providers_detail : config.providers.map((p) => ({ provider: p, is_system_provider: false, credential_source: "system" as const, credential_ready: true }))).map((p, i) => (
|
|
87
|
+
<li key={`${p.provider}-${i}`} className="flex items-center gap-2 text-sm">
|
|
88
|
+
<span className="w-6 text-muted-foreground">{i + 1}.</span><span className="w-32 font-medium">{p.provider}</span>
|
|
89
|
+
<StatusPill tone={p.credential_ready ? "positive" : "warning"} dot>{p.credential_source === "byok" ? "Your key" : `${brand.linkLabel} credits`}{p.credential_ready ? "" : ", not ready"}</StatusPill>
|
|
90
|
+
</li>
|
|
91
|
+
))}
|
|
92
|
+
</ol>
|
|
93
|
+
<KeyValueList columns={2} items={[
|
|
94
|
+
{ label: "Scope", value: config.is_global ? "All lists" : config.list_id != null ? `List #${config.list_id}` : "" },
|
|
95
|
+
{ label: "Pipeline id", value: config.column_id, mono: true, wide: true },
|
|
96
|
+
]} />
|
|
97
|
+
<p className="text-xs text-muted-foreground">Delete it from the list on the left. Values it already wrote stay on the contacts.</p>
|
|
98
|
+
</div>
|
|
99
|
+
</DetailPanel>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface ActiveJob { job_id: string; status?: string | null; column_id?: string | null; enrichment_type?: string | null; completed?: number | null; total?: number | null; failed?: number | null; paused_at?: string | null }
|
|
104
|
+
|
|
105
|
+
/** Jobs running, paused or pending in the last six hours, org-wide (GET /enrichment/active-jobs, read, free): no job id to remember. */
|
|
106
|
+
function ActiveJobs({ jobParam, onOpen }: { jobParam: string; onOpen: (id: string) => void }) {
|
|
107
|
+
const q = useG8Query(async (signal) => {
|
|
108
|
+
const d = (await g8Get("/enrichment/active-jobs", undefined, { signal })).data as { jobs?: ActiveJob[] } | ActiveJob[] | undefined;
|
|
109
|
+
return (Array.isArray(d) ? d : (d?.jobs ?? [])).filter((j) => j?.job_id);
|
|
110
|
+
}, []);
|
|
111
|
+
const rows = q.data ?? [];
|
|
112
|
+
return (
|
|
113
|
+
<SectionCard title="Running now" description="Jobs from the last 6 hours, from anyone on your team." flush actions={<Button size="sm" variant="ghost" onClick={() => void q.refetch()}>Refresh</Button>}>
|
|
114
|
+
{q.error ? <div className="p-3"><ApiError error={q.error} title="Could not load running jobs" onRetry={() => void q.refetch()} /></div> : null}
|
|
115
|
+
{!q.error && q.loading && rows.length === 0 ? <p className="p-4 text-sm text-muted-foreground">Loading jobs...</p> : null}
|
|
116
|
+
{!q.error && !q.loading && rows.length === 0 ? <EmptyState title="Nothing running" description="Jobs that are running, paused or pending show here." /> : null}
|
|
117
|
+
<ul className="divide-y">{rows.map((j) => (
|
|
118
|
+
<li key={j.job_id}>
|
|
119
|
+
<button type="button" aria-label={`Open job ${j.column_id ?? j.enrichment_type ?? j.job_id}`} className={`flex w-full items-center justify-between gap-2 px-4 py-2 text-left text-sm ${j.job_id === jobParam ? "bg-muted/50" : ""}`} onClick={() => onOpen(j.job_id)}>
|
|
120
|
+
<span className="truncate">{j.column_id ?? j.enrichment_type ?? "Enrichment"}</span>
|
|
121
|
+
<span className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
122
|
+
<span className="tabular-nums">{j.completed ?? 0}/{j.total ?? 0}</span>
|
|
123
|
+
<StatusPill tone={j.status === "running" ? "positive" : "neutral"} dot>{j.status ?? "unknown"}</StatusPill>
|
|
124
|
+
</span>
|
|
125
|
+
</button>
|
|
126
|
+
</li>
|
|
127
|
+
))}</ul>
|
|
128
|
+
</SectionCard>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function JobsTab({ jobs, jobParam, setJobParam }: { jobs: string[]; jobParam: string; setJobParam: (v: string | null) => void }) {
|
|
133
|
+
const [typed, setTyped] = useState("");
|
|
134
|
+
const [view, setView] = useUrlState("view");
|
|
135
|
+
const full = view === "full";
|
|
136
|
+
const list = (
|
|
137
|
+
<div className="flex flex-col gap-3 p-1">
|
|
138
|
+
<ActiveJobs jobParam={jobParam} onOpen={setJobParam} />
|
|
139
|
+
<SectionCard title="Started in this session" flush>
|
|
140
|
+
{jobs.length === 0 ? <EmptyState title="No jobs yet" description="Enrich contacts and the job appears here, with live progress." /> : (
|
|
141
|
+
<ul className="divide-y">{jobs.map((id) => (
|
|
142
|
+
<li key={id}><button type="button" className={`w-full px-4 py-2 text-left text-sm font-mono ${id === jobParam ? "bg-muted/50" : ""}`} onClick={() => setJobParam(id)}>{id}</button></li>
|
|
143
|
+
))}</ul>
|
|
144
|
+
)}
|
|
145
|
+
</SectionCard>
|
|
146
|
+
<details className="px-1 text-sm">
|
|
147
|
+
<summary className="cursor-pointer text-muted-foreground">Open an older job by id</summary>
|
|
148
|
+
<form className="mt-2 flex gap-2" onSubmit={(e) => { e.preventDefault(); if (typed.trim()) setJobParam(typed.trim()); }}>
|
|
149
|
+
<Input aria-label="Job id" value={typed} onChange={(e) => setTyped(e.target.value)} placeholder="Job id" />
|
|
150
|
+
<Button type="submit" size="sm" variant="outline" disabled={!typed.trim()}>Open</Button>
|
|
151
|
+
</form>
|
|
152
|
+
</details>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
return (
|
|
156
|
+
<SplitView expanded={full} onExpandedChange={(e) => setView(e ? "full" : null)} list={list} detailOpen={Boolean(jobParam)} onCloseDetail={() => setJobParam(null)} detailTitle="Enrichment job"
|
|
157
|
+
detail={jobParam ? <DetailPanel key={jobParam} onExpand={full ? undefined : () => setView("full")} onCollapse={full ? () => setView(null) : undefined} title="Enrichment job" subtitle={jobParam} onClose={() => setJobParam(null)}><EnrichmentJobDetail jobId={jobParam} /></DetailPanel> : null} />
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function ListRail({ listId, onPick }: { listId: number | null; onPick: (id: number) => void }) {
|
|
162
|
+
return (
|
|
163
|
+
<SectionCard title="List" description="The contacts must be on a list.">
|
|
164
|
+
<ListPicker value={listId} allowCreate={false} onChange={(l) => onPick(l.id)} />
|
|
165
|
+
</SectionCard>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function LookupTab() {
|
|
170
|
+
const [email, setEmail] = useState("");
|
|
171
|
+
const [linkedin, setLinkedin] = useState("");
|
|
172
|
+
const [first, setFirst] = useState("");
|
|
173
|
+
const [last, setLast] = useState("");
|
|
174
|
+
const [domain, setDomain] = useState("");
|
|
175
|
+
const [cDomain, setCDomain] = useState("");
|
|
176
|
+
const [cName, setCName] = useState("");
|
|
177
|
+
const [verifyEmail, setVerifyEmail] = useState("");
|
|
178
|
+
const [contactPick, setContactPick] = useState<ContactOption | null>(null);
|
|
179
|
+
const [companyPick, setCompanyPick] = useState<CompanyOption | null>(null);
|
|
180
|
+
const [applied, setApplied] = useState<string | null>(null);
|
|
181
|
+
const person = { email: email.trim() || undefined, linkedin_url: linkedin.trim() || undefined, first_name: first.trim() || undefined, last_name: last.trim() || undefined, company_domain: domain.trim() || undefined };
|
|
182
|
+
const company = { domain: cDomain.trim() || undefined, name: cName.trim() || undefined };
|
|
183
|
+
return (
|
|
184
|
+
<div className="grid gap-4 lg:grid-cols-2">
|
|
185
|
+
<SectionCard title="Find a person" description={`${CREDITS_PER_FOUND_LOOKUP} credits, only when a match is found.`} actions={<UserSearch className="size-4 text-muted-foreground" aria-hidden />}>
|
|
186
|
+
<div className="flex flex-col gap-3">
|
|
187
|
+
<div className={FIELD}><Label htmlFor="lk-email">Work email</Label><Input id="lk-email" value={email} onChange={(e) => setEmail(e.target.value)} /></div>
|
|
188
|
+
<div className={FIELD}><Label htmlFor="lk-li">LinkedIn URL</Label><Input id="lk-li" value={linkedin} onChange={(e) => setLinkedin(e.target.value)} /></div>
|
|
189
|
+
<div className="grid grid-cols-2 gap-2">
|
|
190
|
+
<div className={FIELD}><Label htmlFor="lk-first">First name</Label><Input id="lk-first" value={first} onChange={(e) => setFirst(e.target.value)} /></div>
|
|
191
|
+
<div className={FIELD}><Label htmlFor="lk-last">Last name</Label><Input id="lk-last" value={last} onChange={(e) => setLast(e.target.value)} /></div>
|
|
192
|
+
</div>
|
|
193
|
+
<div className={FIELD}><Label htmlFor="lk-domain">Company domain</Label><Input id="lk-domain" placeholder="acme.com" value={domain} onChange={(e) => setDomain(e.target.value)} /></div>
|
|
194
|
+
<p className="text-xs text-muted-foreground">Use an email, a LinkedIn URL, or first name, last name and company domain together.</p>
|
|
195
|
+
<div className={FIELD}>
|
|
196
|
+
<Label htmlFor="lk-rec">Save the result on a contact (optional)</Label>
|
|
197
|
+
<div className="flex items-center gap-2">
|
|
198
|
+
<ContactPicker id="lk-rec" label="Contact to update" placeholder="Choose a contact" value={contactPick} onChange={setContactPick} />
|
|
199
|
+
{contactPick ? <Button size="sm" variant="ghost" onClick={() => setContactPick(null)}>Clear</Button> : null}
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
<div className="flex items-center gap-2">
|
|
203
|
+
<EnrichButton kind="person" lookup={person} record={contactPick ? { id: contactPick.id } : undefined} label="Find person" variant="default" size="default" onApplied={() => setApplied(`Updated ${contactPick?.name ?? "the contact"}.`)} />
|
|
204
|
+
{applied ? <span role="status" className="text-sm">{applied}</span> : null}
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</SectionCard>
|
|
208
|
+
<div className="flex flex-col gap-4">
|
|
209
|
+
<SectionCard title="Find a company" description={`${CREDITS_PER_FOUND_LOOKUP} credits, only when a match is found.`} actions={<Building2 className="size-4 text-muted-foreground" aria-hidden />}>
|
|
210
|
+
<div className="flex flex-col gap-3">
|
|
211
|
+
<div className={FIELD}><Label htmlFor="lk-cdomain">Domain</Label><Input id="lk-cdomain" placeholder="acme.com" value={cDomain} onChange={(e) => setCDomain(e.target.value)} /></div>
|
|
212
|
+
<div className={FIELD}><Label htmlFor="lk-cname">Name</Label><Input id="lk-cname" value={cName} onChange={(e) => setCName(e.target.value)} /></div>
|
|
213
|
+
<div className={FIELD}>
|
|
214
|
+
<Label htmlFor="lk-crec">Save the result on a company (optional)</Label>
|
|
215
|
+
<div className="flex items-center gap-2">
|
|
216
|
+
<CompanyPicker id="lk-crec" label="Company to update" placeholder="Choose a company" value={companyPick} onChange={setCompanyPick} />
|
|
217
|
+
{companyPick ? <Button size="sm" variant="ghost" onClick={() => setCompanyPick(null)}>Clear</Button> : null}
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
<div><EnrichButton kind="company" lookup={company} record={companyPick ? { id: companyPick.id } : undefined} label="Find company" variant="default" size="default" /></div>
|
|
221
|
+
</div>
|
|
222
|
+
</SectionCard>
|
|
223
|
+
<SectionCard title="Verify an email" description={`${CREDITS_PER_EMAIL_VERIFICATION} credit per check.`}>
|
|
224
|
+
<div className="flex flex-col gap-3">
|
|
225
|
+
<div className={FIELD}><Label htmlFor="vf-email">Email address</Label><Input id="vf-email" value={verifyEmail} onChange={(e) => setVerifyEmail(e.target.value)} /></div>
|
|
226
|
+
<EmailVerifyBadge key={verifyEmail.trim()} email={verifyEmail.trim()} />
|
|
227
|
+
</div>
|
|
228
|
+
</SectionCard>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function ProvidersTab({ settingsUrl }: { settingsUrl: string }) {
|
|
235
|
+
const brand = useBrand();
|
|
236
|
+
const catalog = useProviders();
|
|
237
|
+
if (catalog.error) return <ApiError error={catalog.error} title="Could not load providers" onRetry={() => void catalog.refetch()} />;
|
|
238
|
+
if (!catalog.loading && catalog.providers.length === 0) return <EmptyState title="No providers" description="The provider catalog is empty for this workspace." />;
|
|
239
|
+
return (
|
|
240
|
+
<SectionCard title="Providers" description={`Providers on ${brand.linkLabel}'s key spend credits when they run. Providers on your own key cost no ${brand.linkLabel} credits.`}
|
|
241
|
+
actions={<a className="inline-flex items-center gap-1 text-sm underline" href={settingsUrl} target="_blank" rel="noreferrer"><KeyRound className="size-4" aria-hidden />Manage keys in {brand.linkLabel}<ExternalLink className="size-3" aria-hidden /></a>}>
|
|
242
|
+
<ul className="divide-y">
|
|
243
|
+
{catalog.providers.map((p) => (
|
|
244
|
+
<li key={p.provider} className="flex flex-wrap items-center gap-3 py-2" data-testid="provider-row">
|
|
245
|
+
<span className="w-36 text-sm font-medium">{p.provider}</span>
|
|
246
|
+
{p.byok_configured ? <StatusPill tone="positive" dot>Your key set</StatusPill> : p.system_funded ? <StatusPill tone="info" dot>{brand.linkLabel} funded</StatusPill> : <StatusPill tone="warning" dot>Needs your key</StatusPill>}
|
|
247
|
+
<span className="min-w-0 flex-1 truncate text-xs text-muted-foreground">{p.actions.join(", ")}</span>
|
|
248
|
+
<span className="text-xs tabular-nums text-muted-foreground">{Object.entries(p.credits_per_row).map(([k, v]) => `${k} ${v}`).join(" / ")}</span>
|
|
249
|
+
</li>
|
|
250
|
+
))}
|
|
251
|
+
</ul>
|
|
252
|
+
</SectionCard>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function EnrichmentScreen({ toolbarActions, renderDetailActions, extraDetailTabs, providerSettingsUrl: providerSettingsUrlProp, className }: EnrichmentScreenProps) {
|
|
257
|
+
const brand = useBrand();
|
|
258
|
+
const providerSettingsUrl = providerSettingsUrlProp ?? brandUrl(brand, "/settings");
|
|
259
|
+
const [tab, setTab] = useUrlState("tab", "list");
|
|
260
|
+
const [listParam, setListParam] = useUrlState("list");
|
|
261
|
+
const [picked, setPicked] = useState<number[]>([]);
|
|
262
|
+
// A finished run reloads the contact table (tableVersion) so the filled values show; the reload clears the ticks,
|
|
263
|
+
// so `runShown` keeps the panel and its result on screen until the person ticks contacts again.
|
|
264
|
+
const [tableVersion, setTableVersion] = useState(0);
|
|
265
|
+
const [runShown, setRunShown] = useState(false);
|
|
266
|
+
const [opened, setOpened] = useState<ContactRecord | null>(null);
|
|
267
|
+
const catalog = useProviders(tab === "providers");
|
|
268
|
+
const [jobParam, setJobParam] = useUrlState("job");
|
|
269
|
+
const [pipelineParam, setPipelineParam] = useUrlState("pipeline");
|
|
270
|
+
const [view, setView] = useUrlState("view");
|
|
271
|
+
const full = view === "full";
|
|
272
|
+
const [jobs, setJobs] = useState<string[]>([]);
|
|
273
|
+
useEffect(() => setJobs(loadJobs()), []);
|
|
274
|
+
const started = (id: string) => setJobs((cur) => { const next = [id, ...cur.filter((x) => x !== id)]; rememberJobs(next); return next; });
|
|
275
|
+
const listId = listParam ? Number(listParam) : null;
|
|
276
|
+
const pick = (id: number) => { setListParam(String(id)); setPicked([]); setRunShown(false); setOpened(null); setPipelineParam(null); };
|
|
277
|
+
|
|
278
|
+
const stats = tab === "providers" && catalog.providers.length > 0 ? [
|
|
279
|
+
{ label: "Providers", value: catalog.providers.length },
|
|
280
|
+
{ label: `${brand.linkLabel} funded`, value: catalog.providers.filter((p) => p.system_funded).length },
|
|
281
|
+
{ label: "Your keys set", value: catalog.providers.filter((p) => p.byok_configured).length },
|
|
282
|
+
] : undefined;
|
|
283
|
+
|
|
284
|
+
let body: ReactNode;
|
|
285
|
+
// WaterfallConfig owns the pipelines request and hands the result up, so the detail view needs no second fetch.
|
|
286
|
+
const [loaded, setLoaded] = useState<{ listId: number | null; configs: WaterfallConfigSummary[] }>({ listId: null, configs: [] });
|
|
287
|
+
const openPipeline = (loaded.listId === listId ? loaded.configs : []).find((c) => c.column_id === pipelineParam);
|
|
288
|
+
if (tab === "lookup") body = <LookupTab />;
|
|
289
|
+
else if (tab === "jobs") body = <JobsTab jobs={jobs} jobParam={jobParam} setJobParam={setJobParam} />;
|
|
290
|
+
else if (tab === "providers") body = <ProvidersTab settingsUrl={providerSettingsUrl} />;
|
|
291
|
+
else {
|
|
292
|
+
body = (
|
|
293
|
+
<div className="grid gap-4 lg:grid-cols-[18rem_minmax(0,1fr)]">
|
|
294
|
+
<ListRail listId={listId} onPick={pick} />
|
|
295
|
+
{listId == null ? (
|
|
296
|
+
<EmptyState icon={<Sparkles className="size-6" aria-hidden />} title="Choose a list" description={tab === "pipelines" ? "Pipelines belong to a list. Pick one to see and create them." : "Pick a list, tick the contacts to fill in, and enrich them."} />
|
|
297
|
+
) : tab === "pipelines" ? (
|
|
298
|
+
<SplitView
|
|
299
|
+
list={<WaterfallConfig key={listId} listId={listId} selectedId={pipelineParam || null} onOpen={(c) => setPipelineParam(c.column_id)} onLoaded={(configs) => setLoaded({ listId, configs })} />}
|
|
300
|
+
expanded={full} onExpandedChange={(e) => setView(e ? "full" : null)} detailOpen={openPipeline != null} onCloseDetail={() => setPipelineParam(null)} detailTitle="Pipeline"
|
|
301
|
+
detail={openPipeline ? <PipelineDetail config={openPipeline} onClose={() => setPipelineParam(null)} onExpand={full ? undefined : () => setView("full")} onCollapse={full ? () => setView(null) : undefined} /> : null} />
|
|
302
|
+
) : (
|
|
303
|
+
<div className="grid min-w-0 gap-4 xl:grid-cols-2">
|
|
304
|
+
<ContactTable key={listId} listId={listId} selectable pageSize={10} refreshKey={tableVersion} onSelectionChange={(ids) => setPicked(ids)} onOpen={setOpened} />
|
|
305
|
+
<div className="flex min-w-0 flex-col gap-4">
|
|
306
|
+
<SectionCard title={`Enrich ${picked.length} selected`} description="Fill work email or phones with a provider waterfall. You confirm the price first.">
|
|
307
|
+
{picked.length === 0 && !runShown ? <p className="text-sm text-muted-foreground">Tick contacts on the left.</p> : <BulkEnrichPanel key={listId} listId={listId} contactIds={picked} onStarted={(id) => { setRunShown(true); started(id); }} onDone={() => setTableVersion((v) => v + 1)} />}
|
|
308
|
+
</SectionCard>
|
|
309
|
+
{opened ? (
|
|
310
|
+
<DetailPanel key={opened.id} title={opened.full_name ?? ([opened.first_name, opened.last_name].filter(Boolean).join(" ") || `Contact #${opened.id}`)} subtitle={opened.job_title} onClose={() => setOpened(null)}>
|
|
311
|
+
<ContactDetail contactId={opened.id} extraTabs={extraDetailTabs}
|
|
312
|
+
renderActions={(c) => (<>
|
|
313
|
+
<EnrichButton kind="person" record={c as ContactRecord & Record<string, unknown>} lookup={{ email: c.work_email ?? undefined, linkedin_url: c.linkedin_url ?? undefined, first_name: c.first_name ?? undefined, last_name: c.last_name ?? undefined, company_domain: c.company_domain ?? undefined }} />
|
|
314
|
+
<EmailVerifyBadge email={c.work_email} />
|
|
315
|
+
{renderDetailActions?.(c)}
|
|
316
|
+
</>)} />
|
|
317
|
+
</DetailPanel>
|
|
318
|
+
) : null}
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
322
|
+
</div>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<ScreenLayout
|
|
328
|
+
className={className}
|
|
329
|
+
title="Enrichment"
|
|
330
|
+
description="Fill in what you do not know about your contacts: emails, phones and company facts. Every paid step shows its price first."
|
|
331
|
+
icon={<Sparkles className="size-5" aria-hidden />}
|
|
332
|
+
actions={toolbarActions}
|
|
333
|
+
tabs={TABS}
|
|
334
|
+
activeTab={TABS.some((t) => t.id === tab) ? tab : "list"}
|
|
335
|
+
onTabChange={setTab}
|
|
336
|
+
stats={stats}
|
|
337
|
+
>
|
|
338
|
+
{body}
|
|
339
|
+
</ScreenLayout>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
@@ -8,14 +8,19 @@
|
|
|
8
8
|
* 1. listWaterfallConfigs GET /enrichment/waterfall/configs?list_id (read) pipelines runnable on the list
|
|
9
9
|
* 2. listEnrichmentProviders GET /enrichment/providers (read) graph8-funded vs own key, credits per action
|
|
10
10
|
* 3. createDefaultWaterfallConfig POST /enrichment/waterfall/configs { list_id, name, field, providers, skip_existing_values }
|
|
11
|
-
* (billable tier) goes through ConfirmSpendDialog first
|
|
11
|
+
* (billable tier) goes through ConfirmSpendDialog first. With `field` (always sent here) it is idempotent per field and
|
|
12
|
+
* list: when the list already has a pipeline for that field the API returns it with `created: false` and its own
|
|
13
|
+
* providers, whatever name and providers were sent. The form then says so instead of claiming a new pipeline.
|
|
12
14
|
* 4. deleteWaterfallConfig DELETE /enrichment/waterfall/configs/{column_id} (destructive, ConfirmSpendDialog)
|
|
13
15
|
*
|
|
16
|
+
* The provider choices follow the chosen field (the API refuses a provider that cannot fill it with 400
|
|
17
|
+
* `unsupported_provider_for_field`): changing the field drops selected providers that no longer fit and says which.
|
|
18
|
+
*
|
|
14
19
|
* Providers on graph8's system key spend credits when the pipeline RUNS; providers on the org's own key (BYOK) do not.
|
|
15
20
|
* A BYOK provider without a key is listed under `missing_credentials` and is skipped at run time.
|
|
16
21
|
*/
|
|
17
22
|
|
|
18
|
-
import { useState } from "react";
|
|
23
|
+
import { useEffect, useRef, useState } from "react";
|
|
19
24
|
import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
|
|
20
25
|
import { ApiError } from "@/g8/components/api-error";
|
|
21
26
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
@@ -23,36 +28,69 @@ import { EmptyState } from "@/g8/components/empty-state";
|
|
|
23
28
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
24
29
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
25
30
|
import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
31
|
+
import { plural } from "@/g8/lib/plural";
|
|
26
32
|
import { useProviders } from "../hooks/use-providers";
|
|
27
|
-
import { TARGET_FIELDS } from "../lib/enrichment-types";
|
|
33
|
+
import { FIELD_PROVIDERS, TARGET_FIELDS } from "../lib/enrichment-types";
|
|
28
34
|
import type { TargetField, WaterfallConfigSummary } from "../lib/enrichment-types";
|
|
35
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
29
36
|
|
|
30
37
|
export interface WaterfallConfigProps {
|
|
31
38
|
listId: number;
|
|
32
39
|
/** Called with the pipeline's column_id after create or delete (for example to refresh a BulkEnrichPanel). */
|
|
33
40
|
onChanged?: () => void;
|
|
41
|
+
/** Makes each pipeline's name open a detail view in your layout. Omit it and the list is unchanged. */
|
|
42
|
+
onOpen?: (config: WaterfallConfigSummary) => void;
|
|
43
|
+
/** Highlights the open pipeline (its column_id). */
|
|
44
|
+
selectedId?: string | null;
|
|
45
|
+
/** Called with the loaded pipelines each time they load or reload, so a screen can show one in a detail view without a second request. */
|
|
46
|
+
onLoaded?: (configs: WaterfallConfigSummary[]) => void;
|
|
34
47
|
className?: string;
|
|
35
48
|
}
|
|
36
49
|
|
|
37
|
-
export function WaterfallConfig({ listId, onChanged, className }: WaterfallConfigProps) {
|
|
50
|
+
export function WaterfallConfig({ listId, onChanged, onOpen, selectedId, onLoaded, className }: WaterfallConfigProps) {
|
|
51
|
+
const brand = useBrand();
|
|
38
52
|
const [name, setName] = useState("");
|
|
39
53
|
const [field, setField] = useState<TargetField>("work_email");
|
|
40
54
|
const [order, setOrder] = useState<string[]>([]);
|
|
41
55
|
const [skipExisting, setSkipExisting] = useState(true);
|
|
42
56
|
const [confirmCreate, setConfirmCreate] = useState(false);
|
|
43
57
|
const [deleting, setDeleting] = useState<WaterfallConfigSummary | null>(null);
|
|
58
|
+
// The answer to the last create when the API reused the list's pipeline for that field instead of creating one.
|
|
59
|
+
const [reused, setReused] = useState<{ name?: string; field: TargetField; providers: string[]; asked: string[] } | null>(null);
|
|
60
|
+
// Providers dropped by the last field change, with the field they cannot fill, so the person sees why they went.
|
|
61
|
+
const [dropped, setDropped] = useState<{ providers: string[]; field: TargetField } | null>(null);
|
|
44
62
|
const catalog = useProviders();
|
|
45
63
|
|
|
46
64
|
const configs = useG8Query((signal) => g8Get<{ configs: WaterfallConfigSummary[] }>("/enrichment/waterfall/configs", { list_id: listId }, { signal }), [listId]);
|
|
47
65
|
const create = useG8Mutation(
|
|
48
|
-
() => g8Post<{ column_id: string; created: boolean }>("/enrichment/waterfall/configs", { list_id: listId, name: name.trim(), field, providers: order, skip_existing_values: skipExisting }),
|
|
49
|
-
{
|
|
66
|
+
() => g8Post<{ column_id: string; created: boolean; name?: string; providers?: string[] }>("/enrichment/waterfall/configs", { list_id: listId, name: name.trim(), field, providers: order, skip_existing_values: skipExisting }),
|
|
67
|
+
{
|
|
68
|
+
onSuccess: (res) => {
|
|
69
|
+
setReused(res.data.created === false ? { name: res.data.name, field, providers: res.data.providers ?? [], asked: order } : null);
|
|
70
|
+
setConfirmCreate(false); setName(""); setOrder([]); void configs.refetch(); onChanged?.();
|
|
71
|
+
},
|
|
72
|
+
// A failed create must not leave the previous answer's "Already exists, reused" next to the error.
|
|
73
|
+
onError: () => setReused(null),
|
|
74
|
+
},
|
|
50
75
|
);
|
|
51
76
|
const remove = useG8Mutation((columnId: string) => g8Delete(`/enrichment/waterfall/configs/${columnId}`), {
|
|
52
77
|
onSuccess: () => { setDeleting(null); void configs.refetch(); onChanged?.(); },
|
|
53
78
|
});
|
|
54
79
|
|
|
55
80
|
const items = configs.data?.data.configs ?? [];
|
|
81
|
+
const loadedRef = useRef(onLoaded);
|
|
82
|
+
loadedRef.current = onLoaded;
|
|
83
|
+
useEffect(() => { if (configs.data) loadedRef.current?.(configs.data.data.configs ?? []); }, [configs.data]);
|
|
84
|
+
const fieldLabel = TARGET_FIELDS.find((f) => f.value === field)?.label ?? field;
|
|
85
|
+
const allowed = FIELD_PROVIDERS[field];
|
|
86
|
+
const unfit = order.filter((p) => !allowed.includes(p));
|
|
87
|
+
const changeField = (next: TargetField) => {
|
|
88
|
+
const gone = order.filter((p) => !FIELD_PROVIDERS[next].includes(p));
|
|
89
|
+
setField(next);
|
|
90
|
+
setOrder(order.filter((p) => FIELD_PROVIDERS[next].includes(p)));
|
|
91
|
+
setDropped(gone.length ? { providers: gone, field: next } : null);
|
|
92
|
+
setReused(null);
|
|
93
|
+
};
|
|
56
94
|
const byName = new Map(catalog.providers.map((p) => [p.provider, p]));
|
|
57
95
|
const systemUsed = order.filter((p) => byName.get(p)?.system_funded && !byName.get(p)?.byok_configured);
|
|
58
96
|
const move = (p: string, d: number) => {
|
|
@@ -73,13 +111,13 @@ export function WaterfallConfig({ listId, onChanged, className }: WaterfallConfi
|
|
|
73
111
|
{!configs.loading && !configs.error && items.length === 0 ? <EmptyState title="No pipelines yet" description="Create one below, or let the first enrichment run create a default." /> : null}
|
|
74
112
|
<ul className="flex flex-col gap-2">
|
|
75
113
|
{items.map((c) => (
|
|
76
|
-
<li key={c.column_id} className=
|
|
114
|
+
<li key={c.column_id} className={`rounded-md border p-3 ${selectedId === c.column_id ? "bg-muted/50" : ""}`} data-testid="pipeline">
|
|
77
115
|
<div className="flex items-center justify-between gap-2">
|
|
78
|
-
<p className="text-sm font-medium">{c.name}{c.is_global ? <span className="ml-2 text-xs text-muted-foreground">all lists</span> : null}</p>
|
|
116
|
+
<p className="text-sm font-medium">{onOpen ? <button type="button" className="underline-offset-2 hover:underline" onClick={() => onOpen(c)}>{c.name}</button> : c.name}{c.is_global ? <span className="ml-2 text-xs text-muted-foreground">all lists</span> : null}</p>
|
|
79
117
|
<Button variant="ghost" size="sm" onClick={() => setDeleting(c)}>Delete</Button>
|
|
80
118
|
</div>
|
|
81
|
-
<p className="text-xs text-muted-foreground">{c.field_to_enrich ?? "field"}: {c.providers.join(" > ") || "no providers"} ({c.step_count
|
|
82
|
-
{c.missing_credentials.length ? <p role="alert" className="mt-1 text-xs text-
|
|
119
|
+
<p className="text-xs text-muted-foreground">{c.field_to_enrich ?? "field"}: {c.providers.join(" > ") || "no providers"} ({plural(c.step_count, "step")})</p>
|
|
120
|
+
{c.missing_credentials.length ? <p role="alert" className="mt-1 text-xs text-warning-foreground">Skipped at run time, no key: {c.missing_credentials.join(", ")}</p> : null}
|
|
83
121
|
</li>
|
|
84
122
|
))}
|
|
85
123
|
</ul>
|
|
@@ -90,7 +128,7 @@ export function WaterfallConfig({ listId, onChanged, className }: WaterfallConfi
|
|
|
90
128
|
<div className="flex flex-wrap items-end gap-3">
|
|
91
129
|
<div className="flex flex-col gap-1"><Label htmlFor="wf-name">Name</Label><Input id="wf-name" className="w-64" value={name} onChange={(e) => setName(e.target.value)} /></div>
|
|
92
130
|
<div className="flex flex-col gap-1"><Label htmlFor="wf-field">Fill</Label>
|
|
93
|
-
<select id="wf-field" className="h-9 rounded-md border bg-background px-2 text-sm" value={field} onChange={(e) =>
|
|
131
|
+
<select id="wf-field" className="h-9 rounded-md border bg-background px-2 text-sm" value={field} onChange={(e) => changeField(e.target.value as TargetField)}>
|
|
94
132
|
{TARGET_FIELDS.map((f) => <option key={f.value} value={f.value}>{f.label}</option>)}
|
|
95
133
|
</select>
|
|
96
134
|
</div>
|
|
@@ -100,25 +138,36 @@ export function WaterfallConfig({ listId, onChanged, className }: WaterfallConfi
|
|
|
100
138
|
{order.map((p, i) => (
|
|
101
139
|
<li key={p} className="flex items-center gap-2 text-sm">
|
|
102
140
|
<span className="w-6 text-muted-foreground">{i + 1}.</span><span className="w-32 font-medium">{p}</span>
|
|
103
|
-
<span className="text-xs text-muted-foreground">{byName.get(p)?.byok_configured ? "your key, free" : byName.get(p)?.system_funded ?
|
|
141
|
+
<span className="text-xs text-muted-foreground">{byName.get(p)?.byok_configured ? "your key, free" : byName.get(p)?.system_funded ? `${brand.linkLabel} credits` : "needs your key"}</span>
|
|
104
142
|
<Button variant="ghost" size="sm" aria-label={`Move ${p} up`} disabled={i === 0} onClick={() => move(p, -1)}>Up</Button>
|
|
105
143
|
<Button variant="ghost" size="sm" aria-label={`Move ${p} down`} disabled={i === order.length - 1} onClick={() => move(p, 1)}>Down</Button>
|
|
106
144
|
<Button variant="ghost" size="sm" aria-label={`Remove ${p}`} onClick={() => setOrder(order.filter((x) => x !== p))}>Remove</Button>
|
|
107
145
|
</li>
|
|
108
146
|
))}
|
|
109
147
|
</ol>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
148
|
+
{dropped ? (
|
|
149
|
+
<p role="status" className="text-xs text-muted-foreground">
|
|
150
|
+
Removed {dropped.providers.join(", ")}: {dropped.providers.length === 1 ? "it cannot" : "they cannot"} find a {TARGET_FIELDS.find((f) => f.value === dropped.field)?.label.toLowerCase()}.
|
|
151
|
+
</p>
|
|
152
|
+
) : null}
|
|
153
|
+
<div role="group" className="flex flex-wrap gap-1" aria-label="Provider choices" title={`Providers that can find a ${fieldLabel.toLowerCase()}`}>
|
|
154
|
+
{catalog.providers.filter((p) => allowed.includes(p.provider) && !order.includes(p.provider)).map((p) => (
|
|
155
|
+
<Button key={p.provider} variant="outline" size="sm" onClick={() => { setDropped(null); setOrder([...order, p.provider]); }}>Add {p.provider}</Button>
|
|
113
156
|
))}
|
|
114
157
|
</div>
|
|
115
158
|
<label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={skipExisting} onChange={(e) => setSkipExisting(e.target.checked)} /> Skip contacts that already have a value</label>
|
|
116
159
|
{create.error ? <ApiError error={create.error} title="Could not create the pipeline" /> : null}
|
|
117
|
-
|
|
160
|
+
{reused ? (
|
|
161
|
+
<p role="status" className="text-sm text-muted-foreground">
|
|
162
|
+
Already exists, reused: this list already has a {TARGET_FIELDS.find((f) => f.value === reused.field)?.label.toLowerCase() ?? reused.field} pipeline{reused.name ? ` ("${reused.name}")` : ""}, so no new one was created.
|
|
163
|
+
{reused.providers.length && reused.providers.join() !== reused.asked.join() ? ` It keeps its providers: ${reused.providers.join(" > ")}. Delete it first to use ${reused.asked.join(" > ")}.` : ""}
|
|
164
|
+
</p>
|
|
165
|
+
) : null}
|
|
166
|
+
<div className="flex justify-end"><Button disabled={!name.trim() || order.length === 0 || unfit.length > 0} onClick={() => setConfirmCreate(true)}>Create pipeline</Button></div>
|
|
118
167
|
</section>
|
|
119
168
|
|
|
120
169
|
<ConfirmSpendDialog open={confirmCreate} tier="billable" title={`Create pipeline "${name.trim()}"`}
|
|
121
|
-
description={systemUsed.length ? `Running it uses
|
|
170
|
+
description={systemUsed.length ? `Running it uses ${brand.linkLabel} credits for ${systemUsed.join(", ")}; providers on your own key cost nothing. Creating the pipeline itself does not run it.` : `All providers use your own keys, so running it costs no ${brand.linkLabel} credits. Creating the pipeline does not run it.`}
|
|
122
171
|
confirmLabel="Create" confirming={create.loading} onConfirm={() => void create.mutate(undefined)} onCancel={() => setConfirmCreate(false)} />
|
|
123
172
|
<ConfirmSpendDialog open={deleting != null} tier="destructive" title={`Delete pipeline "${deleting?.name ?? ""}"`} description="Values it already wrote stay on the contacts."
|
|
124
173
|
affected={1} confirming={remove.loading} onConfirm={() => deleting && void remove.mutate(deleting.column_id)} onCancel={() => setDeleting(null)} />
|
|
@@ -22,6 +22,45 @@ export const TARGET_FIELDS = [
|
|
|
22
22
|
] as const;
|
|
23
23
|
export type TargetField = (typeof TARGET_FIELDS)[number]["value"];
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Providers the API can run for each field: the per-field waterfall templates the server builds pipelines from
|
|
27
|
+
* (a phone runs leadmagic `mobile_finder`, never an email finder). Any other provider is refused with 400
|
|
28
|
+
* `unsupported_provider_for_field`, so the UI only offers these.
|
|
29
|
+
*/
|
|
30
|
+
export const FIELD_PROVIDERS: Record<TargetField, readonly string[]> = {
|
|
31
|
+
work_email: ["apollo", "leadmagic", "prospeo", "hunter", "icypeas", "dropcontact"],
|
|
32
|
+
direct_phone: ["leadmagic", "prospeo", "apollo"],
|
|
33
|
+
mobile_phone: ["leadmagic", "prospeo", "apollo"],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Providers a run tries for a field when nobody chose: the API's own defaults. */
|
|
37
|
+
export const FIELD_DEFAULT_PROVIDERS: Record<TargetField, readonly string[]> = {
|
|
38
|
+
work_email: ["prospeo", "dropcontact"],
|
|
39
|
+
direct_phone: ["leadmagic", "prospeo", "apollo"],
|
|
40
|
+
mobile_phone: ["leadmagic", "prospeo", "apollo"],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The providers to start from for `field`: `preferred` (in its order) kept to those the field supports, else the
|
|
45
|
+
* field's API defaults when none of `preferred` fits (for example email providers asked for a phone).
|
|
46
|
+
*/
|
|
47
|
+
export function providersForField(field: TargetField, preferred?: readonly string[]): string[] {
|
|
48
|
+
const allowed = FIELD_PROVIDERS[field];
|
|
49
|
+
const kept = (preferred ?? FIELD_DEFAULT_PROVIDERS[field]).filter((p) => allowed.includes(p));
|
|
50
|
+
return kept.length ? [...new Set(kept)] : [...FIELD_DEFAULT_PROVIDERS[field]];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** What POST /enrichment/waterfall/configs answers (created, or the list's existing pipeline for that field reused). */
|
|
54
|
+
export interface WaterfallPipelineResult {
|
|
55
|
+
column_id: string;
|
|
56
|
+
config_id?: string;
|
|
57
|
+
name?: string;
|
|
58
|
+
list_id?: number | null;
|
|
59
|
+
created: boolean;
|
|
60
|
+
providers?: string[];
|
|
61
|
+
missing_credentials?: string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
25
64
|
export interface ProviderEntry {
|
|
26
65
|
provider: string;
|
|
27
66
|
system_funded: boolean;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @title Enrich many contacts of a list and wait for the job
|
|
3
3
|
* @when Filling work emails or phones for a list. Order: preflight (free: counts, missing provider keys and the
|
|
4
|
-
* credit quote) -> a human approves the quote ->
|
|
4
|
+
* credit quote) -> a human approves the quote -> create or reuse the field's pipeline and run it -> poll it (the API has no streams or
|
|
5
5
|
* webhook for completion) -> read provider errors if nothing was written. Contact ids must be on the list;
|
|
6
6
|
* ids not on it are skipped and not charged. Never enrich contacts you only have as masked search results:
|
|
7
7
|
* save them to a list first.
|
|
@@ -17,11 +17,22 @@ export async function quote(listId: number, contactIds: number[]) {
|
|
|
17
17
|
return res.data; // contact_count, not_in_list, missing_credentials, warnings, credits_per_record, max_total_credits
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Only after a person approved `max_total_credits`.
|
|
22
|
+
*
|
|
23
|
+
* Runs through a saved waterfall pipeline: create (or reuse) the list's work-email pipeline, then run it on the
|
|
24
|
+
* contacts. Creating is idempotent per (field, list): a list that already has a work-email pipeline reuses it with
|
|
25
|
+
* ITS providers (`created: false`). Keep the column_id: a retry only needs the second call.
|
|
26
|
+
*/
|
|
21
27
|
export async function enrichAndWait(listId: number, contactIds: number[]) {
|
|
22
|
-
const
|
|
23
|
-
body: { list_id: listId,
|
|
28
|
+
const pipeline = await g8.api.enrichment.createDefaultWaterfallConfig({
|
|
29
|
+
body: { list_id: listId, name: "Work email enrichment", field: "CONTACT_WORK_EMAIL", providers: ["prospeo", "dropcontact"] },
|
|
30
|
+
});
|
|
31
|
+
const started = await g8.api.enrichment.runWaterfallPipeline({
|
|
32
|
+
body: { column_id: pipeline.data.column_id, list_id: listId, record_ids: contactIds },
|
|
24
33
|
});
|
|
34
|
+
// A 202 whose status is "error" never started (newer APIs answer 502): do not poll a job that will not exist.
|
|
35
|
+
if (!started.data.job_id || started.data.status === "error") throw new Error("enrichment did not start; nothing was charged");
|
|
25
36
|
const jobId = started.data.job_id;
|
|
26
37
|
|
|
27
38
|
let delay = 2000;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @title Create a waterfall pipeline for a list and see which providers are ready
|
|
3
3
|
* @when Setting up "try provider A, then B" enrichment for a column once, then reusing it. Check the provider
|
|
4
4
|
* catalog first: graph8-funded providers spend credits when the pipeline runs, providers on your own key do not,
|
|
5
|
-
* and a provider without a key is skipped. Creating is idempotent by (list_id, name).
|
|
5
|
+
* and a provider without a key is skipped. Creating with `field` is idempotent per field and list: if the list already has a pipeline for that field it is returned with `created: false` and keeps its own providers, whatever name or providers you send (delete it first to change them). Without `field`, a pipeline is reused by (list_id, name).
|
|
6
6
|
*/
|
|
7
7
|
import { g8 } from "@graph8/sdk";
|
|
8
8
|
|
|
@@ -17,7 +17,8 @@ export async function createPipeline(listId: number) {
|
|
|
17
17
|
const res = await g8.api.enrichment.createDefaultWaterfallConfig({
|
|
18
18
|
body: { list_id: listId, name: "Work email waterfall", field: "work_email", providers: ["prospeo", "dropcontact"], skip_existing_values: true },
|
|
19
19
|
});
|
|
20
|
-
|
|
20
|
+
// created === false: the list already had a work-email pipeline; it was returned as is (check res.data.providers).
|
|
21
|
+
return { columnId: res.data.column_id, created: res.data.created, providers: res.data.providers, missing: res.data.missing_credentials };
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export async function pipelinesOf(listId: number) {
|