@graph8/sdk 0.361.0 → 0.370.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 (388) hide show
  1. package/README.md +14 -10
  2. package/blocks/_foundation/README.md +172 -1
  3. package/blocks/_foundation/block.json +234 -13
  4. package/blocks/_foundation/components/api-error.tsx +71 -25
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  7. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  8. package/blocks/_foundation/components/empty-state.tsx +39 -7
  9. package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
  10. package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
  11. package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
  12. package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
  13. package/blocks/_foundation/components/screen/index.ts +22 -0
  14. package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
  17. package/blocks/_foundation/components/screen/section-card.tsx +47 -0
  18. package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
  19. package/blocks/_foundation/components/screen/split-view.tsx +125 -0
  20. package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
  21. package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
  22. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  23. package/blocks/_foundation/components/screen/time-series-chart.tsx +32 -0
  24. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  25. package/blocks/_foundation/components/screen/toolbar.tsx +144 -0
  26. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  27. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  28. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  29. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  30. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  31. package/blocks/_foundation/hooks/use-url-state.ts +74 -0
  32. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  33. package/blocks/_foundation/lib/g8-client.ts +1 -1
  34. package/blocks/_foundation/lib/plural.ts +23 -0
  35. package/blocks/_foundation/server/connect-route.ts +15 -0
  36. package/blocks/_foundation/server/connect.ts +127 -0
  37. package/blocks/_foundation/server/local-key.ts +70 -0
  38. package/blocks/_foundation/server/proxy.ts +103 -3
  39. package/blocks/_foundation/server/route.ts +16 -4
  40. package/blocks/agents/README.md +14 -0
  41. package/blocks/agents/block.json +33 -1
  42. package/blocks/agents/components/agent-builder.tsx +13 -8
  43. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
  44. package/blocks/agents/components/agent-run-detail.tsx +102 -0
  45. package/blocks/agents/components/agent-runs.tsx +16 -7
  46. package/blocks/agents/components/agents-screen.tsx +184 -0
  47. package/blocks/analytics/README.md +13 -0
  48. package/blocks/analytics/block.json +45 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +209 -0
  50. package/blocks/analytics/components/leaderboard.tsx +14 -7
  51. package/blocks/analytics/components/report-builder.tsx +23 -9
  52. package/blocks/analytics/components/report-detail.tsx +84 -0
  53. package/blocks/analytics/components/trend-chart.tsx +62 -0
  54. package/blocks/appointments/README.md +21 -1
  55. package/blocks/appointments/block.json +43 -2
  56. package/blocks/appointments/components/availability-editor.tsx +17 -5
  57. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  58. package/blocks/appointments/components/event-type-related.tsx +77 -0
  59. package/blocks/appointments/components/meeting-list.tsx +14 -6
  60. package/blocks/appointments/components/meetings-screen.tsx +157 -0
  61. package/blocks/appointments/components/scheduling-screen.tsx +122 -0
  62. package/blocks/appointments/hooks/use-event-types.ts +17 -0
  63. package/blocks/block.schema.json +18 -0
  64. package/blocks/block.types.ts +26 -0
  65. package/blocks/campaigns/README.md +41 -11
  66. package/blocks/campaigns/block.json +105 -42
  67. package/blocks/campaigns/components/campaign-assets.tsx +16 -5
  68. package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
  69. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  70. package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
  71. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  72. package/blocks/campaigns/components/icp-detail.tsx +154 -0
  73. package/blocks/campaigns/components/icp-editor.tsx +15 -15
  74. package/blocks/campaigns/components/persona-detail.tsx +172 -0
  75. package/blocks/campaigns/components/persona-editor.tsx +14 -11
  76. package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
  77. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  78. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  79. package/blocks/coach/README.md +10 -0
  80. package/blocks/coach/block.json +16 -1
  81. package/blocks/coach/components/coach-screen.tsx +222 -0
  82. package/blocks/copilot/README.md +13 -0
  83. package/blocks/copilot/block.json +14 -1
  84. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  85. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  86. package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
  87. package/blocks/crm/README.md +28 -0
  88. package/blocks/crm/block.json +58 -2
  89. package/blocks/crm/components/companies-screen.tsx +345 -0
  90. package/blocks/crm/components/company-detail.tsx +8 -6
  91. package/blocks/crm/components/company-table.tsx +11 -4
  92. package/blocks/crm/components/contact-detail.tsx +6 -3
  93. package/blocks/crm/components/contact-table.tsx +10 -3
  94. package/blocks/crm/components/contacts-screen.tsx +359 -0
  95. package/blocks/crm/components/dedupe-review.tsx +7 -5
  96. package/blocks/crm/components/import-wizard.tsx +4 -2
  97. package/blocks/crm/components/notes-panel.tsx +4 -4
  98. package/blocks/crm/components/record-form.tsx +5 -3
  99. package/blocks/crm/components/task-list.tsx +4 -4
  100. package/blocks/crm/components/tasks-screen.tsx +379 -0
  101. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  102. package/blocks/crm/hooks/use-page-query.ts +7 -1
  103. package/blocks/crm/lib/crm-types.ts +12 -2
  104. package/blocks/crm/lib/screen-parts.tsx +105 -0
  105. package/blocks/deals/README.md +34 -3
  106. package/blocks/deals/block.json +45 -4
  107. package/blocks/deals/components/deal-detail.tsx +27 -13
  108. package/blocks/deals/components/deal-related.tsx +87 -0
  109. package/blocks/deals/components/deals-screen.tsx +298 -0
  110. package/blocks/deals/components/pipeline-board.tsx +4 -2
  111. package/blocks/deals/components/stage-editor.tsx +4 -4
  112. package/blocks/deals/hooks/use-default-owner.ts +60 -0
  113. package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
  114. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  115. package/blocks/deals/lib/deals-types.ts +14 -0
  116. package/blocks/dialer/README.md +34 -2
  117. package/blocks/dialer/block.json +67 -1
  118. package/blocks/dialer/components/call-contact-button.tsx +252 -0
  119. package/blocks/dialer/components/call-log-screen.tsx +205 -0
  120. package/blocks/dialer/components/call-log.tsx +7 -3
  121. package/blocks/dialer/components/call-queue.tsx +1 -1
  122. package/blocks/dialer/components/dialer-screen.tsx +319 -0
  123. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  124. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  125. package/blocks/dialer/components/session-start-form.tsx +4 -2
  126. package/blocks/dialer/components/softphone.tsx +3 -1
  127. package/blocks/enrichment/README.md +14 -2
  128. package/blocks/enrichment/block.json +32 -4
  129. package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
  130. package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
  131. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  132. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  133. package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
  134. package/blocks/enrichment/components/waterfall-config.tsx +39 -10
  135. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  136. package/blocks/forms/README.md +12 -0
  137. package/blocks/forms/block.json +39 -17
  138. package/blocks/forms/components/form-builder.tsx +3 -1
  139. package/blocks/forms/components/form-embed.tsx +3 -1
  140. package/blocks/forms/components/forms-screen.tsx +353 -0
  141. package/blocks/forms/components/submissions-table.tsx +16 -6
  142. package/blocks/inbox/README.md +27 -3
  143. package/blocks/inbox/block.json +14 -2
  144. package/blocks/inbox/components/inbox-list.tsx +7 -3
  145. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  146. package/blocks/inbox/components/reply-composer.tsx +65 -9
  147. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  148. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  149. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  150. package/blocks/inbox/lib/inbox-types.ts +47 -1
  151. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  152. package/blocks/integrations/README.md +9 -0
  153. package/blocks/integrations/block.json +46 -1
  154. package/blocks/integrations/components/crm-detail.tsx +100 -0
  155. package/blocks/integrations/components/integration-list.tsx +9 -4
  156. package/blocks/integrations/components/integrations-screen.tsx +135 -0
  157. package/blocks/integrations/components/sync-status.tsx +5 -3
  158. package/blocks/integrations/components/webhook-detail.tsx +160 -0
  159. package/blocks/integrations/components/webhook-manager.tsx +13 -8
  160. package/blocks/integrations/lib/integrations-utils.ts +6 -1
  161. package/blocks/intent/README.md +14 -0
  162. package/blocks/intent/block.json +27 -1
  163. package/blocks/intent/components/intent-feed.tsx +5 -2
  164. package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
  165. package/blocks/intent/components/intent-screen.tsx +265 -0
  166. package/blocks/intent/components/keyword-detail.tsx +124 -0
  167. package/blocks/lifecycle/README.md +16 -0
  168. package/blocks/lifecycle/block.json +34 -7
  169. package/blocks/lifecycle/components/account-related.tsx +66 -0
  170. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  171. package/blocks/lifecycle/components/lifecycle-screen.tsx +149 -0
  172. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  173. package/blocks/lists/README.md +13 -0
  174. package/blocks/lists/block.json +39 -1
  175. package/blocks/lists/components/add-to-list-button.tsx +3 -2
  176. package/blocks/lists/components/list-picker.tsx +5 -1
  177. package/blocks/lists/components/list-table.tsx +11 -5
  178. package/blocks/lists/components/lists-screen.tsx +344 -0
  179. package/blocks/lists/components/suppression-manager.tsx +6 -2
  180. package/blocks/lists/components/suppressions-screen.tsx +255 -0
  181. package/blocks/mailboxes/README.md +17 -0
  182. package/blocks/mailboxes/block.json +65 -17
  183. package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
  184. package/blocks/mailboxes/components/domain-detail.tsx +142 -0
  185. package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
  186. package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
  187. package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
  188. package/blocks/mailboxes/components/warmup-health.tsx +4 -2
  189. package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
  190. package/blocks/newsletter/README.md +13 -0
  191. package/blocks/newsletter/block.json +47 -23
  192. package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
  193. package/blocks/newsletter/components/newsletter-screen.tsx +404 -0
  194. package/blocks/newsletter/components/subscriber-table.tsx +12 -6
  195. package/blocks/nurture/README.md +12 -0
  196. package/blocks/nurture/block.json +35 -13
  197. package/blocks/nurture/components/nurture-builder.tsx +3 -1
  198. package/blocks/nurture/components/nurture-enroll.tsx +6 -3
  199. package/blocks/nurture/components/nurture-form.tsx +23 -3
  200. package/blocks/nurture/components/nurture-screen.tsx +283 -0
  201. package/blocks/nurture/lib/nurture-types.ts +1 -1
  202. package/blocks/objects/README.md +17 -0
  203. package/blocks/objects/block.json +29 -16
  204. package/blocks/objects/components/object-not-found.tsx +3 -1
  205. package/blocks/objects/components/object-record-detail.tsx +2 -1
  206. package/blocks/objects/components/object-schema-editor.tsx +3 -1
  207. package/blocks/objects/components/object-table.tsx +13 -3
  208. package/blocks/objects/components/objects-screen.tsx +147 -0
  209. package/blocks/objects/lib/object-types.ts +1 -1
  210. package/blocks/pages/README.md +12 -0
  211. package/blocks/pages/block.json +35 -22
  212. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  213. package/blocks/pages/components/content-library.tsx +46 -11
  214. package/blocks/pages/components/page-list.tsx +6 -3
  215. package/blocks/pages/components/pages-screen.tsx +154 -0
  216. package/blocks/quotes/README.md +17 -0
  217. package/blocks/quotes/block.json +38 -9
  218. package/blocks/quotes/components/product-picker.tsx +3 -1
  219. package/blocks/quotes/components/quote-list.tsx +7 -3
  220. package/blocks/quotes/components/quote-related.tsx +123 -0
  221. package/blocks/quotes/components/quotes-screen.tsx +169 -0
  222. package/blocks/radar/README.md +10 -0
  223. package/blocks/radar/block.json +41 -1
  224. package/blocks/radar/components/competitor-detail.tsx +117 -0
  225. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  226. package/blocks/radar/components/radar-report.tsx +4 -2
  227. package/blocks/radar/components/radar-screen.tsx +214 -0
  228. package/blocks/radar/components/report-detail.tsx +104 -0
  229. package/blocks/search/README.md +10 -0
  230. package/blocks/search/block.json +16 -1
  231. package/blocks/search/components/filter-builder.tsx +3 -3
  232. package/blocks/search/components/prospect-results.tsx +5 -3
  233. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  234. package/blocks/search/components/saved-searches.tsx +4 -1
  235. package/blocks/search/hooks/use-filter-options.ts +20 -6
  236. package/blocks/search/lib/search-types.ts +2 -0
  237. package/blocks/sequencer/README.md +49 -9
  238. package/blocks/sequencer/block.json +99 -19
  239. package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
  240. package/blocks/sequencer/components/sequence-builder.tsx +211 -46
  241. package/blocks/sequencer/components/sequence-detail.tsx +259 -0
  242. package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
  243. package/blocks/sequencer/components/sequence-list.tsx +33 -8
  244. package/blocks/sequencer/components/sequence-stats.tsx +3 -2
  245. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  246. package/blocks/sequencer/components/sequences-screen.tsx +159 -0
  247. package/blocks/sequencer/components/step-editor.tsx +3 -1
  248. package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
  249. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  250. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  251. package/blocks/social-ads/README.md +11 -0
  252. package/blocks/social-ads/block.json +101 -2
  253. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  254. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  255. package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
  256. package/blocks/social-ads/components/audience-sync.tsx +11 -7
  257. package/blocks/social-ads/components/campaign-detail.tsx +133 -0
  258. package/blocks/social-ads/components/creative-detail.tsx +150 -0
  259. package/blocks/social-ads/components/creative-gallery.tsx +15 -5
  260. package/blocks/social-ads/components/post-composer.tsx +3 -1
  261. package/blocks/social-ads/components/post-detail.tsx +160 -0
  262. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  263. package/blocks/social-ads/components/social-screen.tsx +160 -0
  264. package/blocks/visitors/README.md +10 -0
  265. package/blocks/visitors/block.json +25 -1
  266. package/blocks/visitors/components/snippet-install.tsx +3 -1
  267. package/blocks/visitors/components/visitor-company-card.tsx +2 -2
  268. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  269. package/blocks/visitors/components/visitor-feed.tsx +11 -2
  270. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  271. package/blocks/webchat/README.md +10 -0
  272. package/blocks/webchat/block.json +21 -7
  273. package/blocks/webchat/components/chat-widget-config.tsx +7 -4
  274. package/blocks/webchat/components/conversation-list.tsx +7 -3
  275. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  276. package/blocks/workbench/README.md +11 -0
  277. package/blocks/workbench/block.json +48 -1
  278. package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
  279. package/blocks/workbench/components/workbench-grid.tsx +9 -3
  280. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  281. package/blocks/workbench/components/workbench-screen.tsx +192 -0
  282. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  283. package/blocks/workflows/README.md +13 -0
  284. package/blocks/workflows/block.json +28 -9
  285. package/blocks/workflows/components/run-history.tsx +31 -9
  286. package/blocks/workflows/components/trigger-config.tsx +3 -1
  287. package/blocks/workflows/components/workflow-editor.tsx +3 -1
  288. package/blocks/workflows/components/workflow-list.tsx +13 -6
  289. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  290. package/dist/cli.js +2785 -1637
  291. package/dist/errors.d.mts +34 -0
  292. package/dist/errors.d.ts +34 -0
  293. package/dist/errors.js +46 -0
  294. package/dist/errors.js.map +1 -0
  295. package/dist/errors.mjs +21 -0
  296. package/dist/errors.mjs.map +1 -0
  297. package/dist/index.browser.js +11 -5
  298. package/dist/index.browser.js.map +1 -1
  299. package/dist/index.browser.mjs +11 -5
  300. package/dist/index.browser.mjs.map +1 -1
  301. package/dist/index.d.mts +164 -25
  302. package/dist/index.d.ts +164 -25
  303. package/dist/index.js +11 -5
  304. package/dist/index.js.map +1 -1
  305. package/dist/index.mjs +11 -5
  306. package/dist/index.mjs.map +1 -1
  307. package/dist/react.browser.js +10 -4
  308. package/dist/react.browser.js.map +1 -1
  309. package/dist/react.browser.mjs +10 -4
  310. package/dist/react.browser.mjs.map +1 -1
  311. package/dist/react.d.mts +142 -5
  312. package/dist/react.d.ts +142 -5
  313. package/dist/react.js +10 -4
  314. package/dist/react.js.map +1 -1
  315. package/dist/react.mjs +10 -4
  316. package/dist/react.mjs.map +1 -1
  317. package/dist/types/blocks/add.d.ts +11 -4
  318. package/dist/types/blocks/brand.d.ts +113 -0
  319. package/dist/types/blocks/compose.d.ts +53 -0
  320. package/dist/types/blocks/create.d.ts +25 -8
  321. package/dist/types/blocks/doctor.d.ts +6 -1
  322. package/dist/types/blocks/flows.d.ts +20 -0
  323. package/dist/types/blocks/navfile.d.ts +35 -0
  324. package/dist/types/blocks/paths.d.ts +4 -0
  325. package/dist/types/blocks/prefixes.d.ts +37 -0
  326. package/dist/types/blocks/registry.d.ts +32 -0
  327. package/dist/types/blocks/screens.d.ts +53 -0
  328. package/dist/types/cli.d.ts +2 -0
  329. package/dist/types/errors.d.ts +33 -0
  330. package/dist/types/http.d.ts +2 -18
  331. package/dist/types/version.d.ts +2 -2
  332. package/docs/agents.md +15 -4
  333. package/docs/api/README.md +2 -2
  334. package/docs/api/by-path.md +1 -0
  335. package/docs/api/desk.d.ts +98 -2
  336. package/docs/api/desk.md +123 -1
  337. package/docs/api/inbox.d.ts +10 -2
  338. package/docs/api/inbox.md +10 -2
  339. package/docs/api/seo.d.ts +19 -1
  340. package/docs/api/seo.md +24 -1
  341. package/package.json +16 -2
  342. package/stacks/6sense.json +41 -16
  343. package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
  344. package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
  345. package/stacks/apollo.json +118 -36
  346. package/stacks/calendly.json +60 -0
  347. package/stacks/clay/pages/companies.tsx.tmpl +15 -0
  348. package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
  349. package/stacks/clay.json +59 -26
  350. package/stacks/gong.json +81 -35
  351. package/stacks/hootsuite.json +99 -0
  352. package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
  353. package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
  354. package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
  355. package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
  356. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  357. package/stacks/hubspot.json +167 -66
  358. package/stacks/intercom.json +48 -20
  359. package/stacks/mailchimp.json +52 -33
  360. package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
  361. package/stacks/outreach.json +88 -25
  362. package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
  363. package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
  364. package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
  365. package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
  366. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  367. package/stacks/pipedrive.json +115 -0
  368. package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
  369. package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
  370. package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
  371. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  372. package/stacks/salesforce.json +81 -31
  373. package/stacks/stack.schema.json +6 -1
  374. package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
  375. package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
  376. package/stacks/zoominfo.json +102 -0
  377. package/styles/styles.css +1 -1
  378. package/templates/next-app/README.md.tmpl +55 -8
  379. package/templates/next-app/app/globals.css.tmpl +105 -0
  380. package/templates/next-app/app/layout.tsx.tmpl +18 -4
  381. package/templates/next-app/components/app-shell.tsx.tmpl +248 -23
  382. package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
  383. package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
  384. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
  385. package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
  386. package/templates/next-app/dot-gitignore.tmpl +2 -0
  387. package/templates/next-app/lib/nav.ts.tmpl +1 -1
  388. package/templates/next-app/package.json.tmpl +1 -1
