@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
@@ -2,31 +2,47 @@
2
2
 
3
3
  /**
4
4
  * CampaignLaunchStep: the last check before a campaign starts messaging real people. Shows what is missing
5
- * (audience, steps, a sender), warns when deliverability is poor, lets the person choose the sending
6
- * mailboxes, and launches behind a confirmation that states the recipient count.
5
+ * (audience, steps, written copy, a sender), warns when deliverability is poor or a chosen mailbox is still in
6
+ * its warm-up hold, lets the person write missing copy with AI, choose the sending mailboxes, and launches
7
+ * behind a confirmation that states the recipient count.
7
8
  *
8
9
  * SDK operations, in order:
9
- * 1. getCampaignFull GET /campaigns/{campaign_id}/full (read; audience attached and its size, steps, is_launched)
10
- * 3. listMailboxes GET /mailboxes (read; active senders to choose from)
11
- * 4. getSafeToSend GET /deliverability/safe-to-send (read; a warning, never a block)
12
- * 5. launchCampaign POST /campaigns/{campaign_id}/launch { sender_mailbox_ids }
13
- * (external, ConfirmSpendDialog with the recipient count)
10
+ * 1. getCampaignFull GET /campaigns/{campaign_id}/full (read; audience and size, steps, documents, is_launched;
11
+ * POLLED while copy is being written)
12
+ * 2. getCampaignGenerationStatus GET /campaigns/{campaign_id}/status (read; `generation_error` when document generation failed)
13
+ * 3. listMailboxes GET /mailboxes (read; active senders to choose from, `created_at` for the hold)
14
+ * 4. getSafeToSend GET /deliverability/safe-to-send (read; a warning, never a block)
15
+ * 5. createCampaignDocument POST /campaigns/{campaign_id}/documents
16
+ * (external; only when no email copy document exists, part of "Write the email copy")
17
+ * regenerateCampaignDocument POST /campaigns/{campaign_id}/documents/{document_id}/regenerate
18
+ * (billable, ConfirmSpendDialog; writes the email copy with AI)
19
+ * 6. launchCampaign POST /campaigns/{campaign_id}/launch { sender_mailbox_ids }
20
+ * (external, ConfirmSpendDialog with the recipient count)
14
21
  *
15
- * Tiers: 5 begins REAL outreach and cannot be recalled. The result is kept in state, so the button is
22
+ * Copy: the launch is refused (400 "no completed copy documents") unless a document other than the sequence and
23
+ * step catalog is completed, so Launch stays disabled until one is. A campaign created without AI documents has
24
+ * none; "Write the email copy with AI" creates the email document (once: its id is kept, and an existing one is
25
+ * reused, so a retry never makes a second) and has AI write it.
26
+ *
27
+ * Tiers: 6 begins REAL outreach and cannot be recalled. The result is kept in state, so the button is
16
28
  * gone after a success and a double click cannot launch twice. After an error, do not blindly retry: a 502
17
29
  * `draft_created_activation_failed` or `activation_state_uncertain` means a sequence may exist; check the
18
- * campaign in the sequencer block first. `is_launched` from /full also refuses a second launch after a reload. Google and Microsoft mailboxes are connected in the graph8 app.
30
+ * campaign in the sequencer block first. `is_launched` from /full (true only for a live launch, not a failed
31
+ * attempt) also refuses a second launch after a reload. Google and Microsoft mailboxes are connected in the graph8 app.
19
32
  */
20
33
 
21
- import { useState } from "react";
34
+ import { useEffect, useState } from "react";
22
35
  import { Button, Skeleton } from "@graph8/sdk/ui";
23
36
  import { ApiError } from "@/g8/components/api-error";
37
+ import { FirstNotice } from "@/g8/components/paging-notices";
24
38
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
25
39
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
26
40
  import { useG8Query } from "@/g8/hooks/use-g8-query";
41
+ import { useJobPoll } from "@/g8/hooks/use-job-poll";
27
42
  import { g8Get, g8Post } from "@/g8/lib/g8-client";
28
- import { GRAPH8_APP_MAILBOXES_URL } from "@/g8/mailboxes/lib/mailboxes-utils";
29
- import { audienceOf, listOf, objectOf, stepsOf } from "../lib/campaigns-utils";
43
+ import { GRAPH8_APP_MAILBOXES_PATH } from "@/g8/mailboxes/lib/mailboxes-utils";
44
+ import { CREDIT_ESTIMATES, EMAILS_DOCUMENT, WARMUP_HOLD_DAYS, audienceOf, copyStateOf, listOf, objectOf, stepsOf, text, warmupHoldUntil } from "../lib/campaigns-utils";
45
+ import { brandUrl, useBrand } from "@/g8/hooks/use-brand";
30
46
 
