@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,189 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ReportDetail: the complete screen for one research report. Reads it by id (so `?id=` deep links work), shows the
|
|
5
|
+
* markdown as safe formatted text (never HTML), lets a person edit the text, approve it into the knowledge base, see the
|
|
6
|
+
* model's recorded reasoning, write it again, or delete it.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. getResearchReport GET /research-reports/{report_id} (read)
|
|
10
|
+
* 2. getResearchReportThinking GET /research-reports/{report_id}/thinking (read) the recorded reasoning, loaded when its tab opens
|
|
11
|
+
* 3. updateResearchReport PATCH /research-reports/{report_id} (BILLABLE tier: it re-indexes the text into knowledge) ConfirmSpendDialog first
|
|
12
|
+
* 4. approveResearchReport POST /research-reports/{report_id}/approve (write) indexes the report into the knowledge base
|
|
13
|
+
* 5. unapproveResearchReport POST /research-reports/{report_id}/unapprove (write) removes it from the index; the report stays
|
|
14
|
+
* 6. generateResearchReport POST /research-reports/generate (BILLABLE) writes this report again; ConfirmSpendDialog first
|
|
15
|
+
* 7. deleteResearchReport DELETE /research-reports/{report_id} (DESTRUCTIVE) ConfirmSpendDialog first
|
|
16
|
+
* Editing sends only the text, and only when it changed; clearing it sends "" (the API ignores null). The edit form opens only
|
|
17
|
+
* once the report has loaded, and a save refreshes the cache so reopening shows the saved text. Confirms show what the report
|
|
18
|
+
* used the last time it ran when the API says so; there is no advance price.
|
|
19
|
+
*/
|
|
20
|
+
import { useState, type ReactNode } from "react";
|
|
21
|
+
import { Check, Pencil, RefreshCw, Trash2, X } from "lucide-react";
|
|
22
|
+
import { Button, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
23
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
24
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
25
|
+
import { useConfirmAction } from "@/g8/components/confirmed-action";
|
|
26
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
27
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
28
|
+
import { KeyValueList, EmptyValue } from "@/g8/components/screen/key-value-list";
|
|
29
|
+
import { StatusPill, toneForStatus } from "@/g8/components/screen/status-pill";
|
|
30
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
31
|
+
import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
32
|
+
import { plural } from "@/g8/lib/plural";
|
|
33
|
+
import { INTEL_KEY, refreshIntelligence, useResearchTarget } from "../hooks/use-intelligence";
|
|
34
|
+
import { eventLine, humanize, intelBody, isGenerating, lastRunCredits, reportEdit, rowsOf, text, when } from "../lib/intelligence-types";
|
|
35
|
+
import type { ReportDetailBody, ThinkingEvent } from "../lib/intelligence-types";
|
|
36
|
+
import { SafeMarkdown } from "../lib/safe-markdown";
|
|
37
|
+
|
|
38
|
+
export interface ReportDetailProps {
|
|
39
|
+
reportId: string;
|
|
40
|
+
onClose?: () => void;
|
|
41
|
+
onExpand?: () => void;
|
|
42
|
+
onCollapse?: () => void;
|
|
43
|
+
/** Called after any write (and after a delete, with the report gone), so the parent list refreshes. */
|
|
44
|
+
onChanged?: () => void;
|
|
45
|
+
extraActions?: ReactNode;
|
|
46
|
+
extraTabs?: { id: string; label: string; content: ReactNode }[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const EVENTS_SHOWN = 50;
|
|
50
|
+
|
|
51
|
+
function Reasoning({ reportId }: { reportId: string }) {
|
|
52
|
+
const [all, setAll] = useState(false);
|
|
53
|
+
const q = useG8Query(async (signal) => rowsOf<ThinkingEvent>(intelBody(await g8Get(`/research-reports/${encodeURIComponent(reportId)}/thinking`, undefined, { signal })), "events"), [reportId], { key: [INTEL_KEY, "thinking", reportId] });
|
|
54
|
+
if (q.error) return <ApiError error={q.error} title="Could not load the reasoning" onRetry={() => void q.refetch()} />;
|
|
55
|
+
if (!q.data) return <Skeleton className="h-24 w-full" />;
|
|
56
|
+
if (q.data.length === 0) return <EmptyState title="No reasoning recorded" description="The model's steps are kept for a limited time after a report is written." compact />;
|
|
57
|
+
const shown = all ? q.data : q.data.slice(0, EVENTS_SHOWN);
|
|
58
|
+
return (
|
|
59
|
+
<div className="space-y-2">
|
|
60
|
+
<ol className="space-y-1.5 text-sm">
|
|
61
|
+
{shown.map((e, i) => (
|
|
62
|
+
<li key={i} className="flex gap-2">
|
|
63
|
+
<span className="w-20 shrink-0 text-xs tabular-nums text-muted-foreground">{e.timestamp ? new Date(e.timestamp).toLocaleTimeString() : ""}</span>
|
|
64
|
+
<StatusPill tone="neutral" quiet>{humanize(e.type ?? "event")}</StatusPill>
|
|
65
|
+
<span className="min-w-0 break-words">{eventLine(e)}</span>
|
|
66
|
+
</li>
|
|
67
|
+
))}
|
|
68
|
+
</ol>
|
|
69
|
+
{q.data.length > EVENTS_SHOWN ? (
|
|
70
|
+
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
71
|
+
<span role="status">Showing {shown.length.toLocaleString()} of {plural(q.data.length, "step")}</span>
|
|
72
|
+
<Button type="button" variant="outline" size="sm" onClick={() => setAll((v) => !v)}>{all ? "Show fewer" : "Show all"}</Button>
|
|
73
|
+
</div>
|
|
74
|
+
) : null}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function ReportDetail({ reportId, onClose, onExpand, onCollapse, onChanged, extraActions, extraTabs }: ReportDetailProps) {
|
|
80
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
81
|
+
const target = useResearchTarget();
|
|
82
|
+
const [draft, setDraft] = useState<string | null>(null);
|
|
83
|
+
const [busy, setBusy] = useState(false);
|
|
84
|
+
const [problem, setProblem] = useState<unknown>(null);
|
|
85
|
+
const query = useG8Query(async (signal) => intelBody<ReportDetailBody>(await g8Get(`/research-reports/${encodeURIComponent(reportId)}`, undefined, { signal })), [reportId], { key: [INTEL_KEY, "report", reportId] });
|
|
86
|
+
const report = query.data;
|
|
87
|
+
|
|
88
|
+
if (query.error) return <ApiError error={query.error} title="Could not load the report" onRetry={() => void query.refetch()} />;
|
|
89
|
+
if (!report) return <Skeleton className="h-40 w-full" />;
|
|
90
|
+
|
|
91
|
+
const title = text(report.report_title) ?? humanize(report.report_type ?? "report");
|
|
92
|
+
const generating = isGenerating(report.status);
|
|
93
|
+
const original = report.markdown_content ?? "";
|
|
94
|
+
const value = draft ?? original;
|
|
95
|
+
const edit = reportEdit(original, value);
|
|
96
|
+
const credits = lastRunCredits(report);
|
|
97
|
+
const website = text(report.website_url) ?? target.primary;
|
|
98
|
+
const changed = () => { void refreshIntelligence(); void query.refetch(); onChanged?.(); };
|
|
99
|
+
|
|
100
|
+
const save = () => edit && ask({
|
|
101
|
+
tier: "billable", title: `Save changes to ${title}`, confirmLabel: "Save",
|
|
102
|
+
description: <p>Saving replaces the report text and re-indexes it in your knowledge base, which uses credits. The API gives no price in advance.</p>,
|
|
103
|
+
run: () => g8Patch(`/research-reports/${encodeURIComponent(reportId)}`, edit),
|
|
104
|
+
onDone: () => { setDraft(null); changed(); },
|
|
105
|
+
});
|
|
106
|
+
const rewrite = () => ask({
|
|
107
|
+
tier: "billable", title: `Write ${title} again`, confirmLabel: "Write again", estimatedCredits: credits ?? undefined,
|
|
108
|
+
description: <p>Runs this 1 report in full and replaces the current text.{credits != null ? " The credits shown are what it used the last time." : " The API gives no price in advance."}</p>,
|
|
109
|
+
run: () => g8Post("/research-reports/generate", { website_url: website, report_number: report.report_number }), onDone: changed,
|
|
110
|
+
});
|
|
111
|
+
const remove = () => ask({
|
|
112
|
+
tier: "destructive", title: `Delete ${title}`, confirmLabel: "Delete", affected: 1,
|
|
113
|
+
description: <p>Deletes this report. Writing it again is a full run{credits != null ? `, which used ${credits.toLocaleString()} credits last time` : ""}.</p>,
|
|
114
|
+
run: () => g8Delete(`/research-reports/${encodeURIComponent(reportId)}`), tolerateStatus: [404],
|
|
115
|
+
onDone: () => { setDraft(null); void refreshIntelligence(); onChanged?.(); onClose?.(); },
|
|
116
|
+
});
|
|
117
|
+
const approve = async (on: boolean) => {
|
|
118
|
+
setBusy(true);
|
|
119
|
+
setProblem(null);
|
|
120
|
+
try { await g8Post(`/research-reports/${encodeURIComponent(reportId)}/${on ? "approve" : "unapprove"}`); changed(); } catch (err) { setProblem(err); } finally { setBusy(false); }
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const approved = Boolean(report.approved);
|
|
124
|
+
const actions = (
|
|
125
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
126
|
+
<Button type="button" size="sm" variant="outline" disabled={busy || generating} onClick={() => void approve(!approved)} title="Approved reports are indexed into the knowledge base that campaigns and the copilot read">
|
|
127
|
+
{approved ? <X className="size-3.5" aria-hidden /> : <Check className="size-3.5" aria-hidden />}{approved ? "Remove from knowledge" : "Approve into knowledge"}
|
|
128
|
+
</Button>
|
|
129
|
+
<Button type="button" size="sm" variant="outline" disabled={generating || !website || report.report_number == null} onClick={rewrite}><RefreshCw className="size-3.5" aria-hidden />Write again</Button>
|
|
130
|
+
<Button type="button" size="sm" variant="ghost" onClick={remove}><Trash2 className="size-3.5" aria-hidden />Delete</Button>
|
|
131
|
+
{extraActions}
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
const tabs = [
|
|
135
|
+
{
|
|
136
|
+
id: "report", label: "Report",
|
|
137
|
+
content: (
|
|
138
|
+
<div className="space-y-3">
|
|
139
|
+
{report.error_message ? <p role="alert" className="text-sm text-error-foreground">{report.error_message}</p> : null}
|
|
140
|
+
{text(report.markdown_content) ? <SafeMarkdown markdown={report.markdown_content as string} /> : <EmptyState title={generating ? "Writing the report" : "No text yet"} description={generating ? "This page updates when it is done." : "This report has not been written."} compact />}
|
|
141
|
+
</div>
|
|
142
|
+
),
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: "edit", label: "Edit",
|
|
146
|
+
content: (
|
|
147
|
+
<form className="space-y-3" onSubmit={(e) => { e.preventDefault(); save(); }}>
|
|
148
|
+
{generating ? <p className="text-sm text-muted-foreground">The report is being written. Edit it once it is done.</p> : null}
|
|
149
|
+
<Textarea aria-label="Report text" rows={18} value={value} disabled={generating} onChange={(e) => setDraft(e.target.value)} className="font-mono text-xs" />
|
|
150
|
+
<div className="flex items-center gap-2">
|
|
151
|
+
<Button type="submit" size="sm" disabled={!edit || generating}><Pencil className="size-3.5" aria-hidden />Save changes</Button>
|
|
152
|
+
{draft !== null ? <Button type="button" size="sm" variant="ghost" onClick={() => setDraft(null)}>Discard changes</Button> : null}
|
|
153
|
+
</div>
|
|
154
|
+
</form>
|
|
155
|
+
),
|
|
156
|
+
},
|
|
157
|
+
{ id: "reasoning", label: "Reasoning", content: <Reasoning reportId={reportId} /> },
|
|
158
|
+
{
|
|
159
|
+
id: "details", label: "Details",
|
|
160
|
+
content: (
|
|
161
|
+
<KeyValueList columns={2} items={[
|
|
162
|
+
{ label: "Type", value: text(report.report_type) ?? <EmptyValue />, mono: true },
|
|
163
|
+
{ label: "Number", value: report.report_number ?? <EmptyValue /> },
|
|
164
|
+
{ label: "Website", value: website ?? <EmptyValue /> },
|
|
165
|
+
{ label: "Approved", value: approved ? (when(report.approved_at) ?? "Yes") : "No" },
|
|
166
|
+
{ label: "Created", value: when(report.created_at) ?? <EmptyValue /> },
|
|
167
|
+
{ label: "Completed", value: when(report.completed_at) ?? <EmptyValue /> },
|
|
168
|
+
{ label: "Credits used last time", value: credits ?? <EmptyValue /> },
|
|
169
|
+
]} />
|
|
170
|
+
),
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
return (
|
|
174
|
+
<>
|
|
175
|
+
<DetailPanel
|
|
176
|
+
title={title}
|
|
177
|
+
subtitle={text(report.website_url) ?? undefined}
|
|
178
|
+
badges={<StatusPill tone={toneForStatus(report.status ?? "")}>{generating ? "Generating" : humanize(report.status ?? "unknown")}</StatusPill>}
|
|
179
|
+
actions={actions}
|
|
180
|
+
tabs={extraTabs?.length ? [...tabs, ...extraTabs] : tabs}
|
|
181
|
+
onClose={onClose}
|
|
182
|
+
onExpand={onExpand}
|
|
183
|
+
onCollapse={onCollapse}
|
|
184
|
+
/>
|
|
185
|
+
{failure || problem ? <ApiError error={failure ?? problem} title="That did not work" /> : null}
|
|
186
|
+
<ConfirmSpendDialog {...dialog} />
|
|
187
|
+
</>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ResearchReports: the six research reports about your market (review sentiment, competitive teardown, voice of customer,
|
|
5
|
+
* GTM channels, analyst and media, buyer psychology) and the workflow that writes them. Start one or all, retry a failed
|
|
6
|
+
* one, resume what is missing, stop a run, reset the workflow. Click a finished report to open it.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. getIntelligencePrimaryWebsite GET /intelligence/primary-website (read, through useResearchTarget)
|
|
10
|
+
* 2. getResearchWorkflowStatus GET /research-reports/workflow?website_url (read) all six slots; re-read every 5 s while one generates
|
|
11
|
+
* 3. generateResearchReport POST /research-reports/generate (BILLABLE) one report; ConfirmSpendDialog first
|
|
12
|
+
* 4. generateAllResearchReports POST /research-reports/generate-all (BILLABLE) all six; ConfirmSpendDialog first
|
|
13
|
+
* 5. resumeResearchWorkflow POST /research-reports/workflow/resume (BILLABLE) every report that is not finished; ConfirmSpendDialog first
|
|
14
|
+
* 6. retryResearchReport POST /research-reports/retry (BILLABLE) one failed report; ConfirmSpendDialog first
|
|
15
|
+
* 7. stopAllResearch POST /research-reports/stop-all (write) cancels what has not started; what already ran stays paid for
|
|
16
|
+
* 8. resetResearchWorkflow POST /research-reports/workflow/reset (DESTRUCTIVE) discards the workflow's progress; ConfirmSpendDialog first
|
|
17
|
+
* The API quotes no price: every confirm states the number of reports it will write (computed from the six slots on screen),
|
|
18
|
+
* never a fixed cost.
|
|
19
|
+
*/
|
|
20
|
+
import { useRef } from "react";
|
|
21
|
+
import { FileSearch, OctagonX, Play, RotateCcw } from "lucide-react";
|
|
22
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
23
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
24
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
25
|
+
import { useConfirmAction } from "@/g8/components/confirmed-action";
|
|
26
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
27
|
+
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
28
|
+
import { StatusPill, toneForStatus } from "@/g8/components/screen/status-pill";
|
|
29
|
+
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
30
|
+
import { g8Post } from "@/g8/lib/g8-client";
|
|
31
|
+
import { plural } from "@/g8/lib/plural";
|
|
32
|
+
import { refreshIntelligence, useResearchTarget, useWorkflow } from "../hooks/use-intelligence";
|
|
33
|
+
import { REPORT_TYPES, humanize, incompleteSlots, isGenerating, isReportDone, slotsOf, text, whenDate } from "../lib/intelligence-types";
|
|
34
|
+
import type { ReportSlot, WorkflowStatus } from "../lib/intelligence-types";
|
|
35
|
+
|
|
36
|
+
export interface ResearchReportsProps {
|
|
37
|
+
/** Called when a person opens a report that has been generated. */
|
|
38
|
+
onSelect?: (slot: ReportSlot) => void;
|
|
39
|
+
/** The open report's id, to mark its row. */
|
|
40
|
+
selectedId?: string;
|
|
41
|
+
/** The URL param the owning screen reads for the open report. */
|
|
42
|
+
idParam?: string;
|
|
43
|
+
/** Called after any write, so a parent can refresh its own numbers. */
|
|
44
|
+
onChanged?: () => void;
|
|
45
|
+
className?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const slotTitle = (s: ReportSlot): string => text(s.report_title) ?? humanize(s.report_type);
|
|
49
|
+
const titles = (slots: ReportSlot[]): string => slots.map(slotTitle).join(", ");
|
|
50
|
+
|
|
51
|
+
export function ResearchReports({ onSelect, selectedId, idParam, onChanged, className }: ResearchReportsProps) {
|
|
52
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
53
|
+
const target = useResearchTarget();
|
|
54
|
+
const host = target.primary;
|
|
55
|
+
const { workflow, error, refetch } = useWorkflow(host);
|
|
56
|
+
const slots = slotsOf(workflow ?? undefined);
|
|
57
|
+
const generating = slots.some((s) => isGenerating(s.status));
|
|
58
|
+
// A failed or superseded refetch resolves undefined: keep the last known workflow so polling continues while a report writes.
|
|
59
|
+
const latest = useRef<WorkflowStatus | undefined>(undefined);
|
|
60
|
+
latest.current = workflow ?? latest.current;
|
|
61
|
+
useJobPoll<WorkflowStatus>(async () => (await refetch()) ?? latest.current ?? {}, {
|
|
62
|
+
enabled: generating, intervalMs: 5000, maxAttempts: 360,
|
|
63
|
+
isTerminal: (w) => !slotsOf(w).some((s) => isGenerating(s.status)),
|
|
64
|
+
onDone: () => { void refreshIntelligence(); onChanged?.(); },
|
|
65
|
+
});
|
|
66
|
+
const done = () => { void refetch(); void refreshIntelligence(); onChanged?.(); };
|
|
67
|
+
|
|
68
|
+
if (target.loading) return <Skeleton className="h-40 w-full" />;
|
|
69
|
+
if (!host) return <EmptyState title="Research a website first" description="The reports are written about your company's website. Start the website research on the Overview tab." icon={<FileSearch />} />;
|
|
70
|
+
if (error) return <ApiError error={error} title="Could not load the reports" onRetry={() => void refetch()} />;
|
|
71
|
+
if (!workflow) return <Skeleton className="h-40 w-full" />;
|
|
72
|
+
|
|
73
|
+
const finished = slots.filter((s) => isReportDone(s.status));
|
|
74
|
+
const missing = incompleteSlots(slots);
|
|
75
|
+
const withContent = slots.filter((s) => s.has_content);
|
|
76
|
+
const generate = (s: ReportSlot) => ask({
|
|
77
|
+
tier: "billable", title: `Write report ${s.report_number}: ${slotTitle(s)}`, confirmLabel: "Write report",
|
|
78
|
+
description: <p>A long multi-pass run against your market, your site and your competitors writes this 1 report. The API gives no price in advance: it is metered by usage.{isReportDone(s.status) ? " It replaces the finished version." : ""}</p>,
|
|
79
|
+
run: () => g8Post("/research-reports/generate", { website_url: host, report_number: s.report_number }), onDone: done,
|
|
80
|
+
});
|
|
81
|
+
const retry = (s: ReportSlot) => ask({
|
|
82
|
+
tier: "billable", title: `Retry report ${s.report_number}: ${slotTitle(s)}`, confirmLabel: "Retry report",
|
|
83
|
+
description: <p>Runs this 1 failed report again in full. The API gives no price in advance: it is metered by usage.</p>,
|
|
84
|
+
run: () => g8Post("/research-reports/retry", { website_url: host, report_type: s.report_type }), onDone: done,
|
|
85
|
+
});
|
|
86
|
+
const generateAll = () => ask({
|
|
87
|
+
tier: "billable", title: `Write all ${plural(slots.length || REPORT_TYPES.length, "report")}`, confirmLabel: "Write all",
|
|
88
|
+
description: <p>Writes {slots.length || REPORT_TYPES.length} reports, each a long multi-pass run.{finished.length ? ` ${plural(finished.length, "report")} already finished will be written again.` : ""} The API gives no price in advance: it is metered by usage, and what already ran stays paid for if you stop.</p>,
|
|
89
|
+
run: () => g8Post("/research-reports/generate-all", { website_url: host }), onDone: done,
|
|
90
|
+
});
|
|
91
|
+
const resume = () => ask({
|
|
92
|
+
tier: "billable", title: `Write the ${plural(missing.length, "missing report")}`, confirmLabel: "Write missing",
|
|
93
|
+
description: <p>Writes the {plural(missing.length, "report")} that {missing.length === 1 ? "is" : "are"} not finished: {titles(missing)}. The API gives no price in advance: it is metered by usage.</p>,
|
|
94
|
+
run: () => g8Post("/research-reports/workflow/resume"), onDone: done,
|
|
95
|
+
});
|
|
96
|
+
const reset = () => ask({
|
|
97
|
+
tier: "destructive", title: "Reset the research workflow", confirmLabel: "Reset", affected: withContent.length,
|
|
98
|
+
description: <p>Discards the workflow's progress, including {plural(withContent.length, "report")} with content. Writing them again is a full run each.</p>,
|
|
99
|
+
run: () => g8Post("/research-reports/workflow/reset", { website_url: host }), onDone: done,
|
|
100
|
+
});
|
|
101
|
+
const stop = () => ask({
|
|
102
|
+
tier: "destructive", title: "Stop the running reports", confirmLabel: "Stop", affected: slots.filter((s) => isGenerating(s.status)).length,
|
|
103
|
+
description: <p>Cancels the reports that have not started. What already ran stays and has already been charged for.</p>,
|
|
104
|
+
run: () => g8Post("/research-reports/stop-all", { website_url: host }), onDone: done,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div className={className}>
|
|
109
|
+
<div className="mb-3 flex flex-wrap items-center gap-2">
|
|
110
|
+
<Button type="button" size="sm" onClick={generateAll} disabled={generating}><Play className="size-3.5" aria-hidden />Write all reports</Button>
|
|
111
|
+
<Button type="button" size="sm" variant="outline" onClick={resume} disabled={generating || missing.length === 0}>Write missing ({missing.length})</Button>
|
|
112
|
+
<Button type="button" size="sm" variant="outline" onClick={stop} disabled={!generating}><OctagonX className="size-3.5" aria-hidden />Stop</Button>
|
|
113
|
+
<Button type="button" size="sm" variant="ghost" onClick={reset} disabled={generating || withContent.length === 0}><RotateCcw className="size-3.5" aria-hidden />Reset workflow</Button>
|
|
114
|
+
<span className="ml-auto text-xs text-muted-foreground tabular-nums" role="status">{workflow.reports_completed ?? finished.length} of {workflow.total_reports ?? slots.length} finished</span>
|
|
115
|
+
</div>
|
|
116
|
+
{failure ? <ApiError error={failure} title="That did not work" className="mb-3" /> : null}
|
|
117
|
+
<ul className="divide-y divide-border rounded-md border border-border">
|
|
118
|
+
{slots.map((s) => (
|
|
119
|
+
<li key={s.report_number} aria-current={s.id && s.id === selectedId ? "true" : undefined} className="flex items-center justify-between gap-3 px-3 py-2 text-sm aria-[current=true]:bg-muted">
|
|
120
|
+
<div className="min-w-0">
|
|
121
|
+
<span className="mr-2 text-xs tabular-nums text-muted-foreground">{s.report_number}</span>
|
|
122
|
+
{s.id && s.has_content ? <RecordLink id={s.id} param={idParam} onOpen={() => onSelect?.(s)}>{slotTitle(s)}</RecordLink> : <span className="font-medium">{slotTitle(s)}</span>}
|
|
123
|
+
<div className="text-xs text-muted-foreground">{[s.approved ? "Approved into knowledge" : null, whenDate(s.completed_at) ? `Finished ${whenDate(s.completed_at)}` : null].filter(Boolean).join(", ") || "Not written yet"}</div>
|
|
124
|
+
</div>
|
|
125
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
126
|
+
<StatusPill tone={toneForStatus(s.status)}>{humanize(s.status)}</StatusPill>
|
|
127
|
+
{s.status === "failed" ? <Button type="button" size="sm" variant="outline" onClick={() => retry(s)} aria-label={`Retry report ${s.report_number}`}>Retry</Button> : null}
|
|
128
|
+
{s.status === "pending" ? <Button type="button" size="sm" variant="outline" onClick={() => generate(s)} aria-label={`Write report ${s.report_number}`}>Write</Button> : null}
|
|
129
|
+
</div>
|
|
130
|
+
</li>
|
|
131
|
+
))}
|
|
132
|
+
</ul>
|
|
133
|
+
<ConfirmSpendDialog {...dialog} />
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ResearchRun: start the website research, follow it step by step, and reset it. This is the workspace's OWN company
|
|
5
|
+
* research (the API locks it to one primary website): it collects the company profile, products, keywords, competitors,
|
|
6
|
+
* news, audience questions and brand assets that the rest of the block shows.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. getIntelligencePrimaryWebsite / getIntelligenceOrgDomain / getLatestIntelligenceTask (read, through the hooks)
|
|
10
|
+
* 2. listIntelligenceDocuments GET /intelligence/documents (read) how many records a reset would remove
|
|
11
|
+
* 3. analyzeWebsite POST /intelligence/analyze (BILLABLE) scrapes and runs models; ConfirmSpendDialog first
|
|
12
|
+
* 4. getIntelligenceStatus GET /intelligence/status/{task_id} (read) polled until the run stops
|
|
13
|
+
* 5. resetIntelligence DELETE /intelligence (DESTRUCTIVE) removes every record and run, unlocks the website; ConfirmSpendDialog first
|
|
14
|
+
*
|
|
15
|
+
* Cost: the API quotes no price in advance and the run is metered by usage, so the confirm states the quantity it can
|
|
16
|
+
* state (how many sources are collected) and whether the call would only return a run from the last hour (which does
|
|
17
|
+
* nothing new and costs nothing). A reset states the real number of records it deletes.
|
|
18
|
+
*/
|
|
19
|
+
import { useEffect, useRef, useState } from "react";
|
|
20
|
+
import { RotateCcw, Sparkles } from "lucide-react";
|
|
21
|
+
import { Button, Input, Label } from "@graph8/sdk/ui";
|
|
22
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
23
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
24
|
+
import { useConfirmAction } from "@/g8/components/confirmed-action";
|
|
25
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
26
|
+
import { StatusPill, toneForStatus } from "@/g8/components/screen/status-pill";
|
|
27
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
28
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
29
|
+
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
30
|
+
import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
31
|
+
import { plural } from "@/g8/lib/plural";
|
|
32
|
+
import { INTEL_KEY, refreshIntelligence, useLatestRun, useResearchTarget } from "../hooks/use-intelligence";
|
|
33
|
+
import { finishedWithinHour, humanize, intelBody, isRunFinal, rowsOf, runSteps, stepTone, when } from "../lib/intelligence-types";
|
|
34
|
+
import type { AnalyzeAccepted, IntelDocument, RunStatus } from "../lib/intelligence-types";
|
|
35
|
+
|
|
36
|
+
export interface ResearchRunProps {
|
|
37
|
+
/** Called after a run was started, finished or reset, so a parent can refresh its own numbers. */
|
|
38
|
+
onChanged?: () => void;
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function ResearchRun({ onChanged, className }: ResearchRunProps) {
|
|
43
|
+
const brand = useBrand();
|
|
44
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
45
|
+
const target = useResearchTarget();
|
|
46
|
+
const latest = useLatestRun();
|
|
47
|
+
const [typed, setTyped] = useState("");
|
|
48
|
+
const [force, setForce] = useState(false);
|
|
49
|
+
const [taskId, setTaskId] = useState<string | null>(null);
|
|
50
|
+
// The poll reads the id from a ref: start() runs its first tick before React re-renders with the new state.
|
|
51
|
+
const polled = useRef<string | null>(null);
|
|
52
|
+
|
|
53
|
+
const docs = useG8Query(async (signal) => rowsOf<IntelDocument>(intelBody(await g8Get("/intelligence/documents", undefined, { signal })), "documents"), [], { key: [INTEL_KEY, "documents"] });
|
|
54
|
+
const activeId = taskId ?? (latest.run && !isRunFinal(latest.run.status) ? latest.run.task_id ?? null : null);
|
|
55
|
+
const shownId = activeId ?? latest.run?.task_id ?? null;
|
|
56
|
+
const status = useJobPoll<RunStatus>(
|
|
57
|
+
async (signal) => intelBody<RunStatus>(await g8Get(`/intelligence/status/${encodeURIComponent(polled.current ?? "")}`, undefined, { signal })),
|
|
58
|
+
{ enabled: false, intervalMs: 3000, maxAttempts: 200, isTerminal: (s) => isRunFinal(s.status), onDone: () => { void refreshIntelligence(); onChanged?.(); } },
|
|
59
|
+
);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!activeId) return undefined;
|
|
62
|
+
polled.current = activeId;
|
|
63
|
+
status.start();
|
|
64
|
+
return () => status.stop();
|
|
65
|
+
// start/stop are stable; the poll restarts only when the run being followed changes.
|
|
66
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
67
|
+
}, [activeId]);
|
|
68
|
+
|
|
69
|
+
// The status of the run on screen: the polled one, else a one-time read of the last finished run.
|
|
70
|
+
const lastRun = useG8Query(async (signal) => (shownId && !activeId ? intelBody<RunStatus>(await g8Get(`/intelligence/status/${encodeURIComponent(shownId)}`, undefined, { signal })) : null), [shownId, activeId], { key: [INTEL_KEY, "run", shownId ?? "none", activeId ? "live" : "done"] });
|
|
71
|
+
const run: RunStatus | null = (activeId ? status.status : lastRun.data) ?? null;
|
|
72
|
+
|
|
73
|
+
const website = target.primary ?? (typed.trim() || null);
|
|
74
|
+
const recent = finishedWithinHour(latest.run) && !force;
|
|
75
|
+
const sources = runSteps(run ?? undefined).length;
|
|
76
|
+
const records = docs.data?.length;
|
|
77
|
+
|
|
78
|
+
const askResearch = () => {
|
|
79
|
+
if (!website) return;
|
|
80
|
+
ask({
|
|
81
|
+
tier: "billable",
|
|
82
|
+
title: `Research ${website}`,
|
|
83
|
+
confirmLabel: "Start research",
|
|
84
|
+
description: (
|
|
85
|
+
<>
|
|
86
|
+
{recent ? <p>A run finished less than an hour ago, so this returns that run and does no new work. Tick "Run again" to collect fresh data.</p>
|
|
87
|
+
: <p>{sources > 0 ? `Collects ${plural(sources, "source")} from the website` : "Collects the company profile, products, keywords, competitors, news and brand assets from the website"}, with scraping and models. The API gives no price in advance: it is metered by usage.</p>}
|
|
88
|
+
{!target.primary ? <p>This website becomes your workspace's research website and is locked: you can only change it by resetting the research.</p> : null}
|
|
89
|
+
</>
|
|
90
|
+
),
|
|
91
|
+
run: async () => {
|
|
92
|
+
const env = await g8Post("/intelligence/analyze", force ? { website_url: website, force: true } : { website_url: website });
|
|
93
|
+
const accepted = intelBody<AnalyzeAccepted>(env);
|
|
94
|
+
if (accepted.task_id) setTaskId(accepted.task_id);
|
|
95
|
+
},
|
|
96
|
+
onDone: () => { void refreshIntelligence(); void target.refetch(); onChanged?.(); },
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const askReset = () => ask({
|
|
101
|
+
tier: "destructive",
|
|
102
|
+
title: "Reset company research",
|
|
103
|
+
confirmLabel: "Reset",
|
|
104
|
+
affected: records,
|
|
105
|
+
description: <p>Deletes every collected source and every research run, and unlocks the research website so you can pick another. Wiki documents and research reports are not touched. Collecting the data again uses credits.</p>,
|
|
106
|
+
run: async () => { await g8Delete("/intelligence"); setTaskId(null); polled.current = null; status.stop(); },
|
|
107
|
+
onDone: () => { void refreshIntelligence(); void target.refetch(); onChanged?.(); },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const steps = runSteps(run ?? undefined);
|
|
111
|
+
const running = run ? !isRunFinal(run.status) : false;
|
|
112
|
+
return (
|
|
113
|
+
<SectionCard
|
|
114
|
+
className={className}
|
|
115
|
+
title="Website research"
|
|
116
|
+
description={`Collects what ${brand.linkLabel} learns about your company from its website.`}
|
|
117
|
+
icon={<Sparkles className="size-4" aria-hidden />}
|
|
118
|
+
actions={<Button type="button" variant="outline" size="sm" onClick={askReset} disabled={!target.primary && !records}><RotateCcw className="size-3.5" aria-hidden />Reset research</Button>}
|
|
119
|
+
>
|
|
120
|
+
<div className="space-y-4">
|
|
121
|
+
{target.error ? <ApiError error={target.error} title="Could not load the research website" onRetry={() => void target.refetch()} compact /> : null}
|
|
122
|
+
<div className="flex flex-wrap items-end gap-2">
|
|
123
|
+
{target.primary ? (
|
|
124
|
+
<div className="space-y-1">
|
|
125
|
+
<Label htmlFor="intel-website">Research website</Label>
|
|
126
|
+
<Input id="intel-website" readOnly value={target.primary} className="w-72" />
|
|
127
|
+
</div>
|
|
128
|
+
) : (
|
|
129
|
+
<div className="space-y-1">
|
|
130
|
+
<Label htmlFor="intel-website">Website to research</Label>
|
|
131
|
+
<Input id="intel-website" placeholder="acme.com" value={typed} onChange={(e) => setTyped(e.target.value)} className="w-72" />
|
|
132
|
+
{target.suggested && !typed ? <button type="button" className="text-xs underline underline-offset-2" onClick={() => setTyped(target.suggested ?? "")}>Use {target.suggested} from your workspace settings</button> : null}
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
<Button type="button" onClick={askResearch} disabled={!website || running}>{running ? "Researching..." : latest.run ? "Research again" : "Research website"}</Button>
|
|
136
|
+
{finishedWithinHour(latest.run) ? (
|
|
137
|
+
<label className="flex items-center gap-1.5 text-sm"><input type="checkbox" checked={force} onChange={(e) => setForce(e.target.checked)} />Run again even though one finished in the last hour</label>
|
|
138
|
+
) : null}
|
|
139
|
+
</div>
|
|
140
|
+
{target.primary ? <p className="text-xs text-muted-foreground">The research is locked to this website. Reset the research to use another.</p> : null}
|
|
141
|
+
{failure ? <ApiError error={failure} title="That did not work" /> : null}
|
|
142
|
+
{status.error && activeId ? <ApiError error={status.error} title="Could not read the run's progress" compact /> : null}
|
|
143
|
+
{run ? (
|
|
144
|
+
<div className="space-y-2" aria-live="polite">
|
|
145
|
+
<div className="flex flex-wrap items-center gap-2 text-sm">
|
|
146
|
+
<StatusPill tone={toneForStatus(run.status ?? "")}>{humanize(run.status ?? "unknown")}</StatusPill>
|
|
147
|
+
{run.current_step && running ? <span className="text-muted-foreground">{run.current_step}</span> : null}
|
|
148
|
+
{typeof run.progress === "number" ? <span className="tabular-nums text-muted-foreground">{run.progress}%</span> : null}
|
|
149
|
+
{when(run.completed_at) ? <span className="text-xs text-muted-foreground">Finished {when(run.completed_at)}</span> : null}
|
|
150
|
+
</div>
|
|
151
|
+
{run.error_message ? <p role="alert" className="text-sm text-error-foreground">{run.error_message}</p> : null}
|
|
152
|
+
<ul className="grid gap-1 sm:grid-cols-2">
|
|
153
|
+
{steps.map((s) => <li key={s.key} className="flex items-center justify-between gap-2 text-sm"><span>{s.label}</span><StatusPill tone={stepTone(s.state)} quiet>{humanize(s.state)}</StatusPill></li>)}
|
|
154
|
+
</ul>
|
|
155
|
+
</div>
|
|
156
|
+
) : latest.loading ? null : <p className="text-sm text-muted-foreground">No research has run yet.</p>}
|
|
157
|
+
</div>
|
|
158
|
+
<ConfirmSpendDialog {...dialog} />
|
|
159
|
+
</SectionCard>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SourceDetail: the complete screen for one intelligence source. Shows what it collected (SourceView), when, whether a
|
|
5
|
+
* person edited it, and can re-collect it. A source that was not collected says so and offers the same button.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. getIntelligencePrimaryWebsite GET /intelligence/primary-website (read, through useResearchTarget)
|
|
9
|
+
* 2. getIntelligenceData GET /intelligence/data/{data_type}?website_url (read) 404 means "not collected"
|
|
10
|
+
* 3. retryIntelligenceSource POST /intelligence/retry (BILLABLE) re-collects ONE source; ConfirmSpendDialog first
|
|
11
|
+
* Re-collecting keeps the last good data if it fails. The API gives no price; the confirm states the one source it touches.
|
|
12
|
+
*/
|
|
13
|
+
import { useRef, useState, type ReactNode } from "react";
|
|
14
|
+
import { RefreshCw } from "lucide-react";
|
|
15
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
16
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
17
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
18
|
+
import { useConfirmAction } from "@/g8/components/confirmed-action";
|
|
19
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
20
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
21
|
+
import { KeyValueList, EmptyValue } from "@/g8/components/screen/key-value-list";
|
|
22
|
+
import { StatusPill, toneForStatus } from "@/g8/components/screen/status-pill";
|
|
23
|
+
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
24
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
25
|
+
import { g8Post } from "@/g8/lib/g8-client";
|
|
26
|
+
import { refreshIntelligence, useResearchTarget, useSourceEntry } from "../hooks/use-intelligence";
|
|
27
|
+
import { humanize, isRec, text, when } from "../lib/intelligence-types";
|
|
28
|
+
import type { Rec } from "../lib/intelligence-types";
|
|
29
|
+
import { SourceView } from "./source-view";
|
|
30
|
+
|
|
31
|
+
export interface SourceDetailProps {
|
|
32
|
+
dataType: string;
|
|
33
|
+
onClose?: () => void;
|
|
34
|
+
onExpand?: () => void;
|
|
35
|
+
onCollapse?: () => void;
|
|
36
|
+
onChanged?: () => void;
|
|
37
|
+
/** Extra header actions from the screen's `renderDetailActions` slot. */
|
|
38
|
+
extraActions?: ReactNode;
|
|
39
|
+
/** Extra tabs from the screen's `extraDetailTabs` slot, after the built-in ones. */
|
|
40
|
+
extraTabs?: { id: string; label: string; content: ReactNode }[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const is404 = (e: unknown): boolean => (e as { status?: number } | null)?.status === 404;
|
|
44
|
+
|
|
45
|
+
export function SourceDetail({ dataType, onClose, onExpand, onCollapse, onChanged, extraActions, extraTabs }: SourceDetailProps) {
|
|
46
|
+
const brand = useBrand();
|
|
47
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
48
|
+
const target = useResearchTarget();
|
|
49
|
+
const host = target.primary;
|
|
50
|
+
const [waiting, setWaiting] = useState(false);
|
|
51
|
+
const before = useRef<string | null>(null);
|
|
52
|
+
const query = useSourceEntry(dataType, host, !target.loading);
|
|
53
|
+
const entry = query.data && isRec(query.data) ? query.data : null;
|
|
54
|
+
const collectedAt = text(entry?.collected_at);
|
|
55
|
+
|
|
56
|
+
// After a re-collect the work runs in the background: look again until the collection time moves.
|
|
57
|
+
useJobPoll<Rec | undefined>(async () => (await query.refetch()) ?? undefined, {
|
|
58
|
+
enabled: waiting, intervalMs: 4000, maxAttempts: 30,
|
|
59
|
+
isTerminal: (e) => text(e?.collected_at) !== before.current,
|
|
60
|
+
onDone: () => { setWaiting(false); void refreshIntelligence(); onChanged?.(); },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const title = text(entry?.display_name) ?? humanize(dataType);
|
|
64
|
+
const askRetry = () => ask({
|
|
65
|
+
tier: "billable",
|
|
66
|
+
title: `Re-collect ${title}`,
|
|
67
|
+
confirmLabel: "Re-collect",
|
|
68
|
+
description: <p>Collects this one source again from {host} with scraping and models. If it fails, the last good data stays. The API gives no price in advance: it is metered by usage.</p>,
|
|
69
|
+
run: async () => { before.current = collectedAt; await g8Post("/intelligence/retry", { website_url: host, data_type: dataType }); setWaiting(true); },
|
|
70
|
+
onDone: () => { void refreshIntelligence(); },
|
|
71
|
+
});
|
|
72
|
+
const retry = (
|
|
73
|
+
<Button type="button" size="sm" variant="outline" disabled={!host || waiting} onClick={askRetry} title={host ? undefined : "Start the website research first"}>
|
|
74
|
+
<RefreshCw className="size-3.5" aria-hidden />{waiting ? "Collecting..." : "Re-collect"}
|
|
75
|
+
</Button>
|
|
76
|
+
);
|
|
77
|
+
const actions = <div className="flex flex-wrap items-center gap-2">{retry}{extraActions}</div>;
|
|
78
|
+
|
|
79
|
+
let body: ReactNode;
|
|
80
|
+
if (query.error && !is404(query.error)) body = <ApiError error={query.error} title="Could not load the source" onRetry={() => void query.refetch()} />;
|
|
81
|
+
else if (!query.data && !query.error) body = <Skeleton className="h-40 w-full" />;
|
|
82
|
+
else if (!entry) body = <EmptyState title="Not collected" description="This source has no data. Re-collect it, or run the website research again." compact />;
|
|
83
|
+
else body = <SourceView dataType={dataType} data={entry.data} />;
|
|
84
|
+
|
|
85
|
+
const edits = isRec(entry?.overrides) ? Object.keys(entry.overrides as Rec).length : 0;
|
|
86
|
+
const tabs = [
|
|
87
|
+
{ id: "data", label: "Data", content: <div className="space-y-3">{edits ? <p className="text-xs text-muted-foreground">{edits === 1 ? "1 field was" : `${edits} fields were`} edited by a person in {brand.linkLabel}; the values below include the edits.</p> : null}{body}</div> },
|
|
88
|
+
{
|
|
89
|
+
id: "details", label: "Details",
|
|
90
|
+
content: (
|
|
91
|
+
<KeyValueList columns={2} items={[
|
|
92
|
+
{ label: "Source", value: dataType, mono: true },
|
|
93
|
+
{ label: "Website", value: text(entry?.website_url) ?? host ?? <EmptyValue /> },
|
|
94
|
+
{ label: "Collected", value: when(collectedAt) ?? <EmptyValue /> },
|
|
95
|
+
{ label: "Last edited", value: when(entry?.edited_at) ? `${when(entry?.edited_at)}${text(entry?.edited_by_name) ? ` by ${text(entry?.edited_by_name)}` : ""}` : <EmptyValue /> },
|
|
96
|
+
{ label: "Note", value: text(entry?.note) ?? <EmptyValue />, wide: true },
|
|
97
|
+
]} />
|
|
98
|
+
),
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
return (
|
|
102
|
+
<>
|
|
103
|
+
<DetailPanel
|
|
104
|
+
title={title}
|
|
105
|
+
subtitle={host ?? undefined}
|
|
106
|
+
badges={entry ? <StatusPill tone={toneForStatus(text(entry.status) ?? "completed")}>{humanize(text(entry.status) ?? "completed")}</StatusPill> : undefined}
|
|
107
|
+
actions={actions}
|
|
108
|
+
tabs={extraTabs?.length ? [...tabs, ...extraTabs] : tabs}
|
|
109
|
+
onClose={onClose}
|
|
110
|
+
onExpand={onExpand}
|
|
111
|
+
onCollapse={onCollapse}
|
|
112
|
+
/>
|
|
113
|
+
{failure ? <ApiError error={failure} title="That did not work" /> : null}
|
|
114
|
+
<ConfirmSpendDialog {...dialog} />
|
|
115
|
+
</>
|
|
116
|
+
);
|
|
117
|
+
}
|