@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,166 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CampaignAds: the individual ads inside one campaign. See each ad's copy and state, edit an ad, pause or activate it,
|
|
5
|
+
* archive it, and add a new one.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in the order the screen uses them:
|
|
8
|
+
* 1. listCampaignAds GET /ad-campaigns/{id}/ads?include_archived (read)
|
|
9
|
+
* 2. updateCampaignAd PATCH /ad-campaigns/{id}/ads/{ad} (EXTERNAL, confirmed: changes the live ad; only changed fields are sent)
|
|
10
|
+
* 3. setCampaignAdStatus POST /ad-campaigns/{id}/ads/{ad}/status { status } (EXTERNAL, confirmed: starts or stops delivery and spend)
|
|
11
|
+
* 4. archiveCampaignAd DELETE /ad-campaigns/{id}/ads/{ad} (destructive, confirmed: takes the ad down)
|
|
12
|
+
* 5. createCampaignAd POST /ad-campaigns/{id}/ads (EXTERNAL, confirmed: adds an ad to the live campaign)
|
|
13
|
+
*
|
|
14
|
+
* Editing a live ad can make the platform replace it and send it to review again; the dialog says so, and the
|
|
15
|
+
* `consequences` the API returns after an edit are shown. The edit form starts from the loaded ad and sends only what
|
|
16
|
+
* changed.
|
|
17
|
+
*/
|
|
18
|
+
import { useState } from "react";
|
|
19
|
+
import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
|
|
20
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
21
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
22
|
+
import { useConfirmAction } from "@/g8/components/confirmed-action";
|
|
23
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
24
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
25
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
26
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
27
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
28
|
+
import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
29
|
+
import { asRec, num, rowsOf, str } from "../lib/ads-extra";
|
|
30
|
+
import type { Rec } from "../lib/ads-extra";
|
|
31
|
+
|
|
32
|
+
export interface CampaignAdsProps {
|
|
33
|
+
campaignId: number;
|
|
34
|
+
/** The platform the campaign runs on, for the confirm text. */
|
|
35
|
+
platform?: string;
|
|
36
|
+
onChanged?: () => void;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const FIELDS = [
|
|
41
|
+
{ key: "name", label: "Ad name" },
|
|
42
|
+
{ key: "headline", label: "Headline" },
|
|
43
|
+
{ key: "primary_text", label: "Primary text" },
|
|
44
|
+
{ key: "description", label: "Description" },
|
|
45
|
+
{ key: "cta", label: "Call to action" },
|
|
46
|
+
{ key: "destination_url", label: "Destination URL" },
|
|
47
|
+
] as const;
|
|
48
|
+
type Key = (typeof FIELDS)[number]["key"];
|
|
49
|
+
|
|
50
|
+
const tone = (s: string) => (["active", "approved", "running"].includes(s) ? "positive" : ["failed", "rejected", "disapproved"].includes(s) ? "negative" : s === "paused" || s === "archived" ? "neutral" : "info") as "positive" | "negative" | "neutral" | "info";
|
|
51
|
+
|
|
52
|
+
/** The edit body: only fields whose text differs from the saved ad. A URL must be http(s) when changed. */
|
|
53
|
+
export function adChanges(saved: Rec, draft: Record<Key, string>): Partial<Record<Key, string>> {
|
|
54
|
+
const out: Partial<Record<Key, string>> = {};
|
|
55
|
+
for (const { key } of FIELDS) if (draft[key].trim() !== str(saved[key]).trim()) out[key] = draft[key].trim();
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const isHttpUrl = (raw: string): boolean => /^https?:\/\/\S+\.\S+$/i.test(raw.trim());
|
|
60
|
+
|
|
61
|
+
function AdEditor({ ad, base, onClose, onSaved }: { ad: Rec; base: string; onClose: () => void; onSaved: () => void }) {
|
|
62
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
63
|
+
const saved: Record<Key, string> = { name: str(ad.name), headline: str(ad.headline), primary_text: str(ad.primary_text), description: str(ad.description), cta: str(ad.cta), destination_url: str(ad.destination_url) };
|
|
64
|
+
const [edits, setEdits] = useState<Partial<Record<Key, string>>>({});
|
|
65
|
+
const draft = { ...saved, ...edits };
|
|
66
|
+
const changes = adChanges(ad, draft);
|
|
67
|
+
const urlBad = "destination_url" in changes && changes.destination_url !== "" && !isHttpUrl(changes.destination_url ?? "");
|
|
68
|
+
const [note, setNote] = useState("");
|
|
69
|
+
const save = (): void => ask({
|
|
70
|
+
tier: "external", title: `Save changes to "${str(ad.name) || "this ad"}"`, confirmLabel: "Save to the platform",
|
|
71
|
+
description: "This changes the ad on the platform. The platform may replace the ad and review it again, so delivery can pause for a while.",
|
|
72
|
+
run: async () => {
|
|
73
|
+
const d = asRec((await g8Patch(`${base}/ads/${encodeURIComponent(str(ad.id))}`, changes)).data);
|
|
74
|
+
const lines = rowsOf<string>(d.consequences).map(String);
|
|
75
|
+
setNote(`Saved.${d.replaced_platform_ad === true ? " The platform ad was replaced." : ""}${lines.length ? ` ${lines.join(" ")}` : ""}`);
|
|
76
|
+
setEdits({});
|
|
77
|
+
},
|
|
78
|
+
onDone: onSaved,
|
|
79
|
+
});
|
|
80
|
+
return (
|
|
81
|
+
<div className="grid gap-3">
|
|
82
|
+
{FIELDS.map(({ key, label }) => (
|
|
83
|
+
<div key={key}><Label htmlFor={`ca-${key}`} className="mb-1">{label}</Label><Input id={`ca-${key}`} value={draft[key]} aria-invalid={key === "destination_url" && urlBad} onChange={(e) => setEdits({ ...edits, [key]: e.target.value })} /></div>
|
|
84
|
+
))}
|
|
85
|
+
{urlBad ? <p role="alert" className="text-error-foreground text-xs">Enter a full address starting with https://.</p> : null}
|
|
86
|
+
{str(ad.approval_nuance) ? <p className="text-muted-foreground text-xs">{str(ad.approval_nuance)}</p> : null}
|
|
87
|
+
{note ? <p role="status" className="text-sm">{note}</p> : null}
|
|
88
|
+
{failure ? <ApiError error={failure} title="The platform did not accept the change" /> : null}
|
|
89
|
+
<div className="flex justify-end gap-2"><Button variant="outline" onClick={onClose}>Close</Button><Button disabled={Object.keys(changes).length === 0 || urlBad} onClick={save}>Save changes</Button></div>
|
|
90
|
+
<ConfirmSpendDialog {...dialog} />
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function CampaignAds({ campaignId, platform, onChanged, className }: CampaignAdsProps) {
|
|
96
|
+
const base = `/ad-campaigns/${encodeURIComponent(String(campaignId))}`;
|
|
97
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
98
|
+
const [archived, setArchived] = useState(false);
|
|
99
|
+
const [editing, setEditing] = useState<string | null>(null);
|
|
100
|
+
const [adding, setAdding] = useState(false);
|
|
101
|
+
const [fresh, setFresh] = useState<Record<Key, string>>({ name: "", headline: "", primary_text: "", description: "", cta: "", destination_url: "" });
|
|
102
|
+
const list = useG8Query((signal) => g8Get(`${base}/ads`, { include_archived: archived || undefined }, { signal }), [campaignId, archived]);
|
|
103
|
+
const ads = rowsOf<Rec>(asRec(list.data?.data).ads);
|
|
104
|
+
const opened = ads.find((a) => str(a.id) === editing);
|
|
105
|
+
const changed = (): void => { void list.refetch(); onChanged?.(); };
|
|
106
|
+
const where = platform || "the platform";
|
|
107
|
+
const freshBody = Object.fromEntries(Object.entries(fresh).filter(([, v]) => v.trim() !== "").map(([k, v]) => [k, v.trim()]));
|
|
108
|
+
const freshBad = fresh.destination_url.trim() !== "" && !isHttpUrl(fresh.destination_url);
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div className={className}>
|
|
112
|
+
<SectionCard title="Ads in this campaign" description="Each ad is a version of the copy and image that is shown."
|
|
113
|
+
actions={<div className="flex items-center gap-2"><label className="flex items-center gap-1 text-xs"><input type="checkbox" checked={archived} onChange={(e) => setArchived(e.target.checked)} />Show archived</label><Button size="sm" onClick={() => setAdding(true)}>Add an ad</Button></div>}>
|
|
114
|
+
{list.error ? <ApiError error={list.error} title="Could not load the ads" onRetry={() => void list.refetch()} /> : list.loading && !list.data ? <Skeleton className="h-24 w-full" aria-busy="true" /> : ads.length === 0 ? (
|
|
115
|
+
<EmptyState compact title="No ads yet" description="Add an ad to this campaign, or generate creatives in the Creatives tab." />
|
|
116
|
+
) : (
|
|
117
|
+
<ul className="grid gap-2">
|
|
118
|
+
{ads.map((a) => {
|
|
119
|
+
const status = str(a.status);
|
|
120
|
+
const live = status === "active";
|
|
121
|
+
return (
|
|
122
|
+
<li key={str(a.id)} className="grid gap-2 rounded-md border p-3">
|
|
123
|
+
<div className="flex items-center justify-between gap-2">
|
|
124
|
+
<div className="min-w-0"><span className="block truncate text-sm font-medium">{str(a.headline) || str(a.name) || `Ad ${str(a.slot_index)}`}</span><span className="text-muted-foreground line-clamp-2 text-xs">{str(a.primary_text)}</span></div>
|
|
125
|
+
<StatusPill tone={tone(status)}>{status || "unknown"}</StatusPill>
|
|
126
|
+
</div>
|
|
127
|
+
{str(a.error_message) ? <p role="alert" className="text-error-foreground text-xs">{str(a.error_message)}</p> : null}
|
|
128
|
+
{status !== "archived" ? (
|
|
129
|
+
<div className="flex flex-wrap gap-1">
|
|
130
|
+
<Button size="sm" variant="outline" aria-label={`Edit ${str(a.name) || str(a.headline) || "ad"}`} onClick={() => setEditing(str(a.id))}>Edit</Button>
|
|
131
|
+
{live || status === "paused" ? <Button size="sm" variant="outline" aria-label={`${live ? "Pause" : "Activate"} ${str(a.name) || str(a.headline) || "ad"}`} onClick={() => ask({
|
|
132
|
+
tier: "external", title: `${live ? "Pause" : "Activate"} "${str(a.name) || str(a.headline) || "this ad"}"`, confirmLabel: live ? "Pause ad" : "Activate ad",
|
|
133
|
+
description: live ? `The ad stops being shown on ${where}.` : `The ad starts being shown on ${where} and spends from the campaign budget once approved.`,
|
|
134
|
+
run: () => g8Post(`${base}/ads/${encodeURIComponent(str(a.id))}/status`, { status: live ? "paused" : "active" }), onDone: changed,
|
|
135
|
+
})}>{live ? "Pause" : "Activate"}</Button> : null}
|
|
136
|
+
<Button size="sm" variant="ghost" aria-label={`Archive ${str(a.name) || str(a.headline) || "ad"}`} onClick={() => ask({
|
|
137
|
+
tier: "destructive", title: `Archive "${str(a.name) || str(a.headline) || "this ad"}"`, confirmLabel: "Archive ad", affected: 1,
|
|
138
|
+
description: `The ad is taken down on ${where} and hidden here. It can be shown again with "Show archived".`, tolerateStatus: [404],
|
|
139
|
+
run: () => g8Delete(`${base}/ads/${encodeURIComponent(str(a.id))}`), onDone: changed,
|
|
140
|
+
})}>Archive</Button>
|
|
141
|
+
</div>
|
|
142
|
+
) : null}
|
|
143
|
+
</li>
|
|
144
|
+
);
|
|
145
|
+
})}
|
|
146
|
+
</ul>
|
|
147
|
+
)}
|
|
148
|
+
{num(asRec(list.data?.data).count) !== undefined && num(asRec(list.data?.data).count)! > ads.length ? <p role="note" className="text-muted-foreground mt-2 text-xs">Showing {ads.length} of {String(asRec(list.data?.data).count)} ads.</p> : null}
|
|
149
|
+
</SectionCard>
|
|
150
|
+
{failure ? <ApiError error={failure} title="That did not work" /> : null}
|
|
151
|
+
<FormSheet open={Boolean(opened)} onOpenChange={(o) => { if (!o) setEditing(null); }} title="Edit ad" description="Only the fields you change are sent." width="lg">
|
|
152
|
+
{opened ? <AdEditor key={str(opened.id)} ad={opened} base={base} onClose={() => setEditing(null)} onSaved={changed} /> : null}
|
|
153
|
+
</FormSheet>
|
|
154
|
+
<FormSheet open={adding} onOpenChange={setAdding} title="Add an ad" description={`Adds an ad to this live campaign on ${where}.`} width="lg"
|
|
155
|
+
footer={<><Button variant="outline" onClick={() => setAdding(false)}>Cancel</Button><Button disabled={Object.keys(freshBody).length === 0 || freshBad} onClick={() => ask({
|
|
156
|
+
tier: "external", title: "Add this ad to the campaign", confirmLabel: "Add ad",
|
|
157
|
+
description: `The ad is created on ${where} and goes to review. It may start spending once approved.`,
|
|
158
|
+
run: async () => { await g8Post(`${base}/ads`, freshBody); setFresh({ name: "", headline: "", primary_text: "", description: "", cta: "", destination_url: "" }); setAdding(false); }, onDone: changed,
|
|
159
|
+
})}>Add ad</Button></>}>
|
|
160
|
+
{FIELDS.map(({ key, label }) => <div key={key}><Label htmlFor={`cn-${key}`} className="mb-1">{label}</Label><Input id={`cn-${key}`} value={fresh[key]} onChange={(e) => setFresh({ ...fresh, [key]: e.target.value })} /></div>)}
|
|
161
|
+
{freshBad ? <p role="alert" className="text-error-foreground text-xs">Enter a full address starting with https://.</p> : null}
|
|
162
|
+
</FormSheet>
|
|
163
|
+
<ConfirmSpendDialog {...dialog} />
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* CampaignDetail: the complete screen for one ad campaign. Finds the campaign by id (so `?id=` deep links work) and shows
|
|
5
5
|
* every field plus its results for a window, and the actions that fit it: refresh its review status, retry a failed launch,
|
|
6
|
-
* stop and delete.
|
|
6
|
+
* stop and delete. The Ads tab lists, edits, pauses, archives and adds the individual ads (CampaignAds); the Edit tab
|
|
7
|
+
* previews and applies changes to the live campaign (CampaignEditor). Date-only values are shown in UTC; spend is in the campaign's own currency.
|
|
7
8
|
*
|
|
8
9
|
* SDK operations, in order:
|
|
9
10
|
* 1. listAdCampaigns GET /ad-campaigns (read)
|
|
@@ -27,6 +28,8 @@ import type { AdCampaign, CampaignMetric } from "../lib/social-ads-types";
|
|
|
27
28
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
28
29
|
import { AskButton, useConfirmAction } from "@/g8/components/confirmed-action";
|
|
29
30
|
import { useBrand } from "@/g8/hooks/use-brand";
|
|
31
|
+
import { CampaignAds } from "./campaign-ads";
|
|
32
|
+
import { CampaignEditor } from "./campaign-editor";
|
|
30
33
|
|
|
31
34
|
export interface CampaignDetailProps {
|
|
32
35
|
campaignId: number;
|
|
@@ -121,6 +124,8 @@ export function CampaignDetail({ campaignId, onClose, onExpand, onCollapse, onCh
|
|
|
121
124
|
</SectionCard>
|
|
122
125
|
),
|
|
123
126
|
},
|
|
127
|
+
{ id: "ads", label: "Ads", content: <CampaignAds campaignId={campaign.id} platform={campaign.platform} onChanged={reload} /> },
|
|
128
|
+
{ id: "edit", label: "Edit", content: <CampaignEditor campaignId={campaign.id} onChanged={reload} /> },
|
|
124
129
|
];
|
|
125
130
|
|
|
126
131
|
return (
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CampaignEditor: change a campaign that already exists on an ad platform. Rename it, change the daily budget, the end
|
|
5
|
+
* date or the spend cap, and pause or resume it. Every change is previewed first and pushed to the platform only after
|
|
6
|
+
* a confirmation.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in the order the screen uses them:
|
|
9
|
+
* 1. getAdCampaign GET /ad-campaigns/{id} (read; the saved values the form starts from)
|
|
10
|
+
* 2. getCampaignEditableFields GET /ad-campaigns/{id}/editable (read; which fields this platform and state accept)
|
|
11
|
+
* 3. previewAdCampaignEdit POST /ad-campaigns/{id}/edit-preview (reads the platform, changes nothing; shows the changes and their consequences)
|
|
12
|
+
* 4. updateAdCampaign PUT /ad-campaigns/{id} (EXTERNAL, confirmed: changes the live campaign; only changed fields are sent)
|
|
13
|
+
*
|
|
14
|
+
* The form is read-only until both reads have loaded, so it can never save blanks over a real campaign. A field the
|
|
15
|
+
* platform will not accept is disabled with the platform's own reason. A 200 is not blanket success: a `partial` result
|
|
16
|
+
* lists which changes failed. The spend cap has no saved value to show, so it only changes when you type one.
|
|
17
|
+
*/
|
|
18
|
+
import { useState } from "react";
|
|
19
|
+
import { Pause, Play } from "lucide-react";
|
|
20
|
+
import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
|
|
21
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
23
|
+
import { useConfirmAction } from "@/g8/components/confirmed-action";
|
|
24
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
25
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
26
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
27
|
+
import { g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
|
|
28
|
+
import { asRec, campaignChanges, capabilityOf, dateInput, num, parseMoney, rowsOf, str } from "../lib/ads-extra";
|
|
29
|
+
import type { CampaignEdit, Rec } from "../lib/ads-extra";
|
|
30
|
+
|
|
31
|
+
export interface CampaignEditorProps {
|
|
32
|
+
campaignId: number;
|
|
33
|
+
/** Called after a change reached the platform, so lists and results refresh. */
|
|
34
|
+
onChanged?: () => void;
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const strings = (v: unknown): string[] => rowsOf<unknown>(v).map((x) => (typeof x === "string" ? x : str(asRec(x).description) || str(asRec(x).field) || JSON.stringify(x)));
|
|
39
|
+
|
|
40
|
+
function Form({ campaign, editable, onChanged }: { campaign: Rec; editable: Rec; onChanged: () => void }) {
|
|
41
|
+
const base = `/ad-campaigns/${encodeURIComponent(str(campaign.id))}`;
|
|
42
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
43
|
+
// Only what the person typed is held as `edits`, shown over the saved copy: a reload never wipes typing, and a field they did not touch follows the server.
|
|
44
|
+
const saved = { name: str(campaign.name), daily_budget: num(campaign.daily_budget) == null ? "" : String(campaign.daily_budget), end_date: dateInput(campaign.end_date), spend_cap: "" };
|
|
45
|
+
const [edits, setEdits] = useState<Partial<typeof saved>>({});
|
|
46
|
+
const draft = { ...saved, ...edits };
|
|
47
|
+
const setDraft = (next: typeof saved): void => setEdits(Object.fromEntries(Object.entries(next).filter(([k, v]) => v !== saved[k as keyof typeof saved])) as Partial<typeof saved>);
|
|
48
|
+
const [review, setReview] = useState<{ body: CampaignEdit; changes: string[]; consequences: string[]; warnings: string[] } | null>(null);
|
|
49
|
+
const [outcome, setOutcome] = useState<{ ok: boolean; lines: string[] } | null>(null);
|
|
50
|
+
const body = campaignChanges(campaign, draft);
|
|
51
|
+
const cap = (f: string) => capabilityOf(editable, f);
|
|
52
|
+
const platform = str(campaign.platform);
|
|
53
|
+
const currency = str(campaign.currency, "USD");
|
|
54
|
+
const paused = str(campaign.status) === "paused";
|
|
55
|
+
|
|
56
|
+
const preview = useG8Mutation(async () => {
|
|
57
|
+
const d = asRec((await g8Post(`${base}/edit-preview`, body)).data);
|
|
58
|
+
setReview({ body, changes: strings(d.changes), consequences: strings(d.consequences), warnings: strings(d.warnings) });
|
|
59
|
+
});
|
|
60
|
+
const apply = (b: CampaignEdit | { action: "pause" | "resume" }, title: string, lines: string[]): void => ask({
|
|
61
|
+
tier: "external", title, confirmLabel: "Apply to the platform",
|
|
62
|
+
description: <><p>This changes the live campaign on {platform || "the platform"}. {platform ? `${platform} may review it again and a budget change can restart its learning phase.` : ""}</p>{lines.length ? <ul className="mt-2 list-disc pl-5 text-sm">{lines.map((l, i) => <li key={i}>{l}</li>)}</ul> : null}</>,
|
|
63
|
+
run: async () => {
|
|
64
|
+
const d = asRec((await g8Put(base, b)).data);
|
|
65
|
+
const failed = str(d.status) === "partial";
|
|
66
|
+
setOutcome({ ok: !failed, lines: [...strings(d.changes), ...strings(d.warnings)] });
|
|
67
|
+
setReview(null);
|
|
68
|
+
setEdits({});
|
|
69
|
+
},
|
|
70
|
+
onDone: onChanged,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const field = (key: "name" | "daily_budget" | "end_date" | "spend_cap", label: string, type: string, help?: string) => {
|
|
74
|
+
const c = cap(key);
|
|
75
|
+
return (
|
|
76
|
+
<div>
|
|
77
|
+
<Label htmlFor={`ce-${key}`} className="mb-1">{label}</Label>
|
|
78
|
+
<Input id={`ce-${key}`} type={type} value={draft[key]} disabled={!c.editable} onChange={(e) => { setDraft({ ...draft, [key]: e.target.value }); setReview(null); setOutcome(null); }} />
|
|
79
|
+
{!c.editable ? <p className="text-muted-foreground mt-1 text-xs">{c.reason ?? "The platform does not allow changing this right now."}</p> : help ? <p className="text-muted-foreground mt-1 text-xs">{help}</p> : null}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
const budgetBad = draft.daily_budget.trim() !== "" && cap("daily_budget").editable && parseMoney(draft.daily_budget) === undefined;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div className="grid gap-4">
|
|
87
|
+
<form className="grid gap-3 sm:grid-cols-2" onSubmit={(e) => { e.preventDefault(); if (Object.keys(body).length) void preview.mutate(undefined); }}>
|
|
88
|
+
{field("name", "Name", "text")}
|
|
89
|
+
{field("daily_budget", `Daily budget (${currency})`, "text", "Spent per day once the platform approves the campaign.")}
|
|
90
|
+
{field("end_date", "End date", "date")}
|
|
91
|
+
{field("spend_cap", `Spend cap (${currency})`, "text", "Pauses the campaign once this much is spent. Leave empty to keep the current cap.")}
|
|
92
|
+
{budgetBad ? <p role="alert" className="text-error-foreground text-xs sm:col-span-2">Enter a positive amount, for example 50 or 49.99.</p> : null}
|
|
93
|
+
<div className="flex gap-2 sm:col-span-2">
|
|
94
|
+
<Button type="submit" variant="outline" loading={preview.loading} disabled={Object.keys(body).length === 0 || budgetBad}>Review changes</Button>
|
|
95
|
+
{cap("status").editable ? (
|
|
96
|
+
<Button type="button" variant="ghost" onClick={() => apply({ action: paused ? "resume" : "pause" }, paused ? "Resume this campaign" : "Pause this campaign", [paused ? "The campaign starts delivering and spending again." : "The campaign stops delivering and spending."])}>
|
|
97
|
+
{paused ? <Play className="size-4" /> : <Pause className="size-4" />}{paused ? "Resume campaign" : "Pause campaign"}
|
|
98
|
+
</Button>
|
|
99
|
+
) : null}
|
|
100
|
+
</div>
|
|
101
|
+
</form>
|
|
102
|
+
{preview.error ? <ApiError error={preview.error} title="Could not preview the changes" /> : null}
|
|
103
|
+
{review ? (
|
|
104
|
+
<SectionCard title="Review before applying" description="Nothing has changed yet.">
|
|
105
|
+
{review.changes.length ? <ul className="list-disc pl-5 text-sm">{review.changes.map((c, i) => <li key={i}>{c}</li>)}</ul> : <p className="text-sm">The platform reports no difference for these values.</p>}
|
|
106
|
+
{review.consequences.map((c, i) => <p key={i} className="text-muted-foreground mt-2 text-xs">{c}</p>)}
|
|
107
|
+
{review.warnings.map((w, i) => <p key={i} role="alert" className="mt-2 text-sm">{w}</p>)}
|
|
108
|
+
<div className="mt-3 flex gap-2"><Button onClick={() => apply(review.body, "Apply these changes", review.changes)}>Apply changes</Button><Button variant="ghost" onClick={() => setReview(null)}>Cancel</Button></div>
|
|
109
|
+
</SectionCard>
|
|
110
|
+
) : null}
|
|
111
|
+
{outcome ? (
|
|
112
|
+
<p role="status" className="text-sm">{outcome.ok ? "Changes applied." : "Some changes were not applied."}{outcome.lines.length ? ` ${outcome.lines.join(" ")}` : ""}</p>
|
|
113
|
+
) : null}
|
|
114
|
+
{failure ? <ApiError error={failure} title="The platform did not accept the change" /> : null}
|
|
115
|
+
<ConfirmSpendDialog {...dialog} />
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function CampaignEditor({ campaignId, onChanged, className }: CampaignEditorProps) {
|
|
121
|
+
const base = `/ad-campaigns/${encodeURIComponent(String(campaignId))}`;
|
|
122
|
+
const campaign = useG8Query((signal) => g8Get(base, undefined, { signal }), [campaignId]);
|
|
123
|
+
const editable = useG8Query((signal) => g8Get(`${base}/editable`, undefined, { signal }), [campaignId]);
|
|
124
|
+
const error = campaign.error ?? editable.error;
|
|
125
|
+
return (
|
|
126
|
+
<div className={className}>
|
|
127
|
+
<SectionCard title="Edit campaign" description="Changes go to the ad platform after you confirm.">
|
|
128
|
+
{error ? <ApiError error={error} title="Could not load the campaign" onRetry={() => { void campaign.refetch(); void editable.refetch(); }} /> : !campaign.data || !editable.data ? <Skeleton className="h-32 w-full" aria-busy="true" /> : (
|
|
129
|
+
<Form key={campaignId} campaign={asRec(campaign.data.data)} editable={asRec(editable.data.data)} onChanged={() => { void campaign.refetch(); void editable.refetch(); onChanged?.(); }} />
|
|
130
|
+
)}
|
|
131
|
+
</SectionCard>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CreativeConcepts: a concept board. Describe the ad and get 3 to 6 creative directions to react to before any image is
|
|
5
|
+
* generated; nothing is saved. Pick the one you like and hand its brief to your generator.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations:
|
|
8
|
+
* 1. generateAdConcepts POST /ad-creatives/concepts { brief?, count } (BILLABLE, confirmed; one AI pass; saves nothing)
|
|
9
|
+
*
|
|
10
|
+
* The API quotes no price in advance and the balance is not checked here, so the dialog says what runs. Each concept is
|
|
11
|
+
* a name, a hook, a tone, a layout and two brand colours. `onUse` receives a ready-to-use brief for the chosen concept.
|
|
12
|
+
*/
|
|
13
|
+
import { useState } from "react";
|
|
14
|
+
import { Button, Input, Label } from "@graph8/sdk/ui";
|
|
15
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
16
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
17
|
+
import { useConfirmAction } from "@/g8/components/confirmed-action";
|
|
18
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
19
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
20
|
+
import { g8Post } from "@/g8/lib/g8-client";
|
|
21
|
+
import { rowsOf, str } from "../lib/ads-extra";
|
|
22
|
+
import type { Rec } from "../lib/ads-extra";
|
|
23
|
+
|
|
24
|
+
export interface CreativeConceptsProps {
|
|
25
|
+
/** Called with a brief built from the chosen concept (its name, hook, tone and layout). */
|
|
26
|
+
onUse?: (brief: string, concept: Rec) => void;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A hex colour the API sent, or undefined: a rogue value is never put in a style. */
|
|
31
|
+
export function safeHex(v: unknown): string | undefined {
|
|
32
|
+
const t = str(v);
|
|
33
|
+
return /^#[0-9a-f]{3,8}$/i.test(t) ? t : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function conceptBrief(c: Rec): string {
|
|
37
|
+
const tone = rowsOf<string>(c.tone).map(String).join(", ");
|
|
38
|
+
return [`Concept: ${str(c.name)}.`, str(c.hook) ? `Hook: ${str(c.hook)}.` : "", tone ? `Tone: ${tone}.` : "", str(c.layout) ? `Layout: ${str(c.layout)}.` : ""].filter(Boolean).join(" ");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function CreativeConcepts({ onUse, className }: CreativeConceptsProps) {
|
|
42
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
43
|
+
const [brief, setBrief] = useState("");
|
|
44
|
+
const [count, setCount] = useState(4);
|
|
45
|
+
const [items, setItems] = useState<Rec[] | null>(null);
|
|
46
|
+
const go = (): void => ask({
|
|
47
|
+
tier: "billable", title: `Get ${count} creative concepts`, confirmLabel: "Get concepts",
|
|
48
|
+
description: "One AI pass writes the directions. Nothing is saved and no image is generated. Uses credits; the API gives no price in advance and your balance is not checked here.",
|
|
49
|
+
run: async () => { const res = await g8Post("/ad-creatives/concepts", { brief: brief.trim() || undefined, count }); setItems(rowsOf<Rec>(res.data, "concepts")); },
|
|
50
|
+
});
|
|
51
|
+
return (
|
|
52
|
+
<SectionCard className={className} title="Concept board" description="React to a few directions before generating anything.">
|
|
53
|
+
<form className="mb-3 flex flex-wrap items-end gap-2" onSubmit={(e) => { e.preventDefault(); go(); }}>
|
|
54
|
+
<div className="min-w-60 flex-1"><Label htmlFor="cc-brief" className="mb-1">Brief for the concepts</Label><Input id="cc-brief" value={brief} maxLength={2000} onChange={(e) => setBrief(e.target.value)} /></div>
|
|
55
|
+
<div><Label htmlFor="cc-count" className="mb-1">How many</Label><select id="cc-count" className="border-input bg-background h-9 rounded-md border px-2 text-sm" value={count} onChange={(e) => setCount(Number(e.target.value))}>{[3, 4, 5, 6].map((n) => <option key={n} value={n}>{n}</option>)}</select></div>
|
|
56
|
+
<Button type="submit">Get concepts</Button>
|
|
57
|
+
</form>
|
|
58
|
+
{items === null ? <EmptyState compact title="No concepts yet" /> : items.length === 0 ? <EmptyState compact title="No concepts came back" description="Try a more specific brief." /> : (
|
|
59
|
+
<ul className="grid gap-3 sm:grid-cols-2">
|
|
60
|
+
{items.map((c, i) => (
|
|
61
|
+
<li key={i} className="grid gap-1 rounded-md border p-3">
|
|
62
|
+
<div className="flex items-center gap-2"><span className="size-4 rounded border" style={{ background: safeHex(c.bg) }} aria-hidden /><span className="size-4 rounded border" style={{ background: safeHex(c.accent) }} aria-hidden /><span className="text-sm font-medium">{str(c.name)}</span></div>
|
|
63
|
+
<p className="text-sm">{str(c.hook)}</p>
|
|
64
|
+
<p className="text-muted-foreground text-xs">{rowsOf<string>(c.tone).join(", ")}{str(c.layout) ? ` · ${str(c.layout)}` : ""}</p>
|
|
65
|
+
{onUse ? <div><Button size="sm" variant="outline" aria-label={`Use ${str(c.name)}`} onClick={() => onUse(conceptBrief(c), c)}>Use this concept</Button></div> : null}
|
|
66
|
+
</li>
|
|
67
|
+
))}
|
|
68
|
+
</ul>
|
|
69
|
+
)}
|
|
70
|
+
{failure ? <ApiError error={failure} title="Could not get concepts" /> : null}
|
|
71
|
+
<ConfirmSpendDialog {...dialog} />
|
|
72
|
+
</SectionCard>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* CreativeDetail: the complete screen for one AI ad creative. Finds it by id (so `?id=` deep links work), shows the image and every
|
|
5
5
|
* copy field, and offers every action: edit headline, primary text, description and call to action; make a new image (billable);
|
|
6
|
-
* move it to the gallery; delete. A creative that is still generating re-reads itself until it finishes.
|
|
6
|
+
* move it to the gallery; delete. A creative that is still generating re-reads itself until it finishes. The AI tools tab edits by
|
|
7
|
+
* instruction, rewrites a field, reviews, makes other sizes and more (CreativeTools); Revisions lists and restores earlier versions.
|
|
7
8
|
*
|
|
8
9
|
* SDK operations, in order:
|
|
9
10
|
* 1. listAdCreatives GET /ad-creatives?limit=100 (read)
|
|
@@ -28,6 +29,8 @@ import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
|
28
29
|
import type { AdCreative } 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 { CreativeRevisions } from "./creative-revisions";
|
|
33
|
+
import { CreativeTools } from "./creative-tools";
|
|
31
34
|
|
|
32
35
|
export interface CreativeDetailProps {
|
|
33
36
|
creativeId: string;
|
|
@@ -139,6 +142,8 @@ export function CreativeDetail({ creativeId, onClose, onExpand, onCollapse, onCh
|
|
|
139
142
|
</SectionCard>
|
|
140
143
|
),
|
|
141
144
|
},
|
|
145
|
+
{ id: "ai", label: "AI tools", content: <CreativeTools creativeId={creativeId} platform={creative.platform} status={creative.status} onChanged={changed} /> },
|
|
146
|
+
{ id: "revisions", label: "Revisions", content: <CreativeRevisions creativeId={creativeId} onRestored={changed} /> },
|
|
142
147
|
];
|
|
143
148
|
return (
|
|
144
149
|
<>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* SDK operations, in order:
|
|
9
9
|
* 1. getAdCreativeSpecs GET /ad-creatives/specs (read) { platforms, specs: { platform: { format: { label } } }, ctas }
|
|
10
|
-
* 2. listAdCreatives GET /ad-creatives?status&platform&limit&offset (read) { items }
|
|
10
|
+
* 2. listAdCreatives GET /ad-creatives?status&platform&limit&offset (read) { items }; 50 at a time, "Load more" reads the next 50
|
|
11
11
|
* 3. createAdCreative POST /ad-creatives { platform, ad_format, prompt, name } (billable) ConfirmSpendDialog first; 201 with status "generating"
|
|
12
12
|
* While any card is `generating` the list is re-read with useJobPoll (no streams) until none is.
|
|
13
13
|
* 5. updateAdCreative PATCH /ad-creatives/{id} { headline, primary_text, description, cta } (write)
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* 7. moveAdCreativeToGallery POST /ad-creatives/{id}/move-to-gallery (write) needs the ads launch permission
|
|
16
16
|
* 8. deleteAdCreative DELETE /ad-creatives/{id} (destructive)
|
|
17
17
|
* The API quotes no price for generation: the dialog says credits are used and the balance is not checked here.
|
|
18
|
+
* A "Concept board" (CreativeConcepts, billable and confirmed) offers directions to react to; choosing one fills the brief.
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
import { useState } from "react";
|
|
@@ -24,9 +25,13 @@ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
|
24
25
|
import type { ConfirmTier } from "@/g8/components/confirm-spend-dialog";
|
|
25
26
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
26
27
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
28
|
+
import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
|
|
29
|
+
import { byOffset, hasNextOf } from "@/g8/lib/paging";
|
|
30
|
+
import { LoadMore } from "@/g8/components/paging-notices";
|
|
27
31
|
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
28
32
|
import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
29
33
|
import { listOf } from "../lib/social-ads-types";
|
|
34
|
+
import { CreativeConcepts } from "./creative-concepts";
|
|
30
35
|
import type { AdCreative } from "../lib/social-ads-types";
|
|
31
36
|
|
|
32
37
|
export interface CreativeGalleryProps {
|
|
@@ -40,6 +45,9 @@ export interface CreativeGalleryProps {
|
|
|
40
45
|
interface Specs { platforms: string[]; specs: Record<string, Record<string, { label: string }>> }
|
|
41
46
|
type Pending = { kind: "create" } | { kind: "regenerate" | "delete"; creative: AdCreative };
|
|
42
47
|
|
|
48
|
+
/** Creatives per read; "Load more" appends the next page (the gallery used to stop silently at 50). */
|
|
49
|
+
const CREATIVE_PAGE = 50;
|
|
50
|
+
|
|
43
51
|
export function CreativeGallery({ status, onSelectCreative, className }: CreativeGalleryProps) {
|
|
44
52
|
const [platform, setPlatform] = useState("");
|
|
45
53
|
const [form, setForm] = useState({ platform: "meta", format: "", prompt: "", name: "" });
|
|
@@ -49,8 +57,13 @@ export function CreativeGallery({ status, onSelectCreative, className }: Creativ
|
|
|
49
57
|
const [failure, setFailure] = useState<unknown>(null);
|
|
50
58
|
|
|
51
59
|
const specs = useG8Query(async (signal) => (await g8Get<Specs>("/ad-creatives/specs", undefined, { signal })).data, []);
|
|
52
|
-
const list =
|
|
60
|
+
const list = useLoadMoreList(byOffset<AdCreative>(CREATIVE_PAGE, async (offset, signal) => {
|
|
61
|
+
const env = await g8Get("/ad-creatives", { status, platform: platform || undefined, limit: CREATIVE_PAGE, offset: offset || undefined }, { signal });
|
|
62
|
+
const rows = listOf<AdCreative>(env.data);
|
|
63
|
+
return { rows, hasNext: hasNextOf(env, CREATIVE_PAGE, rows.length) };
|
|
64
|
+
}), [status, platform]);
|
|
53
65
|
const items = list.data ?? [];
|
|
66
|
+
const [boardOpen, setBoardOpen] = useState(false);
|
|
54
67
|
const generating = items.filter((c) => c.status === "generating");
|
|
55
68
|
useJobPoll(async () => (await list.refetch()) ?? ([{ status: "generating" }] as AdCreative[]), { enabled: generating.length > 0, intervalMs: 4000, maxAttempts: 100, isTerminal: (rows) => !rows.some((c) => c.status === "generating") });
|
|
56
69
|
|
|
@@ -122,6 +135,11 @@ export function CreativeGallery({ status, onSelectCreative, className }: Creativ
|
|
|
122
135
|
</li>
|
|
123
136
|
))}
|
|
124
137
|
</ul>
|
|
138
|
+
<LoadMore shown={items.length} noun="creatives" hasMore={list.hasMore} loading={list.loadingMore} onLoadMore={list.loadMore} />
|
|
139
|
+
<details className="rounded-md border p-3" onToggle={(e) => setBoardOpen((e.currentTarget as HTMLDetailsElement).open)}>
|
|
140
|
+
<summary className="cursor-pointer text-sm font-medium">Concept board</summary>
|
|
141
|
+
{boardOpen ? <CreativeConcepts className="mt-3" onUse={(brief) => setForm((f) => ({ ...f, prompt: brief.slice(0, 2000) }))} /> : null}
|
|
142
|
+
</details>
|
|
125
143
|
<ConfirmSpendDialog open={pending != null} tier={tier}
|
|
126
144
|
title={pending?.kind === "create" ? `Generate a ${form.platform} creative` : pending?.kind === "regenerate" ? `Make a new image for "${name}"` : `Delete "${name}"`}
|
|
127
145
|
description={tier === "billable" ? "Uses credits (copy and image). The API gives no price in advance and your balance is not checked here." : "The creative and its revisions are removed."}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CreativeRevisions: the saved versions of one ad creative (every edit, rewrite and regeneration is kept), with restore.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations:
|
|
7
|
+
* 1. listAdCreativeRevisions GET /ad-creatives/{id}/revisions (read; oldest first, as the API returns them)
|
|
8
|
+
* 2. restoreAdCreativeRevision POST /ad-creatives/{id}/restore-revision { index } (write; the creative takes that version's copy and image)
|
|
9
|
+
*
|
|
10
|
+
* Revisions are open dicts: each row shows whatever of `label`, `source`, `headline` and `created_at` it has. Restoring
|
|
11
|
+
* asks for a second click because it replaces what the creative shows now.
|
|
12
|
+
*/
|
|
13
|
+
import { useState } from "react";
|
|
14
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
15
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
16
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
17
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
18
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
19
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
20
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
21
|
+
import { num, rowsOf, str, whenText } from "../lib/ads-extra";
|
|
22
|
+
import type { Rec } from "../lib/ads-extra";
|
|
23
|
+
|
|
24
|
+
export interface CreativeRevisionsProps {
|
|
25
|
+
creativeId: string;
|
|
26
|
+
/** Called after a revision was restored, so the detail reloads. */
|
|
27
|
+
onRestored?: () => void;
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function CreativeRevisions({ creativeId, onRestored, className }: CreativeRevisionsProps) {
|
|
32
|
+
const base = `/ad-creatives/${encodeURIComponent(creativeId)}`;
|
|
33
|
+
const list = useG8Query((signal) => g8Get(`${base}/revisions`, undefined, { signal }), [creativeId]);
|
|
34
|
+
const rows = rowsOf<Rec>(list.data?.data, "revisions");
|
|
35
|
+
const [sure, setSure] = useState<number | null>(null);
|
|
36
|
+
const restore = useG8Mutation((index: number) => g8Post(`${base}/restore-revision`, { index }), { onSuccess: () => { setSure(null); void list.refetch(); onRestored?.(); } });
|
|
37
|
+
return (
|
|
38
|
+
<SectionCard className={className} title="Revisions" description="Every change is kept. Restoring one replaces what the creative shows now.">
|
|
39
|
+
{list.error ? <ApiError error={list.error} title="Could not load the revisions" onRetry={() => void list.refetch()} /> : list.loading && !list.data ? <Skeleton className="h-20 w-full" aria-busy="true" /> : rows.length === 0 ? (
|
|
40
|
+
<EmptyState compact title="No earlier versions" description="A version is kept each time the creative is edited." />
|
|
41
|
+
) : (
|
|
42
|
+
<ul className="grid gap-2">
|
|
43
|
+
{rows.map((r, i) => {
|
|
44
|
+
const index = num(r.index) ?? i;
|
|
45
|
+
return (
|
|
46
|
+
<li key={index} className="flex items-center justify-between gap-3 rounded-md border p-3">
|
|
47
|
+
<div className="min-w-0 text-sm"><span className="font-medium">Version {index + 1}</span>{str(r.label) || str(r.source) ? <span className="text-muted-foreground"> {str(r.label) || str(r.source).replace(/_/g, " ")}</span> : null}<span className="text-muted-foreground block truncate text-xs">{whenText(r.created_at ?? r.at)}{str(r.headline) ? ` · ${str(r.headline)}` : ""}</span></div>
|
|
48
|
+
{sure === index ? <div className="flex gap-1"><Button size="sm" loading={restore.loading} onClick={() => void restore.mutate(index)}>Replace the current version</Button><Button size="sm" variant="ghost" onClick={() => setSure(null)}>Cancel</Button></div> : <Button size="sm" variant="outline" aria-label={`Restore version ${index + 1}`} onClick={() => setSure(index)}>Restore</Button>}
|
|
49
|
+
</li>
|
|
50
|
+
);
|
|
51
|
+
})}
|
|
52
|
+
</ul>
|
|
53
|
+
)}
|
|
54
|
+
{restore.error ? <ApiError error={restore.error} title="Could not restore that version" /> : null}
|
|
55
|
+
</SectionCard>
|
|
56
|
+
);
|
|
57
|
+
}
|