31
47
  export interface CampaignLaunchStepProps {
32
48
  campaignId: string;
@@ -36,24 +52,46 @@ export interface CampaignLaunchStepProps {
36
52
  className?: string;
37
53
  }
38
54
 
39
- interface Mailbox { id: string | number; email?: string | null; connection_status?: string | null; is_archived?: boolean | null }
55
+ interface Mailbox { id: string | number; email?: string | null; connection_status?: string | null; is_archived?: boolean | null; created_at?: string | null }
40
56
 
41
- function Check({ ok, children }: { ok: boolean | undefined; children: string }) {
57
+ function Check({ ok, children, detail }: { ok: boolean | undefined; children: string; detail?: string }) {
42
58
  return (
43
- <li className={ok ? "text-green-700" : "text-amber-800"}>
59
+ <li className={ok ? "text-positive-foreground" : "text-warning-foreground"}>
44
60
  <span aria-hidden>{ok ? "Ready: " : "Missing: "}</span>{children}
61
+ {!ok && detail ? <span className="block pl-4 text-xs text-muted-foreground">{detail}</span> : null}
45
62
  </li>
46
63
  );
47
64
  }
48
65
 
66
+ const day = (d: Date) => d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" });
67
+
68
+ /** The mailboxes API has no paging: a full read is announced under the sender list. */
69
+ const MAILBOX_LIMIT = 200;
70
+
49
71
  export function CampaignLaunchStep({ campaignId, audienceSize, onLaunched, className }: CampaignLaunchStepProps) {
72
+ const brand = useBrand();
50
73
  const campaign = useG8Query((signal) => g8Get(`/campaigns/${campaignId}/full`, undefined, { signal }), [campaignId]);
51
- const mailboxes = useG8Query((signal) => g8Get("/mailboxes", { limit: 200 }, { signal }), []);
74
+ const generation = useG8Query((signal) => g8Get(`/campaigns/${campaignId}/status`, undefined, { signal }), [campaignId]);
75
+ const mailboxes = useG8Query((signal) => g8Get("/mailboxes", { limit: MAILBOX_LIMIT }, { signal }), []);
52
76
  const verdict = useG8Query((signal) => g8Get("/deliverability/safe-to-send", undefined, { signal }), []);
53
77
  const [chosen, setChosen] = useState<Set<string> | null>(null);
54
78
  const [confirm, setConfirm] = useState(false);
79
+ const [confirmCopy, setConfirmCopy] = useState(false);
80
+ const [emailsDocId, setEmailsDocId] = useState<string | null>(null);
55
81
  const [result, setResult] = useState<Record<string, unknown> | null>(null);
56
82
 
83
+ // Copy being written (by the create-time generator or by "Write the email copy"): re-read /full until it settles.
84
+ const copyPoll = useJobPoll((signal) => g8Get(`/campaigns/${campaignId}/full`, undefined, { signal }), {
85
+ enabled: false, isTerminal: (s) => !copyStateOf(s).generating, intervalMs: 3000, maxAttempts: 60,
86
+ });
87
+ const full = copyPoll.status ?? campaign.data;
88
+ const copy = copyStateOf(full);
89
+ const genError = text(objectOf(generation.data).generation_error);
90
+ // Auto-follow only once: after the poll gives up, "Still writing" asks the person to check back.
91
+ const shouldPoll = copy.generating && !genError && copyPoll.attempts === 0 && !copyPoll.polling;
92
+ useEffect(() => { if (shouldPoll) copyPoll.start(); // eslint-disable-next-line react-hooks/exhaustive-deps
93
+ }, [shouldPoll]);
94
+
57
95
  const active = listOf<Mailbox>(mailboxes.data).filter((m) => m.connection_status === "active" && !m.is_archived);
58
96
  const senders = chosen ?? new Set(active.slice(0, 1).map((m) => String(m.id)));
59
97
  const launch = useG8Mutation(
@@ -63,54 +101,92 @@ export function CampaignLaunchStep({ campaignId, audienceSize, onLaunched, class
63
101
  onError: () => setConfirm(false),
64
102
  },
65
103
  );
104
+ const writeCopy = useG8Mutation(async () => {
105
+ let docId = emailsDocId ?? copy.emailsDocId;
106
+ if (!docId) {
107
+ const created = await g8Post(`/campaigns/${campaignId}/documents`, { ...EMAILS_DOCUMENT, content: "", status: "draft" });
108
+ docId = text(objectOf(created).id);
109
+ setEmailsDocId(docId); // stored at once: a failed regenerate is retried on this document, never a second one
110
+ }
111
+ return g8Post(`/campaigns/${campaignId}/documents/${docId}/regenerate`, {});
112
+ }, {
113
+ onSuccess: () => { setConfirmCopy(false); copyPoll.start(); },
114
+ onError: () => setConfirmCopy(false),
115
+ });
66
116
 
67
117
  const loading = [campaign, mailboxes].some((q) => q.loading && !q.data);
68
118
  const failedQuery = [campaign, mailboxes].find((q) => q.error);
69
119
  if (failedQuery) return <ApiError error={failedQuery.error} title="Could not check launch readiness" onRetry={() => { void campaign.refetch(); void mailboxes.refetch(); }} />;
70
120
  if (loading) return <Skeleton className="h-40 w-full" />;
71
121
 
72
- const c = objectOf(campaign.data);
73
- const audience = audienceOf(campaign.data);
122
+ const c = objectOf(full);
123
+ const audience = audienceOf(full);
74
124
  const hasAudience = audience.listId != null;
75
- const hasSteps = stepsOf(campaign.data).length > 0;
125
+ const hasSteps = stepsOf(full).length > 0;
126
+ // An older API without `documents` cannot be checked here: the server still checks at launch.
127
+ const copyOk = !copy.known || copy.ready;
76
128
  const alreadyLaunched = c.is_launched === true;
77
129
  const recipients = audienceSize ?? audience.size;
78
130
  const v = objectOf(verdict.data);
79
131
  const unsafe = !verdict.error && (v.safe_to_send ?? v.safe) === false;
80
- const ready = hasAudience && hasSteps && senders.size > 0 && !result && !alreadyLaunched;
132
+ const ready = hasAudience && hasSteps && copyOk && senders.size > 0 && !result && !alreadyLaunched;
133
+ // Once the follow-up poll gives up, the person may write the copy again rather than wait forever.
134
+ const writingCopy = writeCopy.loading || copyPoll.polling || (copy.generating && !genError && copyPoll.attempts === 0);
135
+ const held = active.filter((m) => senders.has(String(m.id))).map((m) => ({ m, until: warmupHoldUntil(m.created_at) })).filter((h) => h.until);
136
+ const copyDetail = writingCopy
137
+ ? "The copy is being written. This usually takes a minute or two."
138
+ : "The launch is refused until the campaign has written copy. Write it with AI below, or add it in Campaign assets.";
81
139
 
82
140
  if (result) {
83
141
  const channels = (result.channels_attached as Array<{ email?: string }> | undefined) ?? [];
84
142
  return (
85
- <section aria-label="Launch result" className={`space-y-2 rounded-md border border-green-200 bg-green-50 p-4 ${className ?? ""}`}>
143
+ <section aria-label="Launch result" className={`space-y-2 rounded-md border border-positive-foreground/25 bg-positive p-4 ${className ?? ""}`}>
86
144
  <h3 className="font-medium">Campaign {String(result.status ?? "launched")}</h3>
87
145
  <p className="text-sm">{String(result.message ?? "")}</p>
88
146
  <p className="text-sm text-muted-foreground">
89
147
  Sequence {String(result.sequence_id ?? "")} with {String(result.steps_created ?? 0)} steps, sending from {channels.map((m) => m.email).join(", ") || "the chosen mailboxes"}.
90
148
  </p>
91
- {Array.isArray(result.warnings) && result.warnings.length > 0 ? <ul className="list-disc pl-5 text-sm text-amber-900">{result.warnings.map((w) => <li key={String(w)}>{String(w)}</li>)}</ul> : null}
149
+ {Array.isArray(result.warnings) && result.warnings.length > 0 ? <ul className="list-disc pl-5 text-sm text-warning-foreground">{result.warnings.map((w) => <li key={String(w)}>{String(w)}</li>)}</ul> : null}
92
150
  </section>
93
151
  );
94
152
  }
95
153
  return (
96
154
  <section aria-label="Launch" className={`space-y-3 ${className ?? ""}`}>
97
155
  <h3 className="font-medium">Ready to launch {String(c.name ?? "")}</h3>
98
- {alreadyLaunched ? <p role="status" className="rounded bg-green-50 p-2 text-sm text-green-900">This campaign has already been launched. Manage it in the sequencer.</p> : null}
156
+ {alreadyLaunched ? <p role="status" className="rounded bg-positive p-2 text-sm text-positive-foreground">This campaign has already been launched. Manage it in the sequencer.</p> : null}
99
157
  <ul className="space-y-1 text-sm">
100
158
  <Check ok={hasAudience}>An audience list is attached</Check>
101
159
  <Check ok={hasSteps}>The sequence has at least one step</Check>
160
+ {copy.known ? <Check ok={copy.ready} detail={copyDetail}>The campaign copy is written</Check> : null}
102
161
  <Check ok={senders.size > 0}>At least one active sending mailbox is selected</Check>
103
162
  </ul>
163
+ {genError && !copy.ready ? (
164
+ <p role="alert" className="rounded bg-error p-2 text-sm text-error-foreground">Writing the campaign documents failed: {genError}</p>
165
+ ) : null}
166
+ {copy.known && !copy.ready ? (
167
+ <div className="space-y-1">
168
+ {writeCopy.error ? <ApiError error={writeCopy.error} title="Could not write the copy" /> : null}
169
+ {copyPoll.done && !copyStateOf(copyPoll.status).ready && !writeCopy.loading ? <p role="status" className="text-sm text-warning-foreground">The copy did not finish. Try again, or write it in Campaign assets.</p> : null}
170
+ {!copyPoll.polling && !copyPoll.done && copyPoll.attempts > 0 ? <p role="status" className="text-sm text-warning-foreground">Still writing. Check back in a minute.</p> : null}
171
+ <Button variant="outline" loading={writeCopy.loading} disabled={writingCopy} onClick={() => setConfirmCopy(true)}>Write the email copy with AI</Button>
172
+ </div>
173
+ ) : null}
104
174
  {unsafe ? (
105
- <p role="alert" className="rounded bg-amber-50 p-2 text-sm text-amber-900">
175
+ <p role="alert" className="rounded bg-warning p-2 text-sm text-warning-foreground">
106
176
  Deliverability warning: {typeof v.reason === "string" ? v.reason : "sending is not recommended right now."}
107
177
  </p>
108
178
  ) : null}
179
+ {held.length > 0 ? (
180
+ <div role="status" className="rounded bg-warning p-2 text-sm text-warning-foreground">
181
+ <p>New mailboxes send nothing for their first {WARMUP_HOLD_DAYS} days while they warm up. The campaign goes live, but no email goes out from:</p>
182
+ <ul className="list-disc pl-5">{held.map(({ m, until }) => <li key={m.id}>{m.email ?? `Mailbox ${m.id}`} until {day(until as Date)}</li>)}</ul>
183
+ </div>
184
+ ) : null}
109
185
  <fieldset className="space-y-1">
110
186
  <legend className="text-sm font-medium">Send from</legend>
111
187
  {active.length === 0 ? (
112
188
  <p className="text-sm text-muted-foreground">
113
- No active mailbox. <a className="underline" href={GRAPH8_APP_MAILBOXES_URL} target="_blank" rel="noreferrer">Connect one in graph8</a>, or use the mailboxes block.
189
+ No active mailbox. <a className="underline" href={brandUrl(brand, GRAPH8_APP_MAILBOXES_PATH)} target="_blank" rel="noreferrer">Connect one in {brand.linkLabel}</a>, or use the mailboxes block.
114
190
  </p>
115
191
  ) : null}
116
192
  {active.map((m) => (
@@ -123,11 +199,12 @@ export function CampaignLaunchStep({ campaignId, audienceSize, onLaunched, class
123
199
  {m.email ?? `Mailbox ${m.id}`}
124
200
  </label>
125
201
  ))}
202
+ <FirstNotice shown={listOf<Mailbox>(mailboxes.data).length} limit={MAILBOX_LIMIT} noun="mailboxes" />
126
203
  </fieldset>
127
204
  {launch.error ? (
128
205
  <div className="space-y-1">
129
206
  <ApiError error={launch.error} title="The launch did not complete" />
130
- <p className="text-sm text-amber-900">Do not retry blindly. A sequence may already exist; check the campaign before launching again.</p>
207
+ <p className="text-sm text-warning-foreground">Do not retry blindly. A sequence may already exist; check the campaign before launching again.</p>
131
208
  </div>
132
209
  ) : null}
133
210
  <Button disabled={!ready} loading={launch.loading} onClick={() => setConfirm(true)}>Launch campaign</Button>
@@ -142,6 +219,17 @@ export function CampaignLaunchStep({ campaignId, audienceSize, onLaunched, class
142
219
  onConfirm={() => void launch.mutate(undefined)}
143
220
  onCancel={() => setConfirm(false)}
144
221
  />
222
+ <ConfirmSpendDialog
223
+ open={confirmCopy}
224
+ tier="billable"
225
+ title={`Write the email copy for "${String(c.name ?? "this campaign")}" with AI`}
226
+ description="AI writes the emails from the campaign brief and its steps. Nothing is sent. Estimate only."
227
+ estimatedCredits={CREDIT_ESTIMATES.documentRegenerate}
228
+ confirmLabel="Write copy"
229
+ confirming={writeCopy.loading}
230
+ onConfirm={() => void writeCopy.mutate(undefined)}
231
+ onCancel={() => setConfirmCopy(false)}
232
+ />
145
233
  </section>
146
234
  );
147
235
  }
@@ -25,8 +25,12 @@ import { formatDate, listOf, statusClass } from "../lib/campaigns-utils";
25
25
  import type { CampaignRow } from "../lib/campaigns-utils";
26
26
  import { CampaignAssets } from "./campaign-assets";
27
27
  import { CampaignMetrics } from "./campaign-metrics";
28
+ import { rowActivationProps } from "@/g8/hooks/use-row-activation";
29
+ import { RecordLink } from "@/g8/components/screen/record-link";
28
30
 
29
31
  export interface CampaignListProps {
32
+ /** URL param the host screen reads for this record type. With it the name is a real link (Cmd-click opens it in a new tab); without it a plain button. */
33
+ linkParam?: string;
30
34
  pageSize?: number;
31
35
  /** Called when a row is opened. Without it the row opens metrics and documents below the table. */
32
36
  onOpen?: (campaign: CampaignRow) => void;
@@ -37,7 +41,7 @@ export interface CampaignListProps {
37
41
 
38
42
  const COLUMNS = 6;
39
43
 
40
- export function CampaignList({ pageSize = 25, onOpen, emptyAction, className }: CampaignListProps) {
44
+ export function CampaignList({ linkParam, pageSize = 25, onOpen, emptyAction, className }: CampaignListProps) {
41
45
  const [page, setPage] = useState(1);
42
46
  const [openId, setOpenId] = useState<string | null>(null);
43
47
  const [deleting, setDeleting] = useState<CampaignRow | null>(null);
@@ -74,8 +78,8 @@ export function CampaignList({ pageSize = 25, onOpen, emptyAction, className }:
74
78
  <TableBody>
75
79
  {list.loading && !list.data ? <TableRow><TableCell colSpan={COLUMNS}><Skeleton className="h-6 w-full" /></TableCell></TableRow> : null}
76
80
  {rows.map((c) => (
77
- <TableRow key={c.id} data-state={c.id === openId ? "selected" : undefined}>
78
- <TableCell className="font-medium">{c.name}</TableCell>
81
+ <TableRow key={c.id} data-state={c.id === openId ? "selected" : undefined} {...rowActivationProps(() => open(c))}>
82
+ <TableCell className="font-medium"><RecordLink param={linkParam} id={c.id} onOpen={() => open(c)}>{c.name}</RecordLink></TableCell>
79
83
  <TableCell><span className={`rounded px-2 py-0.5 text-xs ${statusClass(c.status)}`}>{c.status}</span></TableCell>
80
84
  <TableCell>{c.category}</TableCell>
81
85
  <TableCell className="max-w-xs truncate text-muted-foreground">{c.goal}</TableCell>
@@ -40,7 +40,7 @@ function Tiles({ data }: { data: Record<string, unknown> }) {
40
40
  {tiles.map(([k, v]) => (
41
41
  <div key={k} className="rounded-md border p-3">
42
42
  <dt className="text-xs capitalize text-muted-foreground">{k}</dt>
43
- <dd className="text-xl font-semibold">{v}</dd>
43
+ <dd className="text-xl font-medium">{v}</dd>
44
44
  </div>
45
45
  ))}
46
46
  </dl>
@@ -13,14 +13,20 @@
13
13
  * getCampaignGenerationStatus GET /campaigns/{campaign_id}/status (read; POLLED while documents generate)
14
14
  * 4. attach audience, steps/content, launch: see the three step components.
15
15
  *
16
+ * Copy: AI writes the campaign documents by default (`autoGenerateDocuments`, default true). The launch is refused
17
+ * without written copy, so a campaign created without them needs its copy written before launch; the Launch step
18
+ * offers that. If the server could not queue the generation (`generation_status: "failed_to_dispatch"`), the
19
+ * wizard says so and the Launch step offers to write the copy.
20
+ *
16
21
  * Retry safety: the campaign id is stored the moment create succeeds, so a failure in a later step never
17
22
  * creates a second campaign; "Create" turns into "Save changes". "Start over" clears every field, the idea
18
23
  * results and the campaign id (the campaign already created stays in graph8; delete it in CampaignList).
19
24
  */
20
25
 
21
26
  import { useEffect, useState } from "react";
22
- import { Button, Input, Textarea } from "@graph8/sdk/ui";
27
+ import { Button, Input, Skeleton, Textarea } from "@graph8/sdk/ui";
23
28
  import { ApiError } from "@/g8/components/api-error";
29
+ import { FirstNotice } from "@/g8/components/paging-notices";
24
30
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
25
31
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
26
32
  import { useG8Query } from "@/g8/hooks/use-g8-query";
@@ -40,6 +46,11 @@ export interface CampaignWizardProps {
40
46
  /** Overrides the website from the saved profile. */
41
47
  websiteUrl?: string;
42
48
  onLaunched?: (result: Record<string, unknown>) => void;
49
+ /**
50
+ * Whether "Write the campaign documents with AI" starts checked. Default true: a campaign without written copy
51
+ * cannot launch. Pass false to start with it unchecked (the person writes the copy, or the Launch step does).
52
+ */
53
+ autoGenerateDocuments?: boolean;
43
54
  className?: string;
44
55
  }
45
56
 
@@ -48,12 +59,15 @@ const GOALS = ["awareness", "consideration", "decision"];
48
59
  const CHANNELS = ["email", "linkedin", "phone"];
49
60
 
50
61
  interface Draft { name: string; category: string; goal: string; brief: string; core_concept: string; primary_hook: string; target_persona: string; channels: string[]; autoGenerate: boolean }
51
- const emptyDraft: Draft = { name: "", category: "Outbound", goal: "", brief: "", core_concept: "", primary_hook: "", target_persona: "", channels: ["email"], autoGenerate: false };
62
+ const emptyDraft: Draft = { name: "", category: "Outbound", goal: "", brief: "", core_concept: "", primary_hook: "", target_persona: "", channels: ["email"], autoGenerate: true };
52
63
 
53
64
  interface Idea { id?: string; campaign_name?: string; campaign_category?: string; core_concept?: string; why_this_works?: string; primary_hook?: string; target_channels?: string[] }
54
65
 
66
+ /** The persona API has no paging: a full read is announced under the checkboxes. */
67
+ const PERSONA_LIMIT = 50;
68
+
55
69
  function IdeaStep({ websiteUrl, onPick, onSkip }: { websiteUrl: string; onPick: (i: Idea) => void; onSkip: () => void }) {
56
- const personas = useG8Query((signal) => g8Get("/personas", { limit: 50 }, { signal }), []);
70
+ const personas = useG8Query((signal) => g8Get("/personas", { limit: PERSONA_LIMIT }, { signal }), []);
57
71
  const [goal, setGoal] = useState(GOALS[0]);
58
72
  const [count, setCount] = useState(5);
59
73
  const [direction, setDirection] = useState("");
@@ -63,7 +77,7 @@ function IdeaStep({ websiteUrl, onPick, onSkip }: { websiteUrl: string; onPick:
63
77
  const start = useG8Mutation(
64
78
  () => g8Post("/campaigns/generate-ideas-async", {
65
79
  organization_id: "current", website_url: websiteUrl, campaign_goal: goal, num_ideas: count, user_direction: direction || null,
66
- target_personas: selected, persona_mode: selected.length ? "selected" : "auto",
80
+ target_personas: selected, persona_mode: selected.length ? "selected" : "ai_pick", // the server's AI-picks-the-persona mode
67
81
  }),
68
82
  { onSuccess: (res) => { setConfirm(false); setTaskId(text(objectOf(res).task_id) || null); }, onError: () => setConfirm(false) },
69
83
  );
@@ -94,6 +108,7 @@ function IdeaStep({ websiteUrl, onPick, onSkip }: { websiteUrl: string; onPick:
94
108
  <input type="checkbox" checked={selected.includes(p.title ?? "")} onChange={(e) => setSelected(e.target.checked ? [...selected, p.title ?? ""] : selected.filter((t) => t !== p.title))} />{p.title}
95
109
  </label>
96
110
  ))}
111
+ <FirstNotice shown={personaRows.length} limit={PERSONA_LIMIT} noun="personas" />
97
112
  </fieldset>
98
113
  ) : null}
99
114
  {[start.error, poll.error].map((e, i) => (e ? <ApiError key={i} error={e} title="Idea generation failed" /> : null))}
@@ -101,10 +116,10 @@ function IdeaStep({ websiteUrl, onPick, onSkip }: { websiteUrl: string; onPick:
101
116
  <Button disabled={!websiteUrl || poll.polling} loading={start.loading} onClick={() => setConfirm(true)}>Generate ideas</Button>
102
117
  <Button variant="outline" onClick={onSkip}>Skip, write my own</Button>
103
118
  </div>
104
- {!websiteUrl ? <p className="text-sm text-amber-800">Save the company website in IcpEditor first; ideas are generated from it.</p> : null}
119
+ {!websiteUrl ? <p className="text-sm text-warning-foreground">Save the company website in IcpEditor first; ideas are generated from it.</p> : null}
105
120
  {poll.polling ? <p role="status" className="text-sm text-muted-foreground">Generating ideas. This takes 10 to 20 seconds.</p> : null}
106
- {!poll.polling && !poll.done && poll.attempts > 0 && !poll.error ? <p role="status" className="text-sm text-amber-800">Still running. Check back in a minute and generate again if nothing appears.</p> : null}
107
- {failed ? <p role="alert" className="text-sm text-red-700">Generation failed: {text(poll.status?.error) || "try again"}.</p> : null}
121
+ {!poll.polling && !poll.done && poll.attempts > 0 && !poll.error ? <p role="status" className="text-sm text-warning-foreground">Still running. Check back in a minute and generate again if nothing appears.</p> : null}
122
+ {failed ? <p role="alert" className="text-sm text-error-foreground">Generation failed: {text(poll.status?.error) || "try again"}.</p> : null}
108
123
  {poll.done && !failed && ideas.length === 0 ? <p className="text-sm text-muted-foreground">No ideas came back. Try a different goal or direction.</p> : null}
109
124
  <ul className="space-y-2">
110
125
  {ideas.map((idea, i) => (
@@ -132,19 +147,32 @@ function IdeaStep({ websiteUrl, onPick, onSkip }: { websiteUrl: string; onPick:
132
147
  }
133
148
 
134
149
  function GenerationStatus({ campaignId }: { campaignId: string }) {
135
- const poll = useJobPoll(async (signal) => objectOf(await g8Get(`/campaigns/${campaignId}/status`, undefined, { signal })), { isTerminal: settled, maxAttempts: 90 });
136
- const label = poll.polling ? "generating documents" : text(poll.status?.status) || "ready";
137
- return <span role="status" className={`rounded px-2 py-0.5 text-xs ${statusClass(poll.polling ? "generating" : "completed")}`}>Documents: {label}</span>;
150
+ const poll = useJobPoll(async (signal) => objectOf(await g8Get(`/campaigns/${campaignId}/status`, undefined, { signal })), { isTerminal: (s) => Boolean(text(s.generation_error)) || settled(s), maxAttempts: 90 });
151
+ const failed = text(poll.status?.generation_error);
152
+ const label = failed ? "failed" : poll.polling ? "generating documents" : text(poll.status?.status) || "ready";
153
+ return <span role="status" title={failed || undefined} className={`rounded px-2 py-0.5 text-xs ${statusClass(failed ? "failed" : poll.polling ? "generating" : "completed")}`}>Documents: {label}</span>;
138
154
  }
139
155
 
140
- export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, className }: CampaignWizardProps) {
156
+ export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, autoGenerateDocuments = true, className }: CampaignWizardProps) {
141
157
  const profile = useCompanyProfile(websiteUrl);
158
+ const initialDraft: Draft = { ...emptyDraft, autoGenerate: autoGenerateDocuments };
142
159
  const [step, setStep] = useState(initialCampaignId ? 2 : 0);
143
- const [draft, setDraft] = useState<Draft>(emptyDraft);
160
+ const [draft, setDraft] = useState<Draft>(initialDraft);
161
+ const [dispatchFailed, setDispatchFailed] = useState(false);
144
162
  const [campaignId, setCampaignId] = useState<string | null>(initialCampaignId ?? null);
145
163
  const [audience, setAudience] = useState<ListSummary | null>(null);
146
164
  const [confirm, setConfirm] = useState(false);
147
165
  const [resetKey, setResetKey] = useState(0);
166
+ // Resuming a saved campaign: the form shows (and can save) nothing until the campaign itself has loaded, otherwise
167
+ // Save would PATCH the blank defaults over the real name, goal and brief.
168
+ const resume = useG8Query((signal) => g8Get(`/campaigns/${initialCampaignId}`, undefined, { signal }), [initialCampaignId], { enabled: Boolean(initialCampaignId) });
169
+ const [seeded, setSeeded] = useState(!initialCampaignId);
170
+ useEffect(() => {
171
+ if (seeded || !resume.data) return;
172
+ const c = objectOf(resume.data);
173
+ setDraft((d) => ({ ...d, name: text(c.name), category: text(c.category) || d.category, goal: text(c.goal), brief: text(c.brief), core_concept: text(c.core_concept), primary_hook: text(c.primary_hook), target_persona: text(c.target_persona) }));
174
+ setSeeded(true);
175
+ }, [resume.data, seeded]);
148
176
 
149
177
  const save = useG8Mutation(async () => {
150
178
  const fields = { name: draft.name.trim(), category: draft.category, goal: draft.goal || null, brief: draft.brief || null, core_concept: draft.core_concept || null, primary_hook: draft.primary_hook || null, target_persona: draft.target_persona || null };
@@ -155,6 +183,7 @@ export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, clas
155
183
  setConfirm(false);
156
184
  const id = text(objectOf(res).id);
157
185
  if (!campaignId && id) setCampaignId(id); // stored at once: a later failure never re-creates the campaign
186
+ if (!campaignId && objectOf(res).generation_status === "failed_to_dispatch") setDispatchFailed(true);
158
187
  setStep(2);
159
188
  },
160
189
  onError: () => setConfirm(false),
@@ -165,7 +194,7 @@ export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, clas
165
194
  setDraft({ ...draft, name: i.campaign_name ?? "", category: i.campaign_category ?? draft.category, core_concept: i.core_concept ?? "", primary_hook: i.primary_hook ?? "", brief: i.why_this_works ?? "", channels: i.target_channels?.length ? i.target_channels : draft.channels });
166
195
  setStep(1);
167
196
  };
168
- const reset = () => { setStep(0); setDraft(emptyDraft); setCampaignId(null); setAudience(null); setConfirm(false); save.reset(); setResetKey((k) => k + 1); };
197
+ const reset = () => { setSeeded(true); setStep(0); setDraft(initialDraft); setDispatchFailed(false); setCampaignId(null); setAudience(null); setConfirm(false); save.reset(); setResetKey((k) => k + 1); };
169
198
  const autoGen = !campaignId && draft.autoGenerate;
170
199
 
171
200
  return (
@@ -181,8 +210,14 @@ export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, clas
181
210
  <Button size="sm" variant="outline" className="ml-auto" onClick={reset}>Start over</Button>
182
211
  </nav>
183
212
 
213
+ {dispatchFailed ? (
214
+ <p role="alert" className="rounded bg-warning p-2 text-sm text-warning-foreground">
215
+ The campaign was created, but writing its documents could not be started. Write the email copy from the Launch step before launching.
216
+ </p>
217
+ ) : null}
184
218
  {step === 0 ? <IdeaStep key={resetKey} websiteUrl={profile.websiteUrl} onPick={pickIdea} onSkip={() => setStep(1)} /> : null}
185
- {step === 1 ? (
219
+ {step === 1 && !seeded ? (resume.error ? <ApiError error={resume.error} title="Could not load this campaign" onRetry={() => void resume.refetch()} /> : <Skeleton className="h-48 w-full" aria-label="Loading campaign" />) : null}
220
+ {step === 1 && seeded ? (
186
221
  <section aria-label="Campaign" className="space-y-3">
187
222
  <FormField label="Campaign name" htmlFor="cw-name"><Input id="cw-name" value={draft.name} onChange={set("name")} /></FormField>
188
223
  <div className="grid gap-3 sm:grid-cols-2">
@@ -206,6 +241,9 @@ export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, clas
206
241
  <input type="checkbox" checked={draft.autoGenerate} onChange={(e) => setDraft({ ...draft, autoGenerate: e.target.checked })} />
207
242
  Write the campaign documents with AI after creating it (uses credits)
208
243
  </label>
244
+ {!draft.autoGenerate && draft.channels.includes("email") ? (
245
+ <p className="text-sm text-muted-foreground">Without them the campaign has no email copy, and it cannot launch until the copy is written. The Launch step can write it with AI.</p>
246
+ ) : null}
209
247
  </>
210
248
  ) : null}
211
249
  {save.error ? <ApiError error={save.error} title={campaignId ? "Could not save the campaign" : "Could not create the campaign"} /> : null}