@graph8/sdk 0.362.0 → 0.373.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -10
- package/blocks/_foundation/README.md +78 -1
- package/blocks/_foundation/block.json +123 -6
- package/blocks/_foundation/components/api-error.tsx +9 -7
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/confirmed-action.tsx +90 -0
- package/blocks/_foundation/components/connect-graph8.tsx +280 -0
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +2 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-brand.ts +52 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/connect-route.ts +15 -0
- package/blocks/_foundation/server/connect.ts +127 -0
- package/blocks/_foundation/server/local-key.ts +70 -0
- package/blocks/_foundation/server/proxy.ts +103 -3
- package/blocks/_foundation/server/route.ts +16 -4
- package/blocks/agents/README.md +93 -2
- package/blocks/agents/block.json +343 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +29 -10
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +119 -0
- package/blocks/agents/components/agent-runs.tsx +19 -10
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +226 -0
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +222 -0
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +38 -11
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +33 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +4 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +38 -18
- package/blocks/appointments/components/meetings-screen.tsx +7 -4
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/appointments/components/scheduling-screen.tsx +2 -1
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +17 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +17 -6
- package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +163 -0
- package/blocks/campaigns/components/icp-editor.tsx +22 -17
- package/blocks/campaigns/components/persona-detail.tsx +181 -0
- package/blocks/campaigns/components/persona-editor.tsx +19 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
- package/blocks/coach/README.md +10 -0
- package/blocks/coach/block.json +16 -1
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/coach/components/coach-screen.tsx +222 -0
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +161 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +14 -5
- package/blocks/copilot/components/copilot-screen.tsx +148 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
- package/blocks/crm/README.md +34 -0
- package/blocks/crm/block.json +107 -2
- package/blocks/crm/components/companies-screen.tsx +359 -0
- package/blocks/crm/components/company-detail.tsx +10 -8
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +10 -7
- package/blocks/crm/components/contact-lookup-link.tsx +44 -0
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +377 -0
- package/blocks/crm/components/dedupe-review.tsx +13 -6
- package/blocks/crm/components/import-wizard.tsx +7 -9
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +6 -4
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +399 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +9 -1
- package/blocks/crm/lib/crm-types.ts +26 -2
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +24 -6
- package/blocks/deals/block.json +160 -5
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +25 -13
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +31 -10
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +66 -26
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +5 -5
- package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +45 -4
- package/blocks/dialer/block.json +84 -3
- package/blocks/dialer/components/call-contact-button.tsx +269 -0
- package/blocks/dialer/components/call-log-screen.tsx +208 -0
- package/blocks/dialer/components/call-log.tsx +23 -7
- package/blocks/dialer/components/call-queue.tsx +4 -4
- package/blocks/dialer/components/dialer-screen.tsx +361 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +61 -12
- package/blocks/dialer/components/softphone.tsx +4 -2
- package/blocks/enrichment/README.md +29 -7
- package/blocks/enrichment/block.json +49 -9
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
- package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
- package/blocks/enrichment/components/enrich-button.tsx +32 -9
- package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
- package/blocks/enrichment/components/waterfall-config.tsx +66 -17
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +8 -0
- package/blocks/forms/block.json +139 -18
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +4 -2
- package/blocks/forms/components/form-embed.tsx +4 -2
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +56 -19
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +17 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +32 -3
- package/blocks/inbox/block.json +29 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
- package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
- package/blocks/inbox/lib/inbox-types.ts +47 -1
- package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
- package/blocks/integrations/README.md +63 -1
- package/blocks/integrations/block.json +234 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +119 -0
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +13 -8
- package/blocks/integrations/components/integrations-screen.tsx +152 -0
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +11 -9
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +179 -0
- package/blocks/integrations/components/webhook-manager.tsx +43 -25
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +11 -1
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +13 -10
- package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/block.json +13 -8
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +20 -1
- package/blocks/lists/block.json +69 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +20 -9
- package/blocks/lists/components/list-picker.tsx +17 -3
- package/blocks/lists/components/list-table.tsx +12 -6
- package/blocks/lists/components/lists-screen.tsx +346 -0
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +33 -8
- package/blocks/lists/components/suppressions-screen.tsx +264 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +69 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
- package/blocks/mailboxes/components/domain-detail.tsx +148 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
- package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
- package/blocks/mailboxes/components/warmup-health.tsx +24 -22
- package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +157 -24
- package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/block.json +28 -14
- package/blocks/nurture/components/nurture-builder.tsx +13 -3
- package/blocks/nurture/components/nurture-enroll.tsx +12 -10
- package/blocks/nurture/components/nurture-form.tsx +27 -10
- package/blocks/nurture/components/nurture-screen.tsx +26 -11
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +18 -1
- package/blocks/objects/block.json +42 -21
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +10 -2
- package/blocks/objects/components/object-schema-editor.tsx +4 -2
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +140 -23
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +9 -4
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-list.tsx +2 -1
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/quotes/README.md +13 -0
- package/blocks/quotes/block.json +65 -13
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +5 -3
- package/blocks/quotes/components/quote-related.tsx +8 -4
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-report.tsx +4 -2
- package/blocks/radar/components/radar-screen.tsx +214 -0
- package/blocks/radar/components/report-detail.tsx +104 -0
- package/blocks/search/README.md +10 -0
- package/blocks/search/block.json +16 -1
- package/blocks/search/components/filter-builder.tsx +3 -3
- package/blocks/search/components/prospect-results.tsx +5 -3
- package/blocks/search/components/prospect-search-screen.tsx +235 -0
- package/blocks/search/components/saved-searches.tsx +4 -1
- package/blocks/search/hooks/use-filter-options.ts +20 -6
- package/blocks/search/lib/search-types.ts +2 -0
- package/blocks/sequencer/README.md +55 -9
- package/blocks/sequencer/block.json +126 -19
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +224 -60
- package/blocks/sequencer/components/sequence-detail.tsx +261 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
- package/blocks/sequencer/components/sequence-list.tsx +34 -9
- package/blocks/sequencer/components/sequence-stats.tsx +11 -10
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +162 -0
- package/blocks/sequencer/components/step-editor.tsx +14 -12
- package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
- package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
- package/blocks/social-ads/README.md +19 -0
- package/blocks/social-ads/block.json +233 -1
- package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
- package/blocks/social-ads/components/ads-screen.tsx +152 -0
- package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
- package/blocks/social-ads/components/audience-sync.tsx +17 -9
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +138 -0
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +155 -0
- package/blocks/social-ads/components/creative-gallery.tsx +35 -7
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +7 -3
- package/blocks/social-ads/components/post-detail.tsx +163 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +187 -0
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +21 -10
- package/blocks/visitors/components/traffic-chart.tsx +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +26 -16
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +16 -7
- package/blocks/visitors/components/visitors-screen.tsx +166 -0
- package/blocks/webchat/README.md +10 -0
- package/blocks/webchat/block.json +21 -7
- package/blocks/webchat/components/chat-widget-config.tsx +8 -5
- package/blocks/webchat/components/conversation-list.tsx +8 -4
- package/blocks/webchat/components/webchat-screen.tsx +245 -0
- package/blocks/workbench/README.md +11 -0
- package/blocks/workbench/block.json +48 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +10 -4
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +202 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workflows/README.md +13 -0
- package/blocks/workflows/block.json +28 -9
- package/blocks/workflows/components/run-history.tsx +31 -9
- package/blocks/workflows/components/trigger-config.tsx +3 -1
- package/blocks/workflows/components/workflow-editor.tsx +4 -2
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +3109 -1977
- package/dist/errors.d.mts +34 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +21 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.browser.js +11 -5
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +11 -5
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +164 -25
- package/dist/index.d.ts +164 -25
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +10 -4
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +10 -4
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +142 -5
- package/dist/react.d.ts +142 -5
- package/dist/react.js +10 -4
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +10 -4
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/add.d.ts +11 -4
- package/dist/types/blocks/brand.d.ts +122 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +27 -20
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/blocks/paths.d.ts +4 -0
- package/dist/types/blocks/prefixes.d.ts +37 -0
- package/dist/types/blocks/registry.d.ts +17 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +5 -0
- package/dist/types/errors.d.ts +33 -0
- package/dist/types/http.d.ts +2 -18
- package/dist/types/version.d.ts +2 -2
- package/docs/agents.md +15 -4
- package/docs/api/README.md +2 -2
- package/docs/api/by-path.md +1 -0
- package/docs/api/desk.d.ts +98 -2
- package/docs/api/desk.md +123 -1
- package/docs/api/inbox.d.ts +10 -2
- package/docs/api/inbox.md +10 -2
- package/docs/api/seo.d.ts +19 -1
- package/docs/api/seo.md +24 -1
- package/package.json +16 -2
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +55 -17
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +147 -37
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +64 -0
- package/stacks/clay/pages/companies.tsx.tmpl +31 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +73 -27
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +98 -35
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +104 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +181 -64
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +52 -20
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +68 -34
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +106 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +117 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +85 -31
- package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +115 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +40 -9
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/app/layout.tsx.tmpl +7 -2
- package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/** Shapes and diffing for the skill library. Skills answer un-enveloped or enveloped, flat or under `action`; `skillOf` reads all of them. */
|
|
4
|
+
import type { G8Envelope } from "@/g8/lib/g8-client";
|
|
5
|
+
import { bodyOf } from "./agent-types";
|
|
6
|
+
|
|
7
|
+
export interface SkillDetail {
|
|
8
|
+
action_id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
description?: string | null;
|
|
11
|
+
category?: string | null;
|
|
12
|
+
runtime_type?: "llm" | "api" | string;
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
requires_approval?: boolean;
|
|
15
|
+
llm_config?: { model?: string; temperature?: number; max_tokens?: number; prompt_template?: string; prompt?: string } | null;
|
|
16
|
+
api_config?: { endpoint?: string; method?: string; headers?: Record<string, string> | null; body_template?: string | null; requires_auth?: boolean } | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SkillDraft {
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
category: string;
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
model: string;
|
|
25
|
+
prompt_template: string;
|
|
26
|
+
temperature: string;
|
|
27
|
+
max_tokens: string;
|
|
28
|
+
endpoint: string;
|
|
29
|
+
method: string;
|
|
30
|
+
headers: string;
|
|
31
|
+
body_template: string;
|
|
32
|
+
requires_auth: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const BLANK_DRAFT: SkillDraft = { name: "", description: "", category: "", enabled: true, model: "", prompt_template: "", temperature: "0.7", max_tokens: "1000", endpoint: "", method: "POST", headers: "", body_template: "", requires_auth: false };
|
|
36
|
+
export const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"] as const;
|
|
37
|
+
|
|
38
|
+
export function skillOf(res: G8Envelope<unknown> | null | undefined): SkillDetail | undefined {
|
|
39
|
+
const b = bodyOf<{ action?: SkillDetail } & Partial<SkillDetail>>(res);
|
|
40
|
+
const s = (b.action ?? b) as SkillDetail;
|
|
41
|
+
return s && s.action_id ? s : undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function draftOf(s: SkillDetail): SkillDraft {
|
|
45
|
+
const l = s.llm_config ?? {}, a = s.api_config ?? {};
|
|
46
|
+
return {
|
|
47
|
+
name: s.name, description: s.description ?? "", category: s.category ?? "", enabled: s.enabled !== false,
|
|
48
|
+
model: l.model ?? "", prompt_template: l.prompt_template ?? l.prompt ?? "", temperature: String(l.temperature ?? 0.7), max_tokens: String(l.max_tokens ?? 1000),
|
|
49
|
+
endpoint: a.endpoint ?? "", method: a.method ?? "POST", headers: a.headers && Object.keys(a.headers).length ? JSON.stringify(a.headers, null, 2) : "", body_template: a.body_template ?? "", requires_auth: Boolean(a.requires_auth),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The headers textarea as an object; undefined when blank, null when it is not a JSON object of strings. */
|
|
54
|
+
export function parseHeaders(text: string): Record<string, string> | undefined | null {
|
|
55
|
+
if (!text.trim()) return undefined;
|
|
56
|
+
try {
|
|
57
|
+
const v = JSON.parse(text) as unknown;
|
|
58
|
+
if (!v || typeof v !== "object" || Array.isArray(v) || !Object.values(v).every((x) => typeof x === "string")) return null;
|
|
59
|
+
return v as Record<string, string>;
|
|
60
|
+
} catch { return null; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The PUT body: only what changed against the LOADED skill. A runtime config is sent whole when any field in it changed
|
|
65
|
+
* (voice validates it as a unit: a model and a prompt, or an endpoint).
|
|
66
|
+
*/
|
|
67
|
+
export function skillPatch(loaded: SkillDetail, d: SkillDraft): Record<string, unknown> {
|
|
68
|
+
const before = draftOf(loaded);
|
|
69
|
+
const changed = (...keys: (keyof SkillDraft)[]) => keys.some((k) => before[k] !== d[k]);
|
|
70
|
+
const out: Record<string, unknown> = {};
|
|
71
|
+
if (changed("name")) out.name = d.name.trim();
|
|
72
|
+
if (changed("description")) out.description = d.description.trim();
|
|
73
|
+
if (changed("category")) out.category = d.category.trim();
|
|
74
|
+
if (changed("enabled")) out.enabled = d.enabled;
|
|
75
|
+
if (loaded.runtime_type === "llm" && changed("model", "prompt_template", "temperature", "max_tokens")) {
|
|
76
|
+
out.llm_config = { model: d.model, prompt_template: d.prompt_template, temperature: Number(d.temperature), max_tokens: Number(d.max_tokens) };
|
|
77
|
+
}
|
|
78
|
+
if (loaded.runtime_type === "api" && changed("endpoint", "method", "headers", "body_template", "requires_auth")) {
|
|
79
|
+
const headers = parseHeaders(d.headers);
|
|
80
|
+
out.api_config = { endpoint: d.endpoint.trim(), method: d.method, body_template: d.body_template || undefined, requires_auth: d.requires_auth, ...(headers ? { headers } : {}) };
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The POST body for a new skill. */
|
|
86
|
+
export function newSkillBody(runtime: "llm" | "api", d: SkillDraft): Record<string, unknown> {
|
|
87
|
+
const base = { name: d.name.trim(), description: d.description.trim() || undefined, category: d.category.trim() || undefined, runtime_type: runtime };
|
|
88
|
+
if (runtime === "llm") return { ...base, llm_config: { model: d.model, prompt_template: d.prompt_template, temperature: Number(d.temperature), max_tokens: Number(d.max_tokens) } };
|
|
89
|
+
const headers = parseHeaders(d.headers);
|
|
90
|
+
return { ...base, api_config: { endpoint: d.endpoint.trim(), method: d.method, body_template: d.body_template || undefined, requires_auth: d.requires_auth, ...(headers ? { headers } : {}) } };
|
|
91
|
+
}
|
|
@@ -34,7 +34,7 @@ export async function buildAgent() {
|
|
|
34
34
|
body: {
|
|
35
35
|
name: "Summarise the lead",
|
|
36
36
|
runtime_type: "llm",
|
|
37
|
-
llm_config: {
|
|
37
|
+
llm_config: { model: "gpt-4o-mini", prompt_template: "Summarise {contact_name} in two sentences for the sales team." },
|
|
38
38
|
},
|
|
39
39
|
});
|
|
40
40
|
const actionId = (skill as { action_id?: string }).action_id;
|
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
Read-only outbound analytics (top-line numbers, rep ranking, funnels) plus a builder for saved custom-object
|
|
4
4
|
reports. Depends only on the foundation. Install with `npx @graph8/sdk add analytics`.
|
|
5
5
|
|
|
6
|
+
## Screens
|
|
7
|
+
|
|
8
|
+
`AnalyticsScreen` (`components/analytics-screen.tsx`) is the finished dashboard; mount it as the page and you are done.
|
|
9
|
+
|
|
10
|
+
- **Overview**: KPI strip (emails, reply rate, calls, meetings) with a daily sparkline and half-over-half change per
|
|
11
|
+
number, a daily trend chart you switch between email, calls and meetings, the meeting or revenue funnel, and the rep
|
|
12
|
+
leaderboard. One 7 / 30 / 90 day range drives every card; tab and range live in the URL (`?tab=`, `?days=`).
|
|
13
|
+
- **Reports**: every saved report in a searchable list. Opening one (`?id=`, full page with `&view=full`) shows `ReportDetail`
|
|
14
|
+
(definition, latest result, Run again, Delete with confirmation) and an Edit tab (`ReportBuilder reportId`); **New report** opens the builder in a sheet.
|
|
15
|
+
- Needs `analytics:read`; Reports also need `objects:read|write|delete` and the per-organisation reports switch.
|
|
16
|
+
- Slot/props: `defaultDays`, `nameFor`, `objectSlug`, `toolbarActions`. New parts: `TrendChart` (on the kit TimeSeriesChart, three trend
|
|
17
|
+
operations); `Leaderboard` gained `hideRange`; `ReportBuilder` gained `reportId` and `hideList`; new `ReportDetail`.
|
|
18
|
+
|
|
6
19
|
## The flow, in order
|
|
7
20
|
|
|
8
21
|
1. **Tiles** `GET /analytics/outbound/summary?days=` (`KpiTiles`): `emails_sent`, `delivery_rate`,
|
|
@@ -17,7 +17,11 @@
|
|
|
17
17
|
"archive_object_report_api_reports__report_id__delete",
|
|
18
18
|
"create_object_report_api_reports_post",
|
|
19
19
|
"execute_object_report_api_reports__report_id__execute_post",
|
|
20
|
+
"get_call_trend_analytics_outbound_call_metrics_trend_get",
|
|
21
|
+
"get_email_trend_analytics_outbound_email_metrics_trend_get",
|
|
20
22
|
"get_meeting_funnel_analytics_outbound_meeting_funnel_get",
|
|
23
|
+
"get_meeting_trend_analytics_outbound_meeting_metrics_trend_get",
|
|
24
|
+
"get_object_report_api_reports__report_id__get",
|
|
21
25
|
"get_outbound_summary_analytics_outbound_summary_get",
|
|
22
26
|
"get_sdr_leaderboard_analytics_outbound_sdr_activity_leaderboard_get",
|
|
23
27
|
"get_team_conversion_funnel_analytics_team_conversion_funnel_get",
|
|
@@ -26,6 +30,45 @@
|
|
|
26
30
|
"update_object_report_api_reports__report_id__patch"
|
|
27
31
|
],
|
|
28
32
|
"components": [
|
|
33
|
+
{
|
|
34
|
+
"name": "TrendChart",
|
|
35
|
+
"file": "components/trend-chart.tsx",
|
|
36
|
+
"purpose": "Daily email, call or meeting line chart on the kit TimeSeriesChart.",
|
|
37
|
+
"operations": [
|
|
38
|
+
"get_email_trend_analytics_outbound_email_metrics_trend_get",
|
|
39
|
+
"get_call_trend_analytics_outbound_call_metrics_trend_get",
|
|
40
|
+
"get_meeting_trend_analytics_outbound_meeting_metrics_trend_get"
|
|
41
|
+
],
|
|
42
|
+
"tiers": [
|
|
43
|
+
"read"
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "ReportDetail",
|
|
48
|
+
"file": "components/report-detail.tsx",
|
|
49
|
+
"purpose": "One saved report in full: definition, latest result, run again, delete (confirmed).",
|
|
50
|
+
"operations": [
|
|
51
|
+
"get_object_report_api_reports__report_id__get",
|
|
52
|
+
"execute_object_report_api_reports__report_id__execute_post",
|
|
53
|
+
"archive_object_report_api_reports__report_id__delete"
|
|
54
|
+
],
|
|
55
|
+
"tiers": [
|
|
56
|
+
"read",
|
|
57
|
+
"write",
|
|
58
|
+
"destructive"
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "AnalyticsScreen",
|
|
63
|
+
"file": "components/analytics-screen.tsx",
|
|
64
|
+
"purpose": "Finished dashboard: KPI strip with sparklines, daily trend, funnel, rep leaderboard and a Reports tab, one range for all.",
|
|
65
|
+
"operations": [
|
|
66
|
+
"list_object_reports_api_reports_get"
|
|
67
|
+
],
|
|
68
|
+
"tiers": [
|
|
69
|
+
"read"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
29
72
|
{
|
|
30
73
|
"name": "KpiTiles",
|
|
31
74
|
"file": "components/kpi-tiles.tsx",
|
|
@@ -173,6 +216,7 @@
|
|
|
173
216
|
}
|
|
174
217
|
],
|
|
175
218
|
"peerDependencies": {
|
|
176
|
-
"react": ">=18"
|
|
219
|
+
"react": ">=18",
|
|
220
|
+
"lucide-react": ">=0.400.0"
|
|
177
221
|
}
|
|
178
222
|
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AnalyticsScreen: the finished outbound dashboard. Two tabs.
|
|
5
|
+
* Overview: KPI strip with a daily sparkline per number, a daily trend chart (email, calls or meetings), the
|
|
6
|
+
* meeting or revenue funnel, and the rep leaderboard. One 7/30/90 day range drives all of it.
|
|
7
|
+
* Reports: every saved custom-object report as a searchable list; opening one (deep link ?id=) shows a detail panel
|
|
8
|
+
* with Result (ReportDetail: the definition, the latest run, Run again, Delete) and Edit (ReportBuilder),
|
|
9
|
+
* and "New report" opens the builder in a sheet.
|
|
10
|
+
*
|
|
11
|
+
* Composes: StatStrip, TrendChart, FunnelChart, Leaderboard, ReportDetail, ReportBuilder. No required props.
|
|
12
|
+
*
|
|
13
|
+
* SDK operations (all read except the report ones, which ReportBuilder owns and confirms):
|
|
14
|
+
* 0. GET /reports?limit=50 (Reports tab list); the rest of the report lifecycle is ReportDetail and ReportBuilder.
|
|
15
|
+
* 1. GET /analytics/outbound/summary?days= (analytics.getOutboundSummary) KPI numbers
|
|
16
|
+
* 2. GET /analytics/outbound/{email,call,meeting}-metrics/trend?days= sparklines + trend chart
|
|
17
|
+
* 3. GET /analytics/outbound/meeting-funnel?days= or /analytics/team/conversion-funnel?period_days= funnel
|
|
18
|
+
* 4. GET /analytics/outbound/sdr-activity/leaderboard?days=&limit= leaderboard
|
|
19
|
+
* 5. /reports... (Reports tab, see ReportBuilder: delete is destructive and confirmed)
|
|
20
|
+
*
|
|
21
|
+
* User flows: switch range; switch trend channel; switch funnel source; sort the leaderboard; open Reports and run
|
|
22
|
+
* the whole report lifecycle (build, preview, save, re-run, edit, delete); retry any card after an error; read a
|
|
23
|
+
* stale-cache notice instead of silent old numbers.
|
|
24
|
+
* The tab and range are kept in the URL (?tab=, ?days=) so a link opens the same view.
|
|
25
|
+
*/
|
|
26
|
+
import { useEffect, useState, type ReactNode } from "react";
|
|
27
|
+
import { BarChart3, FileText, Plus } from "lucide-react";
|
|
28
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
29
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
30
|
+
import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
|
|
31
|
+
import { byOffset, hasNextOf } from "@/g8/lib/paging";
|
|
32
|
+
import { LoadMore } from "@/g8/components/paging-notices";
|
|
33
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
34
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
35
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
36
|
+
import { SplitView } from "@/g8/components/screen/split-view";
|
|
37
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
38
|
+
import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
39
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
40
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
41
|
+
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
42
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
43
|
+
import { StatStrip, type StatItem } from "@/g8/components/screen/stat-strip";
|
|
44
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
45
|
+
import { useReport } from "../hooks/use-report";
|
|
46
|
+
import { DAY_RANGES, formatNumber, formatPercent, reportBody, type OutboundSummary, type ReportResultData, type SavedReport } from "../lib/analytics-types";
|
|
47
|
+
import { FunnelChart } from "./funnel-chart";
|
|
48
|
+
import { Leaderboard } from "./leaderboard";
|
|
49
|
+
import { ReportBuilder } from "./report-builder";
|
|
50
|
+
import { ReportDetail } from "./report-detail";
|
|
51
|
+
import { TrendChart, TREND_SERIES, seriesOf, type TrendChannel, type TrendRow } from "./trend-chart";
|
|
52
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
53
|
+
|
|
54
|
+
export interface AnalyticsScreenProps {
|
|
55
|
+
/** Window in days when the URL has none. Default 30. */
|
|
56
|
+
defaultDays?: number;
|
|
57
|
+
/** Map a rep email to a display name in the leaderboard. */
|
|
58
|
+
nameFor?: (email: string) => string;
|
|
59
|
+
/** Pre-select the object (slug) in the report builder. */
|
|
60
|
+
objectSlug?: string;
|
|
61
|
+
/** Extra buttons next to the range picker. */
|
|
62
|
+
toolbarActions?: ReactNode;
|
|
63
|
+
className?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const TABS = [{ id: "overview", label: "Overview" }, { id: "reports", label: "Reports" }];
|
|
67
|
+
const CHANNELS: Array<{ id: TrendChannel; label: string }> = [{ id: "email", label: "Email" }, { id: "calls", label: "Calls" }, { id: "meetings", label: "Meetings" }];
|
|
68
|
+
|
|
69
|
+
/** Percent change of the second half of a daily series against the first half; undefined when it cannot be told. */
|
|
70
|
+
export function halfDelta(values: number[]): number | undefined {
|
|
71
|
+
if (values.length < 4) return undefined;
|
|
72
|
+
const mid = Math.floor(values.length / 2);
|
|
73
|
+
const sum = (v: number[]) => v.reduce((a, b) => a + b, 0);
|
|
74
|
+
const before = sum(values.slice(0, mid));
|
|
75
|
+
const after = sum(values.slice(mid));
|
|
76
|
+
return before > 0 ? Math.round(((after - before) / before) * 1000) / 10 : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function Segmented<T extends string>({ label, value, options, onChange }: { label: string; value: T; options: Array<{ id: T; label: string }>; onChange: (v: T) => void }) {
|
|
80
|
+
return (
|
|
81
|
+
<div role="group" aria-label={label} className="flex gap-1">
|
|
82
|
+
{options.map((o) => <Button key={o.id} size="sm" variant={value === o.id ? "default" : "outline"} onClick={() => onChange(o.id)}>{o.label}</Button>)}
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function useKpis(days: number) {
|
|
88
|
+
const summary = useReport<OutboundSummary>("/analytics/outbound/summary", { days });
|
|
89
|
+
const email = useReport<TrendRow[]>(TREND_SERIES.email.path, { days });
|
|
90
|
+
const calls = useReport<TrendRow[]>(TREND_SERIES.calls.path, { days });
|
|
91
|
+
const meetings = useReport<TrendRow[]>(TREND_SERIES.meetings.path, { days });
|
|
92
|
+
const s = summary.data;
|
|
93
|
+
const sent = seriesOf(email.data, "sent");
|
|
94
|
+
const replied = seriesOf(email.data, "replied");
|
|
95
|
+
const rate = sent.map((v, i) => (v > 0 ? (replied[i] / v) * 100 : 0));
|
|
96
|
+
const dials = seriesOf(calls.data, "dials");
|
|
97
|
+
const booked = seriesOf(meetings.data, "booked");
|
|
98
|
+
const items: StatItem[] = [
|
|
99
|
+
{ label: "Emails sent", value: formatNumber(s?.emails_sent), footer: <span className="text-xs text-muted-foreground">{`${formatPercent(s?.delivery_rate)} delivered`}</span>, sparkline: sent, delta: halfDelta(sent) },
|
|
100
|
+
{ label: "Reply rate", value: formatPercent(s?.reply_rate), footer: <span className="text-xs text-muted-foreground">{`${formatNumber(s?.emails_replied)} replies`}</span>, sparkline: rate, delta: halfDelta(rate) },
|
|
101
|
+
{ label: "Calls placed", value: formatNumber(s?.calls_dispatched), footer: <span className="text-xs text-muted-foreground">{`${formatPercent(s?.connect_rate)} connected`}</span>, sparkline: dials, delta: halfDelta(dials) },
|
|
102
|
+
{ label: "Meetings booked", value: formatNumber(s?.meetings_booked), footer: <span className="text-xs text-muted-foreground">{`${formatNumber(s?.meetings_active)} still on the calendar`}</span>, sparkline: booked, delta: halfDelta(booked) },
|
|
103
|
+
];
|
|
104
|
+
const refetch = () => { void summary.refetch(); void email.refetch(); void calls.refetch(); void meetings.refetch(); };
|
|
105
|
+
return { items, loading: summary.loading && !summary.data, error: summary.error, stale: summary.stale, refetch };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function Overview({ days, nameFor }: { days: number; nameFor?: (email: string) => string }) {
|
|
109
|
+
const kpis = useKpis(days);
|
|
110
|
+
const [channel, setChannel] = useState<TrendChannel>("email");
|
|
111
|
+
const [source, setSource] = useState<"meetings" | "revenue">("meetings");
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div className="space-y-4">
|
|
115
|
+
{kpis.error ? (
|
|
116
|
+
<ApiError error={kpis.error} title="Could not load metrics" onRetry={kpis.refetch} />
|
|
117
|
+
) : <StatStrip items={kpis.items} loading={kpis.loading} />}
|
|
118
|
+
{kpis.stale ? <p role="status" className="text-xs text-muted-foreground">These numbers are from a recent cache because a refresh failed.</p> : null}
|
|
119
|
+
<SectionCard title="Daily activity" description="One line for volume, one dashed line for outcomes." actions={<Segmented label="Channel" value={channel} options={CHANNELS} onChange={setChannel} />}>
|
|
120
|
+
<TrendChart channel={channel} days={days} />
|
|
121
|
+
</SectionCard>
|
|
122
|
+
<div className="grid gap-4 lg:grid-cols-2">
|
|
123
|
+
<SectionCard title="Funnel" actions={<Segmented label="Funnel source" value={source} options={[{ id: "meetings", label: "Meetings" }, { id: "revenue", label: "Revenue" }]} onChange={setSource} />}>
|
|
124
|
+
<FunnelChart source={source} days={days} />
|
|
125
|
+
</SectionCard>
|
|
126
|
+
<SectionCard title="Rep leaderboard" description="Ranked by reply rate. Click a column to re-sort.">
|
|
127
|
+
<Leaderboard days={days} hideRange nameFor={nameFor} />
|
|
128
|
+
</SectionCard>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Reports per read; "Load more" appends the next page (the list used to stop silently at 50). */
|
|
135
|
+
const REPORT_PAGE = 50;
|
|
136
|
+
|
|
137
|
+
function ReportsPane({ objectSlug }: { objectSlug?: string }) {
|
|
138
|
+
const brand = useBrand();
|
|
139
|
+
const [selected, setSelected] = useUrlState("id", "");
|
|
140
|
+
const [view, setView] = useUrlState("view", "");
|
|
141
|
+
const [search, setSearch] = useState("");
|
|
142
|
+
const [creating, setCreating] = useState(false);
|
|
143
|
+
// Results live here so switching between Result and Edit never runs the report again (each run is a write).
|
|
144
|
+
const [results, setResults] = useState<Record<string, ReportResultData | null>>({});
|
|
145
|
+
const list = useLoadMoreList(byOffset<SavedReport>(REPORT_PAGE, async (offset, signal) => {
|
|
146
|
+
const env = await g8Get("/reports", { object_slug: objectSlug || undefined, limit: REPORT_PAGE, offset: offset || undefined }, { signal });
|
|
147
|
+
const rows = reportBody<{ reports?: SavedReport[] }>(env).reports ?? [];
|
|
148
|
+
return { rows, hasNext: hasNextOf(env, REPORT_PAGE, rows.length) };
|
|
149
|
+
}), [objectSlug]);
|
|
150
|
+
const switchedOff = (list.error as { status?: number } | undefined)?.status === 404;
|
|
151
|
+
const q = search.trim().toLowerCase();
|
|
152
|
+
const rows = (list.data ?? []).filter((r) => !q || `${r.title} ${r.source.plural_noun}`.toLowerCase().includes(q));
|
|
153
|
+
const open = (list.data ?? []).find((r) => r.id === selected);
|
|
154
|
+
// A deep link to a report on a later page: keep loading pages until it is found.
|
|
155
|
+
useEffect(() => { if (selected && !open && list.hasMore && !list.loading && !list.loadingMore) list.loadMore(); }, [selected, open, list.hasMore, list.loading, list.loadingMore]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
156
|
+
|
|
157
|
+
const detail = open ? (
|
|
158
|
+
<DetailPanel title={open.title} subtitle={open.source.plural_noun} badges={<StatusPill tone={open.visibility === "private" ? "neutral" : "info"}>{open.visibility}</StatusPill>} onClose={() => { setSelected(""); setView(""); }} onExpand={() => setView("full")}
|
|
159
|
+
tabs={[
|
|
160
|
+
{ id: "result", label: "Result", content: <ReportDetail key={open.id} reportId={open.id} result={results[open.id]} onResult={(r) => setResults((m) => ({ ...m, [open.id]: r }))} onDeleted={() => { setSelected(""); void list.refetch(); }} /> },
|
|
161
|
+
{ id: "edit", label: "Edit", content: <ReportBuilder key={open.id} hideList reportId={open.id} autoRun={false} onSaved={() => { setResults((m) => Object.fromEntries(Object.entries(m).filter(([k]) => k !== open.id))); void list.refetch(); }} /> }, // a saved edit changes the query: drop the stale result
|
|
162
|
+
]} />
|
|
163
|
+
) : selected && !list.loading && !list.hasMore ? <EmptyState title="Report not found" description="It may have been deleted." /> : null;
|
|
164
|
+
|
|
165
|
+
const listView = (
|
|
166
|
+
<div className="flex flex-col gap-3">
|
|
167
|
+
<Toolbar search={{ value: search, onChange: setSearch, placeholder: "Search reports" }} status={list.data ? `${rows.length} of ${list.data.length}` : undefined}
|
|
168
|
+
actions={<Button size="sm" onClick={() => setCreating(true)}><Plus className="mr-1 size-4" />New report</Button>} />
|
|
169
|
+
{switchedOff ? <EmptyState title="Reports are not enabled" description={`Custom-object reports are switched on per organisation in ${brand.linkLabel}.`} /> : list.error ? <ApiError error={list.error} title="Could not load saved reports" onRetry={() => void list.refetch()} /> : null}
|
|
170
|
+
{list.loading && !list.data ? <Skeleton className="h-32 w-full" aria-label="Loading reports" /> : null}
|
|
171
|
+
<ul className="rounded-md border">
|
|
172
|
+
{rows.map((r) => (
|
|
173
|
+
<li key={r.id} className={r.id === selected ? "bg-accent" : ""}>
|
|
174
|
+
<button type="button" data-testid={`report-${r.id}`} onClick={() => setSelected(r.id)} className="flex w-full items-center gap-3 border-b px-3 py-2 text-left">
|
|
175
|
+
<span className="min-w-0 flex-1"><span className="block truncate text-sm font-medium">{r.title}</span><span className="block truncate text-xs text-muted-foreground">{r.source.plural_noun} - {r.chart_type}</span></span>
|
|
176
|
+
<StatusPill tone={r.visibility === "private" ? "neutral" : "info"}>{r.visibility}</StatusPill>
|
|
177
|
+
</button>
|
|
178
|
+
</li>
|
|
179
|
+
))}
|
|
180
|
+
</ul>
|
|
181
|
+
<LoadMore shown={(list.data ?? []).length} noun="reports" hasMore={list.hasMore} loading={list.loadingMore} onLoadMore={list.loadMore} />
|
|
182
|
+
{!list.loading && !list.error && rows.length === 0 ? <EmptyState title={list.data?.length ? "No reports match" : "No saved reports yet"} description="Pick an object, a measure and a grouping, preview it, then save it."
|
|
183
|
+
action={list.data?.length ? undefined : <Button onClick={() => setCreating(true)}>New report</Button>} /> : null}
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
return (
|
|
187
|
+
<>
|
|
188
|
+
<SplitView list={listView} detail={detail} detailOpen={Boolean(selected)} onCloseDetail={() => { setSelected(""); setView(""); }} expanded={view === "full"} onExpandedChange={(e) => setView(e ? "full" : "")} detailWidth="xl" />
|
|
189
|
+
<FormSheet open={creating} onOpenChange={setCreating} title="New report" width="xl" description="Preview as often as you like; nothing is saved until you save.">
|
|
190
|
+
<ReportBuilder hideList objectSlug={objectSlug} onSaved={(r) => { setCreating(false); setSelected(r.id); void list.refetch(); }} />
|
|
191
|
+
</FormSheet>
|
|
192
|
+
</>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function AnalyticsScreen({ defaultDays = 30, nameFor, objectSlug, toolbarActions, className }: AnalyticsScreenProps) {
|
|
197
|
+
const [tab, setTab] = useUrlState("tab", "overview");
|
|
198
|
+
const [daysText, setDaysText] = useUrlState("days", String(defaultDays));
|
|
199
|
+
const parsed = Number(daysText);
|
|
200
|
+
const days = (DAY_RANGES as readonly number[]).includes(parsed) ? parsed : defaultDays;
|
|
201
|
+
const active = TABS.some((t) => t.id === tab) ? tab : "overview";
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<ScreenLayout
|
|
205
|
+
className={className}
|
|
206
|
+
title="Analytics"
|
|
207
|
+
description="How outbound is performing: volume, replies, calls, meetings and your reps."
|
|
208
|
+
icon={active === "reports" ? <FileText className="size-5" /> : <BarChart3 className="size-5" />}
|
|
209
|
+
tabs={TABS}
|
|
210
|
+
activeTab={active}
|
|
211
|
+
onTabChange={setTab}
|
|
212
|
+
actions={
|
|
213
|
+
<div className="flex items-center gap-2">
|
|
214
|
+
{active === "overview" ? <Segmented label="Time range" value={String(days)} options={DAY_RANGES.map((d) => ({ id: String(d), label: `Last ${d} days` }))} onChange={setDaysText} /> : null}
|
|
215
|
+
{toolbarActions}
|
|
216
|
+
</div>
|
|
217
|
+
}
|
|
218
|
+
>
|
|
219
|
+
{active === "reports" ? <ReportsPane objectSlug={objectSlug} /> : <Overview days={days} nameFor={nameFor} />}
|
|
220
|
+
</ScreenLayout>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
@@ -61,7 +61,7 @@ export function KpiTiles({ days: initialDays = 30, tiles = DEFAULT_TILES, classN
|
|
|
61
61
|
return (
|
|
62
62
|
<div key={t.key} className="rounded-lg border p-4">
|
|
63
63
|
<div className="text-xs uppercase text-muted-foreground">{t.label}</div>
|
|
64
|
-
<div className="mt-1 text-2xl font-
|
|
64
|
+
<div className="mt-1 text-2xl font-medium">{t.kind === "percent" ? formatPercent(raw) : formatNumber(raw)}</div>
|
|
65
65
|
{data && t.hint ? <div className="mt-1 text-xs text-muted-foreground">{t.hint(data)}</div> : null}
|
|
66
66
|
</div>
|
|
67
67
|
);
|
|
@@ -15,6 +15,8 @@ import { ApiError } from "@/g8/components/api-error";
|
|
|
15
15
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
16
16
|
import { useReport } from "../hooks/use-report";
|
|
17
17
|
import { DAY_RANGES, formatNumber, formatPercent, type LeaderRow } from "../lib/analytics-types";
|
|
18
|
+
import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
19
|
+
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
18
20
|
|
|
19
21
|
type SortKey = "reply_rate" | "emails_sent" | "replies" | "meetings_booked";
|
|
20
22
|
|
|
@@ -24,6 +26,8 @@ export interface LeaderboardProps {
|
|
|
24
26
|
/** Map an email to a display name. */
|
|
25
27
|
nameFor?: (email: string) => string;
|
|
26
28
|
onSelectRep?: (email: string) => void;
|
|
29
|
+
/** Hide the built-in 7/30/90 buttons when the host (for example AnalyticsScreen) owns the range; `days` then drives it. */
|
|
30
|
+
hideRange?: boolean;
|
|
27
31
|
className?: string;
|
|
28
32
|
}
|
|
29
33
|
|
|
@@ -34,17 +38,20 @@ const COLUMNS: ReadonlyArray<{ key: SortKey; label: string; percent?: boolean }>
|
|
|
34
38
|
{ key: "meetings_booked", label: "Meetings" },
|
|
35
39
|
];
|
|
36
40
|
|
|
37
|
-
export function Leaderboard({ days: initialDays = 30, limit = 20, nameFor, onSelectRep, className }: LeaderboardProps) {
|
|
38
|
-
const [
|
|
41
|
+
export function Leaderboard({ days: initialDays = 30, limit = 20, nameFor, onSelectRep, hideRange, className }: LeaderboardProps) {
|
|
42
|
+
const [ownDays, setDays] = useState(initialDays);
|
|
43
|
+
const days = hideRange ? initialDays : ownDays;
|
|
39
44
|
const [sortKey, setSortKey] = useState<SortKey>("reply_rate");
|
|
40
45
|
const { data, stale, loading, error, refetch } = useReport<LeaderRow[]>("/analytics/outbound/sdr-activity/leaderboard", { days, limit });
|
|
41
46
|
const rows = useMemo(() => [...(Array.isArray(data) ? data : [])].sort((a, b) => Number(b[sortKey] ?? 0) - Number(a[sortKey] ?? 0)), [data, sortKey]);
|
|
42
47
|
|
|
43
48
|
return (
|
|
44
49
|
<section className={`space-y-3 ${className ?? ""}`} aria-label="Rep leaderboard">
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
{hideRange ? null : (
|
|
51
|
+
<div role="group" aria-label="Time range" className="flex gap-1">
|
|
52
|
+
{DAY_RANGES.map((d) => <Button key={d} size="sm" variant={days === d ? "default" : "outline"} onClick={() => setDays(d)}>Last {d} days</Button>)}
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
48
55
|
{error ? (
|
|
49
56
|
<ApiError error={error} title="Could not load the leaderboard" onRetry={() => void refetch()} />
|
|
50
57
|
) : loading && !data ? (
|
|
@@ -68,9 +75,9 @@ export function Leaderboard({ days: initialDays = 30, limit = 20, nameFor, onSel
|
|
|
68
75
|
{rows.map((r, i) => {
|
|
69
76
|
const email = r.sdr_email ?? "unknown";
|
|
70
77
|
return (
|
|
71
|
-
<TableRow key={email} className={onSelectRep ? "cursor-pointer" : undefined}
|
|
78
|
+
<TableRow key={email} className={onSelectRep ? "cursor-pointer" : undefined} {...(onSelectRep ? rowActivationProps(() => onSelectRep(email)) : {})}>
|
|
72
79
|
<TableCell>{i + 1}</TableCell>
|
|
73
|
-
<TableCell className="font-medium">{nameFor ? nameFor(email) : email}</TableCell>
|
|
80
|
+
<TableCell className="font-medium">{onSelectRep ? <RecordLink id={email} onOpen={() => onSelectRep(email)}>{nameFor ? nameFor(email) : email}</RecordLink> : nameFor ? nameFor(email) : email}</TableCell>
|
|
74
81
|
{COLUMNS.map((c) => (
|
|
75
82
|
<TableCell key={c.key} className="text-right">{c.percent ? formatPercent(r[c.key] as number | undefined) : formatNumber(r[c.key] as number | undefined)}</TableCell>
|
|
76
83
|
))}
|
|
@@ -16,9 +16,12 @@
|
|
|
16
16
|
* Preview and run share the org's report-run slots (429 `too_many_report_runs` when all are busy: retry shortly).
|
|
17
17
|
* "write" tier on preview/execute is the contract's classification (they create run records); nothing is sent.
|
|
18
18
|
*/
|
|
19
|
-
import { useState } from "react";
|
|
19
|
+
import { useEffect, useRef, useState } from "react";
|
|
20
20
|
import { Button, Input, Label, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
21
21
|
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
|
|
23
|
+
import { byOffset, hasNextOf } from "@/g8/lib/paging";
|
|
24
|
+
import { LoadMore } from "@/g8/components/paging-notices";
|
|
22
25
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
23
26
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
24
27
|
import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
@@ -26,11 +29,18 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
|
26
29
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
27
30
|
import { useDebounced } from "../lib/use-debounced";
|
|
28
31
|
import { reportBody, type ReportResultData, type ResultRow, type SavedReport } from "../lib/analytics-types";
|
|
32
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
29
33
|
|
|
30
34
|
export interface ReportBuilderProps {
|
|
31
35
|
/** Pre-select the object (its slug), for example from the objects block. */
|
|
32
36
|
objectSlug?: string;
|
|
33
37
|
onSaved?: (report: SavedReport) => void;
|
|
38
|
+
/** Open this saved report for editing as soon as it is loaded (it is run once, like clicking it in the list). */
|
|
39
|
+
reportId?: string;
|
|
40
|
+
/** Hide the "Saved reports" column when the host (for example AnalyticsScreen) shows its own list. */
|
|
41
|
+
hideList?: boolean;
|
|
42
|
+
/** With `reportId`: run the report once when it is loaded (default true). False only loads the definition. */
|
|
43
|
+
autoRun?: boolean;
|
|
34
44
|
className?: string;
|
|
35
45
|
}
|
|
36
46
|
|
|
@@ -80,7 +90,7 @@ function groupLabel(g: ResultRow["group"][number]): string {
|
|
|
80
90
|
return String(g);
|
|
81
91
|
}
|
|
82
92
|
|
|
83
|
-
function ResultTable({ result }: { result: ReportResultData }) {
|
|
93
|
+
export function ResultTable({ result }: { result: ReportResultData }) {
|
|
84
94
|
const rows = result.rows ?? [];
|
|
85
95
|
if (result.status === "restricted") return <p role="alert" className="text-sm">You do not have access to every field in this report.</p>;
|
|
86
96
|
if (rows.length === 0) return <EmptyState title="No records match" description="Change the object, field or grouping and preview again." />;
|
|
@@ -107,7 +117,11 @@ function ResultTable({ result }: { result: ReportResultData }) {
|
|
|
107
117
|
);
|
|
108
118
|
}
|
|
109
119
|
|
|
110
|
-
|
|
120
|
+
/** Reports per read; "Load more" appends the next page (the list used to stop silently at 50). */
|
|
121
|
+
const REPORT_PAGE = 50;
|
|
122
|
+
|
|
123
|
+
export function ReportBuilder({ objectSlug: initialSlug = "", onSaved, reportId, hideList, autoRun = true, className }: ReportBuilderProps) {
|
|
124
|
+
const brand = useBrand();
|
|
111
125
|
const [slug, setSlug] = useState(initialSlug);
|
|
112
126
|
const [draft, setDraft] = useState<Draft>({ title: "", chart: "table", measure: "count", measureField: "", groupBy: "", grain: "month" });
|
|
113
127
|
const [loaded, setLoaded] = useState<SavedReport | null>(null);
|
|
@@ -116,7 +130,11 @@ export function ReportBuilder({ objectSlug: initialSlug = "", onSaved, className
|
|
|
116
130
|
const set = <K extends keyof Draft>(k: K, v: Draft[K]) => setDraft((d) => ({ ...d, [k]: v }));
|
|
117
131
|
|
|
118
132
|
const listSlug = useDebounced(slug.trim());
|
|
119
|
-
const saved =
|
|
133
|
+
const saved = useLoadMoreList(byOffset<SavedReport>(REPORT_PAGE, async (offset, signal) => {
|
|
134
|
+
const env = await g8Get("/reports", { object_slug: listSlug || undefined, limit: REPORT_PAGE, offset: offset || undefined }, { signal });
|
|
135
|
+
const rows = reportBody<{ reports?: SavedReport[] }>(env).reports ?? [];
|
|
136
|
+
return { rows, hasNext: hasNextOf(env, REPORT_PAGE, rows.length) };
|
|
137
|
+
}), [listSlug]);
|
|
120
138
|
const body = () => ({ object_slug: slug, chart_type: draft.chart, config: configOf(draft, loaded?.config as Cfg) });
|
|
121
139
|
const preview = useG8Mutation(async () => { const r = reportBody<ReportResultData>(await g8Post("/reports/preview", body())); setResult(r); return r; });
|
|
122
140
|
const run = useG8Mutation(async (r: SavedReport) => { const out = reportBody<ReportResultData>(await g8Post(`/reports/${encodeURIComponent(r.id)}/execute`, {})); setResult(out); return out; });
|
|
@@ -143,28 +161,37 @@ export function ReportBuilder({ objectSlug: initialSlug = "", onSaved, className
|
|
|
143
161
|
remove.reset();
|
|
144
162
|
};
|
|
145
163
|
|
|
146
|
-
const open = (r: SavedReport) => {
|
|
164
|
+
const open = (r: SavedReport, execute = true) => {
|
|
147
165
|
setLoaded(r);
|
|
148
166
|
setSlug(r.source.slug);
|
|
149
167
|
setDraft((d) => draftFromReport(r, d));
|
|
150
|
-
void run.mutate(r);
|
|
168
|
+
if (execute) void run.mutate(r);
|
|
151
169
|
};
|
|
170
|
+
// Preload `reportId` once its row arrives (and again if the id changes).
|
|
171
|
+
const preloaded = useRef<string | null>(null);
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
const row = reportId ? (saved.data ?? []).find((r) => r.id === reportId) : undefined;
|
|
174
|
+
if (row && preloaded.current !== reportId) { preloaded.current = reportId ?? null; open(row, autoRun); }
|
|
175
|
+
// The report sits on a later page: keep loading pages until it is found.
|
|
176
|
+
else if (reportId && !row && saved.hasMore && !saved.loading && !saved.loadingMore) saved.loadMore();
|
|
177
|
+
}); // eslint-disable-line react-hooks/exhaustive-deps
|
|
152
178
|
const failure = preview.error ?? run.error ?? save.error ?? remove.error;
|
|
153
179
|
const switchedOff = (saved.error as { status?: number } | undefined)?.status === 404;
|
|
154
180
|
|
|
155
181
|
return (
|
|
156
|
-
<div className={`grid gap-4 lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)] ${className ?? ""}`}>
|
|
157
|
-
<section aria-label="Saved reports" className="space-y-2">
|
|
158
|
-
<h3 className="text-sm font-
|
|
159
|
-
{switchedOff ? <EmptyState title="Reports are not enabled" description=
|
|
182
|
+
<div className={`grid gap-4 ${hideList ? "" : "lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]"} ${className ?? ""}`}>
|
|
183
|
+
{hideList ? null : <section aria-label="Saved reports" className="space-y-2">
|
|
184
|
+
<h3 className="text-sm font-medium">Saved reports</h3>
|
|
185
|
+
{switchedOff ? <EmptyState title="Reports are not enabled" description={`Custom-object reports are switched on per organisation in ${brand.linkLabel}.`} /> : saved.error ? <ApiError error={saved.error} title="Could not load saved reports" onRetry={() => void saved.refetch()} /> : null}
|
|
160
186
|
{(saved.data ?? []).map((r) => (
|
|
161
187
|
<div key={r.id} className="flex items-center justify-between rounded border p-2 text-sm">
|
|
162
188
|
<button type="button" className="text-left hover:underline" onClick={() => open(r)}>{r.title} <span className="text-muted-foreground">({r.source.plural_noun})</span></button>
|
|
163
189
|
{r.can_delete ? <Button size="sm" variant="ghost" aria-label={`Delete ${r.title}`} onClick={() => setDeleting(r)}>Delete</Button> : null}
|
|
164
190
|
</div>
|
|
165
191
|
))}
|
|
192
|
+
<LoadMore shown={(saved.data ?? []).length} noun="reports" hasMore={saved.hasMore} loading={saved.loadingMore} onLoadMore={saved.loadMore} />
|
|
166
193
|
{!saved.loading && !saved.error && (saved.data ?? []).length === 0 ? <p className="text-sm text-muted-foreground">Nothing saved yet.</p> : null}
|
|
167
|
-
</section>
|
|
194
|
+
</section>}
|
|
168
195
|
|
|
169
196
|
<section aria-label="Report definition" className="space-y-3">
|
|
170
197
|
<div className="grid gap-3 sm:grid-cols-2">
|