@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
|
@@ -20,6 +20,8 @@ import { ApiError } from "@/g8/components/api-error";
|
|
|
20
20
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
21
21
|
import { formatDate, rowsOf, strengthClass, totalOf } from "../lib/intent-utils";
|
|
22
22
|
import type { AbmAccount } from "../lib/intent-utils";
|
|
23
|
+
import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
24
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
23
25
|
|
|
24
26
|
export interface IntentFeedProps {
|
|
25
27
|
/** Look-back window in days. Default 30. */
|
|
@@ -57,7 +59,7 @@ function UrlLookup() {
|
|
|
57
59
|
{ enabled: submitted != null },
|
|
58
60
|
);
|
|
59
61
|
return (
|
|
60
|
-
<section aria-label="URL lookup" className="rounded border border-
|
|
62
|
+
<section aria-label="URL lookup" className="rounded border border-border p-3">
|
|
61
63
|
<form
|
|
62
64
|
className="flex gap-2"
|
|
63
65
|
onSubmit={(e) => {
|
|
@@ -74,13 +76,13 @@ function UrlLookup() {
|
|
|
74
76
|
{result.error ? <ApiError error={result.error} onRetry={() => void result.refetch()} /> : null}
|
|
75
77
|
{submitted && !result.loading && !result.error ? (
|
|
76
78
|
(result.data ?? []).length === 0 ? (
|
|
77
|
-
<p className="mt-3 text-sm text-
|
|
79
|
+
<p className="mt-3 text-sm text-muted-foreground">No identified companies visited this URL in the last 30 days.</p>
|
|
78
80
|
) : (
|
|
79
81
|
<ul className="mt-3 divide-y text-sm">
|
|
80
82
|
{(result.data ?? []).map((c) => (
|
|
81
83
|
<li key={`${c.company_id}-${c.domain}`} className="flex justify-between py-1.5">
|
|
82
84
|
<span>{c.name ?? c.domain}</span>
|
|
83
|
-
<span className="text-
|
|
85
|
+
<span className="text-muted-foreground">
|
|
84
86
|
{c.visit_count} visits, {c.contacts_seen} people
|
|
85
87
|
</span>
|
|
86
88
|
</li>
|
|
@@ -93,6 +95,7 @@ function UrlLookup() {
|
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
export function IntentFeed({ days = 30, keywordIds, pageSize = 25, refreshSeconds, onSelectAccount, className }: IntentFeedProps) {
|
|
98
|
+
const brand = useBrand();
|
|
96
99
|
const [highOnly, setHighOnly] = useState(false);
|
|
97
100
|
const keywordKey = (keywordIds ?? []).join(",");
|
|
98
101
|
// The page belongs to one set of inputs: when days, keywords or page size change, start again at page 1.
|
|
@@ -129,8 +132,8 @@ export function IntentFeed({ days = 30, keywordIds, pageSize = 25, refreshSecond
|
|
|
129
132
|
</div>
|
|
130
133
|
|
|
131
134
|
{feed.error && status === 403 ? (
|
|
132
|
-
<p role="alert" className="rounded border border-
|
|
133
|
-
Account intent is not enabled for this workspace. Ask
|
|
135
|
+
<p role="alert" className="rounded border border-warning-foreground/25 bg-warning p-3 text-sm text-warning-foreground">
|
|
136
|
+
Account intent is not enabled for this workspace. Ask {brand.linkLabel} to switch on ABM intent.
|
|
134
137
|
</p>
|
|
135
138
|
) : feed.error ? (
|
|
136
139
|
<ApiError error={feed.error} onRetry={() => void feed.refetch()} />
|
|
@@ -160,20 +163,20 @@ export function IntentFeed({ days = 30, keywordIds, pageSize = 25, refreshSecond
|
|
|
160
163
|
<TableRow
|
|
161
164
|
key={String(a.global_company_id ?? a.company_id ?? a.company_domain)}
|
|
162
165
|
className={onSelectAccount ? "cursor-pointer" : undefined}
|
|
163
|
-
|
|
166
|
+
{...(onSelectAccount ? rowActivationProps(() => onSelectAccount(a)) : {})}
|
|
164
167
|
>
|
|
165
168
|
<TableCell>
|
|
166
169
|
<div className="font-medium">{a.company_name ?? a.company_domain ?? "Unknown company"}</div>
|
|
167
|
-
<div className="text-xs text-
|
|
170
|
+
<div className="text-xs text-muted-foreground">{a.company_domain}</div>
|
|
168
171
|
</TableCell>
|
|
169
172
|
<TableCell>
|
|
170
173
|
<span className={`rounded px-2 py-0.5 text-xs ${strengthClass(a.account_strength)}`}>{a.account_strength ?? "-"}</span>
|
|
171
|
-
{a.is_high_intent ? <span className="ml-1 text-xs font-medium text-
|
|
174
|
+
{a.is_high_intent ? <span className="ml-1 text-xs font-medium text-error-foreground">High intent</span> : null}
|
|
172
175
|
</TableCell>
|
|
173
176
|
<TableCell>
|
|
174
177
|
{a.total_signal_count ?? 0}
|
|
175
178
|
{typeof a.trend_pct === "number" ? (
|
|
176
|
-
<span className="ml-1 text-xs text-
|
|
179
|
+
<span className="ml-1 text-xs text-muted-foreground">
|
|
177
180
|
({a.trend_pct > 0 ? "+" : ""}
|
|
178
181
|
{Math.round(a.trend_pct)}%)
|
|
179
182
|
</span>
|
|
@@ -192,7 +195,7 @@ export function IntentFeed({ days = 30, keywordIds, pageSize = 25, refreshSecond
|
|
|
192
195
|
</Table>
|
|
193
196
|
)}
|
|
194
197
|
|
|
195
|
-
<div className="flex items-center justify-between text-sm text-
|
|
198
|
+
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
196
199
|
<span>{total != null ? `${total.toLocaleString()} accounts` : ""}</span>
|
|
197
200
|
<div className="flex gap-2">
|
|
198
201
|
<Button variant="outline" size="sm" disabled={page <= 1 || feed.loading} onClick={() => setPage((p) => p - 1)}>
|
|
@@ -28,12 +28,15 @@ import type { ConfirmTier } from "@/g8/components/confirm-spend-dialog";
|
|
|
28
28
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
29
29
|
import { rowsOf } from "../lib/intent-utils";
|
|
30
30
|
import type { IntentKeywordRow } from "../lib/intent-utils";
|
|
31
|
+
import { plural } from "@/g8/lib/plural";
|
|
31
32
|
|
|
32
33
|
export interface IntentKeywordManagerProps {
|
|
33
34
|
/** Ask before listing (the list is billable in the contract). Default true. */
|
|
34
35
|
confirmListing?: boolean;
|
|
35
36
|
pageSize?: number;
|
|
36
37
|
onSelectKeyword?: (keyword: IntentKeywordRow) => void;
|
|
38
|
+
/** Keyword ids to hide (for example one that was just removed from its detail screen). */
|
|
39
|
+
hiddenIds?: readonly string[];
|
|
37
40
|
className?: string;
|
|
38
41
|
}
|
|
39
42
|
|
|
@@ -76,7 +79,7 @@ function describe(p: Pending): { tier: ConfirmTier; title: string; description:
|
|
|
76
79
|
case "add":
|
|
77
80
|
return {
|
|
78
81
|
tier: "billable",
|
|
79
|
-
title: `Track ${p.keywords.length
|
|
82
|
+
title: `Track ${plural(p.keywords.length, "keyword")}`,
|
|
80
83
|
description: p.keywords.join(", "),
|
|
81
84
|
confirmLabel: "Track keywords",
|
|
82
85
|
};
|
|
@@ -114,7 +117,7 @@ function describe(p: Pending): { tier: ConfirmTier; title: string; description:
|
|
|
114
117
|
const companyName = (c: CompanyRow) =>
|
|
115
118
|
c.name ?? c.company_name ?? c.COMPANY_NAME ?? c.domain ?? c.company_domain ?? c.COMPANY_DOMAIN ?? "Unknown";
|
|
116
119
|
|
|
117
|
-
export function IntentKeywordManager({ confirmListing = true, pageSize = 50, onSelectKeyword, className }: IntentKeywordManagerProps) {
|
|
120
|
+
export function IntentKeywordManager({ confirmListing = true, pageSize = 50, onSelectKeyword, hiddenIds, className }: IntentKeywordManagerProps) {
|
|
118
121
|
const [rows, setRows] = useState<IntentKeywordRow[] | null>(null);
|
|
119
122
|
const [pending, setPending] = useState<Pending | null>(null);
|
|
120
123
|
const [keywordText, setKeywordText] = useState("");
|
|
@@ -172,6 +175,7 @@ export function IntentKeywordManager({ confirmListing = true, pageSize = 50, onS
|
|
|
172
175
|
// Load once on mount when listing needs no confirmation.
|
|
173
176
|
}, [confirmListing]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
174
177
|
|
|
178
|
+
const visibleRows = rows == null ? null : rows.filter((k) => !(hiddenIds ?? []).includes(k.id));
|
|
175
179
|
const confirm = pending ? describe(pending) : null;
|
|
176
180
|
const words = keywordText
|
|
177
181
|
.split(/[\n,]/)
|
|
@@ -181,8 +185,8 @@ export function IntentKeywordManager({ confirmListing = true, pageSize = 50, onS
|
|
|
181
185
|
return (
|
|
182
186
|
<div className={`space-y-4 ${className ?? ""}`}>
|
|
183
187
|
<div className="grid gap-3 md:grid-cols-2">
|
|
184
|
-
<section className="space-y-2 rounded border border-
|
|
185
|
-
<h3 className="text-sm font-
|
|
188
|
+
<section className="space-y-2 rounded border border-border p-3">
|
|
189
|
+
<h3 className="text-sm font-medium">Track keywords</h3>
|
|
186
190
|
<Textarea
|
|
187
191
|
aria-label="Keywords"
|
|
188
192
|
rows={3}
|
|
@@ -194,8 +198,8 @@ export function IntentKeywordManager({ confirmListing = true, pageSize = 50, onS
|
|
|
194
198
|
Track keywords
|
|
195
199
|
</Button>
|
|
196
200
|
</section>
|
|
197
|
-
<section className="space-y-2 rounded border border-
|
|
198
|
-
<h3 className="text-sm font-
|
|
201
|
+
<section className="space-y-2 rounded border border-border p-3">
|
|
202
|
+
<h3 className="text-sm font-medium">Track a competitor domain</h3>
|
|
199
203
|
<Input
|
|
200
204
|
aria-label="Competitor domain"
|
|
201
205
|
placeholder="competitor.com"
|
|
@@ -224,7 +228,7 @@ export function IntentKeywordManager({ confirmListing = true, pageSize = 50, onS
|
|
|
224
228
|
</Button>
|
|
225
229
|
</div>
|
|
226
230
|
{domainResult ? (
|
|
227
|
-
<p className="text-xs text-
|
|
231
|
+
<p className="text-xs text-muted-foreground" role="status">
|
|
228
232
|
{domainResult.dry_run ? "Preview: " : "Saved: "}
|
|
229
233
|
{domainResult.pages_seeded ?? 0} pages, {domainResult.contacts_seeded ?? 0} people, {domainResult.companies_seeded ?? 0}{" "}
|
|
230
234
|
companies. {domainResult.message}
|
|
@@ -245,7 +249,7 @@ export function IntentKeywordManager({ confirmListing = true, pageSize = 50, onS
|
|
|
245
249
|
action={<Button onClick={() => setPending({ kind: "list" })}>Load keywords</Button>}
|
|
246
250
|
/>
|
|
247
251
|
)
|
|
248
|
-
) :
|
|
252
|
+
) : (visibleRows ?? []).length === 0 ? (
|
|
249
253
|
<EmptyState
|
|
250
254
|
title="No tracked keywords yet"
|
|
251
255
|
description="Add a keyword or a competitor domain above. Accounts that visit the matching pages show up in the intent feed."
|
|
@@ -267,7 +271,7 @@ export function IntentKeywordManager({ confirmListing = true, pageSize = 50, onS
|
|
|
267
271
|
</TableRow>
|
|
268
272
|
</TableHeader>
|
|
269
273
|
<TableBody>
|
|
270
|
-
{
|
|
274
|
+
{(visibleRows ?? []).map((k) => (
|
|
271
275
|
<TableRow key={k.id}>
|
|
272
276
|
<TableCell>
|
|
273
277
|
<button type="button" className="font-medium underline-offset-2 hover:underline" onClick={() => onSelectKeyword?.(k)}>
|
|
@@ -297,10 +301,10 @@ export function IntentKeywordManager({ confirmListing = true, pageSize = 50, onS
|
|
|
297
301
|
)}
|
|
298
302
|
|
|
299
303
|
{companies ? (
|
|
300
|
-
<section className="rounded border border-
|
|
301
|
-
<h3 className="text-sm font-
|
|
304
|
+
<section className="rounded border border-border p-3" aria-label="Keyword companies">
|
|
305
|
+
<h3 className="text-sm font-medium">Companies interested in "{companies.keyword}"</h3>
|
|
302
306
|
{companies.rows.length === 0 ? (
|
|
303
|
-
<p className="mt-2 text-sm text-
|
|
307
|
+
<p className="mt-2 text-sm text-muted-foreground">No resolved companies yet.</p>
|
|
304
308
|
) : (
|
|
305
309
|
<ul className="mt-2 grid gap-1 text-sm md:grid-cols-2">
|
|
306
310
|
{companies.rows.map((c, i) => (
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* IntentScreen: the finished buyer-intent product page (Apollo / 6sense "intent"). Two tabs:
|
|
5
|
+
* Accounts accounts showing intent, strongest first (IntentFeed), a look-back window filter, a keyword scope chip,
|
|
6
|
+
* and the selected account's detail panel (signals, trend, top keywords, buying-committee coverage).
|
|
7
|
+
* Keywords track keywords and competitor domains (IntentKeywordManager). Each keyword opens its complete detail screen
|
|
8
|
+
* (KeywordDetail: fields, accounts showing intent for it, companies behind it, stop tracking; `?id=`).
|
|
9
|
+
* Every detail also opens as a full page with `?view=full`.
|
|
10
|
+
*
|
|
11
|
+
* User flows: see who is showing intent, filter to high intent, change the window, open an account (deep link
|
|
12
|
+
* `?id=<company id>`), read its buying committee, look up who visited a URL, track keywords, preview and track a
|
|
13
|
+
* competitor domain, list the companies behind a keyword, stop tracking (destructive, confirmed).
|
|
14
|
+
*
|
|
15
|
+
* SDK operations: none of its own. Composes IntentFeed (read), AccountIntentScore / useAccountIntent (read),
|
|
16
|
+
* IntentKeywordManager (billable + destructive, every call confirmed) and one read of listAbmCompanies for the stat strip.
|
|
17
|
+
* Prerequisite: ABM intent enabled for the workspace (a 403 is explained inline), and something to measure.
|
|
18
|
+
*/
|
|
19
|
+
import { useState } from "react";
|
|
20
|
+
import type { ReactNode } from "react";
|
|
21
|
+
import { Button } from "@graph8/sdk/ui";
|
|
22
|
+
import { Radar, Tags, X } from "lucide-react";
|
|
23
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
24
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
25
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
26
|
+
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
27
|
+
import type { StatItem } from "@/g8/components/screen/stat-strip";
|
|
28
|
+
import { SplitView } from "@/g8/components/screen/split-view";
|
|
29
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
30
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
31
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
32
|
+
import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
33
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
34
|
+
import { EntityAvatar } from "@/g8/components/screen/entity-avatar";
|
|
35
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
36
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
37
|
+
import { BarList } from "@/g8/components/screen/bar-list";
|
|
38
|
+
import { useAccountIntent } from "../hooks/use-account-intent";
|
|
39
|
+
import { formatDate, rowsOf, totalOf } from "../lib/intent-utils";
|
|
40
|
+
import type { AbmAccount, IntentKeywordRow } from "../lib/intent-utils";
|
|
41
|
+
import { AccountIntentScore } from "./account-intent-score";
|
|
42
|
+
import { IntentFeed } from "./intent-feed";
|
|
43
|
+
import { IntentKeywordManager } from "./intent-keyword-manager";
|
|
44
|
+
import { KeywordDetail } from "./keyword-detail";
|
|
45
|
+
|
|
46
|
+
export interface IntentScreenProps {
|
|
47
|
+
/** Extra buttons for the toolbar (for example "Add to list" glue from another block). */
|
|
48
|
+
toolbarActions?: ReactNode;
|
|
49
|
+
/** Buttons shown in the account detail header. */
|
|
50
|
+
renderDetailActions?: (account: AbmAccount) => ReactNode;
|
|
51
|
+
/** Extra tabs in the account detail panel. */
|
|
52
|
+
extraDetailTabs?: { id: string; label: string; render: (account: AbmAccount) => ReactNode }[];
|
|
53
|
+
className?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const WINDOWS = [7, 30, 90] as const;
|
|
57
|
+
type Tab = "accounts" | "keywords";
|
|
58
|
+
|
|
59
|
+
export function strengthTone(strength: string | null | undefined): "positive" | "warning" | "neutral" {
|
|
60
|
+
const s = (strength ?? "").toLowerCase();
|
|
61
|
+
if (s.includes("strong") || s === "high") return "positive";
|
|
62
|
+
if (s.includes("moderate") || s === "medium") return "warning";
|
|
63
|
+
return "neutral";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface PanelProps {
|
|
67
|
+
onClose: () => void;
|
|
68
|
+
onExpand?: () => void;
|
|
69
|
+
onCollapse?: () => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function AccountDetail({ account, panel, props }: { account: AbmAccount; panel: PanelProps; props: IntentScreenProps }) {
|
|
73
|
+
const name = account.company_name ?? account.company_domain ?? "Unknown company";
|
|
74
|
+
const keywords = (account.top_keywords ?? []).map((k) => ({ label: k.keyword ?? "keyword", value: k.signal_count ?? 0 }));
|
|
75
|
+
const trend = typeof account.trend_pct === "number" ? `${account.trend_pct > 0 ? "+" : ""}${Math.round(account.trend_pct)}%` : "No trend yet";
|
|
76
|
+
const tabs = [
|
|
77
|
+
{
|
|
78
|
+
id: "signals",
|
|
79
|
+
label: "Signals",
|
|
80
|
+
content: (
|
|
81
|
+
<div className="space-y-4">
|
|
82
|
+
<KeyValueList
|
|
83
|
+
columns={2}
|
|
84
|
+
items={[
|
|
85
|
+
{ label: "Domain", value: account.company_domain ?? "-" },
|
|
86
|
+
{ label: "Industry", value: account.industry ?? "-" },
|
|
87
|
+
{ label: "Employees", value: account.employee_count ?? "-" },
|
|
88
|
+
{ label: "Signals", value: account.total_signal_count ?? 0 },
|
|
89
|
+
{ label: "Trend", value: trend },
|
|
90
|
+
{ label: "Last signal", value: formatDate(account.latest_signal_date) },
|
|
91
|
+
]}
|
|
92
|
+
/>
|
|
93
|
+
<SectionCard title="Top keywords" description="Where this account's interest comes from.">
|
|
94
|
+
{keywords.length ? <BarList items={keywords} /> : <p className="text-sm text-muted-foreground">No keyword breakdown for this account.</p>}
|
|
95
|
+
</SectionCard>
|
|
96
|
+
</div>
|
|
97
|
+
),
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: "committee",
|
|
101
|
+
label: "Buying committee",
|
|
102
|
+
content:
|
|
103
|
+
account.company_id != null ? (
|
|
104
|
+
<AccountIntentScore companyIds={[account.company_id]} />
|
|
105
|
+
) : (
|
|
106
|
+
<p className="text-sm text-muted-foreground">This account has no company id to score.</p>
|
|
107
|
+
),
|
|
108
|
+
},
|
|
109
|
+
...(props.extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(account) })),
|
|
110
|
+
];
|
|
111
|
+
return (
|
|
112
|
+
<DetailPanel
|
|
113
|
+
title={name}
|
|
114
|
+
subtitle={account.company_domain ?? undefined}
|
|
115
|
+
avatar={<EntityAvatar name={name} imageUrl={account.logo_url ?? undefined} kind="company" size="lg" />}
|
|
116
|
+
badges={
|
|
117
|
+
<>
|
|
118
|
+
<StatusPill tone={strengthTone(account.account_strength)}>{account.account_strength ?? "unscored"}</StatusPill>
|
|
119
|
+
{account.is_high_intent ? <StatusPill tone="negative" dot>High intent</StatusPill> : null}
|
|
120
|
+
</>
|
|
121
|
+
}
|
|
122
|
+
actions={props.renderDetailActions?.(account)}
|
|
123
|
+
tabs={tabs}
|
|
124
|
+
onClose={panel.onClose}
|
|
125
|
+
onExpand={panel.onExpand}
|
|
126
|
+
onCollapse={panel.onCollapse}
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function useStats(days: number): { items: StatItem[] | undefined; loading: boolean } {
|
|
132
|
+
const q = useG8Query((signal) => g8Get("/intent/abm/companies", { days, page: 1, limit: 100 }, { signal }), [days]);
|
|
133
|
+
if (q.error || !q.data) return { items: undefined, loading: q.loading };
|
|
134
|
+
// The ABM payload is open-shaped in the contract: only report what the rows actually carry.
|
|
135
|
+
const rows = rowsOf<AbmAccount>(q.data);
|
|
136
|
+
const total = totalOf(q.data) ?? rows.length;
|
|
137
|
+
const items: StatItem[] = [{ label: "Accounts with intent", value: total.toLocaleString(), hint: `last ${days} days` }];
|
|
138
|
+
if (rows.some((r) => typeof r.is_high_intent === "boolean")) {
|
|
139
|
+
const high = rows.filter((r) => r.is_high_intent).length;
|
|
140
|
+
items.push({ label: "High intent", value: high, tone: high ? "negative" : "neutral", hint: "in the first 100" });
|
|
141
|
+
}
|
|
142
|
+
if (rows.some((r) => typeof r.account_strength === "string")) {
|
|
143
|
+
const strong = rows.filter((r) => strengthTone(r.account_strength) === "positive").length;
|
|
144
|
+
items.push({ label: "Strong accounts", value: strong, tone: strong ? "positive" : "neutral", hint: "in the first 100" });
|
|
145
|
+
}
|
|
146
|
+
return { loading: q.loading, items };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function IntentScreen(props: IntentScreenProps) {
|
|
150
|
+
const [tab, setTab] = useUrlState("tab", "accounts");
|
|
151
|
+
const [id, setId] = useUrlState("id", "");
|
|
152
|
+
const [view, setView] = useUrlState("view", "");
|
|
153
|
+
const [days, setDays] = useState<number>(30);
|
|
154
|
+
const [selected, setSelected] = useState<AbmAccount | null>(null);
|
|
155
|
+
const [keyword, setKeyword] = useState<IntentKeywordRow | null>(null);
|
|
156
|
+
// `?kw=<keyword id>` scopes the account feed to one tracked keyword (chip in the toolbar).
|
|
157
|
+
const [scopeId, setScopeId] = useUrlState("kw", "");
|
|
158
|
+
const [scopeLabel, setScopeLabel] = useState<string | null>(null);
|
|
159
|
+
const [removed, setRemoved] = useState<string[]>([]);
|
|
160
|
+
const stats = useStats(days);
|
|
161
|
+
const active: Tab = tab === "keywords" ? "keywords" : "accounts";
|
|
162
|
+
const expanded = view === "full" && Boolean(id);
|
|
163
|
+
// A non-numeric `?id=` opens nothing instead of asking for account NaN.
|
|
164
|
+
const idNumber = id && Number.isFinite(Number(id)) ? Number(id) : null;
|
|
165
|
+
const deepLinked = useAccountIntent(active === "accounts" && idNumber != null && !selected ? [idNumber] : [], days);
|
|
166
|
+
const account = selected ?? (active === "accounts" && idNumber != null && deepLinked.rollups[id] ? { ...deepLinked.rollups[id], company_id: idNumber } : null);
|
|
167
|
+
// A deep-linked account with no signal in this window (or a bad id) gets a not-found state instead of a silent no-op.
|
|
168
|
+
const accountMissing = active === "accounts" && Boolean(id) && !account && (idNumber == null || (!deepLinked.loading && !deepLinked.error));
|
|
169
|
+
const accountFailed = active === "accounts" && Boolean(id) && !account && idNumber != null && Boolean(deepLinked.error);
|
|
170
|
+
|
|
171
|
+
const close = () => {
|
|
172
|
+
setSelected(null);
|
|
173
|
+
setKeyword(null);
|
|
174
|
+
setId("");
|
|
175
|
+
setView("");
|
|
176
|
+
};
|
|
177
|
+
const panel: PanelProps = {
|
|
178
|
+
onClose: close,
|
|
179
|
+
onExpand: expanded ? undefined : () => setView("full"),
|
|
180
|
+
onCollapse: expanded ? () => setView("") : undefined,
|
|
181
|
+
};
|
|
182
|
+
const showAccounts = (k: IntentKeywordRow) => {
|
|
183
|
+
close();
|
|
184
|
+
setScopeId(k.id);
|
|
185
|
+
setScopeLabel(k.keyword);
|
|
186
|
+
setTab("accounts");
|
|
187
|
+
};
|
|
188
|
+
const openAccount = (a: AbmAccount) => {
|
|
189
|
+
setTab("accounts");
|
|
190
|
+
setKeyword(null);
|
|
191
|
+
setSelected(a);
|
|
192
|
+
setId(String(a.company_id ?? ""));
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<ScreenLayout
|
|
197
|
+
title="Intent"
|
|
198
|
+
description="Accounts researching what you sell, and the keywords and competitors you track to find them."
|
|
199
|
+
icon={<Radar className="size-5" />}
|
|
200
|
+
actions={
|
|
201
|
+
<Button onClick={() => { setTab("keywords"); close(); }}>
|
|
202
|
+
<Tags className="mr-1.5 size-4" /> Track keywords
|
|
203
|
+
</Button>
|
|
204
|
+
}
|
|
205
|
+
tabs={[
|
|
206
|
+
{ id: "accounts", label: "Accounts" },
|
|
207
|
+
{ id: "keywords", label: "Keywords" },
|
|
208
|
+
]}
|
|
209
|
+
activeTab={active}
|
|
210
|
+
onTabChange={(next) => {
|
|
211
|
+
setTab(next);
|
|
212
|
+
close();
|
|
213
|
+
}}
|
|
214
|
+
stats={active === "accounts" ? stats.items : undefined}
|
|
215
|
+
className={props.className}
|
|
216
|
+
>
|
|
217
|
+
{active === "accounts" ? (
|
|
218
|
+
<div className="space-y-3">
|
|
219
|
+
<Toolbar actions={props.toolbarActions}>
|
|
220
|
+
{scopeId ? (
|
|
221
|
+
<Button size="sm" variant="outline" aria-label={`Remove keyword scope ${scopeLabel ?? scopeId}`} onClick={() => { setScopeId(null); setScopeLabel(null); }}>
|
|
222
|
+
Keyword: {scopeLabel ?? scopeId} <X className="ml-1 size-3.5" aria-hidden />
|
|
223
|
+
</Button>
|
|
224
|
+
) : null}
|
|
225
|
+
<div className="inline-flex gap-1" role="group" aria-label="Look-back window">
|
|
226
|
+
{WINDOWS.map((w) => (
|
|
227
|
+
<Button key={w} size="sm" variant={days === w ? "default" : "outline"} aria-pressed={days === w} onClick={() => setDays(w)}>
|
|
228
|
+
{w} days
|
|
229
|
+
</Button>
|
|
230
|
+
))}
|
|
231
|
+
</div>
|
|
232
|
+
</Toolbar>
|
|
233
|
+
<SplitView
|
|
234
|
+
detailOpen={Boolean(account) || accountMissing || accountFailed}
|
|
235
|
+
onCloseDetail={close}
|
|
236
|
+
expanded={expanded && Boolean(account)}
|
|
237
|
+
onExpandedChange={(next) => setView(next ? "full" : "")}
|
|
238
|
+
detailWidth="lg"
|
|
239
|
+
list={<IntentFeed key={scopeId || "all"} days={days} keywordIds={scopeId ? [scopeId] : undefined} onSelectAccount={openAccount} />}
|
|
240
|
+
detail={account ? (
|
|
241
|
+
<AccountDetail key={account.company_id ?? account.company_domain} account={account} panel={panel} props={props} />
|
|
242
|
+
) : accountFailed ? (
|
|
243
|
+
<div className="space-y-3 p-4"><ApiError error={deepLinked.error} title="Could not load this account's intent" onRetry={() => void deepLinked.refetch()} /><Button size="sm" variant="outline" onClick={close}>Close</Button></div>
|
|
244
|
+
) : accountMissing ? (
|
|
245
|
+
<div className="space-y-3 p-4"><EmptyState title="No intent for this account" description={`No buying-intent signal for this account in the last ${days} days. Try a longer window.`} /><Button size="sm" variant="outline" onClick={close}>Close</Button></div>
|
|
246
|
+
) : null}
|
|
247
|
+
/>
|
|
248
|
+
</div>
|
|
249
|
+
) : (
|
|
250
|
+
<SplitView
|
|
251
|
+
detailOpen={Boolean(id)}
|
|
252
|
+
onCloseDetail={close}
|
|
253
|
+
expanded={expanded}
|
|
254
|
+
onExpandedChange={(next) => setView(next ? "full" : "")}
|
|
255
|
+
detailWidth="lg"
|
|
256
|
+
list={<IntentKeywordManager hiddenIds={removed} onSelectKeyword={(k) => { setKeyword(k); setId(k.id); }} />}
|
|
257
|
+
detail={id ? (
|
|
258
|
+
<KeywordDetail key={id} keywordId={id} keyword={keyword?.id === id ? keyword : null} onLoaded={setKeyword}
|
|
259
|
+
onSelectAccount={openAccount} onShowAccounts={showAccounts} onRemoved={(kid) => setRemoved((r) => [...r, kid])} {...panel} />
|
|
260
|
+
) : null}
|
|
261
|
+
/>
|
|
262
|
+
)}
|
|
263
|
+
</ScreenLayout>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* KeywordDetail: the complete screen for one tracked keyword or competitor domain. Shows every field the list returns, the
|
|
5
|
+
* accounts currently showing intent for it (the intent feed scoped to this keyword), the companies resolved behind it, and the
|
|
6
|
+
* action to stop tracking. Pass the row from IntentKeywordManager; with only an id (a deep link) it offers to load the row.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. listAbmCompanies GET /intent/abm/companies?keyword_ids (read; inside IntentFeed)
|
|
10
|
+
* 2. keywordsList POST /intent/keywords/list (billable tier, confirmed; only to restore a row from an id)
|
|
11
|
+
* 3. keywordCompanies POST /intent/keywords/{id}/companies (billable, confirmed)
|
|
12
|
+
* 4. keywordsDelete DELETE /intent/keywords/{id} (destructive, confirmed; 404 counts as done)
|
|
13
|
+
*/
|
|
14
|
+
import { useState } from "react";
|
|
15
|
+
import { Button } from "@graph8/sdk/ui";
|
|
16
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
17
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
18
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
19
|
+
import { g8Delete, g8Post } from "@/g8/lib/g8-client";
|
|
20
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
21
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
22
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
23
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
24
|
+
import { rowsOf } from "../lib/intent-utils";
|
|
25
|
+
import type { AbmAccount, IntentKeywordRow } from "../lib/intent-utils";
|
|
26
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
27
|
+
import { AskButton, useConfirmAction } from "@/g8/components/confirmed-action";
|
|
28
|
+
import { IntentFeed } from "./intent-feed";
|
|
29
|
+
|
|
30
|
+
export interface KeywordDetailProps {
|
|
31
|
+
keywordId: string;
|
|
32
|
+
/** The row from the keyword list. Without it the screen can restore it after a confirmed list call. */
|
|
33
|
+
keyword?: IntentKeywordRow | null;
|
|
34
|
+
/** Called with the restored row. */
|
|
35
|
+
onLoaded?: (keyword: IntentKeywordRow) => void;
|
|
36
|
+
onSelectAccount?: (account: AbmAccount) => void;
|
|
37
|
+
onClose?: () => void;
|
|
38
|
+
onExpand?: () => void;
|
|
39
|
+
onCollapse?: () => void;
|
|
40
|
+
/** Called after the keyword was removed. */
|
|
41
|
+
onRemoved?: (keywordId: string) => void;
|
|
42
|
+
/** Shows a "Show accounts" button that scopes the account feed to this keyword. */
|
|
43
|
+
onShowAccounts?: (keyword: IntentKeywordRow) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface CompanyRow {
|
|
47
|
+
name?: string | null;
|
|
48
|
+
company_name?: string | null;
|
|
49
|
+
COMPANY_NAME?: string | null;
|
|
50
|
+
domain?: string | null;
|
|
51
|
+
company_domain?: string | null;
|
|
52
|
+
COMPANY_DOMAIN?: string | null;
|
|
53
|
+
}
|
|
54
|
+
const companyName = (c: CompanyRow) => c.name ?? c.company_name ?? c.COMPANY_NAME ?? c.domain ?? c.company_domain ?? c.COMPANY_DOMAIN ?? "Unknown";
|
|
55
|
+
|
|
56
|
+
export function KeywordDetail({ keywordId, keyword, onLoaded, onSelectAccount, onClose, onExpand, onCollapse, onRemoved, onShowAccounts }: KeywordDetailProps) {
|
|
57
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
58
|
+
const [companies, setCompanies] = useState<CompanyRow[] | null>(null);
|
|
59
|
+
const restore = useG8Mutation(async () => {
|
|
60
|
+
const found = rowsOf<IntentKeywordRow>(await g8Post("/intent/keywords/list", { page: 1, limit: 200 })).find((k) => k.id === keywordId);
|
|
61
|
+
if (found) onLoaded?.(found);
|
|
62
|
+
return found ?? null;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!keyword) {
|
|
66
|
+
return (
|
|
67
|
+
<div className="space-y-3 p-4">
|
|
68
|
+
{restore.data === null ? (
|
|
69
|
+
<EmptyState title="Keyword not found" description="It is no longer among your tracked keywords. It may have been removed." />
|
|
70
|
+
) : (
|
|
71
|
+
<EmptyState title="Keyword details are not loaded" description="Listing tracked keywords is a billable read, so it asks first." />
|
|
72
|
+
)}
|
|
73
|
+
<div className="flex justify-center">
|
|
74
|
+
<AskButton ask={ask} label="Load keyword" tier="billable" title="Load tracked keywords" confirmLabel="Load keywords" description="Listing tracked keywords is classified billable by the API."
|
|
75
|
+
run={async () => { await restore.mutateAsync(undefined); }} />
|
|
76
|
+
</div>
|
|
77
|
+
{/* A failed load surfaces once, through the confirm hook's `failure`. */}
|
|
78
|
+
{failure ? <ApiError error={failure} title="That did not work" /> : null}
|
|
79
|
+
<ConfirmSpendDialog {...dialog} />
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const actions = (
|
|
85
|
+
<div className="flex flex-wrap gap-1">
|
|
86
|
+
{onShowAccounts ? <Button size="sm" variant="outline" onClick={() => onShowAccounts(keyword)}>Show accounts</Button> : null}
|
|
87
|
+
<AskButton ask={ask} label="Load companies" tier="billable" title={`Load companies for "${keyword.keyword}"`} confirmLabel="Load companies" description="Resolves the companies behind this keyword's traffic."
|
|
88
|
+
run={async () => setCompanies(rowsOf<CompanyRow>(await g8Post(`/intent/keywords/${keyword.id}/companies`, { limit: 50, page: 1 })))} />
|
|
89
|
+
<AskButton ask={ask} label="Stop tracking" variant="ghost" tier="destructive" title={`Stop tracking "${keyword.keyword}"`} confirmLabel="Stop tracking" tolerateStatus={[404]}
|
|
90
|
+
description="The keyword is removed. Companies already saved to lists stay." run={() => g8Delete(`/intent/keywords/${keyword.id}`)} onDone={() => { onRemoved?.(keyword.id); onClose?.(); }} />
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
const tabs = [
|
|
94
|
+
{
|
|
95
|
+
id: "overview",
|
|
96
|
+
label: "Overview",
|
|
97
|
+
content: (
|
|
98
|
+
<div className="space-y-4">
|
|
99
|
+
<KeyValueList columns={2} items={[
|
|
100
|
+
{ label: "Keyword", value: keyword.keyword },
|
|
101
|
+
{ label: "Type", value: keyword.signal_type ?? "-" },
|
|
102
|
+
{ label: "Companies resolved", value: keyword.total_resolved_companies ?? 0 },
|
|
103
|
+
{ label: "People resolved", value: keyword.total_resolved_contacts ?? 0 },
|
|
104
|
+
{ label: "Pages tracked", value: keyword.total_urls ?? "-" },
|
|
105
|
+
{ label: "Auto sync", value: keyword.auto_sync_enabled == null ? "-" : keyword.auto_sync_enabled ? "on" : "off" },
|
|
106
|
+
]} />
|
|
107
|
+
<SectionCard title="Companies behind this keyword" description="Loading them is billable, so it asks first (Load companies above).">
|
|
108
|
+
{companies == null ? <p className="text-sm text-muted-foreground">Not loaded yet.</p> : companies.length === 0 ? <p className="text-sm text-muted-foreground">No resolved companies yet.</p> : (
|
|
109
|
+
<ul className="grid gap-1 text-sm md:grid-cols-2">{companies.map((c, i) => <li key={i}>{companyName(c)}</li>)}</ul>
|
|
110
|
+
)}
|
|
111
|
+
</SectionCard>
|
|
112
|
+
</div>
|
|
113
|
+
),
|
|
114
|
+
},
|
|
115
|
+
{ id: "accounts", label: "Accounts showing intent", content: <IntentFeed keywordIds={[keyword.id]} onSelectAccount={onSelectAccount} /> },
|
|
116
|
+
];
|
|
117
|
+
return (
|
|
118
|
+
<>
|
|
119
|
+
<DetailPanel title={keyword.keyword} subtitle="Tracked keyword" badges={<StatusPill tone={keyword.is_active === false ? "neutral" : "positive"}>{keyword.is_active === false ? "inactive" : "tracking"}</StatusPill>} actions={actions} tabs={tabs} onClose={onClose} onExpand={onExpand} onCollapse={onCollapse} />
|
|
120
|
+
{failure ? <ApiError error={failure} title="That did not work" /> : null}
|
|
121
|
+
<ConfirmSpendDialog {...dialog} />
|
|
122
|
+
</>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
@@ -88,8 +88,8 @@ export function formatDate(value: string | null | undefined): string {
|
|
|
88
88
|
/** Tailwind classes for an account-strength label. Unknown labels stay neutral. */
|
|
89
89
|
export function strengthClass(strength: string | null | undefined): string {
|
|
90
90
|
const s = (strength ?? "").toLowerCase();
|
|
91
|
-
if (s.includes("strong") || s === "high") return "bg-
|
|
92
|
-
if (s.includes("moderate") || s === "medium") return "bg-
|
|
93
|
-
if (s.includes("weak") || s === "low") return "bg-
|
|
94
|
-
return "bg-
|
|
91
|
+
if (s.includes("strong") || s === "high") return "bg-positive text-positive-foreground";
|
|
92
|
+
if (s.includes("moderate") || s === "medium") return "bg-warning text-warning-foreground";
|
|
93
|
+
if (s.includes("weak") || s === "low") return "bg-muted text-foreground";
|
|
94
|
+
return "bg-muted text-foreground";
|
|
95
95
|
}
|
|
@@ -13,17 +13,18 @@
|
|
|
13
13
|
"crm"
|
|
14
14
|
],
|
|
15
15
|
"operations": [
|
|
16
|
-
"
|
|
16
|
+
"get_account_intent_signals_accounts__account_id__intent_signals_get",
|
|
17
|
+
"get_brief_customer_success_companies__company_id__brief_get",
|
|
18
|
+
"get_company_companies__company_id__get",
|
|
17
19
|
"get_lifecycle_stage_counts_lifecycle_stages_counts_get",
|
|
20
|
+
"get_revenue_summary_accounts_revenue_summary_get",
|
|
21
|
+
"list_lifecycle_contacts_lifecycle_contacts_get",
|
|
18
22
|
"list_lifecycle_stage_records_lifecycle_stages__stage__records_get",
|
|
23
|
+
"list_lifecycle_stages_lifecycle_stages_get",
|
|
19
24
|
"move_company_lifecycle_stage_companies__company_id__lifecycle_stage_post",
|
|
20
25
|
"move_contact_lifecycle_stage_contacts__contact_id__lifecycle_stage_post",
|
|
21
|
-
"list_lifecycle_contacts_lifecycle_contacts_get",
|
|
22
|
-
"unlock_lifecycle_contacts_lifecycle_contacts_unlock_post",
|
|
23
|
-
"get_revenue_summary_accounts_revenue_summary_get",
|
|
24
|
-
"get_brief_customer_success_companies__company_id__brief_get",
|
|
25
26
|
"synthesize_brief_customer_success_companies__company_id__brief_synthesize_post",
|
|
26
|
-
"
|
|
27
|
+
"unlock_lifecycle_contacts_lifecycle_contacts_unlock_post"
|
|
27
28
|
],
|
|
28
29
|
"components": [
|
|
29
30
|
{
|
|
@@ -103,8 +104,12 @@
|
|
|
103
104
|
"name": "LifecycleScreen",
|
|
104
105
|
"file": "components/lifecycle-screen.tsx",
|
|
105
106
|
"purpose": "Finished lifecycle product: board, one tab per stage (contact queue, customers, churned) and a CRM record panel with Move stage.",
|
|
106
|
-
"operations": [
|
|
107
|
-
|
|
107
|
+
"operations": [
|
|
108
|
+
"get_company_companies__company_id__get"
|
|
109
|
+
],
|
|
110
|
+
"tiers": [
|
|
111
|
+
"read"
|
|
112
|
+
]
|
|
108
113
|
},
|
|
109
114
|
{
|
|
110
115
|
"name": "AccountBriefTab",
|