@graph8/sdk 0.303.0 → 0.357.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -0
- package/blocks/README.md +32 -0
- package/blocks/_foundation/README.md +78 -0
- package/blocks/_foundation/block.json +79 -0
- package/blocks/_foundation/components/api-error.tsx +62 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +142 -0
- package/blocks/_foundation/components/empty-state.tsx +24 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +88 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +87 -0
- package/blocks/_foundation/hooks/use-job-poll.ts +130 -0
- package/blocks/_foundation/hooks/use-paginated.ts +105 -0
- package/blocks/_foundation/lib/g8-client.ts +128 -0
- package/blocks/_foundation/recipes/verify-proxy-key.ts +18 -0
- package/blocks/_foundation/server/proxy.ts +233 -0
- package/blocks/_foundation/server/route.ts +30 -0
- package/blocks/agents/README.md +133 -0
- package/blocks/agents/block.json +223 -0
- package/blocks/agents/components/agent-builder.tsx +199 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +144 -0
- package/blocks/agents/components/agent-runs.tsx +175 -0
- package/blocks/agents/components/agent-skills-panel.tsx +126 -0
- package/blocks/agents/components/agent-test-console.tsx +175 -0
- package/blocks/agents/lib/agent-types.ts +114 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +50 -0
- package/blocks/agents/recipes/run-and-follow-agent-run.ts +46 -0
- package/blocks/agents/recipes/test-agent-before-activating.ts +36 -0
- package/blocks/analytics/README.md +73 -0
- package/blocks/analytics/block.json +178 -0
- package/blocks/analytics/components/funnel-chart.tsx +79 -0
- package/blocks/analytics/components/kpi-tiles.tsx +74 -0
- package/blocks/analytics/components/leaderboard.tsx +86 -0
- package/blocks/analytics/components/report-builder.tsx +200 -0
- package/blocks/analytics/hooks/use-report.ts +26 -0
- package/blocks/analytics/lib/analytics-types.ts +108 -0
- package/blocks/analytics/lib/use-debounced.ts +16 -0
- package/blocks/analytics/recipes/01-weekly-outbound-report.ts +32 -0
- package/blocks/analytics/recipes/02-revenue-snapshot.ts +22 -0
- package/blocks/analytics/recipes/03-saved-report.ts +30 -0
- package/blocks/appointments/README.md +80 -0
- package/blocks/appointments/block.json +218 -0
- package/blocks/appointments/components/availability-editor.tsx +192 -0
- package/blocks/appointments/components/booking-page.tsx +154 -0
- package/blocks/appointments/components/event-type-editor.tsx +214 -0
- package/blocks/appointments/components/meeting-detail.tsx +147 -0
- package/blocks/appointments/components/meeting-list.tsx +148 -0
- package/blocks/appointments/hooks/use-available-slots.ts +29 -0
- package/blocks/appointments/hooks/use-schedules.ts +18 -0
- package/blocks/appointments/lib/appointments-types.ts +159 -0
- package/blocks/appointments/lib/idempotency.ts +30 -0
- package/blocks/appointments/recipes/01-create-event-type-and-hours.ts +27 -0
- package/blocks/appointments/recipes/02-find-slots-and-book.ts +34 -0
- package/blocks/appointments/recipes/03-bookings-digest.ts +22 -0
- package/blocks/appointments/recipes/04-calendar-invite.ts +21 -0
- package/blocks/block.schema.json +130 -0
- package/blocks/block.types.ts +109 -0
- package/blocks/campaigns/README.md +118 -0
- package/blocks/campaigns/block.json +361 -0
- package/blocks/campaigns/components/campaign-assets.tsx +180 -0
- package/blocks/campaigns/components/campaign-audience-step.tsx +91 -0
- package/blocks/campaigns/components/campaign-launch-step.tsx +147 -0
- package/blocks/campaigns/components/campaign-list.tsx +120 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +93 -0
- package/blocks/campaigns/components/campaign-select.tsx +52 -0
- package/blocks/campaigns/components/campaign-sequence-step.tsx +158 -0
- package/blocks/campaigns/components/campaign-wizard.tsx +238 -0
- package/blocks/campaigns/components/form-field.tsx +29 -0
- package/blocks/campaigns/components/icp-editor.tsx +230 -0
- package/blocks/campaigns/components/persona-editor.tsx +210 -0
- package/blocks/campaigns/hooks/use-company-profile.ts +34 -0
- package/blocks/campaigns/lib/campaigns-utils.ts +155 -0
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +33 -0
- package/blocks/campaigns/recipes/campaign-report.ts +22 -0
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +51 -0
- package/blocks/campaigns/recipes/set-up-icp-and-personas.ts +37 -0
- package/blocks/coach/README.md +93 -0
- package/blocks/coach/block.json +125 -0
- package/blocks/coach/components/call-scorecard.tsx +126 -0
- package/blocks/coach/components/coaching-feed.tsx +140 -0
- package/blocks/coach/lib/coach-types.ts +116 -0
- package/blocks/coach/recipes/read-a-scorecard.ts +27 -0
- package/blocks/coach/recipes/weekly-coaching-digest.ts +53 -0
- package/blocks/copilot/README.md +99 -0
- package/blocks/copilot/block.json +151 -0
- package/blocks/copilot/components/copilot-memory.tsx +87 -0
- package/blocks/copilot/components/copilot-panel.tsx +189 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +111 -0
- package/blocks/copilot/lib/copilot-types.ts +111 -0
- package/blocks/copilot/recipes/ask-the-copilot.ts +35 -0
- package/blocks/copilot/recipes/post-in-a-work-channel.ts +33 -0
- package/blocks/copilot/recipes/remember-and-recall.ts +19 -0
- package/blocks/crm/README.md +122 -0
- package/blocks/crm/block.json +378 -0
- package/blocks/crm/components/activity-timeline.tsx +99 -0
- package/blocks/crm/components/company-detail.tsx +118 -0
- package/blocks/crm/components/company-table.tsx +173 -0
- package/blocks/crm/components/contact-detail.tsx +135 -0
- package/blocks/crm/components/contact-table.tsx +217 -0
- package/blocks/crm/components/dedupe-review.tsx +164 -0
- package/blocks/crm/components/import-wizard.tsx +193 -0
- package/blocks/crm/components/notes-panel.tsx +112 -0
- package/blocks/crm/components/record-form.tsx +191 -0
- package/blocks/crm/components/task-list.tsx +107 -0
- package/blocks/crm/hooks/use-custom-fields.ts +18 -0
- package/blocks/crm/hooks/use-debounced.ts +13 -0
- package/blocks/crm/hooks/use-page-query.ts +52 -0
- package/blocks/crm/lib/crm-types.ts +136 -0
- package/blocks/crm/recipes/custom-fields-and-values.ts +28 -0
- package/blocks/crm/recipes/dedupe-and-merge.ts +30 -0
- package/blocks/crm/recipes/notes-tasks-and-activity.ts +23 -0
- package/blocks/crm/recipes/upsert-contact-with-company.ts +33 -0
- package/blocks/deals/README.md +78 -0
- package/blocks/deals/block.json +198 -0
- package/blocks/deals/components/deal-detail.tsx +134 -0
- package/blocks/deals/components/forecast-view.tsx +112 -0
- package/blocks/deals/components/pipeline-board.tsx +142 -0
- package/blocks/deals/components/stage-editor.tsx +96 -0
- package/blocks/deals/hooks/use-pipeline-deals.ts +67 -0
- package/blocks/deals/hooks/use-pipelines.ts +19 -0
- package/blocks/deals/lib/deals-types.ts +128 -0
- package/blocks/deals/recipes/01-create-deal-and-advance.ts +32 -0
- package/blocks/deals/recipes/02-pipeline-report.ts +35 -0
- package/blocks/deals/recipes/03-stale-deals-sweep.ts +20 -0
- package/blocks/dialer/README.md +114 -0
- package/blocks/dialer/block.json +260 -0
- package/blocks/dialer/components/call-log.tsx +78 -0
- package/blocks/dialer/components/call-queue.tsx +188 -0
- package/blocks/dialer/components/dialer-workspace.tsx +47 -0
- package/blocks/dialer/components/disposition-panel.tsx +75 -0
- package/blocks/dialer/components/live-calls-table.tsx +107 -0
- package/blocks/dialer/components/session-start-form.tsx +111 -0
- package/blocks/dialer/components/softphone.tsx +122 -0
- package/blocks/dialer/components/transcript-viewer.tsx +82 -0
- package/blocks/dialer/hooks/use-dialer-numbers.ts +19 -0
- package/blocks/dialer/hooks/use-dialer-session.ts +68 -0
- package/blocks/dialer/hooks/use-dispositions.ts +29 -0
- package/blocks/dialer/lib/dialer-types.ts +158 -0
- package/blocks/dialer/lib/softphone-transport.ts +67 -0
- package/blocks/dialer/lib/twilio-transport.ts +74 -0
- package/blocks/dialer/recipes/01-create-session-and-dial.ts +39 -0
- package/blocks/dialer/recipes/02-disposition-calls.ts +26 -0
- package/blocks/dialer/recipes/03-review-a-call.ts +23 -0
- package/blocks/enrichment/README.md +105 -0
- package/blocks/enrichment/block.json +204 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +205 -0
- package/blocks/enrichment/components/email-verify-badge.tsx +82 -0
- package/blocks/enrichment/components/enrich-button.tsx +136 -0
- package/blocks/enrichment/components/waterfall-config.tsx +127 -0
- package/blocks/enrichment/hooks/use-providers.ts +18 -0
- package/blocks/enrichment/lib/enrichment-types.ts +104 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +41 -0
- package/blocks/enrichment/recipes/lookup-and-verify.ts +25 -0
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +26 -0
- package/blocks/forms/README.md +112 -0
- package/blocks/forms/block.json +200 -0
- package/blocks/forms/components/form-builder.tsx +150 -0
- package/blocks/forms/components/form-embed.tsx +103 -0
- package/blocks/forms/components/form-mapping-editor.tsx +125 -0
- package/blocks/forms/components/site-audit-panel.tsx +73 -0
- package/blocks/forms/components/submissions-table.tsx +158 -0
- package/blocks/forms/lib/form-types.ts +127 -0
- package/blocks/forms/recipes/install-snippet-and-form.ts +21 -0
- package/blocks/forms/recipes/retry-failed-deliveries.ts +26 -0
- package/blocks/forms/recipes/route-form-to-list.ts +44 -0
- package/blocks/inbox/README.md +72 -0
- package/blocks/inbox/block.json +194 -0
- package/blocks/inbox/components/inbox-list.tsx +156 -0
- package/blocks/inbox/components/inbox-screen.tsx +49 -0
- package/blocks/inbox/components/reply-composer.tsx +183 -0
- package/blocks/inbox/components/tag-and-assign.tsx +189 -0
- package/blocks/inbox/components/thread-badge.tsx +17 -0
- package/blocks/inbox/components/thread-view.tsx +108 -0
- package/blocks/inbox/hooks/use-inbox-thread.ts +24 -0
- package/blocks/inbox/hooks/use-inbox-threads.ts +40 -0
- package/blocks/inbox/lib/idempotency.ts +30 -0
- package/blocks/inbox/lib/inbox-types.ts +100 -0
- package/blocks/inbox/recipes/01-triage-and-reply.ts +43 -0
- package/blocks/inbox/recipes/02-tag-and-assign.ts +31 -0
- package/blocks/inbox/recipes/03-hot-contacts-digest.ts +16 -0
- package/blocks/integrations/README.md +75 -0
- package/blocks/integrations/block.json +172 -0
- package/blocks/integrations/components/integration-list.tsx +165 -0
- package/blocks/integrations/components/sync-status.tsx +223 -0
- package/blocks/integrations/components/webhook-manager.tsx +300 -0
- package/blocks/integrations/lib/integrations-utils.ts +62 -0
- package/blocks/integrations/recipes/crm-sync-health.ts +19 -0
- package/blocks/integrations/recipes/push-contacts-to-crm.ts +20 -0
- package/blocks/integrations/recipes/register-webhook.ts +18 -0
- package/blocks/integrations/recipes/verify-webhook-route.ts +33 -0
- package/blocks/intent/README.md +57 -0
- package/blocks/intent/block.json +138 -0
- package/blocks/intent/components/account-intent-score.tsx +91 -0
- package/blocks/intent/components/intent-feed.tsx +215 -0
- package/blocks/intent/components/intent-keyword-manager.tsx +331 -0
- package/blocks/intent/hooks/use-account-intent.ts +46 -0
- package/blocks/intent/lib/intent-utils.ts +95 -0
- package/blocks/intent/recipes/rank-intent-accounts.ts +36 -0
- package/blocks/intent/recipes/track-competitor-domain.ts +23 -0
- package/blocks/intent/recipes/who-visited-url.ts +18 -0
- package/blocks/lifecycle/README.md +79 -0
- package/blocks/lifecycle/block.json +198 -0
- package/blocks/lifecycle/components/churn-list.tsx +92 -0
- package/blocks/lifecycle/components/customer-table.tsx +119 -0
- package/blocks/lifecycle/components/lead-queue.tsx +127 -0
- package/blocks/lifecycle/components/lifecycle-board.tsx +102 -0
- package/blocks/lifecycle/components/stage-move-dialog.tsx +75 -0
- package/blocks/lifecycle/hooks/use-debounced.ts +13 -0
- package/blocks/lifecycle/hooks/use-lifecycle-stages.ts +27 -0
- package/blocks/lifecycle/lib/lifecycle.ts +99 -0
- package/blocks/lifecycle/recipes/customer-brief.ts +23 -0
- package/blocks/lifecycle/recipes/move-company-stage.ts +17 -0
- package/blocks/lifecycle/recipes/read-lifecycle-board.ts +21 -0
- package/blocks/lifecycle/recipes/unlock-stage-contacts.ts +25 -0
- package/blocks/lists/README.md +96 -0
- package/blocks/lists/block.json +166 -0
- package/blocks/lists/components/add-to-list-button.tsx +111 -0
- package/blocks/lists/components/list-picker.tsx +94 -0
- package/blocks/lists/components/list-table.tsx +160 -0
- package/blocks/lists/components/suppression-manager.tsx +153 -0
- package/blocks/lists/recipes/create-list-and-add-contacts.ts +35 -0
- package/blocks/lists/recipes/refresh-and-check-lists.ts +21 -0
- package/blocks/lists/recipes/suppress-and-reinstate.ts +30 -0
- package/blocks/mailboxes/README.md +71 -0
- package/blocks/mailboxes/block.json +187 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +284 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +271 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +185 -0
- package/blocks/mailboxes/components/warmup-health.tsx +232 -0
- package/blocks/mailboxes/lib/mailboxes-utils.ts +69 -0
- package/blocks/mailboxes/recipes/buy-domain-and-mailboxes.ts +64 -0
- package/blocks/mailboxes/recipes/connect-imap-mailbox.ts +38 -0
- package/blocks/mailboxes/recipes/is-it-safe-to-send.ts +17 -0
- package/blocks/mailboxes/recipes/warmup-report.ts +31 -0
- package/blocks/newsletter/README.md +122 -0
- package/blocks/newsletter/block.json +224 -0
- package/blocks/newsletter/components/issue-form.tsx +124 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +152 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +131 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +93 -0
- package/blocks/newsletter/components/subscriber-table.tsx +173 -0
- package/blocks/newsletter/lib/newsletter-types.ts +97 -0
- package/blocks/newsletter/recipes/create-newsletter-and-draft-issue.ts +27 -0
- package/blocks/newsletter/recipes/import-audience-and-send.ts +43 -0
- package/blocks/newsletter/recipes/issue-performance-report.ts +26 -0
- package/blocks/nurture/README.md +110 -0
- package/blocks/nurture/block.json +191 -0
- package/blocks/nurture/components/nurture-builder.tsx +114 -0
- package/blocks/nurture/components/nurture-enroll.tsx +107 -0
- package/blocks/nurture/components/nurture-form.tsx +120 -0
- package/blocks/nurture/components/nurture-stats.tsx +81 -0
- package/blocks/nurture/lib/nurture-types.ts +109 -0
- package/blocks/nurture/recipes/create-a-nurture.ts +34 -0
- package/blocks/nurture/recipes/enroll-contacts-and-launch.ts +24 -0
- package/blocks/nurture/recipes/nurture-health-check.ts +28 -0
- package/blocks/objects/README.md +120 -0
- package/blocks/objects/block.json +243 -0
- package/blocks/objects/components/attribute-field-input.tsx +82 -0
- package/blocks/objects/components/attribute-form-dialog.tsx +159 -0
- package/blocks/objects/components/object-not-found.tsx +34 -0
- package/blocks/objects/components/object-record-detail.tsx +143 -0
- package/blocks/objects/components/object-schema-editor.tsx +197 -0
- package/blocks/objects/components/object-table.tsx +130 -0
- package/blocks/objects/components/record-form-dialog.tsx +112 -0
- package/blocks/objects/components/record-roles-panel.tsx +106 -0
- package/blocks/objects/hooks/use-object-schema.ts +36 -0
- package/blocks/objects/lib/object-types.ts +214 -0
- package/blocks/objects/recipes/define-object-and-add-records.ts +24 -0
- package/blocks/objects/recipes/update-audit-and-archive-records.ts +39 -0
- package/blocks/objects/recipes/upsert-records-from-your-system.ts +30 -0
- package/blocks/pages/README.md +104 -0
- package/blocks/pages/block.json +223 -0
- package/blocks/pages/components/brand-kit-editor.tsx +152 -0
- package/blocks/pages/components/content-library.tsx +163 -0
- package/blocks/pages/components/page-editor.tsx +253 -0
- package/blocks/pages/components/page-list.tsx +159 -0
- package/blocks/pages/components/seo-report.tsx +159 -0
- package/blocks/pages/lib/studio.ts +77 -0
- package/blocks/pages/recipes/audit-site-seo.ts +28 -0
- package/blocks/pages/recipes/create-and-publish-page.ts +26 -0
- package/blocks/pages/recipes/edit-page-section.ts +25 -0
- package/blocks/pages/recipes/refresh-brand-kit.ts +22 -0
- package/blocks/quotes/README.md +79 -0
- package/blocks/quotes/block.json +183 -0
- package/blocks/quotes/components/product-picker.tsx +80 -0
- package/blocks/quotes/components/quote-builder.tsx +195 -0
- package/blocks/quotes/components/quote-list.tsx +79 -0
- package/blocks/quotes/components/quote-viewer.tsx +135 -0
- package/blocks/quotes/components/signature-status.tsx +77 -0
- package/blocks/quotes/hooks/use-debounced.ts +13 -0
- package/blocks/quotes/lib/quotes.ts +142 -0
- package/blocks/quotes/recipes/create-and-send-quote.ts +44 -0
- package/blocks/quotes/recipes/revise-sent-quote.ts +18 -0
- package/blocks/quotes/recipes/track-quote-signature.ts +22 -0
- package/blocks/radar/README.md +106 -0
- package/blocks/radar/block.json +189 -0
- package/blocks/radar/components/competitor-battle-card.tsx +53 -0
- package/blocks/radar/components/competitor-tracker.tsx +158 -0
- package/blocks/radar/components/radar-overview.tsx +67 -0
- package/blocks/radar/components/radar-report.tsx +86 -0
- package/blocks/radar/hooks/use-competitors.ts +23 -0
- package/blocks/radar/lib/radar-types.ts +124 -0
- package/blocks/radar/recipes/battle-card.ts +28 -0
- package/blocks/radar/recipes/deep-dive-report.ts +32 -0
- package/blocks/radar/recipes/marketing-snapshot.ts +18 -0
- package/blocks/radar/recipes/track-and-scan.ts +39 -0
- package/blocks/search/README.md +129 -0
- package/blocks/search/block.json +185 -0
- package/blocks/search/components/filter-builder.tsx +149 -0
- package/blocks/search/components/prospect-results.tsx +175 -0
- package/blocks/search/components/save-to-list-dialog.tsx +135 -0
- package/blocks/search/components/saved-searches.tsx +95 -0
- package/blocks/search/hooks/use-filter-options.ts +41 -0
- package/blocks/search/lib/search-types.ts +149 -0
- package/blocks/search/recipes/explore-filter-values.ts +27 -0
- package/blocks/search/recipes/saved-searches.ts +34 -0
- package/blocks/search/recipes/search-and-save-to-list.ts +37 -0
- package/blocks/sequencer/README.md +87 -0
- package/blocks/sequencer/block.json +214 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +93 -0
- package/blocks/sequencer/components/sequence-builder.tsx +306 -0
- package/blocks/sequencer/components/sequence-list.tsx +200 -0
- package/blocks/sequencer/components/sequence-stats.tsx +256 -0
- package/blocks/sequencer/components/step-editor.tsx +213 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +168 -0
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +51 -0
- package/blocks/sequencer/recipes/launch-and-enroll.ts +44 -0
- package/blocks/sequencer/recipes/pause-when-unsafe.ts +30 -0
- package/blocks/sequencer/recipes/sequence-report.ts +22 -0
- package/blocks/social-ads/README.md +128 -0
- package/blocks/social-ads/block.json +256 -0
- package/blocks/social-ads/components/ad-campaign-list.tsx +136 -0
- package/blocks/social-ads/components/audience-sync.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +121 -0
- package/blocks/social-ads/components/post-composer.tsx +122 -0
- package/blocks/social-ads/components/social-post-list.tsx +116 -0
- package/blocks/social-ads/hooks/use-social-accounts.ts +16 -0
- package/blocks/social-ads/lib/social-ads-types.ts +167 -0
- package/blocks/social-ads/recipes/creative-to-gallery.ts +41 -0
- package/blocks/social-ads/recipes/schedule-post.ts +51 -0
- package/blocks/social-ads/recipes/sync-audience-and-launch.ts +41 -0
- package/blocks/visitors/README.md +56 -0
- package/blocks/visitors/block.json +139 -0
- package/blocks/visitors/components/snippet-install.tsx +169 -0
- package/blocks/visitors/components/traffic-chart.tsx +156 -0
- package/blocks/visitors/components/visitor-company-card.tsx +92 -0
- package/blocks/visitors/components/visitor-feed.tsx +157 -0
- package/blocks/visitors/lib/visitors-utils.ts +78 -0
- package/blocks/visitors/recipes/check-snippet-installed.ts +24 -0
- package/blocks/visitors/recipes/recent-company-visits.ts +27 -0
- package/blocks/visitors/recipes/traffic-summary.ts +24 -0
- package/blocks/webchat/README.md +87 -0
- package/blocks/webchat/block.json +134 -0
- package/blocks/webchat/components/chat-widget-config.tsx +160 -0
- package/blocks/webchat/components/conversation-list.tsx +74 -0
- package/blocks/webchat/lib/webchat-types.ts +83 -0
- package/blocks/webchat/recipes/create-bot-and-style-widget.ts +24 -0
- package/blocks/webchat/recipes/list-chat-visitors.ts +24 -0
- package/blocks/workbench/README.md +135 -0
- package/blocks/workbench/block.json +259 -0
- package/blocks/workbench/components/column-recipe-editor.tsx +183 -0
- package/blocks/workbench/components/run-progress.tsx +132 -0
- package/blocks/workbench/components/workbench-add-rows.tsx +100 -0
- package/blocks/workbench/components/workbench-grid.tsx +159 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +102 -0
- package/blocks/workbench/components/workbench-table-list.tsx +105 -0
- package/blocks/workbench/hooks/use-workbench-table.ts +30 -0
- package/blocks/workbench/lib/waterfall-presets.ts +119 -0
- package/blocks/workbench/lib/workbench-types.ts +191 -0
- package/blocks/workbench/recipes/build-enriched-table.ts +65 -0
- package/blocks/workbench/recipes/import-from-search.ts +42 -0
- package/blocks/workbench/recipes/scrape-into-table.ts +36 -0
- package/blocks/workbench/recipes/waterfall-column.ts +55 -0
- package/blocks/workflows/README.md +100 -0
- package/blocks/workflows/block.json +188 -0
- package/blocks/workflows/components/run-history.tsx +136 -0
- package/blocks/workflows/components/trigger-config.tsx +134 -0
- package/blocks/workflows/components/workflow-editor.tsx +142 -0
- package/blocks/workflows/components/workflow-list.tsx +153 -0
- package/blocks/workflows/hooks/use-workflow.ts +51 -0
- package/blocks/workflows/lib/graph.ts +187 -0
- package/blocks/workflows/recipes/create-workflow.ts +35 -0
- package/blocks/workflows/recipes/find-failed-runs.ts +15 -0
- package/blocks/workflows/recipes/run-workflow-and-wait.ts +32 -0
- package/blocks/workflows/recipes/set-trigger.ts +22 -0
- package/dist/cli.js +5330 -29
- package/dist/index.browser.js +7740 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.browser.mjs +7717 -0
- package/dist/index.browser.mjs.map +1 -0
- package/dist/index.d.mts +2220 -23
- package/dist/index.d.ts +2220 -23
- package/dist/index.js +64 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +64 -3
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +7598 -0
- package/dist/react.browser.js.map +1 -0
- package/dist/react.browser.mjs +7568 -0
- package/dist/react.browser.mjs.map +1 -0
- package/dist/react.d.mts +2082 -20
- package/dist/react.d.ts +2082 -20
- package/dist/react.js +63 -2
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +63 -2
- package/dist/react.mjs.map +1 -1
- package/dist/types/agentSetup.d.ts +6 -6
- package/dist/types/blocks/add.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +46 -0
- package/dist/types/blocks/doctor.d.ts +26 -0
- package/dist/types/blocks/registry.d.ts +134 -0
- package/dist/types/cli.d.ts +16 -0
- package/dist/types/core.d.ts +1 -1
- package/dist/types/crypto-browser.d.ts +2 -0
- package/dist/types/crypto-node.d.ts +6 -0
- package/dist/types/react.d.ts +1 -1
- package/dist/types/version.d.ts +2 -2
- package/dist/types/webhooks.d.ts +2 -2
- package/dist/ui/index.d.mts +54 -0
- package/dist/ui/index.d.ts +54 -0
- package/dist/ui/index.js +105 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/index.mjs +44 -0
- package/dist/ui/index.mjs.map +1 -0
- package/dist/ui/lib/compose-refs.d.mts +19 -0
- package/dist/ui/lib/compose-refs.d.ts +19 -0
- package/dist/ui/lib/compose-refs.js +76 -0
- package/dist/ui/lib/compose-refs.js.map +1 -0
- package/dist/ui/lib/compose-refs.mjs +41 -0
- package/dist/ui/lib/compose-refs.mjs.map +1 -0
- package/dist/ui/lib/use-mobile.d.mts +4 -0
- package/dist/ui/lib/use-mobile.d.ts +4 -0
- package/dist/ui/lib/use-mobile.js +57 -0
- package/dist/ui/lib/use-mobile.js.map +1 -0
- package/dist/ui/lib/use-mobile.mjs +22 -0
- package/dist/ui/lib/use-mobile.mjs.map +1 -0
- package/dist/ui/lib/utils.d.mts +10 -0
- package/dist/ui/lib/utils.d.ts +10 -0
- package/dist/ui/lib/utils.js +33 -0
- package/dist/ui/lib/utils.js.map +1 -0
- package/dist/ui/lib/utils.mjs +9 -0
- package/dist/ui/lib/utils.mjs.map +1 -0
- package/dist/ui/primitives/alert-dialog.d.mts +22 -0
- package/dist/ui/primitives/alert-dialog.d.ts +22 -0
- package/dist/ui/primitives/alert-dialog.js +163 -0
- package/dist/ui/primitives/alert-dialog.js.map +1 -0
- package/dist/ui/primitives/alert-dialog.mjs +119 -0
- package/dist/ui/primitives/alert-dialog.mjs.map +1 -0
- package/dist/ui/primitives/avatar.d.mts +8 -0
- package/dist/ui/primitives/avatar.d.ts +8 -0
- package/dist/ui/primitives/avatar.js +91 -0
- package/dist/ui/primitives/avatar.js.map +1 -0
- package/dist/ui/primitives/avatar.mjs +55 -0
- package/dist/ui/primitives/avatar.mjs.map +1 -0
- package/dist/ui/primitives/breadcrumb.d.mts +13 -0
- package/dist/ui/primitives/breadcrumb.d.ts +13 -0
- package/dist/ui/primitives/breadcrumb.js +127 -0
- package/dist/ui/primitives/breadcrumb.js.map +1 -0
- package/dist/ui/primitives/breadcrumb.mjs +97 -0
- package/dist/ui/primitives/breadcrumb.mjs.map +1 -0
- package/dist/ui/primitives/button.d.mts +15 -0
- package/dist/ui/primitives/button.d.ts +15 -0
- package/dist/ui/primitives/button.js +91 -0
- package/dist/ui/primitives/button.js.map +1 -0
- package/dist/ui/primitives/button.mjs +56 -0
- package/dist/ui/primitives/button.mjs.map +1 -0
- package/dist/ui/primitives/card.d.mts +10 -0
- package/dist/ui/primitives/card.d.ts +10 -0
- package/dist/ui/primitives/card.js +101 -0
- package/dist/ui/primitives/card.js.map +1 -0
- package/dist/ui/primitives/card.mjs +62 -0
- package/dist/ui/primitives/card.mjs.map +1 -0
- package/dist/ui/primitives/carousel.d.mts +45 -0
- package/dist/ui/primitives/carousel.d.ts +45 -0
- package/dist/ui/primitives/carousel.js +300 -0
- package/dist/ui/primitives/carousel.js.map +1 -0
- package/dist/ui/primitives/carousel.mjs +261 -0
- package/dist/ui/primitives/carousel.mjs.map +1 -0
- package/dist/ui/primitives/chart.d.mts +68 -0
- package/dist/ui/primitives/chart.d.ts +68 -0
- package/dist/ui/primitives/chart.js +280 -0
- package/dist/ui/primitives/chart.js.map +1 -0
- package/dist/ui/primitives/chart.mjs +241 -0
- package/dist/ui/primitives/chart.mjs.map +1 -0
- package/dist/ui/primitives/collapsible.d.mts +8 -0
- package/dist/ui/primitives/collapsible.d.ts +8 -0
- package/dist/ui/primitives/collapsible.js +72 -0
- package/dist/ui/primitives/collapsible.js.map +1 -0
- package/dist/ui/primitives/collapsible.mjs +36 -0
- package/dist/ui/primitives/collapsible.mjs.map +1 -0
- package/dist/ui/primitives/context-menu.d.mts +29 -0
- package/dist/ui/primitives/context-menu.d.ts +29 -0
- package/dist/ui/primitives/context-menu.js +203 -0
- package/dist/ui/primitives/context-menu.js.map +1 -0
- package/dist/ui/primitives/context-menu.mjs +155 -0
- package/dist/ui/primitives/context-menu.mjs.map +1 -0
- package/dist/ui/primitives/dialog.d.mts +26 -0
- package/dist/ui/primitives/dialog.d.ts +26 -0
- package/dist/ui/primitives/dialog.js +192 -0
- package/dist/ui/primitives/dialog.js.map +1 -0
- package/dist/ui/primitives/dialog.mjs +148 -0
- package/dist/ui/primitives/dialog.mjs.map +1 -0
- package/dist/ui/primitives/drawer.d.mts +26 -0
- package/dist/ui/primitives/drawer.d.ts +26 -0
- package/dist/ui/primitives/drawer.js +146 -0
- package/dist/ui/primitives/drawer.js.map +1 -0
- package/dist/ui/primitives/drawer.mjs +103 -0
- package/dist/ui/primitives/drawer.mjs.map +1 -0
- package/dist/ui/primitives/input.d.mts +5 -0
- package/dist/ui/primitives/input.d.ts +5 -0
- package/dist/ui/primitives/input.js +46 -0
- package/dist/ui/primitives/input.js.map +1 -0
- package/dist/ui/primitives/input.mjs +22 -0
- package/dist/ui/primitives/input.mjs.map +1 -0
- package/dist/ui/primitives/item.d.mts +27 -0
- package/dist/ui/primitives/item.d.ts +27 -0
- package/dist/ui/primitives/item.js +221 -0
- package/dist/ui/primitives/item.js.map +1 -0
- package/dist/ui/primitives/item.mjs +188 -0
- package/dist/ui/primitives/item.mjs.map +1 -0
- package/dist/ui/primitives/kbd.d.mts +15 -0
- package/dist/ui/primitives/kbd.d.ts +15 -0
- package/dist/ui/primitives/kbd.js +45 -0
- package/dist/ui/primitives/kbd.js.map +1 -0
- package/dist/ui/primitives/kbd.mjs +21 -0
- package/dist/ui/primitives/kbd.mjs.map +1 -0
- package/dist/ui/primitives/label.d.mts +6 -0
- package/dist/ui/primitives/label.d.ts +6 -0
- package/dist/ui/primitives/label.js +55 -0
- package/dist/ui/primitives/label.js.map +1 -0
- package/dist/ui/primitives/label.mjs +21 -0
- package/dist/ui/primitives/label.mjs.map +1 -0
- package/dist/ui/primitives/resizable.d.mts +10 -0
- package/dist/ui/primitives/resizable.d.ts +10 -0
- package/dist/ui/primitives/resizable.js +68 -0
- package/dist/ui/primitives/resizable.js.map +1 -0
- package/dist/ui/primitives/resizable.mjs +42 -0
- package/dist/ui/primitives/resizable.mjs.map +1 -0
- package/dist/ui/primitives/save-status.d.mts +31 -0
- package/dist/ui/primitives/save-status.d.ts +31 -0
- package/dist/ui/primitives/save-status.js +150 -0
- package/dist/ui/primitives/save-status.js.map +1 -0
- package/dist/ui/primitives/save-status.mjs +126 -0
- package/dist/ui/primitives/save-status.mjs.map +1 -0
- package/dist/ui/primitives/separator.d.mts +6 -0
- package/dist/ui/primitives/separator.d.ts +6 -0
- package/dist/ui/primitives/separator.js +62 -0
- package/dist/ui/primitives/separator.js.map +1 -0
- package/dist/ui/primitives/separator.mjs +28 -0
- package/dist/ui/primitives/separator.mjs.map +1 -0
- package/dist/ui/primitives/sheet.d.mts +16 -0
- package/dist/ui/primitives/sheet.d.ts +16 -0
- package/dist/ui/primitives/sheet.js +170 -0
- package/dist/ui/primitives/sheet.js.map +1 -0
- package/dist/ui/primitives/sheet.mjs +129 -0
- package/dist/ui/primitives/sheet.mjs.map +1 -0
- package/dist/ui/primitives/sidebar.d.mts +74 -0
- package/dist/ui/primitives/sidebar.d.ts +74 -0
- package/dist/ui/primitives/sidebar.js +697 -0
- package/dist/ui/primitives/sidebar.js.map +1 -0
- package/dist/ui/primitives/sidebar.mjs +651 -0
- package/dist/ui/primitives/sidebar.mjs.map +1 -0
- package/dist/ui/primitives/skeleton.d.mts +5 -0
- package/dist/ui/primitives/skeleton.d.ts +5 -0
- package/dist/ui/primitives/skeleton.js +40 -0
- package/dist/ui/primitives/skeleton.js.map +1 -0
- package/dist/ui/primitives/skeleton.mjs +16 -0
- package/dist/ui/primitives/skeleton.mjs.map +1 -0
- package/dist/ui/primitives/slider.d.mts +20 -0
- package/dist/ui/primitives/slider.d.ts +20 -0
- package/dist/ui/primitives/slider.js +112 -0
- package/dist/ui/primitives/slider.js.map +1 -0
- package/dist/ui/primitives/slider.mjs +78 -0
- package/dist/ui/primitives/slider.mjs.map +1 -0
- package/dist/ui/primitives/sortable.d.mts +50 -0
- package/dist/ui/primitives/sortable.d.ts +50 -0
- package/dist/ui/primitives/sortable.js +456 -0
- package/dist/ui/primitives/sortable.js.map +1 -0
- package/dist/ui/primitives/sortable.mjs +435 -0
- package/dist/ui/primitives/sortable.mjs.map +1 -0
- package/dist/ui/primitives/switch.d.mts +6 -0
- package/dist/ui/primitives/switch.d.ts +6 -0
- package/dist/ui/primitives/switch.js +67 -0
- package/dist/ui/primitives/switch.js.map +1 -0
- package/dist/ui/primitives/switch.mjs +33 -0
- package/dist/ui/primitives/switch.mjs.map +1 -0
- package/dist/ui/primitives/table.d.mts +12 -0
- package/dist/ui/primitives/table.d.ts +12 -0
- package/dist/ui/primitives/table.js +130 -0
- package/dist/ui/primitives/table.js.map +1 -0
- package/dist/ui/primitives/table.mjs +89 -0
- package/dist/ui/primitives/table.mjs.map +1 -0
- package/dist/ui/primitives/tabs.d.mts +9 -0
- package/dist/ui/primitives/tabs.d.ts +9 -0
- package/dist/ui/primitives/tabs.js +86 -0
- package/dist/ui/primitives/tabs.js.map +1 -0
- package/dist/ui/primitives/tabs.mjs +49 -0
- package/dist/ui/primitives/tabs.mjs.map +1 -0
- package/dist/ui/primitives/textarea.d.mts +5 -0
- package/dist/ui/primitives/textarea.d.ts +5 -0
- package/dist/ui/primitives/textarea.js +43 -0
- package/dist/ui/primitives/textarea.js.map +1 -0
- package/dist/ui/primitives/textarea.mjs +19 -0
- package/dist/ui/primitives/textarea.mjs.map +1 -0
- package/dist/ui/primitives/tooltip.d.mts +9 -0
- package/dist/ui/primitives/tooltip.d.ts +9 -0
- package/dist/ui/primitives/tooltip.js +94 -0
- package/dist/ui/primitives/tooltip.js.map +1 -0
- package/dist/ui/primitives/tooltip.mjs +57 -0
- package/dist/ui/primitives/tooltip.mjs.map +1 -0
- package/docs/agents.md +72 -29
- package/docs/api/README.md +7 -3
- package/docs/api/appObjects.d.ts +3 -1
- package/docs/api/appObjects.md +2 -1
- package/docs/api/by-path.md +51 -0
- package/docs/api/dataPipelines.d.ts +149 -0
- package/docs/api/dataPipelines.md +232 -0
- package/docs/api/forms.d.ts +16 -0
- package/docs/api/forms.md +8 -0
- package/docs/api/objects.d.ts +705 -5
- package/docs/api/objects.md +1099 -14
- package/docs/api/ops.d.ts +1 -1
- package/docs/api/ops.md +1 -1
- package/docs/api/processLists.d.ts +247 -0
- package/docs/api/processLists.md +409 -0
- package/docs/api/quotes.d.ts +3 -1
- package/docs/api/quotes.md +7 -2
- package/docs/api/radar.d.ts +4 -0
- package/docs/api/radar.md +1 -0
- package/docs/api/reports.d.ts +476 -0
- package/docs/api/reports.md +539 -0
- package/docs/api/scheduleBuy.d.ts +12 -8
- package/docs/api/scheduleBuy.md +13 -10
- package/docs/api/seo.d.ts +406 -3
- package/docs/api/seo.md +505 -1
- package/docs/api/workspaceBackup.d.ts +324 -0
- package/docs/api/workspaceBackup.md +427 -0
- package/package.json +174 -5
- package/stacks/6sense.json +46 -0
- package/stacks/README.md +20 -0
- package/stacks/apollo.json +128 -0
- package/stacks/clay.json +59 -0
- package/stacks/gong.json +72 -0
- package/stacks/hubspot.json +162 -0
- package/stacks/intercom.json +53 -0
- package/stacks/mailchimp.json +71 -0
- package/stacks/outreach.json +86 -0
- package/stacks/salesforce.json +94 -0
- package/stacks/stack.schema.json +32 -0
- package/stacks/starter.json +22 -0
- package/styles/styles.css +2 -0
- package/styles/theme.css +181 -0
- package/styles/tokens.css +284 -0
- package/templates/next-app/README.md.tmpl +35 -0
- package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
- package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
- package/templates/next-app/app/globals.css.tmpl +8 -0
- package/templates/next-app/app/layout.tsx.tmpl +19 -0
- package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
- package/templates/next-app/dot-env.local.example.tmpl +6 -0
- package/templates/next-app/dot-gitignore.tmpl +7 -0
- package/templates/next-app/lib/nav.ts.tmpl +2 -0
- package/templates/next-app/next.config.mjs.tmpl +4 -0
- package/templates/next-app/package.json.tmpl +25 -0
- package/templates/next-app/postcss.config.mjs.tmpl +4 -0
- package/templates/next-app/tsconfig.json.tmpl +21 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CompetitorTracker: the roster of competitors graph8 watches. Add one (or several detected ones), scan, pause,
|
|
5
|
+
* resume or remove. Shows threat score, traffic, posting and ads at a glance and follows a scan until it finishes.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. listCompetitors GET /radar/competitors (read) { competitors, capacity }; polled while a scan runs
|
|
9
|
+
* 2. competitorSuggestions GET /radar/competitors/suggestions (read) detected, untracked competitors { suggestions }
|
|
10
|
+
* 3. createCompetitor POST /radar/competitors { domain, name, tier, scan_on_add } (billable tier)
|
|
11
|
+
* 4. bulkAddCompetitors POST /radar/competitors/bulk { competitors, scan_on_add } (billable tier)
|
|
12
|
+
* 5. scanCompetitor POST /radar/competitors/{id}/scan (billable) 1 credit per page up to the budget + 1
|
|
13
|
+
* 6. scanAllCompetitors POST /radar/scan-all (billable) every active competitor not already scanning
|
|
14
|
+
* 7. updateCompetitor PATCH /radar/competitors/{id} { is_active } (billable tier; free unless it triggers a scan) pause or resume
|
|
15
|
+
* 8. deleteCompetitor DELETE /radar/competitors/{id} (destructive) removes its history too
|
|
16
|
+
* Every one of these goes through ConfirmSpendDialog. Adding or pausing is free when `scan_on_add` is off, and the dialog
|
|
17
|
+
* then says 0 credits. The API has no quote or balance read: the scan figure is the most one scan can charge.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { useRef, useState } from "react";
|
|
21
|
+
import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
|
|
22
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
23
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
24
|
+
import type { ConfirmTier } from "@/g8/components/confirm-spend-dialog";
|
|
25
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
26
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
27
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
28
|
+
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
29
|
+
import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
30
|
+
import { useCompetitors } from "../hooks/use-competitors";
|
|
31
|
+
import { formatCount, isScanning, normalizeDomain, radarBody, scanCostEstimate } from "../lib/radar-types";
|
|
32
|
+
import type { Competitor, CompetitorList, CompetitorSuggestion } from "../lib/radar-types";
|
|
33
|
+
|
|
34
|
+
export interface CompetitorTrackerProps {
|
|
35
|
+
/** Called when a person opens a competitor (for example to show its CompetitorBattleCard). */
|
|
36
|
+
onSelect?: (competitor: Competitor) => void;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type Pending =
|
|
41
|
+
| { type: "add"; domain: string; name: string; tier: string; scan: boolean }
|
|
42
|
+
| { type: "bulk"; domains: string[]; scan: boolean }
|
|
43
|
+
| { type: "scan"; competitor: Competitor }
|
|
44
|
+
| { type: "scanAll" }
|
|
45
|
+
| { type: "toggle"; competitor: Competitor }
|
|
46
|
+
| { type: "delete"; competitor: Competitor };
|
|
47
|
+
|
|
48
|
+
export function CompetitorTracker({ onSelect, className }: CompetitorTrackerProps) {
|
|
49
|
+
const { competitors, capacity, scanning, loading, error, refetch } = useCompetitors();
|
|
50
|
+
const [domain, setDomain] = useState("");
|
|
51
|
+
const [name, setName] = useState("");
|
|
52
|
+
const [tier, setTier] = useState("tier2");
|
|
53
|
+
const [scanOnAdd, setScanOnAdd] = useState(false);
|
|
54
|
+
const [picked, setPicked] = useState<string[]>([]);
|
|
55
|
+
const [pending, setPending] = useState<Pending | null>(null);
|
|
56
|
+
const suggestions = useG8Query(async (signal) => radarBody<{ suggestions: CompetitorSuggestion[] }>(await g8Get("/radar/competitors/suggestions", undefined, { signal })).suggestions ?? [], []);
|
|
57
|
+
// `refetch` resolves undefined when it failed OR was superseded by another refetch (done() below). That must never read
|
|
58
|
+
// as "scans finished": fall back to the last known list so polling carries on while a scan is still running.
|
|
59
|
+
const latest = useRef<CompetitorList>({ competitors, capacity: capacity ?? { limit: 0, used: 0, remaining: 0 } });
|
|
60
|
+
latest.current = { competitors, capacity: capacity ?? latest.current.capacity };
|
|
61
|
+
useJobPoll(async () => (await refetch()) ?? latest.current, { enabled: scanning, intervalMs: 5000, maxAttempts: 120, isTerminal: (list) => !list.competitors.some(isScanning) });
|
|
62
|
+
|
|
63
|
+
const done = () => { setPending(null); void refetch(); void suggestions.refetch(); };
|
|
64
|
+
const run = useG8Mutation(async (p: Pending) => {
|
|
65
|
+
if (p.type === "add") return g8Post("/radar/competitors", { domain: p.domain, name: p.name || undefined, tier: p.tier, scan_on_add: p.scan });
|
|
66
|
+
if (p.type === "bulk") return g8Post("/radar/competitors/bulk", { competitors: p.domains.map((d) => ({ domain: d })), scan_on_add: p.scan });
|
|
67
|
+
if (p.type === "scan") return g8Post(`/radar/competitors/${p.competitor.id}/scan`, {});
|
|
68
|
+
if (p.type === "scanAll") return g8Post("/radar/scan-all", {});
|
|
69
|
+
if (p.type === "toggle") return g8Patch(`/radar/competitors/${p.competitor.id}`, { is_active: !p.competitor.is_active });
|
|
70
|
+
return g8Delete(`/radar/competitors/${p.competitor.id}`);
|
|
71
|
+
}, { onSuccess: (_r, p) => { if (p.type === "add") { setDomain(""); setName(""); } if (p.type === "bulk") setPicked([]); done(); } });
|
|
72
|
+
|
|
73
|
+
const active = competitors.filter((c) => c.is_active && !isScanning(c));
|
|
74
|
+
const detected = (suggestions.data ?? []).filter((s) => s.domain && !s.already_tracked);
|
|
75
|
+
// Ticked suggestions that are no longer on offer (tracked meanwhile, or the list reloaded) drop out of the selection.
|
|
76
|
+
const chosen = picked.filter((d) => detected.some((s) => s.domain === d));
|
|
77
|
+
const plan = describe(pending, active);
|
|
78
|
+
const normalized = normalizeDomain(domain);
|
|
79
|
+
|
|
80
|
+
if (error) return <ApiError error={error} title="Could not load competitors" onRetry={() => void refetch()} />;
|
|
81
|
+
return (
|
|
82
|
+
<div className={`flex flex-col gap-5 ${className ?? ""}`}>
|
|
83
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
84
|
+
<p className="text-sm text-muted-foreground">{capacity ? `${capacity.used} of ${capacity.limit} competitors tracked` : "Competitors"}</p>
|
|
85
|
+
<Button size="sm" variant="outline" disabled={active.length === 0} onClick={() => setPending({ type: "scanAll" })}>Scan all ({active.length})</Button>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<form className="flex flex-wrap items-end gap-2" onSubmit={(e) => { e.preventDefault(); if (normalized) setPending({ type: "add", domain: normalized, name: name.trim(), tier, scan: scanOnAdd }); }}>
|
|
89
|
+
<div className="flex flex-col gap-1"><Label htmlFor="rd-domain">Website</Label><Input id="rd-domain" value={domain} onChange={(e) => setDomain(e.target.value)} placeholder="competitor.com" /></div>
|
|
90
|
+
<div className="flex flex-col gap-1"><Label htmlFor="rd-name">Name (optional)</Label><Input id="rd-name" value={name} onChange={(e) => setName(e.target.value)} /></div>
|
|
91
|
+
<select aria-label="Tier" className="h-9 rounded-md border bg-background px-2" value={tier} onChange={(e) => setTier(e.target.value)}>
|
|
92
|
+
<option value="tier1">Tier 1 (25 pages per scan)</option><option value="tier2">Watchlist (8 pages per scan)</option>
|
|
93
|
+
</select>
|
|
94
|
+
<label className="flex items-center gap-1 text-sm"><input type="checkbox" checked={scanOnAdd} onChange={(e) => setScanOnAdd(e.target.checked)} /> Scan now</label>
|
|
95
|
+
<Button type="submit" disabled={!normalized}>Track</Button>
|
|
96
|
+
</form>
|
|
97
|
+
{run.error ? <ApiError error={run.error} title="That did not work" /> : null}
|
|
98
|
+
|
|
99
|
+
{detected.length > 0 ? (
|
|
100
|
+
<section aria-label="Detected competitors" className="rounded-md border p-3">
|
|
101
|
+
<h3 className="mb-2 text-sm font-medium">Detected from your market research</h3>
|
|
102
|
+
<ul className="flex flex-col gap-1">
|
|
103
|
+
{detected.map((s) => (
|
|
104
|
+
<li key={s.domain!} className="flex items-center gap-2 text-sm">
|
|
105
|
+
<input type="checkbox" aria-label={`Track ${s.domain}`} checked={picked.includes(s.domain!)} onChange={() => setPicked(picked.includes(s.domain!) ? picked.filter((d) => d !== s.domain) : [...picked, s.domain!])} />
|
|
106
|
+
<span className="font-medium">{s.name || s.domain}</span><span className="text-muted-foreground">{s.domain}</span>
|
|
107
|
+
</li>
|
|
108
|
+
))}
|
|
109
|
+
</ul>
|
|
110
|
+
<div className="mt-2"><Button size="sm" variant="outline" disabled={chosen.length === 0} onClick={() => setPending({ type: "bulk", domains: chosen, scan: false })}>Track {chosen.length} selected</Button></div>
|
|
111
|
+
</section>
|
|
112
|
+
) : null}
|
|
113
|
+
|
|
114
|
+
{loading && competitors.length === 0 ? <Skeleton className="h-24 w-full" /> : null}
|
|
115
|
+
{!loading && competitors.length === 0 ? <EmptyState title="No competitors tracked yet" description="Add a website above, or pick one graph8 detected from your market research." /> : null}
|
|
116
|
+
<ul className="flex flex-col gap-2">
|
|
117
|
+
{competitors.map((c) => (
|
|
118
|
+
<li key={c.id} className="flex flex-wrap items-center justify-between gap-2 rounded-md border p-3" data-testid="competitor">
|
|
119
|
+
<button type="button" className="flex flex-col text-left" onClick={() => onSelect?.(c)}>
|
|
120
|
+
<span className="text-sm font-medium">{c.name || c.domain}{!c.is_active ? <span className="ml-2 text-xs text-muted-foreground">paused</span> : null}</span>
|
|
121
|
+
<span className="text-xs text-muted-foreground">
|
|
122
|
+
{c.domain} · {c.tier === "tier1" ? "tier 1" : "watchlist"} · threat {c.threat_score ?? "-"} · traffic {formatCount(c.traffic_estimate)} · {formatCount(c.ads_live_count)} ads live · {isScanning(c) ? "scanning..." : c.last_scanned_at ? `scanned ${c.last_scanned_at.slice(0, 10)}` : "not scanned yet"}
|
|
123
|
+
</span>
|
|
124
|
+
</button>
|
|
125
|
+
<div className="flex gap-1">
|
|
126
|
+
<Button size="sm" variant="outline" disabled={isScanning(c) || !c.is_active} onClick={() => setPending({ type: "scan", competitor: c })}>Scan</Button>
|
|
127
|
+
<Button size="sm" variant="ghost" onClick={() => setPending({ type: "toggle", competitor: c })}>{c.is_active ? "Pause" : "Resume"}</Button>
|
|
128
|
+
<Button size="sm" variant="ghost" onClick={() => setPending({ type: "delete", competitor: c })}>Remove</Button>
|
|
129
|
+
</div>
|
|
130
|
+
</li>
|
|
131
|
+
))}
|
|
132
|
+
</ul>
|
|
133
|
+
|
|
134
|
+
<ConfirmSpendDialog
|
|
135
|
+
open={pending != null}
|
|
136
|
+
tier={plan.tier}
|
|
137
|
+
title={plan.title}
|
|
138
|
+
description={plan.description}
|
|
139
|
+
estimatedCredits={plan.credits}
|
|
140
|
+
affected={plan.affected}
|
|
141
|
+
confirmLabel={plan.confirm}
|
|
142
|
+
confirming={run.loading}
|
|
143
|
+
onConfirm={() => pending && void run.mutate(pending)}
|
|
144
|
+
onCancel={() => setPending(null)}
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function describe(p: Pending | null, active: Competitor[]): { tier: ConfirmTier; title: string; description: string; credits?: number; affected?: number; confirm: string } {
|
|
151
|
+
if (!p) return { tier: "billable", title: "", description: "", confirm: "Confirm" };
|
|
152
|
+
if (p.type === "add") return { tier: "billable", title: `Track ${p.domain}`, confirm: "Track", credits: p.scan ? scanCostEstimate({ tier: p.tier, page_budget: 0 }) : 0, description: p.scan ? "The first scan starts now: 1 credit per page captured plus 1 for the traffic lookup, at most this many (the maximum, not an average)." : "Adding is free. No scan runs until you start one." };
|
|
153
|
+
if (p.type === "bulk") return { tier: "billable", title: `Track ${p.domains.length} competitors`, confirm: "Track", credits: p.scan ? p.domains.length * scanCostEstimate({ tier: "tier2", page_budget: 0 }) : 0, description: "Adding is free. No scan runs until you start one." };
|
|
154
|
+
if (p.type === "scan") return { tier: "billable", title: `Scan ${p.competitor.name || p.competitor.domain}`, confirm: "Scan", credits: scanCostEstimate(p.competitor), description: `1 credit per page captured (up to ${p.competitor.page_budget}) plus 1 for the traffic lookup. This is the most it can charge; your balance is not checked here.` };
|
|
155
|
+
if (p.type === "scanAll") return { tier: "billable", title: `Scan ${active.length} competitors`, confirm: "Scan all", credits: active.reduce((sum, c) => sum + scanCostEstimate(c), 0), affected: active.length, description: "Every active competitor that is not already scanning. This is the most it can charge." };
|
|
156
|
+
if (p.type === "toggle") return { tier: "billable", title: `${p.competitor.is_active ? "Pause" : "Resume"} ${p.competitor.name || p.competitor.domain}`, confirm: p.competitor.is_active ? "Pause" : "Resume", credits: 0, description: p.competitor.is_active ? "A paused competitor is no longer scanned. Its history is kept." : "Scans resume on the normal schedule." };
|
|
157
|
+
return { tier: "destructive", title: `Remove ${p.competitor.name || p.competitor.domain}`, confirm: "Remove", description: "The competitor and its captured pages, ads and history are deleted." };
|
|
158
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RadarOverview: the one-screen state of competitive intelligence: how many rivals are tracked, moves in the last
|
|
5
|
+
* 7 days, open gaps, the threat board, recent moves and alerts. Read-only.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. overview GET /radar/overview (read) { summary, threat_board, moves, alerts, ... }
|
|
9
|
+
* Nothing here spends credits.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
13
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
14
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
15
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
16
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
17
|
+
import { radarBody } from "../lib/radar-types";
|
|
18
|
+
import type { RadarOverviewBody } from "../lib/radar-types";
|
|
19
|
+
|
|
20
|
+
export interface RadarOverviewProps {
|
|
21
|
+
/** Called with a competitor id when a threat-board row is opened. */
|
|
22
|
+
onSelectCompetitor?: (competitorId: string) => void;
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function RadarOverview({ onSelectCompetitor, className }: RadarOverviewProps) {
|
|
27
|
+
const query = useG8Query(async (signal) => radarBody<RadarOverviewBody>(await g8Get("/radar/overview", undefined, { signal })), []);
|
|
28
|
+
const o = query.data;
|
|
29
|
+
|
|
30
|
+
if (query.error) return <ApiError error={query.error} title="Could not load the overview" onRetry={() => void query.refetch()} />;
|
|
31
|
+
if (query.loading && !o) return <Skeleton className="h-40 w-full" />;
|
|
32
|
+
if (!o || o.summary.tracked === 0) return <EmptyState title="Nothing is tracked yet" description="Add a competitor to start seeing moves, gaps and a threat board." />;
|
|
33
|
+
|
|
34
|
+
const s = o.summary;
|
|
35
|
+
const stats: Array<[string, number]> = [["Tracked", s.tracked], ["Tier 1", s.tier1], ["Moves (7 days)", s.moves_7d], ["Open gaps", s.open_gaps], ["Act now", s.act_now]];
|
|
36
|
+
return (
|
|
37
|
+
<div className={`flex flex-col gap-5 ${className ?? ""}`}>
|
|
38
|
+
<dl className="grid grid-cols-2 gap-3 sm:grid-cols-5">
|
|
39
|
+
{stats.map(([label, value]) => (
|
|
40
|
+
<div key={label} className="rounded-md border p-3"><dt className="text-xs text-muted-foreground">{label}</dt><dd className="text-xl font-semibold">{value}</dd></div>
|
|
41
|
+
))}
|
|
42
|
+
</dl>
|
|
43
|
+
{s.scan_stale ? <p role="status" className="text-sm text-amber-700">The last scan{s.last_scan_at ? ` (${s.last_scan_at.slice(0, 10)})` : ""} is older than your cadence.</p> : null}
|
|
44
|
+
{s.credit_cap_reached ? <p role="status" className="text-sm text-amber-700">The monthly radar credit cap is reached: automatic scans are paused.</p> : null}
|
|
45
|
+
|
|
46
|
+
<section aria-label="Threat board"><h3 className="mb-2 text-sm font-medium">Threat board</h3>
|
|
47
|
+
<ul className="flex flex-col gap-1">
|
|
48
|
+
{o.threat_board.map((r) => (
|
|
49
|
+
<li key={r.id}><Button variant="ghost" className="w-full justify-between" onClick={() => onSelectCompetitor?.(r.id)}>
|
|
50
|
+
<span>{r.name || r.domain}</span><span className="text-xs text-muted-foreground">threat {r.threat_score ?? "-"} · {r.moves_7d} moves</span>
|
|
51
|
+
</Button></li>
|
|
52
|
+
))}
|
|
53
|
+
</ul>
|
|
54
|
+
</section>
|
|
55
|
+
<section aria-label="Alerts"><h3 className="mb-2 text-sm font-medium">Alerts</h3>
|
|
56
|
+
{o.alerts.length === 0 ? <p className="text-sm text-muted-foreground">No alerts.</p> : (
|
|
57
|
+
<ul className="flex flex-col gap-1">{o.alerts.map((a) => <li key={a.id} className="text-sm"><span className="font-medium">{a.title}</span>{a.detail ? <span className="text-muted-foreground"> {a.detail}</span> : null}</li>)}</ul>
|
|
58
|
+
)}
|
|
59
|
+
</section>
|
|
60
|
+
<section aria-label="Recent moves"><h3 className="mb-2 text-sm font-medium">Recent moves</h3>
|
|
61
|
+
{o.moves.length === 0 ? <p className="text-sm text-muted-foreground">No moves yet.</p> : (
|
|
62
|
+
<ul className="flex flex-col gap-1">{o.moves.map((m) => <li key={m.id} className="text-sm">{m.competitor_domain ? <span className="font-medium">{m.competitor_domain}: </span> : null}{m.text || m.title}{m.became_gap ? <span className="ml-1 text-xs text-amber-700">became a gap</span> : null}</li>)}</ul>
|
|
63
|
+
)}
|
|
64
|
+
</section>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RadarReport: deep-dive reports on one capability (a claim from your feature matrix) for an audience. Start one
|
|
5
|
+
* (runs a model, billable), watch it generate, then open the finished HTML or PDF.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. featureMatrix GET /radar/feature-matrix (read) { capabilities: [{ id, name }] }: where capability_id comes from
|
|
9
|
+
* 2. listRadarReports GET /radar/reports (read) { data: { items } }; polled while any report is generating
|
|
10
|
+
* 3. createRadarReport POST /radar/reports { audience, capability_id, edge } (billable) ConfirmSpendDialog first; 202-style { report_id, status: "generating" }
|
|
11
|
+
* 4. getRadarReportHtml / getRadarReportPdf GET /radar/reports/{id}/html | /pdf (read) opened as links through your /api/g8 proxy once status is completed
|
|
12
|
+
* Regenerating the same capability, audience and edge overwrites the earlier report; one already generating answers 409.
|
|
13
|
+
* The API quotes no price: the dialog says the report uses credits and that your balance is not checked here.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useRef, useState } from "react";
|
|
17
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
18
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
19
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
20
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
21
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
22
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
23
|
+
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
24
|
+
import { G8_PROXY_BASE, g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
25
|
+
import { isReportFinal, radarBody } from "../lib/radar-types";
|
|
26
|
+
import type { Capability, RadarReportRow } from "../lib/radar-types";
|
|
27
|
+
|
|
28
|
+
export interface RadarReportProps {
|
|
29
|
+
/** Preselect a capability (a feature-matrix row id). */
|
|
30
|
+
capabilityId?: string;
|
|
31
|
+
className?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function RadarReport({ capabilityId, className }: RadarReportProps) {
|
|
35
|
+
const [capability, setCapability] = useState(capabilityId ?? "");
|
|
36
|
+
const [audience, setAudience] = useState<"executive" | "developer">("executive");
|
|
37
|
+
const [edge, setEdge] = useState<"us" | "them">("us");
|
|
38
|
+
const [confirm, setConfirm] = useState(false);
|
|
39
|
+
|
|
40
|
+
const matrix = useG8Query(async (signal) => radarBody<{ capabilities: Capability[] }>(await g8Get("/radar/feature-matrix", undefined, { signal })).capabilities ?? [], []);
|
|
41
|
+
const reports = useG8Query(async (signal) => radarBody<{ items?: RadarReportRow[]; reports?: RadarReportRow[] }>(await g8Get("/radar/reports", undefined, { signal })), []);
|
|
42
|
+
const rows = reports.data?.items ?? reports.data?.reports ?? [];
|
|
43
|
+
const generating = rows.some((r) => !isReportFinal(r.status));
|
|
44
|
+
// A failed or superseded refetch resolves undefined: keep the last known rows so polling continues while a report generates.
|
|
45
|
+
const latest = useRef(reports.data);
|
|
46
|
+
latest.current = reports.data ?? latest.current;
|
|
47
|
+
useJobPoll(async () => (await reports.refetch()) ?? latest.current ?? { items: [] }, { enabled: generating, intervalMs: 4000, maxAttempts: 150, isTerminal: (d) => !(d.items ?? d.reports ?? []).some((r) => !isReportFinal(r.status)) });
|
|
48
|
+
|
|
49
|
+
const create = useG8Mutation(() => g8Post("/radar/reports", { audience, capability_id: capability, edge }), { onSuccess: () => { setConfirm(false); void reports.refetch(); } });
|
|
50
|
+
const name = (matrix.data ?? []).find((c) => c.id === capability)?.name ?? "this capability";
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className={`flex flex-col gap-4 ${className ?? ""}`}>
|
|
54
|
+
<div className="flex flex-wrap items-end gap-2">
|
|
55
|
+
<label className="flex flex-col gap-1 text-sm">Capability
|
|
56
|
+
<select aria-label="Capability" className="h-9 max-w-xs rounded-md border bg-background px-2" value={capability} onChange={(e) => setCapability(e.target.value)}>
|
|
57
|
+
<option value="">Choose one</option>
|
|
58
|
+
{(matrix.data ?? []).map((c) => <option key={c.id} value={c.id}>{c.name.length > 80 ? `${c.name.slice(0, 80)}...` : c.name}</option>)}
|
|
59
|
+
</select>
|
|
60
|
+
</label>
|
|
61
|
+
<select aria-label="Audience" className="h-9 rounded-md border bg-background px-2" value={audience} onChange={(e) => setAudience(e.target.value as "executive" | "developer")}><option value="executive">Executive</option><option value="developer">Developer</option></select>
|
|
62
|
+
<select aria-label="Angle" className="h-9 rounded-md border bg-background px-2" value={edge} onChange={(e) => setEdge(e.target.value as "us" | "them")}><option value="us">Where we win</option><option value="them">Where they win</option></select>
|
|
63
|
+
<Button disabled={!capability} onClick={() => setConfirm(true)}>Generate report</Button>
|
|
64
|
+
</div>
|
|
65
|
+
{matrix.error ? <ApiError error={matrix.error} title="Could not load capabilities" onRetry={() => void matrix.refetch()} /> : null}
|
|
66
|
+
{create.error ? <ApiError error={create.error} title="Could not start the report" /> : null}
|
|
67
|
+
{reports.error ? <ApiError error={reports.error} title="Could not load reports" onRetry={() => void reports.refetch()} /> : null}
|
|
68
|
+
{reports.loading && !reports.data ? <Skeleton className="h-16 w-full" /> : null}
|
|
69
|
+
{!reports.loading && !reports.error && rows.length === 0 ? <EmptyState title="No reports yet" description="Pick a capability and generate a deep-dive." /> : null}
|
|
70
|
+
<ul className="flex flex-col gap-2">
|
|
71
|
+
{rows.map((r) => (
|
|
72
|
+
<li key={r.id} className="flex items-center justify-between gap-2 rounded-md border p-3" data-testid="report">
|
|
73
|
+
<div className="flex flex-col"><span className="text-sm font-medium">{r.title || r.capability_name}</span>
|
|
74
|
+
<span className="text-xs text-muted-foreground">{r.audience} · {r.status}{r.error_message ? ` · ${r.error_message}` : ""}</span></div>
|
|
75
|
+
{r.status === "completed" ? (
|
|
76
|
+
<div className="flex gap-2"><a className="text-sm underline" href={`${G8_PROXY_BASE}/radar/reports/${r.id}/html`} target="_blank" rel="noreferrer">Open</a><a className="text-sm underline" href={`${G8_PROXY_BASE}/radar/reports/${r.id}/pdf`} target="_blank" rel="noreferrer">PDF</a></div>
|
|
77
|
+
) : null}
|
|
78
|
+
</li>
|
|
79
|
+
))}
|
|
80
|
+
</ul>
|
|
81
|
+
<ConfirmSpendDialog open={confirm} tier="billable" title={`Generate a ${audience} report on ${name}`}
|
|
82
|
+
description="A model writes the report and it uses credits. The API gives no price in advance and your balance is not checked here. Regenerating overwrites the earlier report for this capability and audience."
|
|
83
|
+
confirmLabel="Generate" confirming={create.loading} onConfirm={() => void create.mutate(undefined)} onCancel={() => setConfirm(false)} />
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
4
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
5
|
+
import { radarBody } from "../lib/radar-types";
|
|
6
|
+
import type { CompetitorList } from "../lib/radar-types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Every tracked competitor (active and paused) with the plan's capacity (`GET /radar/competitors`).
|
|
10
|
+
* `scanning` is true while any scan is queued or running, which is when a caller should poll.
|
|
11
|
+
*/
|
|
12
|
+
export function useCompetitors() {
|
|
13
|
+
const query = useG8Query(async (signal) => radarBody<CompetitorList>(await g8Get("/radar/competitors", undefined, { signal })), []);
|
|
14
|
+
const competitors = query.data?.competitors ?? [];
|
|
15
|
+
return {
|
|
16
|
+
competitors,
|
|
17
|
+
capacity: query.data?.capacity,
|
|
18
|
+
scanning: competitors.some((c) => c.scan_status === "scanning" || c.scan_status === "queued"),
|
|
19
|
+
loading: query.loading,
|
|
20
|
+
error: query.error,
|
|
21
|
+
refetch: query.refetch,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shapes and helpers for the radar components.
|
|
5
|
+
*
|
|
6
|
+
* Radar answers with the app's own bodies, UNWRAPPED (`{ competitors, capacity }`, `{ summary, moves, ... }`), while the
|
|
7
|
+
* report endpoints use the usual `{ data }` envelope. `radarBody` reads either, so a component never cares which.
|
|
8
|
+
*
|
|
9
|
+
* Money: a scan bills 1 credit per page captured plus 1 for the traffic lookup, up to the competitor's `page_budget`
|
|
10
|
+
* (25 pages for tier1, 8 for tier2). The API gives no quote and no balance read, so `scanCostEstimate` is the most
|
|
11
|
+
* one scan can charge. Adding or editing a competitor is free unless it also starts a scan.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type Tier = "tier1" | "tier2" | string;
|
|
15
|
+
|
|
16
|
+
export interface Competitor {
|
|
17
|
+
id: string;
|
|
18
|
+
domain: string;
|
|
19
|
+
name?: string | null;
|
|
20
|
+
tier: Tier;
|
|
21
|
+
is_active: boolean;
|
|
22
|
+
threat_score?: number | null;
|
|
23
|
+
traffic_estimate?: number | null;
|
|
24
|
+
traffic_trend_pct?: number | null;
|
|
25
|
+
posts_per_week?: number | null;
|
|
26
|
+
ads_live_count?: number | null;
|
|
27
|
+
last_scanned_at?: string | null;
|
|
28
|
+
/** "idle" or "scanning" (queued and running scans), else the last outcome. */
|
|
29
|
+
scan_status: string;
|
|
30
|
+
page_budget: number;
|
|
31
|
+
enrichment_errors?: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CompetitorList {
|
|
35
|
+
competitors: Competitor[];
|
|
36
|
+
capacity: { limit: number; used: number; remaining: number };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CompetitorSuggestion {
|
|
40
|
+
/** Null when the detected competitor has no known website: it cannot be tracked until one is typed in. */
|
|
41
|
+
domain: string | null;
|
|
42
|
+
name?: string | null;
|
|
43
|
+
threat_level?: string | null;
|
|
44
|
+
description?: string | null;
|
|
45
|
+
already_tracked: boolean;
|
|
46
|
+
tracking_status?: "active" | "paused" | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface RadarMove {
|
|
50
|
+
id: string;
|
|
51
|
+
kind: string;
|
|
52
|
+
text?: string | null;
|
|
53
|
+
title?: string | null;
|
|
54
|
+
competitor_domain?: string | null;
|
|
55
|
+
captured_at?: string | null;
|
|
56
|
+
became_gap?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface RadarAlert { id: string; kind: string; severity: string; title: string; detail?: string | null; competitor_domain?: string | null }
|
|
60
|
+
|
|
61
|
+
export interface RadarOverviewBody {
|
|
62
|
+
summary: {
|
|
63
|
+
tracked: number; tier1: number; watchlist: number; moves_7d: number; open_gaps: number; act_now: number;
|
|
64
|
+
last_scan_at?: string | null; scan_stale?: boolean; credit_cap_reached?: boolean;
|
|
65
|
+
};
|
|
66
|
+
threat_board: Array<{ id: string; domain: string; name?: string | null; tier: Tier; threat_score?: number | null; moves_7d: number; scan_status: string }>;
|
|
67
|
+
moves: RadarMove[];
|
|
68
|
+
alerts: RadarAlert[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface BattleCard {
|
|
72
|
+
competitor: Competitor;
|
|
73
|
+
summary: string;
|
|
74
|
+
our_advantages: Array<{ claim: string; verified: boolean; strength?: string | null; dimension?: string | null }>;
|
|
75
|
+
watch_outs: Array<{ id: string; title: string; detail?: string | null }>;
|
|
76
|
+
recent_moves: Array<{ id: string; kind: string; title?: string | null; url?: string | null; captured_at?: string | null }>;
|
|
77
|
+
talk_track: { lead_with: string[]; be_ready_for: string[] };
|
|
78
|
+
unverified_advantages_count?: number;
|
|
79
|
+
unverified_advantages_hidden?: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface RadarReportRow {
|
|
83
|
+
id: string;
|
|
84
|
+
audience: "executive" | "developer" | string;
|
|
85
|
+
status: "pending" | "generating" | "completed" | "failed" | string;
|
|
86
|
+
title?: string | null;
|
|
87
|
+
capability_id?: string | null;
|
|
88
|
+
capability_name?: string | null;
|
|
89
|
+
edge?: string | null;
|
|
90
|
+
created_at?: string | null;
|
|
91
|
+
completed_at?: string | null;
|
|
92
|
+
error_message?: string | null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface Capability { id: string; name: string; dimension?: string | null }
|
|
96
|
+
|
|
97
|
+
/** Read a body that may or may not be wrapped in `{ data }`. */
|
|
98
|
+
export function radarBody<T>(env: unknown): T {
|
|
99
|
+
const e = env as { data?: unknown } | null;
|
|
100
|
+
return (e && typeof e === "object" && "data" in e && e.data != null && !Array.isArray(e.data) ? e.data : env) as T;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The most one scan of this competitor can charge: a credit per page up to the budget, plus the traffic lookup. */
|
|
104
|
+
export function scanCostEstimate(competitor: Pick<Competitor, "page_budget" | "tier">): number {
|
|
105
|
+
return (competitor.page_budget || (competitor.tier === "tier1" ? 25 : 8)) + 1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function isScanning(c: Pick<Competitor, "scan_status">): boolean {
|
|
109
|
+
return c.scan_status === "scanning" || c.scan_status === "queued";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Reports are final once they stop generating. */
|
|
113
|
+
export function isReportFinal(status: string | undefined | null): boolean {
|
|
114
|
+
return status != null && status !== "pending" && status !== "generating";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A domain as a person types it: strips the scheme, www. and any path. */
|
|
118
|
+
export function normalizeDomain(input: string): string {
|
|
119
|
+
return input.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "").split(/[/?#]/)[0];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function formatCount(n: number | null | undefined): string {
|
|
123
|
+
return n == null ? "-" : n.toLocaleString();
|
|
124
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Read a competitor battle card and the market overview
|
|
3
|
+
* @when Preparing a sales call or a weekly digest. Free reads. The card only claims advantages that have evidence
|
|
4
|
+
* (`include_unverified` adds the rest, which you must label as unverified). The overview gives tracked counts, moves
|
|
5
|
+
* in the last 7 days, open gaps, the threat board and alerts. Radar bodies are unwrapped (no `data`).
|
|
6
|
+
*/
|
|
7
|
+
import { g8 } from "@graph8/sdk";
|
|
8
|
+
|
|
9
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
10
|
+
|
|
11
|
+
export async function battleCard(competitorId: string, includeUnverified = false) {
|
|
12
|
+
const card = (await g8.api.radar.competitorBattleCard({ path: { competitor_id: competitorId }, query: { include_unverified: includeUnverified } })) as unknown as {
|
|
13
|
+
summary: string;
|
|
14
|
+
talk_track: { lead_with: string[]; be_ready_for: string[] };
|
|
15
|
+
our_advantages: Array<{ claim: string; verified: boolean }>;
|
|
16
|
+
watch_outs: Array<{ title: string }>;
|
|
17
|
+
};
|
|
18
|
+
return { summary: card.summary, leadWith: card.talk_track.lead_with, beReadyFor: card.talk_track.be_ready_for };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function weeklyDigest() {
|
|
22
|
+
const o = (await g8.api.radar.overview()) as unknown as {
|
|
23
|
+
summary: { tracked: number; moves_7d: number; open_gaps: number; act_now: number };
|
|
24
|
+
threat_board: Array<{ domain: string; threat_score: number | null }>;
|
|
25
|
+
moves: Array<{ competitor_domain?: string; text?: string }>;
|
|
26
|
+
};
|
|
27
|
+
return { ...o.summary, topThreats: o.threat_board.slice(0, 3), moves: o.moves.slice(0, 5) };
|
|
28
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Generate a deep-dive report on one capability and download it
|
|
3
|
+
* @when A leadership or enablement document on one claim from the feature matrix. Generating runs a model and is
|
|
4
|
+
* BILLABLE with no advance price: get a human to approve it. Order: feature matrix (capability ids) -> create the
|
|
5
|
+
* report (a duplicate already generating answers 409; regenerating overwrites) -> poll the list until its status is
|
|
6
|
+
* final -> fetch the HTML. Reports use the usual `{ data }` envelope; competitors and the matrix do not.
|
|
7
|
+
*/
|
|
8
|
+
import { g8 } from "@graph8/sdk";
|
|
9
|
+
|
|
10
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
11
|
+
|
|
12
|
+
export async function capabilities() {
|
|
13
|
+
const res = (await g8.api.radar.featureMatrix()) as unknown as { capabilities: Array<{ id: string; name: string }> };
|
|
14
|
+
return res.capabilities;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function generate(capabilityId: string, audience: "executive" | "developer" = "executive", edge: "us" | "them" = "us") {
|
|
18
|
+
const res = await g8.api.radarReports.createRadarReport({ body: { capability_id: capabilityId, audience, edge } });
|
|
19
|
+
return (res.data as { report_id: string }).report_id;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function waitForReport(reportId: string) {
|
|
23
|
+
let delay = 4000;
|
|
24
|
+
for (let i = 0; i < 80; i++) {
|
|
25
|
+
const res = await g8.api.radarReports.listRadarReports();
|
|
26
|
+
const row = ((res.data as { items?: Array<{ id: string; status: string; error_message?: string }> }).items ?? []).find((r) => r.id === reportId);
|
|
27
|
+
if (row && row.status !== "pending" && row.status !== "generating") return row;
|
|
28
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
29
|
+
delay = Math.min(20000, Math.round(delay * 1.3));
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`report ${reportId} did not finish in time`);
|
|
32
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Pair competitor moves with your own marketing numbers
|
|
3
|
+
* @when A "market vs us" summary. Free reads from marketing intelligence (your site, campaigns and outbound bridge)
|
|
4
|
+
* and radar (the rivals). `days` is the lookback; the response is `{ data, meta }` where `meta.cached` says whether it
|
|
5
|
+
* came from cache. Combine with the radar overview: moves by rivals next to what your own traffic did that week.
|
|
6
|
+
*/
|
|
7
|
+
import { g8 } from "@graph8/sdk";
|
|
8
|
+
|
|
9
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
10
|
+
|
|
11
|
+
export async function snapshot(days = 7) {
|
|
12
|
+
const [mine, rivals] = await Promise.all([
|
|
13
|
+
g8.api.marketingIntelligence.getMarketingOverview({ query: { days } }),
|
|
14
|
+
g8.api.radar.overview(),
|
|
15
|
+
]);
|
|
16
|
+
const summary = (rivals as unknown as { summary: { moves_7d: number; open_gaps: number } }).summary;
|
|
17
|
+
return { days, mine: mine.data, rivalMoves: summary.moves_7d, openGaps: summary.open_gaps };
|
|
18
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Track competitors, scan them and wait for the scan
|
|
3
|
+
* @when Setting up competitive monitoring. Adding a competitor is free unless `scan_on_add` is true; a scan is
|
|
4
|
+
* BILLABLE (1 credit per page captured up to the tier budget, plus 1 for the traffic lookup: at most 26 for tier 1,
|
|
5
|
+
* 9 for the watchlist) and the API gives no quote, so tell a human that figure first. Radar answers with the app's
|
|
6
|
+
* own unwrapped bodies, so read `res.competitors`, not `res.data`. Order: suggestions -> add -> scan -> poll the
|
|
7
|
+
* list until no competitor has scan_status "scanning".
|
|
8
|
+
*/
|
|
9
|
+
import { g8 } from "@graph8/sdk";
|
|
10
|
+
|
|
11
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
12
|
+
|
|
13
|
+
type CompetitorRow = { id: string; domain: string; tier: string; page_budget: number; scan_status: string; threat_score?: number | null };
|
|
14
|
+
|
|
15
|
+
export async function suggestions() {
|
|
16
|
+
const res = (await g8.api.radar.competitorSuggestions()) as unknown as { suggestions: Array<{ name?: string; domain: string | null; already_tracked: boolean }> };
|
|
17
|
+
return res.suggestions.filter((s) => s.domain && !s.already_tracked);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Free. 409 means it is already tracked, 422 means the plan's competitor limit is reached. */
|
|
21
|
+
export async function track(domain: string, tier: "tier1" | "tier2" = "tier2") {
|
|
22
|
+
return (await g8.api.radar.createCompetitor({ body: { domain, tier, scan_on_add: false } })) as unknown as CompetitorRow;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Billable. Only after a person approved up to `page_budget + 1` credits. */
|
|
26
|
+
export async function scan(competitorId: string) {
|
|
27
|
+
return g8.api.radar.scanCompetitor({ path: { competitor_id: competitorId } });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function waitForScans() {
|
|
31
|
+
let delay = 5000;
|
|
32
|
+
for (let i = 0; i < 60; i++) {
|
|
33
|
+
const res = (await g8.api.radar.listCompetitors()) as unknown as { competitors: CompetitorRow[] };
|
|
34
|
+
if (!res.competitors.some((c) => c.scan_status === "scanning" || c.scan_status === "queued")) return res.competitors;
|
|
35
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
36
|
+
delay = Math.min(30000, Math.round(delay * 1.3));
|
|
37
|
+
}
|
|
38
|
+
throw new Error("scans did not finish in time; check the list again later");
|
|
39
|
+
}
|