@graph8/sdk 0.370.0 → 0.373.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/blocks/_foundation/README.md +14 -0
- package/blocks/_foundation/block.json +44 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +1 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/agents/README.md +81 -4
- package/blocks/agents/block.json +311 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +17 -3
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +20 -3
- package/blocks/agents/components/agent-runs.tsx +3 -3
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +53 -11
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/components/analytics-screen.tsx +16 -3
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/report-builder.tsx +15 -2
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +27 -0
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +29 -13
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/campaigns/components/campaign-assets.tsx +1 -1
- package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
- package/blocks/campaigns/components/icp-detail.tsx +11 -2
- package/blocks/campaigns/components/icp-editor.tsx +8 -3
- package/blocks/campaigns/components/persona-detail.tsx +11 -2
- package/blocks/campaigns/components/persona-editor.tsx +6 -1
- package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +161 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +1 -1
- package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
- package/blocks/crm/README.md +6 -0
- package/blocks/crm/block.json +53 -4
- package/blocks/crm/components/companies-screen.tsx +23 -9
- package/blocks/crm/components/company-detail.tsx +2 -2
- package/blocks/crm/components/contact-detail.tsx +4 -4
- package/blocks/crm/components/contact-lookup-link.tsx +44 -0
- package/blocks/crm/components/contacts-screen.tsx +31 -13
- package/blocks/crm/components/dedupe-review.tsx +6 -1
- package/blocks/crm/components/import-wizard.tsx +3 -7
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +1 -1
- package/blocks/crm/components/tasks-screen.tsx +35 -15
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-page-query.ts +3 -1
- package/blocks/crm/lib/crm-types.ts +14 -0
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/deals/README.md +24 -6
- package/blocks/deals/block.json +151 -2
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +2 -2
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +30 -9
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +57 -22
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +1 -1
- package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +14 -5
- package/blocks/dialer/block.json +17 -2
- package/blocks/dialer/components/call-contact-button.tsx +26 -9
- package/blocks/dialer/components/call-log-screen.tsx +4 -1
- package/blocks/dialer/components/call-log.tsx +16 -4
- package/blocks/dialer/components/call-queue.tsx +3 -3
- package/blocks/dialer/components/dialer-screen.tsx +69 -27
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +58 -11
- package/blocks/dialer/components/softphone.tsx +1 -1
- package/blocks/enrichment/README.md +16 -6
- package/blocks/enrichment/block.json +19 -7
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +76 -27
- package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
- package/blocks/enrichment/components/enrich-button.tsx +1 -1
- package/blocks/enrichment/components/enrichment-screen.tsx +70 -17
- package/blocks/enrichment/components/waterfall-config.tsx +27 -7
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/forms/README.md +8 -0
- package/blocks/forms/block.json +107 -0
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +1 -1
- package/blocks/forms/components/form-embed.tsx +1 -1
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +51 -17
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +15 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +5 -0
- package/blocks/inbox/block.json +15 -0
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- package/blocks/integrations/README.md +54 -1
- package/blocks/integrations/block.json +189 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +20 -1
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +4 -4
- package/blocks/integrations/components/integrations-screen.tsx +20 -3
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +6 -6
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +26 -7
- package/blocks/integrations/components/webhook-manager.tsx +30 -17
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +5 -0
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +8 -8
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lists/README.md +7 -1
- package/blocks/lists/block.json +31 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +17 -7
- package/blocks/lists/components/list-picker.tsx +13 -3
- package/blocks/lists/components/list-table.tsx +1 -1
- package/blocks/lists/components/lists-screen.tsx +4 -2
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +27 -6
- package/blocks/lists/components/suppressions-screen.tsx +21 -12
- package/blocks/mailboxes/block.json +4 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
- package/blocks/mailboxes/components/domain-detail.tsx +10 -4
- package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
- package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
- package/blocks/mailboxes/components/warmup-health.tsx +20 -20
- package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +117 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +36 -13
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/components/nurture-builder.tsx +10 -2
- package/blocks/nurture/components/nurture-enroll.tsx +6 -7
- package/blocks/nurture/components/nurture-form.tsx +5 -8
- package/blocks/nurture/components/nurture-screen.tsx +16 -5
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/objects/README.md +1 -1
- package/blocks/objects/block.json +13 -5
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-record-detail.tsx +8 -1
- package/blocks/objects/components/object-schema-editor.tsx +1 -1
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +113 -1
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/quotes/README.md +13 -0
- package/blocks/quotes/block.json +48 -1
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +5 -3
- package/blocks/quotes/components/quote-related.tsx +3 -2
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-screen.tsx +2 -2
- package/blocks/sequencer/README.md +6 -0
- package/blocks/sequencer/block.json +28 -1
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +19 -20
- package/blocks/sequencer/components/sequence-detail.tsx +4 -2
- package/blocks/sequencer/components/sequence-enrollments.tsx +4 -2
- package/blocks/sequencer/components/sequence-list.tsx +1 -1
- package/blocks/sequencer/components/sequence-stats.tsx +8 -8
- package/blocks/sequencer/components/sequences-screen.tsx +3 -0
- package/blocks/sequencer/components/step-editor.tsx +11 -11
- package/blocks/sequencer/lib/sequencer-utils.ts +5 -5
- package/blocks/social-ads/README.md +8 -0
- package/blocks/social-ads/block.json +137 -4
- package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
- package/blocks/social-ads/components/audience-sync.tsx +6 -2
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +6 -1
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +6 -1
- package/blocks/social-ads/components/creative-gallery.tsx +20 -2
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +4 -2
- package/blocks/social-ads/components/post-detail.tsx +3 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-screen.tsx +33 -6
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/components/snippet-install.tsx +18 -9
- package/blocks/visitors/components/traffic-chart.tsx +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +24 -14
- package/blocks/visitors/components/visitor-feed.tsx +5 -5
- package/blocks/webchat/components/chat-widget-config.tsx +1 -1
- package/blocks/webchat/components/conversation-list.tsx +1 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +1 -1
- package/blocks/workbench/components/workbench-screen.tsx +15 -5
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workflows/components/workflow-editor.tsx +1 -1
- package/dist/cli.js +536 -392
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +1 -1
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +1 -1
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/brand.d.ts +11 -2
- package/dist/types/blocks/create.d.ts +7 -3
- package/dist/types/blocks/doctor.d.ts +1 -0
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/version.d.ts +2 -2
- package/docs/api/desk.d.ts +2 -2
- package/docs/api/desk.md +3 -3
- package/package.json +1 -1
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +17 -4
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
- package/stacks/apollo/pages/contacts.tsx.tmpl +29 -95
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +36 -8
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +8 -4
- package/stacks/clay/pages/companies.tsx.tmpl +17 -1
- package/stacks/clay/pages/contacts.tsx.tmpl +12 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +16 -3
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +23 -6
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +9 -4
- package/stacks/hubspot/pages/companies.tsx.tmpl +14 -35
- package/stacks/hubspot/pages/contacts.tsx.tmpl +29 -95
- package/stacks/hubspot/pages/deals.tsx.tmpl +10 -1
- package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +21 -5
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +8 -4
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +20 -5
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +27 -66
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +25 -7
- package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +15 -35
- package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +4 -2
- package/stacks/salesforce/pages/companies.tsx.tmpl +14 -35
- package/stacks/salesforce/pages/contacts.tsx.tmpl +20 -35
- package/stacks/salesforce/pages/deals.tsx.tmpl +10 -1
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +7 -3
- package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +16 -3
- package/styles/styles.css +1 -1
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
|
@@ -0,0 +1,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
|
+
}
|
|
@@ -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
|
+
}
|