@graph8/sdk 0.362.0 → 0.373.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -10
- package/blocks/_foundation/README.md +78 -1
- package/blocks/_foundation/block.json +123 -6
- package/blocks/_foundation/components/api-error.tsx +9 -7
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/confirmed-action.tsx +90 -0
- package/blocks/_foundation/components/connect-graph8.tsx +280 -0
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +2 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-brand.ts +52 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/connect-route.ts +15 -0
- package/blocks/_foundation/server/connect.ts +127 -0
- package/blocks/_foundation/server/local-key.ts +70 -0
- package/blocks/_foundation/server/proxy.ts +103 -3
- package/blocks/_foundation/server/route.ts +16 -4
- package/blocks/agents/README.md +93 -2
- package/blocks/agents/block.json +343 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +29 -10
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +119 -0
- package/blocks/agents/components/agent-runs.tsx +19 -10
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +226 -0
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +222 -0
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +38 -11
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +33 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +4 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +38 -18
- package/blocks/appointments/components/meetings-screen.tsx +7 -4
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/appointments/components/scheduling-screen.tsx +2 -1
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +17 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +17 -6
- package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +163 -0
- package/blocks/campaigns/components/icp-editor.tsx +22 -17
- package/blocks/campaigns/components/persona-detail.tsx +181 -0
- package/blocks/campaigns/components/persona-editor.tsx +19 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
- package/blocks/coach/README.md +10 -0
- package/blocks/coach/block.json +16 -1
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/coach/components/coach-screen.tsx +222 -0
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +161 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +14 -5
- package/blocks/copilot/components/copilot-screen.tsx +148 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
- package/blocks/crm/README.md +34 -0
- package/blocks/crm/block.json +107 -2
- package/blocks/crm/components/companies-screen.tsx +359 -0
- package/blocks/crm/components/company-detail.tsx +10 -8
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +10 -7
- package/blocks/crm/components/contact-lookup-link.tsx +44 -0
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +377 -0
- package/blocks/crm/components/dedupe-review.tsx +13 -6
- package/blocks/crm/components/import-wizard.tsx +7 -9
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +6 -4
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +399 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +9 -1
- package/blocks/crm/lib/crm-types.ts +26 -2
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +24 -6
- package/blocks/deals/block.json +160 -5
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +25 -13
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +31 -10
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +66 -26
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +5 -5
- package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +45 -4
- package/blocks/dialer/block.json +84 -3
- package/blocks/dialer/components/call-contact-button.tsx +269 -0
- package/blocks/dialer/components/call-log-screen.tsx +208 -0
- package/blocks/dialer/components/call-log.tsx +23 -7
- package/blocks/dialer/components/call-queue.tsx +4 -4
- package/blocks/dialer/components/dialer-screen.tsx +361 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +61 -12
- package/blocks/dialer/components/softphone.tsx +4 -2
- package/blocks/enrichment/README.md +29 -7
- package/blocks/enrichment/block.json +49 -9
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
- package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
- package/blocks/enrichment/components/enrich-button.tsx +32 -9
- package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
- package/blocks/enrichment/components/waterfall-config.tsx +66 -17
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +8 -0
- package/blocks/forms/block.json +139 -18
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +4 -2
- package/blocks/forms/components/form-embed.tsx +4 -2
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +56 -19
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +17 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +32 -3
- package/blocks/inbox/block.json +29 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
- package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
- package/blocks/inbox/lib/inbox-types.ts +47 -1
- package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
- package/blocks/integrations/README.md +63 -1
- package/blocks/integrations/block.json +234 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +119 -0
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +13 -8
- package/blocks/integrations/components/integrations-screen.tsx +152 -0
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +11 -9
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +179 -0
- package/blocks/integrations/components/webhook-manager.tsx +43 -25
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +11 -1
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +13 -10
- package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/block.json +13 -8
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +20 -1
- package/blocks/lists/block.json +69 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +20 -9
- package/blocks/lists/components/list-picker.tsx +17 -3
- package/blocks/lists/components/list-table.tsx +12 -6
- package/blocks/lists/components/lists-screen.tsx +346 -0
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +33 -8
- package/blocks/lists/components/suppressions-screen.tsx +264 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +69 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
- package/blocks/mailboxes/components/domain-detail.tsx +148 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
- package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
- package/blocks/mailboxes/components/warmup-health.tsx +24 -22
- package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +157 -24
- package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/block.json +28 -14
- package/blocks/nurture/components/nurture-builder.tsx +13 -3
- package/blocks/nurture/components/nurture-enroll.tsx +12 -10
- package/blocks/nurture/components/nurture-form.tsx +27 -10
- package/blocks/nurture/components/nurture-screen.tsx +26 -11
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +18 -1
- package/blocks/objects/block.json +42 -21
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +10 -2
- package/blocks/objects/components/object-schema-editor.tsx +4 -2
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +140 -23
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +9 -4
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-list.tsx +2 -1
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/quotes/README.md +13 -0
- package/blocks/quotes/block.json +65 -13
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +5 -3
- package/blocks/quotes/components/quote-related.tsx +8 -4
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-report.tsx +4 -2
- package/blocks/radar/components/radar-screen.tsx +214 -0
- package/blocks/radar/components/report-detail.tsx +104 -0
- package/blocks/search/README.md +10 -0
- package/blocks/search/block.json +16 -1
- package/blocks/search/components/filter-builder.tsx +3 -3
- package/blocks/search/components/prospect-results.tsx +5 -3
- package/blocks/search/components/prospect-search-screen.tsx +235 -0
- package/blocks/search/components/saved-searches.tsx +4 -1
- package/blocks/search/hooks/use-filter-options.ts +20 -6
- package/blocks/search/lib/search-types.ts +2 -0
- package/blocks/sequencer/README.md +55 -9
- package/blocks/sequencer/block.json +126 -19
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +224 -60
- package/blocks/sequencer/components/sequence-detail.tsx +261 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
- package/blocks/sequencer/components/sequence-list.tsx +34 -9
- package/blocks/sequencer/components/sequence-stats.tsx +11 -10
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +162 -0
- package/blocks/sequencer/components/step-editor.tsx +14 -12
- package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
- package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
- package/blocks/social-ads/README.md +19 -0
- package/blocks/social-ads/block.json +233 -1
- package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
- package/blocks/social-ads/components/ads-screen.tsx +152 -0
- package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
- package/blocks/social-ads/components/audience-sync.tsx +17 -9
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +138 -0
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +155 -0
- package/blocks/social-ads/components/creative-gallery.tsx +35 -7
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +7 -3
- package/blocks/social-ads/components/post-detail.tsx +163 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +187 -0
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +21 -10
- package/blocks/visitors/components/traffic-chart.tsx +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +26 -16
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +16 -7
- package/blocks/visitors/components/visitors-screen.tsx +166 -0
- package/blocks/webchat/README.md +10 -0
- package/blocks/webchat/block.json +21 -7
- package/blocks/webchat/components/chat-widget-config.tsx +8 -5
- package/blocks/webchat/components/conversation-list.tsx +8 -4
- package/blocks/webchat/components/webchat-screen.tsx +245 -0
- package/blocks/workbench/README.md +11 -0
- package/blocks/workbench/block.json +48 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +10 -4
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +202 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workflows/README.md +13 -0
- package/blocks/workflows/block.json +28 -9
- package/blocks/workflows/components/run-history.tsx +31 -9
- package/blocks/workflows/components/trigger-config.tsx +3 -1
- package/blocks/workflows/components/workflow-editor.tsx +4 -2
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +3109 -1977
- package/dist/errors.d.mts +34 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +21 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.browser.js +11 -5
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +11 -5
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +164 -25
- package/dist/index.d.ts +164 -25
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +10 -4
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +10 -4
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +142 -5
- package/dist/react.d.ts +142 -5
- package/dist/react.js +10 -4
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +10 -4
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/add.d.ts +11 -4
- package/dist/types/blocks/brand.d.ts +122 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +27 -20
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/blocks/paths.d.ts +4 -0
- package/dist/types/blocks/prefixes.d.ts +37 -0
- package/dist/types/blocks/registry.d.ts +17 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +5 -0
- package/dist/types/errors.d.ts +33 -0
- package/dist/types/http.d.ts +2 -18
- package/dist/types/version.d.ts +2 -2
- package/docs/agents.md +15 -4
- package/docs/api/README.md +2 -2
- package/docs/api/by-path.md +1 -0
- package/docs/api/desk.d.ts +98 -2
- package/docs/api/desk.md +123 -1
- package/docs/api/inbox.d.ts +10 -2
- package/docs/api/inbox.md +10 -2
- package/docs/api/seo.d.ts +19 -1
- package/docs/api/seo.md +24 -1
- package/package.json +16 -2
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +55 -17
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +147 -37
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +64 -0
- package/stacks/clay/pages/companies.tsx.tmpl +31 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +73 -27
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +98 -35
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +104 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +181 -64
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +52 -20
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +68 -34
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +106 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +117 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +85 -31
- package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +115 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +40 -9
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/app/layout.tsx.tmpl +7 -2
- package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SourceView: shows what one intelligence source collected, in a form a person can read. No SDK operations: pass it the
|
|
5
|
+
* `data` of a source (`GET /intelligence/data/{data_type}` or one entry of `GET /intelligence/all/{website}`).
|
|
6
|
+
*
|
|
7
|
+
* The payloads are open-shaped, so every field is read defensively: a missing value shows a dash (never 0 and never a
|
|
8
|
+
* guess), web content is drawn as text (never HTML), and links are drawn only for http(s) URLs. A source with a shape
|
|
9
|
+
* this component does not know falls back to a readable tree of its values.
|
|
10
|
+
*
|
|
11
|
+
* Known shapes (from the writers in the app): company_enrichment (flat profile), competitor_discovery ({ competitors }),
|
|
12
|
+
* brand_news ({ articles }), product_inventory ({ products, services, capabilities }), company_keywords / organic_keywords /
|
|
13
|
+
* paid_keywords ({ keywords }), audience_questions ({ questions }), search_trends ({ trends }), top_content ({ pages }),
|
|
14
|
+
* website_scrape ({ pages }), visual_assets ({ logo_url, branding }), brand_sentiment (aggregates).
|
|
15
|
+
*/
|
|
16
|
+
import { useState } from "react";
|
|
17
|
+
import { ExternalLink } from "lucide-react";
|
|
18
|
+
import { Button } from "@graph8/sdk/ui";
|
|
19
|
+
import { KeyValueList, EmptyValue } from "@/g8/components/screen/key-value-list";
|
|
20
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
21
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
22
|
+
import { plural } from "@/g8/lib/plural";
|
|
23
|
+
import { asArray, humanize, isRec, num, safeHttpUrl, text, whenDate } from "../lib/intelligence-types";
|
|
24
|
+
import type { Rec } from "../lib/intelligence-types";
|
|
25
|
+
|
|
26
|
+
const SHOW = 12;
|
|
27
|
+
const HEX = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
28
|
+
|
|
29
|
+
function Link({ url, children }: { url: unknown; children?: React.ReactNode }) {
|
|
30
|
+
const href = safeHttpUrl(url);
|
|
31
|
+
if (!href) return <>{children ?? text(url) ?? <EmptyValue />}</>;
|
|
32
|
+
return (
|
|
33
|
+
<a href={href} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 underline underline-offset-2">
|
|
34
|
+
{children ?? href.replace(/^https?:\/\/(www\.)?/, "")}
|
|
35
|
+
<ExternalLink className="size-3" aria-hidden />
|
|
36
|
+
</a>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A list that shows its first rows and says how many more there are, with a button to see them. */
|
|
41
|
+
function More<T>({ items, noun, render }: { items: T[]; noun: string; render: (item: T, index: number) => React.ReactNode }) {
|
|
42
|
+
const [all, setAll] = useState(false);
|
|
43
|
+
const shown = all ? items : items.slice(0, SHOW);
|
|
44
|
+
return (
|
|
45
|
+
<div className="space-y-2">
|
|
46
|
+
<ul className="divide-y divide-border">{shown.map((item, i) => <li key={i} className="py-2 text-sm">{render(item, i)}</li>)}</ul>
|
|
47
|
+
{items.length > SHOW ? (
|
|
48
|
+
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
49
|
+
<span role="status">Showing {shown.length.toLocaleString()} of {plural(items.length, noun)}</span>
|
|
50
|
+
<Button type="button" variant="outline" size="sm" onClick={() => setAll((v) => !v)}>{all ? "Show fewer" : "Show all"}</Button>
|
|
51
|
+
</div>
|
|
52
|
+
) : null}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const PROFILE_FIELDS: Array<[string, string]> = [
|
|
58
|
+
["company_name", "Company name"], ["industry", "Industry"], ["employee_count", "Employees"], ["revenue", "Revenue"],
|
|
59
|
+
["founded_year", "Founded"], ["funding", "Funding"], ["address", "Headquarters"], ["phone", "Phone"], ["primary_email", "Email"],
|
|
60
|
+
];
|
|
61
|
+
const PROFILE_LINKS: Array<[string, string]> = [
|
|
62
|
+
["linkedin", "LinkedIn"], ["twitter", "X / Twitter"], ["facebook", "Facebook"], ["instagram", "Instagram"], ["youtube", "YouTube"], ["github", "GitHub"],
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
export function CompanyProfileFields({ data }: { data: Rec }) {
|
|
66
|
+
const description = text(data.description);
|
|
67
|
+
return (
|
|
68
|
+
<div className="space-y-3">
|
|
69
|
+
{description ? <p className="text-sm leading-6">{description}</p> : null}
|
|
70
|
+
<KeyValueList columns={2} items={[
|
|
71
|
+
...PROFILE_FIELDS.map(([k, label]) => ({ label, value: text(data[k]) ?? <EmptyValue /> })),
|
|
72
|
+
...PROFILE_LINKS.filter(([k]) => text(data[k])).map(([k, label]) => ({ label, value: <Link url={data[k]} /> })),
|
|
73
|
+
]} />
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function Competitors({ data }: { data: Rec }) {
|
|
79
|
+
const rows = asArray(data.competitors).filter(isRec);
|
|
80
|
+
const summary = text(data.market_summary);
|
|
81
|
+
return (
|
|
82
|
+
<div className="space-y-3">
|
|
83
|
+
{summary ? <p className="text-sm text-muted-foreground">{summary}</p> : null}
|
|
84
|
+
{rows.length === 0 ? <p className="text-sm text-muted-foreground">No competitors in this source.</p> : (
|
|
85
|
+
<More items={rows} noun="competitor" render={(c) => (
|
|
86
|
+
<div className="space-y-1">
|
|
87
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
88
|
+
<span className="font-medium">{text(c.name) ?? text(c.domain) ?? "Unnamed"}</span>
|
|
89
|
+
{text(c.domain) ? <span className="text-xs text-muted-foreground">{text(c.domain)}</span> : null}
|
|
90
|
+
{text(c.competitive_type) ? <StatusPill tone="info">{humanize(text(c.competitive_type) as string)}</StatusPill> : null}
|
|
91
|
+
{text(c.similarity) ? <StatusPill tone="neutral">{`${humanize(text(c.similarity) as string)} similarity`}</StatusPill> : null}
|
|
92
|
+
{text(c.confidence) ? <StatusPill tone="neutral" quiet>{`${humanize(text(c.confidence) as string)} confidence`}</StatusPill> : null}
|
|
93
|
+
</div>
|
|
94
|
+
{text(c.description) ? <p className="text-muted-foreground">{text(c.description)}</p> : null}
|
|
95
|
+
{text(c.reasoning) ? <p className="text-xs text-muted-foreground">{text(c.reasoning)}</p> : null}
|
|
96
|
+
</div>
|
|
97
|
+
)} />
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const sentimentTone = (s: number | null): "positive" | "negative" | "neutral" => (s == null ? "neutral" : s > 0.2 ? "positive" : s < -0.2 ? "negative" : "neutral");
|
|
104
|
+
|
|
105
|
+
function News({ data }: { data: Rec }) {
|
|
106
|
+
const rows = asArray(data.articles).filter(isRec);
|
|
107
|
+
if (rows.length === 0) return <p className="text-sm text-muted-foreground">No articles in this source.</p>;
|
|
108
|
+
return (
|
|
109
|
+
<More items={rows} noun="article" render={(a) => {
|
|
110
|
+
const source = isRec(a.source) ? text(a.source.name) : text(a.source);
|
|
111
|
+
const sentiment = num(a.sentiment);
|
|
112
|
+
return (
|
|
113
|
+
<div className="space-y-0.5">
|
|
114
|
+
<div className="font-medium"><Link url={a.url}>{text(a.title) ?? "Untitled article"}</Link></div>
|
|
115
|
+
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
116
|
+
{source ? <span>{source}</span> : null}
|
|
117
|
+
{whenDate(a.published_at) ? <span>{whenDate(a.published_at)}</span> : null}
|
|
118
|
+
{sentiment != null ? <StatusPill tone={sentimentTone(sentiment)} quiet>{`Sentiment ${sentiment.toFixed(2)}`}</StatusPill> : null}
|
|
119
|
+
</div>
|
|
120
|
+
{text(a.description) ? <p className="text-muted-foreground">{text(a.description)}</p> : null}
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}} />
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function Products({ data }: { data: Rec }) {
|
|
128
|
+
const lists: Array<[string, unknown[]]> = [["Products", asArray(data.products)], ["Services", asArray(data.services)], ["Capabilities", asArray(data.capabilities)]];
|
|
129
|
+
const description = text(data.company_description);
|
|
130
|
+
return (
|
|
131
|
+
<div className="space-y-3">
|
|
132
|
+
{description ? <p className="text-sm leading-6">{description}</p> : null}
|
|
133
|
+
{lists.filter(([, rows]) => rows.length).map(([label, rows]) => (
|
|
134
|
+
<SectionCard key={label} title={label} description={plural(rows.length, label.toLowerCase().replace(/s$/, ""))}>
|
|
135
|
+
<More items={rows} noun={label.toLowerCase().replace(/s$/, "")} render={(r) => isRec(r)
|
|
136
|
+
? <div><span className="font-medium">{text(r.name) ?? "Unnamed"}</span>{text(r.description) ? <p className="text-muted-foreground">{text(r.description)}</p> : null}</div>
|
|
137
|
+
: <span>{text(r) ?? <EmptyValue />}</span>} />
|
|
138
|
+
</SectionCard>
|
|
139
|
+
))}
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Keyword rows use legacy SEMrush codes: Ph = phrase, Nq = monthly searches, Cp = cost per click, Po = position, Ur = url. */
|
|
145
|
+
const KEYWORD_COLUMNS: Array<[string, string]> = [["Nq", "Searches"], ["Po", "Position"], ["Cp", "CPC"], ["Ur", "URL"]];
|
|
146
|
+
|
|
147
|
+
function Keywords({ data }: { data: Rec }) {
|
|
148
|
+
const rows = asArray(data.keywords);
|
|
149
|
+
if (rows.length === 0) return <p className="text-sm text-muted-foreground">No keywords in this source.</p>;
|
|
150
|
+
return (
|
|
151
|
+
<More items={rows} noun="keyword" render={(k) => {
|
|
152
|
+
if (!isRec(k)) return <span>{text(k) ?? <EmptyValue />}</span>;
|
|
153
|
+
const extras = KEYWORD_COLUMNS.filter(([key]) => k[key] != null && k[key] !== "");
|
|
154
|
+
return (
|
|
155
|
+
<div className="flex flex-wrap items-baseline gap-x-3">
|
|
156
|
+
<span className="font-medium">{text(k.Ph) ?? text(k.keyword) ?? text(k.name) ?? "Keyword"}</span>
|
|
157
|
+
{extras.map(([key, label]) => <span key={key} className="text-xs text-muted-foreground">{label} {key === "Ur" ? <Link url={k[key]} /> : text(k[key])}</span>)}
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}} />
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function Questions({ data }: { data: Rec }) {
|
|
165
|
+
const rows = asArray(data.questions);
|
|
166
|
+
if (rows.length === 0) return <p className="text-sm text-muted-foreground">No questions in this source.</p>;
|
|
167
|
+
return <More items={rows} noun="question" render={(q) => <span>{isRec(q) ? text(q.question) ?? text(q.text) ?? <EmptyValue /> : text(q) ?? <EmptyValue />}</span>} />;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function Trends({ data }: { data: Rec }) {
|
|
171
|
+
const rows = asArray(data.trends);
|
|
172
|
+
if (rows.length === 0) return <p className="text-sm text-muted-foreground">No trends in this source.</p>;
|
|
173
|
+
return <More items={rows} noun="trend" render={(t) => (isRec(t) ? <GenericRow row={t} lead="keyword" /> : <span>{text(t) ?? <EmptyValue />}</span>)} />;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function Pages({ data, withText }: { data: Rec; withText?: boolean }) {
|
|
177
|
+
const rows = asArray(data.pages).filter(isRec);
|
|
178
|
+
const [open, setOpen] = useState<number | null>(null);
|
|
179
|
+
if (rows.length === 0) return <p className="text-sm text-muted-foreground">No pages in this source.</p>;
|
|
180
|
+
return (
|
|
181
|
+
<More items={rows} noun="page" render={(p, i) => {
|
|
182
|
+
const body = text(p.markdown) ?? text(p.content);
|
|
183
|
+
return (
|
|
184
|
+
<div className="space-y-1">
|
|
185
|
+
<div className="flex items-center justify-between gap-2">
|
|
186
|
+
<Link url={p.url ?? p.Ur}>{text(p.title) ?? undefined}</Link>
|
|
187
|
+
{withText && body ? <Button type="button" variant="ghost" size="sm" onClick={() => setOpen(open === i ? null : i)}>{open === i ? "Hide text" : `Read text (${plural(body.length, "character")})`}</Button> : null}
|
|
188
|
+
</div>
|
|
189
|
+
{withText && open === i && body ? <pre className="max-h-64 overflow-auto whitespace-pre-wrap rounded-md bg-muted p-2 text-xs">{body.slice(0, 20_000)}</pre> : null}
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
}} />
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function Visual({ data }: { data: Rec }) {
|
|
197
|
+
const logo = safeHttpUrl(data.logo_url);
|
|
198
|
+
const branding = isRec(data.branding) ? data.branding : {};
|
|
199
|
+
const colors = isRec(branding.colors) ? Object.entries(branding.colors).filter(([, v]) => typeof v === "string" && HEX.test(v)) : [];
|
|
200
|
+
const fonts = asArray(branding.fonts).map((f) => (isRec(f) ? text(f.family) ?? text(f.name) : text(f))).filter(Boolean) as string[];
|
|
201
|
+
return (
|
|
202
|
+
<div className="space-y-3">
|
|
203
|
+
{logo ? <img src={logo} alt="Company logo" className="h-12 max-w-48 object-contain" /> : null}
|
|
204
|
+
{colors.length ? (
|
|
205
|
+
<ul className="flex flex-wrap gap-3">
|
|
206
|
+
{colors.map(([name, value]) => (
|
|
207
|
+
<li key={name} className="flex items-center gap-2 text-xs">
|
|
208
|
+
<span aria-hidden className="size-6 rounded border border-border" style={{ backgroundColor: value as string }} />
|
|
209
|
+
<span>{humanize(name)} <span className="text-muted-foreground">{value as string}</span></span>
|
|
210
|
+
</li>
|
|
211
|
+
))}
|
|
212
|
+
</ul>
|
|
213
|
+
) : null}
|
|
214
|
+
{fonts.length ? <p className="text-sm">Fonts: {fonts.join(", ")}</p> : null}
|
|
215
|
+
{!logo && !colors.length && !fonts.length ? <p className="text-sm text-muted-foreground">No brand assets in this source.</p> : null}
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function GenericRow({ row, lead }: { row: Rec; lead?: string }) {
|
|
221
|
+
const entries = Object.entries(row).filter(([, v]) => typeof v === "string" || typeof v === "number" || typeof v === "boolean");
|
|
222
|
+
const first = lead ? entries.find(([k]) => k === lead) : undefined;
|
|
223
|
+
return (
|
|
224
|
+
<div className="flex flex-wrap items-baseline gap-x-3">
|
|
225
|
+
{first ? <span className="font-medium">{String(first[1])}</span> : null}
|
|
226
|
+
{entries.filter(([k]) => k !== lead).slice(0, 8).map(([k, v]) => <span key={k} className="text-xs text-muted-foreground">{humanize(k)} {String(v)}</span>)}
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Fallback: scalars as key/value pairs, nested objects and lists one level down. Depth-limited so a huge blob stays readable. */
|
|
232
|
+
export function GenericData({ value, depth = 0 }: { value: unknown; depth?: number }) {
|
|
233
|
+
if (Array.isArray(value)) {
|
|
234
|
+
if (value.length === 0) return <p className="text-sm text-muted-foreground">Nothing here.</p>;
|
|
235
|
+
return <More items={value} noun="item" render={(v) => (isRec(v) && depth < 2 ? <GenericRow row={v} /> : <span>{typeof v === "object" ? "" : text(v) ?? ""}</span>)} />;
|
|
236
|
+
}
|
|
237
|
+
if (!isRec(value)) return <span className="text-sm">{text(value) ?? <EmptyValue />}</span>;
|
|
238
|
+
const scalars = Object.entries(value).filter(([, v]) => ["string", "number", "boolean"].includes(typeof v));
|
|
239
|
+
const nested = Object.entries(value).filter(([, v]) => v != null && typeof v === "object");
|
|
240
|
+
return (
|
|
241
|
+
<div className="space-y-3">
|
|
242
|
+
{scalars.length ? <KeyValueList columns={2} items={scalars.map(([k, v]) => ({ label: humanize(k), value: String(v) }))} /> : null}
|
|
243
|
+
{depth < 2 ? nested.map(([k, v]) => <SectionCard key={k} title={humanize(k)}><GenericData value={v} depth={depth + 1} /></SectionCard>) : null}
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export interface SourceViewProps {
|
|
249
|
+
/** The source's type key, for example `company_enrichment`. */
|
|
250
|
+
dataType: string;
|
|
251
|
+
/** The `data` blob of the source. */
|
|
252
|
+
data: unknown;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function SourceView({ dataType, data }: SourceViewProps) {
|
|
256
|
+
if (data == null) return <p className="text-sm text-muted-foreground">This source has no data yet.</p>;
|
|
257
|
+
const rec = isRec(data) ? data : null;
|
|
258
|
+
if (!rec) return <GenericData value={data} />;
|
|
259
|
+
switch (dataType) {
|
|
260
|
+
case "company_enrichment": return <CompanyProfileFields data={rec} />;
|
|
261
|
+
case "competitor_discovery": return <Competitors data={rec} />;
|
|
262
|
+
case "brand_news": return <News data={rec} />;
|
|
263
|
+
case "product_inventory": return <Products data={rec} />;
|
|
264
|
+
case "company_keywords":
|
|
265
|
+
case "organic_keywords":
|
|
266
|
+
case "paid_keywords": return <Keywords data={rec} />;
|
|
267
|
+
case "audience_questions": return <Questions data={rec} />;
|
|
268
|
+
case "search_trends": return <Trends data={rec} />;
|
|
269
|
+
case "top_content": return <Pages data={rec} />;
|
|
270
|
+
case "website_scrape": return <Pages data={rec} withText />;
|
|
271
|
+
case "visual_assets": return <Visual data={rec} />;
|
|
272
|
+
default: return <GenericData value={rec} />;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WikiDocumentDetail: the complete screen for one wiki document. Finds it by id (so `?id=` deep links work), shows its
|
|
5
|
+
* content as safe formatted text (markdown from the API is never turned into HTML), its state and version, and can write
|
|
6
|
+
* it again from the collected research.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. getGlobalContextTreeFlat GET /global-context/tree/flat (read)
|
|
10
|
+
* 2. getIntelligencePrimaryWebsite GET /intelligence/primary-website (read, through useResearchTarget)
|
|
11
|
+
* 3. generateGlobalContextDocument POST /global-context/generate/{doc_type} (BILLABLE) a model rewrites this ONE document; ConfirmSpendDialog first
|
|
12
|
+
* After the call is accepted the document is read again until its update time moves. The confirm shows what this document
|
|
13
|
+
* used the last time it was written (a real number from the document), when the API reports one; there is no advance price.
|
|
14
|
+
*/
|
|
15
|
+
import { useRef, useState, type ReactNode } from "react";
|
|
16
|
+
import { RefreshCw } from "lucide-react";
|
|
17
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
18
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
19
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
20
|
+
import { useConfirmAction } from "@/g8/components/confirmed-action";
|
|
21
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
22
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
23
|
+
import { KeyValueList, EmptyValue } from "@/g8/components/screen/key-value-list";
|
|
24
|
+
import { StatusPill, toneForStatus } from "@/g8/components/screen/status-pill";
|
|
25
|
+
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
26
|
+
import { g8Post } from "@/g8/lib/g8-client";
|
|
27
|
+
import { refreshIntelligence, useResearchTarget, useWikiDocuments } from "../hooks/use-intelligence";
|
|
28
|
+
import { docTitle, humanize, num, text, when } from "../lib/intelligence-types";
|
|
29
|
+
import { SafeMarkdown } from "../lib/safe-markdown";
|
|
30
|
+
|
|
31
|
+
export interface WikiDocumentDetailProps {
|
|
32
|
+
docId: string;
|
|
33
|
+
onClose?: () => void;
|
|
34
|
+
onExpand?: () => void;
|
|
35
|
+
onCollapse?: () => void;
|
|
36
|
+
onChanged?: () => void;
|
|
37
|
+
extraActions?: ReactNode;
|
|
38
|
+
extraTabs?: { id: string; label: string; content: ReactNode }[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function WikiDocumentDetail({ docId, onClose, onExpand, onCollapse, onChanged, extraActions, extraTabs }: WikiDocumentDetailProps) {
|
|
42
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
43
|
+
const target = useResearchTarget();
|
|
44
|
+
const { docs, error, refetch } = useWikiDocuments();
|
|
45
|
+
const [waiting, setWaiting] = useState(false);
|
|
46
|
+
const before = useRef<string | null>(null);
|
|
47
|
+
const doc = docs?.find((d) => d.id === docId);
|
|
48
|
+
|
|
49
|
+
useJobPoll<string | null>(async () => { const rows = await refetch(); return text(rows?.find((d) => d.id === docId)?.updated_at); }, {
|
|
50
|
+
enabled: waiting, intervalMs: 4000, maxAttempts: 40,
|
|
51
|
+
isTerminal: (updated) => updated != null && updated !== before.current,
|
|
52
|
+
onDone: () => { setWaiting(false); void refreshIntelligence(); onChanged?.(); },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (error) return <ApiError error={error} title="Could not load the document" onRetry={() => void refetch()} />;
|
|
56
|
+
if (!docs) return <Skeleton className="h-40 w-full" />;
|
|
57
|
+
if (!doc) return <EmptyState title="Document not found" description="It may have been deleted." />;
|
|
58
|
+
|
|
59
|
+
const title = docTitle(doc);
|
|
60
|
+
const lastCredits = num(doc.credits_used);
|
|
61
|
+
const canRegenerate = Boolean(doc.file_type && target.primary);
|
|
62
|
+
const askRegenerate = () => ask({
|
|
63
|
+
tier: "billable",
|
|
64
|
+
title: `Rewrite ${title}`,
|
|
65
|
+
confirmLabel: "Rewrite",
|
|
66
|
+
estimatedCredits: lastCredits != null && lastCredits > 0 ? lastCredits : undefined,
|
|
67
|
+
description: <p>A model writes this one document again from the collected research and replaces version {doc.version ?? "the current one"}.{lastCredits != null && lastCredits > 0 ? " The credits shown are what it used the last time." : " The API gives no price in advance."}</p>,
|
|
68
|
+
run: async () => {
|
|
69
|
+
before.current = text(doc.updated_at);
|
|
70
|
+
await g8Post(`/global-context/generate/${encodeURIComponent(doc.file_type as string)}`, { doc_type: doc.file_type, website_url: target.primary });
|
|
71
|
+
setWaiting(true);
|
|
72
|
+
},
|
|
73
|
+
onDone: () => { void refreshIntelligence(); },
|
|
74
|
+
});
|
|
75
|
+
const actions = (
|
|
76
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
77
|
+
<Button type="button" size="sm" variant="outline" disabled={!canRegenerate || waiting} onClick={askRegenerate} title={canRegenerate ? undefined : "Start the website research first"}>
|
|
78
|
+
<RefreshCw className="size-3.5" aria-hidden />{waiting ? "Writing..." : "Rewrite"}
|
|
79
|
+
</Button>
|
|
80
|
+
{extraActions}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
const content = text(doc.content);
|
|
84
|
+
const tabs = [
|
|
85
|
+
{ id: "document", label: "Document", content: content ? <SafeMarkdown markdown={content} /> : <EmptyState title="No content yet" description="This document has not been written." compact /> },
|
|
86
|
+
{
|
|
87
|
+
id: "details", label: "Details",
|
|
88
|
+
content: (
|
|
89
|
+
<KeyValueList columns={2} items={[
|
|
90
|
+
{ label: "Type", value: text(doc.file_type) ?? <EmptyValue />, mono: true },
|
|
91
|
+
{ label: "Folder", value: text(doc.folder_path) ?? <EmptyValue /> },
|
|
92
|
+
{ label: "Category", value: text(doc.category) ?? <EmptyValue /> },
|
|
93
|
+
{ label: "Version", value: doc.version ?? <EmptyValue /> },
|
|
94
|
+
{ label: "Last updated", value: when(doc.updated_at) ?? <EmptyValue /> },
|
|
95
|
+
{ label: "Credits used last time", value: lastCredits != null && lastCredits > 0 ? lastCredits : <EmptyValue /> },
|
|
96
|
+
]} />
|
|
97
|
+
),
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
return (
|
|
101
|
+
<>
|
|
102
|
+
<DetailPanel
|
|
103
|
+
title={title}
|
|
104
|
+
subtitle={text(doc.folder_path) ?? undefined}
|
|
105
|
+
badges={<StatusPill tone={toneForStatus(doc.status ?? "")}>{doc.status ? humanize(doc.status) : "Unknown"}</StatusPill>}
|
|
106
|
+
actions={actions}
|
|
107
|
+
tabs={extraTabs?.length ? [...tabs, ...extraTabs] : tabs}
|
|
108
|
+
onClose={onClose}
|
|
109
|
+
onExpand={onExpand}
|
|
110
|
+
onCollapse={onCollapse}
|
|
111
|
+
/>
|
|
112
|
+
{failure ? <ApiError error={failure} title="That did not work" /> : null}
|
|
113
|
+
<ConfirmSpendDialog {...dialog} />
|
|
114
|
+
</>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WikiDocuments: the wiki documents graph8 writes from the research (brand, audience, market, messaging ...), listed by
|
|
5
|
+
* folder with their state, version and last update. Search and a folder filter narrow the list. Click a row to open it.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. getGlobalContextTreeFlat GET /global-context/tree/flat (read) every wiki document, content included
|
|
9
|
+
* The API does not page this list (a few dozen documents); the header says how many there are. Folder and search filter on
|
|
10
|
+
* screen. Opening and regenerating a document is WikiDocumentDetail.
|
|
11
|
+
*/
|
|
12
|
+
import { useMemo, useState } from "react";
|
|
13
|
+
import { FileText } from "lucide-react";
|
|
14
|
+
import { Skeleton } from "@graph8/sdk/ui";
|
|
15
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
16
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
17
|
+
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
18
|
+
import { SelectFilter } from "@/g8/components/screen/filter-controls";
|
|
19
|
+
import { StatusPill, toneForStatus } from "@/g8/components/screen/status-pill";
|
|
20
|
+
import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
21
|
+
import { plural } from "@/g8/lib/plural";
|
|
22
|
+
import { useWikiDocuments } from "../hooks/use-intelligence";
|
|
23
|
+
import { docTitle, humanize, text, whenDate } from "../lib/intelligence-types";
|
|
24
|
+
import type { WikiDoc } from "../lib/intelligence-types";
|
|
25
|
+
|
|
26
|
+
export interface WikiDocumentsProps {
|
|
27
|
+
onSelect?: (doc: WikiDoc) => void;
|
|
28
|
+
selectedId?: string;
|
|
29
|
+
/** The URL param the owning screen reads for the open document, so Cmd-click opens it in a new tab. */
|
|
30
|
+
idParam?: string;
|
|
31
|
+
className?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function WikiDocuments({ onSelect, selectedId, idParam, className }: WikiDocumentsProps) {
|
|
35
|
+
const { docs, error, refetch } = useWikiDocuments();
|
|
36
|
+
const [search, setSearch] = useState("");
|
|
37
|
+
const [folder, setFolder] = useState("");
|
|
38
|
+
const folders = useMemo(() => [...new Set((docs ?? []).map((d) => text(d.folder_path)).filter(Boolean) as string[])].sort(), [docs]);
|
|
39
|
+
if (error) return <ApiError error={error} title="Could not load the documents" onRetry={() => void refetch()} />;
|
|
40
|
+
if (!docs) return <Skeleton className="h-40 w-full" />;
|
|
41
|
+
if (docs.length === 0) return <EmptyState title="No documents yet" description="The wiki documents are written from the research. Run the website research first." icon={<FileText />} />;
|
|
42
|
+
const needle = search.trim().toLowerCase();
|
|
43
|
+
const rows = docs.filter((d) => (!folder || d.folder_path === folder) && (!needle || docTitle(d).toLowerCase().includes(needle)));
|
|
44
|
+
return (
|
|
45
|
+
<div className={className}>
|
|
46
|
+
<Toolbar
|
|
47
|
+
search={{ value: search, onChange: setSearch, placeholder: "Search documents", label: "Search documents" }}
|
|
48
|
+
filters={folders.length > 1 ? <SelectFilter label="folder" value={folder} onChange={setFolder} any="All folders" options={folders} /> : undefined}
|
|
49
|
+
status={rows.length === docs.length ? `All ${plural(docs.length, "document")}` : `${rows.length.toLocaleString()} of ${plural(docs.length, "document")}`}
|
|
50
|
+
/>
|
|
51
|
+
{rows.length === 0 ? <EmptyState title="No documents match" description="Try another search or folder." compact /> : (
|
|
52
|
+
<ul className="mt-2 divide-y divide-border rounded-md border border-border">
|
|
53
|
+
{rows.map((d) => (
|
|
54
|
+
<li key={d.id} aria-current={d.id === selectedId ? "true" : undefined} className="flex items-center justify-between gap-3 px-3 py-2 text-sm aria-[current=true]:bg-muted">
|
|
55
|
+
<div className="min-w-0">
|
|
56
|
+
<RecordLink id={d.id} param={idParam} onOpen={() => onSelect?.(d)}>{docTitle(d)}</RecordLink>
|
|
57
|
+
<div className="text-xs text-muted-foreground">{[text(d.folder_path), d.version != null ? `Version ${d.version}` : null, whenDate(d.updated_at) ? `Updated ${whenDate(d.updated_at)}` : null].filter(Boolean).join(", ")}</div>
|
|
58
|
+
</div>
|
|
59
|
+
<StatusPill tone={toneForStatus(d.status ?? "")} quiet>{d.status ? humanize(d.status) : "Unknown"}</StatusPill>
|
|
60
|
+
</li>
|
|
61
|
+
))}
|
|
62
|
+
</ul>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared reads for the company-intelligence components, all cached under the `company-intelligence` key prefix so one
|
|
5
|
+
* `refreshIntelligence()` after any write refreshes every panel that shows the result.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. getIntelligencePrimaryWebsite GET /intelligence/primary-website (read) the website the research is locked to
|
|
9
|
+
* 2. getIntelligenceOrgDomain GET /intelligence/org-domain (read) a suggestion for it, read only while none is set
|
|
10
|
+
* 3. getLatestIntelligenceTask GET /intelligence/latest-task (read) the most recent research run
|
|
11
|
+
* 4. getAllIntelligenceData GET /intelligence/all/{website_url} (read) every source collected for one website
|
|
12
|
+
* 5. getIntelligenceStatus GET /intelligence/status/{task_id} (read) the step states of one run
|
|
13
|
+
* 6. listIntelligenceDocuments GET /intelligence/documents (read) every collected source
|
|
14
|
+
* 7. getGlobalContextTreeFlat GET /global-context/tree/flat (read) the wiki documents
|
|
15
|
+
* 8. getGlobalContextTree GET /global-context/tree (read) how many wiki documents exist and are finished
|
|
16
|
+
* 9. getResearchWorkflowStatus GET /research-reports/workflow (read) the six research reports and how far they are
|
|
17
|
+
* 10. getIntelligenceData GET /intelligence/data/{data_type} (read) one collected source
|
|
18
|
+
*/
|
|
19
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
20
|
+
import { invalidate } from "@/g8/lib/g8-cache";
|
|
21
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
22
|
+
import { canonicalDomain, intelBody, isRec, num, rowsOf, text } from "../lib/intelligence-types";
|
|
23
|
+
import type { IntelDocument, LatestTask, Rec, RunStatus, WikiDoc, WorkflowStatus } from "../lib/intelligence-types";
|
|
24
|
+
|
|
25
|
+
export const INTEL_KEY = "company-intelligence";
|
|
26
|
+
|
|
27
|
+
/** Mark every intelligence read stale after a write, so the panels on screen show the new state. */
|
|
28
|
+
export function refreshIntelligence(): Promise<void> {
|
|
29
|
+
return invalidate([INTEL_KEY]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The website intelligence is about. `primary` is the workspace's own website (set by the first research run, then
|
|
34
|
+
* locked); `suggested` is the company domain in the workspace settings, offered only while no primary website exists.
|
|
35
|
+
* Neither is ever invented: with both null the person types the website.
|
|
36
|
+
*/
|
|
37
|
+
export function useResearchTarget() {
|
|
38
|
+
const primary = useG8Query(
|
|
39
|
+
async (signal) => {
|
|
40
|
+
const body = intelBody<unknown>(await g8Get("/intelligence/primary-website", undefined, { signal }));
|
|
41
|
+
return isRec(body) ? text(body.primary_website_url) : null;
|
|
42
|
+
},
|
|
43
|
+
[],
|
|
44
|
+
{ key: [INTEL_KEY, "primary-website"] },
|
|
45
|
+
);
|
|
46
|
+
const wantSuggestion = primary.data === null;
|
|
47
|
+
const suggestion = useG8Query(
|
|
48
|
+
async (signal) => {
|
|
49
|
+
const body = intelBody<unknown>(await g8Get("/intelligence/org-domain", undefined, { signal }));
|
|
50
|
+
return isRec(body) ? text(body.company_domain) : null;
|
|
51
|
+
},
|
|
52
|
+
[],
|
|
53
|
+
{ key: [INTEL_KEY, "org-domain"], enabled: wantSuggestion },
|
|
54
|
+
);
|
|
55
|
+
return {
|
|
56
|
+
primary: primary.data ?? null,
|
|
57
|
+
primaryDomain: primary.data ? canonicalDomain(primary.data) : null,
|
|
58
|
+
suggested: wantSuggestion ? suggestion.data ?? null : null,
|
|
59
|
+
loading: primary.loading && primary.data === undefined,
|
|
60
|
+
error: primary.error,
|
|
61
|
+
refetch: primary.refetch,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The most recent research run of the workspace, or null when none ran yet. */
|
|
66
|
+
export function useLatestRun() {
|
|
67
|
+
const q = useG8Query(
|
|
68
|
+
async (signal) => {
|
|
69
|
+
const body = intelBody<unknown>(await g8Get("/intelligence/latest-task", undefined, { signal }));
|
|
70
|
+
return isRec(body) && text(body.task_id) ? (body as LatestTask) : null;
|
|
71
|
+
},
|
|
72
|
+
[],
|
|
73
|
+
{ key: [INTEL_KEY, "latest-run"] },
|
|
74
|
+
);
|
|
75
|
+
return { run: q.data ?? null, loading: q.loading && q.data === undefined, error: q.error, refetch: q.refetch };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Everything collected for one website, keyed by source type (`GET /intelligence/all/{website_url}`, read).
|
|
80
|
+
* `host` is the website as a host (no scheme); pass null to hold the request.
|
|
81
|
+
*/
|
|
82
|
+
export function useAllIntelligence(host: string | null) {
|
|
83
|
+
const q = useG8Query(
|
|
84
|
+
async (signal) => {
|
|
85
|
+
const body = intelBody<unknown>(await g8Get(`/intelligence/all/${encodeURIComponent(host as string)}`, undefined, { signal }));
|
|
86
|
+
const out: Record<string, Rec> = {};
|
|
87
|
+
if (isRec(body)) for (const [type, entry] of Object.entries(body)) if (isRec(entry)) out[type] = entry;
|
|
88
|
+
return out;
|
|
89
|
+
},
|
|
90
|
+
[host],
|
|
91
|
+
{ key: [INTEL_KEY, "all", host ?? "none"], enabled: Boolean(host) },
|
|
92
|
+
);
|
|
93
|
+
return { entries: q.data ?? null, loading: q.loading && q.data === undefined, error: q.error, refetch: q.refetch };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The `data` blob of one source entry, or null. */
|
|
97
|
+
export function entryData(entry: Rec | undefined | null): Rec | null {
|
|
98
|
+
return entry && isRec(entry.data) ? entry.data : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** One read of a run's step states (`GET /intelligence/status/{task_id}`, read). For the live view use ResearchRun, which polls. */
|
|
102
|
+
export function useRunStatus(taskId: string | null | undefined) {
|
|
103
|
+
const q = useG8Query(
|
|
104
|
+
async (signal) => intelBody<RunStatus>(await g8Get(`/intelligence/status/${encodeURIComponent(taskId as string)}`, undefined, { signal })),
|
|
105
|
+
[taskId],
|
|
106
|
+
{ key: [INTEL_KEY, "status", taskId ?? "none"], enabled: Boolean(taskId) },
|
|
107
|
+
);
|
|
108
|
+
return { status: q.data ?? null, loading: q.loading && q.data === undefined, error: q.error, refetch: q.refetch };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Every collected source of the workspace (`GET /intelligence/documents`, read), newest first as the API returns them. */
|
|
112
|
+
export function useSourceDocuments() {
|
|
113
|
+
const q = useG8Query(
|
|
114
|
+
async (signal) => rowsOf<IntelDocument>(intelBody(await g8Get("/intelligence/documents", undefined, { signal })), "documents"),
|
|
115
|
+
[],
|
|
116
|
+
{ key: [INTEL_KEY, "documents"] },
|
|
117
|
+
);
|
|
118
|
+
return { docs: q.data ?? null, loading: q.loading && q.data === undefined, error: q.error, refetch: q.refetch };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The wiki documents graph8 writes from the research (brand, audience, market, messaging ...), content included
|
|
123
|
+
* (`GET /global-context/tree/flat`, read). The API does not page this list and it holds a few dozen documents.
|
|
124
|
+
*/
|
|
125
|
+
export function useWikiDocuments() {
|
|
126
|
+
const q = useG8Query(
|
|
127
|
+
async (signal) => rowsOf<WikiDoc>(intelBody(await g8Get("/global-context/tree/flat", undefined, { signal })), "documents"),
|
|
128
|
+
[],
|
|
129
|
+
{ key: [INTEL_KEY, "wiki", "flat"] },
|
|
130
|
+
);
|
|
131
|
+
return { docs: q.data ?? null, loading: q.loading && q.data === undefined, error: q.error, refetch: q.refetch };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** How many wiki documents exist and how many are finished (`GET /global-context/tree`, read). Null when the API did not say. */
|
|
135
|
+
export function useWikiStats() {
|
|
136
|
+
const q = useG8Query(
|
|
137
|
+
async (signal) => {
|
|
138
|
+
const body = intelBody<unknown>(await g8Get("/global-context/tree", undefined, { signal }));
|
|
139
|
+
const stats = isRec(body) && isRec(body.stats) ? body.stats : null;
|
|
140
|
+
const total = num(stats?.total);
|
|
141
|
+
const completed = num(stats?.completed);
|
|
142
|
+
return total != null && completed != null ? { total, completed } : null;
|
|
143
|
+
},
|
|
144
|
+
[],
|
|
145
|
+
{ key: [INTEL_KEY, "wiki", "tree"] },
|
|
146
|
+
);
|
|
147
|
+
return { stats: q.data ?? null, loading: q.loading && q.data === undefined, error: q.error, refetch: q.refetch };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* How the six-report research workflow is going for a website (`GET /research-reports/workflow`, read). It always lists
|
|
152
|
+
* all six slots, including the ones not generated yet. `host` null holds the request.
|
|
153
|
+
*/
|
|
154
|
+
export function useWorkflow(host: string | null) {
|
|
155
|
+
const q = useG8Query(
|
|
156
|
+
async (signal) => intelBody<WorkflowStatus>(await g8Get("/research-reports/workflow", { website_url: host }, { signal })),
|
|
157
|
+
[host],
|
|
158
|
+
{ key: [INTEL_KEY, "workflow", host ?? "none"], enabled: Boolean(host) },
|
|
159
|
+
);
|
|
160
|
+
return { workflow: q.data ?? null, loading: q.loading && q.data === undefined, error: q.error, refetch: q.refetch };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* One collected source for a website (`GET /intelligence/data/{data_type}`, read): the entry with its `data` blob,
|
|
165
|
+
* collection time and any edits. A 404 means the source was not collected (the query errors with status 404).
|
|
166
|
+
*/
|
|
167
|
+
export function useSourceEntry(dataType: string, host: string | null, enabled = true) {
|
|
168
|
+
return useG8Query(
|
|
169
|
+
async (signal) => intelBody<Rec>(await g8Get(`/intelligence/data/${encodeURIComponent(dataType)}`, host ? { website_url: host } : undefined, { signal })),
|
|
170
|
+
[dataType, host],
|
|
171
|
+
{ key: [INTEL_KEY, "source", dataType, host ?? "any"], enabled },
|
|
172
|
+
);
|
|
173
|
+
}
|