@graph8/sdk 0.362.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.
Files changed (615) hide show
  1. package/README.md +33 -10
  2. package/blocks/_foundation/README.md +78 -1
  3. package/blocks/_foundation/block.json +123 -6
  4. package/blocks/_foundation/components/api-error.tsx +9 -7
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  7. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  8. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  9. package/blocks/_foundation/components/empty-state.tsx +5 -2
  10. package/blocks/_foundation/components/list-select.tsx +112 -0
  11. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  12. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  13. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  14. package/blocks/_foundation/components/screen/index.ts +2 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  17. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  18. package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
  19. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  20. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  21. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  22. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  23. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  24. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  25. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  26. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  27. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  28. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  29. package/blocks/_foundation/lib/g8-client.ts +1 -1
  30. package/blocks/_foundation/lib/paging.ts +57 -0
  31. package/blocks/_foundation/lib/plural.ts +23 -0
  32. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  33. package/blocks/_foundation/server/connect-route.ts +15 -0
  34. package/blocks/_foundation/server/connect.ts +127 -0
  35. package/blocks/_foundation/server/local-key.ts +70 -0
  36. package/blocks/_foundation/server/proxy.ts +103 -3
  37. package/blocks/_foundation/server/route.ts +16 -4
  38. package/blocks/agents/README.md +93 -2
  39. package/blocks/agents/block.json +343 -2
  40. package/blocks/agents/components/agent-activity.tsx +112 -0
  41. package/blocks/agents/components/agent-approvals.tsx +135 -0
  42. package/blocks/agents/components/agent-builder.tsx +29 -10
  43. package/blocks/agents/components/agent-conversations.tsx +143 -0
  44. package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
  45. package/blocks/agents/components/agent-operators.tsx +164 -0
  46. package/blocks/agents/components/agent-run-detail.tsx +119 -0
  47. package/blocks/agents/components/agent-runs.tsx +19 -10
  48. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  49. package/blocks/agents/components/agent-test-history.tsx +121 -0
  50. package/blocks/agents/components/agents-screen.tsx +226 -0
  51. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  52. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  53. package/blocks/agents/components/from-template-button.tsx +65 -0
  54. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  55. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  56. package/blocks/agents/components/knowledge-form.tsx +69 -0
  57. package/blocks/agents/components/name-dialog.tsx +32 -0
  58. package/blocks/agents/components/receptionist-button.tsx +46 -0
  59. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  60. package/blocks/agents/components/skill-editor.tsx +147 -0
  61. package/blocks/agents/components/skill-library.tsx +111 -0
  62. package/blocks/agents/lib/agent-types.ts +7 -1
  63. package/blocks/agents/lib/facts.tsx +13 -0
  64. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  65. package/blocks/agents/lib/load-more.tsx +141 -0
  66. package/blocks/agents/lib/skill-utils.ts +91 -0
  67. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  68. package/blocks/analytics/README.md +13 -0
  69. package/blocks/analytics/block.json +45 -1
  70. package/blocks/analytics/components/analytics-screen.tsx +222 -0
  71. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  72. package/blocks/analytics/components/leaderboard.tsx +14 -7
  73. package/blocks/analytics/components/report-builder.tsx +38 -11
  74. package/blocks/analytics/components/report-detail.tsx +84 -0
  75. package/blocks/analytics/components/trend-chart.tsx +62 -0
  76. package/blocks/appointments/README.md +6 -0
  77. package/blocks/appointments/block.json +33 -2
  78. package/blocks/appointments/components/availability-editor.tsx +17 -5
  79. package/blocks/appointments/components/booking-page.tsx +2 -2
  80. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  81. package/blocks/appointments/components/event-type-related.tsx +4 -2
  82. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  83. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  84. package/blocks/appointments/components/meeting-list.tsx +38 -18
  85. package/blocks/appointments/components/meetings-screen.tsx +7 -4
  86. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  87. package/blocks/appointments/components/scheduling-screen.tsx +2 -1
  88. package/blocks/block.schema.json +18 -0
  89. package/blocks/block.types.ts +17 -0
  90. package/blocks/campaigns/README.md +41 -11
  91. package/blocks/campaigns/block.json +105 -42
  92. package/blocks/campaigns/components/campaign-assets.tsx +17 -6
  93. package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
  94. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  95. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  96. package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
  97. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  98. package/blocks/campaigns/components/icp-detail.tsx +163 -0
  99. package/blocks/campaigns/components/icp-editor.tsx +22 -17
  100. package/blocks/campaigns/components/persona-detail.tsx +181 -0
  101. package/blocks/campaigns/components/persona-editor.tsx +19 -11
  102. package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
  103. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  104. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  105. package/blocks/coach/README.md +10 -0
  106. package/blocks/coach/block.json +16 -1
  107. package/blocks/coach/components/call-scorecard.tsx +7 -7
  108. package/blocks/coach/components/coach-screen.tsx +222 -0
  109. package/blocks/company-intelligence/README.md +132 -0
  110. package/blocks/company-intelligence/block.json +318 -0
  111. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  112. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  113. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  114. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  115. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  116. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  117. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  118. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  119. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  120. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  121. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  122. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  123. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  124. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  125. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  126. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  127. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  128. package/blocks/copilot/README.md +13 -0
  129. package/blocks/copilot/block.json +14 -1
  130. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  131. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  132. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  133. package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
  134. package/blocks/crm/README.md +34 -0
  135. package/blocks/crm/block.json +107 -2
  136. package/blocks/crm/components/companies-screen.tsx +359 -0
  137. package/blocks/crm/components/company-detail.tsx +10 -8
  138. package/blocks/crm/components/company-table.tsx +11 -4
  139. package/blocks/crm/components/contact-detail.tsx +10 -7
  140. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  141. package/blocks/crm/components/contact-table.tsx +10 -3
  142. package/blocks/crm/components/contacts-screen.tsx +377 -0
  143. package/blocks/crm/components/dedupe-review.tsx +13 -6
  144. package/blocks/crm/components/import-wizard.tsx +7 -9
  145. package/blocks/crm/components/list-select.tsx +20 -0
  146. package/blocks/crm/components/notes-panel.tsx +4 -4
  147. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  148. package/blocks/crm/components/record-form.tsx +6 -4
  149. package/blocks/crm/components/task-list.tsx +4 -4
  150. package/blocks/crm/components/tasks-screen.tsx +399 -0
  151. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  152. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  153. package/blocks/crm/hooks/use-page-query.ts +9 -1
  154. package/blocks/crm/lib/crm-types.ts +26 -2
  155. package/blocks/crm/lib/read-lists.ts +16 -0
  156. package/blocks/crm/lib/screen-parts.tsx +105 -0
  157. package/blocks/deals/README.md +24 -6
  158. package/blocks/deals/block.json +160 -5
  159. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  160. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  161. package/blocks/deals/components/deal-detail.tsx +25 -13
  162. package/blocks/deals/components/deal-filters.tsx +65 -0
  163. package/blocks/deals/components/deal-line-items.tsx +120 -0
  164. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  165. package/blocks/deals/components/deal-related.tsx +31 -10
  166. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  167. package/blocks/deals/components/deals-screen.tsx +66 -26
  168. package/blocks/deals/components/forecast-view.tsx +4 -4
  169. package/blocks/deals/components/pipeline-board.tsx +8 -6
  170. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  171. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  172. package/blocks/deals/components/stage-editor.tsx +5 -5
  173. package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
  174. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  175. package/blocks/deals/lib/deal-extras.ts +156 -0
  176. package/blocks/dialer/README.md +45 -4
  177. package/blocks/dialer/block.json +84 -3
  178. package/blocks/dialer/components/call-contact-button.tsx +269 -0
  179. package/blocks/dialer/components/call-log-screen.tsx +208 -0
  180. package/blocks/dialer/components/call-log.tsx +23 -7
  181. package/blocks/dialer/components/call-queue.tsx +4 -4
  182. package/blocks/dialer/components/dialer-screen.tsx +361 -0
  183. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  184. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  185. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  186. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  187. package/blocks/dialer/components/session-start-form.tsx +61 -12
  188. package/blocks/dialer/components/softphone.tsx +4 -2
  189. package/blocks/enrichment/README.md +29 -7
  190. package/blocks/enrichment/block.json +49 -9
  191. package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
  192. package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
  193. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  194. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  195. package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
  196. package/blocks/enrichment/components/waterfall-config.tsx +66 -17
  197. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  198. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  199. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  200. package/blocks/forms/README.md +8 -0
  201. package/blocks/forms/block.json +139 -18
  202. package/blocks/forms/components/form-activity.tsx +71 -0
  203. package/blocks/forms/components/form-backfill.tsx +108 -0
  204. package/blocks/forms/components/form-builder.tsx +4 -2
  205. package/blocks/forms/components/form-embed.tsx +4 -2
  206. package/blocks/forms/components/form-groups.tsx +151 -0
  207. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  208. package/blocks/forms/components/form-route-history.tsx +72 -0
  209. package/blocks/forms/components/form-saved-views.tsx +81 -0
  210. package/blocks/forms/components/forms-screen.tsx +56 -19
  211. package/blocks/forms/components/more-rows.tsx +30 -0
  212. package/blocks/forms/components/submissions-export.tsx +67 -0
  213. package/blocks/forms/components/submissions-table.tsx +17 -6
  214. package/blocks/forms/lib/forms-extra.ts +67 -0
  215. package/blocks/inbox/README.md +32 -3
  216. package/blocks/inbox/block.json +29 -2
  217. package/blocks/inbox/components/inbox-list.tsx +7 -3
  218. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  219. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  220. package/blocks/inbox/components/reply-composer.tsx +65 -9
  221. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  222. package/blocks/inbox/components/thread-badge.tsx +2 -2
  223. package/blocks/inbox/components/thread-view.tsx +1 -1
  224. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  225. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  226. package/blocks/inbox/lib/inbox-types.ts +47 -1
  227. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  228. package/blocks/integrations/README.md +63 -1
  229. package/blocks/integrations/block.json +234 -15
  230. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  231. package/blocks/integrations/components/connection-health.tsx +59 -0
  232. package/blocks/integrations/components/crm-detail.tsx +119 -0
  233. package/blocks/integrations/components/crm-records.tsx +103 -0
  234. package/blocks/integrations/components/field-mappings.tsx +61 -0
  235. package/blocks/integrations/components/integration-list.tsx +13 -8
  236. package/blocks/integrations/components/integrations-screen.tsx +152 -0
  237. package/blocks/integrations/components/migration-panel.tsx +133 -0
  238. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  239. package/blocks/integrations/components/sync-status.tsx +11 -9
  240. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  241. package/blocks/integrations/components/webhook-detail.tsx +179 -0
  242. package/blocks/integrations/components/webhook-manager.tsx +43 -25
  243. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  244. package/blocks/integrations/lib/crm-utils.ts +39 -0
  245. package/blocks/integrations/lib/facts.tsx +13 -0
  246. package/blocks/integrations/lib/integrations-utils.ts +11 -1
  247. package/blocks/integrations/lib/load-more.tsx +141 -0
  248. package/blocks/intent/README.md +14 -0
  249. package/blocks/intent/block.json +27 -1
  250. package/blocks/intent/components/account-intent-score.tsx +5 -5
  251. package/blocks/intent/components/intent-feed.tsx +13 -10
  252. package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
  253. package/blocks/intent/components/intent-screen.tsx +265 -0
  254. package/blocks/intent/components/keyword-detail.tsx +124 -0
  255. package/blocks/intent/lib/intent-utils.ts +4 -4
  256. package/blocks/lifecycle/block.json +13 -8
  257. package/blocks/lifecycle/components/account-related.tsx +8 -1
  258. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  259. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  260. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  261. package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
  262. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  263. package/blocks/lists/README.md +20 -1
  264. package/blocks/lists/block.json +69 -3
  265. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  266. package/blocks/lists/components/add-to-list-button.tsx +20 -9
  267. package/blocks/lists/components/list-picker.tsx +17 -3
  268. package/blocks/lists/components/list-table.tsx +12 -6
  269. package/blocks/lists/components/lists-screen.tsx +346 -0
  270. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  271. package/blocks/lists/components/suppression-manager.tsx +33 -8
  272. package/blocks/lists/components/suppressions-screen.tsx +264 -0
  273. package/blocks/mailboxes/README.md +17 -0
  274. package/blocks/mailboxes/block.json +69 -17
  275. package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
  276. package/blocks/mailboxes/components/domain-detail.tsx +148 -0
  277. package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
  278. package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
  279. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  280. package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
  281. package/blocks/mailboxes/components/warmup-health.tsx +24 -22
  282. package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
  283. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  284. package/blocks/newsletter/README.md +12 -0
  285. package/blocks/newsletter/block.json +157 -24
  286. package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
  287. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  288. package/blocks/newsletter/components/more-rows.tsx +30 -0
  289. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  290. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  291. package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
  292. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  293. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  294. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  295. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  296. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  297. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  298. package/blocks/nurture/block.json +28 -14
  299. package/blocks/nurture/components/nurture-builder.tsx +13 -3
  300. package/blocks/nurture/components/nurture-enroll.tsx +12 -10
  301. package/blocks/nurture/components/nurture-form.tsx +27 -10
  302. package/blocks/nurture/components/nurture-screen.tsx +26 -11
  303. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  304. package/blocks/nurture/lib/nurture-types.ts +1 -1
  305. package/blocks/objects/README.md +18 -1
  306. package/blocks/objects/block.json +42 -21
  307. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  308. package/blocks/objects/components/object-not-found.tsx +3 -1
  309. package/blocks/objects/components/object-record-detail.tsx +10 -2
  310. package/blocks/objects/components/object-schema-editor.tsx +4 -2
  311. package/blocks/objects/components/object-table.tsx +13 -3
  312. package/blocks/objects/components/objects-screen.tsx +147 -0
  313. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  314. package/blocks/objects/lib/object-types.ts +1 -1
  315. package/blocks/pages/README.md +14 -0
  316. package/blocks/pages/block.json +140 -23
  317. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  318. package/blocks/pages/components/content-library.tsx +9 -4
  319. package/blocks/pages/components/more-rows.tsx +30 -0
  320. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  321. package/blocks/pages/components/page-analytics.tsx +176 -0
  322. package/blocks/pages/components/page-clone.tsx +99 -0
  323. package/blocks/pages/components/page-domains.tsx +167 -0
  324. package/blocks/pages/components/page-editor.tsx +1 -1
  325. package/blocks/pages/components/page-generation-status.tsx +48 -0
  326. package/blocks/pages/components/page-list.tsx +2 -1
  327. package/blocks/pages/components/page-tracking.tsx +137 -0
  328. package/blocks/pages/components/pages-screen.tsx +57 -16
  329. package/blocks/pages/components/seo-report.tsx +6 -1
  330. package/blocks/pages/lib/studio.ts +7 -0
  331. package/blocks/phone-numbers/README.md +74 -0
  332. package/blocks/phone-numbers/block.json +262 -0
  333. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  334. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  335. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  336. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  337. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  338. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  339. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  340. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  341. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  342. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  343. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  344. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  345. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  346. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  347. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  348. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  349. package/blocks/pickers/README.md +23 -0
  350. package/blocks/pickers/block.json +103 -0
  351. package/blocks/pickers/components/company-picker.tsx +90 -0
  352. package/blocks/pickers/components/contact-picker.tsx +131 -0
  353. package/blocks/pickers/components/member-picker.tsx +99 -0
  354. package/blocks/pickers/components/search-picker.tsx +98 -0
  355. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  356. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  357. package/blocks/pickers/lib/use-debounced.ts +13 -0
  358. package/blocks/quotes/README.md +13 -0
  359. package/blocks/quotes/block.json +65 -13
  360. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  361. package/blocks/quotes/components/product-picker.tsx +3 -1
  362. package/blocks/quotes/components/quote-builder.tsx +1 -1
  363. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  364. package/blocks/quotes/components/quote-list.tsx +5 -3
  365. package/blocks/quotes/components/quote-related.tsx +8 -4
  366. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  367. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  368. package/blocks/radar/README.md +10 -0
  369. package/blocks/radar/block.json +41 -1
  370. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  371. package/blocks/radar/components/competitor-detail.tsx +117 -0
  372. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  373. package/blocks/radar/components/radar-overview.tsx +4 -4
  374. package/blocks/radar/components/radar-report.tsx +4 -2
  375. package/blocks/radar/components/radar-screen.tsx +214 -0
  376. package/blocks/radar/components/report-detail.tsx +104 -0
  377. package/blocks/search/README.md +10 -0
  378. package/blocks/search/block.json +16 -1
  379. package/blocks/search/components/filter-builder.tsx +3 -3
  380. package/blocks/search/components/prospect-results.tsx +5 -3
  381. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  382. package/blocks/search/components/saved-searches.tsx +4 -1
  383. package/blocks/search/hooks/use-filter-options.ts +20 -6
  384. package/blocks/search/lib/search-types.ts +2 -0
  385. package/blocks/sequencer/README.md +55 -9
  386. package/blocks/sequencer/block.json +126 -19
  387. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  388. package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
  389. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  390. package/blocks/sequencer/components/sequence-builder.tsx +224 -60
  391. package/blocks/sequencer/components/sequence-detail.tsx +261 -0
  392. package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
  393. package/blocks/sequencer/components/sequence-list.tsx +34 -9
  394. package/blocks/sequencer/components/sequence-stats.tsx +11 -10
  395. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  396. package/blocks/sequencer/components/sequences-screen.tsx +162 -0
  397. package/blocks/sequencer/components/step-editor.tsx +14 -12
  398. package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
  399. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  400. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  401. package/blocks/social-ads/README.md +19 -0
  402. package/blocks/social-ads/block.json +233 -1
  403. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  404. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  405. package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
  406. package/blocks/social-ads/components/audience-sync.tsx +17 -9
  407. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  408. package/blocks/social-ads/components/campaign-detail.tsx +138 -0
  409. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  410. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  411. package/blocks/social-ads/components/creative-detail.tsx +155 -0
  412. package/blocks/social-ads/components/creative-gallery.tsx +35 -7
  413. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  414. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  415. package/blocks/social-ads/components/post-composer.tsx +7 -3
  416. package/blocks/social-ads/components/post-detail.tsx +163 -0
  417. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  418. package/blocks/social-ads/components/social-performance.tsx +80 -0
  419. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  420. package/blocks/social-ads/components/social-screen.tsx +187 -0
  421. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  422. package/blocks/surveys/README.md +91 -0
  423. package/blocks/surveys/block.json +299 -0
  424. package/blocks/surveys/components/account-nps.tsx +69 -0
  425. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  426. package/blocks/surveys/components/org-summary.tsx +82 -0
  427. package/blocks/surveys/components/sending-domains.tsx +113 -0
  428. package/blocks/surveys/components/survey-form.tsx +126 -0
  429. package/blocks/surveys/components/survey-responses.tsx +145 -0
  430. package/blocks/surveys/components/survey-results.tsx +72 -0
  431. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  432. package/blocks/surveys/components/survey-send.tsx +162 -0
  433. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  434. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  435. package/blocks/surveys/lib/surveys-types.ts +228 -0
  436. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  437. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  438. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  439. package/blocks/visitors/README.md +10 -0
  440. package/blocks/visitors/block.json +25 -1
  441. package/blocks/visitors/components/snippet-install.tsx +21 -10
  442. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  443. package/blocks/visitors/components/visitor-company-card.tsx +26 -16
  444. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  445. package/blocks/visitors/components/visitor-feed.tsx +16 -7
  446. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  447. package/blocks/webchat/README.md +10 -0
  448. package/blocks/webchat/block.json +21 -7
  449. package/blocks/webchat/components/chat-widget-config.tsx +8 -5
  450. package/blocks/webchat/components/conversation-list.tsx +8 -4
  451. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  452. package/blocks/workbench/README.md +11 -0
  453. package/blocks/workbench/block.json +48 -1
  454. package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
  455. package/blocks/workbench/components/run-progress.tsx +1 -1
  456. package/blocks/workbench/components/workbench-grid.tsx +10 -4
  457. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  458. package/blocks/workbench/components/workbench-screen.tsx +202 -0
  459. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  460. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  461. package/blocks/workflows/README.md +13 -0
  462. package/blocks/workflows/block.json +28 -9
  463. package/blocks/workflows/components/run-history.tsx +31 -9
  464. package/blocks/workflows/components/trigger-config.tsx +3 -1
  465. package/blocks/workflows/components/workflow-editor.tsx +4 -2
  466. package/blocks/workflows/components/workflow-list.tsx +13 -6
  467. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  468. package/dist/cli.js +3109 -1977
  469. package/dist/errors.d.mts +34 -0
  470. package/dist/errors.d.ts +34 -0
  471. package/dist/errors.js +46 -0
  472. package/dist/errors.js.map +1 -0
  473. package/dist/errors.mjs +21 -0
  474. package/dist/errors.mjs.map +1 -0
  475. package/dist/index.browser.js +11 -5
  476. package/dist/index.browser.js.map +1 -1
  477. package/dist/index.browser.mjs +11 -5
  478. package/dist/index.browser.mjs.map +1 -1
  479. package/dist/index.d.mts +164 -25
  480. package/dist/index.d.ts +164 -25
  481. package/dist/index.js +11 -5
  482. package/dist/index.js.map +1 -1
  483. package/dist/index.mjs +11 -5
  484. package/dist/index.mjs.map +1 -1
  485. package/dist/react.browser.js +10 -4
  486. package/dist/react.browser.js.map +1 -1
  487. package/dist/react.browser.mjs +10 -4
  488. package/dist/react.browser.mjs.map +1 -1
  489. package/dist/react.d.mts +142 -5
  490. package/dist/react.d.ts +142 -5
  491. package/dist/react.js +10 -4
  492. package/dist/react.js.map +1 -1
  493. package/dist/react.mjs +10 -4
  494. package/dist/react.mjs.map +1 -1
  495. package/dist/types/blocks/add.d.ts +11 -4
  496. package/dist/types/blocks/brand.d.ts +122 -0
  497. package/dist/types/blocks/compose.d.ts +53 -0
  498. package/dist/types/blocks/create.d.ts +27 -20
  499. package/dist/types/blocks/doctor.d.ts +6 -1
  500. package/dist/types/blocks/flows.d.ts +7 -1
  501. package/dist/types/blocks/navfile.d.ts +35 -0
  502. package/dist/types/blocks/orgBrand.d.ts +34 -0
  503. package/dist/types/blocks/paths.d.ts +4 -0
  504. package/dist/types/blocks/prefixes.d.ts +37 -0
  505. package/dist/types/blocks/registry.d.ts +17 -0
  506. package/dist/types/blocks/screens.d.ts +53 -0
  507. package/dist/types/cli.d.ts +5 -0
  508. package/dist/types/errors.d.ts +33 -0
  509. package/dist/types/http.d.ts +2 -18
  510. package/dist/types/version.d.ts +2 -2
  511. package/docs/agents.md +15 -4
  512. package/docs/api/README.md +2 -2
  513. package/docs/api/by-path.md +1 -0
  514. package/docs/api/desk.d.ts +98 -2
  515. package/docs/api/desk.md +123 -1
  516. package/docs/api/inbox.d.ts +10 -2
  517. package/docs/api/inbox.md +10 -2
  518. package/docs/api/seo.d.ts +19 -1
  519. package/docs/api/seo.md +24 -1
  520. package/package.json +16 -2
  521. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  522. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  523. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  524. package/stacks/6sense.json +55 -17
  525. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  526. package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
  527. package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
  528. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  529. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  530. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  531. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  532. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  533. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  534. package/stacks/apollo.json +147 -37
  535. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  536. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  537. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  538. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  539. package/stacks/calendly.json +64 -0
  540. package/stacks/clay/pages/companies.tsx.tmpl +31 -0
  541. package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
  542. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  543. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  544. package/stacks/clay.json +73 -27
  545. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  546. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  547. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  548. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  549. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  550. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  551. package/stacks/gong.json +98 -35
  552. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  553. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  554. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  555. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  556. package/stacks/hootsuite.json +104 -0
  557. package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
  558. package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
  559. package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
  560. package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
  561. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  562. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  563. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  564. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  565. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  566. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  567. package/stacks/hubspot.json +181 -64
  568. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  569. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  570. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  571. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  572. package/stacks/intercom.json +52 -20
  573. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  574. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  575. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  576. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  577. package/stacks/mailchimp.json +68 -34
  578. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  579. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  580. package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
  581. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  582. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  583. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  584. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  585. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  586. package/stacks/outreach.json +106 -25
  587. package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
  588. package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
  589. package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
  590. package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
  591. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  592. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  593. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  594. package/stacks/pipedrive.json +117 -0
  595. package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
  596. package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
  597. package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
  598. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  599. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  600. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  601. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  602. package/stacks/salesforce.json +85 -31
  603. package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
  604. package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
  605. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  606. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  607. package/stacks/zoominfo.json +115 -0
  608. package/styles/styles.css +1 -1
  609. package/templates/next-app/README.md.tmpl +40 -9
  610. package/templates/next-app/app/globals.css.tmpl +349 -64
  611. package/templates/next-app/app/layout.tsx.tmpl +7 -2
  612. package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
  613. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
  614. package/templates/next-app/dot-gitignore.tmpl +2 -0
  615. package/templates/next-app/package.json.tmpl +1 -1
