@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,150 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SafeMarkdown: renders the markdown the API returns (research reports, wiki documents) without ever turning it into HTML.
|
|
5
|
+
* Every piece of text becomes a React text node, so raw HTML, `<script>`, event handlers and `javascript:` links in a
|
|
6
|
+
* report show up as plain text instead of running. Links are drawn only for http(s) URLs (with `rel="noopener noreferrer"`);
|
|
7
|
+
* images are not drawn, they stay as their text. No `dangerouslySetInnerHTML` anywhere.
|
|
8
|
+
*
|
|
9
|
+
* Supported: headings, paragraphs, bullet and numbered lists, block quotes, fenced code, horizontal rules, pipe tables,
|
|
10
|
+
* and inline bold, italic, code and links. Anything else is a paragraph of plain text.
|
|
11
|
+
*
|
|
12
|
+
* SDK operations: none.
|
|
13
|
+
*/
|
|
14
|
+
import type { ReactNode } from "react";
|
|
15
|
+
import { cn } from "@graph8/sdk/ui";
|
|
16
|
+
import { safeHttpUrl } from "./intelligence-types";
|
|
17
|
+
|
|
18
|
+
// Underscore emphasis only at word edges, so snake_case_names are left alone.
|
|
19
|
+
const INLINE = /(\*\*([^*]+)\*\*|(?<!\w)__([^_]+)__(?!\w)|`([^`]+)`|\[([^\]]+)\]\(([^)\s]+)\)|\*([^*\s][^*]*)\*|(?<!\w)_([^_\s][^_]*)_(?!\w))/g;
|
|
20
|
+
|
|
21
|
+
/** Inline markup to React nodes. Unsafe link targets keep their text and lose the link. */
|
|
22
|
+
export function renderInline(source: string, keyPrefix = "i"): ReactNode[] {
|
|
23
|
+
const out: ReactNode[] = [];
|
|
24
|
+
let last = 0;
|
|
25
|
+
let n = 0;
|
|
26
|
+
for (const m of source.matchAll(INLINE)) {
|
|
27
|
+
const at = m.index ?? 0;
|
|
28
|
+
if (at > last) out.push(source.slice(last, at));
|
|
29
|
+
const key = `${keyPrefix}${n++}`;
|
|
30
|
+
if (m[2] ?? m[3]) out.push(<strong key={key}>{m[2] ?? m[3]}</strong>);
|
|
31
|
+
else if (m[4]) out.push(<code key={key} className="rounded bg-muted px-1 py-0.5 text-[0.9em]">{m[4]}</code>);
|
|
32
|
+
else if (m[5]) {
|
|
33
|
+
const href = safeHttpUrl(m[6]);
|
|
34
|
+
out.push(href ? <a key={key} href={href} target="_blank" rel="noopener noreferrer" className="underline underline-offset-2">{m[5]}</a> : <span key={key}>{m[5]}</span>);
|
|
35
|
+
} else out.push(<em key={key}>{m[7] ?? m[8]}</em>);
|
|
36
|
+
last = at + m[0].length;
|
|
37
|
+
}
|
|
38
|
+
if (last < source.length) out.push(source.slice(last));
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const HEADING = /^(#{1,6})\s+(.*)$/;
|
|
43
|
+
const BULLET = /^\s*[-*+]\s+(.*)$/;
|
|
44
|
+
const ORDERED = /^\s*\d+[.)]\s+(.*)$/;
|
|
45
|
+
const RULE = /^\s*([-*_])(\s*\1){2,}\s*$/;
|
|
46
|
+
const TABLE_ROW = /^\s*\|.*\|\s*$/;
|
|
47
|
+
const TABLE_SEP = /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/;
|
|
48
|
+
|
|
49
|
+
const cells = (line: string): string[] => line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
|
|
50
|
+
|
|
51
|
+
type Block =
|
|
52
|
+
| { kind: "heading"; level: number; text: string }
|
|
53
|
+
| { kind: "para"; text: string }
|
|
54
|
+
| { kind: "ul" | "ol"; items: string[] }
|
|
55
|
+
| { kind: "quote"; text: string }
|
|
56
|
+
| { kind: "code"; text: string }
|
|
57
|
+
| { kind: "rule" }
|
|
58
|
+
| { kind: "table"; head: string[]; rows: string[][] };
|
|
59
|
+
|
|
60
|
+
/** Split markdown into blocks. Pure, so it is tested without rendering. */
|
|
61
|
+
export function parseBlocks(markdown: string): Block[] {
|
|
62
|
+
const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
|
|
63
|
+
const blocks: Block[] = [];
|
|
64
|
+
let i = 0;
|
|
65
|
+
while (i < lines.length) {
|
|
66
|
+
const line = lines[i];
|
|
67
|
+
if (!line.trim()) { i++; continue; }
|
|
68
|
+
if (/^\s*```/.test(line)) {
|
|
69
|
+
const body: string[] = [];
|
|
70
|
+
for (i++; i < lines.length && !/^\s*```/.test(lines[i]); i++) body.push(lines[i]);
|
|
71
|
+
i++;
|
|
72
|
+
blocks.push({ kind: "code", text: body.join("\n") });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const h = HEADING.exec(line);
|
|
76
|
+
if (h) { blocks.push({ kind: "heading", level: h[1].length, text: h[2].trim() }); i++; continue; }
|
|
77
|
+
if (RULE.test(line)) { blocks.push({ kind: "rule" }); i++; continue; }
|
|
78
|
+
if (TABLE_ROW.test(line) && i + 1 < lines.length && TABLE_SEP.test(lines[i + 1])) {
|
|
79
|
+
const rows: string[][] = [];
|
|
80
|
+
let j = i + 2;
|
|
81
|
+
for (; j < lines.length && TABLE_ROW.test(lines[j]); j++) rows.push(cells(lines[j]));
|
|
82
|
+
blocks.push({ kind: "table", head: cells(line), rows });
|
|
83
|
+
i = j;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (BULLET.test(line) || ORDERED.test(line)) {
|
|
87
|
+
const ordered = ORDERED.test(line) && !BULLET.test(line);
|
|
88
|
+
const items: string[] = [];
|
|
89
|
+
for (; i < lines.length; i++) {
|
|
90
|
+
const m = (ordered ? ORDERED : BULLET).exec(lines[i]);
|
|
91
|
+
if (!m) break;
|
|
92
|
+
items.push(m[1]);
|
|
93
|
+
}
|
|
94
|
+
blocks.push({ kind: ordered ? "ol" : "ul", items });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (/^\s*>/.test(line)) {
|
|
98
|
+
const body: string[] = [];
|
|
99
|
+
for (; i < lines.length && /^\s*>/.test(lines[i]); i++) body.push(lines[i].replace(/^\s*>\s?/, ""));
|
|
100
|
+
blocks.push({ kind: "quote", text: body.join(" ") });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const para: string[] = [];
|
|
104
|
+
for (; i < lines.length && lines[i].trim() && !HEADING.test(lines[i]) && !BULLET.test(lines[i]) && !ORDERED.test(lines[i]) && !/^\s*(```|>)/.test(lines[i]); i++) para.push(lines[i].trim());
|
|
105
|
+
blocks.push({ kind: "para", text: para.join(" ") });
|
|
106
|
+
}
|
|
107
|
+
return blocks;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const HEADING_CLASS = ["", "text-lg font-semibold", "text-base font-semibold", "text-sm font-semibold", "text-sm font-medium", "text-sm font-medium", "text-sm font-medium"];
|
|
111
|
+
|
|
112
|
+
function BlockView({ block, k }: { block: Block; k: number }): ReactNode {
|
|
113
|
+
switch (block.kind) {
|
|
114
|
+
case "heading": {
|
|
115
|
+
const Tag = `h${Math.min(block.level + 1, 6)}` as "h2";
|
|
116
|
+
return <Tag className={cn("mt-4 text-foreground first:mt-0", HEADING_CLASS[block.level])}>{renderInline(block.text, `h${k}-`)}</Tag>;
|
|
117
|
+
}
|
|
118
|
+
case "ul":
|
|
119
|
+
case "ol": {
|
|
120
|
+
const Tag = block.kind;
|
|
121
|
+
return <Tag className={cn("my-2 space-y-1 pl-5", block.kind === "ul" ? "list-disc" : "list-decimal")}>{block.items.map((t, n) => <li key={n}>{renderInline(t, `l${k}-${n}-`)}</li>)}</Tag>;
|
|
122
|
+
}
|
|
123
|
+
case "quote": return <blockquote className="my-2 border-l-2 border-border pl-3 text-muted-foreground">{renderInline(block.text, `q${k}-`)}</blockquote>;
|
|
124
|
+
case "code": return <pre className="my-2 overflow-x-auto rounded-md bg-muted p-3 text-xs"><code>{block.text}</code></pre>;
|
|
125
|
+
case "rule": return <hr className="my-3 border-border" />;
|
|
126
|
+
case "table":
|
|
127
|
+
return (
|
|
128
|
+
<div className="my-2 overflow-x-auto">
|
|
129
|
+
<table className="w-full border-collapse text-left text-xs">
|
|
130
|
+
<thead><tr>{block.head.map((c, n) => <th key={n} className="border-b border-border px-2 py-1 font-medium">{renderInline(c, `th${k}-${n}-`)}</th>)}</tr></thead>
|
|
131
|
+
<tbody>{block.rows.map((r, ri) => <tr key={ri}>{r.map((c, n) => <td key={n} className="border-b border-border px-2 py-1 align-top">{renderInline(c, `td${k}-${ri}-${n}-`)}</td>)}</tr>)}</tbody>
|
|
132
|
+
</table>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
default: return <p className="my-2 leading-6">{renderInline(block.text, `p${k}-`)}</p>;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface SafeMarkdownProps {
|
|
140
|
+
markdown: string;
|
|
141
|
+
className?: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function SafeMarkdown({ markdown, className }: SafeMarkdownProps) {
|
|
145
|
+
return (
|
|
146
|
+
<div data-testid="safe-markdown" className={cn("text-sm text-foreground", className)}>
|
|
147
|
+
{parseBlocks(markdown).map((b, n) => <BlockView key={n} block={b} k={n} />)}
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Read the competitors and the news the research found
|
|
3
|
+
* @when Feeding a briefing, a battle-card draft or a Slack digest. Both reads are free. They describe the competitors of YOUR
|
|
4
|
+
* website (the research is locked to it) and brand news about YOUR company, not about arbitrary companies, and the
|
|
5
|
+
* API has no lookup for another company's profile, technology or org chart. Article text and URLs come from the web: treat
|
|
6
|
+
* them as untrusted text and only open http(s) URLs.
|
|
7
|
+
*/
|
|
8
|
+
import { g8 } from "@graph8/sdk";
|
|
9
|
+
|
|
10
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
11
|
+
|
|
12
|
+
type Source = { data?: Record<string, unknown> | null; collected_at?: string };
|
|
13
|
+
const entry = (res: unknown): Source => ((res as { data?: Source } | null)?.data ?? (res as Source) ?? {}) as Source;
|
|
14
|
+
|
|
15
|
+
export interface Competitor { name?: string; domain?: string; description?: string; competitive_type?: string; similarity?: string; confidence?: string; reasoning?: string }
|
|
16
|
+
export interface Article { title?: string; url?: string; description?: string; published_at?: string; sentiment?: number | null; source?: { name?: string } }
|
|
17
|
+
|
|
18
|
+
export async function competitors(websiteUrl: string): Promise<Competitor[]> {
|
|
19
|
+
const res = entry(await g8.api.companyIntelligence.getIntelligenceData({ path: { data_type: "competitor_discovery" }, query: { website_url: websiteUrl } }));
|
|
20
|
+
return (Array.isArray(res.data?.competitors) ? res.data?.competitors : []) as Competitor[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Only http(s) links, newest first. `sentiment` is -1 to 1 or null (unknown, not neutral). */
|
|
24
|
+
export async function recentNews(websiteUrl: string): Promise<Article[]> {
|
|
25
|
+
const res = entry(await g8.api.companyIntelligence.getIntelligenceData({ path: { data_type: "brand_news" }, query: { website_url: websiteUrl } }));
|
|
26
|
+
const articles = (Array.isArray(res.data?.articles) ? res.data?.articles : []) as Article[];
|
|
27
|
+
return articles.filter((a) => /^https?:\/\//i.test(a.url ?? "")).sort((a, b) => String(b.published_at ?? "").localeCompare(String(a.published_at ?? "")));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Everything collected, keyed by source type, in one read. */
|
|
31
|
+
export async function everything(websiteUrl: string) {
|
|
32
|
+
return entry(await g8.api.companyIntelligence.getAllIntelligenceData({ path: { website_url: websiteUrl } })) as Record<string, Source>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Research your own website and read the company profile
|
|
3
|
+
* @when Setting up company intelligence for a workspace. The research is locked to ONE website (the workspace's primary
|
|
4
|
+
* website, set by the first run), so this reads it first and never guesses a domain. Starting a run is BILLABLE and the API
|
|
5
|
+
* quotes no price (it is metered by usage): tell a human what will be collected and get a yes first. A run finished less than an
|
|
6
|
+
* hour ago is returned as is (no new work, no charge) unless `force` is true. Order: primary website -> analyze -> poll the
|
|
7
|
+
* run -> read the company profile.
|
|
8
|
+
*/
|
|
9
|
+
import { g8 } from "@graph8/sdk";
|
|
10
|
+
|
|
11
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
12
|
+
|
|
13
|
+
type Body = Record<string, unknown>;
|
|
14
|
+
const body = (res: unknown): Body => ((res as { data?: Body } | null)?.data ?? (res as Body) ?? {}) as Body;
|
|
15
|
+
|
|
16
|
+
/** The website the research is locked to, or null before the first run. Never invent one. */
|
|
17
|
+
export async function primaryWebsite(): Promise<string | null> {
|
|
18
|
+
const res = body(await g8.api.companyIntelligence.getIntelligencePrimaryWebsite());
|
|
19
|
+
return typeof res.primary_website_url === "string" && res.primary_website_url ? res.primary_website_url : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Billable. Only after a person approved researching `websiteUrl`. With no primary website yet, this one becomes the locked one. */
|
|
23
|
+
export async function startResearch(websiteUrl: string, force = false) {
|
|
24
|
+
const res = body(await g8.api.companyIntelligence.analyzeWebsite({ body: force ? { website_url: websiteUrl, force: true } : { website_url: websiteUrl } }));
|
|
25
|
+
return res.task_id as string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function waitForRun(taskId: string) {
|
|
29
|
+
let delay = 3000;
|
|
30
|
+
for (let i = 0; i < 100; i++) {
|
|
31
|
+
const run = body(await g8.api.companyIntelligence.getIntelligenceStatus({ path: { task_id: taskId } }));
|
|
32
|
+
if (run.status !== "pending" && run.status !== "running") return run;
|
|
33
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
34
|
+
delay = Math.min(15000, Math.round(delay * 1.3));
|
|
35
|
+
}
|
|
36
|
+
throw new Error("the research did not finish in time; read the status again later");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Free. A source that was not collected answers 404: treat that as "not collected", not as an error. */
|
|
40
|
+
export async function companyProfile(websiteUrl: string) {
|
|
41
|
+
const entry = body(await g8.api.companyIntelligence.getIntelligenceData({ path: { data_type: "company_enrichment" }, query: { website_url: websiteUrl } }));
|
|
42
|
+
return (entry.data ?? null) as Record<string, unknown> | null;
|
|
43
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Write the six research reports, follow them and approve one
|
|
3
|
+
* @when Building market research from your website. Writing a report is the most expensive single action in Studio and
|
|
4
|
+
* the API quotes no price: `generateResearchReport` is one long multi-pass run, `generateAllResearchReports` is six,
|
|
5
|
+
* `resumeResearchWorkflow` writes every report that is not finished. Read the workflow first to count them, tell a human the
|
|
6
|
+
* number, and get a yes. Approving a report indexes it into the knowledge base that campaigns and the copilot read.
|
|
7
|
+
* Order: workflow status -> generate (or retry a failed one) -> poll the workflow -> read the report -> approve.
|
|
8
|
+
*/
|
|
9
|
+
import { g8 } from "@graph8/sdk";
|
|
10
|
+
|
|
11
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
12
|
+
|
|
13
|
+
type Body = Record<string, unknown>;
|
|
14
|
+
const body = (res: unknown): Body => ((res as { data?: Body } | null)?.data ?? (res as Body) ?? {}) as Body;
|
|
15
|
+
|
|
16
|
+
export interface Slot { id: string | null; report_number: number; report_type: string; status: string; approved?: boolean }
|
|
17
|
+
|
|
18
|
+
/** Free. Always six slots (`report_1`..`report_6`), including the ones not written yet. */
|
|
19
|
+
export async function workflow(websiteUrl: string) {
|
|
20
|
+
const w = body(await g8.api.research.getResearchWorkflowStatus({ query: { website_url: websiteUrl } }));
|
|
21
|
+
const slots = Object.values((w.reports ?? {}) as Record<string, Slot>);
|
|
22
|
+
return { status: w.workflow_status as string, slots, missing: slots.filter((s) => s.status !== "completed" && s.status !== "approved" && s.status !== "generating") };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Billable. `reportNumber` is 1 to 6. */
|
|
26
|
+
export async function writeReport(websiteUrl: string, reportNumber: number) {
|
|
27
|
+
return g8.api.research.generateResearchReport({ body: { website_url: websiteUrl, report_number: reportNumber } });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Billable. Re-runs ONE failed report; `reportType` is one of the six types in the workflow slots. */
|
|
31
|
+
export async function retryFailed(websiteUrl: string, reportType: "review_sentiment" | "competitive_teardown" | "voice_of_customer" | "gtm_channel" | "analyst_media" | "buyer_psychology") {
|
|
32
|
+
return g8.api.research.retryResearchReport({ body: { website_url: websiteUrl, report_type: reportType } });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function waitForReports(websiteUrl: string) {
|
|
36
|
+
let delay = 5000;
|
|
37
|
+
for (let i = 0; i < 200; i++) {
|
|
38
|
+
const w = await workflow(websiteUrl);
|
|
39
|
+
if (!w.slots.some((s) => s.status === "generating")) return w;
|
|
40
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
41
|
+
delay = Math.min(30000, Math.round(delay * 1.3));
|
|
42
|
+
}
|
|
43
|
+
throw new Error("the reports did not finish in time; read the workflow again later");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Free. The markdown is model output: render it as text, never as HTML. */
|
|
47
|
+
export async function readReport(reportId: string) {
|
|
48
|
+
return body(await g8.api.research.getResearchReport({ path: { report_id: reportId } }));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Free, but it publishes the report to everything that reads the knowledge base. */
|
|
52
|
+
export async function approve(reportId: string) {
|
|
53
|
+
return g8.api.research.approveResearchReport({ path: { report_id: reportId } });
|
|
54
|
+
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { useState } from "react";
|
|
17
17
|
import { Button, Input, Skeleton } from "@graph8/sdk/ui";
|
|
18
18
|
import { ApiError } from "@/g8/components/api-error";
|
|
19
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
19
20
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
20
21
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
21
22
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
@@ -30,6 +31,8 @@ export interface CopilotMemoryProps {
|
|
|
30
31
|
className?: string;
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
/** The memory API has no paging: a full read is announced under the list. */
|
|
35
|
+
const MEMORY_LIMIT = 100;
|
|
33
36
|
const idOf = (m: MemoryRow): string => m.memory_id ?? m.id ?? "";
|
|
34
37
|
|
|
35
38
|
export function CopilotMemory({ scope = "user", className }: CopilotMemoryProps) {
|
|
@@ -37,7 +40,7 @@ export function CopilotMemory({ scope = "user", className }: CopilotMemoryProps)
|
|
|
37
40
|
const [forgetting, setForgetting] = useState<MemoryRow | null>(null);
|
|
38
41
|
const [showTools, setShowTools] = useState(false);
|
|
39
42
|
|
|
40
|
-
const memories = useG8Query((signal) => g8Get("/copilot/memory", { scope, limit:
|
|
43
|
+
const memories = useG8Query((signal) => g8Get("/copilot/memory", { scope, limit: MEMORY_LIMIT }, { signal }), [scope]);
|
|
41
44
|
const tools = useG8Query((signal) => g8Get("/copilot/tools", undefined, { signal }), [], { enabled: showTools });
|
|
42
45
|
const add = useG8Mutation(() => g8Post("/copilot/memory", { content: note.trim(), scope }), { onSuccess: () => { setNote(""); void memories.refetch(); } });
|
|
43
46
|
const forget = useG8Mutation((id: string) => g8Delete(`/copilot/memory/${id}`), { onSuccess: () => { setForgetting(null); void memories.refetch(); } });
|
|
@@ -62,6 +65,7 @@ export function CopilotMemory({ scope = "user", className }: CopilotMemoryProps)
|
|
|
62
65
|
</li>
|
|
63
66
|
))}
|
|
64
67
|
</ul>
|
|
68
|
+
<FirstNotice shown={rows.length} limit={MEMORY_LIMIT} noun="memories" />
|
|
65
69
|
{!memories.loading && !memories.error && rows.length === 0 ? <EmptyState title="Nothing remembered yet" description="Add the rules and facts you want in every answer." /> : null}
|
|
66
70
|
{forget.error ? <ApiError error={forget.error} title="Could not forget it" /> : null}
|
|
67
71
|
|
|
@@ -73,7 +77,7 @@ export function CopilotMemory({ scope = "user", className }: CopilotMemoryProps)
|
|
|
73
77
|
{toolRows.map((t) => (
|
|
74
78
|
<li key={t.name} className="flex items-start justify-between gap-3 p-2 text-sm">
|
|
75
79
|
<span><span className="font-medium">{t.name}</span>{t.description ? <span className="block text-xs text-muted-foreground">{t.description}</span> : null}</span>
|
|
76
|
-
{t.requires_approval || t.needs_approval ? <span className="text-xs text-
|
|
80
|
+
{t.requires_approval || t.needs_approval ? <span className="text-xs text-warning-foreground">needs approval</span> : null}
|
|
77
81
|
</li>
|
|
78
82
|
))}
|
|
79
83
|
</ul>
|
|
@@ -153,7 +153,7 @@ export function CopilotPanel({ context, initialTab = "ask", confirmEveryMessage
|
|
|
153
153
|
<div key={t.id} data-testid={`turn-${t.role}`} className={`max-w-[85%] rounded-lg px-3 py-2 text-sm ${t.role === "user" ? "self-end bg-primary text-primary-foreground" : t.error ? "self-start border border-destructive" : "self-start bg-muted"}`}>
|
|
154
154
|
<p className="whitespace-pre-wrap">{t.text}</p>
|
|
155
155
|
{t.tools?.length ? <p className="mt-1 text-xs opacity-70">Used: {t.tools.join(", ")}</p> : null}
|
|
156
|
-
{t.needsApproval?.length ? <p className="mt-1 text-xs text-
|
|
156
|
+
{t.needsApproval?.length ? <p className="mt-1 text-xs text-warning-foreground">Needs your approval in {brand.linkLabel} before it runs: {t.needsApproval.join(", ")}</p> : null}
|
|
157
157
|
{t.sources ? <p className="mt-1 text-xs opacity-70">{t.sources} sources</p> : null}
|
|
158
158
|
</div>
|
|
159
159
|
))}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { useEffect, useRef, useState } from "react";
|
|
20
20
|
import { Button, Input } from "@graph8/sdk/ui";
|
|
21
21
|
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
22
23
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
23
24
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
24
25
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
@@ -43,6 +44,9 @@ interface Page { conversation_id?: string; messages?: WorkMessageRow[] }
|
|
|
43
44
|
|
|
44
45
|
const newKey = (): string => `team-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
45
46
|
|
|
47
|
+
/** The agents roster read has no paging here: a full read is announced under the composer. */
|
|
48
|
+
const AGENT_LIMIT = 50;
|
|
49
|
+
|
|
46
50
|
export function CopilotTeamChat({ channelId, pollMs = 4000, hideChannelPicker, className }: CopilotTeamChatProps) {
|
|
47
51
|
const [channel, setChannel] = useState<string | undefined>(channelId);
|
|
48
52
|
const [text, setText] = useState("");
|
|
@@ -51,7 +55,7 @@ export function CopilotTeamChat({ channelId, pollMs = 4000, hideChannelPicker, c
|
|
|
51
55
|
const draftKey = useRef(newKey());
|
|
52
56
|
|
|
53
57
|
const channels = useG8Query((signal) => g8Get("/work/channels", undefined, { signal }), [], { enabled: !(hideChannelPicker && channelId) });
|
|
54
|
-
const agents = useG8Query((signal) => g8Get("/work/agents", { limit:
|
|
58
|
+
const agents = useG8Query((signal) => g8Get("/work/agents", { limit: AGENT_LIMIT }, { signal }), []);
|
|
55
59
|
const rows = listOf<WorkChannel>(channels.data, "channels");
|
|
56
60
|
const roster = listOf<AgentRow>(agents.data, "items");
|
|
57
61
|
const active = channel ?? rows[0]?.id ?? undefined;
|
|
@@ -103,6 +107,7 @@ export function CopilotTeamChat({ channelId, pollMs = 4000, hideChannelPicker, c
|
|
|
103
107
|
onKeyDown={(e) => { if (e.key === "Enter" && text.trim() && !post.loading) submit(); }} />
|
|
104
108
|
<Button loading={post.loading} disabled={!text.trim()} onClick={submit}>Post</Button>
|
|
105
109
|
</div>
|
|
110
|
+
<FirstNotice shown={roster.length} limit={AGENT_LIMIT} noun="agents" />
|
|
106
111
|
</>
|
|
107
112
|
) : null}
|
|
108
113
|
|
package/blocks/crm/README.md
CHANGED
|
@@ -148,3 +148,9 @@ Ask the person: who may delete or merge records (decide it in the proxy's `autho
|
|
|
148
148
|
| mergeContact / mergeCompany | POST /contacts/{id}/merge, /companies/{id}/merge | destructive | DedupeReview |
|
|
149
149
|
| listAllTasks / getTask / createGlobalTask | GET /tasks, GET /tasks/{task_id}, POST /tasks | read / read / write | TasksScreen |
|
|
150
150
|
| assertContact, assertCompany (+ batch) | PUT /contacts/assert, /companies/assert | write | recipes (`g8.contacts.upsert`) |
|
|
151
|
+
|
|
152
|
+
## Cross-block glue
|
|
153
|
+
|
|
154
|
+
- `ContactLookupLink` (`components/contact-lookup-link.tsx`): "Open contact" for something that only knows an email (an inbox thread, a meeting attendee). It looks the email up (`GET /contacts?email=`) and shows a button only when a contact has it; `onOpen(contactId)` routes to your contacts screen. `ContactsScreen.onOpenCompany`, `CompaniesScreen.onOpenContact` and `TasksScreen.onOpenRecord` are the other ends of the same links: the stack pages pass them (`router.push("/companies?id=...")`).
|
|
155
|
+
|
|
156
|
+
- `RecordEngagementTab` (`components/record-engagement-tab.tsx`): engagement counts, last touch (contacts) and pages visited for a contact or company, an `extraDetailTabs` entry: `{ id: "engagement", label: "Engagement", render: (c) => <RecordEngagementTab kind="contacts" id={c.id} /> }`. Reads `getContactEngagementSummary` / `getCompanyEngagementSummary` and `getContactSitesVisited` / `getCompanySitesVisited`. Also: the contact subtitle is `contactSubtitle(contact)` ("VP of Sales at Acme"; the company is never the person's own name, and "at" is left out without a company).
|
package/blocks/crm/block.json
CHANGED
|
@@ -37,7 +37,11 @@
|
|
|
37
37
|
"get_activity_feed_activity_feed_get",
|
|
38
38
|
"get_bulk_merge_status_duplicates_bulk_merge__run_id__get",
|
|
39
39
|
"get_company_companies__company_id__get",
|
|
40
|
+
"get_company_engagement_summary_companies__company_id__engagement_summary_get",
|
|
41
|
+
"get_company_sites_visited_companies__company_id__sites_visited_get",
|
|
40
42
|
"get_contact_contacts__contact_id__get",
|
|
43
|
+
"get_contact_engagement_summary_contacts__contact_id__engagement_summary_get",
|
|
44
|
+
"get_contact_sites_visited_contacts__contact_id__sites_visited_get",
|
|
41
45
|
"get_task_tasks__task_id__get",
|
|
42
46
|
"import_contacts_imports__import_id__contacts_post",
|
|
43
47
|
"list_all_tasks_tasks_get",
|
|
@@ -296,17 +300,53 @@
|
|
|
296
300
|
"file": "components/tasks-screen.tsx",
|
|
297
301
|
"purpose": "Workspace-wide task queue: Open/Completed/All, search and filters, complete or reopen, create, edit, delete, bulk complete and delete, detail panel.",
|
|
298
302
|
"operations": [
|
|
299
|
-
"list_all_tasks_tasks_get",
|
|
300
|
-
"get_task_tasks__task_id__get",
|
|
301
303
|
"create_global_task_tasks_post",
|
|
302
|
-
"
|
|
303
|
-
"
|
|
304
|
+
"delete_task_tasks__task_id__delete",
|
|
305
|
+
"get_task_tasks__task_id__get",
|
|
306
|
+
"list_all_tasks_tasks_get",
|
|
307
|
+
"update_task_tasks__task_id__patch"
|
|
304
308
|
],
|
|
305
309
|
"tiers": [
|
|
306
310
|
"read",
|
|
307
311
|
"write",
|
|
308
312
|
"destructive"
|
|
309
313
|
]
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"name": "ListSelect",
|
|
317
|
+
"file": "components/list-select.tsx",
|
|
318
|
+
"purpose": "Audience-list dropdown, searchable and paged (the foundation ListSelect with GET /lists wired in). Replaces a select fed by one capped list read.",
|
|
319
|
+
"operations": [
|
|
320
|
+
"list_lists_lists_get"
|
|
321
|
+
],
|
|
322
|
+
"tiers": [
|
|
323
|
+
"read"
|
|
324
|
+
]
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
"name": "ContactLookupLink",
|
|
328
|
+
"file": "components/contact-lookup-link.tsx",
|
|
329
|
+
"purpose": "\"Open contact\" button for something that only knows an email (inbox thread, meeting attendee, visitor): looks the email up in the CRM and calls onOpen(contactId); renders nothing when nobody has it.",
|
|
330
|
+
"operations": [
|
|
331
|
+
"list_contacts_contacts_get"
|
|
332
|
+
],
|
|
333
|
+
"tiers": [
|
|
334
|
+
"read"
|
|
335
|
+
]
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
"name": "RecordEngagementTab",
|
|
339
|
+
"file": "components/record-engagement-tab.tsx",
|
|
340
|
+
"purpose": "Engagement counts, last touch and pages visited for one contact or company; an extraDetailTabs entry for ContactsScreen and CompaniesScreen.",
|
|
341
|
+
"operations": [
|
|
342
|
+
"get_contact_engagement_summary_contacts__contact_id__engagement_summary_get",
|
|
343
|
+
"get_company_engagement_summary_companies__company_id__engagement_summary_get",
|
|
344
|
+
"get_contact_sites_visited_contacts__contact_id__sites_visited_get",
|
|
345
|
+
"get_company_sites_visited_companies__company_id__sites_visited_get"
|
|
346
|
+
],
|
|
347
|
+
"tiers": [
|
|
348
|
+
"read"
|
|
349
|
+
]
|
|
310
350
|
}
|
|
311
351
|
],
|
|
312
352
|
"hooks": [
|
|
@@ -328,6 +368,11 @@
|
|
|
328
368
|
"list_contact_fields_fields_get",
|
|
329
369
|
"list_company_fields_fields_companies_get"
|
|
330
370
|
]
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
"name": "useAllLists",
|
|
374
|
+
"file": "hooks/use-all-lists.ts",
|
|
375
|
+
"purpose": "Every list, page after page, for lookups by id (a list's size or name); ListSelect is the picker."
|
|
331
376
|
}
|
|
332
377
|
],
|
|
333
378
|
"lib": [
|
|
@@ -338,6 +383,10 @@
|
|
|
338
383
|
{
|
|
339
384
|
"file": "lib/screen-parts.tsx",
|
|
340
385
|
"purpose": "Sorting, row selection, bulk delete and pager shared by ContactsScreen, CompaniesScreen and TasksScreen."
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
"file": "lib/read-lists.ts",
|
|
389
|
+
"purpose": "One page of audience lists (GET /lists), the read behind ListSelect and useAllLists."
|
|
341
390
|
}
|
|
342
391
|
],
|
|
343
392
|
"recipes": [
|
|
@@ -37,8 +37,9 @@ import { RecordLink } from "@/g8/components/screen/record-link";
|
|
|
37
37
|
import { plural } from "@/g8/lib/plural";
|
|
38
38
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
39
39
|
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
40
|
+
import { useUrlFilters } from "@/g8/hooks/use-url-filters";
|
|
41
|
+
import { MoreFilters, RangeFilter, TextFilter } from "@/g8/components/screen/filter-controls";
|
|
40
42
|
import { g8Delete, g8Get } from "@/g8/lib/g8-client";
|
|
41
|
-
import { useDebouncedValue } from "../hooks/use-debounced";
|
|
42
43
|
import { usePageQuery } from "../hooks/use-page-query";
|
|
43
44
|
import { companyName, errorStatus, location } from "../lib/crm-types";
|
|
44
45
|
import type { CompanyRecord, ContactRecord, ScreenActionContext } from "../lib/crm-types";
|
|
@@ -67,8 +68,10 @@ export interface CompaniesScreenProps {
|
|
|
67
68
|
className?: string;
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
const
|
|
71
|
+
/** Every filter the companies API takes: text (name, domain, industry, country, state, city, description) and four from/to ranges. */
|
|
72
|
+
const FILTER_KEYS = ["name", "domain", "industry", "country", "state", "city", "description", "employee_count_min", "employee_count_max", "revenue_min", "revenue_max", "founded_year_min", "founded_year_max", "linkedin_followers_min", "linkedin_followers_max"] as const;
|
|
73
|
+
type FilterKey = (typeof FILTER_KEYS)[number];
|
|
74
|
+
const MORE_KEYS: readonly FilterKey[] = FILTER_KEYS.filter((k) => !["name", "domain", "industry", "country"].includes(k));
|
|
72
75
|
|
|
73
76
|
function sortValue(row: CompanyRecord, key: string): string | null | undefined {
|
|
74
77
|
if (key === "name") return row.name || row.domain;
|
|
@@ -91,8 +94,9 @@ export function CompaniesScreen(props: CompaniesScreenProps) {
|
|
|
91
94
|
const parsedId = idParam ? Number(idParam) : null;
|
|
92
95
|
/** A malformed deep link (`?id=abc`) opens nothing instead of fetching /companies/NaN. */
|
|
93
96
|
const openId = parsedId != null && Number.isFinite(parsedId) ? parsedId : null;
|
|
94
|
-
|
|
95
|
-
const
|
|
97
|
+
// The filters live in the URL (`co_city=...`), so a filtered view survives reload, Back and sharing.
|
|
98
|
+
const urlFilters = useUrlFilters(FILTER_KEYS, { prefix: "co_" });
|
|
99
|
+
const { draft, filters } = urlFilters;
|
|
96
100
|
const [sheet, setSheet] = useState<"new" | "dupes" | null>(null);
|
|
97
101
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
98
102
|
const [note, setNote] = useState<string | null>(null);
|
|
@@ -103,15 +107,16 @@ export function CompaniesScreen(props: CompaniesScreenProps) {
|
|
|
103
107
|
[filters],
|
|
104
108
|
pageSize,
|
|
105
109
|
["companies", "list", "screen"],
|
|
110
|
+
urlFilters.ready,
|
|
106
111
|
);
|
|
107
112
|
const dupes = useG8Query((signal) => g8Get<{ pending?: number; count?: number }>("/duplicates/counts", { entity_type: "company" }, { signal }), [], { key: ["duplicates", "counts", "company"] });
|
|
108
113
|
const selection = useRowSelection<CompanyRecord>(JSON.stringify(filters));
|
|
109
114
|
const rows = useMemo(() => sortRows(list.rows, sort, sortValue), [list.rows, sort]);
|
|
110
|
-
const hasFilters =
|
|
115
|
+
const hasFilters = urlFilters.active;
|
|
111
116
|
const pending = dupes.data?.data.pending ?? dupes.data?.data.count;
|
|
112
117
|
const industries = new Set(list.rows.map((r) => r.industry).filter(Boolean)).size;
|
|
113
118
|
const reload = () => { void list.refetch(); void dupes.refetch(); };
|
|
114
|
-
const set = (key:
|
|
119
|
+
const set = (key: FilterKey, v: string) => urlFilters.set(key, v);
|
|
115
120
|
const allOnPage = rows.length > 0 && rows.every((r) => selection.selected.has(r.id));
|
|
116
121
|
|
|
117
122
|
const bulk = useBulkDelete("/companies", (gone, total) => {
|
|
@@ -157,7 +162,16 @@ export function CompaniesScreen(props: CompaniesScreenProps) {
|
|
|
157
162
|
<input aria-label="Filter by domain" placeholder="Domain" className="h-9 w-36 rounded-md border bg-background px-3 text-sm" value={draft.domain} onChange={(e) => set("domain", e.target.value)} />
|
|
158
163
|
<input aria-label="Filter by industry" placeholder="Industry" className="h-9 w-36 rounded-md border bg-background px-3 text-sm" value={draft.industry} onChange={(e) => set("industry", e.target.value)} />
|
|
159
164
|
<input aria-label="Filter by country" placeholder="Country" className="h-9 w-32 rounded-md border bg-background px-3 text-sm" value={draft.country} onChange={(e) => set("country", e.target.value)} />
|
|
160
|
-
|
|
165
|
+
<MoreFilters activeCount={MORE_KEYS.filter((k) => draft[k]).length}>
|
|
166
|
+
<TextFilter label="state" value={draft.state} onChange={(v) => set("state", v)} className="w-28" />
|
|
167
|
+
<TextFilter label="city" value={draft.city} onChange={(v) => set("city", v)} className="w-32" />
|
|
168
|
+
<TextFilter label="description" value={draft.description} onChange={(v) => set("description", v)} className="w-44" />
|
|
169
|
+
<RangeFilter label="Employees" min={draft.employee_count_min} max={draft.employee_count_max} onMinChange={(v) => set("employee_count_min", v)} onMaxChange={(v) => set("employee_count_max", v)} />
|
|
170
|
+
<RangeFilter label="Revenue" min={draft.revenue_min} max={draft.revenue_max} onMinChange={(v) => set("revenue_min", v)} onMaxChange={(v) => set("revenue_max", v)} />
|
|
171
|
+
<RangeFilter label="Founded" min={draft.founded_year_min} max={draft.founded_year_max} onMinChange={(v) => set("founded_year_min", v)} onMaxChange={(v) => set("founded_year_max", v)} />
|
|
172
|
+
<RangeFilter label="LinkedIn followers" min={draft.linkedin_followers_min} max={draft.linkedin_followers_max} onMinChange={(v) => set("linkedin_followers_min", v)} onMaxChange={(v) => set("linkedin_followers_max", v)} />
|
|
173
|
+
</MoreFilters>
|
|
174
|
+
{hasFilters ? <Button variant="ghost" size="sm" onClick={urlFilters.clear}>Clear filters</Button> : null}
|
|
161
175
|
</>
|
|
162
176
|
}
|
|
163
177
|
/>
|
|
@@ -210,7 +224,7 @@ export function CompaniesScreen(props: CompaniesScreenProps) {
|
|
|
210
224
|
<EmptyState
|
|
211
225
|
title={hasFilters ? "No companies match these filters" : "No companies yet"}
|
|
212
226
|
description={hasFilters ? "Try fewer filters." : "Add a company, or import contacts and their companies are created from their domains."}
|
|
213
|
-
action={hasFilters ? <Button variant="outline" onClick={
|
|
227
|
+
action={hasFilters ? <Button variant="outline" onClick={urlFilters.clear}>Clear filters</Button> : <Button onClick={() => setSheet("new")}>New company</Button>}
|
|
214
228
|
/>
|
|
215
229
|
) : null}
|
|
216
230
|
<Pager page={list.page} pageCount={list.pageCount} total={list.total} noun="companies" hasNext={list.hasNext} loading={list.loading} onPage={list.setPage} />
|
|
@@ -66,9 +66,9 @@ export function CompanyDetail({ companyId, onDeleted, onOpenContact, renderActio
|
|
|
66
66
|
return (
|
|
67
67
|
<div className={`flex flex-col gap-4 ${className ?? ""}`}>
|
|
68
68
|
<header className="flex flex-wrap items-center gap-3">
|
|
69
|
-
<div aria-hidden className="flex size-12 items-center justify-center rounded-md bg-muted text-sm font-
|
|
69
|
+
<div aria-hidden className="flex size-12 items-center justify-center rounded-md bg-muted text-sm font-medium">{initials(name)}</div>
|
|
70
70
|
<div className="min-w-0 flex-1">
|
|
71
|
-
<h2 className="truncate text-lg font-
|
|
71
|
+
<h2 className="truncate text-lg font-medium">{name}</h2>
|
|
72
72
|
<p className="truncate text-sm text-muted-foreground">{[company.domain, company.industry].filter(Boolean).join(" - ") || "No domain or industry"}</p>
|
|
73
73
|
</div>
|
|
74
74
|
<div className="flex flex-wrap items-center gap-2">
|
|
@@ -25,7 +25,7 @@ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
|
25
25
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
26
26
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
27
27
|
import { g8Delete, g8Get } from "@/g8/lib/g8-client";
|
|
28
|
-
import { contactName, initials, isMasked, location } from "../lib/crm-types";
|
|
28
|
+
import { contactName, contactSubtitle, initials, isMasked, location } from "../lib/crm-types";
|
|
29
29
|
import type { ContactRecord } from "../lib/crm-types";
|
|
30
30
|
import { ActivityTimeline } from "./activity-timeline";
|
|
31
31
|
import { NotesPanel } from "./notes-panel";
|
|
@@ -75,11 +75,11 @@ export function ContactDetail({ contactId, onDeleted, onOpenCompany, renderActio
|
|
|
75
75
|
return (
|
|
76
76
|
<div className={`flex flex-col gap-4 ${className ?? ""}`}>
|
|
77
77
|
<header className="flex flex-wrap items-center gap-3">
|
|
78
|
-
<div aria-hidden className="flex size-12 items-center justify-center rounded-full bg-muted text-sm font-
|
|
78
|
+
<div aria-hidden className="flex size-12 items-center justify-center rounded-full bg-muted text-sm font-medium">{initials(name)}</div>
|
|
79
79
|
<div className="min-w-0 flex-1">
|
|
80
|
-
<h2 className="truncate text-lg font-
|
|
80
|
+
<h2 className="truncate text-lg font-medium">{name}</h2>
|
|
81
81
|
<p className="truncate text-sm text-muted-foreground">
|
|
82
|
-
{
|
|
82
|
+
{contactSubtitle(contact)}
|
|
83
83
|
</p>
|
|
84
84
|
</div>
|
|
85
85
|
<div className="flex flex-wrap items-center gap-2">
|