@graph8/sdk 0.370.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 +19 -0
- package/blocks/_foundation/README.md +14 -0
- package/blocks/_foundation/block.json +44 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +1 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/agents/README.md +81 -4
- package/blocks/agents/block.json +311 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +17 -3
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +20 -3
- package/blocks/agents/components/agent-runs.tsx +3 -3
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +53 -11
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/components/analytics-screen.tsx +16 -3
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/report-builder.tsx +15 -2
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +27 -0
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +29 -13
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/campaigns/components/campaign-assets.tsx +1 -1
- package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
- package/blocks/campaigns/components/icp-detail.tsx +11 -2
- package/blocks/campaigns/components/icp-editor.tsx +8 -3
- package/blocks/campaigns/components/persona-detail.tsx +11 -2
- package/blocks/campaigns/components/persona-editor.tsx +6 -1
- package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +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/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +1 -1
- package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
- package/blocks/crm/README.md +6 -0
- package/blocks/crm/block.json +53 -4
- package/blocks/crm/components/companies-screen.tsx +23 -9
- package/blocks/crm/components/company-detail.tsx +2 -2
- package/blocks/crm/components/contact-detail.tsx +4 -4
- package/blocks/crm/components/contact-lookup-link.tsx +44 -0
- package/blocks/crm/components/contacts-screen.tsx +31 -13
- package/blocks/crm/components/dedupe-review.tsx +6 -1
- package/blocks/crm/components/import-wizard.tsx +3 -7
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +1 -1
- package/blocks/crm/components/tasks-screen.tsx +35 -15
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-page-query.ts +3 -1
- package/blocks/crm/lib/crm-types.ts +14 -0
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/deals/README.md +24 -6
- package/blocks/deals/block.json +151 -2
- 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 +2 -2
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +30 -9
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +57 -22
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +1 -1
- package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +14 -5
- package/blocks/dialer/block.json +17 -2
- package/blocks/dialer/components/call-contact-button.tsx +26 -9
- package/blocks/dialer/components/call-log-screen.tsx +4 -1
- package/blocks/dialer/components/call-log.tsx +16 -4
- package/blocks/dialer/components/call-queue.tsx +3 -3
- package/blocks/dialer/components/dialer-screen.tsx +69 -27
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +58 -11
- package/blocks/dialer/components/softphone.tsx +1 -1
- package/blocks/enrichment/README.md +16 -6
- package/blocks/enrichment/block.json +19 -7
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +76 -27
- package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
- package/blocks/enrichment/components/enrich-button.tsx +1 -1
- package/blocks/enrichment/components/enrichment-screen.tsx +70 -17
- package/blocks/enrichment/components/waterfall-config.tsx +27 -7
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/forms/README.md +8 -0
- package/blocks/forms/block.json +107 -0
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +1 -1
- package/blocks/forms/components/form-embed.tsx +1 -1
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +51 -17
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +15 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +5 -0
- package/blocks/inbox/block.json +15 -0
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- package/blocks/integrations/README.md +54 -1
- package/blocks/integrations/block.json +189 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +20 -1
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +4 -4
- package/blocks/integrations/components/integrations-screen.tsx +20 -3
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +6 -6
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +26 -7
- package/blocks/integrations/components/webhook-manager.tsx +30 -17
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +5 -0
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +8 -8
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lists/README.md +7 -1
- package/blocks/lists/block.json +31 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +17 -7
- package/blocks/lists/components/list-picker.tsx +13 -3
- package/blocks/lists/components/list-table.tsx +1 -1
- package/blocks/lists/components/lists-screen.tsx +4 -2
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +27 -6
- package/blocks/lists/components/suppressions-screen.tsx +21 -12
- package/blocks/mailboxes/block.json +4 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
- package/blocks/mailboxes/components/domain-detail.tsx +10 -4
- package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
- package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
- package/blocks/mailboxes/components/warmup-health.tsx +20 -20
- package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +117 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +36 -13
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/components/nurture-builder.tsx +10 -2
- package/blocks/nurture/components/nurture-enroll.tsx +6 -7
- package/blocks/nurture/components/nurture-form.tsx +5 -8
- package/blocks/nurture/components/nurture-screen.tsx +16 -5
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/objects/README.md +1 -1
- package/blocks/objects/block.json +13 -5
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-record-detail.tsx +8 -1
- package/blocks/objects/components/object-schema-editor.tsx +1 -1
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +113 -1
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/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 +48 -1
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- 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 +3 -2
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-screen.tsx +2 -2
- package/blocks/sequencer/README.md +6 -0
- package/blocks/sequencer/block.json +28 -1
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +19 -20
- package/blocks/sequencer/components/sequence-detail.tsx +4 -2
- package/blocks/sequencer/components/sequence-enrollments.tsx +4 -2
- package/blocks/sequencer/components/sequence-list.tsx +1 -1
- package/blocks/sequencer/components/sequence-stats.tsx +8 -8
- package/blocks/sequencer/components/sequences-screen.tsx +3 -0
- package/blocks/sequencer/components/step-editor.tsx +11 -11
- package/blocks/sequencer/lib/sequencer-utils.ts +5 -5
- package/blocks/social-ads/README.md +8 -0
- package/blocks/social-ads/block.json +137 -4
- package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
- package/blocks/social-ads/components/audience-sync.tsx +6 -2
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +6 -1
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +6 -1
- package/blocks/social-ads/components/creative-gallery.tsx +20 -2
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +4 -2
- package/blocks/social-ads/components/post-detail.tsx +3 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-screen.tsx +33 -6
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/components/snippet-install.tsx +18 -9
- package/blocks/visitors/components/traffic-chart.tsx +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +24 -14
- package/blocks/visitors/components/visitor-feed.tsx +5 -5
- package/blocks/webchat/components/chat-widget-config.tsx +1 -1
- package/blocks/webchat/components/conversation-list.tsx +1 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +1 -1
- package/blocks/workbench/components/workbench-screen.tsx +15 -5
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workflows/components/workflow-editor.tsx +1 -1
- package/dist/cli.js +536 -392
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +1 -1
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +1 -1
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/brand.d.ts +11 -2
- package/dist/types/blocks/create.d.ts +7 -3
- package/dist/types/blocks/doctor.d.ts +1 -0
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/version.d.ts +2 -2
- package/docs/api/desk.d.ts +2 -2
- package/docs/api/desk.md +3 -3
- package/package.json +1 -1
- 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 +17 -4
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
- package/stacks/apollo/pages/contacts.tsx.tmpl +29 -95
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +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 +36 -8
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +8 -4
- package/stacks/clay/pages/companies.tsx.tmpl +17 -1
- package/stacks/clay/pages/contacts.tsx.tmpl +12 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +16 -3
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +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 +23 -6
- 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 +9 -4
- package/stacks/hubspot/pages/companies.tsx.tmpl +14 -35
- package/stacks/hubspot/pages/contacts.tsx.tmpl +29 -95
- package/stacks/hubspot/pages/deals.tsx.tmpl +10 -1
- package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +21 -5
- 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 +8 -4
- 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 +20 -5
- 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 +27 -66
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +25 -7
- package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +15 -35
- package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +4 -2
- package/stacks/salesforce/pages/companies.tsx.tmpl +14 -35
- package/stacks/salesforce/pages/contacts.tsx.tmpl +20 -35
- package/stacks/salesforce/pages/deals.tsx.tmpl +10 -1
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +7 -3
- package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +16 -3
- package/styles/styles.css +1 -1
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CompanyIntelligenceTab: an optional tab for a company's detail page. Mount it from the CRM block's `extraDetailTabs`:
|
|
5
|
+
*
|
|
6
|
+
* <CompaniesScreen extraDetailTabs={[{ id: "intelligence", label: "Intelligence", render: (c) => <CompanyIntelligenceTab company={c} /> }]} />
|
|
7
|
+
*
|
|
8
|
+
* What it can honestly show: the company intelligence API researches YOUR OWN workspace website (it is locked to one
|
|
9
|
+
* website) and has no lookup for other companies. So the tab shows
|
|
10
|
+
* - the full company profile when the company on screen IS your workspace's website,
|
|
11
|
+
* - the competitor entry your research holds for it when the company is one of the competitors your research found,
|
|
12
|
+
* - otherwise a clear note that there is nothing to show (it never invents firmographics, news or technology data).
|
|
13
|
+
*
|
|
14
|
+
* SDK operations, in order:
|
|
15
|
+
* 1. getIntelligencePrimaryWebsite GET /intelligence/primary-website (read, through useResearchTarget)
|
|
16
|
+
* 2. getAllIntelligenceData GET /intelligence/all/{website_url} (read, through CompanyProfileCard) own-company case
|
|
17
|
+
* 3. getIntelligenceData GET /intelligence/data/competitor_discovery (read, through useSourceEntry) competitor case
|
|
18
|
+
*/
|
|
19
|
+
import { Skeleton } from "@graph8/sdk/ui";
|
|
20
|
+
import { Telescope } from "lucide-react";
|
|
21
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
23
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
24
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
25
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
26
|
+
import { entryData, useResearchTarget, useSourceEntry } from "../hooks/use-intelligence";
|
|
27
|
+
import { asArray, canonicalDomain, humanize, isRec, text } from "../lib/intelligence-types";
|
|
28
|
+
import { CompanyProfileCard } from "./company-profile";
|
|
29
|
+
|
|
30
|
+
/** The few company fields the tab reads; the CRM block's CompanyRecord satisfies it. */
|
|
31
|
+
export interface TabCompany { name?: string | null; domain?: string | null; website?: string | null }
|
|
32
|
+
|
|
33
|
+
export interface CompanyIntelligenceTabProps {
|
|
34
|
+
company: TabCompany;
|
|
35
|
+
/** Called when a person asks for the full intelligence screen. Omit it to hide the button. */
|
|
36
|
+
onOpenIntelligence?: () => void;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function CompanyIntelligenceTab({ company, onOpenIntelligence, className }: CompanyIntelligenceTabProps) {
|
|
41
|
+
const brand = useBrand();
|
|
42
|
+
const host = canonicalDomain(company.domain ?? company.website) || null;
|
|
43
|
+
const target = useResearchTarget();
|
|
44
|
+
const own = Boolean(host && target.primaryDomain && host === target.primaryDomain);
|
|
45
|
+
const competitors = useSourceEntry("competitor_discovery", target.primary, Boolean(target.primary) && !own);
|
|
46
|
+
if (target.loading) return <Skeleton className="h-32 w-full" />;
|
|
47
|
+
if (target.error) return <ApiError error={target.error} title="Could not load company intelligence" onRetry={() => void target.refetch()} />;
|
|
48
|
+
if (!host) return <EmptyState title="No website on record" description="Add this company's website to see what your research knows about it." compact icon={<Telescope />} />;
|
|
49
|
+
if (own) {
|
|
50
|
+
return (
|
|
51
|
+
<div className={className}>
|
|
52
|
+
<CompanyProfileCard website={host} />
|
|
53
|
+
{onOpenIntelligence ? <button type="button" className="mt-3 text-sm underline underline-offset-2" onClick={onOpenIntelligence}>Open company intelligence</button> : null}
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
if (competitors.error && (competitors.error as { status?: number }).status !== 404) return <ApiError error={competitors.error} title="Could not load the competitors" onRetry={() => void competitors.refetch()} />;
|
|
58
|
+
const list = asArray(entryData(competitors.data ?? null)?.competitors).filter(isRec);
|
|
59
|
+
const match = list.find((c) => canonicalDomain(text(c.domain)) === host);
|
|
60
|
+
if (match) {
|
|
61
|
+
const pills = [text(match.competitive_type), text(match.similarity) ? `${text(match.similarity)} similarity` : null, text(match.confidence) ? `${text(match.confidence)} confidence` : null].filter(Boolean) as string[];
|
|
62
|
+
return (
|
|
63
|
+
<div className={className}>
|
|
64
|
+
<SectionCard title={text(match.name) ?? host} description="A competitor your company research found">
|
|
65
|
+
<div className="space-y-2 text-sm">
|
|
66
|
+
<div className="flex flex-wrap gap-1.5">{pills.map((p) => <StatusPill key={p} tone="info" quiet>{humanize(p)}</StatusPill>)}</div>
|
|
67
|
+
{text(match.description) ? <p>{text(match.description)}</p> : null}
|
|
68
|
+
{text(match.reasoning) ? <p className="text-muted-foreground">{text(match.reasoning)}</p> : null}
|
|
69
|
+
</div>
|
|
70
|
+
</SectionCard>
|
|
71
|
+
{onOpenIntelligence ? <button type="button" className="mt-3 text-sm underline underline-offset-2" onClick={onOpenIntelligence}>Open company intelligence</button> : null}
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (competitors.loading && !competitors.data && target.primary) return <Skeleton className="h-32 w-full" />;
|
|
76
|
+
return (
|
|
77
|
+
<EmptyState
|
|
78
|
+
className={className}
|
|
79
|
+
compact
|
|
80
|
+
icon={<Telescope />}
|
|
81
|
+
title="No intelligence for this company"
|
|
82
|
+
description={target.primary
|
|
83
|
+
? `${brand.linkLabel} researches your own website (${target.primary}). It has no profile, news or technology data for other companies, and this one is not among the competitors your research found.`
|
|
84
|
+
: `${brand.linkLabel} researches your own website, and that research has not run yet. It has no profile, news or technology data for other companies.`}
|
|
85
|
+
action={onOpenIntelligence ? <button type="button" className="text-sm underline underline-offset-2" onClick={onOpenIntelligence}>Open company intelligence</button> : undefined}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CompanyProfileCard: the company profile the research collected (industry, size, funding, links) with a one-line count of
|
|
5
|
+
* what else was found (products, keywords, competitors, articles). Mount it alone on any page.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. getIntelligencePrimaryWebsite GET /intelligence/primary-website (read, through useResearchTarget) used when no `website` prop is given
|
|
9
|
+
* 2. getAllIntelligenceData GET /intelligence/all/{website_url} (read) every source collected for the website
|
|
10
|
+
* Nothing here is billable. A source that was not collected is said to be missing, never shown as zero.
|
|
11
|
+
*/
|
|
12
|
+
import { Building2 } from "lucide-react";
|
|
13
|
+
import { Skeleton } from "@graph8/sdk/ui";
|
|
14
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
15
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
16
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
17
|
+
import { EntityAvatar } from "@/g8/components/screen/entity-avatar";
|
|
18
|
+
import { plural } from "@/g8/lib/plural";
|
|
19
|
+
import { entryData, useAllIntelligence, useResearchTarget } from "../hooks/use-intelligence";
|
|
20
|
+
import { asArray, canonicalDomain, safeHttpUrl, text, whenDate } from "../lib/intelligence-types";
|
|
21
|
+
import { CompanyProfileFields } from "./source-view";
|
|
22
|
+
|
|
23
|
+
export interface CompanyProfileCardProps {
|
|
24
|
+
/** A website or host. Default: the workspace's research website. */
|
|
25
|
+
website?: string | null;
|
|
26
|
+
/** Called with a source type when a person asks for the full source. */
|
|
27
|
+
onOpenSource?: (dataType: string) => void;
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const FOUND: Array<[string, string, string, string]> = [
|
|
32
|
+
["product_inventory", "products", "product", "Products"],
|
|
33
|
+
["company_keywords", "keywords", "keyword", "Keywords"],
|
|
34
|
+
["competitor_discovery", "competitors", "competitor", "Competitors"],
|
|
35
|
+
["brand_news", "articles", "article", "News"],
|
|
36
|
+
["audience_questions", "questions", "audience question", "Audience questions"],
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export function CompanyProfileCard({ website, onOpenSource, className }: CompanyProfileCardProps) {
|
|
40
|
+
const target = useResearchTarget();
|
|
41
|
+
const host = canonicalDomain(website ?? target.primary) || null;
|
|
42
|
+
const all = useAllIntelligence(host);
|
|
43
|
+
if (target.loading && !website) return <Skeleton className="h-40 w-full" />;
|
|
44
|
+
if (!host) return <EmptyState title="No research website yet" description="Start the website research to build the company profile." compact icon={<Building2 />} />;
|
|
45
|
+
if (all.error) return <ApiError error={all.error} title="Could not load the company profile" onRetry={() => void all.refetch()} />;
|
|
46
|
+
if (all.loading || !all.entries) return <Skeleton className="h-40 w-full" />;
|
|
47
|
+
const profile = entryData(all.entries.company_enrichment);
|
|
48
|
+
const logo = safeHttpUrl(entryData(all.entries.visual_assets)?.logo_url);
|
|
49
|
+
const name = text(profile?.company_name) ?? host;
|
|
50
|
+
const collected = whenDate(all.entries.company_enrichment?.collected_at);
|
|
51
|
+
return (
|
|
52
|
+
<SectionCard className={className} title={name} description={host + (collected ? `, collected ${collected}` : "")} icon={<EntityAvatar name={name} imageUrl={logo ?? undefined} kind="company" size="sm" />}>
|
|
53
|
+
{profile ? <CompanyProfileFields data={profile} /> : <p className="text-sm text-muted-foreground">The company profile was not collected for this website.</p>}
|
|
54
|
+
<ul className="mt-3 flex flex-wrap gap-x-4 gap-y-1 border-t border-border pt-3 text-xs text-muted-foreground">
|
|
55
|
+
{FOUND.map(([type, key, noun, label]) => {
|
|
56
|
+
const data = entryData(all.entries?.[type]);
|
|
57
|
+
if (!data) return <li key={type}>{label}: not collected</li>;
|
|
58
|
+
const n = asArray(data[key]).length;
|
|
59
|
+
return (
|
|
60
|
+
<li key={type}>
|
|
61
|
+
{onOpenSource && n ? <button type="button" className="underline underline-offset-2" onClick={() => onOpenSource(type)}>{plural(n, noun)}</button> : plural(n, noun)}
|
|
62
|
+
</li>
|
|
63
|
+
);
|
|
64
|
+
})}
|
|
65
|
+
</ul>
|
|
66
|
+
</SectionCard>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* IntelligenceSources: the list of data sources the research collected (company profile, products, keywords, competitors,
|
|
5
|
+
* news, audience questions, brand assets ...), plus the sources of the last run that failed or have not finished, so a
|
|
6
|
+
* missing source is visible and can be re-collected. Click a row to open it.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. listIntelligenceDocuments GET /intelligence/documents (read) the collected sources
|
|
10
|
+
* 2. getLatestIntelligenceTask GET /intelligence/latest-task (read) the last run
|
|
11
|
+
* 3. getIntelligenceStatus GET /intelligence/status/{task_id} (read) which steps of that run did not complete
|
|
12
|
+
* The API does not page this list (at most one row per source type); the header says how many there are.
|
|
13
|
+
*/
|
|
14
|
+
import { Database } from "lucide-react";
|
|
15
|
+
import { Skeleton } from "@graph8/sdk/ui";
|
|
16
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
17
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
18
|
+
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
19
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
20
|
+
import { plural } from "@/g8/lib/plural";
|
|
21
|
+
import { useLatestRun, useRunStatus, useSourceDocuments } from "../hooks/use-intelligence";
|
|
22
|
+
import { humanize, runSteps, stepTone, text, titleize, whenDate } from "../lib/intelligence-types";
|
|
23
|
+
|
|
24
|
+
export interface SourceRow { dataType: string; label: string; folder: string | null; collectedAt: string | null; state: string }
|
|
25
|
+
|
|
26
|
+
export interface IntelligenceSourcesProps {
|
|
27
|
+
/** Called when a person opens a source. */
|
|
28
|
+
onSelect?: (source: SourceRow) => void;
|
|
29
|
+
/** The open source's type, to mark its row. */
|
|
30
|
+
selectedType?: string;
|
|
31
|
+
/** The URL param the owning screen reads for the open source, so Cmd-click opens it in a new tab. */
|
|
32
|
+
idParam?: string;
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function IntelligenceSources({ onSelect, selectedType, idParam, className }: IntelligenceSourcesProps) {
|
|
37
|
+
const { docs, error, refetch } = useSourceDocuments();
|
|
38
|
+
const { run } = useLatestRun();
|
|
39
|
+
const { status } = useRunStatus(run?.task_id);
|
|
40
|
+
if (error) return <ApiError error={error} title="Could not load the sources" onRetry={() => void refetch()} />;
|
|
41
|
+
if (!docs) return <Skeleton className="h-40 w-full" />;
|
|
42
|
+
const collected: SourceRow[] = docs.map((d) => ({ dataType: d.data_type, label: text(d.display_name) ?? humanize(d.data_type), folder: text(d.folder), collectedAt: d.collected_at ?? null, state: "completed" }));
|
|
43
|
+
const have = new Set(collected.map((r) => r.dataType));
|
|
44
|
+
const missing: SourceRow[] = runSteps(status ?? undefined).filter((s) => !have.has(s.key) && s.state !== "completed").map((s) => ({ dataType: s.key, label: titleize(s.key), folder: null, collectedAt: null, state: s.state }));
|
|
45
|
+
const rows = [...collected, ...missing];
|
|
46
|
+
if (rows.length === 0) return <EmptyState title="No sources yet" description="Start the website research and the sources it collects will appear here." icon={<Database />} />;
|
|
47
|
+
return (
|
|
48
|
+
<div className={className}>
|
|
49
|
+
<p className="mb-2 text-xs text-muted-foreground" role="status">All {plural(rows.length, "source")}{missing.length ? `, ${plural(missing.length, "source")} not collected` : ""}</p>
|
|
50
|
+
<ul className="divide-y divide-border rounded-md border border-border">
|
|
51
|
+
{rows.map((r) => (
|
|
52
|
+
<li key={r.dataType} aria-current={r.dataType === selectedType ? "true" : undefined} className="flex items-center justify-between gap-3 px-3 py-2 text-sm aria-[current=true]:bg-muted">
|
|
53
|
+
<div className="min-w-0">
|
|
54
|
+
<RecordLink id={r.dataType} param={idParam} onOpen={() => onSelect?.(r)}>{r.label}</RecordLink>
|
|
55
|
+
<div className="text-xs text-muted-foreground">{[r.folder, r.collectedAt ? `Collected ${whenDate(r.collectedAt)}` : null].filter(Boolean).join(", ") || "Not collected"}</div>
|
|
56
|
+
</div>
|
|
57
|
+
<StatusPill tone={stepTone(r.state)} quiet>{r.state === "completed" ? "Collected" : humanize(r.state)}</StatusPill>
|
|
58
|
+
</li>
|
|
59
|
+
))}
|
|
60
|
+
</ul>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -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
|
+
}
|