@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,176 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CreativeTools: AI help and housekeeping for one ad creative. Describe a change and the creative is edited, rewrite the
|
|
5
|
+
* copy or one field, review it and apply the exact fixes, make the other sizes, retry a failed generation, send it back
|
|
6
|
+
* to draft, or keep it as a template.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in the order the screen uses them:
|
|
9
|
+
* 1. chatEditAdCreative POST /ad-creatives/{id}/chat-edit { instruction } (BILLABLE, confirmed)
|
|
10
|
+
* 2. regenerateAdCreativeCopy POST /ad-creatives/{id}/regenerate-copy (BILLABLE, confirmed)
|
|
11
|
+
* 3. rewriteAdCreativeField POST /ad-creatives/{id}/rewrite-field { field, instruction } (BILLABLE, confirmed; the previous text is shown so it can be put back)
|
|
12
|
+
* 4. reviewAdCreative POST /ad-creatives/{id}/ai-review { deep } (BILLABLE, confirmed; changes nothing)
|
|
13
|
+
* 5. applyAdCreativeReviewFindings POST /ad-creatives/{id}/ai-review/apply { finding_ids } (write, free)
|
|
14
|
+
* 6. createAdCreativeSizeSet POST /ad-creatives/{id}/size-set { formats? } (BILLABLE, confirmed: one new creative per format)
|
|
15
|
+
* 7. retryAdCreativeGeneration POST /ad-creatives/{id}/retry (BILLABLE, confirmed; only for a failed creative)
|
|
16
|
+
* 8. moveAdCreativeToDraft POST /ad-creatives/{id}/move-to-draft (write)
|
|
17
|
+
* 9. saveAdCreativeAsTemplate POST /ad-creatives/{id}/save-as-template (write)
|
|
18
|
+
* 10. updateAdCreative PATCH /ad-creatives/{id} { field } (write; "Put the old text back")
|
|
19
|
+
*
|
|
20
|
+
* The API quotes no price in advance and the balance is not checked here, so each dialog says what runs and how many
|
|
21
|
+
* AI passes it is; a quick check runs no model (0 credits). Nothing here publishes or launches an ad.
|
|
22
|
+
*/
|
|
23
|
+
import { useState } from "react";
|
|
24
|
+
import { Button, Input, Label, Textarea } from "@graph8/sdk/ui";
|
|
25
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
26
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
27
|
+
import { useConfirmAction } from "@/g8/components/confirmed-action";
|
|
28
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
29
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
30
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
31
|
+
import type { Tone } from "@/g8/components/screen/status-pill";
|
|
32
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
33
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
34
|
+
import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
35
|
+
import { asRec, num, rowsOf, str } from "../lib/ads-extra";
|
|
36
|
+
import type { Rec } from "../lib/ads-extra";
|
|
37
|
+
|
|
38
|
+
export interface CreativeToolsProps {
|
|
39
|
+
creativeId: string;
|
|
40
|
+
platform: string;
|
|
41
|
+
/** The creative's state: "failed" offers Retry, anything but "draft" offers Move to draft. */
|
|
42
|
+
status: string;
|
|
43
|
+
/** Called after the creative changed, so the detail reloads. */
|
|
44
|
+
onChanged?: () => void;
|
|
45
|
+
className?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const FIELDS = [
|
|
49
|
+
{ key: "headline", label: "Headline" },
|
|
50
|
+
{ key: "primary_text", label: "Primary text" },
|
|
51
|
+
{ key: "description", label: "Description" },
|
|
52
|
+
] as const;
|
|
53
|
+
const SEVERITY: Record<string, Tone> = { error: "negative", critical: "negative", high: "negative", warning: "warning", medium: "warning", info: "neutral", low: "neutral" };
|
|
54
|
+
const PASS = "Uses credits; the API gives no price in advance and your balance is not checked here.";
|
|
55
|
+
|
|
56
|
+
export function CreativeTools({ creativeId, platform, status, onChanged, className }: CreativeToolsProps) {
|
|
57
|
+
const base = `/ad-creatives/${encodeURIComponent(creativeId)}`;
|
|
58
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
59
|
+
const [instruction, setInstruction] = useState("");
|
|
60
|
+
const [field, setField] = useState<(typeof FIELDS)[number]["key"]>("headline");
|
|
61
|
+
const [fieldInstruction, setFieldInstruction] = useState("");
|
|
62
|
+
const [reply, setReply] = useState("");
|
|
63
|
+
const [previous, setPrevious] = useState<{ field: string; value: string } | null>(null);
|
|
64
|
+
const [findings, setFindings] = useState<Rec[] | null>(null);
|
|
65
|
+
const [picked, setPicked] = useState<string[]>([]);
|
|
66
|
+
const [made, setMade] = useState("");
|
|
67
|
+
const [formats, setFormats] = useState<string[]>([]);
|
|
68
|
+
const specs = useG8Query((signal) => g8Get("/ad-creatives/specs", undefined, { signal }), [creativeId]);
|
|
69
|
+
const platformFormats = Object.entries(asRec(asRec(asRec(specs.data?.data).specs)[platform])).map(([key, v]) => ({ key, label: str(asRec(v).label) || key }));
|
|
70
|
+
|
|
71
|
+
const changed = (): void => onChanged?.();
|
|
72
|
+
const restore = useG8Mutation(() => g8Patch(base, { [previous?.field ?? "headline"]: previous?.value ?? "" }), { onSuccess: () => { setPrevious(null); changed(); } });
|
|
73
|
+
const apply = useG8Mutation(() => g8Post(`${base}/ai-review/apply`, { finding_ids: picked }), {
|
|
74
|
+
onSuccess: (res) => {
|
|
75
|
+
const d = asRec(res.data);
|
|
76
|
+
const left = new Set(rowsOf<Rec>(asRec(d.review).findings).map((f) => str(f.id)));
|
|
77
|
+
setFindings((cur) => (cur ?? []).filter((f) => left.has(str(f.id))));
|
|
78
|
+
setPicked([]);
|
|
79
|
+
setReply(`Applied ${num(d.applied) ?? 0} fix${num(d.applied) === 1 ? "" : "es"}.`);
|
|
80
|
+
changed();
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
const move = useG8Mutation(() => g8Post(`${base}/move-to-draft`), { onSuccess: changed });
|
|
84
|
+
const template = useG8Mutation(() => g8Post(`${base}/save-as-template`), { onSuccess: () => setReply("Saved as a template.") });
|
|
85
|
+
|
|
86
|
+
const chat = (): void => ask({
|
|
87
|
+
tier: "billable", title: "Edit this creative with AI", confirmLabel: "Edit with AI",
|
|
88
|
+
description: `One AI pass applies your instruction to the copy and image. ${PASS}`,
|
|
89
|
+
run: async () => { const d = asRec((await g8Post(`${base}/chat-edit`, { instruction: instruction.trim() })).data); setReply(str(d.reply) || "Edit applied."); setInstruction(""); }, onDone: changed,
|
|
90
|
+
});
|
|
91
|
+
const newCopy = (): void => ask({
|
|
92
|
+
tier: "billable", title: "Write new copy for this creative", confirmLabel: "Write new copy",
|
|
93
|
+
description: `One AI pass replaces the headline, primary text and description. The old text is not kept here (use the Revisions tab to go back). ${PASS}`,
|
|
94
|
+
run: async () => { await g8Post(`${base}/regenerate-copy`); setReply("New copy written."); }, onDone: changed,
|
|
95
|
+
});
|
|
96
|
+
const rewrite = (): void => ask({
|
|
97
|
+
tier: "billable", title: `Rewrite the ${FIELDS.find((f) => f.key === field)?.label.toLowerCase()}`, confirmLabel: "Rewrite",
|
|
98
|
+
description: `One AI pass rewrites this field only. ${PASS}`,
|
|
99
|
+
run: async () => { const d = asRec((await g8Post(`${base}/rewrite-field`, { field, instruction: fieldInstruction.trim() })).data); setPrevious({ field, value: str(d.previous_value) }); setFieldInstruction(""); }, onDone: changed,
|
|
100
|
+
});
|
|
101
|
+
const review = (deep: boolean): void => ask({
|
|
102
|
+
tier: "billable", title: deep ? "Review this creative with AI" : "Quick check of this creative", confirmLabel: deep ? "Review with AI" : "Run quick check",
|
|
103
|
+
estimatedCredits: deep ? undefined : 0,
|
|
104
|
+
description: deep ? `One AI pass checks the copy quality on top of the built-in checks. ${PASS} Nothing changes.` : "Runs the built-in checks only, no AI model: 0 credits. Nothing changes.",
|
|
105
|
+
run: async () => { const d = asRec((await g8Post(`${base}/ai-review`, { deep })).data); setFindings(rowsOf<Rec>(d.findings).filter((f) => str(f.id) !== "")); setPicked([]); },
|
|
106
|
+
});
|
|
107
|
+
const sizes = (): void => ask({
|
|
108
|
+
tier: "billable", title: formats.length ? `Make ${formats.length} more size${formats.length === 1 ? "" : "s"}` : `Make every other ${platform} size`, confirmLabel: "Make sizes",
|
|
109
|
+
description: `Creates one new creative per size with this copy kept word for word; each one generates an image. ${PASS}`,
|
|
110
|
+
run: async () => { const d = asRec((await g8Post(`${base}/size-set`, formats.length ? { formats } : {})).data); setMade(`${num(d.total) ?? rowsOf(d.ad_creatives).length} new creatives are being made. They appear in the gallery.`); }, onDone: changed,
|
|
111
|
+
});
|
|
112
|
+
const retry = (): void => ask({
|
|
113
|
+
tier: "billable", title: "Retry generating this creative", confirmLabel: "Retry",
|
|
114
|
+
description: `Runs the generation again from the same brief. ${PASS}`, run: () => g8Post(`${base}/retry`), onDone: changed,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div className={className}>
|
|
119
|
+
<div className="grid gap-4">
|
|
120
|
+
<SectionCard title="Edit with AI" description="Say what to change, for example: make it punchier and add the free trial.">
|
|
121
|
+
<form className="grid gap-2" onSubmit={(e) => { e.preventDefault(); if (instruction.trim()) chat(); }}>
|
|
122
|
+
<Label htmlFor="ct-chat" className="sr-only">What should change</Label>
|
|
123
|
+
<Textarea id="ct-chat" rows={2} value={instruction} onChange={(e) => setInstruction(e.target.value)} />
|
|
124
|
+
<div className="flex flex-wrap gap-2"><Button type="submit" disabled={!instruction.trim()}>Edit with AI</Button><Button type="button" variant="outline" onClick={newCopy}>Write all new copy</Button></div>
|
|
125
|
+
</form>
|
|
126
|
+
</SectionCard>
|
|
127
|
+
<SectionCard title="Rewrite one field">
|
|
128
|
+
<form className="flex flex-wrap items-end gap-2" onSubmit={(e) => { e.preventDefault(); if (fieldInstruction.trim()) rewrite(); }}>
|
|
129
|
+
<select aria-label="Field to rewrite" className="border-input bg-background h-9 rounded-md border px-2 text-sm" value={field} onChange={(e) => setField(e.target.value as typeof field)}>{FIELDS.map((f) => <option key={f.key} value={f.key}>{f.label}</option>)}</select>
|
|
130
|
+
<div className="min-w-48 flex-1"><Input aria-label="How to rewrite it" placeholder="Shorter, with a number" value={fieldInstruction} onChange={(e) => setFieldInstruction(e.target.value)} /></div>
|
|
131
|
+
<Button type="submit" disabled={!fieldInstruction.trim()}>Rewrite</Button>
|
|
132
|
+
</form>
|
|
133
|
+
{previous ? (
|
|
134
|
+
<div className="mt-2 flex items-center gap-2 text-sm" role="status"><span>Rewritten. Before: "{previous.value}"</span><Button size="sm" variant="outline" loading={restore.loading} onClick={() => void restore.mutate(undefined)}>Put the old text back</Button></div>
|
|
135
|
+
) : null}
|
|
136
|
+
{restore.error ? <ApiError error={restore.error} title="Could not put the text back" /> : null}
|
|
137
|
+
</SectionCard>
|
|
138
|
+
<SectionCard title="Review" description="Check the creative against ad rules and copy quality."
|
|
139
|
+
actions={<div className="flex gap-1"><Button size="sm" variant="outline" onClick={() => review(false)}>Quick check</Button><Button size="sm" onClick={() => review(true)}>Review with AI</Button></div>}>
|
|
140
|
+
{findings === null ? <EmptyState compact title="No review yet" /> : findings.length === 0 ? <EmptyState compact title="Nothing to fix" description="The review found no remaining problems." /> : (
|
|
141
|
+
<div className="grid gap-2">
|
|
142
|
+
<ul className="grid gap-2">{findings.map((f) => (
|
|
143
|
+
<li key={str(f.id)} className="flex items-start gap-3 rounded-md border p-3">
|
|
144
|
+
<input type="checkbox" className="mt-1" aria-label={`Select ${str(f.title) || str(f.id)}`} checked={picked.includes(str(f.id))} onChange={() => setPicked((c) => (c.includes(str(f.id)) ? c.filter((x) => x !== str(f.id)) : [...c, str(f.id)]))} />
|
|
145
|
+
<div className="min-w-0 flex-1"><div className="flex items-center gap-2"><span className="text-sm font-medium">{str(f.title) || str(f.message) || str(f.id)}</span><StatusPill tone={SEVERITY[str(f.severity).toLowerCase()] ?? "neutral"}>{str(f.severity, "note")}</StatusPill></div>{str(f.description) ? <p className="text-muted-foreground text-xs">{str(f.description)}</p> : null}</div>
|
|
146
|
+
</li>
|
|
147
|
+
))}</ul>
|
|
148
|
+
<div><Button size="sm" loading={apply.loading} disabled={picked.length === 0} onClick={() => void apply.mutate(undefined)}>Apply {picked.length || ""} selected (free)</Button></div>
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
{apply.error ? <ApiError error={apply.error} title="Could not apply the fixes" /> : null}
|
|
152
|
+
</SectionCard>
|
|
153
|
+
<SectionCard title="Other sizes" description={`Make this creative in the other ${platform} formats with the same copy.`}>
|
|
154
|
+
{platformFormats.length === 0 ? <p className="text-muted-foreground text-sm">No other formats are listed for {platform}.</p> : (
|
|
155
|
+
<div className="grid gap-2">
|
|
156
|
+
<div className="flex flex-wrap gap-3">{platformFormats.map((f) => <label key={f.key} className="flex items-center gap-1 text-sm"><input type="checkbox" checked={formats.includes(f.key)} onChange={() => setFormats((c) => (c.includes(f.key) ? c.filter((x) => x !== f.key) : [...c, f.key]))} />{f.label}</label>)}</div>
|
|
157
|
+
<div><Button variant="outline" onClick={sizes}>{formats.length ? `Make ${formats.length} selected` : "Make all other sizes"}</Button></div>
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
{made ? <p role="status" className="mt-2 text-sm">{made}</p> : null}
|
|
161
|
+
</SectionCard>
|
|
162
|
+
<SectionCard title="Housekeeping">
|
|
163
|
+
<div className="flex flex-wrap gap-2">
|
|
164
|
+
{status === "failed" ? <Button variant="outline" onClick={retry}>Retry generation</Button> : null}
|
|
165
|
+
{status !== "draft" && status !== "generating" ? <Button variant="outline" loading={move.loading} onClick={() => void move.mutate(undefined)}>Move back to draft</Button> : null}
|
|
166
|
+
<Button variant="outline" loading={template.loading} onClick={() => void template.mutate(undefined)}>Save as a template</Button>
|
|
167
|
+
</div>
|
|
168
|
+
{[move.error, template.error].filter(Boolean).map((e, i) => <ApiError key={i} error={e} title="That did not work" />)}
|
|
169
|
+
</SectionCard>
|
|
170
|
+
{reply ? <p role="status" className="text-sm">{reply}</p> : null}
|
|
171
|
+
{failure ? <ApiError error={failure} title="That did not work" /> : null}
|
|
172
|
+
</div>
|
|
173
|
+
<ConfirmSpendDialog {...dialog} />
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
@@ -29,6 +29,8 @@ import type { PostVariant, SocialPost } from "../lib/social-ads-types";
|
|
|
29
29
|
import { useBrand } from "@/g8/hooks/use-brand";
|
|
30
30
|
|
|
31
31
|
export interface PostComposerProps {
|
|
32
|
+
/** Start with this AI brief filled in (for example from a post idea). */
|
|
33
|
+
initialBrief?: string;
|
|
32
34
|
/** Called after a post was saved, scheduled or published. */
|
|
33
35
|
onSaved?: (post: SocialPost, action: "draft" | "scheduled" | "published") => void;
|
|
34
36
|
className?: string;
|
|
@@ -37,14 +39,14 @@ export interface PostComposerProps {
|
|
|
37
39
|
type Action = "schedule" | "publish";
|
|
38
40
|
const newKey = () => globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
39
41
|
|
|
40
|
-
export function PostComposer({ onSaved, className }: PostComposerProps) {
|
|
42
|
+
export function PostComposer({ initialBrief, onSaved, className }: PostComposerProps) {
|
|
41
43
|
const brand = useBrand();
|
|
42
44
|
const { accounts, loading, error, refetch } = useSocialAccounts();
|
|
43
45
|
const [account, setAccount] = useState("");
|
|
44
46
|
const [body, setBody] = useState("");
|
|
45
47
|
const [imageUrl, setImageUrl] = useState("");
|
|
46
48
|
const [when, setWhen] = useState("");
|
|
47
|
-
const [brief, setBrief] = useState("");
|
|
49
|
+
const [brief, setBrief] = useState(initialBrief ?? "");
|
|
48
50
|
const [variants, setVariants] = useState<PostVariant[]>([]);
|
|
49
51
|
const [confirm, setConfirm] = useState<Action | "generate" | null>(null);
|
|
50
52
|
const [notice, setNotice] = useState<string | null>(null);
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* 4. publishSocialPost POST /social/posts/{post_id}/publish (external, confirmed)
|
|
13
13
|
* 5. cancelSocialPost POST /social/posts/{post_id}/cancel (destructive, confirmed)
|
|
14
14
|
* 6. deleteSocialPost DELETE /social/posts/{post_id} (destructive, confirmed; 404 counts as done)
|
|
15
|
+
* 7. getSocialPostMetrics GET /social/posts/{post_id}/metrics (read; the Performance tab, only for a post that is live)
|
|
15
16
|
* Times are instants shown in the viewer's zone with its name.
|
|
16
17
|
*/
|
|
17
18
|
import { useState } from "react";
|
|
@@ -28,6 +29,7 @@ import { formatInstant, isFuture, localInputToIso } from "../lib/social-ads-type
|
|
|
28
29
|
import type { SocialPost } from "../lib/social-ads-types";
|
|
29
30
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
30
31
|
import { AskButton, useConfirmAction } from "@/g8/components/confirmed-action";
|
|
32
|
+
import { PostMetrics } from "./social-performance";
|
|
31
33
|
|
|
32
34
|
export interface PostDetailProps {
|
|
33
35
|
postId: string;
|
|
@@ -147,6 +149,7 @@ export function PostDetail({ postId, onClose, onExpand, onCollapse, onChanged, o
|
|
|
147
149
|
),
|
|
148
150
|
}]
|
|
149
151
|
: []),
|
|
152
|
+
...(post.status === "live" ? [{ id: "performance", label: "Performance", content: <PostMetrics postId={postId} /> }] : []),
|
|
150
153
|
];
|
|
151
154
|
|
|
152
155
|
return (
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SocialIdeas: post ideas mined from your own signals and content, with the reason each one fits. Pick one to start a
|
|
5
|
+
* draft with its hook as the brief.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations:
|
|
8
|
+
* 1. listSocialIdeas GET /social/ideas?source (read; the ideas already mined. This screen never asks the API to mine new ones.)
|
|
9
|
+
*
|
|
10
|
+
* `onUse` receives a brief for the chosen idea; SocialScreen opens the composer with it. Nothing is written or spent here.
|
|
11
|
+
*/
|
|
12
|
+
import { useState } from "react";
|
|
13
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
14
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
15
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
16
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
17
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
18
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
19
|
+
import { rowsOf, str } from "../lib/ads-extra";
|
|
20
|
+
import type { Rec } from "../lib/ads-extra";
|
|
21
|
+
|
|
22
|
+
export interface SocialIdeasProps {
|
|
23
|
+
/** Called with a brief built from the idea. */
|
|
24
|
+
onUse?: (brief: string, idea: Rec) => void;
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const SOURCES = [
|
|
29
|
+
{ value: "", label: "All sources" },
|
|
30
|
+
{ value: "signal_mining", label: "Your signals" },
|
|
31
|
+
{ value: "linkedin_trending", label: "Trending on LinkedIn" },
|
|
32
|
+
{ value: "proof_freshness", label: "Fresh proof" },
|
|
33
|
+
{ value: "repurpose", label: "Repurposed content" },
|
|
34
|
+
{ value: "ai", label: "AI" },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export function ideaBrief(idea: Rec): string {
|
|
38
|
+
return [str(idea.title), str(idea.primary_hook) ? `Hook: ${str(idea.primary_hook)}` : "", str(idea.concept), str(idea.proof_points) ? `Proof: ${str(idea.proof_points)}` : ""].filter(Boolean).join(". ");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function SocialIdeas({ onUse, className }: SocialIdeasProps) {
|
|
42
|
+
const [source, setSource] = useState("");
|
|
43
|
+
const res = useG8Query((signal) => g8Get("/social/ideas", { source: source || undefined }, { signal }), [source]);
|
|
44
|
+
const ideas = rowsOf<Rec>(res.data?.data, "ideas");
|
|
45
|
+
return (
|
|
46
|
+
<SectionCard className={className} title="Post ideas" description="Starting points from your own signals and content."
|
|
47
|
+
actions={<select aria-label="Idea source" className="border-input bg-background h-8 rounded-md border px-2 text-sm" value={source} onChange={(e) => setSource(e.target.value)}>{SOURCES.map((s) => <option key={s.value} value={s.value}>{s.label}</option>)}</select>}>
|
|
48
|
+
{res.error ? <ApiError error={res.error} title="Could not load ideas" onRetry={() => void res.refetch()} /> : res.loading && !res.data ? <Skeleton className="h-24 w-full" aria-busy="true" /> : ideas.length === 0 ? (
|
|
49
|
+
<EmptyState compact title="No ideas yet" description="Ideas appear as signals and content worth posting about come in." />
|
|
50
|
+
) : (
|
|
51
|
+
<ul className="grid gap-3">
|
|
52
|
+
{ideas.map((i) => (
|
|
53
|
+
<li key={str(i.id)} className="grid gap-1 rounded-md border p-3">
|
|
54
|
+
<span className="text-sm font-medium">{str(i.title)}</span>
|
|
55
|
+
{str(i.why) ? <p className="text-muted-foreground text-xs">{str(i.why)}</p> : null}
|
|
56
|
+
{str(i.primary_hook) ? <p className="text-sm">{str(i.primary_hook)}</p> : null}
|
|
57
|
+
{onUse ? <div><Button size="sm" variant="outline" aria-label={`Draft a post from ${str(i.title)}`} onClick={() => onUse(ideaBrief(i), i)}>Draft a post</Button></div> : null}
|
|
58
|
+
</li>
|
|
59
|
+
))}
|
|
60
|
+
</ul>
|
|
61
|
+
)}
|
|
62
|
+
</SectionCard>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SocialPerformance: how your social posts perform. Totals for a window with a per-day table, and (as PostMetrics)
|
|
5
|
+
* the numbers for one post.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations:
|
|
8
|
+
* 1. getSocialAnalytics GET /social/analytics?days (read; totals and per-day rows)
|
|
9
|
+
* 2. getSocialPostMetrics GET /social/posts/{id}/metrics (read; impressions, reactions, comments, shares)
|
|
10
|
+
*
|
|
11
|
+
* Both payloads are open: every numeric total is shown with its name, and a figure the network does not provide is a
|
|
12
|
+
* dash, never a zero. `supported: false` on a post means the network gives no numbers for it.
|
|
13
|
+
*/
|
|
14
|
+
import { useState } from "react";
|
|
15
|
+
import { Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
16
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
17
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
18
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
19
|
+
import { StatStrip } from "@/g8/components/screen/stat-strip";
|
|
20
|
+
import type { StatItem } from "@/g8/components/screen/stat-strip";
|
|
21
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
22
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
23
|
+
import { asRec, num, rowsOf, str, whenText } from "../lib/ads-extra";
|
|
24
|
+
import type { Rec } from "../lib/ads-extra";
|
|
25
|
+
|
|
26
|
+
export interface SocialPerformanceProps {
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const label = (k: string): string => k.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
|
|
31
|
+
const count = (v: unknown): string => (num(v) == null ? "-" : (v as number).toLocaleString());
|
|
32
|
+
|
|
33
|
+
export function SocialPerformance({ className }: SocialPerformanceProps) {
|
|
34
|
+
const [days, setDays] = useState(30);
|
|
35
|
+
const res = useG8Query((signal) => g8Get("/social/analytics", { days }, { signal }), [days]);
|
|
36
|
+
const d = asRec(res.data?.data);
|
|
37
|
+
const totals = Object.entries(asRec(d.totals)).filter(([, v]) => num(v) !== undefined);
|
|
38
|
+
const perDay = rowsOf<Rec>(d.per_day);
|
|
39
|
+
const columns = [...new Set(perDay.flatMap((r) => Object.entries(r).filter(([k, v]) => k !== "date" && num(v) !== undefined).map(([k]) => k)))];
|
|
40
|
+
const stats: StatItem[] = totals.map(([k, v]) => ({ label: label(k), value: count(v) }));
|
|
41
|
+
return (
|
|
42
|
+
<div className={className}>
|
|
43
|
+
<div className="mb-4"><select aria-label="Reporting window" className="border-input bg-background h-9 rounded-md border px-2 text-sm" value={days} onChange={(e) => setDays(Number(e.target.value))}>{[7, 30, 90].map((n) => <option key={n} value={n}>Last {n} days</option>)}</select></div>
|
|
44
|
+
{res.error ? <ApiError error={res.error} title="Could not load post performance" onRetry={() => void res.refetch()} /> : res.loading && !res.data ? <Skeleton className="h-24 w-full" aria-busy="true" /> : stats.length === 0 && perDay.length === 0 ? (
|
|
45
|
+
<EmptyState title="No performance numbers yet" description="Numbers appear after posts have been live for a while." />
|
|
46
|
+
) : (
|
|
47
|
+
<div className="grid gap-4">
|
|
48
|
+
{stats.length ? <StatStrip items={stats} /> : null}
|
|
49
|
+
{perDay.length ? (
|
|
50
|
+
<SectionCard title="By day" flush>
|
|
51
|
+
<Table>
|
|
52
|
+
<TableHeader><TableRow><TableHead>Day</TableHead>{columns.map((c) => <TableHead key={c}>{label(c)}</TableHead>)}</TableRow></TableHeader>
|
|
53
|
+
<TableBody>{perDay.map((r, i) => <TableRow key={i}><TableCell className="font-medium">{str(r.date)}</TableCell>{columns.map((c) => <TableCell key={c}>{count(r[c])}</TableCell>)}</TableRow>)}</TableBody>
|
|
54
|
+
</Table>
|
|
55
|
+
</SectionCard>
|
|
56
|
+
) : null}
|
|
57
|
+
</div>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function PostMetrics({ postId, className }: { postId: string; className?: string }) {
|
|
64
|
+
const res = useG8Query((signal) => g8Get(`/social/posts/${encodeURIComponent(postId)}/metrics`, undefined, { signal }), [postId]);
|
|
65
|
+
const d = asRec(res.data?.data);
|
|
66
|
+
return (
|
|
67
|
+
<SectionCard className={className} title="Post performance" description={d.fetched_at ? `Read ${whenText(d.fetched_at)}` : undefined}>
|
|
68
|
+
{res.error ? <ApiError error={res.error} title="Could not load the numbers" onRetry={() => void res.refetch()} /> : res.loading && !res.data ? <Skeleton className="h-16 w-full" aria-busy="true" /> : d.supported === false ? (
|
|
69
|
+
<EmptyState compact title="No numbers for this post" description="This network does not provide performance numbers for it." />
|
|
70
|
+
) : (
|
|
71
|
+
<StatStrip items={[
|
|
72
|
+
{ label: "Impressions", value: count(d.impressions) },
|
|
73
|
+
{ label: "Reactions", value: count(d.reactions) },
|
|
74
|
+
{ label: "Comments", value: count(d.comments) },
|
|
75
|
+
{ label: "Shares", value: count(d.shares) },
|
|
76
|
+
]} />
|
|
77
|
+
)}
|
|
78
|
+
</SectionCard>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* SocialScreen: the finished social publishing page.
|
|
4
|
+
* SocialScreen: the finished social publishing page. Four tabs:
|
|
5
5
|
* Posts every post by status (SocialPostList): publish a draft, cancel a scheduled post, delete. "New post" opens the
|
|
6
6
|
* composer in a side sheet (PostComposer): write or draft with AI, save, schedule or publish.
|
|
7
|
-
* Accounts the connected social accounts with their publishing capacity. Connecting an account
|
|
8
|
-
* works in the graph8 app, so this tab links out instead of offering a dead button.
|
|
7
|
+
* Accounts the connected social accounts with their publishing capacity; disconnect one (confirmed). Connecting an account
|
|
8
|
+
* (LinkedIn sign-in) only works in the graph8 app, so this tab links out instead of offering a dead button.
|
|
9
|
+
* Ideas post ideas mined from your signals and content (SocialIdeas); "Draft a post" opens the composer with the idea as the brief.
|
|
10
|
+
* Performance totals and a per-day table for a window (SocialPerformance); a live post also has a Performance tab of its own.
|
|
9
11
|
*
|
|
10
12
|
* Every post opens a complete detail screen (PostDetail, `?id=`, full page with `?view=full`).
|
|
11
13
|
*
|
|
12
|
-
* SDK operations: one read of listSocialPosts (page_size 1) for the per-status counts in the stat strip
|
|
14
|
+
* SDK operations: one read of listSocialPosts (page_size 1) for the per-status counts in the stat strip, and
|
|
15
|
+
* disconnectSocialAccount (DELETE /social/accounts/{id}, destructive, confirmed: publishing from it stops). Composes
|
|
13
16
|
* PostComposer (billable AI drafts, external schedule/publish, all confirmed), SocialPostList (external + destructive,
|
|
14
17
|
* confirmed) and useSocialAccounts (read).
|
|
15
18
|
*/
|
|
@@ -35,6 +38,11 @@ import { PostComposer } from "./post-composer";
|
|
|
35
38
|
import { SocialPostList } from "./social-post-list";
|
|
36
39
|
import { PostDetail } from "./post-detail";
|
|
37
40
|
import { brandUrl, useBrand } from "@/g8/hooks/use-brand";
|
|
41
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
42
|
+
import { useConfirmAction } from "@/g8/components/confirmed-action";
|
|
43
|
+
import { g8Delete } from "@/g8/lib/g8-client";
|
|
44
|
+
import { SocialIdeas } from "./social-ideas";
|
|
45
|
+
import { SocialPerformance } from "./social-performance";
|
|
38
46
|
|
|
39
47
|
export interface SocialScreenProps {
|
|
40
48
|
/** Where "Connect" sends people. Default: graph8 Settings > Integrations. */
|
|
@@ -49,6 +57,7 @@ const APP_PATH = "/studio/settings?tab=integrations";
|
|
|
49
57
|
function Accounts({ connectUrl }: { connectUrl: string }) {
|
|
50
58
|
const brand = useBrand();
|
|
51
59
|
const { accounts, loading, error, refetch } = useSocialAccounts();
|
|
60
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
52
61
|
if (error) return <ApiError error={error} onRetry={() => void refetch()} />;
|
|
53
62
|
if (loading && accounts.length === 0) return <p className="text-sm text-muted-foreground" role="status">Loading accounts...</p>;
|
|
54
63
|
if (accounts.length === 0) {
|
|
@@ -61,6 +70,9 @@ function Accounts({ connectUrl }: { connectUrl: string }) {
|
|
|
61
70
|
);
|
|
62
71
|
}
|
|
63
72
|
return (
|
|
73
|
+
<>
|
|
74
|
+
{failure ? <ApiError error={failure} title="Could not disconnect the account" /> : null}
|
|
75
|
+
<ConfirmSpendDialog {...dialog} />
|
|
64
76
|
<ul className="grid gap-3 md:grid-cols-2">
|
|
65
77
|
{accounts.map((a) => (
|
|
66
78
|
<li key={a.id}>
|
|
@@ -74,11 +86,17 @@ function Accounts({ connectUrl }: { connectUrl: string }) {
|
|
|
74
86
|
</p>
|
|
75
87
|
</div>
|
|
76
88
|
{a.profile_url ? <a className="text-sm underline" href={a.profile_url} target="_blank" rel="noreferrer">Profile</a> : null}
|
|
89
|
+
<Button size="sm" variant="ghost" aria-label={`Disconnect ${a.display_name ?? a.provider}`} onClick={() => ask({
|
|
90
|
+
tier: "destructive", title: `Disconnect ${a.display_name ?? a.provider}`, confirmLabel: "Disconnect", affected: 1,
|
|
91
|
+
description: `Publishing and scheduled posts from this account stop. Posts already live stay live. Connect it again in ${brand.linkLabel} to resume.`,
|
|
92
|
+
tolerateStatus: [404], run: () => g8Delete(`/social/accounts/${encodeURIComponent(a.id)}`), onDone: () => void refetch(),
|
|
93
|
+
})}>Disconnect</Button>
|
|
77
94
|
</div>
|
|
78
95
|
</SectionCard>
|
|
79
96
|
</li>
|
|
80
97
|
))}
|
|
81
98
|
</ul>
|
|
99
|
+
</>
|
|
82
100
|
);
|
|
83
101
|
}
|
|
84
102
|
|
|
@@ -86,11 +104,12 @@ export function SocialScreen(props: SocialScreenProps) {
|
|
|
86
104
|
const brand = useBrand();
|
|
87
105
|
const connectUrl = props.connectUrl ?? brandUrl(brand, APP_PATH);
|
|
88
106
|
const [tabValue, setTab] = useUrlState("tab", "posts");
|
|
89
|
-
const tab = tabValue === "accounts"
|
|
107
|
+
const tab = tabValue === "accounts" || tabValue === "ideas" || tabValue === "performance" ? tabValue : "posts";
|
|
90
108
|
const [id, setId] = useUrlState("id", "");
|
|
91
109
|
const [view, setView] = useUrlState("view", "");
|
|
92
110
|
const expanded = view === "full" && Boolean(id);
|
|
93
111
|
const [composing, setComposing] = useState(false);
|
|
112
|
+
const [composeBrief, setComposeBrief] = useState("");
|
|
94
113
|
const [refreshKey, setRefreshKey] = useState(0);
|
|
95
114
|
const counts = useG8Query(async (signal) => (await g8Get<SocialPostPage>("/social/posts", { page: 1, page_size: 1 }, { signal })).data, [refreshKey]);
|
|
96
115
|
|
|
@@ -112,13 +131,15 @@ export function SocialScreen(props: SocialScreenProps) {
|
|
|
112
131
|
actions={
|
|
113
132
|
<>
|
|
114
133
|
{props.toolbarActions}
|
|
115
|
-
<Button onClick={() => setComposing(true)}>
|
|
134
|
+
<Button onClick={() => { setComposeBrief(""); setComposing(true); }}>
|
|
116
135
|
<Plus className="mr-1.5 size-4" /> New post
|
|
117
136
|
</Button>
|
|
118
137
|
</>
|
|
119
138
|
}
|
|
120
139
|
tabs={[
|
|
121
140
|
{ id: "posts", label: "Posts" },
|
|
141
|
+
{ id: "ideas", label: "Ideas" },
|
|
142
|
+
{ id: "performance", label: "Performance" },
|
|
122
143
|
{ id: "accounts", label: "Accounts" },
|
|
123
144
|
]}
|
|
124
145
|
activeTab={tab}
|
|
@@ -144,11 +165,17 @@ export function SocialScreen(props: SocialScreenProps) {
|
|
|
144
165
|
onChanged={() => setRefreshKey((k) => k + 1)} />
|
|
145
166
|
) : null}
|
|
146
167
|
/>
|
|
168
|
+
) : tab === "ideas" ? (
|
|
169
|
+
<SocialIdeas onUse={(brief) => { setComposeBrief(brief); setComposing(true); }} />
|
|
170
|
+
) : tab === "performance" ? (
|
|
171
|
+
<SocialPerformance />
|
|
147
172
|
) : (
|
|
148
173
|
<Accounts connectUrl={connectUrl} />
|
|
149
174
|
)}
|
|
150
175
|
<FormSheet open={composing} onOpenChange={setComposing} title="New post" description="Write it, draft it with AI, then save, schedule or publish.">
|
|
151
176
|
<PostComposer
|
|
177
|
+
key={composeBrief}
|
|
178
|
+
initialBrief={composeBrief || undefined}
|
|
152
179
|
onSaved={() => {
|
|
153
180
|
setComposing(false);
|
|
154
181
|
setRefreshKey((k) => k + 1);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helpers for the ads and social management components (campaign editor, campaign ads, creative tools, revisions,
|
|
5
|
+
* concepts, performance, ideas). These endpoints return open dicts, so every reader accepts what is present.
|
|
6
|
+
*/
|
|
7
|
+
export type Rec = Record<string, unknown>;
|
|
8
|
+
|
|
9
|
+
export function asRec(value: unknown): Rec {
|
|
10
|
+
return value && typeof value === "object" && !Array.isArray(value) ? (value as Rec) : {};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function str(value: unknown, fallback = ""): string {
|
|
14
|
+
return typeof value === "string" ? value : value == null ? fallback : String(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function num(value: unknown): number | undefined {
|
|
18
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function rowsOf<T = Rec>(data: unknown, ...keys: string[]): T[] {
|
|
22
|
+
if (Array.isArray(data)) return data as T[];
|
|
23
|
+
const rec = asRec(data);
|
|
24
|
+
for (const key of [...keys, "items"]) if (Array.isArray(rec[key])) return rec[key] as T[];
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const whenText = (v: unknown): string => {
|
|
29
|
+
const d = new Date(str(v));
|
|
30
|
+
return Number.isNaN(d.getTime()) ? "" : d.toLocaleString();
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** What the API's `/editable` map says about one field. A field it does not list is treated as not editable. */
|
|
34
|
+
export interface FieldCapability { editable: boolean; reason?: string; consequences: string[] }
|
|
35
|
+
|
|
36
|
+
export function capabilityOf(editable: unknown, field: string): FieldCapability {
|
|
37
|
+
const f = asRec(asRec(asRec(editable).fields)[field]);
|
|
38
|
+
return { editable: f.editable === true, reason: str(f.reason) || undefined, consequences: rowsOf<string>(f.consequences).map(String) };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A money string from an input: a positive number with at most two decimals, else undefined. */
|
|
42
|
+
export function parseMoney(raw: string): number | undefined {
|
|
43
|
+
const t = raw.trim();
|
|
44
|
+
if (!/^\d+(\.\d{1,2})?$/.test(t)) return undefined;
|
|
45
|
+
const n = Number(t);
|
|
46
|
+
return n > 0 ? n : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `YYYY-MM-DD` from an API date or midnight-UTC timestamp, for a date input. Never goes through the local zone. */
|
|
50
|
+
export function dateInput(value: unknown): string {
|
|
51
|
+
const t = str(value);
|
|
52
|
+
return /^\d{4}-\d{2}-\d{2}/.test(t) ? t.slice(0, 10) : "";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface CampaignEdit { name?: string; daily_budget?: number; end_date?: string; spend_cap?: number }
|
|
56
|
+
|
|
57
|
+
/** The body of an edit: only fields that differ from the saved campaign. Empty means nothing to save. */
|
|
58
|
+
export function campaignChanges(saved: { name?: unknown; daily_budget?: unknown; end_date?: unknown }, draft: { name: string; daily_budget: string; end_date: string; spend_cap: string }): CampaignEdit {
|
|
59
|
+
const out: CampaignEdit = {};
|
|
60
|
+
if (draft.name.trim() && draft.name.trim() !== str(saved.name)) out.name = draft.name.trim();
|
|
61
|
+
const budget = parseMoney(draft.daily_budget);
|
|
62
|
+
if (budget !== undefined && budget !== num(saved.daily_budget)) out.daily_budget = budget;
|
|
63
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(draft.end_date) && draft.end_date !== dateInput(saved.end_date)) out.end_date = draft.end_date;
|
|
64
|
+
const cap = parseMoney(draft.spend_cap);
|
|
65
|
+
if (cap !== undefined) out.spend_cap = cap;
|
|
66
|
+
return out;
|
|
67
|
+
}
|