@@ -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
+ }
@@ -0,0 +1,155 @@
1
+ "use client";
2
+
3
+ /**
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
+ * 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. The AI tools tab edits by
7
+ * instruction, rewrites a field, reviews, makes other sizes and more (CreativeTools); Revisions lists and restores earlier versions.
8
+ *
9
+ * SDK operations, in order:
10
+ * 1. listAdCreatives GET /ad-creatives?limit=100 (read)
11
+ * 2. updateAdCreative PATCH /ad-creatives/{id} (write)
12
+ * 3. regenerateAdCreativeImage POST /ad-creatives/{id}/regenerate-image (billable, confirmed; no advance price)
13
+ * 4. moveAdCreativeToGallery POST /ad-creatives/{id}/move-to-gallery (write)
14
+ * 5. deleteAdCreative DELETE /ad-creatives/{id} (destructive, confirmed; 404 counts as done)
15
+ * 6. getAdCreativeStatus GET /ad-creatives/{id}/status (read; polled every 4 s while the creative is generating.
16
+ * The payload is open-shaped in the contract: only a string `status` is read, and a change reloads the creative.)
17
+ */
18
+ import { useEffect, useState } from "react";
19
+ import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
20
+ import { ApiError } from "@/g8/components/api-error";
21
+ import { EmptyState } from "@/g8/components/empty-state";
22
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
23
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
24
+ import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
25
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
26
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
27
+ import { SectionCard } from "@/g8/components/screen/section-card";
28
+ import { StatusPill } from "@/g8/components/screen/status-pill";
29
+ import type { AdCreative } from "../lib/social-ads-types";
30
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
31
+ import { AskButton, useConfirmAction } from "@/g8/components/confirmed-action";
32
+ import { CreativeRevisions } from "./creative-revisions";
33
+ import { CreativeTools } from "./creative-tools";
34
+
35
+ export interface CreativeDetailProps {
36
+ creativeId: string;
37
+ onClose?: () => void;
38
+ onExpand?: () => void;
39
+ onCollapse?: () => void;
40
+ onChanged?: () => void;
41
+ }
42
+
43
+ const tone = (s: string) => (s === "gallery" || s === "completed" ? "positive" : s === "failed" ? "negative" : s === "generating" ? "info" : "neutral") as "positive" | "negative" | "info" | "neutral";
44
+
45
+ export function CreativeDetail({ creativeId, onClose, onExpand, onCollapse, onChanged }: CreativeDetailProps) {
46
+ const { ask, dialog, failure } = useConfirmAction();
47
+ // Read the one creative by id (GET /ad-creatives/{id}), so an older creative outside the newest page still opens.
48
+ // A 404 means it no longer exists.
49
+ const list = useG8Query(async (signal) => {
50
+ try {
51
+ const body = (await g8Get<AdCreative | { data?: AdCreative }>(`/ad-creatives/${encodeURIComponent(creativeId)}`, undefined, { signal })).data;
52
+ const one = body && typeof body === "object" && "data" in body && body.data ? body.data : (body as AdCreative | null);
53
+ return one && one.id != null ? one : null;
54
+ } catch (err) {
55
+ if ((err as { status?: number })?.status === 404) return null;
56
+ throw err;
57
+ }
58
+ }, [creativeId]);
59
+ const creative = list.data ?? undefined;
60
+ const [form, setForm] = useState({ headline: "", primary_text: "", description: "", cta: "" });
61
+ const generating = creative?.status === "generating";
62
+
63
+ useEffect(() => {
64
+ if (!creative) return;
65
+ setForm({ headline: creative.headline ?? "", primary_text: creative.primary_text ?? "", description: creative.description ?? "", cta: creative.cta ?? "" });
66
+ }, [creative?.id, creative?.headline, creative?.primary_text, creative?.description, creative?.cta]); // eslint-disable-line react-hooks/exhaustive-deps
67
+
68
+ useEffect(() => {
69
+ if (!generating) return;
70
+ const timer = setInterval(() => {
71
+ void g8Get(`/ad-creatives/${creativeId}/status`)
72
+ .then((res) => {
73
+ const next = (res.data as { status?: unknown } | null | undefined)?.status;
74
+ if (typeof next === "string" && next !== "generating") void list.refetch();
75
+ })
76
+ .catch(() => void list.refetch());
77
+ }, 4000);
78
+ return () => clearInterval(timer);
79
+ }, [generating, creativeId, list.refetch]); // eslint-disable-line react-hooks/exhaustive-deps
80
+
81
+ const save = useG8Mutation(() => g8Patch(`/ad-creatives/${creativeId}`, form), { onSuccess: () => { void list.refetch(); onChanged?.(); } });
82
+ const gallery = useG8Mutation(() => g8Post(`/ad-creatives/${creativeId}/move-to-gallery`, {}), { onSuccess: () => { void list.refetch(); onChanged?.(); } });
83
+ const changed = () => { void list.refetch(); onChanged?.(); };
84
+
85
+ if (list.error) return <ApiError error={list.error} title="Could not load the creative" onRetry={() => void list.refetch()} />;
86
+ if (list.data === undefined) return <Skeleton className="h-40 w-full" />;
87
+ if (!creative) return <EmptyState title="Creative not found" description="It may have been deleted." />;
88
+
89
+ const actions = (
90
+ <div className="flex flex-wrap gap-1">
91
+ <AskButton ask={ask} label="New image" tier="billable" title={`Make a new image for "${creative.name}"`} confirmLabel="Generate" disabled={generating}
92
+ description="Uses credits. The API gives no price in advance and your balance is not checked here." run={() => g8Post(`/ad-creatives/${creativeId}/regenerate-image`, {})} onDone={changed} />
93
+ {creative.status !== "gallery" && !generating ? (
94
+ <Button size="sm" variant="outline" disabled={gallery.loading} onClick={() => void gallery.mutate(undefined)}>Move to gallery</Button>
95
+ ) : null}
96
+ <AskButton ask={ask} label="Delete" variant="ghost" tier="destructive" title={`Delete "${creative.name}"`} confirmLabel="Delete"
97
+ description="The creative and its revisions are removed." tolerateStatus={[404]} run={() => g8Delete(`/ad-creatives/${creativeId}`)} onDone={() => { changed(); onClose?.(); }} />
98
+ </div>
99
+ );
100
+
101
+ const field = (key: keyof typeof form, label: string) => (
102
+ <div className="flex flex-col gap-1" key={key}>
103
+ <Label htmlFor={`cd-${key}`}>{label}</Label>
104
+ <Input id={`cd-${key}`} value={form[key]} onChange={(e) => setForm({ ...form, [key]: e.target.value })} />
105
+ </div>
106
+ );
107
+ const tabs = [
108
+ {
109
+ id: "creative",
110
+ label: "Creative",
111
+ content: (
112
+ <div className="space-y-4">
113
+ {creative.image_url ? <img src={creative.image_url} alt={creative.headline ?? creative.name} className="aspect-video w-full rounded object-cover" /> : (
114
+ <div className="flex aspect-video items-center justify-center rounded bg-muted text-sm text-muted-foreground">{generating ? "Generating..." : "No image"}</div>
115
+ )}
116
+ {creative.status === "failed" && creative.error_message ? <p role="alert" className="text-sm text-error-foreground">{creative.error_message}</p> : null}
117
+ <KeyValueList columns={2} items={[
118
+ { label: "Platform", value: creative.platform },
119
+ { label: "Format", value: creative.ad_format },
120
+ { label: "Headline", value: creative.headline ?? "-" },
121
+ { label: "Call to action", value: creative.cta ?? "-" },
122
+ { label: "Primary text", value: creative.primary_text ?? "-" },
123
+ { label: "Description", value: creative.description ?? "-" },
124
+ ]} />
125
+ </div>
126
+ ),
127
+ },
128
+ {
129
+ id: "edit",
130
+ label: "Edit copy",
131
+ content: (
132
+ <SectionCard title="Edit copy">
133
+ <div className="space-y-3">
134
+ {field("headline", "Headline")}
135
+ {field("primary_text", "Primary text")}
136
+ {field("description", "Description")}
137
+ {field("cta", "Call to action")}
138
+ <Button size="sm" disabled={save.loading || generating} onClick={() => void save.mutate(undefined)}>Save copy</Button>
139
+ {save.error ? <ApiError error={save.error} title="Could not save" /> : null}
140
+ {gallery.error ? <ApiError error={gallery.error} title="Could not move to the gallery" /> : null}
141
+ </div>
142
+ </SectionCard>
143
+ ),
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} /> },
147
+ ];
148
+ return (
149
+ <>
150
+ <DetailPanel title={creative.headline || creative.name} subtitle={`${creative.platform} · ${creative.ad_format}`} badges={<StatusPill tone={tone(creative.status)}>{creative.status}</StatusPill>} actions={actions} tabs={tabs} onClose={onClose} onExpand={onExpand} onCollapse={onCollapse} />
151
+ {failure ? <ApiError error={failure} title="That did not work" /> : null}
152
+ <ConfirmSpendDialog {...dialog} />
153
+ </>
154
+ );
155
+ }
@@ -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,31 +25,45 @@ 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 {
33
38
  /** Show only creatives in this state. Default: all working creatives. */
34
39
  status?: "draft" | "completed" | "gallery";
40
+ /** Called when a person opens a creative (show a CreativeDetail). */
41
+ onSelectCreative?: (creative: AdCreative) => void;
35
42
  className?: string;
36
43
  }
