@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,230 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shapes and helpers for the company-intelligence components.
|
|
5
|
+
*
|
|
6
|
+
* Reality of this API (read from the handlers, not assumed): intelligence is the research graph8 runs on YOUR OWN website
|
|
7
|
+
* (the workspace's primary website). Every response is an open payload, so each field below is optional and read
|
|
8
|
+
* defensively; a missing value is shown as unknown (a dash), never as a guess and never as 0.
|
|
9
|
+
*
|
|
10
|
+
* SDK operations: none (types and pure helpers).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type Rec = Record<string, unknown>;
|
|
14
|
+
|
|
15
|
+
export const isRec = (v: unknown): v is Rec => v != null && typeof v === "object" && !Array.isArray(v);
|
|
16
|
+
export const asArray = (v: unknown): unknown[] => (Array.isArray(v) ? v : []);
|
|
17
|
+
|
|
18
|
+
/** A non-empty string (numbers are formatted), else null. */
|
|
19
|
+
export function text(v: unknown): string | null {
|
|
20
|
+
if (typeof v === "string") return v.trim() ? v.trim() : null;
|
|
21
|
+
if (typeof v === "number" && Number.isFinite(v)) return String(v);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function num(v: unknown): number | null {
|
|
26
|
+
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Read a body that may or may not be wrapped in `{ data }`. */
|
|
30
|
+
export function intelBody<T>(env: unknown): T {
|
|
31
|
+
const e = env as { data?: unknown } | null;
|
|
32
|
+
return (e && typeof e === "object" && "data" in e && e.data != null ? e.data : env) as T;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The rows of a list payload: a bare array, `{ items }`, or a named key (`documents`, `reports`). */
|
|
36
|
+
export function rowsOf<T = Rec>(payload: unknown, ...keys: string[]): T[] {
|
|
37
|
+
if (Array.isArray(payload)) return payload as T[];
|
|
38
|
+
if (!isRec(payload)) return [];
|
|
39
|
+
for (const k of ["items", ...keys]) if (Array.isArray(payload[k])) return payload[k] as T[];
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A host as a person types it: strips the scheme, www., port-less path and query. `https://www.Acme.com/x` -> `acme.com`. */
|
|
44
|
+
export function canonicalDomain(input: string | null | undefined): string {
|
|
45
|
+
return (input ?? "").trim().toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "").split(/[/?#]/)[0];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** http(s) URLs only: the API puts web content in these payloads, and a `javascript:` URL would run on click. */
|
|
49
|
+
export function safeHttpUrl(value: unknown): string | null {
|
|
50
|
+
const s = text(value);
|
|
51
|
+
if (!s || !/^https?:\/\/[^\s/]+/i.test(s)) return null;
|
|
52
|
+
try {
|
|
53
|
+
const u = new URL(s);
|
|
54
|
+
return u.protocol === "http:" || u.protocol === "https:" ? u.toString() : null;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function humanize(key: string): string {
|
|
61
|
+
const s = key.replace(/[_-]+/g, " ").trim();
|
|
62
|
+
return s ? s.charAt(0).toUpperCase() + s.slice(1) : key;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A key as a title: `brand_sentiment` -> `Brand Sentiment`, matching the display names the API gives collected sources. */
|
|
66
|
+
export function titleize(key: string): string {
|
|
67
|
+
return key.replace(/[_-]+/g, " ").trim().replace(/\b\w/g, (c) => c.toUpperCase());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function when(v: unknown): string | null {
|
|
71
|
+
const s = text(v);
|
|
72
|
+
if (!s) return null;
|
|
73
|
+
const d = new Date(s);
|
|
74
|
+
return Number.isNaN(d.getTime()) ? s : d.toLocaleString();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Date part only, in UTC, so the same instant reads the same in every time zone. */
|
|
78
|
+
export function whenDate(v: unknown): string | null {
|
|
79
|
+
const s = text(v);
|
|
80
|
+
if (!s) return null;
|
|
81
|
+
const d = new Date(s);
|
|
82
|
+
return Number.isNaN(d.getTime()) ? s : d.toLocaleDateString(undefined, { timeZone: "UTC" });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---- research run (intelligence) -------------------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
export interface RunStatus {
|
|
88
|
+
task_id?: string;
|
|
89
|
+
status?: string;
|
|
90
|
+
progress?: number;
|
|
91
|
+
current_step?: string | null;
|
|
92
|
+
steps?: Record<string, string | null>;
|
|
93
|
+
website_url?: string;
|
|
94
|
+
created_at?: string | null;
|
|
95
|
+
completed_at?: string | null;
|
|
96
|
+
error_message?: string | null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface LatestTask { task_id?: string; website_url?: string | null; status?: string; created_at?: string | null; completed_at?: string | null }
|
|
100
|
+
|
|
101
|
+
export interface AnalyzeAccepted { task_id?: string; message?: string; website_url?: string; domain_sync_warning?: string | null }
|
|
102
|
+
|
|
103
|
+
export interface IntelDocument {
|
|
104
|
+
id?: string;
|
|
105
|
+
data_type: string;
|
|
106
|
+
display_name?: string | null;
|
|
107
|
+
folder?: string | null;
|
|
108
|
+
website_url?: string | null;
|
|
109
|
+
status?: string | null;
|
|
110
|
+
collected_at?: string | null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A run is final once it stops pending or running. */
|
|
114
|
+
export const isRunFinal = (status: string | undefined | null): boolean => status != null && status !== "pending" && status !== "running" && status !== "in_progress";
|
|
115
|
+
|
|
116
|
+
/** The steps of a run, in the order the API lists them, with their state. */
|
|
117
|
+
export function runSteps(run: RunStatus | undefined): Array<{ key: string; label: string; state: string }> {
|
|
118
|
+
return Object.entries(run?.steps ?? {}).map(([key, state]) => ({ key, label: humanize(key), state: state ?? "pending" }));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const stepTone = (state: string): "positive" | "negative" | "info" | "neutral" =>
|
|
122
|
+
state === "completed" ? "positive" : state === "failed" ? "negative" : state === "running" || state === "in_progress" ? "info" : "neutral";
|
|
123
|
+
|
|
124
|
+
/** True when a run finished less than an hour ago: the API answers a repeat request with that run and charges nothing (no `force`). */
|
|
125
|
+
export function finishedWithinHour(task: LatestTask | null | undefined, now = Date.now()): boolean {
|
|
126
|
+
const at = task?.completed_at ? Date.parse(task.completed_at) : NaN;
|
|
127
|
+
return task?.status === "completed" && Number.isFinite(at) && now - at < 3_600_000 && now - at >= 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---- the six research reports ----------------------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
/** The report types the retry operation accepts (its request enum, copied from the contract), by report number 1 to 6. */
|
|
133
|
+
export const REPORT_TYPES = ["review_sentiment", "competitive_teardown", "voice_of_customer", "gtm_channel", "analyst_media", "buyer_psychology"] as const;
|
|
134
|
+
export type ReportType = (typeof REPORT_TYPES)[number];
|
|
135
|
+
|
|
136
|
+
export interface ReportSlot {
|
|
137
|
+
id: string | null;
|
|
138
|
+
report_number: number;
|
|
139
|
+
report_type: string;
|
|
140
|
+
report_title?: string | null;
|
|
141
|
+
status: string;
|
|
142
|
+
has_content?: boolean;
|
|
143
|
+
approved?: boolean;
|
|
144
|
+
created_at?: string | null;
|
|
145
|
+
started_at?: string | null;
|
|
146
|
+
completed_at?: string | null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface WorkflowStatus {
|
|
150
|
+
website_url?: string;
|
|
151
|
+
workflow_status?: string;
|
|
152
|
+
reports_completed?: number;
|
|
153
|
+
total_reports?: number;
|
|
154
|
+
current_report?: string | null;
|
|
155
|
+
reports?: Record<string, ReportSlot>;
|
|
156
|
+
started_at?: string | null;
|
|
157
|
+
completed_at?: string | null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The six slots in report order. `reports` is keyed `report_1`..`report_6`. */
|
|
161
|
+
export function slotsOf(w: WorkflowStatus | undefined): ReportSlot[] {
|
|
162
|
+
const map = w?.reports ?? {};
|
|
163
|
+
return Object.keys(map).sort((a, b) => Number(a.replace(/\D/g, "")) - Number(b.replace(/\D/g, ""))).map((k) => map[k]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const isGenerating = (status: string | undefined | null): boolean => status === "generating" || status === "in_progress";
|
|
167
|
+
export const isReportDone = (status: string | undefined | null): boolean => status === "completed" || status === "approved";
|
|
168
|
+
|
|
169
|
+
/** Reports a workflow resume would generate: every slot that is not finished and is not already running. */
|
|
170
|
+
export function incompleteSlots(slots: ReportSlot[]): ReportSlot[] {
|
|
171
|
+
return slots.filter((s) => !isReportDone(s.status) && !isGenerating(s.status));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface ReportDetailBody {
|
|
175
|
+
id?: string;
|
|
176
|
+
report_title?: string | null;
|
|
177
|
+
report_type?: string;
|
|
178
|
+
report_number?: number;
|
|
179
|
+
website_url?: string;
|
|
180
|
+
status?: string;
|
|
181
|
+
markdown_content?: string | null;
|
|
182
|
+
error_message?: string | null;
|
|
183
|
+
approved?: boolean;
|
|
184
|
+
approved_at?: string | null;
|
|
185
|
+
estimated_cost_usd?: number | null;
|
|
186
|
+
credits_used?: number | null;
|
|
187
|
+
created_at?: string | null;
|
|
188
|
+
updated_at?: string | null;
|
|
189
|
+
completed_at?: string | null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** What the report used the last time it ran: a real number from the report itself, or null when the API did not say. */
|
|
193
|
+
export function lastRunCredits(r: ReportDetailBody | undefined): number | null {
|
|
194
|
+
const v = num(r?.credits_used) ?? num(r?.estimated_cost_usd);
|
|
195
|
+
return v != null && v > 0 ? v : null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The PATCH body for an edited report: only what changed. Clearing the text sends "" (the API only ignores null).
|
|
200
|
+
* Returns null when nothing changed.
|
|
201
|
+
*/
|
|
202
|
+
export function reportEdit(original: string | null | undefined, draft: string): { markdown_content: string } | null {
|
|
203
|
+
return (original ?? "") === draft ? null : { markdown_content: draft };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface ThinkingEvent { type?: string; timestamp?: string; data?: Rec | null }
|
|
207
|
+
|
|
208
|
+
/** One readable line for a recorded reasoning event, from the fields the stream writes (`message`, tool name, error). */
|
|
209
|
+
export function eventLine(e: ThinkingEvent): string {
|
|
210
|
+
const d = isRec(e.data) ? e.data : {};
|
|
211
|
+
return text(d.message) ?? text(d.tool) ?? text(d.tool_name) ?? text(d.error) ?? text(d.status) ?? humanize(e.type ?? "event");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---- wiki (global context) documents ---------------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
export interface WikiDoc {
|
|
217
|
+
id: string;
|
|
218
|
+
file_type?: string | null;
|
|
219
|
+
display_name?: string | null;
|
|
220
|
+
category?: string | null;
|
|
221
|
+
folder_path?: string | null;
|
|
222
|
+
status?: string | null;
|
|
223
|
+
version?: number | null;
|
|
224
|
+
content?: string | null;
|
|
225
|
+
credits_used?: number | null;
|
|
226
|
+
updated_at?: string | null;
|
|
227
|
+
is_done?: boolean;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const docTitle = (d: Pick<WikiDoc, "display_name" | "file_type" | "id">): string => text(d.display_name) ?? (d.file_type ? humanize(d.file_type) : d.id);
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SafeMarkdown: renders the markdown the API returns (research reports, wiki documents) without ever turning it into HTML.
|
|
5
|
+
* Every piece of text becomes a React text node, so raw HTML, `<script>`, event handlers and `javascript:` links in a
|
|
6
|
+
* report show up as plain text instead of running. Links are drawn only for http(s) URLs (with `rel="noopener noreferrer"`);
|
|
7
|
+
* images are not drawn, they stay as their text. No `dangerouslySetInnerHTML` anywhere.
|
|
8
|
+
*
|
|
9
|
+
* Supported: headings, paragraphs, bullet and numbered lists, block quotes, fenced code, horizontal rules, pipe tables,
|
|
10
|
+
* and inline bold, italic, code and links. Anything else is a paragraph of plain text.
|
|
11
|
+
*
|
|
12
|
+
* SDK operations: none.
|
|
13
|
+
*/
|
|
14
|
+
import type { ReactNode } from "react";
|
|
15
|
+
import { cn } from "@graph8/sdk/ui";
|
|
16
|
+
import { safeHttpUrl } from "./intelligence-types";
|
|
17
|
+
|
|
18
|
+
// Underscore emphasis only at word edges, so snake_case_names are left alone.
|
|
19
|
+
const INLINE = /(\*\*([^*]+)\*\*|(?<!\w)__([^_]+)__(?!\w)|`([^`]+)`|\[([^\]]+)\]\(([^)\s]+)\)|\*([^*\s][^*]*)\*|(?<!\w)_([^_\s][^_]*)_(?!\w))/g;
|
|
20
|
+
|
|
21
|
+
/** Inline markup to React nodes. Unsafe link targets keep their text and lose the link. */
|
|
22
|
+
export function renderInline(source: string, keyPrefix = "i"): ReactNode[] {
|
|
23
|
+
const out: ReactNode[] = [];
|
|
24
|
+
let last = 0;
|
|
25
|
+
let n = 0;
|
|
26
|
+
for (const m of source.matchAll(INLINE)) {
|
|
27
|
+
const at = m.index ?? 0;
|
|
28
|
+
if (at > last) out.push(source.slice(last, at));
|
|
29
|
+
const key = `${keyPrefix}${n++}`;
|
|
30
|
+
if (m[2] ?? m[3]) out.push(<strong key={key}>{m[2] ?? m[3]}</strong>);
|
|
31
|
+
else if (m[4]) out.push(<code key={key} className="rounded bg-muted px-1 py-0.5 text-[0.9em]">{m[4]}</code>);
|
|
32
|
+
else if (m[5]) {
|
|
33
|
+
const href = safeHttpUrl(m[6]);
|
|
34
|
+
out.push(href ? <a key={key} href={href} target="_blank" rel="noopener noreferrer" className="underline underline-offset-2">{m[5]}</a> : <span key={key}>{m[5]}</span>);
|
|
35
|
+
} else out.push(<em key={key}>{m[7] ?? m[8]}</em>);
|
|
36
|
+
last = at + m[0].length;
|
|
37
|
+
}
|
|
38
|
+
if (last < source.length) out.push(source.slice(last));
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const HEADING = /^(#{1,6})\s+(.*)$/;
|
|
43
|
+
const BULLET = /^\s*[-*+]\s+(.*)$/;
|
|
44
|
+
const ORDERED = /^\s*\d+[.)]\s+(.*)$/;
|
|
45
|
+
const RULE = /^\s*([-*_])(\s*\1){2,}\s*$/;
|
|
46
|
+
const TABLE_ROW = /^\s*\|.*\|\s*$/;
|
|
47
|
+
const TABLE_SEP = /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/;
|
|
48
|
+
|
|
49
|
+
const cells = (line: string): string[] => line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
|
|
50
|
+
|
|
51
|
+
type Block =
|
|
52
|
+
| { kind: "heading"; level: number; text: string }
|
|
53
|
+
| { kind: "para"; text: string }
|
|
54
|
+
| { kind: "ul" | "ol"; items: string[] }
|
|
55
|
+
| { kind: "quote"; text: string }
|
|
56
|
+
| { kind: "code"; text: string }
|
|
57
|
+
| { kind: "rule" }
|
|
58
|
+
| { kind: "table"; head: string[]; rows: string[][] };
|
|
59
|
+
|
|
60
|
+
/** Split markdown into blocks. Pure, so it is tested without rendering. */
|
|
61
|
+
export function parseBlocks(markdown: string): Block[] {
|
|
62
|
+
const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
|
|
63
|
+
const blocks: Block[] = [];
|
|
64
|
+
let i = 0;
|
|
65
|
+
while (i < lines.length) {
|
|
66
|
+
const line = lines[i];
|
|
67
|
+
if (!line.trim()) { i++; continue; }
|
|
68
|
+
if (/^\s*```/.test(line)) {
|
|
69
|
+
const body: string[] = [];
|
|
70
|
+
for (i++; i < lines.length && !/^\s*```/.test(lines[i]); i++) body.push(lines[i]);
|
|
71
|
+
i++;
|
|
72
|
+
blocks.push({ kind: "code", text: body.join("\n") });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const h = HEADING.exec(line);
|
|
76
|
+
if (h) { blocks.push({ kind: "heading", level: h[1].length, text: h[2].trim() }); i++; continue; }
|
|
77
|
+
if (RULE.test(line)) { blocks.push({ kind: "rule" }); i++; continue; }
|
|
78
|
+
if (TABLE_ROW.test(line) && i + 1 < lines.length && TABLE_SEP.test(lines[i + 1])) {
|
|
79
|
+
const rows: string[][] = [];
|
|
80
|
+
let j = i + 2;
|
|
81
|
+
for (; j < lines.length && TABLE_ROW.test(lines[j]); j++) rows.push(cells(lines[j]));
|
|
82
|
+
blocks.push({ kind: "table", head: cells(line), rows });
|
|
83
|
+
i = j;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (BULLET.test(line) || ORDERED.test(line)) {
|
|
87
|
+
const ordered = ORDERED.test(line) && !BULLET.test(line);
|
|
88
|
+
const items: string[] = [];
|
|
89
|
+
for (; i < lines.length; i++) {
|
|
90
|
+
const m = (ordered ? ORDERED : BULLET).exec(lines[i]);
|
|
91
|
+
if (!m) break;
|
|
92
|
+
items.push(m[1]);
|
|
93
|
+
}
|
|
94
|
+
blocks.push({ kind: ordered ? "ol" : "ul", items });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (/^\s*>/.test(line)) {
|
|
98
|
+
const body: string[] = [];
|
|
99
|
+
for (; i < lines.length && /^\s*>/.test(lines[i]); i++) body.push(lines[i].replace(/^\s*>\s?/, ""));
|
|
100
|
+
blocks.push({ kind: "quote", text: body.join(" ") });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const para: string[] = [];
|
|
104
|
+
for (; i < lines.length && lines[i].trim() && !HEADING.test(lines[i]) && !BULLET.test(lines[i]) && !ORDERED.test(lines[i]) && !/^\s*(```|>)/.test(lines[i]); i++) para.push(lines[i].trim());
|
|
105
|
+
blocks.push({ kind: "para", text: para.join(" ") });
|
|
106
|
+
}
|
|
107
|
+
return blocks;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const HEADING_CLASS = ["", "text-lg font-semibold", "text-base font-semibold", "text-sm font-semibold", "text-sm font-medium", "text-sm font-medium", "text-sm font-medium"];
|
|
111
|
+
|
|
112
|
+
function BlockView({ block, k }: { block: Block; k: number }): ReactNode {
|
|
113
|
+
switch (block.kind) {
|
|
114
|
+
case "heading": {
|
|
115
|
+
const Tag = `h${Math.min(block.level + 1, 6)}` as "h2";
|
|
116
|
+
return <Tag className={cn("mt-4 text-foreground first:mt-0", HEADING_CLASS[block.level])}>{renderInline(block.text, `h${k}-`)}</Tag>;
|
|
117
|
+
}
|
|
118
|
+
case "ul":
|
|
119
|
+
case "ol": {
|
|
120
|
+
const Tag = block.kind;
|
|
121
|
+
return <Tag className={cn("my-2 space-y-1 pl-5", block.kind === "ul" ? "list-disc" : "list-decimal")}>{block.items.map((t, n) => <li key={n}>{renderInline(t, `l${k}-${n}-`)}</li>)}</Tag>;
|
|
122
|
+
}
|
|
123
|
+
case "quote": return <blockquote className="my-2 border-l-2 border-border pl-3 text-muted-foreground">{renderInline(block.text, `q${k}-`)}</blockquote>;
|
|
124
|
+
case "code": return <pre className="my-2 overflow-x-auto rounded-md bg-muted p-3 text-xs"><code>{block.text}</code></pre>;
|
|
125
|
+
case "rule": return <hr className="my-3 border-border" />;
|
|
126
|
+
case "table":
|
|
127
|
+
return (
|
|
128
|
+
<div className="my-2 overflow-x-auto">
|
|
129
|
+
<table className="w-full border-collapse text-left text-xs">
|
|
130
|
+
<thead><tr>{block.head.map((c, n) => <th key={n} className="border-b border-border px-2 py-1 font-medium">{renderInline(c, `th${k}-${n}-`)}</th>)}</tr></thead>
|
|
131
|
+
<tbody>{block.rows.map((r, ri) => <tr key={ri}>{r.map((c, n) => <td key={n} className="border-b border-border px-2 py-1 align-top">{renderInline(c, `td${k}-${ri}-${n}-`)}</td>)}</tr>)}</tbody>
|
|
132
|
+
</table>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
default: return <p className="my-2 leading-6">{renderInline(block.text, `p${k}-`)}</p>;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface SafeMarkdownProps {
|
|
140
|
+
markdown: string;
|
|
141
|
+
className?: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function SafeMarkdown({ markdown, className }: SafeMarkdownProps) {
|
|
145
|
+
return (
|
|
146
|
+
<div data-testid="safe-markdown" className={cn("text-sm text-foreground", className)}>
|
|
147
|
+
{parseBlocks(markdown).map((b, n) => <BlockView key={n} block={b} k={n} />)}
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Read the competitors and the news the research found
|
|
3
|
+
* @when Feeding a briefing, a battle-card draft or a Slack digest. Both reads are free. They describe the competitors of YOUR
|
|
4
|
+
* website (the research is locked to it) and brand news about YOUR company, not about arbitrary companies, and the
|
|
5
|
+
* API has no lookup for another company's profile, technology or org chart. Article text and URLs come from the web: treat
|
|
6
|
+
* them as untrusted text and only open http(s) URLs.
|
|
7
|
+
*/
|
|
8
|
+
import { g8 } from "@graph8/sdk";
|
|
9
|
+
|
|
10
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
11
|
+
|
|
12
|
+
type Source = { data?: Record<string, unknown> | null; collected_at?: string };
|
|
13
|
+
const entry = (res: unknown): Source => ((res as { data?: Source } | null)?.data ?? (res as Source) ?? {}) as Source;
|
|
14
|
+
|
|
15
|
+
export interface Competitor { name?: string; domain?: string; description?: string; competitive_type?: string; similarity?: string; confidence?: string; reasoning?: string }
|
|
16
|
+
export interface Article { title?: string; url?: string; description?: string; published_at?: string; sentiment?: number | null; source?: { name?: string } }
|
|
17
|
+
|
|
18
|
+
export async function competitors(websiteUrl: string): Promise<Competitor[]> {
|
|
19
|
+
const res = entry(await g8.api.companyIntelligence.getIntelligenceData({ path: { data_type: "competitor_discovery" }, query: { website_url: websiteUrl } }));
|
|
20
|
+
return (Array.isArray(res.data?.competitors) ? res.data?.competitors : []) as Competitor[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Only http(s) links, newest first. `sentiment` is -1 to 1 or null (unknown, not neutral). */
|
|
24
|
+
export async function recentNews(websiteUrl: string): Promise<Article[]> {
|
|
25
|
+
const res = entry(await g8.api.companyIntelligence.getIntelligenceData({ path: { data_type: "brand_news" }, query: { website_url: websiteUrl } }));
|
|
26
|
+
const articles = (Array.isArray(res.data?.articles) ? res.data?.articles : []) as Article[];
|
|
27
|
+
return articles.filter((a) => /^https?:\/\//i.test(a.url ?? "")).sort((a, b) => String(b.published_at ?? "").localeCompare(String(a.published_at ?? "")));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Everything collected, keyed by source type, in one read. */
|
|
31
|
+
export async function everything(websiteUrl: string) {
|
|
32
|
+
return entry(await g8.api.companyIntelligence.getAllIntelligenceData({ path: { website_url: websiteUrl } })) as Record<string, Source>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Research your own website and read the company profile
|
|
3
|
+
* @when Setting up company intelligence for a workspace. The research is locked to ONE website (the workspace's primary
|
|
4
|
+
* website, set by the first run), so this reads it first and never guesses a domain. Starting a run is BILLABLE and the API
|
|
5
|
+
* quotes no price (it is metered by usage): tell a human what will be collected and get a yes first. A run finished less than an
|
|
6
|
+
* hour ago is returned as is (no new work, no charge) unless `force` is true. Order: primary website -> analyze -> poll the
|
|
7
|
+
* run -> read the company profile.
|
|
8
|
+
*/
|
|
9
|
+
import { g8 } from "@graph8/sdk";
|
|
10
|
+
|
|
11
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
12
|
+
|
|
13
|
+
type Body = Record<string, unknown>;
|
|
14
|
+
const body = (res: unknown): Body => ((res as { data?: Body } | null)?.data ?? (res as Body) ?? {}) as Body;
|
|
15
|
+
|
|
16
|
+
/** The website the research is locked to, or null before the first run. Never invent one. */
|
|
17
|
+
export async function primaryWebsite(): Promise<string | null> {
|
|
18
|
+
const res = body(await g8.api.companyIntelligence.getIntelligencePrimaryWebsite());
|
|
19
|
+
return typeof res.primary_website_url === "string" && res.primary_website_url ? res.primary_website_url : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Billable. Only after a person approved researching `websiteUrl`. With no primary website yet, this one becomes the locked one. */
|
|
23
|
+
export async function startResearch(websiteUrl: string, force = false) {
|
|
24
|
+
const res = body(await g8.api.companyIntelligence.analyzeWebsite({ body: force ? { website_url: websiteUrl, force: true } : { website_url: websiteUrl } }));
|
|
25
|
+
return res.task_id as string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function waitForRun(taskId: string) {
|
|
29
|
+
let delay = 3000;
|
|
30
|
+
for (let i = 0; i < 100; i++) {
|
|
31
|
+
const run = body(await g8.api.companyIntelligence.getIntelligenceStatus({ path: { task_id: taskId } }));
|
|
32
|
+
if (run.status !== "pending" && run.status !== "running") return run;
|
|
33
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
34
|
+
delay = Math.min(15000, Math.round(delay * 1.3));
|
|
35
|
+
}
|
|
36
|
+
throw new Error("the research did not finish in time; read the status again later");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Free. A source that was not collected answers 404: treat that as "not collected", not as an error. */
|
|
40
|
+
export async function companyProfile(websiteUrl: string) {
|
|
41
|
+
const entry = body(await g8.api.companyIntelligence.getIntelligenceData({ path: { data_type: "company_enrichment" }, query: { website_url: websiteUrl } }));
|
|
42
|
+
return (entry.data ?? null) as Record<string, unknown> | null;
|
|
43
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Write the six research reports, follow them and approve one
|
|
3
|
+
* @when Building market research from your website. Writing a report is the most expensive single action in Studio and
|
|
4
|
+
* the API quotes no price: `generateResearchReport` is one long multi-pass run, `generateAllResearchReports` is six,
|
|
5
|
+
* `resumeResearchWorkflow` writes every report that is not finished. Read the workflow first to count them, tell a human the
|
|
6
|
+
* number, and get a yes. Approving a report indexes it into the knowledge base that campaigns and the copilot read.
|
|
7
|
+
* Order: workflow status -> generate (or retry a failed one) -> poll the workflow -> read the report -> approve.
|
|
8
|
+
*/
|
|
9
|
+
import { g8 } from "@graph8/sdk";
|
|
10
|
+
|
|
11
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
12
|
+
|
|
13
|
+
type Body = Record<string, unknown>;
|
|
14
|
+
const body = (res: unknown): Body => ((res as { data?: Body } | null)?.data ?? (res as Body) ?? {}) as Body;
|
|
15
|
+
|
|
16
|
+
export interface Slot { id: string | null; report_number: number; report_type: string; status: string; approved?: boolean }
|
|
17
|
+
|
|
18
|
+
/** Free. Always six slots (`report_1`..`report_6`), including the ones not written yet. */
|
|
19
|
+
export async function workflow(websiteUrl: string) {
|
|
20
|
+
const w = body(await g8.api.research.getResearchWorkflowStatus({ query: { website_url: websiteUrl } }));
|
|
21
|
+
const slots = Object.values((w.reports ?? {}) as Record<string, Slot>);
|
|
22
|
+
return { status: w.workflow_status as string, slots, missing: slots.filter((s) => s.status !== "completed" && s.status !== "approved" && s.status !== "generating") };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Billable. `reportNumber` is 1 to 6. */
|
|
26
|
+
export async function writeReport(websiteUrl: string, reportNumber: number) {
|
|
27
|
+
return g8.api.research.generateResearchReport({ body: { website_url: websiteUrl, report_number: reportNumber } });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Billable. Re-runs ONE failed report; `reportType` is one of the six types in the workflow slots. */
|
|
31
|
+
export async function retryFailed(websiteUrl: string, reportType: "review_sentiment" | "competitive_teardown" | "voice_of_customer" | "gtm_channel" | "analyst_media" | "buyer_psychology") {
|
|
32
|
+
return g8.api.research.retryResearchReport({ body: { website_url: websiteUrl, report_type: reportType } });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function waitForReports(websiteUrl: string) {
|
|
36
|
+
let delay = 5000;
|
|
37
|
+
for (let i = 0; i < 200; i++) {
|
|
38
|
+
const w = await workflow(websiteUrl);
|
|
39
|
+
if (!w.slots.some((s) => s.status === "generating")) return w;
|
|
40
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
41
|
+
delay = Math.min(30000, Math.round(delay * 1.3));
|
|
42
|
+
}
|
|
43
|
+
throw new Error("the reports did not finish in time; read the workflow again later");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Free. The markdown is model output: render it as text, never as HTML. */
|
|
47
|
+
export async function readReport(reportId: string) {
|
|
48
|
+
return body(await g8.api.research.getResearchReport({ path: { report_id: reportId } }));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Free, but it publishes the report to everything that reads the knowledge base. */
|
|
52
|
+
export async function approve(reportId: string) {
|
|
53
|
+
return g8.api.research.approveResearchReport({ path: { report_id: reportId } });
|
|
54
|
+
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { useState } from "react";
|
|
17
17
|
import { Button, Input, Skeleton } from "@graph8/sdk/ui";
|
|
18
18
|
import { ApiError } from "@/g8/components/api-error";
|
|
19
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
19
20
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
20
21
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
21
22
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
@@ -30,6 +31,8 @@ export interface CopilotMemoryProps {
|
|
|
30
31
|
className?: string;
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
/** The memory API has no paging: a full read is announced under the list. */
|
|
35
|
+
const MEMORY_LIMIT = 100;
|
|
33
36
|
const idOf = (m: MemoryRow): string => m.memory_id ?? m.id ?? "";
|
|
34
37
|
|
|
35
38
|
export function CopilotMemory({ scope = "user", className }: CopilotMemoryProps) {
|
|
@@ -37,7 +40,7 @@ export function CopilotMemory({ scope = "user", className }: CopilotMemoryProps)
|
|
|
37
40
|
const [forgetting, setForgetting] = useState<MemoryRow | null>(null);
|
|
38
41
|
const [showTools, setShowTools] = useState(false);
|
|
39
42
|
|
|
40
|
-
const memories = useG8Query((signal) => g8Get("/copilot/memory", { scope, limit:
|
|
43
|
+
const memories = useG8Query((signal) => g8Get("/copilot/memory", { scope, limit: MEMORY_LIMIT }, { signal }), [scope]);
|
|
41
44
|
const tools = useG8Query((signal) => g8Get("/copilot/tools", undefined, { signal }), [], { enabled: showTools });
|
|
42
45
|
const add = useG8Mutation(() => g8Post("/copilot/memory", { content: note.trim(), scope }), { onSuccess: () => { setNote(""); void memories.refetch(); } });
|
|
43
46
|
const forget = useG8Mutation((id: string) => g8Delete(`/copilot/memory/${id}`), { onSuccess: () => { setForgetting(null); void memories.refetch(); } });
|
|
@@ -62,6 +65,7 @@ export function CopilotMemory({ scope = "user", className }: CopilotMemoryProps)
|
|
|
62
65
|
</li>
|
|
63
66
|
))}
|
|
64
67
|
</ul>
|
|
68
|
+
<FirstNotice shown={rows.length} limit={MEMORY_LIMIT} noun="memories" />
|
|
65
69
|
{!memories.loading && !memories.error && rows.length === 0 ? <EmptyState title="Nothing remembered yet" description="Add the rules and facts you want in every answer." /> : null}
|
|
66
70
|
{forget.error ? <ApiError error={forget.error} title="Could not forget it" /> : null}
|
|
67
71
|
|
|
@@ -73,7 +77,7 @@ export function CopilotMemory({ scope = "user", className }: CopilotMemoryProps)
|
|
|
73
77
|
{toolRows.map((t) => (
|
|
74
78
|
<li key={t.name} className="flex items-start justify-between gap-3 p-2 text-sm">
|
|
75
79
|
<span><span className="font-medium">{t.name}</span>{t.description ? <span className="block text-xs text-muted-foreground">{t.description}</span> : null}</span>
|
|
76
|
-
{t.requires_approval || t.needs_approval ? <span className="text-xs text-
|
|
80
|
+
{t.requires_approval || t.needs_approval ? <span className="text-xs text-warning-foreground">needs approval</span> : null}
|
|
77
81
|
</li>
|
|
78
82
|
))}
|
|
79
83
|
</ul>
|
|
@@ -153,7 +153,7 @@ export function CopilotPanel({ context, initialTab = "ask", confirmEveryMessage
|
|
|
153
153
|
<div key={t.id} data-testid={`turn-${t.role}`} className={`max-w-[85%] rounded-lg px-3 py-2 text-sm ${t.role === "user" ? "self-end bg-primary text-primary-foreground" : t.error ? "self-start border border-destructive" : "self-start bg-muted"}`}>
|
|
154
154
|
<p className="whitespace-pre-wrap">{t.text}</p>
|
|
155
155
|
{t.tools?.length ? <p className="mt-1 text-xs opacity-70">Used: {t.tools.join(", ")}</p> : null}
|
|
156
|
-
{t.needsApproval?.length ? <p className="mt-1 text-xs text-
|
|
156
|
+
{t.needsApproval?.length ? <p className="mt-1 text-xs text-warning-foreground">Needs your approval in {brand.linkLabel} before it runs: {t.needsApproval.join(", ")}</p> : null}
|
|
157
157
|
{t.sources ? <p className="mt-1 text-xs opacity-70">{t.sources} sources</p> : null}
|
|
158
158
|
</div>
|
|
159
159
|
))}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { useEffect, useRef, useState } from "react";
|
|
20
20
|
import { Button, Input } 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";
|
|
@@ -43,6 +44,9 @@ interface Page { conversation_id?: string; messages?: WorkMessageRow[] }
|
|
|
43
44
|
|
|
44
45
|
const newKey = (): string => `team-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
45
46
|
|
|
47
|
+
/** The agents roster read has no paging here: a full read is announced under the composer. */
|
|
48
|
+
const AGENT_LIMIT = 50;
|
|
49
|
+
|
|
46
50
|
export function CopilotTeamChat({ channelId, pollMs = 4000, hideChannelPicker, className }: CopilotTeamChatProps) {
|
|
47
51
|
const [channel, setChannel] = useState<string | undefined>(channelId);
|
|
48
52
|
const [text, setText] = useState("");
|
|
@@ -51,7 +55,7 @@ export function CopilotTeamChat({ channelId, pollMs = 4000, hideChannelPicker, c
|
|
|
51
55
|
const draftKey = useRef(newKey());
|
|
52
56
|
|
|
53
57
|
const channels = useG8Query((signal) => g8Get("/work/channels", undefined, { signal }), [], { enabled: !(hideChannelPicker && channelId) });
|
|
54
|
-
const agents = useG8Query((signal) => g8Get("/work/agents", { limit:
|
|
58
|
+
const agents = useG8Query((signal) => g8Get("/work/agents", { limit: AGENT_LIMIT }, { signal }), []);
|
|
55
59
|
const rows = listOf<WorkChannel>(channels.data, "channels");
|
|
56
60
|
const roster = listOf<AgentRow>(agents.data, "items");
|
|
57
61
|
const active = channel ?? rows[0]?.id ?? undefined;
|
|
@@ -103,6 +107,7 @@ export function CopilotTeamChat({ channelId, pollMs = 4000, hideChannelPicker, c
|
|
|
103
107
|
onKeyDown={(e) => { if (e.key === "Enter" && text.trim() && !post.loading) submit(); }} />
|
|
104
108
|
<Button loading={post.loading} disabled={!text.trim()} onClick={submit}>Post</Button>
|
|
105
109
|
</div>
|
|
110
|
+
<FirstNotice shown={roster.length} limit={AGENT_LIMIT} noun="agents" />
|
|
106
111
|
</>
|
|
107
112
|
) : null}
|
|
108
113
|
|
package/blocks/crm/README.md
CHANGED
|
@@ -18,6 +18,23 @@ Three finished screens compose the parts below into complete workspaces. Each ne
|
|
|
18
18
|
| `CompaniesScreen` | Same flow for accounts: filters (name, domain, industry, country), detail with People, Activity, Notes, Tasks, create (domain required), edit, delete, duplicate review. | `renderRowActions(row, { reload })`, `renderDetailActions(company, { reload })`, `extraDetailTabs`, `toolbarActions`, `renderBulkActions`, `onOpenContact` |
|
|
19
19
|
| `TasksScreen` | The workspace-wide task queue: Open / Completed / All, search, priority / type / linked-record filters, tick to complete or reopen, create, edit, delete, bulk complete and delete, overdue marker. | `renderRowActions`, `renderDetailActions`, `toolbarActions`, `defaultRecord`, `onOpenRecord` |
|
|
20
20
|
|
|
21
|
+
### Record actions on contacts
|
|
22
|
+
|
|
23
|
+
`ContactsScreen` also carries the write actions a CRM needs. Each exists three ways, a row (the "..." button), a selection (the bar
|
|
24
|
+
above the table) and the open contact, and each is an exported component you can place yourself.
|
|
25
|
+
|
|
26
|
+
| Action | Component | Notes |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| Assign or remove the owner of a WHOLE LIST | `OwnerAssignListButton` (via `ContactsScreen`'s `renderOwnerPicker`) | Never for a selection: the API assigns owners by filter (`list_id` + `filterModel`) across every match, and has no id-addressed owner write (API gap: `assign-owner` needs an `ids` field). The picker is injected (`renderOwnerPicker={(p) => <MemberPicker label="New owner" {...p} />}`) so crm stays free of the pickers block; without it the action is hidden. Offered only with a list in scope and no other filter; N is the list's real count; assign and remove are both confirmed with the list and N. |
|
|
29
|
+
| Tags | `ContactTagChips` (open contact), `TagAssignButton` (selection), `TagManager` (create, rename, recolour, archive, delete, merge) | Delete states how many contacts lose the tag and can move them to another tag first. Merge removes the source tags. A tag's description cannot be cleared (the API ignores null), so it is not editable. |
|
|
30
|
+
| Edit one field on a selection | `BulkUpdateButton` | Title, department, seniority, city, state, country. Chunks of 100, "N of M updated". Clearing is not offered (no documented clear). |
|
|
31
|
+
| Verify email | `VerifyEmailButton` | Billable, about 1 credit per email. Only contacts with an unlocked email are counted and sent; the result line shows the credits actually charged. Max 1000. |
|
|
32
|
+
| Unlock info | `UnlockInfoButton` | Billable, 1 credit per newly unlocked contact. Only locked contacts are counted and sent. Max 1000 per call. |
|
|
33
|
+
| AI documents | `ContactDocumentsTab` | Write, rewrite, write all, read. Billable with no published price, so the confirm shows none. Rewrite replaces the stored text. |
|
|
34
|
+
| Signals and next best step | `ContactInsightsTab` | Signals load on open. The next best step is a billable GET, so it is a button behind a confirm. |
|
|
35
|
+
|
|
36
|
+
Prerequisites added: the `enrichment:run` scope (verify, unlock, documents, next best step). The owner action needs the `team-members` prefix and `team:read` only in apps that inject a picker (they already include `pickers`).
|
|
37
|
+
|
|
21
38
|
```tsx
|
|
22
39
|
"use client";
|
|
23
40
|
import { ContactsScreen } from "@/components/g8/crm/contacts-screen";
|
|
@@ -148,3 +165,11 @@ Ask the person: who may delete or merge records (decide it in the proxy's `autho
|
|
|
148
165
|
| mergeContact / mergeCompany | POST /contacts/{id}/merge, /companies/{id}/merge | destructive | DedupeReview |
|
|
149
166
|
| listAllTasks / getTask / createGlobalTask | GET /tasks, GET /tasks/{task_id}, POST /tasks | read / read / write | TasksScreen |
|
|
150
167
|
| assertContact, assertCompany (+ batch) | PUT /contacts/assert, /companies/assert | write | recipes (`g8.contacts.upsert`) |
|
|
168
|
+
|
|
169
|
+
## Cross-block glue
|
|
170
|
+
|
|
171
|
+
- `ContactLookupLink` (`components/contact-lookup-link.tsx`): "Open contact" for something that only knows an email (an inbox thread, a meeting attendee). It looks the email up (`GET /contacts?email=`) and shows a button only when a contact has it; `onOpen(contactId)` routes to your contacts screen. `ContactsScreen.onOpenCompany`, `CompaniesScreen.onOpenContact` and `TasksScreen.onOpenRecord` are the other ends of the same links: the stack pages pass them (`router.push("/companies?id=...")`).
|
|
172
|
+
- `ContactLookupLink` can also create: pass `createIfMissing` (optionally with the sender's `firstName`/`lastName`) and a stranger gets "Add to contacts" (`POST /contacts`, then it opens the new contact). Nothing is offered while the lookup loads or after it failed.
|
|
173
|
+
- `RecordOpenLinks` (`components/record-open-links.tsx`): "Open contact" and "Open company" for something that only knows ids (a call log entry, a visitor). The ids are verified with `GET /contacts/{id}` and `GET /companies/{id}` first (optionally against an expected company domain or contact name), so a link never points at a missing or wrong record.
|
|
174
|
+
|
|
175
|
+
- `RecordEngagementTab` (`components/record-engagement-tab.tsx`): engagement counts, last touch (contacts) and pages visited for a contact or company, an `extraDetailTabs` entry: `{ id: "engagement", label: "Engagement", render: (c) => <RecordEngagementTab kind="contacts" id={c.id} /> }`. Reads `getContactEngagementSummary` / `getCompanyEngagementSummary` and `getContactSitesVisited` / `getCompanySitesVisited`. Also: the contact subtitle is `contactSubtitle(contact)` ("VP of Sales at Acme"; the company is never the person's own name, and "at" is left out without a company).
|