@@ -15,45 +15,51 @@
15
15
  "mailboxes"
16
16
  ],
17
17
  "operations": [
18
- "get_company_profile_company_profile_get",
19
- "update_company_profile_company_profile_put",
20
- "generate_company_profile_company_profile_generate_post",
21
- "get_company_profile_workflow_status_company_profile_workflow_status_get",
22
18
  "approve_company_profile_company_profile_approve_post",
23
- "list_icps_icps_get",
24
- "create_icp_icps_post",
25
- "update_icp_icps__icp_id__patch",
26
19
  "archive_icp_icps__icp_id__archive_post",
27
- "list_personas_personas_get",
28
- "create_persona_personas_post",
29
- "update_persona_personas__persona_id__patch",
30
20
  "archive_persona_personas__persona_id__archive_post",
31
- "recommend_campaign_personas_campaigns_recommend_personas_post",
32
- "generate_campaign_ideas_async_campaigns_generate_ideas_async_post",
33
- "get_campaign_ideas_task_status_campaigns_ideas_status__task_id__get",
34
- "create_campaign_campaigns_post",
35
- "update_campaign_campaigns__campaign_id__patch",
36
- "get_campaign_generation_status_campaigns__campaign_id__status_get",
37
- "get_campaign_full_campaigns__campaign_id__full_get",
38
21
  "attach_audience_to_campaign_campaigns__campaign_id__audience_put",
39
- "get_campaign_sequence_campaigns__campaign_id__sequence_get",
22
+ "create_campaign_campaigns_post",
23
+ "create_campaign_document_campaigns__campaign_id__documents_post",
40
24
  "create_campaign_step_campaigns__campaign_id__sequence_steps_post",
41
- "update_campaign_step_campaigns__campaign_id__sequence_steps__step_id__put",
25
+ "create_icp_icps_post",
26
+ "create_persona_personas_post",
27
+ "delete_campaign_campaigns__campaign_id__delete",
28
+ "delete_campaign_document_campaigns__campaign_id__documents__document_id__delete",
42
29
  "delete_campaign_step_campaigns__campaign_id__sequence_steps__step_id__delete",
30
+ "generate_campaign_ideas_async_campaigns_generate_ideas_async_post",
43
31
  "generate_campaign_step_content_campaigns__campaign_id__sequence_steps__step_id__generate_post",
44
- "list_mailboxes_mailboxes_get",
32
+ "generate_company_profile_company_profile_generate_post",
33
+ "get_campaign_campaigns__campaign_id__get",
34
+ "get_campaign_document_campaigns__campaign_id__documents__document_id__get",
35
+ "get_campaign_full_campaigns__campaign_id__full_get",
36
+ "get_campaign_generation_status_campaigns__campaign_id__status_get",
37
+ "get_campaign_ideas_task_status_campaigns_ideas_status__task_id__get",
38
+ "get_campaign_metrics_campaigns__campaign_id__metrics_get",
39
+ "get_campaign_sequence_campaigns__campaign_id__sequence_get",
40
+ "get_company_profile_company_profile_get",
41
+ "get_company_profile_workflow_status_company_profile_workflow_status_get",
42
+ "get_persona_personas__persona_id__get",
45
43
  "get_safe_to_send_deliverability_safe_to_send_get",
46
44
  "launch_campaign_campaigns__campaign_id__launch_post",
45
+ "list_campaign_documents_campaigns__campaign_id__documents_get",
47
46
  "list_campaigns_campaigns_get",
48
- "delete_campaign_campaigns__campaign_id__delete",
49
47
  "list_campaigns_for_picker_campaigns_picker_get",
50
- "get_campaign_metrics_campaigns__campaign_id__metrics_get",
51
- "list_campaign_documents_campaigns__campaign_id__documents_get",
52
- "get_campaign_document_campaigns__campaign_id__documents__document_id__get",
53
- "create_campaign_document_campaigns__campaign_id__documents_post",
48
+ "list_icps_icps_get",
49
+ "list_mailboxes_mailboxes_get",
50
+ "list_personas_personas_get",
51
+ "pin_icp_icps__icp_id__pin_post",
52
+ "pin_persona_personas__persona_id__pin_post",
53
+ "recommend_campaign_personas_campaigns_recommend_personas_post",
54
+ "regenerate_campaign_document_campaigns__campaign_id__documents__document_id__regenerate_post",
55
+ "unpin_icp_icps__icp_id__unpin_post",
56
+ "unpin_persona_personas__persona_id__unpin_post",
57
+ "update_campaign_campaigns__campaign_id__patch",
54
58
  "update_campaign_document_campaigns__campaign_id__documents__document_id__put",
55
- "delete_campaign_document_campaigns__campaign_id__documents__document_id__delete",
56
- "regenerate_campaign_document_campaigns__campaign_id__documents__document_id__regenerate_post"
59
+ "update_campaign_step_campaigns__campaign_id__sequence_steps__step_id__put",
60
+ "update_company_profile_company_profile_put",
61
+ "update_icp_icps__icp_id__patch",
62
+ "update_persona_personas__persona_id__patch"
57
63
  ],