37
44
 
38
45
  interface Specs { platforms: string[]; specs: Record<string, Record<string, { label: string }>> }
39
46
  type Pending = { kind: "create" } | { kind: "regenerate" | "delete"; creative: AdCreative };
40
47
 
41
- export function CreativeGallery({ status, className }: CreativeGalleryProps) {
48
+ /** Creatives per read; "Load more" appends the next page (the gallery used to stop silently at 50). */
49
+ const CREATIVE_PAGE = 50;
50
+
51
+ export function CreativeGallery({ status, onSelectCreative, className }: CreativeGalleryProps) {
42
52
  const [platform, setPlatform] = useState("");
43
53
  const [form, setForm] = useState({ platform: "meta", format: "", prompt: "", name: "" });
44
54
  const [pending, setPending] = useState<Pending | null>(null);
45
- const [editing, setEditing] = useState<{ id: string; headline: string; primary_text: string } | null>(null);
55
+ const [editing, setEditing] = useState<{ id: string; headline: string; primary_text: string; orig: { headline: string; primary_text: string } } | null>(null);
46
56
  const [busy, setBusy] = useState<string | null>(null);
47
57
  const [failure, setFailure] = useState<unknown>(null);
48
58
 
49
59
  const specs = useG8Query(async (signal) => (await g8Get<Specs>("/ad-creatives/specs", undefined, { signal })).data, []);
50
- const list = useG8Query(async (signal) => listOf<AdCreative>((await g8Get("/ad-creatives", { status, platform: platform || undefined, limit: 50 }, { signal })).data), [status, platform]);
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]);
51
65
  const items = list.data ?? [];
