@graph8/sdk 0.308.1 → 0.357.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -0
- package/blocks/README.md +32 -0
- package/blocks/_foundation/README.md +78 -0
- package/blocks/_foundation/block.json +79 -0
- package/blocks/_foundation/components/api-error.tsx +62 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +142 -0
- package/blocks/_foundation/components/empty-state.tsx +24 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +88 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +87 -0
- package/blocks/_foundation/hooks/use-job-poll.ts +130 -0
- package/blocks/_foundation/hooks/use-paginated.ts +105 -0
- package/blocks/_foundation/lib/g8-client.ts +128 -0
- package/blocks/_foundation/recipes/verify-proxy-key.ts +18 -0
- package/blocks/_foundation/server/proxy.ts +233 -0
- package/blocks/_foundation/server/route.ts +30 -0
- package/blocks/agents/README.md +133 -0
- package/blocks/agents/block.json +223 -0
- package/blocks/agents/components/agent-builder.tsx +199 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +144 -0
- package/blocks/agents/components/agent-runs.tsx +175 -0
- package/blocks/agents/components/agent-skills-panel.tsx +126 -0
- package/blocks/agents/components/agent-test-console.tsx +175 -0
- package/blocks/agents/lib/agent-types.ts +114 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +50 -0
- package/blocks/agents/recipes/run-and-follow-agent-run.ts +46 -0
- package/blocks/agents/recipes/test-agent-before-activating.ts +36 -0
- package/blocks/analytics/README.md +73 -0
- package/blocks/analytics/block.json +178 -0
- package/blocks/analytics/components/funnel-chart.tsx +79 -0
- package/blocks/analytics/components/kpi-tiles.tsx +74 -0
- package/blocks/analytics/components/leaderboard.tsx +86 -0
- package/blocks/analytics/components/report-builder.tsx +200 -0
- package/blocks/analytics/hooks/use-report.ts +26 -0
- package/blocks/analytics/lib/analytics-types.ts +108 -0
- package/blocks/analytics/lib/use-debounced.ts +16 -0
- package/blocks/analytics/recipes/01-weekly-outbound-report.ts +32 -0
- package/blocks/analytics/recipes/02-revenue-snapshot.ts +22 -0
- package/blocks/analytics/recipes/03-saved-report.ts +30 -0
- package/blocks/appointments/README.md +80 -0
- package/blocks/appointments/block.json +218 -0
- package/blocks/appointments/components/availability-editor.tsx +192 -0
- package/blocks/appointments/components/booking-page.tsx +154 -0
- package/blocks/appointments/components/event-type-editor.tsx +214 -0
- package/blocks/appointments/components/meeting-detail.tsx +147 -0
- package/blocks/appointments/components/meeting-list.tsx +148 -0
- package/blocks/appointments/hooks/use-available-slots.ts +29 -0
- package/blocks/appointments/hooks/use-schedules.ts +18 -0
- package/blocks/appointments/lib/appointments-types.ts +159 -0
- package/blocks/appointments/lib/idempotency.ts +30 -0
- package/blocks/appointments/recipes/01-create-event-type-and-hours.ts +27 -0
- package/blocks/appointments/recipes/02-find-slots-and-book.ts +34 -0
- package/blocks/appointments/recipes/03-bookings-digest.ts +22 -0
- package/blocks/appointments/recipes/04-calendar-invite.ts +21 -0
- package/blocks/block.schema.json +130 -0
- package/blocks/block.types.ts +109 -0
- package/blocks/campaigns/README.md +118 -0
- package/blocks/campaigns/block.json +361 -0
- package/blocks/campaigns/components/campaign-assets.tsx +180 -0
- package/blocks/campaigns/components/campaign-audience-step.tsx +91 -0
- package/blocks/campaigns/components/campaign-launch-step.tsx +147 -0
- package/blocks/campaigns/components/campaign-list.tsx +120 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +93 -0
- package/blocks/campaigns/components/campaign-select.tsx +52 -0
- package/blocks/campaigns/components/campaign-sequence-step.tsx +158 -0
- package/blocks/campaigns/components/campaign-wizard.tsx +238 -0
- package/blocks/campaigns/components/form-field.tsx +29 -0
- package/blocks/campaigns/components/icp-editor.tsx +230 -0
- package/blocks/campaigns/components/persona-editor.tsx +210 -0
- package/blocks/campaigns/hooks/use-company-profile.ts +34 -0
- package/blocks/campaigns/lib/campaigns-utils.ts +155 -0
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +33 -0
- package/blocks/campaigns/recipes/campaign-report.ts +22 -0
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +51 -0
- package/blocks/campaigns/recipes/set-up-icp-and-personas.ts +37 -0
- package/blocks/coach/README.md +93 -0
- package/blocks/coach/block.json +125 -0
- package/blocks/coach/components/call-scorecard.tsx +126 -0
- package/blocks/coach/components/coaching-feed.tsx +140 -0
- package/blocks/coach/lib/coach-types.ts +116 -0
- package/blocks/coach/recipes/read-a-scorecard.ts +27 -0
- package/blocks/coach/recipes/weekly-coaching-digest.ts +53 -0
- package/blocks/copilot/README.md +99 -0
- package/blocks/copilot/block.json +151 -0
- package/blocks/copilot/components/copilot-memory.tsx +87 -0
- package/blocks/copilot/components/copilot-panel.tsx +189 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +111 -0
- package/blocks/copilot/lib/copilot-types.ts +111 -0
- package/blocks/copilot/recipes/ask-the-copilot.ts +35 -0
- package/blocks/copilot/recipes/post-in-a-work-channel.ts +33 -0
- package/blocks/copilot/recipes/remember-and-recall.ts +19 -0
- package/blocks/crm/README.md +122 -0
- package/blocks/crm/block.json +378 -0
- package/blocks/crm/components/activity-timeline.tsx +99 -0
- package/blocks/crm/components/company-detail.tsx +118 -0
- package/blocks/crm/components/company-table.tsx +173 -0
- package/blocks/crm/components/contact-detail.tsx +135 -0
- package/blocks/crm/components/contact-table.tsx +217 -0
- package/blocks/crm/components/dedupe-review.tsx +164 -0
- package/blocks/crm/components/import-wizard.tsx +193 -0
- package/blocks/crm/components/notes-panel.tsx +112 -0
- package/blocks/crm/components/record-form.tsx +191 -0
- package/blocks/crm/components/task-list.tsx +107 -0
- package/blocks/crm/hooks/use-custom-fields.ts +18 -0
- package/blocks/crm/hooks/use-debounced.ts +13 -0
- package/blocks/crm/hooks/use-page-query.ts +52 -0
- package/blocks/crm/lib/crm-types.ts +136 -0
- package/blocks/crm/recipes/custom-fields-and-values.ts +28 -0
- package/blocks/crm/recipes/dedupe-and-merge.ts +30 -0
- package/blocks/crm/recipes/notes-tasks-and-activity.ts +23 -0
- package/blocks/crm/recipes/upsert-contact-with-company.ts +33 -0
- package/blocks/deals/README.md +78 -0
- package/blocks/deals/block.json +198 -0
- package/blocks/deals/components/deal-detail.tsx +134 -0
- package/blocks/deals/components/forecast-view.tsx +112 -0
- package/blocks/deals/components/pipeline-board.tsx +142 -0
- package/blocks/deals/components/stage-editor.tsx +96 -0
- package/blocks/deals/hooks/use-pipeline-deals.ts +67 -0
- package/blocks/deals/hooks/use-pipelines.ts +19 -0
- package/blocks/deals/lib/deals-types.ts +128 -0
- package/blocks/deals/recipes/01-create-deal-and-advance.ts +32 -0
- package/blocks/deals/recipes/02-pipeline-report.ts +35 -0
- package/blocks/deals/recipes/03-stale-deals-sweep.ts +20 -0
- package/blocks/dialer/README.md +114 -0
- package/blocks/dialer/block.json +260 -0
- package/blocks/dialer/components/call-log.tsx +78 -0
- package/blocks/dialer/components/call-queue.tsx +188 -0
- package/blocks/dialer/components/dialer-workspace.tsx +47 -0
- package/blocks/dialer/components/disposition-panel.tsx +75 -0
- package/blocks/dialer/components/live-calls-table.tsx +107 -0
- package/blocks/dialer/components/session-start-form.tsx +111 -0
- package/blocks/dialer/components/softphone.tsx +122 -0
- package/blocks/dialer/components/transcript-viewer.tsx +82 -0
- package/blocks/dialer/hooks/use-dialer-numbers.ts +19 -0
- package/blocks/dialer/hooks/use-dialer-session.ts +68 -0
- package/blocks/dialer/hooks/use-dispositions.ts +29 -0
- package/blocks/dialer/lib/dialer-types.ts +158 -0
- package/blocks/dialer/lib/softphone-transport.ts +67 -0
- package/blocks/dialer/lib/twilio-transport.ts +74 -0
- package/blocks/dialer/recipes/01-create-session-and-dial.ts +39 -0
- package/blocks/dialer/recipes/02-disposition-calls.ts +26 -0
- package/blocks/dialer/recipes/03-review-a-call.ts +23 -0
- package/blocks/enrichment/README.md +105 -0
- package/blocks/enrichment/block.json +204 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +205 -0
- package/blocks/enrichment/components/email-verify-badge.tsx +82 -0
- package/blocks/enrichment/components/enrich-button.tsx +136 -0
- package/blocks/enrichment/components/waterfall-config.tsx +127 -0
- package/blocks/enrichment/hooks/use-providers.ts +18 -0
- package/blocks/enrichment/lib/enrichment-types.ts +104 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +41 -0
- package/blocks/enrichment/recipes/lookup-and-verify.ts +25 -0
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +26 -0
- package/blocks/forms/README.md +112 -0
- package/blocks/forms/block.json +200 -0
- package/blocks/forms/components/form-builder.tsx +150 -0
- package/blocks/forms/components/form-embed.tsx +103 -0
- package/blocks/forms/components/form-mapping-editor.tsx +125 -0
- package/blocks/forms/components/site-audit-panel.tsx +73 -0
- package/blocks/forms/components/submissions-table.tsx +158 -0
- package/blocks/forms/lib/form-types.ts +127 -0
- package/blocks/forms/recipes/install-snippet-and-form.ts +21 -0
- package/blocks/forms/recipes/retry-failed-deliveries.ts +26 -0
- package/blocks/forms/recipes/route-form-to-list.ts +44 -0
- package/blocks/inbox/README.md +72 -0
- package/blocks/inbox/block.json +194 -0
- package/blocks/inbox/components/inbox-list.tsx +156 -0
- package/blocks/inbox/components/inbox-screen.tsx +49 -0
- package/blocks/inbox/components/reply-composer.tsx +183 -0
- package/blocks/inbox/components/tag-and-assign.tsx +189 -0
- package/blocks/inbox/components/thread-badge.tsx +17 -0
- package/blocks/inbox/components/thread-view.tsx +108 -0
- package/blocks/inbox/hooks/use-inbox-thread.ts +24 -0
- package/blocks/inbox/hooks/use-inbox-threads.ts +40 -0
- package/blocks/inbox/lib/idempotency.ts +30 -0
- package/blocks/inbox/lib/inbox-types.ts +100 -0
- package/blocks/inbox/recipes/01-triage-and-reply.ts +43 -0
- package/blocks/inbox/recipes/02-tag-and-assign.ts +31 -0
- package/blocks/inbox/recipes/03-hot-contacts-digest.ts +16 -0
- package/blocks/integrations/README.md +75 -0
- package/blocks/integrations/block.json +172 -0
- package/blocks/integrations/components/integration-list.tsx +165 -0
- package/blocks/integrations/components/sync-status.tsx +223 -0
- package/blocks/integrations/components/webhook-manager.tsx +300 -0
- package/blocks/integrations/lib/integrations-utils.ts +62 -0
- package/blocks/integrations/recipes/crm-sync-health.ts +19 -0
- package/blocks/integrations/recipes/push-contacts-to-crm.ts +20 -0
- package/blocks/integrations/recipes/register-webhook.ts +18 -0
- package/blocks/integrations/recipes/verify-webhook-route.ts +33 -0
- package/blocks/intent/README.md +57 -0
- package/blocks/intent/block.json +138 -0
- package/blocks/intent/components/account-intent-score.tsx +91 -0
- package/blocks/intent/components/intent-feed.tsx +215 -0
- package/blocks/intent/components/intent-keyword-manager.tsx +331 -0
- package/blocks/intent/hooks/use-account-intent.ts +46 -0
- package/blocks/intent/lib/intent-utils.ts +95 -0
- package/blocks/intent/recipes/rank-intent-accounts.ts +36 -0
- package/blocks/intent/recipes/track-competitor-domain.ts +23 -0
- package/blocks/intent/recipes/who-visited-url.ts +18 -0
- package/blocks/lifecycle/README.md +79 -0
- package/blocks/lifecycle/block.json +198 -0
- package/blocks/lifecycle/components/churn-list.tsx +92 -0
- package/blocks/lifecycle/components/customer-table.tsx +119 -0
- package/blocks/lifecycle/components/lead-queue.tsx +127 -0
- package/blocks/lifecycle/components/lifecycle-board.tsx +102 -0
- package/blocks/lifecycle/components/stage-move-dialog.tsx +75 -0
- package/blocks/lifecycle/hooks/use-debounced.ts +13 -0
- package/blocks/lifecycle/hooks/use-lifecycle-stages.ts +27 -0
- package/blocks/lifecycle/lib/lifecycle.ts +99 -0
- package/blocks/lifecycle/recipes/customer-brief.ts +23 -0
- package/blocks/lifecycle/recipes/move-company-stage.ts +17 -0
- package/blocks/lifecycle/recipes/read-lifecycle-board.ts +21 -0
- package/blocks/lifecycle/recipes/unlock-stage-contacts.ts +25 -0
- package/blocks/lists/README.md +96 -0
- package/blocks/lists/block.json +166 -0
- package/blocks/lists/components/add-to-list-button.tsx +111 -0
- package/blocks/lists/components/list-picker.tsx +94 -0
- package/blocks/lists/components/list-table.tsx +160 -0
- package/blocks/lists/components/suppression-manager.tsx +153 -0
- package/blocks/lists/recipes/create-list-and-add-contacts.ts +35 -0
- package/blocks/lists/recipes/refresh-and-check-lists.ts +21 -0
- package/blocks/lists/recipes/suppress-and-reinstate.ts +30 -0
- package/blocks/mailboxes/README.md +71 -0
- package/blocks/mailboxes/block.json +187 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +284 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +271 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +185 -0
- package/blocks/mailboxes/components/warmup-health.tsx +232 -0
- package/blocks/mailboxes/lib/mailboxes-utils.ts +69 -0
- package/blocks/mailboxes/recipes/buy-domain-and-mailboxes.ts +64 -0
- package/blocks/mailboxes/recipes/connect-imap-mailbox.ts +38 -0
- package/blocks/mailboxes/recipes/is-it-safe-to-send.ts +17 -0
- package/blocks/mailboxes/recipes/warmup-report.ts +31 -0
- package/blocks/newsletter/README.md +122 -0
- package/blocks/newsletter/block.json +224 -0
- package/blocks/newsletter/components/issue-form.tsx +124 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +152 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +131 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +93 -0
- package/blocks/newsletter/components/subscriber-table.tsx +173 -0
- package/blocks/newsletter/lib/newsletter-types.ts +97 -0
- package/blocks/newsletter/recipes/create-newsletter-and-draft-issue.ts +27 -0
- package/blocks/newsletter/recipes/import-audience-and-send.ts +43 -0
- package/blocks/newsletter/recipes/issue-performance-report.ts +26 -0
- package/blocks/nurture/README.md +110 -0
- package/blocks/nurture/block.json +191 -0
- package/blocks/nurture/components/nurture-builder.tsx +114 -0
- package/blocks/nurture/components/nurture-enroll.tsx +107 -0
- package/blocks/nurture/components/nurture-form.tsx +120 -0
- package/blocks/nurture/components/nurture-stats.tsx +81 -0
- package/blocks/nurture/lib/nurture-types.ts +109 -0
- package/blocks/nurture/recipes/create-a-nurture.ts +34 -0
- package/blocks/nurture/recipes/enroll-contacts-and-launch.ts +24 -0
- package/blocks/nurture/recipes/nurture-health-check.ts +28 -0
- package/blocks/objects/README.md +120 -0
- package/blocks/objects/block.json +243 -0
- package/blocks/objects/components/attribute-field-input.tsx +82 -0
- package/blocks/objects/components/attribute-form-dialog.tsx +159 -0
- package/blocks/objects/components/object-not-found.tsx +34 -0
- package/blocks/objects/components/object-record-detail.tsx +143 -0
- package/blocks/objects/components/object-schema-editor.tsx +197 -0
- package/blocks/objects/components/object-table.tsx +130 -0
- package/blocks/objects/components/record-form-dialog.tsx +112 -0
- package/blocks/objects/components/record-roles-panel.tsx +106 -0
- package/blocks/objects/hooks/use-object-schema.ts +36 -0
- package/blocks/objects/lib/object-types.ts +214 -0
- package/blocks/objects/recipes/define-object-and-add-records.ts +24 -0
- package/blocks/objects/recipes/update-audit-and-archive-records.ts +39 -0
- package/blocks/objects/recipes/upsert-records-from-your-system.ts +30 -0
- package/blocks/pages/README.md +104 -0
- package/blocks/pages/block.json +223 -0
- package/blocks/pages/components/brand-kit-editor.tsx +152 -0
- package/blocks/pages/components/content-library.tsx +163 -0
- package/blocks/pages/components/page-editor.tsx +253 -0
- package/blocks/pages/components/page-list.tsx +159 -0
- package/blocks/pages/components/seo-report.tsx +159 -0
- package/blocks/pages/lib/studio.ts +77 -0
- package/blocks/pages/recipes/audit-site-seo.ts +28 -0
- package/blocks/pages/recipes/create-and-publish-page.ts +26 -0
- package/blocks/pages/recipes/edit-page-section.ts +25 -0
- package/blocks/pages/recipes/refresh-brand-kit.ts +22 -0
- package/blocks/quotes/README.md +79 -0
- package/blocks/quotes/block.json +183 -0
- package/blocks/quotes/components/product-picker.tsx +80 -0
- package/blocks/quotes/components/quote-builder.tsx +195 -0
- package/blocks/quotes/components/quote-list.tsx +79 -0
- package/blocks/quotes/components/quote-viewer.tsx +135 -0
- package/blocks/quotes/components/signature-status.tsx +77 -0
- package/blocks/quotes/hooks/use-debounced.ts +13 -0
- package/blocks/quotes/lib/quotes.ts +142 -0
- package/blocks/quotes/recipes/create-and-send-quote.ts +44 -0
- package/blocks/quotes/recipes/revise-sent-quote.ts +18 -0
- package/blocks/quotes/recipes/track-quote-signature.ts +22 -0
- package/blocks/radar/README.md +106 -0
- package/blocks/radar/block.json +189 -0
- package/blocks/radar/components/competitor-battle-card.tsx +53 -0
- package/blocks/radar/components/competitor-tracker.tsx +158 -0
- package/blocks/radar/components/radar-overview.tsx +67 -0
- package/blocks/radar/components/radar-report.tsx +86 -0
- package/blocks/radar/hooks/use-competitors.ts +23 -0
- package/blocks/radar/lib/radar-types.ts +124 -0
- package/blocks/radar/recipes/battle-card.ts +28 -0
- package/blocks/radar/recipes/deep-dive-report.ts +32 -0
- package/blocks/radar/recipes/marketing-snapshot.ts +18 -0
- package/blocks/radar/recipes/track-and-scan.ts +39 -0
- package/blocks/search/README.md +129 -0
- package/blocks/search/block.json +185 -0
- package/blocks/search/components/filter-builder.tsx +149 -0
- package/blocks/search/components/prospect-results.tsx +175 -0
- package/blocks/search/components/save-to-list-dialog.tsx +135 -0
- package/blocks/search/components/saved-searches.tsx +95 -0
- package/blocks/search/hooks/use-filter-options.ts +41 -0
- package/blocks/search/lib/search-types.ts +149 -0
- package/blocks/search/recipes/explore-filter-values.ts +27 -0
- package/blocks/search/recipes/saved-searches.ts +34 -0
- package/blocks/search/recipes/search-and-save-to-list.ts +37 -0
- package/blocks/sequencer/README.md +87 -0
- package/blocks/sequencer/block.json +214 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +93 -0
- package/blocks/sequencer/components/sequence-builder.tsx +306 -0
- package/blocks/sequencer/components/sequence-list.tsx +200 -0
- package/blocks/sequencer/components/sequence-stats.tsx +256 -0
- package/blocks/sequencer/components/step-editor.tsx +213 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +168 -0
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +51 -0
- package/blocks/sequencer/recipes/launch-and-enroll.ts +44 -0
- package/blocks/sequencer/recipes/pause-when-unsafe.ts +30 -0
- package/blocks/sequencer/recipes/sequence-report.ts +22 -0
- package/blocks/social-ads/README.md +128 -0
- package/blocks/social-ads/block.json +256 -0
- package/blocks/social-ads/components/ad-campaign-list.tsx +136 -0
- package/blocks/social-ads/components/audience-sync.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +121 -0
- package/blocks/social-ads/components/post-composer.tsx +122 -0
- package/blocks/social-ads/components/social-post-list.tsx +116 -0
- package/blocks/social-ads/hooks/use-social-accounts.ts +16 -0
- package/blocks/social-ads/lib/social-ads-types.ts +167 -0
- package/blocks/social-ads/recipes/creative-to-gallery.ts +41 -0
- package/blocks/social-ads/recipes/schedule-post.ts +51 -0
- package/blocks/social-ads/recipes/sync-audience-and-launch.ts +41 -0
- package/blocks/visitors/README.md +56 -0
- package/blocks/visitors/block.json +139 -0
- package/blocks/visitors/components/snippet-install.tsx +169 -0
- package/blocks/visitors/components/traffic-chart.tsx +156 -0
- package/blocks/visitors/components/visitor-company-card.tsx +92 -0
- package/blocks/visitors/components/visitor-feed.tsx +157 -0
- package/blocks/visitors/lib/visitors-utils.ts +78 -0
- package/blocks/visitors/recipes/check-snippet-installed.ts +24 -0
- package/blocks/visitors/recipes/recent-company-visits.ts +27 -0
- package/blocks/visitors/recipes/traffic-summary.ts +24 -0
- package/blocks/webchat/README.md +87 -0
- package/blocks/webchat/block.json +134 -0
- package/blocks/webchat/components/chat-widget-config.tsx +160 -0
- package/blocks/webchat/components/conversation-list.tsx +74 -0
- package/blocks/webchat/lib/webchat-types.ts +83 -0
- package/blocks/webchat/recipes/create-bot-and-style-widget.ts +24 -0
- package/blocks/webchat/recipes/list-chat-visitors.ts +24 -0
- package/blocks/workbench/README.md +135 -0
- package/blocks/workbench/block.json +259 -0
- package/blocks/workbench/components/column-recipe-editor.tsx +183 -0
- package/blocks/workbench/components/run-progress.tsx +132 -0
- package/blocks/workbench/components/workbench-add-rows.tsx +100 -0
- package/blocks/workbench/components/workbench-grid.tsx +159 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +102 -0
- package/blocks/workbench/components/workbench-table-list.tsx +105 -0
- package/blocks/workbench/hooks/use-workbench-table.ts +30 -0
- package/blocks/workbench/lib/waterfall-presets.ts +119 -0
- package/blocks/workbench/lib/workbench-types.ts +191 -0
- package/blocks/workbench/recipes/build-enriched-table.ts +65 -0
- package/blocks/workbench/recipes/import-from-search.ts +42 -0
- package/blocks/workbench/recipes/scrape-into-table.ts +36 -0
- package/blocks/workbench/recipes/waterfall-column.ts +55 -0
- package/blocks/workflows/README.md +100 -0
- package/blocks/workflows/block.json +188 -0
- package/blocks/workflows/components/run-history.tsx +136 -0
- package/blocks/workflows/components/trigger-config.tsx +134 -0
- package/blocks/workflows/components/workflow-editor.tsx +142 -0
- package/blocks/workflows/components/workflow-list.tsx +153 -0
- package/blocks/workflows/hooks/use-workflow.ts +51 -0
- package/blocks/workflows/lib/graph.ts +187 -0
- package/blocks/workflows/recipes/create-workflow.ts +35 -0
- package/blocks/workflows/recipes/find-failed-runs.ts +15 -0
- package/blocks/workflows/recipes/run-workflow-and-wait.ts +32 -0
- package/blocks/workflows/recipes/set-trigger.ts +22 -0
- package/dist/cli.js +5330 -29
- package/dist/index.browser.js +7740 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.browser.mjs +7717 -0
- package/dist/index.browser.mjs.map +1 -0
- package/dist/index.d.mts +1440 -21
- package/dist/index.d.ts +1440 -21
- package/dist/index.js +42 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +42 -3
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +7598 -0
- package/dist/react.browser.js.map +1 -0
- package/dist/react.browser.mjs +7568 -0
- package/dist/react.browser.mjs.map +1 -0
- package/dist/react.d.mts +1340 -18
- package/dist/react.d.ts +1340 -18
- package/dist/react.js +41 -2
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +41 -2
- package/dist/react.mjs.map +1 -1
- package/dist/types/agentSetup.d.ts +6 -6
- package/dist/types/blocks/add.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +46 -0
- package/dist/types/blocks/doctor.d.ts +26 -0
- package/dist/types/blocks/registry.d.ts +134 -0
- package/dist/types/cli.d.ts +16 -0
- package/dist/types/core.d.ts +1 -1
- package/dist/types/crypto-browser.d.ts +2 -0
- package/dist/types/crypto-node.d.ts +6 -0
- package/dist/types/react.d.ts +1 -1
- package/dist/types/version.d.ts +2 -2
- package/dist/types/webhooks.d.ts +2 -2
- package/dist/ui/index.d.mts +54 -0
- package/dist/ui/index.d.ts +54 -0
- package/dist/ui/index.js +105 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/index.mjs +44 -0
- package/dist/ui/index.mjs.map +1 -0
- package/dist/ui/lib/compose-refs.d.mts +19 -0
- package/dist/ui/lib/compose-refs.d.ts +19 -0
- package/dist/ui/lib/compose-refs.js +76 -0
- package/dist/ui/lib/compose-refs.js.map +1 -0
- package/dist/ui/lib/compose-refs.mjs +41 -0
- package/dist/ui/lib/compose-refs.mjs.map +1 -0
- package/dist/ui/lib/use-mobile.d.mts +4 -0
- package/dist/ui/lib/use-mobile.d.ts +4 -0
- package/dist/ui/lib/use-mobile.js +57 -0
- package/dist/ui/lib/use-mobile.js.map +1 -0
- package/dist/ui/lib/use-mobile.mjs +22 -0
- package/dist/ui/lib/use-mobile.mjs.map +1 -0
- package/dist/ui/lib/utils.d.mts +10 -0
- package/dist/ui/lib/utils.d.ts +10 -0
- package/dist/ui/lib/utils.js +33 -0
- package/dist/ui/lib/utils.js.map +1 -0
- package/dist/ui/lib/utils.mjs +9 -0
- package/dist/ui/lib/utils.mjs.map +1 -0
- package/dist/ui/primitives/alert-dialog.d.mts +22 -0
- package/dist/ui/primitives/alert-dialog.d.ts +22 -0
- package/dist/ui/primitives/alert-dialog.js +163 -0
- package/dist/ui/primitives/alert-dialog.js.map +1 -0
- package/dist/ui/primitives/alert-dialog.mjs +119 -0
- package/dist/ui/primitives/alert-dialog.mjs.map +1 -0
- package/dist/ui/primitives/avatar.d.mts +8 -0
- package/dist/ui/primitives/avatar.d.ts +8 -0
- package/dist/ui/primitives/avatar.js +91 -0
- package/dist/ui/primitives/avatar.js.map +1 -0
- package/dist/ui/primitives/avatar.mjs +55 -0
- package/dist/ui/primitives/avatar.mjs.map +1 -0
- package/dist/ui/primitives/breadcrumb.d.mts +13 -0
- package/dist/ui/primitives/breadcrumb.d.ts +13 -0
- package/dist/ui/primitives/breadcrumb.js +127 -0
- package/dist/ui/primitives/breadcrumb.js.map +1 -0
- package/dist/ui/primitives/breadcrumb.mjs +97 -0
- package/dist/ui/primitives/breadcrumb.mjs.map +1 -0
- package/dist/ui/primitives/button.d.mts +15 -0
- package/dist/ui/primitives/button.d.ts +15 -0
- package/dist/ui/primitives/button.js +91 -0
- package/dist/ui/primitives/button.js.map +1 -0
- package/dist/ui/primitives/button.mjs +56 -0
- package/dist/ui/primitives/button.mjs.map +1 -0
- package/dist/ui/primitives/card.d.mts +10 -0
- package/dist/ui/primitives/card.d.ts +10 -0
- package/dist/ui/primitives/card.js +101 -0
- package/dist/ui/primitives/card.js.map +1 -0
- package/dist/ui/primitives/card.mjs +62 -0
- package/dist/ui/primitives/card.mjs.map +1 -0
- package/dist/ui/primitives/carousel.d.mts +45 -0
- package/dist/ui/primitives/carousel.d.ts +45 -0
- package/dist/ui/primitives/carousel.js +300 -0
- package/dist/ui/primitives/carousel.js.map +1 -0
- package/dist/ui/primitives/carousel.mjs +261 -0
- package/dist/ui/primitives/carousel.mjs.map +1 -0
- package/dist/ui/primitives/chart.d.mts +68 -0
- package/dist/ui/primitives/chart.d.ts +68 -0
- package/dist/ui/primitives/chart.js +280 -0
- package/dist/ui/primitives/chart.js.map +1 -0
- package/dist/ui/primitives/chart.mjs +241 -0
- package/dist/ui/primitives/chart.mjs.map +1 -0
- package/dist/ui/primitives/collapsible.d.mts +8 -0
- package/dist/ui/primitives/collapsible.d.ts +8 -0
- package/dist/ui/primitives/collapsible.js +72 -0
- package/dist/ui/primitives/collapsible.js.map +1 -0
- package/dist/ui/primitives/collapsible.mjs +36 -0
- package/dist/ui/primitives/collapsible.mjs.map +1 -0
- package/dist/ui/primitives/context-menu.d.mts +29 -0
- package/dist/ui/primitives/context-menu.d.ts +29 -0
- package/dist/ui/primitives/context-menu.js +203 -0
- package/dist/ui/primitives/context-menu.js.map +1 -0
- package/dist/ui/primitives/context-menu.mjs +155 -0
- package/dist/ui/primitives/context-menu.mjs.map +1 -0
- package/dist/ui/primitives/dialog.d.mts +26 -0
- package/dist/ui/primitives/dialog.d.ts +26 -0
- package/dist/ui/primitives/dialog.js +192 -0
- package/dist/ui/primitives/dialog.js.map +1 -0
- package/dist/ui/primitives/dialog.mjs +148 -0
- package/dist/ui/primitives/dialog.mjs.map +1 -0
- package/dist/ui/primitives/drawer.d.mts +26 -0
- package/dist/ui/primitives/drawer.d.ts +26 -0
- package/dist/ui/primitives/drawer.js +146 -0
- package/dist/ui/primitives/drawer.js.map +1 -0
- package/dist/ui/primitives/drawer.mjs +103 -0
- package/dist/ui/primitives/drawer.mjs.map +1 -0
- package/dist/ui/primitives/input.d.mts +5 -0
- package/dist/ui/primitives/input.d.ts +5 -0
- package/dist/ui/primitives/input.js +46 -0
- package/dist/ui/primitives/input.js.map +1 -0
- package/dist/ui/primitives/input.mjs +22 -0
- package/dist/ui/primitives/input.mjs.map +1 -0
- package/dist/ui/primitives/item.d.mts +27 -0
- package/dist/ui/primitives/item.d.ts +27 -0
- package/dist/ui/primitives/item.js +221 -0
- package/dist/ui/primitives/item.js.map +1 -0
- package/dist/ui/primitives/item.mjs +188 -0
- package/dist/ui/primitives/item.mjs.map +1 -0
- package/dist/ui/primitives/kbd.d.mts +15 -0
- package/dist/ui/primitives/kbd.d.ts +15 -0
- package/dist/ui/primitives/kbd.js +45 -0
- package/dist/ui/primitives/kbd.js.map +1 -0
- package/dist/ui/primitives/kbd.mjs +21 -0
- package/dist/ui/primitives/kbd.mjs.map +1 -0
- package/dist/ui/primitives/label.d.mts +6 -0
- package/dist/ui/primitives/label.d.ts +6 -0
- package/dist/ui/primitives/label.js +55 -0
- package/dist/ui/primitives/label.js.map +1 -0
- package/dist/ui/primitives/label.mjs +21 -0
- package/dist/ui/primitives/label.mjs.map +1 -0
- package/dist/ui/primitives/resizable.d.mts +10 -0
- package/dist/ui/primitives/resizable.d.ts +10 -0
- package/dist/ui/primitives/resizable.js +68 -0
- package/dist/ui/primitives/resizable.js.map +1 -0
- package/dist/ui/primitives/resizable.mjs +42 -0
- package/dist/ui/primitives/resizable.mjs.map +1 -0
- package/dist/ui/primitives/save-status.d.mts +31 -0
- package/dist/ui/primitives/save-status.d.ts +31 -0
- package/dist/ui/primitives/save-status.js +150 -0
- package/dist/ui/primitives/save-status.js.map +1 -0
- package/dist/ui/primitives/save-status.mjs +126 -0
- package/dist/ui/primitives/save-status.mjs.map +1 -0
- package/dist/ui/primitives/separator.d.mts +6 -0
- package/dist/ui/primitives/separator.d.ts +6 -0
- package/dist/ui/primitives/separator.js +62 -0
- package/dist/ui/primitives/separator.js.map +1 -0
- package/dist/ui/primitives/separator.mjs +28 -0
- package/dist/ui/primitives/separator.mjs.map +1 -0
- package/dist/ui/primitives/sheet.d.mts +16 -0
- package/dist/ui/primitives/sheet.d.ts +16 -0
- package/dist/ui/primitives/sheet.js +170 -0
- package/dist/ui/primitives/sheet.js.map +1 -0
- package/dist/ui/primitives/sheet.mjs +129 -0
- package/dist/ui/primitives/sheet.mjs.map +1 -0
- package/dist/ui/primitives/sidebar.d.mts +74 -0
- package/dist/ui/primitives/sidebar.d.ts +74 -0
- package/dist/ui/primitives/sidebar.js +697 -0
- package/dist/ui/primitives/sidebar.js.map +1 -0
- package/dist/ui/primitives/sidebar.mjs +651 -0
- package/dist/ui/primitives/sidebar.mjs.map +1 -0
- package/dist/ui/primitives/skeleton.d.mts +5 -0
- package/dist/ui/primitives/skeleton.d.ts +5 -0
- package/dist/ui/primitives/skeleton.js +40 -0
- package/dist/ui/primitives/skeleton.js.map +1 -0
- package/dist/ui/primitives/skeleton.mjs +16 -0
- package/dist/ui/primitives/skeleton.mjs.map +1 -0
- package/dist/ui/primitives/slider.d.mts +20 -0
- package/dist/ui/primitives/slider.d.ts +20 -0
- package/dist/ui/primitives/slider.js +112 -0
- package/dist/ui/primitives/slider.js.map +1 -0
- package/dist/ui/primitives/slider.mjs +78 -0
- package/dist/ui/primitives/slider.mjs.map +1 -0
- package/dist/ui/primitives/sortable.d.mts +50 -0
- package/dist/ui/primitives/sortable.d.ts +50 -0
- package/dist/ui/primitives/sortable.js +456 -0
- package/dist/ui/primitives/sortable.js.map +1 -0
- package/dist/ui/primitives/sortable.mjs +435 -0
- package/dist/ui/primitives/sortable.mjs.map +1 -0
- package/dist/ui/primitives/switch.d.mts +6 -0
- package/dist/ui/primitives/switch.d.ts +6 -0
- package/dist/ui/primitives/switch.js +67 -0
- package/dist/ui/primitives/switch.js.map +1 -0
- package/dist/ui/primitives/switch.mjs +33 -0
- package/dist/ui/primitives/switch.mjs.map +1 -0
- package/dist/ui/primitives/table.d.mts +12 -0
- package/dist/ui/primitives/table.d.ts +12 -0
- package/dist/ui/primitives/table.js +130 -0
- package/dist/ui/primitives/table.js.map +1 -0
- package/dist/ui/primitives/table.mjs +89 -0
- package/dist/ui/primitives/table.mjs.map +1 -0
- package/dist/ui/primitives/tabs.d.mts +9 -0
- package/dist/ui/primitives/tabs.d.ts +9 -0
- package/dist/ui/primitives/tabs.js +86 -0
- package/dist/ui/primitives/tabs.js.map +1 -0
- package/dist/ui/primitives/tabs.mjs +49 -0
- package/dist/ui/primitives/tabs.mjs.map +1 -0
- package/dist/ui/primitives/textarea.d.mts +5 -0
- package/dist/ui/primitives/textarea.d.ts +5 -0
- package/dist/ui/primitives/textarea.js +43 -0
- package/dist/ui/primitives/textarea.js.map +1 -0
- package/dist/ui/primitives/textarea.mjs +19 -0
- package/dist/ui/primitives/textarea.mjs.map +1 -0
- package/dist/ui/primitives/tooltip.d.mts +9 -0
- package/dist/ui/primitives/tooltip.d.ts +9 -0
- package/dist/ui/primitives/tooltip.js +94 -0
- package/dist/ui/primitives/tooltip.js.map +1 -0
- package/dist/ui/primitives/tooltip.mjs +57 -0
- package/dist/ui/primitives/tooltip.mjs.map +1 -0
- package/docs/agents.md +72 -29
- package/docs/api/README.md +6 -3
- package/docs/api/appObjects.d.ts +1 -1
- package/docs/api/appObjects.md +1 -1
- package/docs/api/by-path.md +29 -0
- package/docs/api/objects.d.ts +72 -5
- package/docs/api/objects.md +84 -7
- package/docs/api/processLists.d.ts +247 -0
- package/docs/api/processLists.md +409 -0
- package/docs/api/reports.d.ts +476 -0
- package/docs/api/reports.md +539 -0
- package/docs/api/scheduleBuy.d.ts +12 -8
- package/docs/api/scheduleBuy.md +13 -10
- package/docs/api/seo.d.ts +406 -3
- package/docs/api/seo.md +505 -1
- package/docs/api/workspaceBackup.d.ts +324 -0
- package/docs/api/workspaceBackup.md +427 -0
- package/package.json +174 -5
- package/stacks/6sense.json +46 -0
- package/stacks/README.md +20 -0
- package/stacks/apollo.json +128 -0
- package/stacks/clay.json +59 -0
- package/stacks/gong.json +72 -0
- package/stacks/hubspot.json +162 -0
- package/stacks/intercom.json +53 -0
- package/stacks/mailchimp.json +71 -0
- package/stacks/outreach.json +86 -0
- package/stacks/salesforce.json +94 -0
- package/stacks/stack.schema.json +32 -0
- package/stacks/starter.json +22 -0
- package/styles/styles.css +2 -0
- package/styles/theme.css +181 -0
- package/styles/tokens.css +284 -0
- package/templates/next-app/README.md.tmpl +35 -0
- package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
- package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
- package/templates/next-app/app/globals.css.tmpl +8 -0
- package/templates/next-app/app/layout.tsx.tmpl +19 -0
- package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
- package/templates/next-app/dot-env.local.example.tmpl +6 -0
- package/templates/next-app/dot-gitignore.tmpl +7 -0
- package/templates/next-app/lib/nav.ts.tmpl +2 -0
- package/templates/next-app/next.config.mjs.tmpl +4 -0
- package/templates/next-app/package.json.tmpl +25 -0
- package/templates/next-app/postcss.config.mjs.tmpl +4 -0
- package/templates/next-app/tsconfig.json.tmpl +21 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../block.schema.json",
|
|
3
|
+
"id": "enrichment",
|
|
4
|
+
"title": "Enrichment",
|
|
5
|
+
"summary": "Look up a person or company, verify emails, enrich many contacts with a credit quote and a polled job, and manage waterfall pipelines.",
|
|
6
|
+
"features": [
|
|
7
|
+
"single person/company lookup that can apply found fields to the record",
|
|
8
|
+
"email verification chip with a one-click verify",
|
|
9
|
+
"bulk enrichment with a free preflight, credit estimate confirmation, polled progress and cancel",
|
|
10
|
+
"waterfall pipeline manager with provider order and credential readiness"
|
|
11
|
+
],
|
|
12
|
+
"dependsOn": [
|
|
13
|
+
"lists"
|
|
14
|
+
],
|
|
15
|
+
"operations": [
|
|
16
|
+
"cancel_enrichment_job_enrichment_jobs__job_id__cancel_post",
|
|
17
|
+
"create_default_waterfall_config_enrichment_waterfall_configs_post",
|
|
18
|
+
"create_list_lists_post",
|
|
19
|
+
"delete_contact_contacts__contact_id__delete",
|
|
20
|
+
"delete_waterfall_config_enrichment_waterfall_configs__column_id__delete",
|
|
21
|
+
"get_enrichment_job_enrichment_jobs__job_id__get",
|
|
22
|
+
"list_contact_fields_fields_get",
|
|
23
|
+
"list_contacts_contacts_get",
|
|
24
|
+
"list_enrichment_providers_enrichment_providers_get",
|
|
25
|
+
"list_lists_lists_get",
|
|
26
|
+
"list_waterfall_configs_enrichment_waterfall_configs_get",
|
|
27
|
+
"lookup_company_enrichment_lookup_company_post",
|
|
28
|
+
"lookup_person_enrichment_lookup_person_post",
|
|
29
|
+
"preflight_enrichment_enrichment_preflight_post",
|
|
30
|
+
"start_enrichment_enrichment_enrich_post",
|
|
31
|
+
"update_company_companies__company_id__patch",
|
|
32
|
+
"update_contact_contacts__contact_id__patch",
|
|
33
|
+
"verify_email_enrichment_verify_email_post"
|
|
34
|
+
],
|
|
35
|
+
"components": [
|
|
36
|
+
{
|
|
37
|
+
"name": "EnrichButton",
|
|
38
|
+
"file": "components/enrich-button.tsx",
|
|
39
|
+
"purpose": "Look up one person or company (2 credits only if found, confirmed) and optionally apply the chosen fields to the CRM record.",
|
|
40
|
+
"operations": [
|
|
41
|
+
"lookup_person_enrichment_lookup_person_post",
|
|
42
|
+
"lookup_company_enrichment_lookup_company_post",
|
|
43
|
+
"update_contact_contacts__contact_id__patch",
|
|
44
|
+
"update_company_companies__company_id__patch"
|
|
45
|
+
],
|
|
46
|
+
"tiers": [
|
|
47
|
+
"write",
|
|
48
|
+
"billable"
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "EmailVerifyBadge",
|
|
53
|
+
"file": "components/email-verify-badge.tsx",
|
|
54
|
+
"purpose": "Status chip for an email (Unverified, Valid, Risky, Invalid) with a confirmed 1-credit Verify action; never verifies a locked email.",
|
|
55
|
+
"operations": [
|
|
56
|
+
"verify_email_enrichment_verify_email_post"
|
|
57
|
+
],
|
|
58
|
+
"tiers": [
|
|
59
|
+
"billable"
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "BulkEnrichPanel",
|
|
64
|
+
"file": "components/bulk-enrich-panel.tsx",
|
|
65
|
+
"purpose": "Fill a field for many contacts of a list: free preflight, credit estimate in ConfirmSpendDialog, start the job, poll it with useJobPoll, cancel. With no props it is a whole page (pick a list, tick contacts).",
|
|
66
|
+
"operations": [
|
|
67
|
+
"list_enrichment_providers_enrichment_providers_get",
|
|
68
|
+
"preflight_enrichment_enrichment_preflight_post",
|
|
69
|
+
"start_enrichment_enrichment_enrich_post",
|
|
70
|
+
"get_enrichment_job_enrichment_jobs__job_id__get",
|
|
71
|
+
"cancel_enrichment_job_enrichment_jobs__job_id__cancel_post",
|
|
72
|
+
"list_lists_lists_get",
|
|
73
|
+
"create_list_lists_post",
|
|
74
|
+
"list_contacts_contacts_get",
|
|
75
|
+
"list_contact_fields_fields_get",
|
|
76
|
+
"delete_contact_contacts__contact_id__delete"
|
|
77
|
+
],
|
|
78
|
+
"tiers": [
|
|
79
|
+
"read",
|
|
80
|
+
"write",
|
|
81
|
+
"billable",
|
|
82
|
+
"destructive"
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "WaterfallConfig",
|
|
87
|
+
"file": "components/waterfall-config.tsx",
|
|
88
|
+
"purpose": "Saved waterfall pipelines of a list plus a form to create one: field, providers, order, credential readiness.",
|
|
89
|
+
"operations": [
|
|
90
|
+
"list_waterfall_configs_enrichment_waterfall_configs_get",
|
|
91
|
+
"list_enrichment_providers_enrichment_providers_get",
|
|
92
|
+
"create_default_waterfall_config_enrichment_waterfall_configs_post",
|
|
93
|
+
"delete_waterfall_config_enrichment_waterfall_configs__column_id__delete"
|
|
94
|
+
],
|
|
95
|
+
"tiers": [
|
|
96
|
+
"read",
|
|
97
|
+
"billable",
|
|
98
|
+
"destructive"
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
"hooks": [
|
|
103
|
+
{
|
|
104
|
+
"name": "useProviders",
|
|
105
|
+
"file": "hooks/use-providers.ts",
|
|
106
|
+
"purpose": "The enrichment provider catalog: graph8-funded vs own key, credits per action.",
|
|
107
|
+
"operations": [
|
|
108
|
+
"list_enrichment_providers_enrichment_providers_get"
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
"lib": [
|
|
113
|
+
{
|
|
114
|
+
"file": "lib/enrichment-types.ts",
|
|
115
|
+
"purpose": "Credit prices, field and job vocabularies, preflight/job/pipeline shapes, estimateCredits and isJobTerminal."
|
|
116
|
+
}
|
|
117
|
+
],
|
|
118
|
+
"recipes": [
|
|
119
|
+
{
|
|
120
|
+
"file": "recipes/lookup-and-verify.ts",
|
|
121
|
+
"title": "Look up a person or company and verify an email",
|
|
122
|
+
"when": "One-off enrichment of a single record. A lookup costs 2 credits ONLY when it finds a match (a miss is free); an email verification costs 1 credit. Both are billable: ask a human before looping over many records (use the bulk job for that). The lookup returns data, it does not update the CRM: write the fields you want with `g8.contacts.update` afterwards."
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"file": "recipes/bulk-enrich-and-poll.ts",
|
|
126
|
+
"title": "Enrich many contacts of a list and wait for the job",
|
|
127
|
+
"when": "Filling work emails or phones for a list. Order: preflight (free: counts, missing provider keys and the credit quote) -> a human approves the quote -> start the job -> poll it (the API has no streams or webhook for completion) -> read provider errors if nothing was written. Contact ids must be on the list; ids not on it are skipped and not charged. Never enrich contacts you only have as masked search results: save them to a list first."
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"file": "recipes/waterfall-pipeline.ts",
|
|
131
|
+
"title": "Create a waterfall pipeline for a list and see which providers are ready",
|
|
132
|
+
"when": "Setting up \"try provider A, then B\" enrichment for a column once, then reusing it. Check the provider catalog first: graph8-funded providers spend credits when the pipeline runs, providers on your own key do not, and a provider without a key is skipped. Creating is idempotent by (list_id, name)."
|
|
133
|
+
}
|
|
134
|
+
],
|
|
135
|
+
"prerequisites": [
|
|
136
|
+
{
|
|
137
|
+
"id": "crm-installed",
|
|
138
|
+
"where": "app",
|
|
139
|
+
"description": "The crm block is installed (the CLI does it); enrichment acts on CRM contacts that sit on a list.",
|
|
140
|
+
"verify": "components/g8/crm/ exists."
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"id": "proxy-allowlist",
|
|
144
|
+
"where": "app",
|
|
145
|
+
"description": "allowedPrefixes in the /api/g8 route includes `enrichment` (plus `contacts` and `companies` for Apply).",
|
|
146
|
+
"verify": "GET /api/g8/enrichment/providers returns 200 when signed in."
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"id": "provider-keys",
|
|
150
|
+
"where": "api",
|
|
151
|
+
"description": "For providers that are not graph8-funded, the workspace has its own API key saved in graph8 settings (Integrations). Without it that step is skipped. There is no API to add provider keys.",
|
|
152
|
+
"verify": "GET /enrichment/providers shows byok_configured true for the provider, or the preflight lists no missing_credentials."
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"id": "credits",
|
|
156
|
+
"where": "api",
|
|
157
|
+
"description": "The workspace has credits for graph8-funded providers. The API has no balance read, so the UI shows estimates only; a run without enough credits fails with 402.",
|
|
158
|
+
"verify": "A one-contact enrichment succeeds."
|
|
159
|
+
}
|
|
160
|
+
],
|
|
161
|
+
"scopes": [
|
|
162
|
+
"companies:write",
|
|
163
|
+
"contacts:delete",
|
|
164
|
+
"contacts:read",
|
|
165
|
+
"contacts:write",
|
|
166
|
+
"enrichment:delete",
|
|
167
|
+
"enrichment:read",
|
|
168
|
+
"enrichment:run",
|
|
169
|
+
"enrichment:write",
|
|
170
|
+
"fields:read",
|
|
171
|
+
"lists:read",
|
|
172
|
+
"lists:write"
|
|
173
|
+
],
|
|
174
|
+
"defaults": [
|
|
175
|
+
{
|
|
176
|
+
"decision": "Providers a bulk run tries",
|
|
177
|
+
"default": "prospeo then dropcontact (the API's own default), filtered to those in the provider catalog; editable in the panel",
|
|
178
|
+
"askHuman": false
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"decision": "Field a bulk run fills",
|
|
182
|
+
"default": "work email",
|
|
183
|
+
"askHuman": false
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"decision": "Credit estimate shown before a run",
|
|
187
|
+
"default": "The preflight's max_total_credits; else 2 credits x contacts on the list",
|
|
188
|
+
"askHuman": false
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"decision": "Lookup results applied to a record",
|
|
192
|
+
"default": "Never automatically: the person ticks fields; fields the record already has start unticked",
|
|
193
|
+
"askHuman": false
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"decision": "Who may spend credits on enrichment",
|
|
197
|
+
"default": "Anyone the proxy's authorize lets through",
|
|
198
|
+
"askHuman": true
|
|
199
|
+
}
|
|
200
|
+
],
|
|
201
|
+
"peerDependencies": {
|
|
202
|
+
"react": ">=18"
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BulkEnrichPanel: fill a field (work email, direct phone, mobile phone) for many contacts of a list with a
|
|
5
|
+
* provider waterfall. Shows a free preflight (counts, missing provider keys, warnings, the credit quote),
|
|
6
|
+
* asks for confirmation with that estimate, starts the job and follows it to the end.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. listEnrichmentProviders GET /enrichment/providers (read) provider choices, graph8-funded vs own key
|
|
10
|
+
* 2. preflightEnrichment POST /enrichment/preflight (read, free; nothing is written or charged)
|
|
11
|
+
* { list_id, contact_ids, mode: "waterfall", providers, target_field } -> contact_count, not_in_list,
|
|
12
|
+
* missing_credentials, warnings, credits_per_record, max_total_credits
|
|
13
|
+
* 3. startEnrichment POST /enrichment/enrich (billable) 202 { job_id, status }
|
|
14
|
+
* Goes through ConfirmSpendDialog first with `max_total_credits` (or 2 credits x contacts when the
|
|
15
|
+
* preflight could not price it). There is no balance read, so this is an estimate of the most it can charge.
|
|
16
|
+
* 4. getEnrichmentJob GET /enrichment/jobs/{job_id} (read) polled with useJobPoll (no streams), backoff;
|
|
17
|
+
* read once more with ?include_provider_errors=true when nothing was enriched or some records failed.
|
|
18
|
+
* 5. cancelEnrichmentJob POST /enrichment/jobs/{job_id}/cancel (write, free; records already done keep their data and charge)
|
|
19
|
+
*
|
|
20
|
+
* The contacts must be on `listId`; ids not on it are skipped and not charged. Locked contacts are enriched
|
|
21
|
+
* like any other once they sit in a list.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { useState } from "react";
|
|
25
|
+
import { Button } from "@graph8/sdk/ui";
|
|
26
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
27
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
28
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
29
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
30
|
+
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
31
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
32
|
+
import { ContactTable } from "@/g8/crm/components/contact-table";
|
|
33
|
+
import { ListPicker } from "@/g8/lists/components/list-picker";
|
|
34
|
+
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";
|
|
37
|
+
|
|
38
|
+
export interface BulkEnrichPanelProps {
|
|
39
|
+
/** 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
|
+
listId?: number;
|
|
41
|
+
contactIds?: number[];
|
|
42
|
+
/** Providers tried, in order, when the person has not chosen. Default: prospeo, dropcontact (the API's own default). */
|
|
43
|
+
defaultProviders?: string[];
|
|
44
|
+
defaultField?: TargetField;
|
|
45
|
+
/** Called when the job reached a final state. */
|
|
46
|
+
onDone?: (job: EnrichJob) => void;
|
|
47
|
+
className?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const DEFAULT_PROVIDERS = ["prospeo", "dropcontact"];
|
|
51
|
+
|
|
52
|
+
function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDERS, defaultField = "work_email", onDone, className }: BulkEnrichPanelProps & { listId: number; contactIds: number[] }) {
|
|
53
|
+
const [field, setField] = useState<TargetField>(defaultField);
|
|
54
|
+
const [chosen, setChosen] = useState<string[] | null>(null);
|
|
55
|
+
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
56
|
+
const [jobId, setJobId] = useState<string | null>(null);
|
|
57
|
+
const catalog = useProviders();
|
|
58
|
+
|
|
59
|
+
const known = new Set(catalog.providers.map((p) => p.provider));
|
|
60
|
+
const providers = chosen ?? defaultProviders.filter((p) => known.size === 0 || known.has(p));
|
|
61
|
+
const column = TARGET_FIELDS.find((f) => f.value === field)!;
|
|
62
|
+
const ids = [...new Set(contactIds)];
|
|
63
|
+
|
|
64
|
+
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(","), providers.join(","), field],
|
|
67
|
+
{ enabled: ids.length > 0 && !jobId },
|
|
68
|
+
);
|
|
69
|
+
const quote = preflight.data;
|
|
70
|
+
const credits = estimateCredits(quote, ids.length);
|
|
71
|
+
const runnable = quote ? quote.contact_count - quote.not_in_list.length : ids.length;
|
|
72
|
+
|
|
73
|
+
const start = useG8Mutation(() => g8Post<{ job_id: string }>("/enrichment/enrich", { contact_ids: ids, list_id: listId, fields_config: { [column.column]: providers } }), {
|
|
74
|
+
onSuccess: (res) => { setConfirmOpen(false); setJobId(res.data.job_id); },
|
|
75
|
+
});
|
|
76
|
+
const job = useJobPoll<EnrichJob>(
|
|
77
|
+
async (signal) => (await g8Get<EnrichJob>(`/enrichment/jobs/${jobId}`, undefined, { signal })).data,
|
|
78
|
+
{ enabled: jobId != null, intervalMs: 2000, isTerminal: (j) => isJobTerminal(j.status), onDone: (j) => onDone?.(j) },
|
|
79
|
+
);
|
|
80
|
+
const cancel = useG8Mutation(() => g8Post(`/enrichment/jobs/${jobId}/cancel`, {}));
|
|
81
|
+
const finished = job.status;
|
|
82
|
+
const nothingWritten = job.done && ((finished?.successful_enrichments ?? 0) === 0 || (finished?.failed_enrichments ?? 0) > 0);
|
|
83
|
+
const errors = useG8Query(
|
|
84
|
+
async (signal) => (await g8Get<EnrichJob>(`/enrichment/jobs/${jobId}`, { include_provider_errors: true }, { signal })).data.provider_errors ?? [],
|
|
85
|
+
[jobId, job.done],
|
|
86
|
+
{ enabled: job.done && nothingWritten },
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const toggleProvider = (name: string) => setChosen(providers.includes(name) ? providers.filter((p) => p !== name) : [...providers, name]);
|
|
90
|
+
const move = (name: string, delta: number) => {
|
|
91
|
+
const i = providers.indexOf(name), j = i + delta;
|
|
92
|
+
if (i < 0 || j < 0 || j >= providers.length) return;
|
|
93
|
+
const next = [...providers];
|
|
94
|
+
[next[i], next[j]] = [next[j], next[i]];
|
|
95
|
+
setChosen(next);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (jobId) {
|
|
99
|
+
const j = job.status;
|
|
100
|
+
const total = j?.total ?? runnable;
|
|
101
|
+
const pct = total ? Math.round(((j?.completed ?? 0) / total) * 100) : 0;
|
|
102
|
+
return (
|
|
103
|
+
<div className={`flex flex-col gap-3 ${className ?? ""}`} role="status">
|
|
104
|
+
<h3 className="text-base font-semibold">{job.done ? (j?.status === "completed" ? "Enrichment finished" : `Enrichment ${j?.status}`) : "Enriching..."}</h3>
|
|
105
|
+
<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
|
+
<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>
|
|
107
|
+
{j?.warnings?.map((w) => <p key={w} className="text-xs text-muted-foreground">{w}</p>)}
|
|
108
|
+
{job.error ? <ApiError error={job.error} title="Could not read the job" onRetry={() => job.start()} /> : null}
|
|
109
|
+
{errors.data?.length ? (
|
|
110
|
+
<ul className="text-xs text-destructive" aria-label="Provider errors">
|
|
111
|
+
{errors.data.slice(0, 5).map((e, i) => <li key={i}>{e.provider}: {e.error}</li>)}
|
|
112
|
+
</ul>
|
|
113
|
+
) : null}
|
|
114
|
+
{cancel.error ? <ApiError error={cancel.error} title="Could not cancel" /> : null}
|
|
115
|
+
<div className="flex gap-2">
|
|
116
|
+
{!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}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className={`flex flex-col gap-4 ${className ?? ""}`}>
|
|
125
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
126
|
+
<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)}>
|
|
128
|
+
{TARGET_FIELDS.map((f) => <option key={f.value} value={f.value}>{f.label}</option>)}
|
|
129
|
+
</select>
|
|
130
|
+
</label>
|
|
131
|
+
<span className="text-sm text-muted-foreground">for {ids.length.toLocaleString()} contact{ids.length === 1 ? "" : "s"}</span>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<fieldset className="flex flex-col gap-1">
|
|
135
|
+
<legend className="mb-1 text-sm font-medium">Providers, tried in this order</legend>
|
|
136
|
+
{catalog.error ? <ApiError error={catalog.error} title="Could not load providers" onRetry={() => void catalog.refetch()} /> : null}
|
|
137
|
+
{providers.map((p, i) => (
|
|
138
|
+
<div key={p} className="flex items-center gap-2 text-sm">
|
|
139
|
+
<span className="w-6 text-muted-foreground">{i + 1}.</span><span className="w-32 font-medium">{p}</span>
|
|
140
|
+
<Button variant="ghost" size="sm" aria-label={`Move ${p} up`} disabled={i === 0} onClick={() => move(p, -1)}>Up</Button>
|
|
141
|
+
<Button variant="ghost" size="sm" aria-label={`Move ${p} down`} disabled={i === providers.length - 1} onClick={() => move(p, 1)}>Down</Button>
|
|
142
|
+
<Button variant="ghost" size="sm" aria-label={`Remove ${p}`} onClick={() => toggleProvider(p)}>Remove</Button>
|
|
143
|
+
</div>
|
|
144
|
+
))}
|
|
145
|
+
<div className="flex flex-wrap gap-1">
|
|
146
|
+
{catalog.providers.filter((p) => !providers.includes(p.provider)).map((p) => (
|
|
147
|
+
<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
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
</fieldset>
|
|
151
|
+
|
|
152
|
+
{preflight.error ? <ApiError error={preflight.error} title="The check failed" onRetry={() => void preflight.refetch()} /> : null}
|
|
153
|
+
{quote ? (
|
|
154
|
+
<div className="rounded-md border p-3 text-sm" aria-label="Preflight">
|
|
155
|
+
<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
|
+
<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-amber-700">No API key for {quote.missing_credentials.join(", ")}: those steps are skipped. Add the keys in graph8 settings.</p> : null}
|
|
158
|
+
{quote.warnings.map((w) => <p key={w} className="mt-1 text-xs text-muted-foreground">{w}</p>)}
|
|
159
|
+
</div>
|
|
160
|
+
) : null}
|
|
161
|
+
{start.error ? <ApiError error={start.error} title="Could not start the enrichment" /> : null}
|
|
162
|
+
|
|
163
|
+
<div className="flex justify-end">
|
|
164
|
+
<Button disabled={ids.length === 0 || providers.length === 0 || preflight.loading || runnable === 0 || Boolean(preflight.error)} onClick={() => setConfirmOpen(true)}>
|
|
165
|
+
Enrich {runnable.toLocaleString()} contact{runnable === 1 ? "" : "s"}
|
|
166
|
+
</Button>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<ConfirmSpendDialog
|
|
170
|
+
open={confirmOpen}
|
|
171
|
+
tier="billable"
|
|
172
|
+
title={`Enrich ${runnable.toLocaleString()} contacts: ${column.label.toLowerCase()}`}
|
|
173
|
+
description={`Providers in order: ${providers.join(", ")}. The estimate is the most this run can charge; your balance is not checked here.`}
|
|
174
|
+
estimatedCredits={credits}
|
|
175
|
+
affected={runnable}
|
|
176
|
+
confirmLabel="Start enrichment"
|
|
177
|
+
confirming={start.loading}
|
|
178
|
+
onConfirm={() => void start.mutate(undefined)}
|
|
179
|
+
onCancel={() => setConfirmOpen(false)}
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Enrich the contacts of a list. Give it `listId` and `contactIds` (for example from a ContactTable selection), or
|
|
187
|
+
* mount it with no props as a page: choose a list, tick the contacts to enrich, then run.
|
|
188
|
+
*/
|
|
189
|
+
export function BulkEnrichPanel(props: BulkEnrichPanelProps) {
|
|
190
|
+
const [listId, setListId] = useState<number | undefined>(undefined);
|
|
191
|
+
const [picked, setPicked] = useState<number[]>([]);
|
|
192
|
+
if (props.listId != null && props.contactIds) return <BulkEnrichCore {...props} listId={props.listId} contactIds={props.contactIds} />;
|
|
193
|
+
const { listId: _l, contactIds: _c, ...rest } = props;
|
|
194
|
+
return (
|
|
195
|
+
<div className={`flex flex-col gap-4 ${props.className ?? ""}`}>
|
|
196
|
+
<ListPicker value={listId} allowCreate={false} onChange={(l) => { setListId(l.id); setPicked([]); }} />
|
|
197
|
+
{listId != null ? (
|
|
198
|
+
<>
|
|
199
|
+
<ContactTable listId={listId} selectable pageSize={10} onSelectionChange={(ids) => setPicked(ids)} />
|
|
200
|
+
<BulkEnrichCore {...rest} className={undefined} listId={listId} contactIds={picked} />
|
|
201
|
+
</>
|
|
202
|
+
) : <p className="text-sm text-muted-foreground">Choose a list to enrich its contacts.</p>}
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* EmailVerifyBadge: a small status chip next to an email address (Unverified, Valid, Risky, Invalid) with
|
|
5
|
+
* a "Verify" action.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. verifyEmail POST /enrichment/verify-email { email } (billable)
|
|
9
|
+
* Goes through ConfirmSpendDialog first: 1 credit per verification.
|
|
10
|
+
*
|
|
11
|
+
* A locked ("***") or missing email renders "Locked" / nothing and cannot be verified: the placeholder is
|
|
12
|
+
* never sent to the API. Reveal the contact first (save it to a list or enrich it).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useState } from "react";
|
|
16
|
+
import { Button } from "@graph8/sdk/ui";
|
|
17
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
18
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
19
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
20
|
+
import { g8Post } from "@/g8/lib/g8-client";
|
|
21
|
+
import { CREDITS_PER_EMAIL_VERIFICATION, isMaskedValue } from "../lib/enrichment-types";
|
|
22
|
+
|
|
23
|
+
export type VerifyState = "unverified" | "valid" | "risky" | "invalid";
|
|
24
|
+
|
|
25
|
+
export interface EmailVerifyBadgeProps {
|
|
26
|
+
email?: string | null;
|
|
27
|
+
/** A status you already know (for example from the contact's meta), so the badge starts verified. */
|
|
28
|
+
initialStatus?: string | null;
|
|
29
|
+
onVerified?: (result: { email: string; status: string; is_valid: boolean }) => void;
|
|
30
|
+
/** Hide the Verify button and only show the chip. */
|
|
31
|
+
readOnly?: boolean;
|
|
32
|
+
className?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Collapse the verifier's vocabulary into the four states the chip shows. */
|
|
36
|
+
export function classifyVerification(status: string | null | undefined, isValid?: boolean): VerifyState {
|
|
37
|
+
const s = String(status ?? "").toLowerCase().replace(/_/g, "-");
|
|
38
|
+
if (!s) return "unverified";
|
|
39
|
+
if (["catch-all", "unknown", "role-based", "accept-all"].includes(s)) return "risky";
|
|
40
|
+
if (s === "valid") return "valid";
|
|
41
|
+
if (["invalid", "do-not-mail", "spamtrap", "abuse", "disposable", "bounced"].includes(s)) return "invalid";
|
|
42
|
+
return isValid === true ? "valid" : "unverified"; // an unrecognised status is not proof of a bad address
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const STYLE: Record<VerifyState, string> = {
|
|
46
|
+
unverified: "bg-muted text-muted-foreground",
|
|
47
|
+
valid: "bg-emerald-100 text-emerald-800",
|
|
48
|
+
risky: "bg-amber-100 text-amber-800",
|
|
49
|
+
invalid: "bg-red-100 text-red-800",
|
|
50
|
+
};
|
|
51
|
+
const LABEL: Record<VerifyState, string> = { unverified: "Unverified", valid: "Valid", risky: "Risky", invalid: "Invalid" };
|
|
52
|
+
|
|
53
|
+
export function EmailVerifyBadge({ email, initialStatus, onVerified, readOnly, className }: EmailVerifyBadgeProps) {
|
|
54
|
+
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
55
|
+
// The verdict belongs to one address: keyed by email, so reusing the badge for another contact starts clean.
|
|
56
|
+
const [verdict, setVerdict] = useState<{ email: string; state: VerifyState; detail: string | null } | null>(null);
|
|
57
|
+
const mine = verdict && verdict.email === email ? verdict : null;
|
|
58
|
+
const state = mine?.state ?? classifyVerification(initialStatus);
|
|
59
|
+
const detail = mine?.detail ?? null;
|
|
60
|
+
|
|
61
|
+
const verify = useG8Mutation(() => g8Post<{ email: string; status: string; sub_status?: string | null; is_valid: boolean }>("/enrichment/verify-email", { email }), {
|
|
62
|
+
onSuccess: (res) => {
|
|
63
|
+
setConfirmOpen(false);
|
|
64
|
+
setVerdict({ email: res.data.email ?? String(email), state: classifyVerification(res.data.status, res.data.is_valid), detail: [res.data.status, res.data.sub_status].filter(Boolean).join(": ") });
|
|
65
|
+
onVerified?.(res.data);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (!email) return null;
|
|
70
|
+
if (isMaskedValue(email)) return <span className={`inline-flex rounded-full px-2 py-0.5 text-xs italic ${STYLE.unverified} ${className ?? ""}`} title="Locked: save to a list or enrich to reveal">Locked</span>;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<span className={`inline-flex items-center gap-1 ${className ?? ""}`}>
|
|
74
|
+
<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}
|
|
76
|
+
{verify.error ? <ApiError error={verify.error} title="Could not verify" /> : null}
|
|
77
|
+
<ConfirmSpendDialog open={confirmOpen} tier="billable" title={`Verify ${email}`} description="Checks whether the address can receive mail."
|
|
78
|
+
estimatedCredits={CREDITS_PER_EMAIL_VERIFICATION} confirmLabel="Verify" confirming={verify.loading}
|
|
79
|
+
onConfirm={() => void verify.mutate(undefined)} onCancel={() => setConfirmOpen(false)} />
|
|
80
|
+
</span>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* EnrichButton: look up ONE person or company and (optionally) copy the result onto a CRM record.
|
|
5
|
+
* Drop it into ContactDetail's `renderActions` or a company page.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. lookupPerson | lookupCompany POST /enrichment/lookup/person | /enrichment/lookup/company (billable)
|
|
9
|
+
* Goes through ConfirmSpendDialog first: 2 credits, charged ONLY when a match is found (a miss is free).
|
|
10
|
+
* Person: email, or linkedin_url, or first_name + last_name + company_domain. Company: domain or name.
|
|
11
|
+
* 2. updateContact | updateCompany PATCH /contacts/{id} | /companies/{id} (write) only when the person
|
|
12
|
+
* clicks "Apply" and only for the fields they ticked; nothing is written automatically.
|
|
13
|
+
*
|
|
14
|
+
* Apply is opt-in per field: fields the record already has are shown unticked so a lookup never silently
|
|
15
|
+
* overwrites good data. A locked ("***") value on the record counts as empty.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useState } from "react";
|
|
19
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@graph8/sdk/ui";
|
|
20
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
21
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
22
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
23
|
+
import { g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
24
|
+
import { CREDITS_PER_FOUND_LOOKUP, isMaskedValue } from "../lib/enrichment-types";
|
|
25
|
+
|
|
26
|
+
export interface PersonLookup { email?: string; linkedin_url?: string; first_name?: string; last_name?: string; company_domain?: string }
|
|
27
|
+
export interface CompanyLookup { domain?: string; name?: string }
|
|
28
|
+
interface LookupResult { found: boolean; confidence: number; data: Record<string, unknown> | null }
|
|
29
|
+
|
|
30
|
+
export interface EnrichButtonProps {
|
|
31
|
+
kind: "person" | "company";
|
|
32
|
+
lookup: PersonLookup | CompanyLookup;
|
|
33
|
+
/** The CRM record the result can be applied to (its current values decide what is pre-ticked). */
|
|
34
|
+
record?: { id: number } & Record<string, unknown>;
|
|
35
|
+
label?: string;
|
|
36
|
+
variant?: "default" | "outline" | "secondary" | "ghost";
|
|
37
|
+
size?: "default" | "sm";
|
|
38
|
+
onResult?: (result: LookupResult) => void;
|
|
39
|
+
/** Called after the chosen fields were written to the record. */
|
|
40
|
+
onApplied?: (applied: Record<string, string>) => void;
|
|
41
|
+
className?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const APPLY_KEYS: Record<EnrichButtonProps["kind"], string[]> = {
|
|
45
|
+
person: ["first_name", "last_name", "work_email", "direct_phone", "mobile_phone", "job_title", "job_department", "seniority_level", "linkedin_url", "city", "state", "country"],
|
|
46
|
+
company: ["name", "website", "phone", "industry", "employee_count", "linkedin_url", "city", "state", "country"],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Lookup keys come back lowercased from the index; company ones may carry a `company_` prefix. */
|
|
50
|
+
function normalise(kind: EnrichButtonProps["kind"], data: Record<string, unknown>): Record<string, string> {
|
|
51
|
+
const out: Record<string, string> = {};
|
|
52
|
+
for (const [k, v] of Object.entries(data)) {
|
|
53
|
+
const key = kind === "company" ? k.replace(/^company_/, "") : k;
|
|
54
|
+
if (APPLY_KEYS[kind].includes(key) && v != null && String(v).trim() !== "" && !isMaskedValue(v)) out[key] = String(v);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function hasIdentity(kind: EnrichButtonProps["kind"], l: PersonLookup & CompanyLookup): boolean {
|
|
60
|
+
return kind === "person" ? Boolean(l.email || l.linkedin_url || (l.first_name && l.last_name && l.company_domain)) : Boolean(l.domain || l.name);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function EnrichButton({ kind, lookup, record, label, variant = "outline", size = "sm", onResult, onApplied, className }: EnrichButtonProps) {
|
|
64
|
+
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
65
|
+
const [result, setResult] = useState<LookupResult | null>(null);
|
|
66
|
+
const [ticked, setTicked] = useState<Set<string>>(new Set());
|
|
67
|
+
const [applied, setApplied] = useState(false);
|
|
68
|
+
const clean = Object.fromEntries(Object.entries(lookup).filter(([, v]) => typeof v === "string" && v.trim() && !isMaskedValue(v)));
|
|
69
|
+
const ready = hasIdentity(kind, clean);
|
|
70
|
+
|
|
71
|
+
const run = useG8Mutation(() => g8Post<LookupResult>(`/enrichment/lookup/${kind}`, clean), {
|
|
72
|
+
onSuccess: (res) => {
|
|
73
|
+
setConfirmOpen(false);
|
|
74
|
+
setResult(res.data);
|
|
75
|
+
setApplied(false);
|
|
76
|
+
const fields = res.data.data ? normalise(kind, res.data.data) : {};
|
|
77
|
+
setTicked(new Set(Object.keys(fields).filter((k) => !record || !record[k] || isMaskedValue(record[k]))));
|
|
78
|
+
onResult?.(res.data);
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
const apply = useG8Mutation((values: Record<string, string>) => g8Patch(`/${kind === "person" ? "contacts" : "companies"}/${record!.id}`, values), {
|
|
82
|
+
onSuccess: (_r, values) => { setApplied(true); onApplied?.(values); },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const fields = result?.data ? normalise(kind, result.data) : {};
|
|
86
|
+
const chosen = Object.fromEntries(Object.entries(fields).filter(([k]) => ticked.has(k)));
|
|
87
|
+
const noun = kind === "person" ? "person" : "company";
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<>
|
|
91
|
+
<Button className={className} variant={variant} size={size} disabled={!ready} title={ready ? undefined : `Needs ${kind === "person" ? "an email, a LinkedIn URL, or a name plus company domain" : "a domain or a name"}`} onClick={() => setConfirmOpen(true)}>
|
|
92
|
+
{label ?? `Enrich ${noun}`}
|
|
93
|
+
</Button>
|
|
94
|
+
|
|
95
|
+
<ConfirmSpendDialog
|
|
96
|
+
open={confirmOpen}
|
|
97
|
+
tier="billable"
|
|
98
|
+
title={`Look up this ${noun}`}
|
|
99
|
+
description={`Charged only if a match is found. A lookup that finds nothing costs nothing.`}
|
|
100
|
+
estimatedCredits={CREDITS_PER_FOUND_LOOKUP}
|
|
101
|
+
confirmLabel="Look up"
|
|
102
|
+
confirming={run.loading}
|
|
103
|
+
onConfirm={() => void run.mutate(undefined)}
|
|
104
|
+
onCancel={() => setConfirmOpen(false)}
|
|
105
|
+
/>
|
|
106
|
+
|
|
107
|
+
<Dialog open={result != null} onOpenChange={(o) => !o && (setResult(null), run.reset(), apply.reset())}>
|
|
108
|
+
<DialogContent>
|
|
109
|
+
<DialogHeader>
|
|
110
|
+
<DialogTitle>{result?.found ? `Found this ${noun}` : "No match found"}</DialogTitle>
|
|
111
|
+
<DialogDescription>{result?.found ? `Confidence ${Math.round((result.confidence ?? 0) * 100)}%. About ${CREDITS_PER_FOUND_LOOKUP} credits were used (current pricing).` : "Nothing was charged."}</DialogDescription>
|
|
112
|
+
</DialogHeader>
|
|
113
|
+
{result?.found ? (
|
|
114
|
+
<ul className="flex flex-col gap-1 text-sm" aria-label="Found fields">
|
|
115
|
+
{Object.entries(fields).map(([k, v]) => (
|
|
116
|
+
<li key={k} className="flex items-center gap-2">
|
|
117
|
+
{record ? <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
|
+
<span className="w-36 text-muted-foreground">{k.replace(/_/g, " ")}</span><span className="truncate">{v}</span>
|
|
119
|
+
{record && record[k] && !isMaskedValue(record[k]) && record[k] !== v ? <span className="text-xs text-amber-600">replaces {String(record[k])}</span> : null}
|
|
120
|
+
</li>
|
|
121
|
+
))}
|
|
122
|
+
{Object.keys(fields).length === 0 ? <li className="text-muted-foreground">The match had no fields to show.</li> : null}
|
|
123
|
+
</ul>
|
|
124
|
+
) : null}
|
|
125
|
+
{apply.error ? <ApiError error={apply.error} title="Could not update the record" /> : null}
|
|
126
|
+
{applied ? <p role="status" className="text-sm">Updated the record.</p> : null}
|
|
127
|
+
<DialogFooter>
|
|
128
|
+
<Button variant="ghost" onClick={() => setResult(null)}>Close</Button>
|
|
129
|
+
{result?.found && record && !applied ? <Button loading={apply.loading} disabled={Object.keys(chosen).length === 0} onClick={() => void apply.mutate(chosen)}>Apply {Object.keys(chosen).length} field{Object.keys(chosen).length === 1 ? "" : "s"}</Button> : null}
|
|
130
|
+
</DialogFooter>
|
|
131
|
+
</DialogContent>
|
|
132
|
+
</Dialog>
|
|
133
|
+
{run.error ? <ApiError error={run.error} title="Lookup failed" /> : null}
|
|
134
|
+
</>
|
|
135
|
+
);
|
|
136
|
+
}
|