58
64
  "components": [
59
65
  {
@@ -102,23 +108,26 @@
102
108
  "file": "components/campaign-wizard.tsx",
103
109
  "purpose": "Idea -> create campaign -> attach audience -> steps and copy -> launch. Every billable or external step is confirmed; ideas are polled.",
104
110
  "operations": [
105
- "get_company_profile_company_profile_get",
106
- "list_personas_personas_get",
107
- "generate_campaign_ideas_async_campaigns_generate_ideas_async_post",
108
- "get_campaign_ideas_task_status_campaigns_ideas_status__task_id__get",
109
- "create_campaign_campaigns_post",
110
- "update_campaign_campaigns__campaign_id__patch",
111
- "get_campaign_generation_status_campaigns__campaign_id__status_get",
112
- "get_campaign_full_campaigns__campaign_id__full_get",
113
111
  "attach_audience_to_campaign_campaigns__campaign_id__audience_put",
114
- "get_campaign_sequence_campaigns__campaign_id__sequence_get",
112
+ "create_campaign_campaigns_post",
113
+ "create_campaign_document_campaigns__campaign_id__documents_post",
115
114
  "create_campaign_step_campaigns__campaign_id__sequence_steps_post",
116
- "update_campaign_step_campaigns__campaign_id__sequence_steps__step_id__put",
117
115
  "delete_campaign_step_campaigns__campaign_id__sequence_steps__step_id__delete",
116
+ "generate_campaign_ideas_async_campaigns_generate_ideas_async_post",
118
117
  "generate_campaign_step_content_campaigns__campaign_id__sequence_steps__step_id__generate_post",
119
- "list_mailboxes_mailboxes_get",
118
+ "get_campaign_campaigns__campaign_id__get",
119
+ "get_campaign_full_campaigns__campaign_id__full_get",
120
+ "get_campaign_generation_status_campaigns__campaign_id__status_get",
121
+ "get_campaign_ideas_task_status_campaigns_ideas_status__task_id__get",
122
+ "get_campaign_sequence_campaigns__campaign_id__sequence_get",
123
+ "get_company_profile_company_profile_get",
120
124
  "get_safe_to_send_deliverability_safe_to_send_get",
121
- "launch_campaign_campaigns__campaign_id__launch_post"
125
+ "launch_campaign_campaigns__campaign_id__launch_post",
126
+ "list_mailboxes_mailboxes_get",
127
+ "list_personas_personas_get",
128
+ "regenerate_campaign_document_campaigns__campaign_id__documents__document_id__regenerate_post",
129
+ "update_campaign_campaigns__campaign_id__patch",
130
+ "update_campaign_step_campaigns__campaign_id__sequence_steps__step_id__put"
122
131
  ],
123
132
  "tiers": [
124
133
  "read",
@@ -161,15 +170,19 @@
161
170
  {
162
171
  "name": "CampaignLaunchStep",
163
172
  "file": "components/campaign-launch-step.tsx",
164
- "purpose": "Readiness checklist, sender choice and deliverability warning, then a confirmed launch.",
173
+ "purpose": "Readiness checklist (audience, steps, written copy, sender), deliverability and warm-up hold warnings, writes missing email copy with AI, then a confirmed launch.",
165
174
  "operations": [
166
175
  "get_campaign_full_campaigns__campaign_id__full_get",
167
176
  "list_mailboxes_mailboxes_get",
168
177
  "get_safe_to_send_deliverability_safe_to_send_get",
169
- "launch_campaign_campaigns__campaign_id__launch_post"
178
+ "launch_campaign_campaigns__campaign_id__launch_post",
179
+ "get_campaign_generation_status_campaigns__campaign_id__status_get",
180
+ "create_campaign_document_campaigns__campaign_id__documents_post",
181
+ "regenerate_campaign_document_campaigns__campaign_id__documents__document_id__regenerate_post"
170
182
  ],
171
183
  "tiers": [
172
184
  "read",
185
+ "billable",
173
186
  "external"
174
187
  ]
175
188
  },
@@ -245,6 +258,55 @@
245
258
  "purpose": "A label around a control; the layout piece the editors share.",
246
259
  "operations": [],
247
260
  "tiers": []
261
+ },
262
+ {
263
+ "name": "CampaignsScreen",
264
+ "file": "components/campaigns-screen.tsx",
265
+ "purpose": "Finished campaigns page: list, wizard sheet, detail tabs (details, metrics, documents, audience, sequence, launch), company profile and personas.",
266
+ "operations": [
267
+ "get_campaign_campaigns__campaign_id__get",
268
+ "get_campaign_full_campaigns__campaign_id__full_get",
269
+ "update_campaign_campaigns__campaign_id__patch"
270
+ ],
271
+ "tiers": [
272
+ "read",
273
+ "external"
274
+ ]
275
+ },
276
+ {
277
+ "name": "IcpDetail",
278
+ "file": "components/icp-detail.tsx",
279
+ "purpose": "One ideal customer profile, complete: every field; edit, pin or unpin, archive (confirmed).",
280
+ "operations": [
281
+ "list_icps_icps_get",
282
+ "update_icp_icps__icp_id__patch",
283
+ "pin_icp_icps__icp_id__pin_post",
284
+ "unpin_icp_icps__icp_id__unpin_post",
285
+ "archive_icp_icps__icp_id__archive_post"
286
+ ],
287
+ "tiers": [
288
+ "read",
289
+ "write",
290
+ "destructive"
291
+ ]
292
+ },
293
+ {
294
+ "name": "PersonaDetail",
295
+ "file": "components/persona-detail.tsx",
296
+ "purpose": "One buyer persona, complete: every field and the campaigns targeting it; edit, pin or unpin, archive (confirmed).",
297
+ "operations": [
298
+ "get_persona_personas__persona_id__get",
299
+ "list_campaigns_campaigns_get",
300
+ "update_persona_personas__persona_id__patch",
301
+ "pin_persona_personas__persona_id__pin_post",
302
+ "unpin_persona_personas__persona_id__unpin_post",
303
+ "archive_persona_personas__persona_id__archive_post"
304
+ ],
305
+ "tiers": [
306
+ "read",
307
+ "write",
308
+ "destructive"
309
+ ]
248
310
  }
249
311
  ],
250
312
  "hooks": [
@@ -356,6 +418,7 @@
356
418
  }
357
419
  ],