66
+ const [boardOpen, setBoardOpen] = useState(false);
52
67
  const generating = items.filter((c) => c.status === "generating");
53
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") });
54
69
 
@@ -68,7 +83,15 @@ export function CreativeGallery({ status, className }: CreativeGalleryProps) {
68
83
  if (pending.kind === "regenerate") return void run(id, () => g8Post(`/ad-creatives/${id}/regenerate-image`, {}));
69
84
  return void run(id, () => g8Delete(`/ad-creatives/${id}`), true);
70
85
  };
71
- const saveCopy = () => editing && void run(editing.id, async () => { await g8Patch(`/ad-creatives/${editing.id}`, { headline: editing.headline, primary_text: editing.primary_text }); setEditing(null); });
86
+ // Only the field(s) the person changed are sent (the backend overwrites whatever it receives, and a regenerate may finish while the copy is open); nothing changed means no request.
87
+ const saveCopy = () => {
88
+ if (!editing) return;
89
+ const changed: Record<string, string> = {};
90
+ if (editing.headline !== editing.orig.headline) changed.headline = editing.headline;
91
+ if (editing.primary_text !== editing.orig.primary_text) changed.primary_text = editing.primary_text;
92
+ if (Object.keys(changed).length === 0) return setEditing(null);
93
+ void run(editing.id, async () => { await g8Patch(`/ad-creatives/${editing.id}`, changed); setEditing(null); });
94
+ };
72
95
 
