@graph8/sdk 0.370.0 → 0.374.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -3
- package/blocks/_foundation/README.md +61 -0
- package/blocks/_foundation/block.json +57 -0
- package/blocks/_foundation/components/auth-gate.tsx +363 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +1 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/auth.ts +815 -0
- package/blocks/agents/README.md +81 -4
- package/blocks/agents/block.json +311 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +17 -3
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +20 -3
- package/blocks/agents/components/agent-runs.tsx +3 -3
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +53 -11
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/components/analytics-screen.tsx +16 -3
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/report-builder.tsx +15 -2
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +27 -0
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +29 -13
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/campaigns/README.md +1 -1
- package/blocks/campaigns/block.json +34 -1
- package/blocks/campaigns/components/campaign-assets.tsx +1 -1
- package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
- package/blocks/campaigns/components/campaign-links.tsx +180 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
- package/blocks/campaigns/components/icp-detail.tsx +11 -2
- package/blocks/campaigns/components/icp-editor.tsx +8 -3
- package/blocks/campaigns/components/persona-detail.tsx +11 -2
- package/blocks/campaigns/components/persona-editor.tsx +6 -1
- package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +165 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +1 -1
- package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
- package/blocks/crm/README.md +25 -0
- package/blocks/crm/block.json +257 -9
- package/blocks/crm/components/bulk-update.tsx +119 -0
- package/blocks/crm/components/companies-screen.tsx +23 -9
- package/blocks/crm/components/company-detail.tsx +2 -2
- package/blocks/crm/components/contact-detail.tsx +4 -4
- package/blocks/crm/components/contact-documents-tab.tsx +144 -0
- package/blocks/crm/components/contact-insights-tab.tsx +118 -0
- package/blocks/crm/components/contact-lookup-link.tsx +85 -0
- package/blocks/crm/components/contact-row-actions.tsx +43 -0
- package/blocks/crm/components/contact-tag-chips.tsx +113 -0
- package/blocks/crm/components/contacts-screen.tsx +77 -18
- package/blocks/crm/components/dedupe-review.tsx +6 -1
- package/blocks/crm/components/import-wizard.tsx +3 -7
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/owner-assign.tsx +132 -0
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +1 -1
- package/blocks/crm/components/record-open-links.tsx +79 -0
- package/blocks/crm/components/tag-assign.tsx +112 -0
- package/blocks/crm/components/tag-manager.tsx +178 -0
- package/blocks/crm/components/tasks-screen.tsx +35 -15
- package/blocks/crm/components/unlock-info-button.tsx +82 -0
- package/blocks/crm/components/verify-email-button.tsx +93 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-contact-tags.ts +29 -0
- package/blocks/crm/hooks/use-page-query.ts +3 -1
- package/blocks/crm/lib/crm-types.ts +14 -0
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/record-actions.ts +102 -0
- package/blocks/deals/README.md +25 -6
- package/blocks/deals/block.json +168 -2
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/create-deal-button.tsx +109 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +7 -5
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +30 -9
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +57 -22
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +1 -1
- package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +14 -5
- package/blocks/dialer/block.json +17 -2
- package/blocks/dialer/components/call-contact-button.tsx +26 -9
- package/blocks/dialer/components/call-log-screen.tsx +10 -2
- package/blocks/dialer/components/call-log.tsx +16 -4
- package/blocks/dialer/components/call-queue.tsx +3 -3
- package/blocks/dialer/components/dialer-screen.tsx +69 -27
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +58 -11
- package/blocks/dialer/components/softphone.tsx +1 -1
- package/blocks/documents/README.md +86 -0
- package/blocks/documents/block.json +378 -0
- package/blocks/documents/components/account-bar.tsx +48 -0
- package/blocks/documents/components/correspondence-form.tsx +95 -0
- package/blocks/documents/components/correspondence-import.tsx +106 -0
- package/blocks/documents/components/correspondence-panel.tsx +191 -0
- package/blocks/documents/components/documents-screen.tsx +76 -0
- package/blocks/documents/components/library-document-detail.tsx +175 -0
- package/blocks/documents/components/library-document-form.tsx +105 -0
- package/blocks/documents/components/library-folder-form.tsx +74 -0
- package/blocks/documents/components/library-panel.tsx +235 -0
- package/blocks/documents/components/library-upload.tsx +160 -0
- package/blocks/documents/components/question-row.tsx +130 -0
- package/blocks/documents/components/questionnaire-detail.tsx +197 -0
- package/blocks/documents/components/questionnaires-panel.tsx +173 -0
- package/blocks/documents/components/record-documents-tab.tsx +109 -0
- package/blocks/documents/lib/documents-types.ts +252 -0
- package/blocks/documents/lib/download.ts +64 -0
- package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
- package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
- package/blocks/documents/recipes/organize-the-library.ts +25 -0
- package/blocks/email-signatures/README.md +123 -0
- package/blocks/email-signatures/block.json +341 -0
- package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
- package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
- package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
- package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
- package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
- package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
- package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
- package/blocks/email-signatures/components/signature-policy.tsx +130 -0
- package/blocks/email-signatures/components/signature-preview.tsx +119 -0
- package/blocks/email-signatures/components/signature-report.tsx +77 -0
- package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
- package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
- package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
- package/blocks/email-signatures/lib/signatures-types.ts +392 -0
- package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
- package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
- package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
- package/blocks/enrichment/README.md +38 -10
- package/blocks/enrichment/block.json +164 -12
- package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
- package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
- package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
- package/blocks/enrichment/components/enrich-button.tsx +1 -1
- package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
- package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
- package/blocks/enrichment/components/list-pipelines.tsx +97 -0
- package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
- package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
- package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
- package/blocks/enrichment/components/waterfall-config.tsx +27 -7
- package/blocks/enrichment/lib/enrichment-types.ts +51 -0
- package/blocks/enrichment/lib/pipeline-types.ts +234 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/forms/README.md +10 -0
- package/blocks/forms/block.json +107 -0
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +1 -1
- package/blocks/forms/components/form-embed.tsx +1 -1
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +57 -20
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +15 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +45 -4
- package/blocks/inbox/block.json +145 -8
- package/blocks/inbox/components/blocked-senders.tsx +103 -0
- package/blocks/inbox/components/compose-message.tsx +193 -0
- package/blocks/inbox/components/inbox-screen.tsx +54 -6
- package/blocks/inbox/components/inbox-search.tsx +106 -0
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/tag-manager.tsx +136 -0
- package/blocks/inbox/components/thread-actions.tsx +133 -0
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +3 -1
- package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
- package/blocks/inbox/lib/inbox-manage.ts +227 -0
- package/blocks/integrations/README.md +54 -1
- package/blocks/integrations/block.json +189 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +20 -1
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +4 -4
- package/blocks/integrations/components/integrations-screen.tsx +20 -3
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +6 -6
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +26 -7
- package/blocks/integrations/components/webhook-manager.tsx +30 -17
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +5 -0
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +8 -8
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lists/README.md +7 -1
- package/blocks/lists/block.json +31 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +17 -7
- package/blocks/lists/components/list-picker.tsx +13 -3
- package/blocks/lists/components/list-table.tsx +1 -1
- package/blocks/lists/components/lists-screen.tsx +4 -2
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +27 -6
- package/blocks/lists/components/suppressions-screen.tsx +21 -12
- package/blocks/mailboxes/block.json +4 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
- package/blocks/mailboxes/components/domain-detail.tsx +10 -4
- package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
- package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
- package/blocks/mailboxes/components/warmup-health.tsx +20 -20
- package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +117 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +38 -13
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/components/nurture-builder.tsx +10 -2
- package/blocks/nurture/components/nurture-enroll.tsx +6 -7
- package/blocks/nurture/components/nurture-form.tsx +5 -8
- package/blocks/nurture/components/nurture-screen.tsx +16 -5
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/objects/README.md +1 -1
- package/blocks/objects/block.json +13 -5
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-record-detail.tsx +8 -1
- package/blocks/objects/components/object-schema-editor.tsx +1 -1
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +123 -2
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/programmatic-pages/README.md +82 -0
- package/blocks/programmatic-pages/block.json +292 -0
- package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
- package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
- package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
- package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
- package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
- package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
- package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
- package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
- package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
- package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
- package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
- package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
- package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
- package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
- package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
- package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
- package/blocks/quotes/README.md +38 -1
- package/blocks/quotes/block.json +170 -6
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/manual-products.tsx +144 -0
- package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-documents.tsx +94 -0
- package/blocks/quotes/components/quote-evidence.tsx +95 -0
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +28 -8
- package/blocks/quotes/components/quote-related.tsx +3 -2
- package/blocks/quotes/components/quote-settings.tsx +184 -0
- package/blocks/quotes/components/quote-templates.tsx +189 -0
- package/blocks/quotes/components/quote-viewer.tsx +23 -5
- package/blocks/quotes/components/quotes-screen.tsx +15 -5
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/quotes/lib/quote-admin.ts +264 -0
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-screen.tsx +2 -2
- package/blocks/sequencer/README.md +61 -0
- package/blocks/sequencer/block.json +200 -6
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
- package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +32 -23
- package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
- package/blocks/sequencer/components/sequence-detail.tsx +10 -2
- package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
- package/blocks/sequencer/components/sequence-list.tsx +1 -1
- package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
- package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
- package/blocks/sequencer/components/sequence-stats.tsx +8 -8
- package/blocks/sequencer/components/sequence-templates.tsx +160 -0
- package/blocks/sequencer/components/sequences-screen.tsx +39 -6
- package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
- package/blocks/sequencer/components/step-editor.tsx +21 -12
- package/blocks/sequencer/components/step-templates.tsx +224 -0
- package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
- package/blocks/signals/README.md +39 -0
- package/blocks/signals/block.json +287 -0
- package/blocks/signals/components/auto-seed-panel.tsx +111 -0
- package/blocks/signals/components/listener-form.tsx +184 -0
- package/blocks/signals/components/matched-posts.tsx +195 -0
- package/blocks/signals/components/post-engagements.tsx +114 -0
- package/blocks/signals/components/record-social-tab.tsx +49 -0
- package/blocks/signals/components/social-listeners-screen.tsx +250 -0
- package/blocks/signals/components/social-settings.tsx +128 -0
- package/blocks/signals/components/source-lists.tsx +83 -0
- package/blocks/signals/components/top-engagers.tsx +68 -0
- package/blocks/signals/components/x-integration.tsx +129 -0
- package/blocks/signals/lib/signals-types.ts +228 -0
- package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
- package/blocks/signals/recipes/spend-against-caps.ts +14 -0
- package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
- package/blocks/social-ads/README.md +8 -0
- package/blocks/social-ads/block.json +137 -4
- package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
- package/blocks/social-ads/components/audience-sync.tsx +6 -2
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +6 -1
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +6 -1
- package/blocks/social-ads/components/creative-gallery.tsx +20 -2
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +4 -2
- package/blocks/social-ads/components/post-detail.tsx +3 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-screen.tsx +33 -6
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/components/snippet-install.tsx +18 -9
- package/blocks/visitors/components/traffic-chart.tsx +7 -6
- package/blocks/visitors/components/visitor-company-card.tsx +24 -14
- package/blocks/visitors/components/visitor-feed.tsx +5 -5
- package/blocks/web-analytics/README.md +102 -0
- package/blocks/web-analytics/block.json +292 -0
- package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
- package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
- package/blocks/web-analytics/components/audience-tab.tsx +135 -0
- package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
- package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
- package/blocks/web-analytics/components/overview-tab.tsx +102 -0
- package/blocks/web-analytics/components/pages-tab.tsx +148 -0
- package/blocks/web-analytics/components/ranked-table.tsx +103 -0
- package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
- package/blocks/web-analytics/components/report-card.tsx +71 -0
- package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
- package/blocks/web-analytics/hooks/use-report.ts +48 -0
- package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
- package/blocks/web-analytics/lib/page-path.ts +14 -0
- package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
- package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
- package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
- package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
- package/blocks/webchat/components/chat-widget-config.tsx +1 -1
- package/blocks/webchat/components/conversation-list.tsx +1 -1
- package/blocks/workbench/README.md +54 -2
- package/blocks/workbench/block.json +234 -5
- package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
- package/blocks/workbench/components/workbench-automation.tsx +36 -0
- package/blocks/workbench/components/workbench-connections.tsx +136 -0
- package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
- package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
- package/blocks/workbench/components/workbench-grid.tsx +71 -8
- package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
- package/blocks/workbench/components/workbench-schedule.tsx +191 -0
- package/blocks/workbench/components/workbench-screen.tsx +65 -22
- package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
- package/blocks/workbench/components/workbench-triggers.tsx +178 -0
- package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
- package/blocks/workbench/lib/api-status.ts +8 -0
- package/blocks/workbench/lib/automation-types.ts +257 -0
- package/blocks/workflows/components/workflow-editor.tsx +1 -1
- package/dist/cli.js +744 -394
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +1 -1
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +1 -1
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/auth.d.ts +45 -0
- package/dist/types/blocks/brand.d.ts +11 -2
- package/dist/types/blocks/create.d.ts +11 -4
- package/dist/types/blocks/doctor.d.ts +1 -0
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/version.d.ts +2 -2
- package/docs/api/desk.d.ts +2 -2
- package/docs/api/desk.md +3 -3
- package/docs/api/forms.md +1 -1
- package/package.json +1 -1
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +37 -4
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
- package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +24 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +68 -9
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +8 -4
- package/stacks/clay/pages/companies.tsx.tmpl +17 -1
- package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
- package/stacks/clay/pages/lists.tsx.tmpl +24 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +16 -3
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +34 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +23 -6
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +19 -4
- package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
- package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
- package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
- package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +18 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
- package/stacks/hubspot.json +66 -7
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +18 -4
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
- package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +22 -6
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +18 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +38 -9
- package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
- package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +14 -2
- package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
- package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
- package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +17 -3
- package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +36 -3
- package/styles/styles.css +1 -1
- package/templates/auth/auth-route.ts.tmpl +16 -0
- package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
- package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
- package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
- package/templates/auth/g8.auth.password.ts.tmpl +18 -0
- package/templates/auth/g8.auth.session.ts.tmpl +23 -0
- package/templates/auth/middleware.ts.tmpl +14 -0
- package/templates/next-app/README.md.tmpl +5 -0
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../block.schema.json",
|
|
3
|
+
"id": "signals",
|
|
4
|
+
"title": "Signals (social listening)",
|
|
5
|
+
"summary": "Watch X, LinkedIn, Reddit, Instagram and YouTube for posts about you, your competitors and your keywords: listeners, matched posts, engagers, starter seeds, spend limits and your own X app.",
|
|
6
|
+
"features": [
|
|
7
|
+
"create, edit, pause, resume and delete listeners with a cost estimate and a monthly cap",
|
|
8
|
+
"posts matched by every listener, triaged, saved as contacts or added to a sequence (confirmed)",
|
|
9
|
+
"who engaged with a post, turned into a list (confirmed)",
|
|
10
|
+
"the accounts that engage most",
|
|
11
|
+
"a listener per profile on a list",
|
|
12
|
+
"starter listeners from your keywords, brand, competitors and buyer profile",
|
|
13
|
+
"org-wide spend caps, per-channel ceilings and contact capture",
|
|
14
|
+
"connect or remove your own X developer app",
|
|
15
|
+
"a contact's matched posts as a tab"
|
|
16
|
+
],
|
|
17
|
+
"dependsOn": [
|
|
18
|
+
"intent"
|
|
19
|
+
],
|
|
20
|
+
"operations": [
|
|
21
|
+
"add_engagers_to_list_social_listeners_posts__post_id__engagements_add_to_list_post",
|
|
22
|
+
"add_post_author_to_sequence_social_listeners_posts__post_id__add_to_sequence_post",
|
|
23
|
+
"auto_seed_all_listeners_social_listeners_auto_seed_all_post",
|
|
24
|
+
"auto_seed_brand_listener_social_listeners_auto_seed_brand_post",
|
|
25
|
+
"auto_seed_competitor_listeners_social_listeners_auto_seed_competitors_post",
|
|
26
|
+
"auto_seed_engagement_listeners_social_listeners_auto_seed_engagement_post",
|
|
27
|
+
"auto_seed_keyword_listeners_social_listeners_auto_seed_keywords_post",
|
|
28
|
+
"bulk_create_listeners_from_list_social_listeners_bulk_from_list_post",
|
|
29
|
+
"connect_x_credentials_social_listeners_integrations_x_post",
|
|
30
|
+
"create_listener_social_listeners_post",
|
|
31
|
+
"delete_listener_social_listeners__listener_id__delete",
|
|
32
|
+
"disconnect_x_credentials_social_listeners_integrations_x_delete",
|
|
33
|
+
"generate_listener_query_social_listeners_queries_generate_post",
|
|
34
|
+
"get_auto_seed_status_social_listeners_auto_seed_status_get",
|
|
35
|
+
"get_listener_social_listeners__listener_id__get",
|
|
36
|
+
"get_social_settings_social_listeners_settings_get",
|
|
37
|
+
"get_social_stats_social_listeners_stats_get",
|
|
38
|
+
"get_x_effective_mode_social_listeners_integrations_x_mode_get",
|
|
39
|
+
"get_x_integration_social_listeners_integrations_x_get",
|
|
40
|
+
"list_listeners_social_listeners_get",
|
|
41
|
+
"list_lists_lists_get",
|
|
42
|
+
"list_posts_by_contact_social_listeners_posts_by_contact__contact_id__get",
|
|
43
|
+
"list_sequences_sequences_get",
|
|
44
|
+
"list_source_lists_social_listeners_source_lists_get",
|
|
45
|
+
"list_top_engaged_accounts_social_listeners_engagements_top_accounts_get",
|
|
46
|
+
"mark_post_social_listeners_posts__post_id__mark_post",
|
|
47
|
+
"preview_listener_cost_social_listeners_preview_cost_post",
|
|
48
|
+
"save_post_author_as_contact_social_listeners_posts__post_id__save_contact_post",
|
|
49
|
+
"search_matched_posts_social_listeners_posts_search_post",
|
|
50
|
+
"search_post_engagements_social_listeners_posts__post_id__engagements_search_post",
|
|
51
|
+
"test_x_credentials_social_listeners_integrations_x_test_post",
|
|
52
|
+
"update_listener_social_listeners__listener_id__patch",
|
|
53
|
+
"update_social_settings_social_listeners_settings_patch"
|
|
54
|
+
],
|
|
55
|
+
"components": [
|
|
56
|
+
{
|
|
57
|
+
"name": "SocialListenersScreen",
|
|
58
|
+
"file": "components/social-listeners-screen.tsx",
|
|
59
|
+
"purpose": "Finished social listening workspace: listeners with search, filters and totals, a deep-linkable detail (overview, matches, edit), create, pause and resume, delete, posts, top engagers, sources and starter seeds, spend limits and the X connection.",
|
|
60
|
+
"operations": [
|
|
61
|
+
"delete_listener_social_listeners__listener_id__delete",
|
|
62
|
+
"get_listener_social_listeners__listener_id__get",
|
|
63
|
+
"get_social_stats_social_listeners_stats_get",
|
|
64
|
+
"list_listeners_social_listeners_get",
|
|
65
|
+
"update_listener_social_listeners__listener_id__patch"
|
|
66
|
+
],
|
|
67
|
+
"tiers": [
|
|
68
|
+
"read",
|
|
69
|
+
"billable",
|
|
70
|
+
"destructive"
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "ListenerForm",
|
|
75
|
+
"file": "components/listener-form.tsx",
|
|
76
|
+
"purpose": "Create a listener (type, channel, query or account, interval, monthly cap) with a cost estimate and an optional generated query, or edit one; edit reads the full record first and sends only changed fields.",
|
|
77
|
+
"operations": [
|
|
78
|
+
"create_listener_social_listeners_post",
|
|
79
|
+
"generate_listener_query_social_listeners_queries_generate_post",
|
|
80
|
+
"get_listener_social_listeners__listener_id__get",
|
|
81
|
+
"preview_listener_cost_social_listeners_preview_cost_post",
|
|
82
|
+
"update_listener_social_listeners__listener_id__patch"
|
|
83
|
+
],
|
|
84
|
+
"tiers": [
|
|
85
|
+
"read",
|
|
86
|
+
"billable"
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"name": "MatchedPosts",
|
|
91
|
+
"file": "components/matched-posts.tsx",
|
|
92
|
+
"purpose": "Posts your listeners matched, filtered and paged; mark reviewed or dismissed, save an author as a contact, add the author to a live sequence, and open the engagers. Every write is confirmed.",
|
|
93
|
+
"operations": [
|
|
94
|
+
"add_post_author_to_sequence_social_listeners_posts__post_id__add_to_sequence_post",
|
|
95
|
+
"list_sequences_sequences_get",
|
|
96
|
+
"mark_post_social_listeners_posts__post_id__mark_post",
|
|
97
|
+
"save_post_author_as_contact_social_listeners_posts__post_id__save_contact_post",
|
|
98
|
+
"search_matched_posts_social_listeners_posts_search_post"
|
|
99
|
+
],
|
|
100
|
+
"tiers": [
|
|
101
|
+
"read",
|
|
102
|
+
"billable",
|
|
103
|
+
"external"
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"name": "PostEngagements",
|
|
108
|
+
"file": "components/post-engagements.tsx",
|
|
109
|
+
"purpose": "Who commented on or reacted to one matched post, and a confirmed one-step add of those people to a new or existing list.",
|
|
110
|
+
"operations": [
|
|
111
|
+
"add_engagers_to_list_social_listeners_posts__post_id__engagements_add_to_list_post",
|
|
112
|
+
"list_lists_lists_get",
|
|
113
|
+
"search_post_engagements_social_listeners_posts__post_id__engagements_search_post"
|
|
114
|
+
],
|
|
115
|
+
"tiers": [
|
|
116
|
+
"read",
|
|
117
|
+
"billable"
|
|
118
|
+
]
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"name": "TopEngagers",
|
|
122
|
+
"file": "components/top-engagers.tsx",
|
|
123
|
+
"purpose": "The accounts that engage with your matched posts most often, by window, channel and minimum posts.",
|
|
124
|
+
"operations": [
|
|
125
|
+
"list_top_engaged_accounts_social_listeners_engagements_top_accounts_get"
|
|
126
|
+
],
|
|
127
|
+
"tiers": [
|
|
128
|
+
"read"
|
|
129
|
+
]
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"name": "SourceLists",
|
|
133
|
+
"file": "components/source-lists.tsx",
|
|
134
|
+
"purpose": "Lists already feeding account listeners, and a confirmed one-step listener per social profile on a list and channel.",
|
|
135
|
+
"operations": [
|
|
136
|
+
"bulk_create_listeners_from_list_social_listeners_bulk_from_list_post",
|
|
137
|
+
"list_lists_lists_get",
|
|
138
|
+
"list_source_lists_social_listeners_source_lists_get"
|
|
139
|
+
],
|
|
140
|
+
"tiers": [
|
|
141
|
+
"read",
|
|
142
|
+
"billable"
|
|
143
|
+
]
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"name": "AutoSeedPanel",
|
|
147
|
+
"file": "components/auto-seed-panel.tsx",
|
|
148
|
+
"purpose": "Starter listeners from your keywords, brand, competitors and buyer profile, or all at once in the background; each is confirmed with its credits and the last run is followed.",
|
|
149
|
+
"operations": [
|
|
150
|
+
"auto_seed_all_listeners_social_listeners_auto_seed_all_post",
|
|
151
|
+
"auto_seed_brand_listener_social_listeners_auto_seed_brand_post",
|
|
152
|
+
"auto_seed_competitor_listeners_social_listeners_auto_seed_competitors_post",
|
|
153
|
+
"auto_seed_engagement_listeners_social_listeners_auto_seed_engagement_post",
|
|
154
|
+
"auto_seed_keyword_listeners_social_listeners_auto_seed_keywords_post",
|
|
155
|
+
"get_auto_seed_status_social_listeners_auto_seed_status_get"
|
|
156
|
+
],
|
|
157
|
+
"tiers": [
|
|
158
|
+
"read",
|
|
159
|
+
"billable"
|
|
160
|
+
]
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"name": "SocialSettingsPanel",
|
|
164
|
+
"file": "components/social-settings.tsx",
|
|
165
|
+
"purpose": "Org-wide spend caps, per-channel ceilings and contact capture rules; admin only; every change is confirmed with before and after.",
|
|
166
|
+
"operations": [
|
|
167
|
+
"get_social_settings_social_listeners_settings_get",
|
|
168
|
+
"update_social_settings_social_listeners_settings_patch"
|
|
169
|
+
],
|
|
170
|
+
"tiers": [
|
|
171
|
+
"read",
|
|
172
|
+
"billable"
|
|
173
|
+
]
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"name": "XIntegration",
|
|
177
|
+
"file": "components/x-integration.tsx",
|
|
178
|
+
"purpose": "Which X credentials listeners poll with, test and connect your own X developer app (write-only, confirmed), and disconnect it (confirmed with the count of listeners it pauses).",
|
|
179
|
+
"operations": [
|
|
180
|
+
"connect_x_credentials_social_listeners_integrations_x_post",
|
|
181
|
+
"disconnect_x_credentials_social_listeners_integrations_x_delete",
|
|
182
|
+
"get_x_effective_mode_social_listeners_integrations_x_mode_get",
|
|
183
|
+
"get_x_integration_social_listeners_integrations_x_get",
|
|
184
|
+
"list_listeners_social_listeners_get",
|
|
185
|
+
"test_x_credentials_social_listeners_integrations_x_test_post"
|
|
186
|
+
],
|
|
187
|
+
"tiers": [
|
|
188
|
+
"read",
|
|
189
|
+
"billable",
|
|
190
|
+
"destructive"
|
|
191
|
+
]
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"name": "RecordSocialTab",
|
|
195
|
+
"file": "components/record-social-tab.tsx",
|
|
196
|
+
"purpose": "One contact's matched social posts; drop into ContactsScreen extraDetailTabs.",
|
|
197
|
+
"operations": [
|
|
198
|
+
"list_posts_by_contact_social_listeners_posts_by_contact__contact_id__get"
|
|
199
|
+
],
|
|
200
|
+
"tiers": [
|
|
201
|
+
"read"
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
],
|
|
205
|
+
"hooks": [],
|
|
206
|
+
"lib": [
|
|
207
|
+
{
|
|
208
|
+
"file": "lib/signals-types.ts",
|
|
209
|
+
"purpose": "Listener, post, engagement, settings and X types; the API's exact vocabularies (channels, listener types, statuses); the changed-fields diff; paging and number helpers."
|
|
210
|
+
}
|
|
211
|
+
],
|
|
212
|
+
"recipes": [
|
|
213
|
+
{
|
|
214
|
+
"file": "recipes/preview-and-create-a-listener.ts",
|
|
215
|
+
"title": "Estimate a listener's cost, then create it with a cap",
|
|
216
|
+
"when": "Starting to watch a keyword or competitor; creating spends credits as posts are read, so a person confirms the cap."
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"file": "recipes/triage-matched-posts.ts",
|
|
220
|
+
"title": "Read new matched posts and mark them reviewed",
|
|
221
|
+
"when": "A daily triage queue for social signals."
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"file": "recipes/spend-against-caps.ts",
|
|
225
|
+
"title": "Compare this month's credit spend with the org-wide cap",
|
|
226
|
+
"when": "A budget alert for social listening."
|
|
227
|
+
}
|
|
228
|
+
],
|
|
229
|
+
"prerequisites": [
|
|
230
|
+
{
|
|
231
|
+
"id": "signals-scopes",
|
|
232
|
+
"description": "The key needs intent:read to read, intent:run to create or change listeners and settings, and intent:delete to delete a listener. Settings changes also need an org admin.",
|
|
233
|
+
"where": "api",
|
|
234
|
+
"verify": "GET /social-listeners/stats returns 200, not 403."
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"id": "credit-balance",
|
|
238
|
+
"description": "Reading posts spends credits as they arrive. Set a monthly cap on every listener and review the org-wide caps in Settings before creating many.",
|
|
239
|
+
"where": "app",
|
|
240
|
+
"verify": "The owner reviewed the caps."
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
"id": "x-credentials",
|
|
244
|
+
"description": "X listeners poll with graph8's shared allowance or your own X developer app. Without either, creating an X listener answers 400.",
|
|
245
|
+
"where": "api",
|
|
246
|
+
"verify": "GET /social-listeners/integrations/x/mode shows an auth mode."
|
|
247
|
+
}
|
|
248
|
+
],
|
|
249
|
+
"scopes": [
|
|
250
|
+
"intent:delete",
|
|
251
|
+
"intent:read",
|
|
252
|
+
"intent:run",
|
|
253
|
+
"lists:read",
|
|
254
|
+
"sequences:read"
|
|
255
|
+
],
|
|
256
|
+
"defaults": [
|
|
257
|
+
{
|
|
258
|
+
"decision": "Poll interval and monthly cap for a new listener",
|
|
259
|
+
"default": "Not sent: the app's own defaults apply unless the person types a value",
|
|
260
|
+
"askHuman": false
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"decision": "Which sequence an author is added to",
|
|
264
|
+
"default": "None: a person always picks a live sequence",
|
|
265
|
+
"askHuman": true
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"decision": "Pausing versus deleting a listener",
|
|
269
|
+
"default": "Pause (keeps the matched posts); delete only when asked, after a confirmation",
|
|
270
|
+
"askHuman": false
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"decision": "Auto-seed passes that use a language model",
|
|
274
|
+
"default": "Never without the person confirming the credits",
|
|
275
|
+
"askHuman": false
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
"decision": "Connecting an X developer app",
|
|
279
|
+
"default": "Never automatic: test first, then a person confirms",
|
|
280
|
+
"askHuman": true
|
|
281
|
+
}
|
|
282
|
+
],
|
|
283
|
+
"peerDependencies": {
|
|
284
|
+
"react": ">=18",
|
|
285
|
+
"lucide-react": ">=0.400.0"
|
|
286
|
+
}
|
|
287
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AutoSeedPanel: create starter listeners from what graph8 already knows about your business.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. getAutoSeedStatus (GET /social-listeners/auto-seed-status) read the most recent run on open, and the run just started, polled
|
|
8
|
+
* 2. autoSeedKeywordListeners (POST /social-listeners/auto-seed/keywords) billable tier, charges nothing (reads keywords you already have)
|
|
9
|
+
* 3. autoSeedBrandListener (POST /social-listeners/auto-seed/brand) billable one language-model pass, 5 credits
|
|
10
|
+
* 4. autoSeedCompetitorListeners (POST /social-listeners/auto-seed/competitors) billable one pass, 5 credits
|
|
11
|
+
* 5. autoSeedEngagementListeners (POST /social-listeners/auto-seed/engagement) billable one pass, 5 credits however many channels
|
|
12
|
+
* 6. autoSeedAllListeners (POST /social-listeners/auto-seed/all) billable up to three passes (15 credits) AFTER the 202, so the confirm says so
|
|
13
|
+
* Every action is confirmed with the credits it can spend. The per-pass price is the backend constant (AI_PASS_CREDITS).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useEffect, useRef, useState } from "react";
|
|
17
|
+
import { Wand2 } from "lucide-react";
|
|
18
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
19
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
20
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
21
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
22
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
23
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
24
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
25
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
26
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
27
|
+
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
28
|
+
import { AI_PASS_CREDITS, PLATFORMS } from "../lib/signals-types";
|
|
29
|
+
import type { Platform, SeedJob } from "../lib/signals-types";
|
|
30
|
+
|
|
31
|
+
type Kind = "keywords" | "brand" | "competitors" | "engagement" | "all";
|
|
32
|
+
const SEEDS: { kind: Kind; label: string; hint: string; passes: number; path: string; channels: boolean }[] = [
|
|
33
|
+
{ kind: "keywords", label: "From your keywords", hint: "One listener per keyword you already track. Free.", passes: 0, path: "keywords", channels: false },
|
|
34
|
+
{ kind: "brand", label: "Your brand", hint: "Reads your brand documents for names to watch.", passes: 1, path: "brand", channels: false },
|
|
35
|
+
{ kind: "competitors", label: "Competitors", hint: "Reads your brand documents for competitor names.", passes: 1, path: "competitors", channels: false },
|
|
36
|
+
{ kind: "engagement", label: "Buyer engagement", hint: "Query buckets shaped like your ideal customer.", passes: 1, path: "engagement", channels: true },
|
|
37
|
+
{ kind: "all", label: "All of them", hint: "Brand, competitors, keywords and engagement in one background run.", passes: 3, path: "all", channels: true },
|
|
38
|
+
];
|
|
39
|
+
const DONE = new Set(["done", "failed"]);
|
|
40
|
+
|
|
41
|
+
export function AutoSeedPanel({ onSeeded }: { onSeeded?: () => void }) {
|
|
42
|
+
const brand = useBrand();
|
|
43
|
+
const [channels, setChannels] = useState<Platform[]>(["x"]);
|
|
44
|
+
const [confirm, setConfirm] = useState<Kind | null>(null);
|
|
45
|
+
const [note, setNote] = useState<string | null>(null);
|
|
46
|
+
const jobId = useRef<string | undefined>(undefined);
|
|
47
|
+
|
|
48
|
+
const last = useG8Query((signal) => g8Get<SeedJob>("/social-listeners/auto-seed-status", {}, { signal }).then((r) => r.data), [], { key: ["signals", "seed-status"] });
|
|
49
|
+
const poll = useJobPoll<SeedJob>((signal) => g8Get<SeedJob>("/social-listeners/auto-seed-status", { job_id: jobId.current }, { signal }).then((r) => r.data), {
|
|
50
|
+
isTerminal: (s) => DONE.has(String(s.status)),
|
|
51
|
+
enabled: false,
|
|
52
|
+
onDone: () => { void last.refetch(); onSeeded?.(); },
|
|
53
|
+
});
|
|
54
|
+
useEffect(() => { if (last.data?.status === "running" && last.data.job_id && !poll.polling) { jobId.current = last.data.job_id; poll.start(); } }, [last.data?.status, last.data?.job_id]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
55
|
+
|
|
56
|
+
const seed = SEEDS.find((s) => s.kind === confirm);
|
|
57
|
+
const run = useG8Mutation(async (s: (typeof SEEDS)[number]): Promise<Record<string, unknown>> => {
|
|
58
|
+
const body = s.channels ? { channels } : undefined;
|
|
59
|
+
return (await g8Post<Record<string, unknown>>(`/social-listeners/auto-seed/${s.path}`, body)).data;
|
|
60
|
+
}, {
|
|
61
|
+
onSuccess: (r, s) => {
|
|
62
|
+
setConfirm(null);
|
|
63
|
+
if (s.kind === "all") { jobId.current = typeof r.job_id === "string" ? r.job_id : undefined; setNote("The run started in the background. Credits are charged as each pass completes."); poll.start(); }
|
|
64
|
+
else { setNote(`${s.label}: done.`); onSeeded?.(); }
|
|
65
|
+
},
|
|
66
|
+
invalidates: [["signals", "listeners"], ["signals", "stats"], ["signals", "seed-status"]],
|
|
67
|
+
});
|
|
68
|
+
const toggle = (p: Platform): void => setChannels((c) => (c.includes(p) ? c.filter((x) => x !== p) : [...c, p]));
|
|
69
|
+
const shown = poll.status ?? last.data;
|
|
70
|
+
const credits = (seed?.passes ?? 0) * AI_PASS_CREDITS;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<SectionCard title={`Start from what ${brand.linkLabel} knows`} description="Create starter listeners instead of writing queries. Each one asks first." icon={<Wand2 className="size-4" />}>
|
|
74
|
+
<div className="grid gap-3">
|
|
75
|
+
<fieldset className="flex flex-wrap gap-3"><legend className="mb-1 text-sm font-medium">Channels for buyer engagement and the full run</legend>{PLATFORMS.map((p) => <label key={p.value} className="flex items-center gap-1 text-sm"><input type="checkbox" checked={channels.includes(p.value)} onChange={() => toggle(p.value)} />{p.label}</label>)}</fieldset>
|
|
76
|
+
<ul className="grid gap-2">
|
|
77
|
+
{SEEDS.map((s) => (
|
|
78
|
+
<li key={s.kind} className="flex flex-wrap items-center justify-between gap-2 rounded-md border p-2">
|
|
79
|
+
<span><span className="block text-sm font-medium">{s.label}</span><span className="text-muted-foreground block text-xs">{s.hint}</span></span>
|
|
80
|
+
<Button size="sm" variant="outline" aria-label={`Seed ${s.label}`} disabled={(s.channels && channels.length === 0) || poll.polling} onClick={() => setConfirm(s.kind)}>Create</Button>
|
|
81
|
+
</li>
|
|
82
|
+
))}
|
|
83
|
+
</ul>
|
|
84
|
+
{note ? <p role="status" className="text-sm">{note}</p> : null}
|
|
85
|
+
{last.loading && !last.data ? <Skeleton className="h-5 w-48" /> : null}
|
|
86
|
+
{last.error ? <ApiError compact error={last.error} title="Could not read the last run" onRetry={() => void last.refetch()} /> : null}
|
|
87
|
+
{shown?.status ? (
|
|
88
|
+
<p className="flex flex-wrap items-center gap-2 text-sm" role="status">Last run <StatusPill tone={shown.status === "failed" ? "negative" : shown.status === "done" ? "positive" : "info"}>{shown.status}</StatusPill>{shown.stage && shown.status === "running" ? `at ${shown.stage}` : ""}
|
|
89
|
+
{shown.result ? ` · ${shown.result.brand_created ?? 0} brand, ${shown.result.competitors_created ?? 0} competitor, ${shown.result.keywords_created ?? 0} keyword, ${shown.result.engagement_created ?? 0} engagement listeners created` : ""}{shown.error ? ` · ${shown.error}` : ""}</p>
|
|
90
|
+
) : null}
|
|
91
|
+
{poll.error ? <ApiError compact error={poll.error} title="Could not follow the run" onRetry={() => poll.start()} /> : null}
|
|
92
|
+
</div>
|
|
93
|
+
<ConfirmSpendDialog
|
|
94
|
+
open={seed !== undefined}
|
|
95
|
+
tier="billable"
|
|
96
|
+
title={`Create ${seed?.label.toLowerCase() ?? ""} listeners?`}
|
|
97
|
+
estimatedCredits={credits}
|
|
98
|
+
description={(
|
|
99
|
+
<>
|
|
100
|
+
<p>{seed?.passes === 0 ? "This reads keywords you already track and charges nothing to create the listeners." : `${seed?.passes === 3 ? "Up to three" : "One"} language-model ${seed?.passes === 1 ? "pass" : "passes"} at ${AI_PASS_CREDITS} credits each.`}{seed?.kind === "all" ? " The run is in the background and the charges land after it starts." : ""} The new listeners then spend credits as they read posts, within their monthly caps.</p>
|
|
101
|
+
{run.error ? <ApiError error={run.error} title="The listeners were not created" /> : null}
|
|
102
|
+
</>
|
|
103
|
+
)}
|
|
104
|
+
confirmLabel="Create"
|
|
105
|
+
confirming={run.loading}
|
|
106
|
+
onCancel={() => setConfirm(null)}
|
|
107
|
+
onConfirm={() => { if (seed) void run.mutate(seed); }}
|
|
108
|
+
/>
|
|
109
|
+
</SectionCard>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ListenerForm: create a listener (what to watch, on which channel, how often, the monthly credit cap) or edit one.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. getListener (GET /social-listeners/{listener_id}) read edit only: the FULL record is read before the form shows
|
|
8
|
+
* 2. previewListenerCost (POST /social-listeners/preview-cost) read an estimate, charges nothing
|
|
9
|
+
* 3. generateListenerQuery (POST /social-listeners/queries/generate) billable a language-model call; ConfirmSpendDialog, 5 credits a pass
|
|
10
|
+
* 4. createListener (POST /social-listeners) billable ConfirmSpendDialog; a brand listener with no query also pays for one generated query
|
|
11
|
+
* 5. updateListener (PATCH /social-listeners/{listener_id}) billable ConfirmSpendDialog; only changed fields are sent
|
|
12
|
+
* Nothing is invented: the poll interval and the credit cap are sent only when the person typed them, and the confirm
|
|
13
|
+
* says what bounds the spend (the cap) instead of a made-up total. After a save the listener caches are refreshed.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useState } from "react";
|
|
17
|
+
import { Sparkles } from "lucide-react";
|
|
18
|
+
import { Button, Input, Label, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
19
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
20
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
21
|
+
import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
22
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
23
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
24
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
25
|
+
import { AI_PASS_CREDITS, LISTENER_TYPES, MIN_POLL_SEC, PLATFORMS, capText, fmt, listenerDiff, num, typeLabel } from "../lib/signals-types";
|
|
26
|
+
import type { CostPreview, Listener, ListenerDetailData, ListenerType, Platform } from "../lib/signals-types";
|
|
27
|
+
|
|
28
|
+
export interface ListenerFormProps {
|
|
29
|
+
/** Edit this listener; omit to create one. */
|
|
30
|
+
listener?: Listener;
|
|
31
|
+
onSaved: (listener: Listener) => void;
|
|
32
|
+
onCancel?: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const SELECT = "border-input bg-background h-9 w-full rounded-md border px-2 text-sm disabled:opacity-50";
|
|
36
|
+
|
|
37
|
+
/** Edit mode reads the full record first: a list row may omit fields, and saving a form seeded from it would blank them. */
|
|
38
|
+
export function ListenerForm({ listener, onSaved, onCancel }: ListenerFormProps) {
|
|
39
|
+
const full = useG8Query(
|
|
40
|
+
(signal) => g8Get<ListenerDetailData>(`/social-listeners/${encodeURIComponent(listener!.id)}`, {}, { signal }).then((r) => r.data.listener),
|
|
41
|
+
[listener?.id],
|
|
42
|
+
{ enabled: Boolean(listener), key: ["signals", "listener", listener?.id ?? ""], placeholderData: listener },
|
|
43
|
+
);
|
|
44
|
+
if (!listener) return <ListenerFields onSaved={onSaved} onCancel={onCancel} />;
|
|
45
|
+
if (full.error && (!full.data || full.isPlaceholderData)) return <ApiError error={full.error} title="Could not load this listener" onRetry={() => void full.refetch()} />;
|
|
46
|
+
if (!full.data || full.isPlaceholderData || full.data.id !== listener.id) return <Skeleton className="h-40 w-full" aria-label="Loading listener" />;
|
|
47
|
+
return <ListenerFields key={full.data.id} listener={full.data} onSaved={onSaved} onCancel={onCancel} />;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ListenerFields({ listener, onSaved, onCancel }: ListenerFormProps) {
|
|
51
|
+
const brand = useBrand();
|
|
52
|
+
const editing = Boolean(listener);
|
|
53
|
+
const [type, setType] = useState<ListenerType>((listener?.listener_type as ListenerType) ?? "keyword");
|
|
54
|
+
const [platform, setPlatform] = useState<Platform>((listener?.platform as Platform) ?? "x");
|
|
55
|
+
const [name, setName] = useState(listener?.name ?? "");
|
|
56
|
+
const [query, setQuery] = useState(listener?.query ?? "");
|
|
57
|
+
const [handle, setHandle] = useState(listener?.target_handle ?? "");
|
|
58
|
+
const [poll, setPoll] = useState(listener?.poll_interval_sec != null ? String(listener.poll_interval_sec) : "");
|
|
59
|
+
const [cap, setCap] = useState(listener?.monthly_credit_cap != null ? String(listener.monthly_credit_cap) : "");
|
|
60
|
+
const [describe, setDescribe] = useState("");
|
|
61
|
+
const [confirm, setConfirm] = useState<"save" | "generate" | null>(null);
|
|
62
|
+
|
|
63
|
+
const pollN = poll.trim() === "" ? null : Number(poll);
|
|
64
|
+
const capN = cap.trim() === "" ? null : Number(cap);
|
|
65
|
+
const pollOk = pollN === null || (Number.isInteger(pollN) && pollN >= MIN_POLL_SEC);
|
|
66
|
+
const capOk = capN === null || (Number.isInteger(capN) && capN >= 0);
|
|
67
|
+
const isUser = type === "user";
|
|
68
|
+
const brandNoQuery = !editing && type === "brand" && query.trim() === "";
|
|
69
|
+
// The API cannot unset a query once it is set (a null is dropped), so clearing one is refused here rather than reported as saved.
|
|
70
|
+
const clearedQuery = editing && Boolean(listener?.query) && !isUser && query.trim() === "";
|
|
71
|
+
const valid = name.trim() !== "" && pollOk && capOk && !clearedQuery && (isUser ? handle.trim() !== "" : type === "brand" || query.trim() !== "");
|
|
72
|
+
const diff = listener ? listenerDiff(listener, { name, query, target_handle: handle, poll_interval_sec: pollN, monthly_credit_cap: capN }) : {};
|
|
73
|
+
const changed = Object.keys(diff).length > 0;
|
|
74
|
+
|
|
75
|
+
const cost = useG8Mutation(async (): Promise<CostPreview> => {
|
|
76
|
+
const body: Record<string, unknown> = { listener_type: type, platform };
|
|
77
|
+
if (query.trim()) body.query = query.trim();
|
|
78
|
+
if (isUser && handle.trim()) body.target_handle = handle.trim();
|
|
79
|
+
if (pollN !== null) body.poll_interval_sec = pollN;
|
|
80
|
+
return (await g8Post<CostPreview>("/social-listeners/preview-cost", body)).data;
|
|
81
|
+
});
|
|
82
|
+
const generate = useG8Mutation(async (): Promise<{ query?: string; suggested_name?: string }> => (await g8Post<{ query?: string; suggested_name?: string }>("/social-listeners/queries/generate", { listener_type: type, description: describe.trim() })).data, {
|
|
83
|
+
onSuccess: (r) => { setConfirm(null); if (r.query) setQuery(r.query); if (r.suggested_name && !name.trim()) setName(r.suggested_name); },
|
|
84
|
+
});
|
|
85
|
+
const save = useG8Mutation(async (): Promise<Listener> => {
|
|
86
|
+
if (listener) return (await g8Patch<Listener>(`/social-listeners/${encodeURIComponent(listener.id)}`, diff)).data;
|
|
87
|
+
const body: Record<string, unknown> = { listener_type: type, platform, name: name.trim() };
|
|
88
|
+
if (query.trim()) body.query = query.trim();
|
|
89
|
+
if (isUser) body.target_handle = handle.trim();
|
|
90
|
+
if (pollN !== null) body.poll_interval_sec = pollN;
|
|
91
|
+
if (capN !== null) body.monthly_credit_cap = capN;
|
|
92
|
+
return (await g8Post<Listener>("/social-listeners", body)).data;
|
|
93
|
+
}, { onSuccess: (saved) => { setConfirm(null); onSaved(saved); }, invalidates: (saved) => [["signals", "listeners"], ["signals", "listener", saved.id], ["signals", "stats"]] });
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<form className="grid gap-3" onSubmit={(e) => { e.preventDefault(); if (valid && (!editing || changed)) setConfirm("save"); }}>
|
|
97
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
98
|
+
<div>
|
|
99
|
+
<Label htmlFor="g8-sl-type" className="mb-1">What to watch</Label>
|
|
100
|
+
<select id="g8-sl-type" className={SELECT} value={type} disabled={editing} onChange={(e) => setType(e.target.value as ListenerType)}>
|
|
101
|
+
{LISTENER_TYPES.map((t) => <option key={t.value} value={t.value}>{t.label}</option>)}
|
|
102
|
+
</select>
|
|
103
|
+
<p className="text-muted-foreground mt-1 text-xs">{editing ? `${typeLabel(type)} is fixed once a listener exists.` : LISTENER_TYPES.find((t) => t.value === type)?.hint}</p>
|
|
104
|
+
</div>
|
|
105
|
+
<div>
|
|
106
|
+
<Label htmlFor="g8-sl-platform" className="mb-1">Channel</Label>
|
|
107
|
+
<select id="g8-sl-platform" className={SELECT} value={platform} disabled={editing} onChange={(e) => setPlatform(e.target.value as Platform)}>
|
|
108
|
+
{PLATFORMS.map((p) => <option key={p.value} value={p.value}>{p.label}</option>)}
|
|
109
|
+
</select>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
<div><Label htmlFor="g8-sl-name" className="mb-1">Name</Label><Input id="g8-sl-name" value={name} onChange={(e) => setName(e.target.value)} placeholder="Competitor mentions" /></div>
|
|
113
|
+
{isUser ? (
|
|
114
|
+
<div><Label htmlFor="g8-sl-handle" className="mb-1">Account handle</Label><Input id="g8-sl-handle" value={handle} onChange={(e) => setHandle(e.target.value)} placeholder="@handle" /></div>
|
|
115
|
+
) : (
|
|
116
|
+
<div>
|
|
117
|
+
<Label htmlFor="g8-sl-query" className="mb-1">Search query{type === "brand" ? " (optional)" : ""}</Label>
|
|
118
|
+
<Textarea id="g8-sl-query" rows={2} value={query} onChange={(e) => setQuery(e.target.value)} />
|
|
119
|
+
{clearedQuery ? <p role="note" className="text-warning-foreground mt-1 text-xs">A query can't be removed once set; edit it or delete the listener.</p> : null}
|
|
120
|
+
{brandNoQuery ? <p className="text-warning-foreground mt-1 text-xs" role="note">With no query the app writes one with a language model, which is charged ({AI_PASS_CREDITS} credits).</p> : null}
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
{!isUser ? (
|
|
124
|
+
<div className="grid gap-2 rounded-md border p-3">
|
|
125
|
+
<Label htmlFor="g8-sl-describe">Describe it and let {brand.linkLabel} write the query</Label>
|
|
126
|
+
<Textarea id="g8-sl-describe" rows={2} maxLength={1000} value={describe} onChange={(e) => setDescribe(e.target.value)} placeholder="People complaining about slow onboarding in CRM tools" />
|
|
127
|
+
<div><Button type="button" size="sm" variant="outline" disabled={describe.trim() === ""} onClick={() => setConfirm("generate")}><Sparkles className="size-4" />Write the query</Button></div>
|
|
128
|
+
{generate.error ? <ApiError error={generate.error} title="Could not write the query" /> : null}
|
|
129
|
+
</div>
|
|
130
|
+
) : null}
|
|
131
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
132
|
+
<div><Label htmlFor="g8-sl-poll" className="mb-1">Check every (seconds, optional)</Label><Input id="g8-sl-poll" inputMode="numeric" value={poll} onChange={(e) => setPoll(e.target.value)} placeholder="The app's own default" /></div>
|
|
133
|
+
<div><Label htmlFor="g8-sl-cap" className="mb-1">Monthly credit cap (optional)</Label><Input id="g8-sl-cap" inputMode="numeric" value={cap} onChange={(e) => setCap(e.target.value)} placeholder="The app's own default" /></div>
|
|
134
|
+
</div>
|
|
135
|
+
{!pollOk ? <p role="alert" className="text-error text-xs">The interval must be a whole number of at least {MIN_POLL_SEC} seconds.</p> : null}
|
|
136
|
+
{!capOk ? <p role="alert" className="text-error text-xs">The cap must be a whole number, 0 or more.</p> : null}
|
|
137
|
+
<div className="grid gap-1">
|
|
138
|
+
<div><Button type="button" size="sm" variant="outline" loading={cost.loading} disabled={!(isUser ? handle.trim() : query.trim()) || !pollOk} onClick={() => void cost.mutate(undefined)}>Estimate monthly cost</Button></div>
|
|
139
|
+
{cost.data ? (
|
|
140
|
+
<p role="status" className="text-muted-foreground text-xs">
|
|
141
|
+
About {fmt(cost.data.posts_per_day_estimate)} posts a day, {fmt(cost.data.credits_per_month_estimate)} credits a month. {cost.data.note ?? ""}
|
|
142
|
+
</p>
|
|
143
|
+
) : null}
|
|
144
|
+
{cost.error ? <ApiError error={cost.error} title="Could not estimate the cost" /> : null}
|
|
145
|
+
</div>
|
|
146
|
+
{save.error ? <ApiError error={save.error} title="Could not save the listener" /> : null}
|
|
147
|
+
<div className="flex gap-2">
|
|
148
|
+
<Button type="submit" disabled={!valid || (editing && !changed)}>{editing ? "Save changes" : "Create listener"}</Button>
|
|
149
|
+
{onCancel ? <Button type="button" variant="outline" onClick={onCancel}>Cancel</Button> : null}
|
|
150
|
+
</div>
|
|
151
|
+
<ConfirmSpendDialog
|
|
152
|
+
open={confirm === "generate"}
|
|
153
|
+
tier="billable"
|
|
154
|
+
title="Write a query with a language model?"
|
|
155
|
+
estimatedCredits={AI_PASS_CREDITS}
|
|
156
|
+
description={<p>One language-model call turns your description into a search query. You can edit the result before anything is created.</p>}
|
|
157
|
+
confirmLabel="Write query"
|
|
158
|
+
confirming={generate.loading}
|
|
159
|
+
onCancel={() => setConfirm(null)}
|
|
160
|
+
onConfirm={() => void generate.mutate(undefined)}
|
|
161
|
+
/>
|
|
162
|
+
<ConfirmSpendDialog
|
|
163
|
+
open={confirm === "save"}
|
|
164
|
+
tier="billable"
|
|
165
|
+
title={editing ? `Save changes to ${listener!.name}?` : `Create the ${typeLabel(type).toLowerCase()} listener "${name.trim()}"?`}
|
|
166
|
+
estimatedCredits={brandNoQuery ? AI_PASS_CREDITS : undefined}
|
|
167
|
+
description={(
|
|
168
|
+
<>
|
|
169
|
+
<p>
|
|
170
|
+
{editing ? "The listener keeps polling with its new settings." : "The listener starts polling. Reading posts costs credits as they arrive."} Spend is bounded by its monthly cap: {capText(editing ? (capN ?? num(listener?.monthly_credit_cap)) : capN)}
|
|
171
|
+
{!editing && capN === null ? "; none was set here, so the org-wide caps in Settings apply" : ""}.
|
|
172
|
+
</p>
|
|
173
|
+
{editing && "target_handle" in diff ? <p>Changing the account re-resolves it against the channel, which uses X API quota.</p> : null}
|
|
174
|
+
{save.error ? <ApiError error={save.error} title="The listener was not saved" /> : null}
|
|
175
|
+
</>
|
|
176
|
+
)}
|
|
177
|
+
confirmLabel={editing ? "Save" : "Create"}
|
|
178
|
+
confirming={save.loading}
|
|
179
|
+
onCancel={() => setConfirm(null)}
|
|
180
|
+
onConfirm={() => void save.mutate(undefined)}
|
|
181
|
+
/>
|
|
182
|
+
</form>
|
|
183
|
+
);
|
|
184
|
+
}
|