358
420
  "peerDependencies": {
359
- "react": ">=18"
421
+ "react": ">=18",
422
+ "lucide-react": ">=0.400.0"
360
423
  }
361
424
  }
@@ -27,13 +27,17 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
27
27
  import { useG8Query } from "@/g8/hooks/use-g8-query";
28
28
  import { useJobPoll } from "@/g8/hooks/use-job-poll";
29
29
  import { g8Delete, g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
30
- import { CREDIT_ESTIMATES, MAX_BODY_BYTES, byteLength, listOf, objectOf, statusClass, text } from "../lib/campaigns-utils";
30
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
31
+ import { CREDIT_ESTIMATES, MAX_BODY_BYTES, byteLength, fieldItems, listOf, objectOf, statusClass, text } from "../lib/campaigns-utils";
31
32
  import type { CampaignDocRow } from "../lib/campaigns-utils";
32
33
  import { CampaignSelect } from "./campaign-select";
33
34
  import { FormField } from "./form-field";
34
35
 
35
36
  export interface CampaignAssetsProps {
36
37
  campaignId?: string;
38
+ /** Controlled open document (a screen deep-links it). Omit to let the component keep it. */
39
+ documentId?: string | null;
40
+ onDocumentChange?: (documentId: string | null) => void;
37
41
  className?: string;
38
42
  }
39
43
 
@@ -74,6 +78,7 @@ function Editor({ campaignId, doc, onChanged, onClosed }: { campaignId: string;
74
78
  <h4 className="font-medium">{doc.display_name}</h4>
75
79
  <Button size="sm" variant="outline" onClick={onClosed}>Close</Button>
76
80
  </div>
81
+ <KeyValueList columns={2} items={fieldItems({ type: doc.file_type, status: text(objectOf(full.data).status) || doc.status, folder: doc.folder_path, updated_at: doc.updated_at, size: `${byteLength(text(objectOf(full.data).content)).toLocaleString()} bytes` })} />
77
82
  {full.error ? <ApiError error={full.error} title="Could not load the document" onRetry={() => void full.refetch()} /> : null}
78
83
  {full.loading && !full.data ? <Skeleton className="h-32 w-full" /> : null}
79
84
  {run.error ? <ApiError error={run.error} /> : null}
@@ -105,9 +110,15 @@ function Editor({ campaignId, doc, onChanged, onClosed }: { campaignId: string;
105
110
  );
106
111
  }
107
112
 
108
- function Documents({ campaignId, className }: { campaignId: string; className?: string }) {
113
+ function Documents({ campaignId, documentId, onDocumentChange, className }: { campaignId: string; documentId?: string | null; onDocumentChange?: (id: string | null) => void; className?: string }) {
109
114
  const docs = useG8Query((signal) => g8Get(`/campaigns/${campaignId}/documents`, undefined, { signal }), [campaignId]);
110
- const [openId, setOpenId] = useState<string | null>(null);
115
+ const [localId, setLocalId] = useState<string | null>(null);
116
+ const openId = documentId !== undefined ? documentId : localId;
117
+ const setOpenId = (next: string | null | ((cur: string | null) => string | null)) => {
118
+ const value = typeof next === "function" ? next(openId) : next;
119
+ setLocalId(value);
120
+ onDocumentChange?.(value);
121
+ };
111
122
  const [creating, setCreating] = useState(false);
112
123
  const [form, setForm] = useState({ display_name: "", file_type: FILE_TYPES[0], content: "" });
113
124
  const [confirmCreate, setConfirmCreate] = useState(false);
@@ -168,13 +179,13 @@ function Documents({ campaignId, className }: { campaignId: string; className?:
168
179
  );
169
180
  }
170
181
 
171
- export function CampaignAssets({ campaignId, className }: CampaignAssetsProps) {
182
+ export function CampaignAssets({ campaignId, documentId, onDocumentChange, className }: CampaignAssetsProps) {
172
183
  const [picked, setPicked] = useState<string | null>(null);
173
184
  const id = campaignId ?? picked;
174
185
  return (
175
186
  <div className={`space-y-3 ${className ?? ""}`}>
176
187
  {campaignId ? null : <CampaignSelect value={picked} onChange={setPicked} />}
177
- {id ? <Documents key={id} campaignId={id} /> : null}
188
+ {id ? <Documents key={id} campaignId={id} documentId={documentId} onDocumentChange={onDocumentChange} /> : null}
178
189
  </div>
179
190
  );
180
191
  }
@@ -2,31 +2,46 @@
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";
24
37
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
25
38
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
26
39
  import { useG8Query } from "@/g8/hooks/use-g8-query";
40
+ import { useJobPoll } from "@/g8/hooks/use-job-poll";
27
41
  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";
42
+ import { GRAPH8_APP_MAILBOXES_PATH } from "@/g8/mailboxes/lib/mailboxes-utils";
43
+ import { CREDIT_ESTIMATES, EMAILS_DOCUMENT, WARMUP_HOLD_DAYS, audienceOf, copyStateOf, listOf, objectOf, stepsOf, text, warmupHoldUntil } from "../lib/campaigns-utils";
44
+ import { brandUrl, useBrand } from "@/g8/hooks/use-brand";
30
45
 
31
46
  export interface CampaignLaunchStepProps {
32
47
  campaignId: string;
@@ -36,24 +51,43 @@ export interface CampaignLaunchStepProps {
36
51
  className?: string;
37
52
  }
38
53
 
39
- interface Mailbox { id: string | number; email?: string | null; connection_status?: string | null; is_archived?: boolean | null }
54
+ interface Mailbox { id: string | number; email?: string | null; connection_status?: string | null; is_archived?: boolean | null; created_at?: string | null }
40
55
 
41
- function Check({ ok, children }: { ok: boolean | undefined; children: string }) {
56
+ function Check({ ok, children, detail }: { ok: boolean | undefined; children: string; detail?: string }) {
42
57
  return (
43
58
  <li className={ok ? "text-green-700" : "text-amber-800"}>
44
59
  <span aria-hidden>{ok ? "Ready: " : "Missing: "}</span>{children}
60
+ {!ok && detail ? <span className="block pl-4 text-xs text-muted-foreground">{detail}</span> : null}
45
61
  </li>
46
62
  );
47
63
  }
48
64
 
65
+ const day = (d: Date) => d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" });
66
+
49
67
  export function CampaignLaunchStep({ campaignId, audienceSize, onLaunched, className }: CampaignLaunchStepProps) {
68
+ const brand = useBrand();
50
69
  const campaign = useG8Query((signal) => g8Get(`/campaigns/${campaignId}/full`, undefined, { signal }), [campaignId]);
70
+ const generation = useG8Query((signal) => g8Get(`/campaigns/${campaignId}/status`, undefined, { signal }), [campaignId]);
51
71
  const mailboxes = useG8Query((signal) => g8Get("/mailboxes", { limit: 200 }, { signal }), []);
52
72
  const verdict = useG8Query((signal) => g8Get("/deliverability/safe-to-send", undefined, { signal }), []);
53
73
  const [chosen, setChosen] = useState<Set<string> | null>(null);
54
74
  const [confirm, setConfirm] = useState(false);
75
+ const [confirmCopy, setConfirmCopy] = useState(false);
76
+ const [emailsDocId, setEmailsDocId] = useState<string | null>(null);
55
77
  const [result, setResult] = useState<Record<string, unknown> | null>(null);
56
78
 
79
+ // Copy being written (by the create-time generator or by "Write the email copy"): re-read /full until it settles.
80
+ const copyPoll = useJobPoll((signal) => g8Get(`/campaigns/${campaignId}/full`, undefined, { signal }), {
81
+ enabled: false, isTerminal: (s) => !copyStateOf(s).generating, intervalMs: 3000, maxAttempts: 60,
82
+ });
83
+ const full = copyPoll.status ?? campaign.data;
84
+ const copy = copyStateOf(full);
85
+ const genError = text(objectOf(generation.data).generation_error);
86
+ // Auto-follow only once: after the poll gives up, "Still writing" asks the person to check back.
87
+ const shouldPoll = copy.generating && !genError && copyPoll.attempts === 0 && !copyPoll.polling;
88
+ useEffect(() => { if (shouldPoll) copyPoll.start(); // eslint-disable-next-line react-hooks/exhaustive-deps
89
+ }, [shouldPoll]);
90
+
57
91
  const active = listOf<Mailbox>(mailboxes.data).filter((m) => m.connection_status === "active" && !m.is_archived);
58
92
  const senders = chosen ?? new Set(active.slice(0, 1).map((m) => String(m.id)));
59
93
  const launch = useG8Mutation(
@@ -63,21 +97,41 @@ export function CampaignLaunchStep({ campaignId, audienceSize, onLaunched, class
63
97
  onError: () => setConfirm(false),
64
98
  },
65
99
  );
100
+ const writeCopy = useG8Mutation(async () => {
101
+ let docId = emailsDocId ?? copy.emailsDocId;
102
+ if (!docId) {
103
+ const created = await g8Post(`/campaigns/${campaignId}/documents`, { ...EMAILS_DOCUMENT, content: "", status: "draft" });
104
+ docId = text(objectOf(created).id);
105
+ setEmailsDocId(docId); // stored at once: a failed regenerate is retried on this document, never a second one
106
+ }
107
+ return g8Post(`/campaigns/${campaignId}/documents/${docId}/regenerate`, {});
108
+ }, {
109
+ onSuccess: () => { setConfirmCopy(false); copyPoll.start(); },
110
+ onError: () => setConfirmCopy(false),
111
+ });
66
112
 
67
113
  const loading = [campaign, mailboxes].some((q) => q.loading && !q.data);
68
114
  const failedQuery = [campaign, mailboxes].find((q) => q.error);
69
115
  if (failedQuery) return <ApiError error={failedQuery.error} title="Could not check launch readiness" onRetry={() => { void campaign.refetch(); void mailboxes.refetch(); }} />;
70
116
  if (loading) return <Skeleton className="h-40 w-full" />;
71
117
 
72
- const c = objectOf(campaign.data);
73
- const audience = audienceOf(campaign.data);
118
+ const c = objectOf(full);
119
+ const audience = audienceOf(full);
74
120
  const hasAudience = audience.listId != null;
75
- const hasSteps = stepsOf(campaign.data).length > 0;
121
+ const hasSteps = stepsOf(full).length > 0;
122
+ // An older API without `documents` cannot be checked here: the server still checks at launch.
123
+ const copyOk = !copy.known || copy.ready;
76
124
  const alreadyLaunched = c.is_launched === true;
77
125
  const recipients = audienceSize ?? audience.size;
78
126
  const v = objectOf(verdict.data);
79
127
  const unsafe = !verdict.error && (v.safe_to_send ?? v.safe) === false;
80
- const ready = hasAudience && hasSteps && senders.size > 0 && !result && !alreadyLaunched;
128
+ const ready = hasAudience && hasSteps && copyOk && senders.size > 0 && !result && !alreadyLaunched;
129
+ // Once the follow-up poll gives up, the person may write the copy again rather than wait forever.
130
+ const writingCopy = writeCopy.loading || copyPoll.polling || (copy.generating && !genError && copyPoll.attempts === 0);
131
+ const held = active.filter((m) => senders.has(String(m.id))).map((m) => ({ m, until: warmupHoldUntil(m.created_at) })).filter((h) => h.until);
132
+ const copyDetail = writingCopy
133
+ ? "The copy is being written. This usually takes a minute or two."
134
+ : "The launch is refused until the campaign has written copy. Write it with AI below, or add it in Campaign assets.";
81
135
 
82
136
  if (result) {
83
137
  const channels = (result.channels_attached as Array<{ email?: string }> | undefined) ?? [];
@@ -99,18 +153,36 @@ export function CampaignLaunchStep({ campaignId, audienceSize, onLaunched, class
99
153
  <ul className="space-y-1 text-sm">
100
154
  <Check ok={hasAudience}>An audience list is attached</Check>
101
155
  <Check ok={hasSteps}>The sequence has at least one step</Check>
156
+ {copy.known ? <Check ok={copy.ready} detail={copyDetail}>The campaign copy is written</Check> : null}
102
157
  <Check ok={senders.size > 0}>At least one active sending mailbox is selected</Check>
103
158
  </ul>
159
+ {genError && !copy.ready ? (
160
+ <p role="alert" className="rounded bg-red-50 p-2 text-sm text-red-800">Writing the campaign documents failed: {genError}</p>
161
+ ) : null}
162
+ {copy.known && !copy.ready ? (
163
+ <div className="space-y-1">
164
+ {writeCopy.error ? <ApiError error={writeCopy.error} title="Could not write the copy" /> : null}
165
+ {copyPoll.done && !copyStateOf(copyPoll.status).ready && !writeCopy.loading ? <p role="status" className="text-sm text-amber-900">The copy did not finish. Try again, or write it in Campaign assets.</p> : null}
166
+ {!copyPoll.polling && !copyPoll.done && copyPoll.attempts > 0 ? <p role="status" className="text-sm text-amber-900">Still writing. Check back in a minute.</p> : null}
167
+ <Button variant="outline" loading={writeCopy.loading} disabled={writingCopy} onClick={() => setConfirmCopy(true)}>Write the email copy with AI</Button>
168
+ </div>
169
+ ) : null}
104
170
  {unsafe ? (
105
171
  <p role="alert" className="rounded bg-amber-50 p-2 text-sm text-amber-900">
106
172
  Deliverability warning: {typeof v.reason === "string" ? v.reason : "sending is not recommended right now."}
107
173
  </p>
108
174
  ) : null}
175
+ {held.length > 0 ? (
176
+ <div role="status" className="rounded bg-amber-50 p-2 text-sm text-amber-900">
177
+ <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>
178
+ <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>
179
+ </div>
180
+ ) : null}
109
181
  <fieldset className="space-y-1">
110
182
  <legend className="text-sm font-medium">Send from</legend>
111
183
  {active.length === 0 ? (
112
184
  <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.
185
+ 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
186
  </p>
115
187
  ) : null}
116
188
  {active.map((m) => (
@@ -142,6 +214,17 @@ export function CampaignLaunchStep({ campaignId, audienceSize, onLaunched, class
142
214
  onConfirm={() => void launch.mutate(undefined)}
143
215
  onCancel={() => setConfirm(false)}
144
216
  />
217
+ <ConfirmSpendDialog
218
+ open={confirmCopy}
219
+ tier="billable"
220
+ title={`Write the email copy for "${String(c.name ?? "this campaign")}" with AI`}
221
+ description="AI writes the emails from the campaign brief and its steps. Nothing is sent. Estimate only."
222
+ estimatedCredits={CREDIT_ESTIMATES.documentRegenerate}
223
+ confirmLabel="Write copy"
224
+ confirming={writeCopy.loading}
225
+ onConfirm={() => void writeCopy.mutate(undefined)}
226
+ onCancel={() => setConfirmCopy(false)}
227
+ />
145
228
  </section>
146
229
  );
147
230
  }
@@ -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>
@@ -13,13 +13,18 @@
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";
24
29
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
25
30
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
@@ -40,6 +45,11 @@ export interface CampaignWizardProps {
40
45
  /** Overrides the website from the saved profile. */
41
46
  websiteUrl?: string;
42
47
  onLaunched?: (result: Record<string, unknown>) => void;
48
+ /**
49
+ * Whether "Write the campaign documents with AI" starts checked. Default true: a campaign without written copy
50
+ * cannot launch. Pass false to start with it unchecked (the person writes the copy, or the Launch step does).
51
+ */
52
+ autoGenerateDocuments?: boolean;
43
53
  className?: string;
44
54
  }
45
55
 
@@ -48,7 +58,7 @@ const GOALS = ["awareness", "consideration", "decision"];
48
58
  const CHANNELS = ["email", "linkedin", "phone"];
49
59
 
50
60
  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 };
61
+ const emptyDraft: Draft = { name: "", category: "Outbound", goal: "", brief: "", core_concept: "", primary_hook: "", target_persona: "", channels: ["email"], autoGenerate: true };
52
62
 
53
63
  interface Idea { id?: string; campaign_name?: string; campaign_category?: string; core_concept?: string; why_this_works?: string; primary_hook?: string; target_channels?: string[] }
54
64
 
@@ -63,7 +73,7 @@ function IdeaStep({ websiteUrl, onPick, onSkip }: { websiteUrl: string; onPick:
63
73
  const start = useG8Mutation(
64
74
  () => g8Post("/campaigns/generate-ideas-async", {
65
75
  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",
76
+ target_personas: selected, persona_mode: selected.length ? "selected" : "ai_pick", // the server's AI-picks-the-persona mode
67
77
  }),
68
78
  { onSuccess: (res) => { setConfirm(false); setTaskId(text(objectOf(res).task_id) || null); }, onError: () => setConfirm(false) },
69
79
  );
@@ -132,19 +142,32 @@ function IdeaStep({ websiteUrl, onPick, onSkip }: { websiteUrl: string; onPick:
132
142
  }
133
143
 
134
144
  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>;
145
+ const poll = useJobPoll(async (signal) => objectOf(await g8Get(`/campaigns/${campaignId}/status`, undefined, { signal })), { isTerminal: (s) => Boolean(text(s.generation_error)) || settled(s), maxAttempts: 90 });
146
+ const failed = text(poll.status?.generation_error);
147
+ const label = failed ? "failed" : poll.polling ? "generating documents" : text(poll.status?.status) || "ready";
148
+ 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
149
  }
139
150
 
140
- export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, className }: CampaignWizardProps) {
151
+ export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, autoGenerateDocuments = true, className }: CampaignWizardProps) {
141
152
  const profile = useCompanyProfile(websiteUrl);
153
+ const initialDraft: Draft = { ...emptyDraft, autoGenerate: autoGenerateDocuments };
142
154
  const [step, setStep] = useState(initialCampaignId ? 2 : 0);
143
- const [draft, setDraft] = useState<Draft>(emptyDraft);
155
+ const [draft, setDraft] = useState<Draft>(initialDraft);
156
+ const [dispatchFailed, setDispatchFailed] = useState(false);
144
157
  const [campaignId, setCampaignId] = useState<string | null>(initialCampaignId ?? null);
145
158
  const [audience, setAudience] = useState<ListSummary | null>(null);
146
159
  const [confirm, setConfirm] = useState(false);
147
160
  const [resetKey, setResetKey] = useState(0);
161
+ // Resuming a saved campaign: the form shows (and can save) nothing until the campaign itself has loaded, otherwise
162
+ // Save would PATCH the blank defaults over the real name, goal and brief.
163
+ const resume = useG8Query((signal) => g8Get(`/campaigns/${initialCampaignId}`, undefined, { signal }), [initialCampaignId], { enabled: Boolean(initialCampaignId) });
164
+ const [seeded, setSeeded] = useState(!initialCampaignId);
165
+ useEffect(() => {
166
+ if (seeded || !resume.data) return;
167
+ const c = objectOf(resume.data);
168
+ 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) }));
169
+ setSeeded(true);
170
+ }, [resume.data, seeded]);
148
171
 
149
172
  const save = useG8Mutation(async () => {
150
173
  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 +178,7 @@ export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, clas
155
178
  setConfirm(false);
156
179
  const id = text(objectOf(res).id);
157
180
  if (!campaignId && id) setCampaignId(id); // stored at once: a later failure never re-creates the campaign
181
+ if (!campaignId && objectOf(res).generation_status === "failed_to_dispatch") setDispatchFailed(true);
158
182
  setStep(2);
159
183
  },
160
184
  onError: () => setConfirm(false),
@@ -165,7 +189,7 @@ export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, clas
165
189
  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
190
  setStep(1);
167
191
  };
168
- const reset = () => { setStep(0); setDraft(emptyDraft); setCampaignId(null); setAudience(null); setConfirm(false); save.reset(); setResetKey((k) => k + 1); };
192
+ const reset = () => { setSeeded(true); setStep(0); setDraft(initialDraft); setDispatchFailed(false); setCampaignId(null); setAudience(null); setConfirm(false); save.reset(); setResetKey((k) => k + 1); };
169
193
  const autoGen = !campaignId && draft.autoGenerate;
170
194
 
171
195
  return (
@@ -181,8 +205,14 @@ export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, clas
181
205
  <Button size="sm" variant="outline" className="ml-auto" onClick={reset}>Start over</Button>
182
206
  </nav>
183
207
 
208
+ {dispatchFailed ? (
209
+ <p role="alert" className="rounded bg-amber-50 p-2 text-sm text-amber-900">
210
+ The campaign was created, but writing its documents could not be started. Write the email copy from the Launch step before launching.
211
+ </p>
212
+ ) : null}
184
213
  {step === 0 ? <IdeaStep key={resetKey} websiteUrl={profile.websiteUrl} onPick={pickIdea} onSkip={() => setStep(1)} /> : null}
185
- {step === 1 ? (
214
+ {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}
215
+ {step === 1 && seeded ? (
186
216
  <section aria-label="Campaign" className="space-y-3">
187
217
  <FormField label="Campaign name" htmlFor="cw-name"><Input id="cw-name" value={draft.name} onChange={set("name")} /></FormField>
188
218
  <div className="grid gap-3 sm:grid-cols-2">
@@ -206,6 +236,9 @@ export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, clas
206
236
  <input type="checkbox" checked={draft.autoGenerate} onChange={(e) => setDraft({ ...draft, autoGenerate: e.target.checked })} />
207
237
  Write the campaign documents with AI after creating it (uses credits)
208
238
  </label>
239
+ {!draft.autoGenerate && draft.channels.includes("email") ? (
240
+ <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>
241
+ ) : null}
209
242
  </>
210
243
  ) : null}
211
244
  {save.error ? <ApiError error={save.error} title={campaignId ? "Could not save the campaign" : "Could not create the campaign"} /> : null}