73
96
  const tier: ConfirmTier = pending?.kind === "delete" ? "destructive" : "billable";
74
97
  const name = pending && pending.kind !== "create" ? pending.creative.name : "";
@@ -101,10 +124,10 @@ export function CreativeGallery({ status, className }: CreativeGalleryProps) {
101
124
  <Input aria-label="Primary text" value={editing.primary_text} onChange={(e) => setEditing({ ...editing, primary_text: e.target.value })} />
102
125
  <div className="flex gap-1"><Button size="sm" loading={busy === c.id} onClick={saveCopy}>Save</Button><Button size="sm" variant="ghost" onClick={() => setEditing(null)}>Cancel</Button></div>
103
126
  </div>
104
- ) : <div><p className="text-sm font-medium">{c.headline || c.name}</p><p className="line-clamp-2 text-sm text-muted-foreground">{c.primary_text}</p></div>}
127
+ ) : <div>{onSelectCreative ? <button type="button" className="text-left text-sm font-medium hover:underline" onClick={() => onSelectCreative(c)}>{c.headline || c.name}</button> : <p className="text-sm font-medium">{c.headline || c.name}</p>}<p className="line-clamp-2 text-sm text-muted-foreground">{c.primary_text}</p></div>}
105
128
  {c.status === "failed" && c.error_message ? <p className="text-xs text-destructive">{c.error_message}</p> : null}
