@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
|
@@ -18,9 +18,12 @@ import { useState } from "react";
|
|
|
18
18
|
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
19
19
|
import { g8Get } from "@/g8/lib/g8-client";
|
|
20
20
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
21
|
+
import { useAllPages } from "@/g8/hooks/use-all-pages";
|
|
22
|
+
import { hasNextOf } from "@/g8/lib/paging";
|
|
21
23
|
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
22
24
|
import { ApiError } from "@/g8/components/api-error";
|
|
23
25
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
26
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
24
27
|
|
|
25
28
|
export interface SnippetInstallProps {
|
|
26
29
|
/** Pre-select a stream id (from listOrgStreams). Default: the org's main stream. */
|
|
@@ -71,7 +74,7 @@ function CodeBlock({ label, code }: { label: string; code: string }) {
|
|
|
71
74
|
{copied ? "Copied" : "Copy"}
|
|
72
75
|
</Button>
|
|
73
76
|
</div>
|
|
74
|
-
<pre className="overflow-x-auto rounded bg-
|
|
77
|
+
<pre className="overflow-x-auto rounded bg-muted p-3 text-xs text-foreground">
|
|
75
78
|
<code>{code}</code>
|
|
76
79
|
</pre>
|
|
77
80
|
</div>
|
|
@@ -98,32 +101,40 @@ function InstallCheck({ writeKey, onInstalled }: { writeKey: string; onInstalled
|
|
|
98
101
|
</Button>
|
|
99
102
|
{check.error ? <ApiError error={check.error} /> : null}
|
|
100
103
|
{s?.status === "receiving" ? (
|
|
101
|
-
<p role="status" className="text-sm text-
|
|
104
|
+
<p role="status" className="text-sm text-positive-foreground">
|
|
102
105
|
Installed: {s.event_count.toLocaleString()} events received.
|
|
103
106
|
</p>
|
|
104
107
|
) : null}
|
|
105
108
|
{s?.status === "unknown" ? (
|
|
106
|
-
<p role="status" className="text-sm text-
|
|
109
|
+
<p role="status" className="text-sm text-warning-foreground">
|
|
107
110
|
Your workspace cannot be probed yet. Open your site once and check the Visitors feed.
|
|
108
111
|
</p>
|
|
109
112
|
) : null}
|
|
110
113
|
{s?.status === "no_events" && !check.polling ? (
|
|
111
|
-
<p role="status" className="text-sm text-
|
|
114
|
+
<p role="status" className="text-sm text-muted-foreground">
|
|
112
115
|
No events yet. Load a page that has the snippet, then check again.
|
|
113
116
|
</p>
|
|
114
117
|
) : null}
|
|
115
118
|
{s?.status === "no_events" && check.polling ? (
|
|
116
|
-
<p className="text-sm text-
|
|
119
|
+
<p className="text-sm text-muted-foreground">No events yet. Open a page that has the snippet.</p>
|
|
117
120
|
) : null}
|
|
118
121
|
</div>
|
|
119
122
|
);
|
|
120
123
|
}
|
|
121
124
|
|
|
125
|
+
/** Streams per read: every page is read, so the stream picker offers all of them. */
|
|
126
|
+
const STREAM_PAGE = 50;
|
|
127
|
+
|
|
122
128
|
export function SnippetInstall({ streamId, onInstalled, className }: SnippetInstallProps) {
|
|
129
|
+
const brand = useBrand();
|
|
123
130
|
const [selected, setSelected] = useState<string | undefined>(streamId);
|
|
124
|
-
const streams =
|
|
131
|
+
const streams = useAllPages<Stream>(async (page, signal) => {
|
|
132
|
+
const env = await g8Get<Stream[]>("/streams", { page, limit: STREAM_PAGE }, { signal });
|
|
133
|
+
const rows = env.data ?? [];
|
|
134
|
+
return { rows, hasNext: hasNextOf(env, STREAM_PAGE, rows.length) };
|
|
135
|
+
}, []);
|
|
125
136
|
const snippet = useG8Query((signal) => g8Get<SnippetPayload>("/snippet", { stream_id: selected }, { signal }), [selected]);
|
|
126
|
-
const list = streams.
|
|
137
|
+
const list = streams.items;
|
|
127
138
|
|
|
128
139
|
if (snippet.error) return <ApiError error={snippet.error} onRetry={() => void snippet.refetch()} className={className} />;
|
|
129
140
|
if (snippet.loading && !snippet.data) return <Skeleton className={`h-40 w-full ${className ?? ""}`} />;
|
|
@@ -132,7 +143,7 @@ export function SnippetInstall({ streamId, onInstalled, className }: SnippetInst
|
|
|
132
143
|
return (
|
|
133
144
|
<EmptyState
|
|
134
145
|
title="No tracking stream yet"
|
|
135
|
-
description=
|
|
146
|
+
description={`Create a website stream in ${brand.linkLabel} first, then come back for its snippet.`}
|
|
136
147
|
className={className}
|
|
137
148
|
/>
|
|
138
149
|
);
|
|
@@ -143,7 +154,7 @@ export function SnippetInstall({ streamId, onInstalled, className }: SnippetInst
|
|
|
143
154
|
<label className="flex items-center gap-2 text-sm">
|
|
144
155
|
Website
|
|
145
156
|
<select
|
|
146
|
-
className="rounded border border-
|
|
157
|
+
className="rounded border border-input px-2 py-1"
|
|
147
158
|
value={selected ?? s.stream_id ?? ""}
|
|
148
159
|
onChange={(e) => setSelected(e.target.value || undefined)}
|
|
149
160
|
>
|
|
@@ -157,7 +168,7 @@ export function SnippetInstall({ streamId, onInstalled, className }: SnippetInst
|
|
|
157
168
|
</label>
|
|
158
169
|
) : null}
|
|
159
170
|
{s.warning ? (
|
|
160
|
-
<p role="alert" className="rounded border border-
|
|
171
|
+
<p role="alert" className="rounded border border-warning-foreground/25 bg-warning p-2 text-sm text-warning-foreground">
|
|
161
172
|
{s.warning}
|
|
162
173
|
</p>
|
|
163
174
|
) : null}
|
|
@@ -62,7 +62,7 @@ export function LineChart({ points, metric }: { points: TrendPoint[]; metric: Me
|
|
|
62
62
|
const y = (v: number) => height - (v / max) * (height - 12) - 6;
|
|
63
63
|
const path = values.map((v, i) => `${i === 0 ? "M" : "L"}${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" ");
|
|
64
64
|
return (
|
|
65
|
-
<svg viewBox={`0 0 ${width} ${height}`} role="img" aria-label={`${metric} per day`} className="h-40 w-full text-
|
|
65
|
+
<svg viewBox={`0 0 ${width} ${height}`} role="img" aria-label={`${metric} per day`} className="h-40 w-full text-foreground">
|
|
66
66
|
<path d={path} fill="none" stroke="currentColor" strokeWidth="2" />
|
|
67
67
|
{values.map((v, i) => (
|
|
68
68
|
<circle key={points[i].date} cx={x(i)} cy={y(v)} r="2.5" fill="currentColor">
|
|
@@ -113,7 +113,7 @@ export function TrafficChart({ days = 30, writeKey, realtimeSeconds = 10, classN
|
|
|
113
113
|
</Button>
|
|
114
114
|
))}
|
|
115
115
|
</div>
|
|
116
|
-
<p className="text-sm text-
|
|
116
|
+
<p className="text-sm text-muted-foreground" aria-live="polite">
|
|
117
117
|
{now != null ? `${now} on the site now` : ""}
|
|
118
118
|
</p>
|
|
119
119
|
</div>
|
|
@@ -126,7 +126,7 @@ export function TrafficChart({ days = 30, writeKey, realtimeSeconds = 10, classN
|
|
|
126
126
|
<EmptyState title="No traffic yet" description="Install the snippet and visits will chart here." />
|
|
127
127
|
) : (
|
|
128
128
|
<div>
|
|
129
|
-
<p className="text-sm text-
|
|
129
|
+
<p className="text-sm text-muted-foreground">
|
|
130
130
|
{total.toLocaleString()} {METRICS.find((m) => m.key === metric)?.label.toLowerCase()} in {days} days
|
|
131
131
|
</p>
|
|
132
132
|
<LineChart points={points} metric={metric} />
|
|
@@ -136,7 +136,7 @@ export function TrafficChart({ days = 30, writeKey, realtimeSeconds = 10, classN
|
|
|
136
136
|
{topPages.length > 0 ? (
|
|
137
137
|
<table className="w-full text-sm">
|
|
138
138
|
<thead>
|
|
139
|
-
<tr className="text-left text-
|
|
139
|
+
<tr className="text-left text-muted-foreground">
|
|
140
140
|
<th className="py-1">Top pages</th>
|
|
141
141
|
<th className="py-1 text-right">Views</th>
|
|
142
142
|
</tr>
|
|
@@ -17,10 +17,13 @@ import { useState } from "react";
|
|
|
17
17
|
import type { ReactNode } from "react";
|
|
18
18
|
import { Button, Card, CardContent, CardHeader, CardTitle, Skeleton } from "@graph8/sdk/ui";
|
|
19
19
|
import { g8Get } from "@/g8/lib/g8-client";
|
|
20
|
-
import {
|
|
20
|
+
import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
|
|
21
|
+
import { byPage, hasNextOf } from "@/g8/lib/paging";
|
|
22
|
+
import { LoadMore } from "@/g8/components/paging-notices";
|
|
21
23
|
import { ApiError } from "@/g8/components/api-error";
|
|
22
24
|
import { companyLabel, pageLabel, pageOf, timeAgo } from "../lib/visitors-utils";
|
|
23
25
|
import type { FlowEvent, VisitorGroup } from "../lib/visitors-utils";
|
|
26
|
+
import { plural } from "@/g8/lib/plural";
|
|
24
27
|
|
|
25
28
|
export interface VisitorCompanyCardProps {
|
|
26
29
|
visit: VisitorGroup;
|
|
@@ -31,33 +34,41 @@ export interface VisitorCompanyCardProps {
|
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
function PageList({ pages }: { pages: FlowEvent[] }) {
|
|
34
|
-
if (pages.length === 0) return <p className="text-sm text-
|
|
37
|
+
if (pages.length === 0) return <p className="text-sm text-muted-foreground">No page views recorded.</p>;
|
|
35
38
|
return (
|
|
36
39
|
<ol className="space-y-1 text-sm">
|
|
37
40
|
{pages.map((p, i) => (
|
|
38
41
|
<li key={p.message_id ?? `${p.received_at}-${i}`} className="flex justify-between gap-3">
|
|
39
42
|
<span className="truncate">
|
|
40
43
|
{pageLabel(p)}
|
|
41
|
-
{p.page_referrer ? <span className="ml-2 text-xs text-
|
|
44
|
+
{p.page_referrer ? <span className="ml-2 text-xs text-muted-foreground">from {p.page_referrer}</span> : null}
|
|
42
45
|
</span>
|
|
43
|
-
<span className="shrink-0 text-xs text-
|
|
46
|
+
<span className="shrink-0 text-xs text-muted-foreground">{timeAgo(p.received_at)}</span>
|
|
44
47
|
</li>
|
|
45
48
|
))}
|
|
46
49
|
</ol>
|
|
47
50
|
);
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
/** Page views per read; "Load more" appends the next page of the journey. */
|
|
54
|
+
const JOURNEY_PAGE = 100;
|
|
55
|
+
|
|
50
56
|
function Journey({ companyId }: { companyId: number }) {
|
|
51
|
-
const journey =
|
|
52
|
-
(signal)
|
|
53
|
-
|
|
54
|
-
[
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
const journey = useLoadMoreList(byPage<FlowEvent>(JOURNEY_PAGE, async (page, signal) => {
|
|
58
|
+
const env = await g8Get("/visitors/companies/events", { show_mode: "raw", filter: JSON.stringify({ company_id: companyId }), page, limit: JOURNEY_PAGE }, { signal });
|
|
59
|
+
// Raw mode returns one row per page view.
|
|
60
|
+
const rows = pageOf(env).events as unknown as FlowEvent[];
|
|
61
|
+
return { rows, hasNext: hasNextOf(env, JOURNEY_PAGE, rows.length) };
|
|
62
|
+
}), [companyId]);
|
|
63
|
+
if (journey.loading && !journey.data) return <Skeleton className="h-16 w-full" />;
|
|
57
64
|
if (journey.error) return <ApiError error={journey.error} onRetry={() => void journey.refetch()} />;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
const views = journey.data ?? [];
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
<PageList pages={views} />
|
|
69
|
+
<LoadMore shown={views.length} noun="page views" hasMore={journey.hasMore} loading={journey.loadingMore} onLoadMore={journey.loadMore} />
|
|
70
|
+
</>
|
|
71
|
+
);
|
|
61
72
|
}
|
|
62
73
|
|
|
63
74
|
export function VisitorCompanyCard({ visit, previewPages = 5, actions, className }: VisitorCompanyCardProps) {
|
|
@@ -68,9 +79,8 @@ export function VisitorCompanyCard({ visit, previewPages = 5, actions, className
|
|
|
68
79
|
<CardHeader className="flex flex-row items-start justify-between gap-3">
|
|
69
80
|
<div>
|
|
70
81
|
<CardTitle className="text-base">{companyLabel(visit)}</CardTitle>
|
|
71
|
-
<p className="text-xs text-
|
|
72
|
-
{visit.COMPANY_DOMAIN ? `${visit.COMPANY_DOMAIN} - ` : ""}last seen {timeAgo(visit.received_at)} - {pages.length
|
|
73
|
-
{pages.length === 1 ? "" : "s"}
|
|
82
|
+
<p className="text-xs text-muted-foreground">
|
|
83
|
+
{visit.COMPANY_DOMAIN ? `${visit.COMPANY_DOMAIN} - ` : ""}last seen {timeAgo(visit.received_at)} - {plural(pages.length, "page view")}
|
|
74
84
|
</p>
|
|
75
85
|
</div>
|
|
76
86
|
{actions ? <div className="flex gap-2">{actions}</div> : null}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* VisitorDetail: the complete screen for one visitor, a visiting company or an identified person. Shows every field the feed
|
|
5
|
+
* carries (name, domain, ids, last seen, visit count), every page viewed with its referrer, and, for a company, the full journey of
|
|
6
|
+
* raw page views. Pass the grouped row from VisitorFeed, or just `kind` and `id` (a deep link): the row is then found in the latest
|
|
7
|
+
* 100 grouped visits.
|
|
8
|
+
*
|
|
9
|
+
* SDK operations, in order (all read tier, nothing billable or external):
|
|
10
|
+
* 1. listCompanyVisitorEvents GET /visitors/companies/events (show_mode=grouped to find the row; show_mode=raw with filter={"company_id": id} for the journey)
|
|
11
|
+
* 2. listContactVisitorEvents GET /visitors/contacts/events (show_mode=grouped to find the row; needs contacts:read too)
|
|
12
|
+
*/
|
|
13
|
+
import { Skeleton } from "@graph8/sdk/ui";
|
|
14
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
15
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
16
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
17
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
18
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
19
|
+
import { EntityAvatar } from "@/g8/components/screen/entity-avatar";
|
|
20
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
21
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
22
|
+
import { companyLabel, pageLabel, pageOf, personLabel, timeAgo } from "../lib/visitors-utils";
|
|
23
|
+
import type { FlowEvent, VisitorGroup } from "../lib/visitors-utils";
|
|
24
|
+
import { VisitorCompanyCard } from "./visitor-company-card";
|
|
25
|
+
|
|
26
|
+
export interface VisitorDetailProps {
|
|
27
|
+
kind: "company" | "person";
|
|
28
|
+
/** COMPANY_ID for a company, CONTACT_ID for a person. */
|
|
29
|
+
id: number;
|
|
30
|
+
/** The row from the feed; without it the screen looks it up. */
|
|
31
|
+
visit?: VisitorGroup | null;
|
|
32
|
+
/** Buttons in the header (for example "Open contact" or "Add to list"). */
|
|
33
|
+
actions?: React.ReactNode;
|
|
34
|
+
/** Extra tabs from the screen's `extraDetailTabs` slot, after the built-in ones. */
|
|
35
|
+
extraTabs?: { id: string; label: string; content: React.ReactNode }[];
|
|
36
|
+
onClose?: () => void;
|
|
37
|
+
onExpand?: () => void;
|
|
38
|
+
onCollapse?: () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function Pages({ pages }: { pages: FlowEvent[] }) {
|
|
42
|
+
if (pages.length === 0) return <p className="text-sm text-muted-foreground">No page views recorded.</p>;
|
|
43
|
+
return (
|
|
44
|
+
<ol className="space-y-1.5 text-sm">
|
|
45
|
+
{pages.map((p, i) => (
|
|
46
|
+
<li key={p.message_id ?? `${p.received_at}-${i}`} className="flex justify-between gap-3">
|
|
47
|
+
<span className="min-w-0 truncate">
|
|
48
|
+
{pageLabel(p)}
|
|
49
|
+
{p.page_path ? <span className="ml-2 text-xs text-muted-foreground">{p.page_path}</span> : null}
|
|
50
|
+
{p.page_referrer ? <span className="ml-2 text-xs text-muted-foreground">from {p.page_referrer}</span> : null}
|
|
51
|
+
</span>
|
|
52
|
+
<span className="shrink-0 text-xs text-muted-foreground">{timeAgo(p.received_at)}</span>
|
|
53
|
+
</li>
|
|
54
|
+
))}
|
|
55
|
+
</ol>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function VisitorDetail({ kind, id, visit: given, actions, extraTabs, onClose, onExpand, onCollapse }: VisitorDetailProps) {
|
|
60
|
+
const path = kind === "company" ? "/visitors/companies/events" : "/visitors/contacts/events";
|
|
61
|
+
const lookup = useG8Query(
|
|
62
|
+
async (signal) => pageOf(await g8Get(path, { show_mode: "grouped", page: 1, limit: 100 }, { signal })).events,
|
|
63
|
+
[kind],
|
|
64
|
+
{ enabled: !given },
|
|
65
|
+
);
|
|
66
|
+
const visit = given ?? (lookup.data ?? []).find((v) => (kind === "company" ? v.COMPANY_ID : v.CONTACT_ID) === id);
|
|
67
|
+
|
|
68
|
+
if (!given && lookup.error) return <ApiError error={lookup.error} title="Could not load the visitor" onRetry={() => void lookup.refetch()} />;
|
|
69
|
+
if (!visit && lookup.loading) return <Skeleton className="h-40 w-full" />;
|
|
70
|
+
if (!visit) {
|
|
71
|
+
if (kind === "company") {
|
|
72
|
+
return <DetailPanel title={`Company ${id}`} subtitle="Not among the latest visits" onClose={onClose} onExpand={onExpand} onCollapse={onCollapse}
|
|
73
|
+
tabs={[{ id: "journey", label: "Journey", content: <VisitorCompanyCard visit={{ COMPANY_ID: id }} /> }]} />;
|
|
74
|
+
}
|
|
75
|
+
return <EmptyState title="Visitor not found" description="They are not among the latest 100 identified visitors." />;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const name = kind === "company" ? companyLabel(visit) : personLabel(visit);
|
|
79
|
+
const pages = visit.children ?? [];
|
|
80
|
+
const tabs = [
|
|
81
|
+
{
|
|
82
|
+
id: "overview",
|
|
83
|
+
label: "Overview",
|
|
84
|
+
content: (
|
|
85
|
+
<div className="space-y-4">
|
|
86
|
+
<KeyValueList columns={2} items={[
|
|
87
|
+
...(kind === "person" ? [{ label: "Person", value: name }, { label: "Contact id", value: visit.CONTACT_ID ?? "-" }] : []),
|
|
88
|
+
{ label: "Company", value: visit.COMPANY_NAME ?? "-" },
|
|
89
|
+
{ label: "Domain", value: visit.COMPANY_DOMAIN ?? "-" },
|
|
90
|
+
{ label: "Company id", value: visit.COMPANY_ID ?? "-" },
|
|
91
|
+
{ label: "Last seen", value: timeAgo(visit.received_at) },
|
|
92
|
+
{ label: "Page views", value: pages.length },
|
|
93
|
+
]} />
|
|
94
|
+
<SectionCard title="Pages viewed"><Pages pages={pages} /></SectionCard>
|
|
95
|
+
</div>
|
|
96
|
+
),
|
|
97
|
+
},
|
|
98
|
+
...(kind === "company" ? [{ id: "journey", label: "Full journey", content: <VisitorCompanyCard visit={visit} /> }] : []),
|
|
99
|
+
];
|
|
100
|
+
return (
|
|
101
|
+
<DetailPanel title={name} subtitle={kind === "person" ? visit.COMPANY_NAME ?? visit.COMPANY_DOMAIN ?? undefined : visit.COMPANY_DOMAIN ?? undefined}
|
|
102
|
+
avatar={<EntityAvatar name={name} kind={kind === "company" ? "company" : "person"} size="lg" />} actions={actions}
|
|
103
|
+
tabs={extraTabs?.length ? [...tabs, ...extraTabs] : tabs} onClose={onClose} onExpand={onExpand} onCollapse={onCollapse} />
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -21,6 +21,7 @@ import { EmptyState } from "@/g8/components/empty-state";
|
|
|
21
21
|
import { VisitorCompanyCard } from "./visitor-company-card";
|
|
22
22
|
import { pageOf, personLabel, pageLabel, timeAgo } from "../lib/visitors-utils";
|
|
23
23
|
import type { VisitorGroup } from "../lib/visitors-utils";
|
|
24
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
24
25
|
|
|
25
26
|
export interface VisitorFeedProps {
|
|
26
27
|
pageSize?: number;
|
|
@@ -30,6 +31,8 @@ export interface VisitorFeedProps {
|
|
|
30
31
|
/** Which tab to open on. Default "companies". */
|
|
31
32
|
initialView?: "companies" | "people";
|
|
32
33
|
onSelectPerson?: (visit: VisitorGroup) => void;
|
|
34
|
+
/** Called when a person opens a visiting company (show a VisitorDetail). Adds an Open button to each company card. */
|
|
35
|
+
onSelectCompany?: (visit: VisitorGroup) => void;
|
|
33
36
|
className?: string;
|
|
34
37
|
}
|
|
35
38
|
|
|
@@ -42,8 +45,10 @@ export function VisitorFeed({
|
|
|
42
45
|
includeInternal = false,
|
|
43
46
|
initialView = "companies",
|
|
44
47
|
onSelectPerson,
|
|
48
|
+
onSelectCompany,
|
|
45
49
|
className,
|
|
46
50
|
}: VisitorFeedProps) {
|
|
51
|
+
const brand = useBrand();
|
|
47
52
|
const [view, setView] = useState<View>(initialView);
|
|
48
53
|
// The page belongs to one set of inputs: a different tab, page size or bot/internal filter starts at page 1.
|
|
49
54
|
const scope = `${view}|${pageSize}|${includeBots}|${includeInternal}`;
|
|
@@ -113,35 +118,39 @@ export function VisitorFeed({
|
|
|
113
118
|
) : events.length === 0 ? (
|
|
114
119
|
<EmptyState
|
|
115
120
|
title={view === "companies" ? "No company visits yet" : "No identified people yet"}
|
|
116
|
-
description=
|
|
121
|
+
description={`Visits appear once the ${brand.linkLabel} snippet is on your site. Check the install in SnippetInstall.`}
|
|
117
122
|
/>
|
|
118
123
|
) : view === "companies" ? (
|
|
119
124
|
<div className="space-y-3">
|
|
120
125
|
{events.map((visit, i) => (
|
|
121
|
-
<VisitorCompanyCard
|
|
126
|
+
<VisitorCompanyCard
|
|
127
|
+
key={`${visit.COMPANY_ID}-${visit.received_at}-${i}`}
|
|
128
|
+
visit={visit}
|
|
129
|
+
actions={onSelectCompany ? <Button size="sm" variant="outline" onClick={() => onSelectCompany(visit)}>Open</Button> : undefined}
|
|
130
|
+
/>
|
|
122
131
|
))}
|
|
123
132
|
</div>
|
|
124
133
|
) : (
|
|
125
|
-
<ul className="divide-y rounded border border-
|
|
134
|
+
<ul className="divide-y rounded border border-border">
|
|
126
135
|
{events.map((visit, i) => (
|
|
127
136
|
<li key={`${visit.CONTACT_ID}-${visit.received_at}-${i}`}>
|
|
128
137
|
<button
|
|
129
138
|
type="button"
|
|
130
|
-
className="flex w-full items-center justify-between px-3 py-2 text-left text-sm hover:bg-
|
|
139
|
+
className="flex w-full items-center justify-between px-3 py-2 text-left text-sm hover:bg-surface-hover"
|
|
131
140
|
onClick={() => onSelectPerson?.(visit)}
|
|
132
141
|
>
|
|
133
142
|
<span>
|
|
134
143
|
<span className="font-medium">{personLabel(visit)}</span>
|
|
135
|
-
<span className="ml-2 text-xs text-
|
|
144
|
+
<span className="ml-2 text-xs text-muted-foreground">{visit.children?.[0] ? pageLabel(visit.children[0]) : ""}</span>
|
|
136
145
|
</span>
|
|
137
|
-
<span className="text-xs text-
|
|
146
|
+
<span className="text-xs text-muted-foreground">{timeAgo(visit.received_at)}</span>
|
|
138
147
|
</button>
|
|
139
148
|
</li>
|
|
140
149
|
))}
|
|
141
150
|
</ul>
|
|
142
151
|
)}
|
|
143
152
|
|
|
144
|
-
<div className="flex items-center justify-between text-sm text-
|
|
153
|
+
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
145
154
|
<span>{total.toLocaleString()} visits</span>
|
|
146
155
|
<div className="flex gap-2">
|
|
147
156
|
<Button size="sm" variant="outline" disabled={page <= 1 || feed.loading} onClick={() => setPage((p) => p - 1)}>
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* VisitorsScreen: the finished "website visitors" product page. Three tabs:
|
|
5
|
+
* Visitors companies (default) and identified people visiting your site (VisitorFeed, polled), a stat strip for the last
|
|
6
|
+
* 7 days, and a complete detail screen for any company or person (VisitorDetail: fields, pages viewed, full journey;
|
|
7
|
+
* deep link `?id=c-<company id>` or `?id=p-<contact id>`, full page with `?view=full`).
|
|
8
|
+
* Traffic visitors, sessions and page views per day, top pages, "N on the site now" (TrafficChart).
|
|
9
|
+
* Install the tracking snippet and an install check that polls until events arrive (SnippetInstall).
|
|
10
|
+
*
|
|
11
|
+
* User flows: get the snippet, prove it works, watch who visits, switch companies/people, open a person, expand a
|
|
12
|
+
* company's journey, chart traffic. A site with no events yet opens on a guided "install the snippet" empty state.
|
|
13
|
+
*
|
|
14
|
+
* SDK operations: none of its own. Composes VisitorFeed, SnippetInstall, TrafficChart (all read tier, nothing is
|
|
15
|
+
* billable or external) and one read of getAudienceTrend (7 days) for the stat strip.
|
|
16
|
+
* Prerequisite: the snippet pasted in the site (a step only a person can do); the People list also needs contacts:read.
|
|
17
|
+
*/
|
|
18
|
+
import { useState } from "react";
|
|
19
|
+
import type { ReactNode } from "react";
|
|
20
|
+
import { Button } from "@graph8/sdk/ui";
|
|
21
|
+
import { Globe } from "lucide-react";
|
|
22
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
23
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
24
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
25
|
+
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
26
|
+
import type { StatItem } from "@/g8/components/screen/stat-strip";
|
|
27
|
+
import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
28
|
+
import { SplitView } from "@/g8/components/screen/split-view";
|
|
29
|
+
import { seriesOf } from "../lib/visitors-utils";
|
|
30
|
+
import type { VisitorGroup } from "../lib/visitors-utils";
|
|
31
|
+
import { SnippetInstall } from "./snippet-install";
|
|
32
|
+
import { TrafficChart } from "./traffic-chart";
|
|
33
|
+
import { VisitorDetail } from "./visitor-detail";
|
|
34
|
+
import { VisitorFeed } from "./visitor-feed";
|
|
35
|
+
|
|
36
|
+
export interface VisitorsScreenProps {
|
|
37
|
+
/** Buttons in the visitor detail header (for example "Open contact" or "Add to list"). */
|
|
38
|
+
renderDetailActions?: (visit: VisitorGroup, kind: "company" | "person") => ReactNode;
|
|
39
|
+
/** Tabs added after the built-in ones on the open visitor (once its row is known). */
|
|
40
|
+
extraDetailTabs?: { id: string; label: string; render: (visit: VisitorGroup, kind: "company" | "person") => ReactNode }[];
|
|
41
|
+
toolbarActions?: ReactNode;
|
|
42
|
+
className?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface TrendPoint {
|
|
46
|
+
date: string;
|
|
47
|
+
users?: number;
|
|
48
|
+
sessions?: number;
|
|
49
|
+
page_views?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type Tab = "visitors" | "traffic" | "install";
|
|
53
|
+
const TABS = ["visitors", "traffic", "install"] as const;
|
|
54
|
+
|
|
55
|
+
function useWeekStats(): { items: StatItem[] | undefined; loading: boolean } {
|
|
56
|
+
const q = useG8Query((signal) => g8Get("/analytics/audience/trend", { days: 7 }, { signal }), []);
|
|
57
|
+
const points = seriesOf<TrendPoint>(q.data);
|
|
58
|
+
if (q.error || points.length === 0) return { items: undefined, loading: q.loading };
|
|
59
|
+
// The trend payload is open-shaped in the contract: chart only the metrics the rows really carry.
|
|
60
|
+
const metrics = [
|
|
61
|
+
{ key: "users", label: "Visitors" },
|
|
62
|
+
{ key: "sessions", label: "Sessions" },
|
|
63
|
+
{ key: "page_views", label: "Page views" },
|
|
64
|
+
] as const;
|
|
65
|
+
const items = metrics
|
|
66
|
+
.filter((m) => points.some((p) => typeof p[m.key] === "number"))
|
|
67
|
+
.map((m): StatItem => ({
|
|
68
|
+
label: m.label,
|
|
69
|
+
value: points.reduce((s, p) => s + (p[m.key] ?? 0), 0).toLocaleString(),
|
|
70
|
+
hint: "last 7 days",
|
|
71
|
+
sparkline: points.map((p) => p[m.key] ?? 0),
|
|
72
|
+
}));
|
|
73
|
+
return { loading: q.loading, items: items.length ? items : undefined };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function VisitorsScreen(props: VisitorsScreenProps) {
|
|
77
|
+
const [tabValue, setTab] = useUrlState("tab", "visitors");
|
|
78
|
+
const tab: Tab = (TABS as readonly string[]).includes(tabValue) ? (tabValue as Tab) : "visitors";
|
|
79
|
+
const [id, setId] = useUrlState("id", "");
|
|
80
|
+
const [view, setView] = useUrlState("view", "");
|
|
81
|
+
const [open, setOpen] = useState<VisitorGroup | null>(null);
|
|
82
|
+
const expanded = view === "full" && Boolean(id);
|
|
83
|
+
const close = () => {
|
|
84
|
+
setOpen(null);
|
|
85
|
+
setId("");
|
|
86
|
+
setView("");
|
|
87
|
+
};
|
|
88
|
+
const kind = id.startsWith("p-") ? "person" : "company";
|
|
89
|
+
// Only `c-<id>` and `p-<id>` open a record: `?id=123` must not be read as company 3.
|
|
90
|
+
const numeric = /^[cp]-\d+$/.test(id) ? Number(id.slice(2)) : NaN;
|
|
91
|
+
const stats = useWeekStats();
|
|
92
|
+
const [bots, setBots] = useState(false);
|
|
93
|
+
const [internal, setInternal] = useState(false);
|
|
94
|
+
const [days, setDays] = useState(30);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<ScreenLayout
|
|
98
|
+
title="Website visitors"
|
|
99
|
+
description="Who is on your site, which companies they work for, and what they looked at."
|
|
100
|
+
icon={<Globe className="size-5" />}
|
|
101
|
+
actions={props.toolbarActions}
|
|
102
|
+
tabs={[
|
|
103
|
+
{ id: "visitors", label: "Visitors" },
|
|
104
|
+
{ id: "traffic", label: "Traffic" },
|
|
105
|
+
{ id: "install", label: "Install" },
|
|
106
|
+
]}
|
|
107
|
+
activeTab={tab}
|
|
108
|
+
onTabChange={(next) => {
|
|
109
|
+
setTab(next);
|
|
110
|
+
close();
|
|
111
|
+
}}
|
|
112
|
+
stats={tab === "visitors" ? stats.items : undefined}
|
|
113
|
+
className={props.className}
|
|
114
|
+
>
|
|
115
|
+
{tab === "visitors" ? (
|
|
116
|
+
<div className="space-y-3">
|
|
117
|
+
<Toolbar>
|
|
118
|
+
<Button size="sm" variant={bots ? "default" : "outline"} aria-pressed={bots} onClick={() => setBots((v) => !v)}>
|
|
119
|
+
Include bots
|
|
120
|
+
</Button>
|
|
121
|
+
<Button size="sm" variant={internal ? "default" : "outline"} aria-pressed={internal} onClick={() => setInternal((v) => !v)}>
|
|
122
|
+
Include internal traffic
|
|
123
|
+
</Button>
|
|
124
|
+
</Toolbar>
|
|
125
|
+
<SplitView
|
|
126
|
+
detailOpen={Boolean(id) && Number.isFinite(numeric)}
|
|
127
|
+
onCloseDetail={close}
|
|
128
|
+
expanded={expanded}
|
|
129
|
+
onExpandedChange={(next) => setView(next ? "full" : "")}
|
|
130
|
+
detailWidth="lg"
|
|
131
|
+
list={
|
|
132
|
+
<VisitorFeed
|
|
133
|
+
key={`${bots}-${internal}`}
|
|
134
|
+
includeBots={bots}
|
|
135
|
+
includeInternal={internal}
|
|
136
|
+
onSelectPerson={(v) => { if (v.CONTACT_ID == null) return; setOpen(v); setId(`p-${v.CONTACT_ID}`); }}
|
|
137
|
+
onSelectCompany={(v) => { if (v.COMPANY_ID == null) return; setOpen(v); setId(`c-${v.COMPANY_ID}`); }}
|
|
138
|
+
/>
|
|
139
|
+
}
|
|
140
|
+
detail={id && Number.isFinite(numeric) ? (
|
|
141
|
+
<VisitorDetail key={id} kind={kind} id={numeric} visit={open && (kind === "company" ? open.COMPANY_ID : open.CONTACT_ID) === numeric ? open : null}
|
|
142
|
+
actions={open ? props.renderDetailActions?.(open, kind) : undefined}
|
|
143
|
+
extraTabs={open ? (props.extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(open, kind) })) : undefined}
|
|
144
|
+
onClose={close} onExpand={expanded ? undefined : () => setView("full")} onCollapse={expanded ? () => setView("") : undefined} />
|
|
145
|
+
) : null}
|
|
146
|
+
/>
|
|
147
|
+
</div>
|
|
148
|
+
) : tab === "traffic" ? (
|
|
149
|
+
<div className="space-y-3">
|
|
150
|
+
<Toolbar>
|
|
151
|
+
<div className="inline-flex gap-1" role="group" aria-label="Traffic window">
|
|
152
|
+
{[7, 30, 90].map((d) => (
|
|
153
|
+
<Button key={d} size="sm" variant={days === d ? "default" : "outline"} aria-pressed={days === d} onClick={() => setDays(d)}>
|
|
154
|
+
{d} days
|
|
155
|
+
</Button>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
</Toolbar>
|
|
159
|
+
<TrafficChart days={days} />
|
|
160
|
+
</div>
|
|
161
|
+
) : (
|
|
162
|
+
<SnippetInstall />
|
|
163
|
+
)}
|
|
164
|
+
</ScreenLayout>
|
|
165
|
+
);
|
|
166
|
+
}
|
package/blocks/webchat/README.md
CHANGED
|
@@ -4,6 +4,14 @@ Configure the AI chat widget on your website and see who used it. `ChatWidgetCon
|
|
|
4
4
|
look, starter questions and the embed code; `ConversationList` shows the widget's visitors and the engagement funnel.
|
|
5
5
|
Message threads and replies live in the `inbox` block.
|
|
6
6
|
|
|
7
|
+
## Screens
|
|
8
|
+
|
|
9
|
+
`WebchatScreen` (`components/webchat-screen.tsx`) needs no props. **Visitors**: funnel stats, search, identified only,
|
|
10
|
+
paging, visitor detail (`?id=`). **Widget**: pick or create a bot, style it, starter questions (delete is confirmed).
|
|
11
|
+
**Install**: the tracking snippet with copy, and what graph8 must have set up (an answering agent mapped to the bot).
|
|
12
|
+
Slots: `toolbarActions`, `renderRowActions`, `renderDetailActions`, `extraDetailTabs` (use them to link a visitor to the inbox).
|
|
13
|
+
Scopes: `inbox:read`, `inbox:write`, `inbox:delete`, `integrations:read`.
|
|
14
|
+
|
|
7
15
|
## Install
|
|
8
16
|
|
|
9
17
|
```
|
|
@@ -20,6 +28,8 @@ Installs `foundation` and `crm` first if missing. Everything lands under `g8/web
|
|
|
20
28
|
| graph8 workspace | An **answering agent** mapped to the bot. A bot with none answers nothing. | `GET /api/v1/webchat/agents?chat_bot_id=` lists an active agent. |
|
|
21
29
|
| Your app | `allowedPrefixes` must include `webchat` and `snippet`. | The route file lists both. |
|
|
22
30
|
|
|
31
|
+
Every detail opens as a full page with `&view=full` (the Open full page button).
|
|
32
|
+
|
|
23
33
|
## The flow, in order (IDs passed between steps)
|
|
24
34
|
|
|
25
35
|
1. `listChatBots` -> **`bot.id`**. This is the `chat_bot_id` of nearly every other call. `createChatBot({ name })` makes one.
|
|
@@ -14,17 +14,17 @@
|
|
|
14
14
|
"crm"
|
|
15
15
|
],
|
|
16
16
|
"operations": [
|
|
17
|
-
"list_chat_bots_webchat_chat_bots_get",
|
|
18
17
|
"create_chat_bot_webchat_chat_bots_post",
|
|
19
|
-
"get_webchat_settings_webchat_settings_get",
|
|
20
|
-
"create_webchat_settings_webchat_settings_post",
|
|
21
|
-
"update_webchat_settings_webchat_settings_patch",
|
|
22
|
-
"list_webchat_questions_webchat_questions_get",
|
|
23
18
|
"create_webchat_question_webchat_questions_post",
|
|
19
|
+
"create_webchat_settings_webchat_settings_post",
|
|
24
20
|
"delete_webchat_question_webchat_questions__question_id__delete",
|
|
25
21
|
"get_org_snippet_snippet_get",
|
|
22
|
+
"get_webchat_settings_webchat_settings_get",
|
|
23
|
+
"get_webchat_visitor_funnel_webchat_visitors_funnel_get",
|
|
24
|
+
"list_chat_bots_webchat_chat_bots_get",
|
|
25
|
+
"list_webchat_questions_webchat_questions_get",
|
|
26
26
|
"list_webchat_visitors_webchat_visitors_get",
|
|
27
|
-
"
|
|
27
|
+
"update_webchat_settings_webchat_settings_patch"
|
|
28
28
|
],
|
|
29
29
|
"components": [
|
|
30
30
|
{
|
|
@@ -59,6 +59,19 @@
|
|
|
59
59
|
"tiers": [
|
|
60
60
|
"read"
|
|
61
61
|
]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "WebchatScreen",
|
|
65
|
+
"file": "components/webchat-screen.tsx",
|
|
66
|
+
"purpose": "Finished web chat page: visitors with funnel stats and detail, widget styling, and install snippet.",
|
|
67
|
+
"operations": [
|
|
68
|
+
"get_org_snippet_snippet_get",
|
|
69
|
+
"get_webchat_visitor_funnel_webchat_visitors_funnel_get",
|
|
70
|
+
"list_webchat_visitors_webchat_visitors_get"
|
|
71
|
+
],
|
|
72
|
+
"tiers": [
|
|
73
|
+
"read"
|
|
74
|
+
]
|
|
62
75
|
}
|
|
63
76
|
],
|
|
64
77
|
"hooks": [],
|
|
@@ -129,6 +142,7 @@
|
|
|
129
142
|
}
|
|
130
143
|
],
|
|
131
144
|
"peerDependencies": {
|
|
132
|
-
"react": ">=18"
|
|
145
|
+
"react": ">=18",
|
|
146
|
+
"lucide-react": ">=0.400.0"
|
|
133
147
|
}
|
|
134
148
|
}
|