@graph8/sdk 0.370.0 → 0.374.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 +22 -3
- package/blocks/_foundation/README.md +61 -0
- package/blocks/_foundation/block.json +57 -0
- package/blocks/_foundation/components/auth-gate.tsx +363 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- 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 +1 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/auth.ts +815 -0
- package/blocks/agents/README.md +81 -4
- package/blocks/agents/block.json +311 -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 +17 -3
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +20 -3
- package/blocks/agents/components/agent-runs.tsx +3 -3
- 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 +53 -11
- 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/components/analytics-screen.tsx +16 -3
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/report-builder.tsx +15 -2
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +27 -0
- package/blocks/appointments/components/booking-page.tsx +2 -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 +29 -13
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/campaigns/README.md +1 -1
- package/blocks/campaigns/block.json +34 -1
- package/blocks/campaigns/components/campaign-assets.tsx +1 -1
- package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
- package/blocks/campaigns/components/campaign-links.tsx +180 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
- package/blocks/campaigns/components/icp-detail.tsx +11 -2
- package/blocks/campaigns/components/icp-editor.tsx +8 -3
- package/blocks/campaigns/components/persona-detail.tsx +11 -2
- package/blocks/campaigns/components/persona-editor.tsx +6 -1
- package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- 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 +165 -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/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +1 -1
- package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
- package/blocks/crm/README.md +25 -0
- package/blocks/crm/block.json +257 -9
- package/blocks/crm/components/bulk-update.tsx +119 -0
- package/blocks/crm/components/companies-screen.tsx +23 -9
- package/blocks/crm/components/company-detail.tsx +2 -2
- package/blocks/crm/components/contact-detail.tsx +4 -4
- package/blocks/crm/components/contact-documents-tab.tsx +144 -0
- package/blocks/crm/components/contact-insights-tab.tsx +118 -0
- package/blocks/crm/components/contact-lookup-link.tsx +85 -0
- package/blocks/crm/components/contact-row-actions.tsx +43 -0
- package/blocks/crm/components/contact-tag-chips.tsx +113 -0
- package/blocks/crm/components/contacts-screen.tsx +77 -18
- package/blocks/crm/components/dedupe-review.tsx +6 -1
- package/blocks/crm/components/import-wizard.tsx +3 -7
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/owner-assign.tsx +132 -0
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +1 -1
- package/blocks/crm/components/record-open-links.tsx +79 -0
- package/blocks/crm/components/tag-assign.tsx +112 -0
- package/blocks/crm/components/tag-manager.tsx +178 -0
- package/blocks/crm/components/tasks-screen.tsx +35 -15
- package/blocks/crm/components/unlock-info-button.tsx +82 -0
- package/blocks/crm/components/verify-email-button.tsx +93 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-contact-tags.ts +29 -0
- package/blocks/crm/hooks/use-page-query.ts +3 -1
- package/blocks/crm/lib/crm-types.ts +14 -0
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/record-actions.ts +102 -0
- package/blocks/deals/README.md +25 -6
- package/blocks/deals/block.json +168 -2
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/create-deal-button.tsx +109 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +7 -5
- 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 +30 -9
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +57 -22
- 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 +1 -1
- package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +14 -5
- package/blocks/dialer/block.json +17 -2
- package/blocks/dialer/components/call-contact-button.tsx +26 -9
- package/blocks/dialer/components/call-log-screen.tsx +10 -2
- package/blocks/dialer/components/call-log.tsx +16 -4
- package/blocks/dialer/components/call-queue.tsx +3 -3
- package/blocks/dialer/components/dialer-screen.tsx +69 -27
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +58 -11
- package/blocks/dialer/components/softphone.tsx +1 -1
- package/blocks/documents/README.md +86 -0
- package/blocks/documents/block.json +378 -0
- package/blocks/documents/components/account-bar.tsx +48 -0
- package/blocks/documents/components/correspondence-form.tsx +95 -0
- package/blocks/documents/components/correspondence-import.tsx +106 -0
- package/blocks/documents/components/correspondence-panel.tsx +191 -0
- package/blocks/documents/components/documents-screen.tsx +76 -0
- package/blocks/documents/components/library-document-detail.tsx +175 -0
- package/blocks/documents/components/library-document-form.tsx +105 -0
- package/blocks/documents/components/library-folder-form.tsx +74 -0
- package/blocks/documents/components/library-panel.tsx +235 -0
- package/blocks/documents/components/library-upload.tsx +160 -0
- package/blocks/documents/components/question-row.tsx +130 -0
- package/blocks/documents/components/questionnaire-detail.tsx +197 -0
- package/blocks/documents/components/questionnaires-panel.tsx +173 -0
- package/blocks/documents/components/record-documents-tab.tsx +109 -0
- package/blocks/documents/lib/documents-types.ts +252 -0
- package/blocks/documents/lib/download.ts +64 -0
- package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
- package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
- package/blocks/documents/recipes/organize-the-library.ts +25 -0
- package/blocks/email-signatures/README.md +123 -0
- package/blocks/email-signatures/block.json +341 -0
- package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
- package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
- package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
- package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
- package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
- package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
- package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
- package/blocks/email-signatures/components/signature-policy.tsx +130 -0
- package/blocks/email-signatures/components/signature-preview.tsx +119 -0
- package/blocks/email-signatures/components/signature-report.tsx +77 -0
- package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
- package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
- package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
- package/blocks/email-signatures/lib/signatures-types.ts +392 -0
- package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
- package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
- package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
- package/blocks/enrichment/README.md +38 -10
- package/blocks/enrichment/block.json +164 -12
- package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
- package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
- package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
- package/blocks/enrichment/components/enrich-button.tsx +1 -1
- package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
- package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
- package/blocks/enrichment/components/list-pipelines.tsx +97 -0
- package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
- package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
- package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
- package/blocks/enrichment/components/waterfall-config.tsx +27 -7
- package/blocks/enrichment/lib/enrichment-types.ts +51 -0
- package/blocks/enrichment/lib/pipeline-types.ts +234 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/forms/README.md +10 -0
- package/blocks/forms/block.json +107 -0
- 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 +1 -1
- package/blocks/forms/components/form-embed.tsx +1 -1
- 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 +57 -20
- 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 +15 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +45 -4
- package/blocks/inbox/block.json +145 -8
- package/blocks/inbox/components/blocked-senders.tsx +103 -0
- package/blocks/inbox/components/compose-message.tsx +193 -0
- package/blocks/inbox/components/inbox-screen.tsx +54 -6
- package/blocks/inbox/components/inbox-search.tsx +106 -0
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/tag-manager.tsx +136 -0
- package/blocks/inbox/components/thread-actions.tsx +133 -0
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +3 -1
- package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
- package/blocks/inbox/lib/inbox-manage.ts +227 -0
- package/blocks/integrations/README.md +54 -1
- package/blocks/integrations/block.json +189 -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 +20 -1
- 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 +4 -4
- package/blocks/integrations/components/integrations-screen.tsx +20 -3
- 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 +6 -6
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +26 -7
- package/blocks/integrations/components/webhook-manager.tsx +30 -17
- 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 +5 -0
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +8 -8
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
- package/blocks/intent/lib/intent-utils.ts +4 -4
- 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/lists/README.md +7 -1
- package/blocks/lists/block.json +31 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +17 -7
- package/blocks/lists/components/list-picker.tsx +13 -3
- package/blocks/lists/components/list-table.tsx +1 -1
- package/blocks/lists/components/lists-screen.tsx +4 -2
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +27 -6
- package/blocks/lists/components/suppressions-screen.tsx +21 -12
- package/blocks/mailboxes/block.json +4 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
- package/blocks/mailboxes/components/domain-detail.tsx +10 -4
- package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
- package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
- package/blocks/mailboxes/components/warmup-health.tsx +20 -20
- package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +117 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
- 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 +38 -13
- 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/components/nurture-builder.tsx +10 -2
- package/blocks/nurture/components/nurture-enroll.tsx +6 -7
- package/blocks/nurture/components/nurture-form.tsx +5 -8
- package/blocks/nurture/components/nurture-screen.tsx +16 -5
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/objects/README.md +1 -1
- package/blocks/objects/block.json +13 -5
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-record-detail.tsx +8 -1
- package/blocks/objects/components/object-schema-editor.tsx +1 -1
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +123 -2
- 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-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/hooks/use-landing-page-paths.ts +60 -0
- 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/programmatic-pages/README.md +82 -0
- package/blocks/programmatic-pages/block.json +292 -0
- package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
- package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
- package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
- package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
- package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
- package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
- package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
- package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
- package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
- package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
- package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
- package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
- package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
- package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
- package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
- package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
- package/blocks/quotes/README.md +38 -1
- package/blocks/quotes/block.json +170 -6
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/manual-products.tsx +144 -0
- package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-documents.tsx +94 -0
- package/blocks/quotes/components/quote-evidence.tsx +95 -0
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +28 -8
- package/blocks/quotes/components/quote-related.tsx +3 -2
- package/blocks/quotes/components/quote-settings.tsx +184 -0
- package/blocks/quotes/components/quote-templates.tsx +189 -0
- package/blocks/quotes/components/quote-viewer.tsx +23 -5
- package/blocks/quotes/components/quotes-screen.tsx +15 -5
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/quotes/lib/quote-admin.ts +264 -0
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-screen.tsx +2 -2
- package/blocks/sequencer/README.md +61 -0
- package/blocks/sequencer/block.json +200 -6
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
- package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +32 -23
- package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
- package/blocks/sequencer/components/sequence-detail.tsx +10 -2
- package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
- package/blocks/sequencer/components/sequence-list.tsx +1 -1
- package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
- package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
- package/blocks/sequencer/components/sequence-stats.tsx +8 -8
- package/blocks/sequencer/components/sequence-templates.tsx +160 -0
- package/blocks/sequencer/components/sequences-screen.tsx +39 -6
- package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
- package/blocks/sequencer/components/step-editor.tsx +21 -12
- package/blocks/sequencer/components/step-templates.tsx +224 -0
- package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
- package/blocks/signals/README.md +39 -0
- package/blocks/signals/block.json +287 -0
- package/blocks/signals/components/auto-seed-panel.tsx +111 -0
- package/blocks/signals/components/listener-form.tsx +184 -0
- package/blocks/signals/components/matched-posts.tsx +195 -0
- package/blocks/signals/components/post-engagements.tsx +114 -0
- package/blocks/signals/components/record-social-tab.tsx +49 -0
- package/blocks/signals/components/social-listeners-screen.tsx +250 -0
- package/blocks/signals/components/social-settings.tsx +128 -0
- package/blocks/signals/components/source-lists.tsx +83 -0
- package/blocks/signals/components/top-engagers.tsx +68 -0
- package/blocks/signals/components/x-integration.tsx +129 -0
- package/blocks/signals/lib/signals-types.ts +228 -0
- package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
- package/blocks/signals/recipes/spend-against-caps.ts +14 -0
- package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
- package/blocks/social-ads/README.md +8 -0
- package/blocks/social-ads/block.json +137 -4
- package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
- package/blocks/social-ads/components/audience-sync.tsx +6 -2
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +6 -1
- 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 +6 -1
- package/blocks/social-ads/components/creative-gallery.tsx +20 -2
- 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 +4 -2
- package/blocks/social-ads/components/post-detail.tsx +3 -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-screen.tsx +33 -6
- 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/components/snippet-install.tsx +18 -9
- package/blocks/visitors/components/traffic-chart.tsx +7 -6
- package/blocks/visitors/components/visitor-company-card.tsx +24 -14
- package/blocks/visitors/components/visitor-feed.tsx +5 -5
- package/blocks/web-analytics/README.md +102 -0
- package/blocks/web-analytics/block.json +292 -0
- package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
- package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
- package/blocks/web-analytics/components/audience-tab.tsx +135 -0
- package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
- package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
- package/blocks/web-analytics/components/overview-tab.tsx +102 -0
- package/blocks/web-analytics/components/pages-tab.tsx +148 -0
- package/blocks/web-analytics/components/ranked-table.tsx +103 -0
- package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
- package/blocks/web-analytics/components/report-card.tsx +71 -0
- package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
- package/blocks/web-analytics/hooks/use-report.ts +48 -0
- package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
- package/blocks/web-analytics/lib/page-path.ts +14 -0
- package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
- package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
- package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
- package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
- package/blocks/webchat/components/chat-widget-config.tsx +1 -1
- package/blocks/webchat/components/conversation-list.tsx +1 -1
- package/blocks/workbench/README.md +54 -2
- package/blocks/workbench/block.json +234 -5
- package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
- package/blocks/workbench/components/workbench-automation.tsx +36 -0
- package/blocks/workbench/components/workbench-connections.tsx +136 -0
- package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
- package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
- package/blocks/workbench/components/workbench-grid.tsx +71 -8
- package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
- package/blocks/workbench/components/workbench-schedule.tsx +191 -0
- package/blocks/workbench/components/workbench-screen.tsx +65 -22
- package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
- package/blocks/workbench/components/workbench-triggers.tsx +178 -0
- package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
- package/blocks/workbench/lib/api-status.ts +8 -0
- package/blocks/workbench/lib/automation-types.ts +257 -0
- package/blocks/workflows/components/workflow-editor.tsx +1 -1
- package/dist/cli.js +744 -394
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +1 -1
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +1 -1
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/auth.d.ts +45 -0
- package/dist/types/blocks/brand.d.ts +11 -2
- package/dist/types/blocks/create.d.ts +11 -4
- package/dist/types/blocks/doctor.d.ts +1 -0
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/version.d.ts +2 -2
- package/docs/api/desk.d.ts +2 -2
- package/docs/api/desk.md +3 -3
- package/docs/api/forms.md +1 -1
- package/package.json +1 -1
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +37 -4
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
- package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
- 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 +24 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +68 -9
- 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 +8 -4
- package/stacks/clay/pages/companies.tsx.tmpl +17 -1
- package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
- package/stacks/clay/pages/lists.tsx.tmpl +24 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +16 -3
- 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 +34 -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 +23 -6
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +19 -4
- package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
- package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
- package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
- package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
- 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/sequences.tsx.tmpl +18 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
- package/stacks/hubspot.json +66 -7
- 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 +18 -4
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
- package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +22 -6
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
- 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 +18 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +38 -9
- package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
- package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +14 -2
- package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
- package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
- package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +17 -3
- package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +36 -3
- package/styles/styles.css +1 -1
- package/templates/auth/auth-route.ts.tmpl +16 -0
- package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
- package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
- package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
- package/templates/auth/g8.auth.password.ts.tmpl +18 -0
- package/templates/auth/g8.auth.session.ts.tmpl +23 -0
- package/templates/auth/middleware.ts.tmpl +14 -0
- package/templates/next-app/README.md.tmpl +5 -0
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CampaignLinksTab: the landing pages, workflows and newsletters tied to ONE campaign. Each row opens the record in
|
|
5
|
+
* your app (`onOpen*` callbacks route to your own screens), and a landing page or newsletter can be linked or unlinked
|
|
6
|
+
* here. Use it as an `extraDetailTabs` entry of CampaignsScreen:
|
|
7
|
+
* { id: "links", label: "Links", render: (c) => <CampaignLinksTab campaignId={c.id} onOpenLandingPage={(id) => router.push(`/pages?id=${id}`)} /> }
|
|
8
|
+
*
|
|
9
|
+
* SDK operations, in order:
|
|
10
|
+
* 1. listCampaignLandingPages GET /campaigns/{id}/landing-pages (read)
|
|
11
|
+
* listLandingPages GET /landing-pages?limit&offset (read; the "link a page" choices, paged)
|
|
12
|
+
* linkLandingPageToCampaign POST /campaigns/{id}/landing-pages (write)
|
|
13
|
+
* unlinkLandingPageFromCampaign DELETE /campaigns/{id}/landing-pages/{page} (DESTRUCTIVE tier -> ConfirmSpendDialog; the page itself is kept)
|
|
14
|
+
* 2. listCampaignWorkflows GET /campaigns/{id}/workflows (read)
|
|
15
|
+
* unlinkCampaignWorkflow DELETE /campaigns/{id}/workflows/{link_id} (DESTRUCTIVE tier -> ConfirmSpendDialog; the workflow itself is kept)
|
|
16
|
+
* 3. listNewsletters GET /newsletters (read; each row names its campaign_id)
|
|
17
|
+
* linkNewsletterCampaign PUT /newsletters/{id}/campaign (write; a newsletter has ONE campaign)
|
|
18
|
+
* unlinkNewsletterCampaign DELETE /newsletters/{id}/campaign (DESTRUCTIVE tier -> ConfirmSpendDialog; the newsletter is kept)
|
|
19
|
+
*
|
|
20
|
+
* API gaps, stated on screen: there is no operation that links a workflow to a campaign (only list, edit and unlink),
|
|
21
|
+
* and a newsletter that already belongs to ANOTHER campaign is not offered, because linking replaces its campaign.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { useState } from "react";
|
|
25
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
26
|
+
import { ExternalLink, Link2, Unlink } from "lucide-react";
|
|
27
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
28
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
29
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
30
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
31
|
+
import { useAllPages } from "@/g8/hooks/use-all-pages";
|
|
32
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
33
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
34
|
+
import { g8Delete, g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
|
|
35
|
+
import { hasNextOf } from "@/g8/lib/paging";
|
|
36
|
+
import { text } from "../lib/campaigns-utils";
|
|
37
|
+
|
|
38
|
+
export interface CampaignLinksTabProps {
|
|
39
|
+
/** The campaign (id or slug as the list gave it). */
|
|
40
|
+
campaignId: string;
|
|
41
|
+
/** Route to a landing page / workflow / newsletter in your app. A section without its callback shows names only. */
|
|
42
|
+
onOpenLandingPage?: (landingPageId: string) => void;
|
|
43
|
+
onOpenWorkflow?: (workflowId: string) => void;
|
|
44
|
+
onOpenNewsletter?: (newsletterId: string) => void;
|
|
45
|
+
className?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type Row = Record<string, unknown>;
|
|
49
|
+
const PAGE = 100;
|
|
50
|
+
|
|
51
|
+
/** The rows of a list payload under any of the keys the API mirrors them on. */
|
|
52
|
+
export function rowsOf(env: unknown, ...keys: string[]): Row[] {
|
|
53
|
+
const data = (env as { data?: unknown } | null)?.data;
|
|
54
|
+
if (Array.isArray(data)) return data as Row[];
|
|
55
|
+
const o = (data ?? {}) as Record<string, unknown>;
|
|
56
|
+
for (const k of ["items", ...keys]) if (Array.isArray(o[k])) return o[k] as Row[];
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const idOf = (v: unknown): string => text(v).trim();
|
|
61
|
+
|
|
62
|
+
function LinkRow({ name, meta, onOpen, onUnlink, busy }: { name: string; meta?: string; onOpen?: () => void; onUnlink?: () => void; busy?: boolean }) {
|
|
63
|
+
return (
|
|
64
|
+
<li className="flex items-center justify-between gap-3 py-2 text-sm">
|
|
65
|
+
<span className="min-w-0 truncate"><span className="font-medium">{name}</span>{meta ? <span className="text-muted-foreground"> · {meta}</span> : null}</span>
|
|
66
|
+
<span className="flex shrink-0 gap-2">
|
|
67
|
+
{onOpen ? <Button size="sm" variant="outline" onClick={onOpen} aria-label={`Open ${name}`}><ExternalLink className="mr-1 size-4" aria-hidden />Open</Button> : null}
|
|
68
|
+
{onUnlink ? <Button size="sm" variant="ghost" disabled={busy} onClick={onUnlink} aria-label={`Unlink ${name}`}><Unlink className="mr-1 size-4" aria-hidden />Unlink</Button> : null}
|
|
69
|
+
</span>
|
|
70
|
+
</li>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function LinkPicker({ label, options, busy, onLink }: { label: string; options: Array<{ id: string; name: string }>; busy: boolean; onLink: (id: string) => void }) {
|
|
75
|
+
const [value, setValue] = useState("");
|
|
76
|
+
return (
|
|
77
|
+
<div className="flex items-center gap-2 pt-2">
|
|
78
|
+
<select aria-label={label} className="h-9 min-w-56 rounded-md border bg-background px-2 text-sm" value={value} onChange={(e) => setValue(e.target.value)}>
|
|
79
|
+
<option value="">{options.length ? label : "Nothing left to link"}</option>
|
|
80
|
+
{options.map((o) => <option key={o.id} value={o.id}>{o.name}</option>)}
|
|
81
|
+
</select>
|
|
82
|
+
<Button size="sm" disabled={!value || busy} loading={busy} onClick={() => { onLink(value); setValue(""); }}><Link2 className="mr-1 size-4" aria-hidden />Link</Button>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type Pending = { kind: "page" | "workflow" | "newsletter"; id: string; name: string };
|
|
88
|
+
|
|
89
|
+
export function CampaignLinksTab({ campaignId, onOpenLandingPage, onOpenWorkflow, onOpenNewsletter, className }: CampaignLinksTabProps) {
|
|
90
|
+
const base = `/campaigns/${encodeURIComponent(campaignId)}`;
|
|
91
|
+
const pages = useG8Query(async (signal) => rowsOf(await g8Get(`${base}/landing-pages`, undefined, { signal }), "landing_pages"), [campaignId], { key: ["campaigns", "links", "pages", campaignId] });
|
|
92
|
+
const flows = useG8Query(async (signal) => rowsOf(await g8Get(`${base}/workflows`, undefined, { signal }), "workflows"), [campaignId], { key: ["campaigns", "links", "workflows", campaignId] });
|
|
93
|
+
const letters = useG8Query(async (signal) => rowsOf(await g8Get("/newsletters", undefined, { signal }), "newsletters"), [campaignId], { key: ["campaigns", "links", "newsletters"] });
|
|
94
|
+
const allPages = useAllPages<Row>(async (page, signal) => {
|
|
95
|
+
const env = await g8Get("/landing-pages", { limit: PAGE, offset: (page - 1) * PAGE }, { signal });
|
|
96
|
+
const rows = rowsOf(env, "pages", "landing_pages");
|
|
97
|
+
return { rows, hasNext: hasNextOf(env, PAGE, rows.length) };
|
|
98
|
+
}, [], { pageSize: PAGE });
|
|
99
|
+
const [pending, setPending] = useState<Pending | null>(null);
|
|
100
|
+
const refresh = () => { void pages.refetch(); void flows.refetch(); void letters.refetch(); };
|
|
101
|
+
const link = useG8Mutation(async (v: { kind: "page" | "newsletter"; id: string }) => {
|
|
102
|
+
if (v.kind === "page") await g8Post(`${base}/landing-pages`, { landing_page_id: v.id });
|
|
103
|
+
else await g8Put(`/newsletters/${encodeURIComponent(v.id)}/campaign`, { campaign_id: campaignId });
|
|
104
|
+
}, { onSuccess: refresh });
|
|
105
|
+
const unlink = useG8Mutation(async (p: Pending) => {
|
|
106
|
+
try {
|
|
107
|
+
if (p.kind === "page") await g8Delete(`${base}/landing-pages/${encodeURIComponent(p.id)}`);
|
|
108
|
+
else if (p.kind === "workflow") await g8Delete(`${base}/workflows/${encodeURIComponent(p.id)}`);
|
|
109
|
+
else await g8Delete(`/newsletters/${encodeURIComponent(p.id)}/campaign`);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if ((err as { status?: number }).status !== 404) throw err; // already unlinked counts as done
|
|
112
|
+
}
|
|
113
|
+
}, { onSuccess: () => { setPending(null); refresh(); }, onError: () => setPending(null) });
|
|
114
|
+
|
|
115
|
+
const linkedPages = (pages.data ?? []).map((r) => {
|
|
116
|
+
const lp = (r.landing_page ?? {}) as Row;
|
|
117
|
+
return { id: idOf(lp.id) || idOf(r.landing_page_id), name: text(lp.name) || `Landing page ${idOf(lp.id) || idOf(r.landing_page_id)}`, status: text(lp.status) };
|
|
118
|
+
}).filter((p) => p.id);
|
|
119
|
+
const linkedFlows = (flows.data ?? []).map((r) => ({ linkId: idOf(r.id), workflowId: idOf(r.workflow_id), name: text(r.workflow_name) || `Workflow ${idOf(r.workflow_id)}`, status: text(r.status), role: text(r.role) })).filter((w) => w.linkId);
|
|
120
|
+
const thisLetters = (letters.data ?? []).filter((n) => idOf(n.campaign_id) === campaignId);
|
|
121
|
+
const freeLetters = (letters.data ?? []).filter((n) => !idOf(n.campaign_id)).map((n) => ({ id: idOf(n.id), name: text(n.name) || `Newsletter ${idOf(n.id)}` }));
|
|
122
|
+
const linkedIds = new Set(linkedPages.map((p) => p.id));
|
|
123
|
+
const pageChoices = allPages.items.map((r) => ({ id: idOf(r.id), name: text(r.name) || `Landing page ${idOf(r.id)}` })).filter((p) => p.id && !linkedIds.has(p.id));
|
|
124
|
+
const otherLetters = (letters.data ?? []).filter((n) => idOf(n.campaign_id) && idOf(n.campaign_id) !== campaignId).length;
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div className={`space-y-4 ${className ?? ""}`}>
|
|
128
|
+
<SectionCard title="Landing pages" description="Pages visitors land on from this campaign. Unlinking keeps the page.">
|
|
129
|
+
{pages.error ? <ApiError error={pages.error} title="Could not load landing pages" onRetry={() => void pages.refetch()} /> : !pages.data ? <Skeleton className="h-16 w-full" aria-label="Loading landing pages" /> : linkedPages.length === 0 ? (
|
|
130
|
+
<EmptyState compact title="No landing page linked" description="Link a page below." />
|
|
131
|
+
) : (
|
|
132
|
+
<ul className="divide-y" aria-label="Linked landing pages">
|
|
133
|
+
{linkedPages.map((p) => <LinkRow key={p.id} name={p.name} meta={p.status || undefined} onOpen={onOpenLandingPage ? () => onOpenLandingPage(p.id) : undefined} onUnlink={() => setPending({ kind: "page", id: p.id, name: p.name })} busy={unlink.loading} />)}
|
|
134
|
+
</ul>
|
|
135
|
+
)}
|
|
136
|
+
{allPages.error ? <ApiError error={allPages.error} title="Could not load the pages to link" onRetry={allPages.refetch} /> : allPages.loaded ? <LinkPicker label="Link a landing page" options={pageChoices} busy={link.loading} onLink={(id) => void link.mutate({ kind: "page", id })} /> : null}
|
|
137
|
+
{allPages.truncated ? <p className="pt-1 text-xs text-muted-foreground">Showing the first {allPages.items.length} landing pages.</p> : null}
|
|
138
|
+
</SectionCard>
|
|
139
|
+
|
|
140
|
+
<SectionCard title="Workflows" description="Automations that run for this campaign. Unlinking keeps the workflow.">
|
|
141
|
+
{flows.error ? <ApiError error={flows.error} title="Could not load workflows" onRetry={() => void flows.refetch()} /> : !flows.data ? <Skeleton className="h-16 w-full" aria-label="Loading workflows" /> : linkedFlows.length === 0 ? (
|
|
142
|
+
<EmptyState compact title="No workflow linked" />
|
|
143
|
+
) : (
|
|
144
|
+
<ul className="divide-y" aria-label="Linked workflows">
|
|
145
|
+
{linkedFlows.map((w) => <LinkRow key={w.linkId} name={w.name} meta={[w.role, w.status].filter(Boolean).join(", ") || undefined} onOpen={onOpenWorkflow && w.workflowId ? () => onOpenWorkflow(w.workflowId) : undefined} onUnlink={() => setPending({ kind: "workflow", id: w.linkId, name: w.name })} busy={unlink.loading} />)}
|
|
146
|
+
</ul>
|
|
147
|
+
)}
|
|
148
|
+
<p className="pt-2 text-xs text-muted-foreground">Linking a new workflow to a campaign is not available through the API yet.</p>
|
|
149
|
+
</SectionCard>
|
|
150
|
+
|
|
151
|
+
<SectionCard title="Newsletters" description="A newsletter belongs to one campaign. Unlinking keeps the newsletter.">
|
|
152
|
+
{letters.error ? <ApiError error={letters.error} title="Could not load newsletters" onRetry={() => void letters.refetch()} /> : !letters.data ? <Skeleton className="h-16 w-full" aria-label="Loading newsletters" /> : (
|
|
153
|
+
<>
|
|
154
|
+
{thisLetters.length === 0 ? <EmptyState compact title="No newsletter linked" /> : (
|
|
155
|
+
<ul className="divide-y" aria-label="Linked newsletters">
|
|
156
|
+
{thisLetters.map((n) => <LinkRow key={idOf(n.id)} name={text(n.name) || `Newsletter ${idOf(n.id)}`} meta={text(n.status) || undefined} onOpen={onOpenNewsletter ? () => onOpenNewsletter(idOf(n.id)) : undefined} onUnlink={() => setPending({ kind: "newsletter", id: idOf(n.id), name: text(n.name) || `Newsletter ${idOf(n.id)}` })} busy={unlink.loading} />)}
|
|
157
|
+
</ul>
|
|
158
|
+
)}
|
|
159
|
+
<LinkPicker label="Link a newsletter" options={freeLetters} busy={link.loading} onLink={(id) => void link.mutate({ kind: "newsletter", id })} />
|
|
160
|
+
{otherLetters > 0 ? <p className="pt-1 text-xs text-muted-foreground">{otherLetters} {otherLetters === 1 ? "newsletter belongs" : "newsletters belong"} to other campaigns and {otherLetters === 1 ? "is" : "are"} not listed, because linking would move {otherLetters === 1 ? "it" : "them"}.</p> : null}
|
|
161
|
+
</>
|
|
162
|
+
)}
|
|
163
|
+
</SectionCard>
|
|
164
|
+
|
|
165
|
+
{link.error ? <ApiError error={link.error} title="Could not link" onRetry={link.reset} /> : null}
|
|
166
|
+
{unlink.error ? <ApiError error={unlink.error} title="Could not unlink" onRetry={unlink.reset} /> : null}
|
|
167
|
+
<ConfirmSpendDialog
|
|
168
|
+
open={pending !== null}
|
|
169
|
+
tier="destructive"
|
|
170
|
+
title={`Unlink "${pending?.name ?? ""}" from this campaign?`}
|
|
171
|
+
description={pending?.kind === "page" ? "The landing page stays published and keeps its other campaigns. Only the tie to this campaign is removed." : pending?.kind === "workflow" ? "The workflow itself is kept. It stops being part of this campaign." : "The newsletter is kept and keeps sending. It is no longer part of this campaign."}
|
|
172
|
+
confirmLabel="Unlink"
|
|
173
|
+
affected={1}
|
|
174
|
+
confirming={unlink.loading}
|
|
175
|
+
onCancel={() => setPending(null)}
|
|
176
|
+
onConfirm={() => { if (pending) void unlink.mutate(pending); }}
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
@@ -40,7 +40,7 @@ function Tiles({ data }: { data: Record<string, unknown> }) {
|
|
|
40
40
|
{tiles.map(([k, v]) => (
|
|
41
41
|
<div key={k} className="rounded-md border p-3">
|
|
42
42
|
<dt className="text-xs capitalize text-muted-foreground">{k}</dt>
|
|
43
|
-
<dd className="text-xl font-
|
|
43
|
+
<dd className="text-xl font-medium">{v}</dd>
|
|
44
44
|
</div>
|
|
45
45
|
))}
|
|
46
46
|
</dl>
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import { useEffect, useState } from "react";
|
|
27
27
|
import { Button, Input, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
28
28
|
import { ApiError } from "@/g8/components/api-error";
|
|
29
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
29
30
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
30
31
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
31
32
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
@@ -62,8 +63,11 @@ const emptyDraft: Draft = { name: "", category: "Outbound", goal: "", brief: "",
|
|
|
62
63
|
|
|
63
64
|
interface Idea { id?: string; campaign_name?: string; campaign_category?: string; core_concept?: string; why_this_works?: string; primary_hook?: string; target_channels?: string[] }
|
|
64
65
|
|
|
66
|
+
/** The persona API has no paging: a full read is announced under the checkboxes. */
|
|
67
|
+
const PERSONA_LIMIT = 50;
|
|
68
|
+
|
|
65
69
|
function IdeaStep({ websiteUrl, onPick, onSkip }: { websiteUrl: string; onPick: (i: Idea) => void; onSkip: () => void }) {
|
|
66
|
-
const personas = useG8Query((signal) => g8Get("/personas", { limit:
|
|
70
|
+
const personas = useG8Query((signal) => g8Get("/personas", { limit: PERSONA_LIMIT }, { signal }), []);
|
|
67
71
|
const [goal, setGoal] = useState(GOALS[0]);
|
|
68
72
|
const [count, setCount] = useState(5);
|
|
69
73
|
const [direction, setDirection] = useState("");
|
|
@@ -104,6 +108,7 @@ function IdeaStep({ websiteUrl, onPick, onSkip }: { websiteUrl: string; onPick:
|
|
|
104
108
|
<input type="checkbox" checked={selected.includes(p.title ?? "")} onChange={(e) => setSelected(e.target.checked ? [...selected, p.title ?? ""] : selected.filter((t) => t !== p.title))} />{p.title}
|
|
105
109
|
</label>
|
|
106
110
|
))}
|
|
111
|
+
<FirstNotice shown={personaRows.length} limit={PERSONA_LIMIT} noun="personas" />
|
|
107
112
|
</fieldset>
|
|
108
113
|
) : null}
|
|
109
114
|
{[start.error, poll.error].map((e, i) => (e ? <ApiError key={i} error={e} title="Idea generation failed" /> : null))}
|
|
@@ -111,10 +116,10 @@ function IdeaStep({ websiteUrl, onPick, onSkip }: { websiteUrl: string; onPick:
|
|
|
111
116
|
<Button disabled={!websiteUrl || poll.polling} loading={start.loading} onClick={() => setConfirm(true)}>Generate ideas</Button>
|
|
112
117
|
<Button variant="outline" onClick={onSkip}>Skip, write my own</Button>
|
|
113
118
|
</div>
|
|
114
|
-
{!websiteUrl ? <p className="text-sm text-
|
|
119
|
+
{!websiteUrl ? <p className="text-sm text-warning-foreground">Save the company website in IcpEditor first; ideas are generated from it.</p> : null}
|
|
115
120
|
{poll.polling ? <p role="status" className="text-sm text-muted-foreground">Generating ideas. This takes 10 to 20 seconds.</p> : null}
|
|
116
|
-
{!poll.polling && !poll.done && poll.attempts > 0 && !poll.error ? <p role="status" className="text-sm text-
|
|
117
|
-
{failed ? <p role="alert" className="text-sm text-
|
|
121
|
+
{!poll.polling && !poll.done && poll.attempts > 0 && !poll.error ? <p role="status" className="text-sm text-warning-foreground">Still running. Check back in a minute and generate again if nothing appears.</p> : null}
|
|
122
|
+
{failed ? <p role="alert" className="text-sm text-error-foreground">Generation failed: {text(poll.status?.error) || "try again"}.</p> : null}
|
|
118
123
|
{poll.done && !failed && ideas.length === 0 ? <p className="text-sm text-muted-foreground">No ideas came back. Try a different goal or direction.</p> : null}
|
|
119
124
|
<ul className="space-y-2">
|
|
120
125
|
{ideas.map((idea, i) => (
|
|
@@ -206,7 +211,7 @@ export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, auto
|
|
|
206
211
|
</nav>
|
|
207
212
|
|
|
208
213
|
{dispatchFailed ? (
|
|
209
|
-
<p role="alert" className="rounded bg-
|
|
214
|
+
<p role="alert" className="rounded bg-warning p-2 text-sm text-warning-foreground">
|
|
210
215
|
The campaign was created, but writing its documents could not be started. Write the email copy from the Launch step before launching.
|
|
211
216
|
</p>
|
|
212
217
|
) : null}
|
|
@@ -17,6 +17,7 @@ import { useState } from "react";
|
|
|
17
17
|
import type { ReactNode } from "react";
|
|
18
18
|
import { Button, Input, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
19
19
|
import { ApiError } from "@/g8/components/api-error";
|
|
20
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
20
21
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
21
22
|
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
22
23
|
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
@@ -48,10 +49,13 @@ function isPinned(r: Record<string, unknown> | undefined): boolean {
|
|
|
48
49
|
return Boolean(r && (r.pinned ?? r.is_pinned));
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
/** The ICP API has no paging: when the read is full, a missing ICP may just be beyond it, and the screen says so. */
|
|
53
|
+
const ICP_LIMIT = 200;
|
|
54
|
+
|
|
51
55
|
export function IcpDetail({ icpId, onClose, onExpand, onCollapse, onChanged, renderActions, className }: IcpDetailProps) {
|
|
52
56
|
const [form, setForm] = useState<Form | null>(null);
|
|
53
57
|
const [archiving, setArchiving] = useState(false);
|
|
54
|
-
const q = useG8Query((signal) => g8Get("/icps", { limit:
|
|
58
|
+
const q = useG8Query((signal) => g8Get("/icps", { limit: ICP_LIMIT }, { signal }), [icpId]);
|
|
55
59
|
const icp = listOf<Record<string, any>>(q.data).find((r) => String(r.id) === icpId);
|
|
56
60
|
|
|
57
61
|
const save = useG8Mutation(async (f: Form) => {
|
|
@@ -75,7 +79,12 @@ export function IcpDetail({ icpId, onClose, onExpand, onCollapse, onChanged, ren
|
|
|
75
79
|
|
|
76
80
|
if (q.error) return <ApiError error={q.error} title="Could not load the ICP" onRetry={() => void q.refetch()} className={className} />;
|
|
77
81
|
if (q.loading && !q.data) return <Skeleton className={`h-48 w-full ${className ?? ""}`} />;
|
|
78
|
-
if (!icp) return
|
|
82
|
+
if (!icp) return (
|
|
83
|
+
<div className={`p-4 text-sm ${className ?? ""}`}>
|
|
84
|
+
<p>This ICP no longer exists or was archived.</p>
|
|
85
|
+
<FirstNotice shown={listOf(q.data).length} limit={ICP_LIMIT} noun="ICPs" />
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
79
88
|
|
|
80
89
|
const f = (icp.firmographics ?? {}) as Record<string, unknown>;
|
|
81
90
|
const arr = (v: unknown) => (Array.isArray(v) ? v.join(", ") : text(v));
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import { useState } from "react";
|
|
22
22
|
import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Textarea } from "@graph8/sdk/ui";
|
|
23
23
|
import { ApiError } from "@/g8/components/api-error";
|
|
24
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
24
25
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
25
26
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
26
27
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
@@ -108,8 +109,8 @@ function ProfileCard({ websiteUrl: override, onChange }: IcpEditorProps) {
|
|
|
108
109
|
</label>
|
|
109
110
|
) : null}
|
|
110
111
|
{status.polling ? <p role="status" className="text-sm text-muted-foreground">Building the profile. This can take a minute.</p> : null}
|
|
111
|
-
{status.done && !status.polling ? <p role="status" className="text-sm text-
|
|
112
|
-
{saved ? <p role="status" className="text-sm text-
|
|
112
|
+
{status.done && !status.polling ? <p role="status" className="text-sm text-positive-foreground">Profile build finished.</p> : null}
|
|
113
|
+
{saved ? <p role="status" className="text-sm text-positive-foreground">Saved.</p> : null}
|
|
113
114
|
{[save.error, generate.error, approve.error, status.error].map((e, i) => (e ? <ApiError key={i} error={e} /> : null))}
|
|
114
115
|
<div className="flex flex-wrap gap-2">
|
|
115
116
|
<Button loading={save.loading} disabled={!website.trim() || (approved && !unlock)} onClick={() => void save.mutate(undefined)}>Save profile</Button>
|
|
@@ -136,10 +137,13 @@ const emptyForm: IcpForm = { name: "", description: "", industries: "", size: ""
|
|
|
136
137
|
|
|
137
138
|
const formOf = (icp: IcpRow): IcpForm => icpFormOf(icp as Record<string, any>);
|
|
138
139
|
|
|
140
|
+
/** The ICP API has no paging: a full read is announced under the table. */
|
|
141
|
+
const ICP_LIMIT = 100;
|
|
142
|
+
|
|
139
143
|
export function IcpEditor(props: IcpEditorProps) {
|
|
140
144
|
const { onChange, onOpen, className } = props;
|
|
141
145
|
const profile = useCompanyProfile(props.websiteUrl);
|
|
142
|
-
const icps = useG8Query((signal) => g8Get("/icps", { limit:
|
|
146
|
+
const icps = useG8Query((signal) => g8Get("/icps", { limit: ICP_LIMIT }, { signal }), [props.refreshKey]);
|
|
143
147
|
const [editing, setEditing] = useState<IcpRow | "new" | null>(null);
|
|
144
148
|
const [form, setForm] = useState<IcpForm>(emptyForm);
|
|
145
149
|
const [archiving, setArchiving] = useState<IcpRow | null>(null);
|
|
@@ -192,6 +196,7 @@ export function IcpEditor(props: IcpEditorProps) {
|
|
|
192
196
|
))}
|
|
193
197
|
</TableBody>
|
|
194
198
|
</Table>
|
|
199
|
+
<FirstNotice shown={rows.length} limit={ICP_LIMIT} noun="ICPs" />
|
|
195
200
|
{!icps.loading && !icps.error && rows.length === 0 ? <EmptyState title="No ICPs yet" description="Describe who you sell to. Campaigns and personas build on it." /> : null}
|
|
196
201
|
</section>
|
|
197
202
|
|
|
@@ -16,6 +16,8 @@ import { useState } from "react";
|
|
|
16
16
|
import type { ReactNode } from "react";
|
|
17
17
|
import { Button, Input, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
18
18
|
import { ApiError } from "@/g8/components/api-error";
|
|
19
|
+
import { useAllPages } from "@/g8/hooks/use-all-pages";
|
|
20
|
+
import { hasNextOf } from "@/g8/lib/paging";
|
|
19
21
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
20
22
|
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
21
23
|
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
@@ -49,11 +51,18 @@ function isPinned(r: Record<string, unknown> | undefined): boolean {
|
|
|
49
51
|
return Boolean(r && (r.pinned ?? r.is_pinned));
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
/** Campaigns per read: every page is read, so no targeting campaign is missed. */
|
|
55
|
+
const CAMPAIGN_PAGE = 100;
|
|
56
|
+
|
|
52
57
|
export function PersonaDetail({ personaId, onClose, onExpand, onCollapse, onChanged, onOpenCampaign, renderActions, className }: PersonaDetailProps) {
|
|
53
58
|
const [form, setForm] = useState<Form | null>(null);
|
|
54
59
|
const [archiving, setArchiving] = useState(false);
|
|
55
60
|
const q = useG8Query((signal) => g8Get(`/personas/${personaId}`, undefined, { signal }), [personaId]);
|
|
56
|
-
const campaigns =
|
|
61
|
+
const campaigns = useAllPages<CampaignRow>(async (page, signal) => {
|
|
62
|
+
const env = await g8Get("/campaigns", { page, limit: CAMPAIGN_PAGE }, { signal });
|
|
63
|
+
const rows = listOf<CampaignRow>(env);
|
|
64
|
+
return { rows, hasNext: hasNextOf(env, CAMPAIGN_PAGE, rows.length) };
|
|
65
|
+
}, [personaId]);
|
|
57
66
|
const p = objectOf(q.data) as Record<string, any>;
|
|
58
67
|
|
|
59
68
|
const save = useG8Mutation(async (f: Form) => {
|
|
@@ -79,7 +88,7 @@ export function PersonaDetail({ personaId, onClose, onExpand, onCollapse, onChan
|
|
|
79
88
|
if (q.loading && !q.data) return <Skeleton className={`h-48 w-full ${className ?? ""}`} />;
|
|
80
89
|
|
|
81
90
|
const title = text(p.title) || `Persona ${personaId}`;
|
|
82
|
-
const targeting =
|
|
91
|
+
const targeting = campaigns.items.filter((c) => (c.target_persona ?? "").trim().toLowerCase() === title.trim().toLowerCase());
|
|
83
92
|
const startEdit = () => {
|
|
84
93
|
save.reset();
|
|
85
94
|
setForm(personaFormOf(p));
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { useState } from "react";
|
|
20
20
|
import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Textarea } from "@graph8/sdk/ui";
|
|
21
21
|
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
22
23
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
23
24
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
24
25
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
@@ -67,9 +68,12 @@ const emptyForm: PersonaForm = { title: "", why: "", signals: "", approach: "",
|
|
|
67
68
|
|
|
68
69
|
const formOf = (p: PersonaRow): PersonaForm => personaFormOf(p as Record<string, any>);
|
|
69
70
|
|
|
71
|
+
/** The persona API has no paging: a full read is announced under the table. */
|
|
72
|
+
const PERSONA_LIMIT = 100;
|
|
73
|
+
|
|
70
74
|
export function PersonaEditor({ websiteUrl, onChange, onOpen, refreshKey, className }: PersonaEditorProps) {
|
|
71
75
|
const profile = useCompanyProfile(websiteUrl);
|
|
72
|
-
const personas = useG8Query((signal) => g8Get("/personas", { limit:
|
|
76
|
+
const personas = useG8Query((signal) => g8Get("/personas", { limit: PERSONA_LIMIT }, { signal }), [refreshKey]);
|
|
73
77
|
const [editing, setEditing] = useState<PersonaRow | "new" | null>(null);
|
|
74
78
|
const [form, setForm] = useState<PersonaForm>(emptyForm);
|
|
75
79
|
const [archiving, setArchiving] = useState<PersonaRow | null>(null);
|
|
@@ -158,6 +162,7 @@ export function PersonaEditor({ websiteUrl, onChange, onOpen, refreshKey, classN
|
|
|
158
162
|
))}
|
|
159
163
|
</TableBody>
|
|
160
164
|
</Table>
|
|
165
|
+
<FirstNotice shown={rows.length} limit={PERSONA_LIMIT} noun="personas" />
|
|
161
166
|
{!personas.loading && !personas.error && rows.length === 0 ? <EmptyState title="No personas yet" description="Add the roles you sell to, or let AI recommend them." /> : null}
|
|
162
167
|
|
|
163
168
|
<Dialog open={editing !== null} onOpenChange={(o) => { if (!o) setEditing(null); }}>
|
|
@@ -98,10 +98,10 @@ export function commaList(value: string): string[] {
|
|
|
98
98
|
|
|
99
99
|
export function statusClass(status: string | null | undefined): string {
|
|
100
100
|
const s = (status ?? "").toLowerCase();
|
|
101
|
-
if (["active", "live", "completed", "approved", "complete", "done"].includes(s)) return "bg-
|
|
102
|
-
if (["draft", "pending", "running", "generating", "in_progress", "copy_in_progress", "scheduling"].includes(s)) return "bg-
|
|
103
|
-
if (s) return "bg-
|
|
104
|
-
return "bg-
|
|
101
|
+
if (["active", "live", "completed", "approved", "complete", "done"].includes(s)) return "bg-positive text-positive-foreground";
|
|
102
|
+
if (["draft", "pending", "running", "generating", "in_progress", "copy_in_progress", "scheduling"].includes(s)) return "bg-warning text-warning-foreground";
|
|
103
|
+
if (s) return "bg-error text-error-foreground";
|
|
104
|
+
return "bg-muted text-foreground";
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
/** Timestamps are shown in UTC so the same value reads the same everywhere. */
|
|
@@ -34,7 +34,7 @@ export interface CallScorecardProps {
|
|
|
34
34
|
className?: string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const VERDICT_STYLE: Record<string, string> = { met: "text-
|
|
37
|
+
const VERDICT_STYLE: Record<string, string> = { met: "text-positive-foreground", partial: "text-warning-foreground", missed: "text-destructive" };
|
|
38
38
|
|
|
39
39
|
export function CallScorecard({ callId, onDeleted, allowDelete = true, className }: CallScorecardProps) {
|
|
40
40
|
const [audio, setAudio] = useState<{ callId: string; url: string } | null>(null);
|
|
@@ -67,13 +67,13 @@ export function CallScorecard({ callId, onDeleted, allowDelete = true, className
|
|
|
67
67
|
<div className={`flex flex-col gap-4 ${className ?? ""}`}>
|
|
68
68
|
<header className="flex flex-wrap items-start justify-between gap-3">
|
|
69
69
|
<div>
|
|
70
|
-
<h2 className="text-lg font-
|
|
70
|
+
<h2 className="text-lg font-medium">{row.prospect_name || "Practice call"}{row.prospect_company ? ` at ${row.prospect_company}` : ""}</h2>
|
|
71
71
|
<p className="text-sm text-muted-foreground">
|
|
72
72
|
{[row.user_email, row.call_mode, row.call_status, row.duration_ms ? clock(row.duration_ms) : null, row.date_of_call ? new Date(row.date_of_call).toLocaleDateString() : null].filter(Boolean).join(" - ")}
|
|
73
73
|
</p>
|
|
74
74
|
</div>
|
|
75
75
|
<div className="text-right">
|
|
76
|
-
<div data-testid="overall-score" className="text-3xl font-
|
|
76
|
+
<div data-testid="overall-score" className="text-3xl font-medium">{card?.scorePct != null ? `${Math.round(card.scorePct)}%` : "-"}</div>
|
|
77
77
|
<div className="text-xs text-muted-foreground">{card ? "overall" : "not graded yet"}</div>
|
|
78
78
|
</div>
|
|
79
79
|
</header>
|
|
@@ -94,20 +94,20 @@ export function CallScorecard({ callId, onDeleted, allowDelete = true, className
|
|
|
94
94
|
))}
|
|
95
95
|
</ul>
|
|
96
96
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
97
|
-
<div><h3 className="mb-1 text-sm font-
|
|
98
|
-
<div><h3 className="mb-1 text-sm font-
|
|
97
|
+
<div><h3 className="mb-1 text-sm font-medium">Strengths</h3><ul className="list-disc pl-5 text-sm">{card.strengths.map((t, i) => <li key={i}>{t}</li>)}</ul></div>
|
|
98
|
+
<div><h3 className="mb-1 text-sm font-medium">To improve</h3><ul className="list-disc pl-5 text-sm">{card.improvements.map((t, i) => <li key={i}>{t}</li>)}</ul></div>
|
|
99
99
|
</div>
|
|
100
100
|
</>
|
|
101
101
|
)}
|
|
102
102
|
|
|
103
103
|
<div className="flex flex-col gap-2">
|
|
104
|
-
<h3 className="text-sm font-
|
|
104
|
+
<h3 className="text-sm font-medium">Recording</h3>
|
|
105
105
|
{row.audio_url ? (shownAudio ? <audio controls src={shownAudio} className="w-full" /> : <div><Button size="sm" variant="outline" loading={loadAudio.loading} onClick={() => void loadAudio.mutate(callId)}>Load recording</Button></div>) : <p className="text-sm text-muted-foreground">No recording.</p>}
|
|
106
106
|
{loadAudio.error ? <ApiError error={loadAudio.error} title="Could not load the recording" /> : null}
|
|
107
107
|
</div>
|
|
108
108
|
|
|
109
109
|
<div className="flex flex-col gap-2">
|
|
110
|
-
<h3 className="text-sm font-
|
|
110
|
+
<h3 className="text-sm font-medium">Transcript</h3>
|
|
111
111
|
{transcript.error ? <ApiError error={transcript.error} title="Could not load the transcript" onRetry={() => void transcript.refetch()} /> : null}
|
|
112
112
|
<ul className="flex max-h-96 flex-col gap-1 overflow-y-auto" data-testid="transcript">
|
|
113
113
|
{segments.map((s, i) => (
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# company-intelligence
|
|
2
|
+
|
|
3
|
+
Research **your own website** and read what it found: the company profile, competitors, brand news, products, keywords,
|
|
4
|
+
audience questions and brand assets; the wiki documents written from that research; and the six market research reports
|
|
5
|
+
(review sentiment, competitive teardown, voice of customer, GTM channels, analyst and media, buyer psychology). Billable steps
|
|
6
|
+
(a research run, a document rewrite, writing a report) are confirmed with the number of things they touch; deleting and
|
|
7
|
+
resetting are confirmed as destructive.
|
|
8
|
+
|
|
9
|
+
**What this is not.** The company intelligence API researches the workspace's own website and is locked to it. It has **no
|
|
10
|
+
lookup for other companies**, no technographics, no org chart, no firmographics for a prospect and no AI briefing endpoint (see
|
|
11
|
+
"API gaps"). The screens say so instead of inventing data.
|
|
12
|
+
|
|
13
|
+
## Screens
|
|
14
|
+
|
|
15
|
+
`CompanyIntelligenceScreen` (`components/company-intelligence-screen.tsx`) is the finished page. Copy it as your route; no props are required.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { CompanyIntelligenceScreen } from "@/g8/company-intelligence/components/company-intelligence-screen";
|
|
19
|
+
export default function Page() { return <CompanyIntelligenceScreen />; }
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Tabs (`?tab=`): **Overview** (start, follow and reset the research; the company profile), **Sources** (everything collected, and the sources that failed; open one with `?id=<type>` to read it and re-collect it), **Documents** (the wiki documents; `?id=` opens one to read or rewrite), **Reports** (the six reports and their workflow; `?id=` opens one). `&view=full` shows a detail as a full page.
|
|
23
|
+
|
|
24
|
+
Flows: research the website (confirmed; follows progress step by step), research again (`force`), reset the research (confirmed with the real record count), read each source (profile, competitors, news with sentiment, products, keywords, questions, trends, pages, brand assets), re-collect one source (confirmed), search and filter the wiki documents, read one as formatted text, rewrite one (confirmed), write one report or all six (confirmed with the count), retry a failed report, write the missing ones, stop a run, reset the workflow, read a report, edit its text (changed text only), approve it into the knowledge base and remove it again, read the model's recorded reasoning, write a report again, delete it. Optional props: `toolbarActions`, `renderDetailActions(record)`, `extraDetailTabs` (each `{ id, label, render(record) }`), where `record` is `{ kind: "source" | "document" | "report", id }`.
|
|
25
|
+
|
|
26
|
+
Parts you can mount alone: `ResearchRun`, `CompanyProfileCard`, `IntelligenceSources`, `SourceDetail`, `SourceView`, `WikiDocuments`, `WikiDocumentDetail`, `ResearchReports`, `ReportDetail`, and the CRM tab below.
|
|
27
|
+
|
|
28
|
+
### On a CRM company page: `CompanyIntelligenceTab`
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
<CompaniesScreen
|
|
32
|
+
extraDetailTabs={[{ id: "intelligence", label: "Intelligence", render: (c) => <CompanyIntelligenceTab company={c} /> }]}
|
|
33
|
+
/>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The stacks that include both `crm` and this block (apollo, zoominfo, clay, 6sense) already mount it. It shows the full profile when the company on screen **is** your workspace's website, the competitor entry your research holds when it is one of the competitors the research found, and otherwise says plainly that there is nothing to show. It never invents data for other companies.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
npx @graph8/sdk add company-intelligence
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Installs `foundation` first if missing. Everything lands under `g8/company-intelligence/`. Add `intelligence`, `global-context` and `research-reports` to the proxy allowlist (the installer does this from the block's operations).
|
|
45
|
+
|
|
46
|
+
## Prerequisites (the human must do these)
|
|
47
|
+
|
|
48
|
+
| Where | What | How to verify |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| graph8 key | `knowledge:read`, `knowledge:run`, `knowledge:delete` (research, documents) and `campaigns:read`, `campaigns:write`, `campaigns:run`, `campaigns:delete` (research reports). | `g8.api.me.describeCurrentKey()` |
|
|
51
|
+
| Credits | Research runs, document rewrites and report writing spend credits. The API quotes no price and has no balance read. | The owner knows the balance. |
|
|
52
|
+
| The business | Which website is researched. The first run locks the workspace to it. | `GET /api/v1/intelligence/primary-website` shows `has_website: true`. |
|
|
53
|
+
|
|
54
|
+
## The flow, in order (IDs passed between steps)
|
|
55
|
+
|
|
56
|
+
1. **Target**: `getIntelligencePrimaryWebsite` -> `primary_website_url` (null before the first run). If null, `getIntelligenceOrgDomain` -> `company_domain` is only a **suggestion**; a person confirms it.
|
|
57
|
+
2. **Research**: `analyzeWebsite({ website_url, force? })` behind `ConfirmSpendDialog` -> **`task_id`**. A run that finished less than an hour ago is returned as is (no new work, no charge) unless `force` is true. If a primary website is set, any other website answers 400.
|
|
58
|
+
3. **Follow**: `getIntelligenceStatus(task_id)` until `status` is not `pending`/`running`; `steps` has one state per source. `getLatestIntelligenceTask` gives the last `task_id`.
|
|
59
|
+
4. **Read**: `listIntelligenceDocuments` (what was collected), `getIntelligenceData(data_type, { website_url })` (one source, 404 = not collected), `getAllIntelligenceData(website_url)` (all of them, keyed by type; `website_url` here is the host without a scheme).
|
|
60
|
+
5. **Fix one source**: `retryIntelligenceSource({ website_url, data_type })` (billable). The last good data stays if it fails.
|
|
61
|
+
6. **Wiki documents**: `getGlobalContextTreeFlat` (each has `id`, `file_type`, `content`, `status`, `version`, `credits_used`), `getGlobalContextTree` (`stats.total`, `stats.completed`), `generateGlobalContextDocument(file_type, { doc_type, website_url })` (billable, rewrites one document).
|
|
62
|
+
7. **Reports**: `getResearchWorkflowStatus({ website_url })` -> six slots `report_1..report_6` with `id`, `report_number`, `report_type`, `status`. Then `generateResearchReport({ website_url, report_number })`, `generateAllResearchReports`, `resumeResearchWorkflow`, `retryResearchReport({ website_url, report_type })` (all billable), `stopAllResearch`, `resetResearchWorkflow` (destructive). `getResearchReport(id)` has the markdown; `updateResearchReport(id, { markdown_content })`, `approveResearchReport(id)`, `unapproveResearchReport(id)`, `deleteResearchReport(id)`, `getResearchReportThinking(id)` (recorded reasoning).
|
|
63
|
+
|
|
64
|
+
## Default decisions (do not ask the human unless marked)
|
|
65
|
+
|
|
66
|
+
| Decision | Default | Ask? |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| Which website is researched | The primary website once set; the workspace settings' company domain only as a suggestion | **yes** |
|
|
69
|
+
| Run again within the hour | No: the API returns the recent run for free | no |
|
|
70
|
+
| Writing reports | Never automatic; a person confirms the number first | no |
|
|
71
|
+
| Approving a report into the knowledge base | Only when asked (campaigns and the copilot read approved reports) | **yes** |
|
|
72
|
+
| Editing a report | Send only the changed text; clearing it sends `""` | no |
|
|
73
|
+
| Missing data | A dash, never 0, never a guess | no |
|
|
74
|
+
|
|
75
|
+
## Cost, in plain numbers
|
|
76
|
+
|
|
77
|
+
The API gives **no price in advance** and no balance read, so no screen shows an invented figure. Each confirm states the quantity
|
|
78
|
+
it can compute from the data on screen, and a real "last time" number when the API reports one.
|
|
79
|
+
|
|
80
|
+
| Action | Tier | Confirm says |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| Research the website | billable | Collects the sources of the last run (their count when known); or that a run under an hour old would be returned for free; that the first website becomes locked |
|
|
83
|
+
| Re-collect one source | billable | The one source and the website |
|
|
84
|
+
| Rewrite one wiki document | billable | The document and its version; credits it used last time when the API reports them |
|
|
85
|
+
| Write one report | billable | 1 report |
|
|
86
|
+
| Write all reports | billable | The number of reports, and how many finished ones are written again |
|
|
87
|
+
| Write the missing reports | billable | The count and the titles of the unfinished reports |
|
|
88
|
+
| Retry a failed report | billable | 1 report |
|
|
89
|
+
| Save report text | billable tier (re-indexes into knowledge) | That saving re-indexes the text |
|
|
90
|
+
| Reset the research | destructive | The real number of collected sources it deletes |
|
|
91
|
+
| Reset the report workflow | destructive | The number of reports that have content |
|
|
92
|
+
| Delete a report | destructive | 1 report, and what it used last time when known |
|
|
93
|
+
| Reads, approve, unapprove, stop | none / write | (stop is confirmed too: it cancels paid work in flight) |
|
|
94
|
+
|
|
95
|
+
## Operation coverage
|
|
96
|
+
|
|
97
|
+
Used (26): `analyzeWebsite`, `getIntelligenceStatus`, `getLatestIntelligenceTask`, `getIntelligencePrimaryWebsite`, `getIntelligenceOrgDomain`, `listIntelligenceDocuments`, `getIntelligenceData`, `getAllIntelligenceData`, `retryIntelligenceSource`, `resetIntelligence`, `getGlobalContextTree`, `getGlobalContextTreeFlat`, `generateGlobalContextDocument`, `getResearchReportThinking` (the 14 `companyIntelligence` operations, all used), plus from the `research` and `gtmContext` namespaces the report operations they need: `getResearchWorkflowStatus`, `getResearchReport`, `generateResearchReport`, `generateAllResearchReports`, `resumeResearchWorkflow`, `retryResearchReport`, `stopAllResearch`, `resetResearchWorkflow`, `approveResearchReport`, `unapproveResearchReport`, `deleteResearchReport`, `updateResearchReport`.
|
|
98
|
+
|
|
99
|
+
Skipped on purpose:
|
|
100
|
+
|
|
101
|
+
| Operation | Why |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `scaffoldResearchReports` | Creates six empty placeholder rows. The Reports tab already shows all six slots from the workflow status, and writing a report creates its row. |
|
|
104
|
+
| `getResearchReportByNumber` | The same report `getResearchReport` returns by id; the workflow status gives every id. |
|
|
105
|
+
| `listResearchReports` (gtmContext) | The workflow status lists the same six reports with their ids and approval state, per website. |
|
|
106
|
+
| `listIntelligenceData` (gtmContext) | The same sources as `listIntelligenceDocuments` / `getAllIntelligenceData`. |
|
|
107
|
+
| `getGlobalContextDocument`, `listGlobalContextDocuments`, `updateGlobalContextDocument`, `deleteDocument`, `deleteAllDocuments`, `regenerateGlobalContext`, `generateAllDocuments`, `resumePipeline`, `stopAllGeneration`, `scaffold*`, `reindexAllDocuments`, `resetStuckDocuments`, pipelines, highlights, analytics, feedback, usage tracking, buying-journey (gtmContext) | Wiki editing and the whole-wiki pipeline belong to the campaigns/GTM context surface, not to company intelligence. This block reads and rewrites one document at a time. |
|
|
108
|
+
|
|
109
|
+
## API gaps (what was asked for and the API does not have)
|
|
110
|
+
|
|
111
|
+
| Asked for | Reality | What the block does |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| Company profile / firmographics / technographics of **any** company | The research is of the workspace's own website and is locked to it (`analyze` answers 400 for another website). Only `company_enrichment`, `product_inventory`, `company_keywords` etc. of that one website exist. No technology-stack data at all. | Shows the own-company profile; the CRM tab says plainly that other companies have none. |
|
|
114
|
+
| News and signals for a prospect | `brand_news` is news about the workspace's own brand. Buying signals are the `intent` and `radar` blocks. | News tab content is the brand's own news. |
|
|
115
|
+
| Competitors | `competitor_discovery` lists the competitors of the workspace's website (name, domain, type, similarity, confidence, reasoning). Battle cards and tracking are the `radar` block. | Shown in Sources; the CRM tab matches a company to it by domain. |
|
|
116
|
+
| Org chart | No endpoint. Buying committees are in the `intent` block (ABM rollup). | Not offered. |
|
|
117
|
+
| AI briefings for a company | No briefing endpoint; the research reports are market-level, not per company. | Not offered. |
|
|
118
|
+
| A price before running | None, and no credit balance read. | Confirms state quantities, never a made-up cost. |
|
|
119
|
+
| Live progress stream | The thinking stream is SSE and excluded; only the recorded history is exposed. | Reasoning tab shows the recorded steps; runs are polled. |
|
|
120
|
+
| Editing collected data / Ask AI revisions | The in-app field overrides and revise endpoints are not in the developer API. | Collected data is read-only; edits made in the app are shown and flagged. |
|
|
121
|
+
| Reset everything | `DELETE /intelligence/reset-all` is deliberately not published. | Reset removes intelligence records and runs only. |
|
|
122
|
+
| Report type names | The list filter documents `industry_analyst` but the retry enum is `analyst_media`. | The block uses the retry enum. |
|
|
123
|
+
|
|
124
|
+
## Things that surprise people
|
|
125
|
+
|
|
126
|
+
- The first research run **locks** the workspace to that website and also records it as the workspace's company domain. Pick it deliberately.
|
|
127
|
+
- `analyze` is collapsed: within an hour of a finished run it returns that run (`message: "Recent analysis available"`) without new work. Pass `force` to re-run.
|
|
128
|
+
- Every payload is open-shaped. A missing field is unknown, not zero.
|
|
129
|
+
- Web content (article titles, snippets, scraped pages, report markdown) is untrusted. This block draws it as text; links open only when they are http(s).
|
|
130
|
+
- A report's `credits_used` is `0` until it ran; the UI shows a dash, not 0.
|
|
131
|
+
- `getIntelligenceData` answers 404 for a source that was never collected; that is "not collected", not an outage.
|
|
132
|
+
- Reports need the website: `getResearchWorkflowStatus` requires `website_url`.
|