106
129
  <div className="flex flex-wrap gap-1">
107
- <Button size="sm" variant="ghost" disabled={busy != null || c.status === "generating"} onClick={() => setEditing({ id: c.id, headline: c.headline ?? "", primary_text: c.primary_text ?? "" })}>Edit copy</Button>
130
+ <Button size="sm" variant="ghost" disabled={busy != null || c.status === "generating"} onClick={() => setEditing({ id: c.id, headline: c.headline ?? "", primary_text: c.primary_text ?? "", orig: { headline: c.headline ?? "", primary_text: c.primary_text ?? "" } })}>Edit copy</Button>
108
131
  <Button size="sm" variant="ghost" loading={busy === c.id} disabled={busy != null || c.status === "generating"} onClick={() => setPending({ kind: "regenerate", creative: c })}>New image</Button>
109
132
  {c.status !== "gallery" && c.status !== "generating" ? <Button size="sm" variant="outline" disabled={busy != null} onClick={() => void run(c.id, () => g8Post(`/ad-creatives/${c.id}/move-to-gallery`, {}))}>Move to gallery</Button> : null}
110
133
  <Button size="sm" variant="ghost" disabled={busy != null} onClick={() => setPending({ kind: "delete", creative: c })}>Delete</Button>
@@ -112,6 +135,11 @@ export function CreativeGallery({ status, className }: CreativeGalleryProps) {
112
135
  </li>
113
136
  ))}
114
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>
115
143
  <ConfirmSpendDialog open={pending != null} tier={tier}
116
144
  title={pending?.kind === "create" ? `Generate a ${form.platform} creative` : pending?.kind === "regenerate" ? `Make a new image for "${name}"` : `Delete "${name}"`}
117
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
+ }