@graph8/sdk 0.303.0 → 0.357.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 (664) hide show
  1. package/README.md +81 -0
  2. package/blocks/README.md +32 -0
  3. package/blocks/_foundation/README.md +78 -0
  4. package/blocks/_foundation/block.json +79 -0
  5. package/blocks/_foundation/components/api-error.tsx +62 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +142 -0
  7. package/blocks/_foundation/components/empty-state.tsx +24 -0
  8. package/blocks/_foundation/hooks/use-g8-mutation.ts +88 -0
  9. package/blocks/_foundation/hooks/use-g8-query.ts +87 -0
  10. package/blocks/_foundation/hooks/use-job-poll.ts +130 -0
  11. package/blocks/_foundation/hooks/use-paginated.ts +105 -0
  12. package/blocks/_foundation/lib/g8-client.ts +128 -0
  13. package/blocks/_foundation/recipes/verify-proxy-key.ts +18 -0
  14. package/blocks/_foundation/server/proxy.ts +233 -0
  15. package/blocks/_foundation/server/route.ts +30 -0
  16. package/blocks/agents/README.md +133 -0
  17. package/blocks/agents/block.json +223 -0
  18. package/blocks/agents/components/agent-builder.tsx +199 -0
  19. package/blocks/agents/components/agent-knowledge-panel.tsx +144 -0
  20. package/blocks/agents/components/agent-runs.tsx +175 -0
  21. package/blocks/agents/components/agent-skills-panel.tsx +126 -0
  22. package/blocks/agents/components/agent-test-console.tsx +175 -0
  23. package/blocks/agents/lib/agent-types.ts +114 -0
  24. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +50 -0
  25. package/blocks/agents/recipes/run-and-follow-agent-run.ts +46 -0
  26. package/blocks/agents/recipes/test-agent-before-activating.ts +36 -0
  27. package/blocks/analytics/README.md +73 -0
  28. package/blocks/analytics/block.json +178 -0
  29. package/blocks/analytics/components/funnel-chart.tsx +79 -0
  30. package/blocks/analytics/components/kpi-tiles.tsx +74 -0
  31. package/blocks/analytics/components/leaderboard.tsx +86 -0
  32. package/blocks/analytics/components/report-builder.tsx +200 -0
  33. package/blocks/analytics/hooks/use-report.ts +26 -0
  34. package/blocks/analytics/lib/analytics-types.ts +108 -0
  35. package/blocks/analytics/lib/use-debounced.ts +16 -0
  36. package/blocks/analytics/recipes/01-weekly-outbound-report.ts +32 -0
  37. package/blocks/analytics/recipes/02-revenue-snapshot.ts +22 -0
  38. package/blocks/analytics/recipes/03-saved-report.ts +30 -0
  39. package/blocks/appointments/README.md +80 -0
  40. package/blocks/appointments/block.json +218 -0
  41. package/blocks/appointments/components/availability-editor.tsx +192 -0
  42. package/blocks/appointments/components/booking-page.tsx +154 -0
  43. package/blocks/appointments/components/event-type-editor.tsx +214 -0
  44. package/blocks/appointments/components/meeting-detail.tsx +147 -0
  45. package/blocks/appointments/components/meeting-list.tsx +148 -0
  46. package/blocks/appointments/hooks/use-available-slots.ts +29 -0
  47. package/blocks/appointments/hooks/use-schedules.ts +18 -0
  48. package/blocks/appointments/lib/appointments-types.ts +159 -0
  49. package/blocks/appointments/lib/idempotency.ts +30 -0
  50. package/blocks/appointments/recipes/01-create-event-type-and-hours.ts +27 -0
  51. package/blocks/appointments/recipes/02-find-slots-and-book.ts +34 -0
  52. package/blocks/appointments/recipes/03-bookings-digest.ts +22 -0
  53. package/blocks/appointments/recipes/04-calendar-invite.ts +21 -0
  54. package/blocks/block.schema.json +130 -0
  55. package/blocks/block.types.ts +109 -0
  56. package/blocks/campaigns/README.md +118 -0
  57. package/blocks/campaigns/block.json +361 -0
  58. package/blocks/campaigns/components/campaign-assets.tsx +180 -0
  59. package/blocks/campaigns/components/campaign-audience-step.tsx +91 -0
  60. package/blocks/campaigns/components/campaign-launch-step.tsx +147 -0
  61. package/blocks/campaigns/components/campaign-list.tsx +120 -0
  62. package/blocks/campaigns/components/campaign-metrics.tsx +93 -0
  63. package/blocks/campaigns/components/campaign-select.tsx +52 -0
  64. package/blocks/campaigns/components/campaign-sequence-step.tsx +158 -0
  65. package/blocks/campaigns/components/campaign-wizard.tsx +238 -0
  66. package/blocks/campaigns/components/form-field.tsx +29 -0
  67. package/blocks/campaigns/components/icp-editor.tsx +230 -0
  68. package/blocks/campaigns/components/persona-editor.tsx +210 -0
  69. package/blocks/campaigns/hooks/use-company-profile.ts +34 -0
  70. package/blocks/campaigns/lib/campaigns-utils.ts +155 -0
  71. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +33 -0
  72. package/blocks/campaigns/recipes/campaign-report.ts +22 -0
  73. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +51 -0
  74. package/blocks/campaigns/recipes/set-up-icp-and-personas.ts +37 -0
  75. package/blocks/coach/README.md +93 -0
  76. package/blocks/coach/block.json +125 -0
  77. package/blocks/coach/components/call-scorecard.tsx +126 -0
  78. package/blocks/coach/components/coaching-feed.tsx +140 -0
  79. package/blocks/coach/lib/coach-types.ts +116 -0
  80. package/blocks/coach/recipes/read-a-scorecard.ts +27 -0
  81. package/blocks/coach/recipes/weekly-coaching-digest.ts +53 -0
  82. package/blocks/copilot/README.md +99 -0
  83. package/blocks/copilot/block.json +151 -0
  84. package/blocks/copilot/components/copilot-memory.tsx +87 -0
  85. package/blocks/copilot/components/copilot-panel.tsx +189 -0
  86. package/blocks/copilot/components/copilot-team-chat.tsx +111 -0
  87. package/blocks/copilot/lib/copilot-types.ts +111 -0
  88. package/blocks/copilot/recipes/ask-the-copilot.ts +35 -0
  89. package/blocks/copilot/recipes/post-in-a-work-channel.ts +33 -0
  90. package/blocks/copilot/recipes/remember-and-recall.ts +19 -0
  91. package/blocks/crm/README.md +122 -0
  92. package/blocks/crm/block.json +378 -0
  93. package/blocks/crm/components/activity-timeline.tsx +99 -0
  94. package/blocks/crm/components/company-detail.tsx +118 -0
  95. package/blocks/crm/components/company-table.tsx +173 -0
  96. package/blocks/crm/components/contact-detail.tsx +135 -0
  97. package/blocks/crm/components/contact-table.tsx +217 -0
  98. package/blocks/crm/components/dedupe-review.tsx +164 -0
  99. package/blocks/crm/components/import-wizard.tsx +193 -0
  100. package/blocks/crm/components/notes-panel.tsx +112 -0
  101. package/blocks/crm/components/record-form.tsx +191 -0
  102. package/blocks/crm/components/task-list.tsx +107 -0
  103. package/blocks/crm/hooks/use-custom-fields.ts +18 -0
  104. package/blocks/crm/hooks/use-debounced.ts +13 -0
  105. package/blocks/crm/hooks/use-page-query.ts +52 -0
  106. package/blocks/crm/lib/crm-types.ts +136 -0
  107. package/blocks/crm/recipes/custom-fields-and-values.ts +28 -0
  108. package/blocks/crm/recipes/dedupe-and-merge.ts +30 -0
  109. package/blocks/crm/recipes/notes-tasks-and-activity.ts +23 -0
  110. package/blocks/crm/recipes/upsert-contact-with-company.ts +33 -0
  111. package/blocks/deals/README.md +78 -0
  112. package/blocks/deals/block.json +198 -0
  113. package/blocks/deals/components/deal-detail.tsx +134 -0
  114. package/blocks/deals/components/forecast-view.tsx +112 -0
  115. package/blocks/deals/components/pipeline-board.tsx +142 -0
  116. package/blocks/deals/components/stage-editor.tsx +96 -0
  117. package/blocks/deals/hooks/use-pipeline-deals.ts +67 -0
  118. package/blocks/deals/hooks/use-pipelines.ts +19 -0
  119. package/blocks/deals/lib/deals-types.ts +128 -0
  120. package/blocks/deals/recipes/01-create-deal-and-advance.ts +32 -0
  121. package/blocks/deals/recipes/02-pipeline-report.ts +35 -0
  122. package/blocks/deals/recipes/03-stale-deals-sweep.ts +20 -0
  123. package/blocks/dialer/README.md +114 -0
  124. package/blocks/dialer/block.json +260 -0
  125. package/blocks/dialer/components/call-log.tsx +78 -0
  126. package/blocks/dialer/components/call-queue.tsx +188 -0
  127. package/blocks/dialer/components/dialer-workspace.tsx +47 -0
  128. package/blocks/dialer/components/disposition-panel.tsx +75 -0
  129. package/blocks/dialer/components/live-calls-table.tsx +107 -0
  130. package/blocks/dialer/components/session-start-form.tsx +111 -0
  131. package/blocks/dialer/components/softphone.tsx +122 -0
  132. package/blocks/dialer/components/transcript-viewer.tsx +82 -0
  133. package/blocks/dialer/hooks/use-dialer-numbers.ts +19 -0
  134. package/blocks/dialer/hooks/use-dialer-session.ts +68 -0
  135. package/blocks/dialer/hooks/use-dispositions.ts +29 -0
  136. package/blocks/dialer/lib/dialer-types.ts +158 -0
  137. package/blocks/dialer/lib/softphone-transport.ts +67 -0
  138. package/blocks/dialer/lib/twilio-transport.ts +74 -0
  139. package/blocks/dialer/recipes/01-create-session-and-dial.ts +39 -0
  140. package/blocks/dialer/recipes/02-disposition-calls.ts +26 -0
  141. package/blocks/dialer/recipes/03-review-a-call.ts +23 -0
  142. package/blocks/enrichment/README.md +105 -0
  143. package/blocks/enrichment/block.json +204 -0
  144. package/blocks/enrichment/components/bulk-enrich-panel.tsx +205 -0
  145. package/blocks/enrichment/components/email-verify-badge.tsx +82 -0
  146. package/blocks/enrichment/components/enrich-button.tsx +136 -0
  147. package/blocks/enrichment/components/waterfall-config.tsx +127 -0
  148. package/blocks/enrichment/hooks/use-providers.ts +18 -0
  149. package/blocks/enrichment/lib/enrichment-types.ts +104 -0
  150. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +41 -0
  151. package/blocks/enrichment/recipes/lookup-and-verify.ts +25 -0
  152. package/blocks/enrichment/recipes/waterfall-pipeline.ts +26 -0
  153. package/blocks/forms/README.md +112 -0
  154. package/blocks/forms/block.json +200 -0
  155. package/blocks/forms/components/form-builder.tsx +150 -0
  156. package/blocks/forms/components/form-embed.tsx +103 -0
  157. package/blocks/forms/components/form-mapping-editor.tsx +125 -0
  158. package/blocks/forms/components/site-audit-panel.tsx +73 -0
  159. package/blocks/forms/components/submissions-table.tsx +158 -0
  160. package/blocks/forms/lib/form-types.ts +127 -0
  161. package/blocks/forms/recipes/install-snippet-and-form.ts +21 -0
  162. package/blocks/forms/recipes/retry-failed-deliveries.ts +26 -0
  163. package/blocks/forms/recipes/route-form-to-list.ts +44 -0
  164. package/blocks/inbox/README.md +72 -0
  165. package/blocks/inbox/block.json +194 -0
  166. package/blocks/inbox/components/inbox-list.tsx +156 -0
  167. package/blocks/inbox/components/inbox-screen.tsx +49 -0
  168. package/blocks/inbox/components/reply-composer.tsx +183 -0
  169. package/blocks/inbox/components/tag-and-assign.tsx +189 -0
  170. package/blocks/inbox/components/thread-badge.tsx +17 -0
  171. package/blocks/inbox/components/thread-view.tsx +108 -0
  172. package/blocks/inbox/hooks/use-inbox-thread.ts +24 -0
  173. package/blocks/inbox/hooks/use-inbox-threads.ts +40 -0
  174. package/blocks/inbox/lib/idempotency.ts +30 -0
  175. package/blocks/inbox/lib/inbox-types.ts +100 -0
  176. package/blocks/inbox/recipes/01-triage-and-reply.ts +43 -0
  177. package/blocks/inbox/recipes/02-tag-and-assign.ts +31 -0
  178. package/blocks/inbox/recipes/03-hot-contacts-digest.ts +16 -0
  179. package/blocks/integrations/README.md +75 -0
  180. package/blocks/integrations/block.json +172 -0
  181. package/blocks/integrations/components/integration-list.tsx +165 -0
  182. package/blocks/integrations/components/sync-status.tsx +223 -0
  183. package/blocks/integrations/components/webhook-manager.tsx +300 -0
  184. package/blocks/integrations/lib/integrations-utils.ts +62 -0
  185. package/blocks/integrations/recipes/crm-sync-health.ts +19 -0
  186. package/blocks/integrations/recipes/push-contacts-to-crm.ts +20 -0
  187. package/blocks/integrations/recipes/register-webhook.ts +18 -0
  188. package/blocks/integrations/recipes/verify-webhook-route.ts +33 -0
  189. package/blocks/intent/README.md +57 -0
  190. package/blocks/intent/block.json +138 -0
  191. package/blocks/intent/components/account-intent-score.tsx +91 -0
  192. package/blocks/intent/components/intent-feed.tsx +215 -0
  193. package/blocks/intent/components/intent-keyword-manager.tsx +331 -0
  194. package/blocks/intent/hooks/use-account-intent.ts +46 -0
  195. package/blocks/intent/lib/intent-utils.ts +95 -0
  196. package/blocks/intent/recipes/rank-intent-accounts.ts +36 -0
  197. package/blocks/intent/recipes/track-competitor-domain.ts +23 -0
  198. package/blocks/intent/recipes/who-visited-url.ts +18 -0
  199. package/blocks/lifecycle/README.md +79 -0
  200. package/blocks/lifecycle/block.json +198 -0
  201. package/blocks/lifecycle/components/churn-list.tsx +92 -0
  202. package/blocks/lifecycle/components/customer-table.tsx +119 -0
  203. package/blocks/lifecycle/components/lead-queue.tsx +127 -0
  204. package/blocks/lifecycle/components/lifecycle-board.tsx +102 -0
  205. package/blocks/lifecycle/components/stage-move-dialog.tsx +75 -0
  206. package/blocks/lifecycle/hooks/use-debounced.ts +13 -0
  207. package/blocks/lifecycle/hooks/use-lifecycle-stages.ts +27 -0
  208. package/blocks/lifecycle/lib/lifecycle.ts +99 -0
  209. package/blocks/lifecycle/recipes/customer-brief.ts +23 -0
  210. package/blocks/lifecycle/recipes/move-company-stage.ts +17 -0
  211. package/blocks/lifecycle/recipes/read-lifecycle-board.ts +21 -0
  212. package/blocks/lifecycle/recipes/unlock-stage-contacts.ts +25 -0
  213. package/blocks/lists/README.md +96 -0
  214. package/blocks/lists/block.json +166 -0
  215. package/blocks/lists/components/add-to-list-button.tsx +111 -0
  216. package/blocks/lists/components/list-picker.tsx +94 -0
  217. package/blocks/lists/components/list-table.tsx +160 -0
  218. package/blocks/lists/components/suppression-manager.tsx +153 -0
  219. package/blocks/lists/recipes/create-list-and-add-contacts.ts +35 -0
  220. package/blocks/lists/recipes/refresh-and-check-lists.ts +21 -0
  221. package/blocks/lists/recipes/suppress-and-reinstate.ts +30 -0
  222. package/blocks/mailboxes/README.md +71 -0
  223. package/blocks/mailboxes/block.json +187 -0
  224. package/blocks/mailboxes/components/connect-mailbox.tsx +284 -0
  225. package/blocks/mailboxes/components/domain-purchase.tsx +271 -0
  226. package/blocks/mailboxes/components/mailbox-list.tsx +185 -0
  227. package/blocks/mailboxes/components/warmup-health.tsx +232 -0
  228. package/blocks/mailboxes/lib/mailboxes-utils.ts +69 -0
  229. package/blocks/mailboxes/recipes/buy-domain-and-mailboxes.ts +64 -0
  230. package/blocks/mailboxes/recipes/connect-imap-mailbox.ts +38 -0
  231. package/blocks/mailboxes/recipes/is-it-safe-to-send.ts +17 -0
  232. package/blocks/mailboxes/recipes/warmup-report.ts +31 -0
  233. package/blocks/newsletter/README.md +122 -0
  234. package/blocks/newsletter/block.json +224 -0
  235. package/blocks/newsletter/components/issue-form.tsx +124 -0
  236. package/blocks/newsletter/components/issue-scheduler.tsx +152 -0
  237. package/blocks/newsletter/components/newsletter-editor.tsx +131 -0
  238. package/blocks/newsletter/components/newsletter-stats.tsx +93 -0
  239. package/blocks/newsletter/components/subscriber-table.tsx +173 -0
  240. package/blocks/newsletter/lib/newsletter-types.ts +97 -0
  241. package/blocks/newsletter/recipes/create-newsletter-and-draft-issue.ts +27 -0
  242. package/blocks/newsletter/recipes/import-audience-and-send.ts +43 -0
  243. package/blocks/newsletter/recipes/issue-performance-report.ts +26 -0
  244. package/blocks/nurture/README.md +110 -0
  245. package/blocks/nurture/block.json +191 -0
  246. package/blocks/nurture/components/nurture-builder.tsx +114 -0
  247. package/blocks/nurture/components/nurture-enroll.tsx +107 -0
  248. package/blocks/nurture/components/nurture-form.tsx +120 -0
  249. package/blocks/nurture/components/nurture-stats.tsx +81 -0
  250. package/blocks/nurture/lib/nurture-types.ts +109 -0
  251. package/blocks/nurture/recipes/create-a-nurture.ts +34 -0
  252. package/blocks/nurture/recipes/enroll-contacts-and-launch.ts +24 -0
  253. package/blocks/nurture/recipes/nurture-health-check.ts +28 -0
  254. package/blocks/objects/README.md +120 -0
  255. package/blocks/objects/block.json +243 -0
  256. package/blocks/objects/components/attribute-field-input.tsx +82 -0
  257. package/blocks/objects/components/attribute-form-dialog.tsx +159 -0
  258. package/blocks/objects/components/object-not-found.tsx +34 -0
  259. package/blocks/objects/components/object-record-detail.tsx +143 -0
  260. package/blocks/objects/components/object-schema-editor.tsx +197 -0
  261. package/blocks/objects/components/object-table.tsx +130 -0
  262. package/blocks/objects/components/record-form-dialog.tsx +112 -0
  263. package/blocks/objects/components/record-roles-panel.tsx +106 -0
  264. package/blocks/objects/hooks/use-object-schema.ts +36 -0
  265. package/blocks/objects/lib/object-types.ts +214 -0
  266. package/blocks/objects/recipes/define-object-and-add-records.ts +24 -0
  267. package/blocks/objects/recipes/update-audit-and-archive-records.ts +39 -0
  268. package/blocks/objects/recipes/upsert-records-from-your-system.ts +30 -0
  269. package/blocks/pages/README.md +104 -0
  270. package/blocks/pages/block.json +223 -0
  271. package/blocks/pages/components/brand-kit-editor.tsx +152 -0
  272. package/blocks/pages/components/content-library.tsx +163 -0
  273. package/blocks/pages/components/page-editor.tsx +253 -0
  274. package/blocks/pages/components/page-list.tsx +159 -0
  275. package/blocks/pages/components/seo-report.tsx +159 -0
  276. package/blocks/pages/lib/studio.ts +77 -0
  277. package/blocks/pages/recipes/audit-site-seo.ts +28 -0
  278. package/blocks/pages/recipes/create-and-publish-page.ts +26 -0
  279. package/blocks/pages/recipes/edit-page-section.ts +25 -0
  280. package/blocks/pages/recipes/refresh-brand-kit.ts +22 -0
  281. package/blocks/quotes/README.md +79 -0
  282. package/blocks/quotes/block.json +183 -0
  283. package/blocks/quotes/components/product-picker.tsx +80 -0
  284. package/blocks/quotes/components/quote-builder.tsx +195 -0
  285. package/blocks/quotes/components/quote-list.tsx +79 -0
  286. package/blocks/quotes/components/quote-viewer.tsx +135 -0
  287. package/blocks/quotes/components/signature-status.tsx +77 -0
  288. package/blocks/quotes/hooks/use-debounced.ts +13 -0
  289. package/blocks/quotes/lib/quotes.ts +142 -0
  290. package/blocks/quotes/recipes/create-and-send-quote.ts +44 -0
  291. package/blocks/quotes/recipes/revise-sent-quote.ts +18 -0
  292. package/blocks/quotes/recipes/track-quote-signature.ts +22 -0
  293. package/blocks/radar/README.md +106 -0
  294. package/blocks/radar/block.json +189 -0
  295. package/blocks/radar/components/competitor-battle-card.tsx +53 -0
  296. package/blocks/radar/components/competitor-tracker.tsx +158 -0
  297. package/blocks/radar/components/radar-overview.tsx +67 -0
  298. package/blocks/radar/components/radar-report.tsx +86 -0
  299. package/blocks/radar/hooks/use-competitors.ts +23 -0
  300. package/blocks/radar/lib/radar-types.ts +124 -0
  301. package/blocks/radar/recipes/battle-card.ts +28 -0
  302. package/blocks/radar/recipes/deep-dive-report.ts +32 -0
  303. package/blocks/radar/recipes/marketing-snapshot.ts +18 -0
  304. package/blocks/radar/recipes/track-and-scan.ts +39 -0
  305. package/blocks/search/README.md +129 -0
  306. package/blocks/search/block.json +185 -0
  307. package/blocks/search/components/filter-builder.tsx +149 -0
  308. package/blocks/search/components/prospect-results.tsx +175 -0
  309. package/blocks/search/components/save-to-list-dialog.tsx +135 -0
  310. package/blocks/search/components/saved-searches.tsx +95 -0
  311. package/blocks/search/hooks/use-filter-options.ts +41 -0
  312. package/blocks/search/lib/search-types.ts +149 -0
  313. package/blocks/search/recipes/explore-filter-values.ts +27 -0
  314. package/blocks/search/recipes/saved-searches.ts +34 -0
  315. package/blocks/search/recipes/search-and-save-to-list.ts +37 -0
  316. package/blocks/sequencer/README.md +87 -0
  317. package/blocks/sequencer/block.json +214 -0
  318. package/blocks/sequencer/components/enroll-dialog.tsx +93 -0
  319. package/blocks/sequencer/components/sequence-builder.tsx +306 -0
  320. package/blocks/sequencer/components/sequence-list.tsx +200 -0
  321. package/blocks/sequencer/components/sequence-stats.tsx +256 -0
  322. package/blocks/sequencer/components/step-editor.tsx +213 -0
  323. package/blocks/sequencer/lib/sequencer-utils.ts +168 -0
  324. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +51 -0
  325. package/blocks/sequencer/recipes/launch-and-enroll.ts +44 -0
  326. package/blocks/sequencer/recipes/pause-when-unsafe.ts +30 -0
  327. package/blocks/sequencer/recipes/sequence-report.ts +22 -0
  328. package/blocks/social-ads/README.md +128 -0
  329. package/blocks/social-ads/block.json +256 -0
  330. package/blocks/social-ads/components/ad-campaign-list.tsx +136 -0
  331. package/blocks/social-ads/components/audience-sync.tsx +150 -0
  332. package/blocks/social-ads/components/creative-gallery.tsx +121 -0
  333. package/blocks/social-ads/components/post-composer.tsx +122 -0
  334. package/blocks/social-ads/components/social-post-list.tsx +116 -0
  335. package/blocks/social-ads/hooks/use-social-accounts.ts +16 -0
  336. package/blocks/social-ads/lib/social-ads-types.ts +167 -0
  337. package/blocks/social-ads/recipes/creative-to-gallery.ts +41 -0
  338. package/blocks/social-ads/recipes/schedule-post.ts +51 -0
  339. package/blocks/social-ads/recipes/sync-audience-and-launch.ts +41 -0
  340. package/blocks/visitors/README.md +56 -0
  341. package/blocks/visitors/block.json +139 -0
  342. package/blocks/visitors/components/snippet-install.tsx +169 -0
  343. package/blocks/visitors/components/traffic-chart.tsx +156 -0
  344. package/blocks/visitors/components/visitor-company-card.tsx +92 -0
  345. package/blocks/visitors/components/visitor-feed.tsx +157 -0
  346. package/blocks/visitors/lib/visitors-utils.ts +78 -0
  347. package/blocks/visitors/recipes/check-snippet-installed.ts +24 -0
  348. package/blocks/visitors/recipes/recent-company-visits.ts +27 -0
  349. package/blocks/visitors/recipes/traffic-summary.ts +24 -0
  350. package/blocks/webchat/README.md +87 -0
  351. package/blocks/webchat/block.json +134 -0
  352. package/blocks/webchat/components/chat-widget-config.tsx +160 -0
  353. package/blocks/webchat/components/conversation-list.tsx +74 -0
  354. package/blocks/webchat/lib/webchat-types.ts +83 -0
  355. package/blocks/webchat/recipes/create-bot-and-style-widget.ts +24 -0
  356. package/blocks/webchat/recipes/list-chat-visitors.ts +24 -0
  357. package/blocks/workbench/README.md +135 -0
  358. package/blocks/workbench/block.json +259 -0
  359. package/blocks/workbench/components/column-recipe-editor.tsx +183 -0
  360. package/blocks/workbench/components/run-progress.tsx +132 -0
  361. package/blocks/workbench/components/workbench-add-rows.tsx +100 -0
  362. package/blocks/workbench/components/workbench-grid.tsx +159 -0
  363. package/blocks/workbench/components/workbench-row-actions.tsx +102 -0
  364. package/blocks/workbench/components/workbench-table-list.tsx +105 -0
  365. package/blocks/workbench/hooks/use-workbench-table.ts +30 -0
  366. package/blocks/workbench/lib/waterfall-presets.ts +119 -0
  367. package/blocks/workbench/lib/workbench-types.ts +191 -0
  368. package/blocks/workbench/recipes/build-enriched-table.ts +65 -0
  369. package/blocks/workbench/recipes/import-from-search.ts +42 -0
  370. package/blocks/workbench/recipes/scrape-into-table.ts +36 -0
  371. package/blocks/workbench/recipes/waterfall-column.ts +55 -0
  372. package/blocks/workflows/README.md +100 -0
  373. package/blocks/workflows/block.json +188 -0
  374. package/blocks/workflows/components/run-history.tsx +136 -0
  375. package/blocks/workflows/components/trigger-config.tsx +134 -0
  376. package/blocks/workflows/components/workflow-editor.tsx +142 -0
  377. package/blocks/workflows/components/workflow-list.tsx +153 -0
  378. package/blocks/workflows/hooks/use-workflow.ts +51 -0
  379. package/blocks/workflows/lib/graph.ts +187 -0
  380. package/blocks/workflows/recipes/create-workflow.ts +35 -0
  381. package/blocks/workflows/recipes/find-failed-runs.ts +15 -0
  382. package/blocks/workflows/recipes/run-workflow-and-wait.ts +32 -0
  383. package/blocks/workflows/recipes/set-trigger.ts +22 -0
  384. package/dist/cli.js +5330 -29
  385. package/dist/index.browser.js +7740 -0
  386. package/dist/index.browser.js.map +1 -0
  387. package/dist/index.browser.mjs +7717 -0
  388. package/dist/index.browser.mjs.map +1 -0
  389. package/dist/index.d.mts +2220 -23
  390. package/dist/index.d.ts +2220 -23
  391. package/dist/index.js +64 -3
  392. package/dist/index.js.map +1 -1
  393. package/dist/index.mjs +64 -3
  394. package/dist/index.mjs.map +1 -1
  395. package/dist/react.browser.js +7598 -0
  396. package/dist/react.browser.js.map +1 -0
  397. package/dist/react.browser.mjs +7568 -0
  398. package/dist/react.browser.mjs.map +1 -0
  399. package/dist/react.d.mts +2082 -20
  400. package/dist/react.d.ts +2082 -20
  401. package/dist/react.js +63 -2
  402. package/dist/react.js.map +1 -1
  403. package/dist/react.mjs +63 -2
  404. package/dist/react.mjs.map +1 -1
  405. package/dist/types/agentSetup.d.ts +6 -6
  406. package/dist/types/blocks/add.d.ts +53 -0
  407. package/dist/types/blocks/create.d.ts +46 -0
  408. package/dist/types/blocks/doctor.d.ts +26 -0
  409. package/dist/types/blocks/registry.d.ts +134 -0
  410. package/dist/types/cli.d.ts +16 -0
  411. package/dist/types/core.d.ts +1 -1
  412. package/dist/types/crypto-browser.d.ts +2 -0
  413. package/dist/types/crypto-node.d.ts +6 -0
  414. package/dist/types/react.d.ts +1 -1
  415. package/dist/types/version.d.ts +2 -2
  416. package/dist/types/webhooks.d.ts +2 -2
  417. package/dist/ui/index.d.mts +54 -0
  418. package/dist/ui/index.d.ts +54 -0
  419. package/dist/ui/index.js +105 -0
  420. package/dist/ui/index.js.map +1 -0
  421. package/dist/ui/index.mjs +44 -0
  422. package/dist/ui/index.mjs.map +1 -0
  423. package/dist/ui/lib/compose-refs.d.mts +19 -0
  424. package/dist/ui/lib/compose-refs.d.ts +19 -0
  425. package/dist/ui/lib/compose-refs.js +76 -0
  426. package/dist/ui/lib/compose-refs.js.map +1 -0
  427. package/dist/ui/lib/compose-refs.mjs +41 -0
  428. package/dist/ui/lib/compose-refs.mjs.map +1 -0
  429. package/dist/ui/lib/use-mobile.d.mts +4 -0
  430. package/dist/ui/lib/use-mobile.d.ts +4 -0
  431. package/dist/ui/lib/use-mobile.js +57 -0
  432. package/dist/ui/lib/use-mobile.js.map +1 -0
  433. package/dist/ui/lib/use-mobile.mjs +22 -0
  434. package/dist/ui/lib/use-mobile.mjs.map +1 -0
  435. package/dist/ui/lib/utils.d.mts +10 -0
  436. package/dist/ui/lib/utils.d.ts +10 -0
  437. package/dist/ui/lib/utils.js +33 -0
  438. package/dist/ui/lib/utils.js.map +1 -0
  439. package/dist/ui/lib/utils.mjs +9 -0
  440. package/dist/ui/lib/utils.mjs.map +1 -0
  441. package/dist/ui/primitives/alert-dialog.d.mts +22 -0
  442. package/dist/ui/primitives/alert-dialog.d.ts +22 -0
  443. package/dist/ui/primitives/alert-dialog.js +163 -0
  444. package/dist/ui/primitives/alert-dialog.js.map +1 -0
  445. package/dist/ui/primitives/alert-dialog.mjs +119 -0
  446. package/dist/ui/primitives/alert-dialog.mjs.map +1 -0
  447. package/dist/ui/primitives/avatar.d.mts +8 -0
  448. package/dist/ui/primitives/avatar.d.ts +8 -0
  449. package/dist/ui/primitives/avatar.js +91 -0
  450. package/dist/ui/primitives/avatar.js.map +1 -0
  451. package/dist/ui/primitives/avatar.mjs +55 -0
  452. package/dist/ui/primitives/avatar.mjs.map +1 -0
  453. package/dist/ui/primitives/breadcrumb.d.mts +13 -0
  454. package/dist/ui/primitives/breadcrumb.d.ts +13 -0
  455. package/dist/ui/primitives/breadcrumb.js +127 -0
  456. package/dist/ui/primitives/breadcrumb.js.map +1 -0
  457. package/dist/ui/primitives/breadcrumb.mjs +97 -0
  458. package/dist/ui/primitives/breadcrumb.mjs.map +1 -0
  459. package/dist/ui/primitives/button.d.mts +15 -0
  460. package/dist/ui/primitives/button.d.ts +15 -0
  461. package/dist/ui/primitives/button.js +91 -0
  462. package/dist/ui/primitives/button.js.map +1 -0
  463. package/dist/ui/primitives/button.mjs +56 -0
  464. package/dist/ui/primitives/button.mjs.map +1 -0
  465. package/dist/ui/primitives/card.d.mts +10 -0
  466. package/dist/ui/primitives/card.d.ts +10 -0
  467. package/dist/ui/primitives/card.js +101 -0
  468. package/dist/ui/primitives/card.js.map +1 -0
  469. package/dist/ui/primitives/card.mjs +62 -0
  470. package/dist/ui/primitives/card.mjs.map +1 -0
  471. package/dist/ui/primitives/carousel.d.mts +45 -0
  472. package/dist/ui/primitives/carousel.d.ts +45 -0
  473. package/dist/ui/primitives/carousel.js +300 -0
  474. package/dist/ui/primitives/carousel.js.map +1 -0
  475. package/dist/ui/primitives/carousel.mjs +261 -0
  476. package/dist/ui/primitives/carousel.mjs.map +1 -0
  477. package/dist/ui/primitives/chart.d.mts +68 -0
  478. package/dist/ui/primitives/chart.d.ts +68 -0
  479. package/dist/ui/primitives/chart.js +280 -0
  480. package/dist/ui/primitives/chart.js.map +1 -0
  481. package/dist/ui/primitives/chart.mjs +241 -0
  482. package/dist/ui/primitives/chart.mjs.map +1 -0
  483. package/dist/ui/primitives/collapsible.d.mts +8 -0
  484. package/dist/ui/primitives/collapsible.d.ts +8 -0
  485. package/dist/ui/primitives/collapsible.js +72 -0
  486. package/dist/ui/primitives/collapsible.js.map +1 -0
  487. package/dist/ui/primitives/collapsible.mjs +36 -0
  488. package/dist/ui/primitives/collapsible.mjs.map +1 -0
  489. package/dist/ui/primitives/context-menu.d.mts +29 -0
  490. package/dist/ui/primitives/context-menu.d.ts +29 -0
  491. package/dist/ui/primitives/context-menu.js +203 -0
  492. package/dist/ui/primitives/context-menu.js.map +1 -0
  493. package/dist/ui/primitives/context-menu.mjs +155 -0
  494. package/dist/ui/primitives/context-menu.mjs.map +1 -0
  495. package/dist/ui/primitives/dialog.d.mts +26 -0
  496. package/dist/ui/primitives/dialog.d.ts +26 -0
  497. package/dist/ui/primitives/dialog.js +192 -0
  498. package/dist/ui/primitives/dialog.js.map +1 -0
  499. package/dist/ui/primitives/dialog.mjs +148 -0
  500. package/dist/ui/primitives/dialog.mjs.map +1 -0
  501. package/dist/ui/primitives/drawer.d.mts +26 -0
  502. package/dist/ui/primitives/drawer.d.ts +26 -0
  503. package/dist/ui/primitives/drawer.js +146 -0
  504. package/dist/ui/primitives/drawer.js.map +1 -0
  505. package/dist/ui/primitives/drawer.mjs +103 -0
  506. package/dist/ui/primitives/drawer.mjs.map +1 -0
  507. package/dist/ui/primitives/input.d.mts +5 -0
  508. package/dist/ui/primitives/input.d.ts +5 -0
  509. package/dist/ui/primitives/input.js +46 -0
  510. package/dist/ui/primitives/input.js.map +1 -0
  511. package/dist/ui/primitives/input.mjs +22 -0
  512. package/dist/ui/primitives/input.mjs.map +1 -0
  513. package/dist/ui/primitives/item.d.mts +27 -0
  514. package/dist/ui/primitives/item.d.ts +27 -0
  515. package/dist/ui/primitives/item.js +221 -0
  516. package/dist/ui/primitives/item.js.map +1 -0
  517. package/dist/ui/primitives/item.mjs +188 -0
  518. package/dist/ui/primitives/item.mjs.map +1 -0
  519. package/dist/ui/primitives/kbd.d.mts +15 -0
  520. package/dist/ui/primitives/kbd.d.ts +15 -0
  521. package/dist/ui/primitives/kbd.js +45 -0
  522. package/dist/ui/primitives/kbd.js.map +1 -0
  523. package/dist/ui/primitives/kbd.mjs +21 -0
  524. package/dist/ui/primitives/kbd.mjs.map +1 -0
  525. package/dist/ui/primitives/label.d.mts +6 -0
  526. package/dist/ui/primitives/label.d.ts +6 -0
  527. package/dist/ui/primitives/label.js +55 -0
  528. package/dist/ui/primitives/label.js.map +1 -0
  529. package/dist/ui/primitives/label.mjs +21 -0
  530. package/dist/ui/primitives/label.mjs.map +1 -0
  531. package/dist/ui/primitives/resizable.d.mts +10 -0
  532. package/dist/ui/primitives/resizable.d.ts +10 -0
  533. package/dist/ui/primitives/resizable.js +68 -0
  534. package/dist/ui/primitives/resizable.js.map +1 -0
  535. package/dist/ui/primitives/resizable.mjs +42 -0
  536. package/dist/ui/primitives/resizable.mjs.map +1 -0
  537. package/dist/ui/primitives/save-status.d.mts +31 -0
  538. package/dist/ui/primitives/save-status.d.ts +31 -0
  539. package/dist/ui/primitives/save-status.js +150 -0
  540. package/dist/ui/primitives/save-status.js.map +1 -0
  541. package/dist/ui/primitives/save-status.mjs +126 -0
  542. package/dist/ui/primitives/save-status.mjs.map +1 -0
  543. package/dist/ui/primitives/separator.d.mts +6 -0
  544. package/dist/ui/primitives/separator.d.ts +6 -0
  545. package/dist/ui/primitives/separator.js +62 -0
  546. package/dist/ui/primitives/separator.js.map +1 -0
  547. package/dist/ui/primitives/separator.mjs +28 -0
  548. package/dist/ui/primitives/separator.mjs.map +1 -0
  549. package/dist/ui/primitives/sheet.d.mts +16 -0
  550. package/dist/ui/primitives/sheet.d.ts +16 -0
  551. package/dist/ui/primitives/sheet.js +170 -0
  552. package/dist/ui/primitives/sheet.js.map +1 -0
  553. package/dist/ui/primitives/sheet.mjs +129 -0
  554. package/dist/ui/primitives/sheet.mjs.map +1 -0
  555. package/dist/ui/primitives/sidebar.d.mts +74 -0
  556. package/dist/ui/primitives/sidebar.d.ts +74 -0
  557. package/dist/ui/primitives/sidebar.js +697 -0
  558. package/dist/ui/primitives/sidebar.js.map +1 -0
  559. package/dist/ui/primitives/sidebar.mjs +651 -0
  560. package/dist/ui/primitives/sidebar.mjs.map +1 -0
  561. package/dist/ui/primitives/skeleton.d.mts +5 -0
  562. package/dist/ui/primitives/skeleton.d.ts +5 -0
  563. package/dist/ui/primitives/skeleton.js +40 -0
  564. package/dist/ui/primitives/skeleton.js.map +1 -0
  565. package/dist/ui/primitives/skeleton.mjs +16 -0
  566. package/dist/ui/primitives/skeleton.mjs.map +1 -0
  567. package/dist/ui/primitives/slider.d.mts +20 -0
  568. package/dist/ui/primitives/slider.d.ts +20 -0
  569. package/dist/ui/primitives/slider.js +112 -0
  570. package/dist/ui/primitives/slider.js.map +1 -0
  571. package/dist/ui/primitives/slider.mjs +78 -0
  572. package/dist/ui/primitives/slider.mjs.map +1 -0
  573. package/dist/ui/primitives/sortable.d.mts +50 -0
  574. package/dist/ui/primitives/sortable.d.ts +50 -0
  575. package/dist/ui/primitives/sortable.js +456 -0
  576. package/dist/ui/primitives/sortable.js.map +1 -0
  577. package/dist/ui/primitives/sortable.mjs +435 -0
  578. package/dist/ui/primitives/sortable.mjs.map +1 -0
  579. package/dist/ui/primitives/switch.d.mts +6 -0
  580. package/dist/ui/primitives/switch.d.ts +6 -0
  581. package/dist/ui/primitives/switch.js +67 -0
  582. package/dist/ui/primitives/switch.js.map +1 -0
  583. package/dist/ui/primitives/switch.mjs +33 -0
  584. package/dist/ui/primitives/switch.mjs.map +1 -0
  585. package/dist/ui/primitives/table.d.mts +12 -0
  586. package/dist/ui/primitives/table.d.ts +12 -0
  587. package/dist/ui/primitives/table.js +130 -0
  588. package/dist/ui/primitives/table.js.map +1 -0
  589. package/dist/ui/primitives/table.mjs +89 -0
  590. package/dist/ui/primitives/table.mjs.map +1 -0
  591. package/dist/ui/primitives/tabs.d.mts +9 -0
  592. package/dist/ui/primitives/tabs.d.ts +9 -0
  593. package/dist/ui/primitives/tabs.js +86 -0
  594. package/dist/ui/primitives/tabs.js.map +1 -0
  595. package/dist/ui/primitives/tabs.mjs +49 -0
  596. package/dist/ui/primitives/tabs.mjs.map +1 -0
  597. package/dist/ui/primitives/textarea.d.mts +5 -0
  598. package/dist/ui/primitives/textarea.d.ts +5 -0
  599. package/dist/ui/primitives/textarea.js +43 -0
  600. package/dist/ui/primitives/textarea.js.map +1 -0
  601. package/dist/ui/primitives/textarea.mjs +19 -0
  602. package/dist/ui/primitives/textarea.mjs.map +1 -0
  603. package/dist/ui/primitives/tooltip.d.mts +9 -0
  604. package/dist/ui/primitives/tooltip.d.ts +9 -0
  605. package/dist/ui/primitives/tooltip.js +94 -0
  606. package/dist/ui/primitives/tooltip.js.map +1 -0
  607. package/dist/ui/primitives/tooltip.mjs +57 -0
  608. package/dist/ui/primitives/tooltip.mjs.map +1 -0
  609. package/docs/agents.md +72 -29
  610. package/docs/api/README.md +7 -3
  611. package/docs/api/appObjects.d.ts +3 -1
  612. package/docs/api/appObjects.md +2 -1
  613. package/docs/api/by-path.md +51 -0
  614. package/docs/api/dataPipelines.d.ts +149 -0
  615. package/docs/api/dataPipelines.md +232 -0
  616. package/docs/api/forms.d.ts +16 -0
  617. package/docs/api/forms.md +8 -0
  618. package/docs/api/objects.d.ts +705 -5
  619. package/docs/api/objects.md +1099 -14
  620. package/docs/api/ops.d.ts +1 -1
  621. package/docs/api/ops.md +1 -1
  622. package/docs/api/processLists.d.ts +247 -0
  623. package/docs/api/processLists.md +409 -0
  624. package/docs/api/quotes.d.ts +3 -1
  625. package/docs/api/quotes.md +7 -2
  626. package/docs/api/radar.d.ts +4 -0
  627. package/docs/api/radar.md +1 -0
  628. package/docs/api/reports.d.ts +476 -0
  629. package/docs/api/reports.md +539 -0
  630. package/docs/api/scheduleBuy.d.ts +12 -8
  631. package/docs/api/scheduleBuy.md +13 -10
  632. package/docs/api/seo.d.ts +406 -3
  633. package/docs/api/seo.md +505 -1
  634. package/docs/api/workspaceBackup.d.ts +324 -0
  635. package/docs/api/workspaceBackup.md +427 -0
  636. package/package.json +174 -5
  637. package/stacks/6sense.json +46 -0
  638. package/stacks/README.md +20 -0
  639. package/stacks/apollo.json +128 -0
  640. package/stacks/clay.json +59 -0
  641. package/stacks/gong.json +72 -0
  642. package/stacks/hubspot.json +162 -0
  643. package/stacks/intercom.json +53 -0
  644. package/stacks/mailchimp.json +71 -0
  645. package/stacks/outreach.json +86 -0
  646. package/stacks/salesforce.json +94 -0
  647. package/stacks/stack.schema.json +32 -0
  648. package/stacks/starter.json +22 -0
  649. package/styles/styles.css +2 -0
  650. package/styles/theme.css +181 -0
  651. package/styles/tokens.css +284 -0
  652. package/templates/next-app/README.md.tmpl +35 -0
  653. package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
  654. package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
  655. package/templates/next-app/app/globals.css.tmpl +8 -0
  656. package/templates/next-app/app/layout.tsx.tmpl +19 -0
  657. package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
  658. package/templates/next-app/dot-env.local.example.tmpl +6 -0
  659. package/templates/next-app/dot-gitignore.tmpl +7 -0
  660. package/templates/next-app/lib/nav.ts.tmpl +2 -0
  661. package/templates/next-app/next.config.mjs.tmpl +4 -0
  662. package/templates/next-app/package.json.tmpl +25 -0
  663. package/templates/next-app/postcss.config.mjs.tmpl +4 -0
  664. package/templates/next-app/tsconfig.json.tmpl +21 -0
@@ -0,0 +1,147 @@
1
+ "use client";
2
+
3
+ /**
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.
7
+ *
8
+ * 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)
14
+ *
15
+ * Tiers: 5 begins REAL outreach and cannot be recalled. The result is kept in state, so the button is
16
+ * gone after a success and a double click cannot launch twice. After an error, do not blindly retry: a 502
17
+ * `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.
19
+ */
20
+
21
+ import { useState } from "react";
22
+ import { Button, Skeleton } from "@graph8/sdk/ui";
23
+ import { ApiError } from "@/g8/components/api-error";
24
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
25
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
26
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
27
+ 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";
30
+
31
+ export interface CampaignLaunchStepProps {
32
+ campaignId: string;
33
+ /** Contacts on the attached list, when you know it (the wizard passes it). Shown in the confirmation. */
34
+ audienceSize?: number;
35
+ onLaunched?: (result: Record<string, unknown>) => void;
36
+ className?: string;
37
+ }
38
+
39
+ interface Mailbox { id: string | number; email?: string | null; connection_status?: string | null; is_archived?: boolean | null }
40
+
41
+ function Check({ ok, children }: { ok: boolean | undefined; children: string }) {
42
+ return (
43
+ <li className={ok ? "text-green-700" : "text-amber-800"}>
44
+ <span aria-hidden>{ok ? "Ready: " : "Missing: "}</span>{children}
45
+ </li>
46
+ );
47
+ }
48
+
49
+ export function CampaignLaunchStep({ campaignId, audienceSize, onLaunched, className }: CampaignLaunchStepProps) {
50
+ const campaign = useG8Query((signal) => g8Get(`/campaigns/${campaignId}/full`, undefined, { signal }), [campaignId]);
51
+ const mailboxes = useG8Query((signal) => g8Get("/mailboxes", { limit: 200 }, { signal }), []);
52
+ const verdict = useG8Query((signal) => g8Get("/deliverability/safe-to-send", undefined, { signal }), []);
53
+ const [chosen, setChosen] = useState<Set<string> | null>(null);
54
+ const [confirm, setConfirm] = useState(false);
55
+ const [result, setResult] = useState<Record<string, unknown> | null>(null);
56
+
57
+ const active = listOf<Mailbox>(mailboxes.data).filter((m) => m.connection_status === "active" && !m.is_archived);
58
+ const senders = chosen ?? new Set(active.slice(0, 1).map((m) => String(m.id)));
59
+ const launch = useG8Mutation(
60
+ () => g8Post(`/campaigns/${campaignId}/launch`, { sender_mailbox_ids: [...senders].map(Number) }),
61
+ {
62
+ onSuccess: (res) => { const data = objectOf(res); setResult(data); setConfirm(false); onLaunched?.(data); },
63
+ onError: () => setConfirm(false),
64
+ },
65
+ );
66
+
67
+ const loading = [campaign, mailboxes].some((q) => q.loading && !q.data);
68
+ const failedQuery = [campaign, mailboxes].find((q) => q.error);
69
+ if (failedQuery) return <ApiError error={failedQuery.error} title="Could not check launch readiness" onRetry={() => { void campaign.refetch(); void mailboxes.refetch(); }} />;
70
+ if (loading) return <Skeleton className="h-40 w-full" />;
71
+
72
+ const c = objectOf(campaign.data);
73
+ const audience = audienceOf(campaign.data);
74
+ const hasAudience = audience.listId != null;
75
+ const hasSteps = stepsOf(campaign.data).length > 0;
76
+ const alreadyLaunched = c.is_launched === true;
77
+ const recipients = audienceSize ?? audience.size;
78
+ const v = objectOf(verdict.data);
79
+ const unsafe = !verdict.error && (v.safe_to_send ?? v.safe) === false;
80
+ const ready = hasAudience && hasSteps && senders.size > 0 && !result && !alreadyLaunched;
81
+
82
+ if (result) {
83
+ const channels = (result.channels_attached as Array<{ email?: string }> | undefined) ?? [];
84
+ return (
85
+ <section aria-label="Launch result" className={`space-y-2 rounded-md border border-green-200 bg-green-50 p-4 ${className ?? ""}`}>
86
+ <h3 className="font-medium">Campaign {String(result.status ?? "launched")}</h3>
87
+ <p className="text-sm">{String(result.message ?? "")}</p>
88
+ <p className="text-sm text-muted-foreground">
89
+ Sequence {String(result.sequence_id ?? "")} with {String(result.steps_created ?? 0)} steps, sending from {channels.map((m) => m.email).join(", ") || "the chosen mailboxes"}.
90
+ </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}
92
+ </section>
93
+ );
94
+ }
95
+ return (
96
+ <section aria-label="Launch" className={`space-y-3 ${className ?? ""}`}>
97
+ <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}
99
+ <ul className="space-y-1 text-sm">
100
+ <Check ok={hasAudience}>An audience list is attached</Check>
101
+ <Check ok={hasSteps}>The sequence has at least one step</Check>
102
+ <Check ok={senders.size > 0}>At least one active sending mailbox is selected</Check>
103
+ </ul>
104
+ {unsafe ? (
105
+ <p role="alert" className="rounded bg-amber-50 p-2 text-sm text-amber-900">
106
+ Deliverability warning: {typeof v.reason === "string" ? v.reason : "sending is not recommended right now."}
107
+ </p>
108
+ ) : null}
109
+ <fieldset className="space-y-1">
110
+ <legend className="text-sm font-medium">Send from</legend>
111
+ {active.length === 0 ? (
112
+ <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.
114
+ </p>
115
+ ) : null}
116
+ {active.map((m) => (
117
+ <label key={m.id} className="flex items-center gap-2 text-sm">
118
+ <input
119
+ type="checkbox"
120
+ checked={senders.has(String(m.id))}
121
+ onChange={(e) => { const next = new Set(senders); if (e.target.checked) next.add(String(m.id)); else next.delete(String(m.id)); setChosen(next); }}
122
+ />
123
+ {m.email ?? `Mailbox ${m.id}`}
124
+ </label>
125
+ ))}
126
+ </fieldset>
127
+ {launch.error ? (
128
+ <div className="space-y-1">
129
+ <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>
131
+ </div>
132
+ ) : null}
133
+ <Button disabled={!ready} loading={launch.loading} onClick={() => setConfirm(true)}>Launch campaign</Button>
134
+ <ConfirmSpendDialog
135
+ open={confirm}
136
+ tier="external"
137
+ title={`Launch "${String(c.name ?? "this campaign")}"`}
138
+ description="This starts real outreach to the attached list and cannot be recalled."
139
+ recipients={recipients}
140
+ confirmLabel="Launch now"
141
+ confirming={launch.loading}
142
+ onConfirm={() => void launch.mutate(undefined)}
143
+ onCancel={() => setConfirm(false)}
144
+ />
145
+ </section>
146
+ );
147
+ }
@@ -0,0 +1,120 @@
1
+ "use client";
2
+
3
+ /**
4
+ * CampaignList: every campaign with its status, and the screen to open one (metrics and documents) or
5
+ * delete it. A page with no required props.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. listCampaigns GET /campaigns?page&limit (read; page-number, no total: a full page means maybe more)
9
+ * 2. deleteCampaign DELETE /campaigns/{campaign_id} (destructive, ConfirmSpendDialog)
10
+ * Opening a row renders CampaignMetrics and CampaignAssets (their own reads) beneath the table.
11
+ *
12
+ * Tiers: only 2 is confirmed. A 404 on delete means it is already gone and counts as done; the list is
13
+ * refetched afterwards and the open campaign is closed if it was the one deleted.
14
+ */
15
+
16
+ import { useState } from "react";
17
+ import { Button, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
18
+ import { ApiError } from "@/g8/components/api-error";
19
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
20
+ import { EmptyState } from "@/g8/components/empty-state";
21
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
22
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
23
+ import { g8Delete, g8Get } from "@/g8/lib/g8-client";
24
+ import { formatDate, listOf, statusClass } from "../lib/campaigns-utils";
25
+ import type { CampaignRow } from "../lib/campaigns-utils";
26
+ import { CampaignAssets } from "./campaign-assets";
27
+ import { CampaignMetrics } from "./campaign-metrics";
28
+
29
+ export interface CampaignListProps {
30
+ pageSize?: number;
31
+ /** Called when a row is opened. Without it the row opens metrics and documents below the table. */
32
+ onOpen?: (campaign: CampaignRow) => void;
33
+ /** Rendered in the empty state: usually a link to the wizard. */
34
+ emptyAction?: React.ReactNode;
35
+ className?: string;
36
+ }
37
+
38
+ const COLUMNS = 6;
39
+
40
+ export function CampaignList({ pageSize = 25, onOpen, emptyAction, className }: CampaignListProps) {
41
+ const [page, setPage] = useState(1);
42
+ const [openId, setOpenId] = useState<string | null>(null);
43
+ const [deleting, setDeleting] = useState<CampaignRow | null>(null);
44
+ const [busyId, setBusyId] = useState<string | null>(null);
45
+ const list = useG8Query((signal) => g8Get("/campaigns", { page, limit: pageSize }, { signal }), [page, pageSize]);
46
+
47
+ const remove = useG8Mutation(async (c: CampaignRow) => {
48
+ setBusyId(c.id);
49
+ try {
50
+ await g8Delete(`/campaigns/${c.id}`);
51
+ } catch (err) {
52
+ if ((err as { status?: number }).status !== 404) throw err; // already deleted counts as done
53
+ } finally {
54
+ setBusyId(null);
55
+ setDeleting(null);
56
+ }
57
+ setOpenId((id) => (id === c.id ? null : id));
58
+ }, { onSuccess: () => void list.refetch() });
59
+
60
+ const rows = listOf<CampaignRow>(list.data);
61
+ // GET /campaigns returns a bare array with no pagination block: a full page means there may be more.
62
+ const hasNext = rows.length >= pageSize;
63
+ const go = (next: number) => { setPage(next); setOpenId(null); };
64
+ const open = (c: CampaignRow) => { if (onOpen) onOpen(c); else setOpenId(c.id === openId ? null : c.id); };
65
+
66
+ return (
67
+ <div className={`space-y-4 ${className ?? ""}`}>
68
+ {list.error ? <ApiError error={list.error} title="Could not load campaigns" onRetry={() => void list.refetch()} /> : null}
69
+ {remove.error ? <ApiError error={remove.error} title="Could not delete the campaign" /> : null}
70
+ <Table>
71
+ <TableHeader>
72
+ <TableRow><TableHead>Name</TableHead><TableHead>Status</TableHead><TableHead>Category</TableHead><TableHead>Goal</TableHead><TableHead>Created</TableHead><TableHead className="w-40" /></TableRow>
73
+ </TableHeader>
74
+ <TableBody>
75
+ {list.loading && !list.data ? <TableRow><TableCell colSpan={COLUMNS}><Skeleton className="h-6 w-full" /></TableCell></TableRow> : null}
76
+ {rows.map((c) => (
77
+ <TableRow key={c.id} data-state={c.id === openId ? "selected" : undefined}>
78
+ <TableCell className="font-medium">{c.name}</TableCell>
79
+ <TableCell><span className={`rounded px-2 py-0.5 text-xs ${statusClass(c.status)}`}>{c.status}</span></TableCell>
80
+ <TableCell>{c.category}</TableCell>
81
+ <TableCell className="max-w-xs truncate text-muted-foreground">{c.goal}</TableCell>
82
+ <TableCell>{formatDate(c.created_at)}</TableCell>
83
+ <TableCell className="space-x-2 text-right">
84
+ <Button size="sm" variant="outline" aria-label={`Open ${c.name}`} onClick={() => open(c)}>{c.id === openId ? "Close" : "Open"}</Button>
85
+ <Button size="sm" variant="outline" aria-label={`Delete ${c.name}`} loading={busyId === c.id} onClick={() => setDeleting(c)}>Delete</Button>
86
+ </TableCell>
87
+ </TableRow>
88
+ ))}
89
+ </TableBody>
90
+ </Table>
91
+ {!list.loading && !list.error && rows.length === 0 ? (
92
+ <EmptyState title="No campaigns yet" description="Create your first campaign with the wizard." action={emptyAction} />
93
+ ) : null}
94
+ {page > 1 || hasNext ? (
95
+ <div className="flex items-center justify-end gap-2">
96
+ <Button size="sm" variant="outline" disabled={page <= 1} onClick={() => go(page - 1)}>Previous</Button>
97
+ <span className="text-sm text-muted-foreground">Page {page}</span>
98
+ <Button size="sm" variant="outline" disabled={!hasNext} onClick={() => go(page + 1)}>Next</Button>
99
+ </div>
100
+ ) : null}
101
+ {openId ? (
102
+ <div className="space-y-6 rounded-md border p-4" key={openId}>
103
+ <CampaignMetrics campaignId={openId} />
104
+ <CampaignAssets campaignId={openId} />
105
+ </div>
106
+ ) : null}
107
+ <ConfirmSpendDialog
108
+ open={deleting !== null}
109
+ tier="destructive"
110
+ title={`Delete "${deleting?.name ?? "this campaign"}"`}
111
+ description="The campaign and its documents are removed. Sequences already launched from it keep running in the sequencer."
112
+ affected={1}
113
+ confirmLabel="Delete campaign"
114
+ confirming={remove.loading}
115
+ onConfirm={() => deleting && void remove.mutate(deleting)}
116
+ onCancel={() => setDeleting(null)}
117
+ />
118
+ </div>
119
+ );
120
+ }
@@ -0,0 +1,93 @@
1
+ "use client";
2
+
3
+ /**
4
+ * CampaignMetrics: how a campaign is performing. A page with no required props: without `campaignId` it
5
+ * shows a campaign picker first.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. listCampaignsForPicker GET /campaigns/picker (read; only when no `campaignId`)
9
+ * 2. getCampaignMetrics GET /campaigns/{campaign_id}/metrics?days (read)
10
+ *
11
+ * Tiers: read only. The metrics payload is open-shaped, so the tiles show every numeric or text field and
12
+ * nested objects get their own small table. Numbers are shown as the API returns them.
13
+ */
14
+
15
+ import { useState } from "react";
16
+ import { Skeleton } from "@graph8/sdk/ui";
17
+ import { ApiError } from "@/g8/components/api-error";
18
+ import { EmptyState } from "@/g8/components/empty-state";
19
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
20
+ import { g8Get } from "@/g8/lib/g8-client";
21
+ import { objectOf, scalars } from "../lib/campaigns-utils";
22
+ import { CampaignSelect } from "./campaign-select";
23
+
24
+ export interface CampaignMetricsProps {
25
+ campaignId?: string;
26
+ /** Window in days (1-365). Default 30. */
27
+ days?: number;
28
+ className?: string;
29
+ }
30
+
31
+ const WINDOWS = [7, 30, 90, 365];
32
+
33
+ function Tiles({ data }: { data: Record<string, unknown> }) {
34
+ const tiles = scalars(data);
35
+ const groups = Object.entries(data).filter(([, v]) => v && typeof v === "object" && !Array.isArray(v)) as Array<[string, Record<string, unknown>]>;
36
+ return (
37
+ <div className="space-y-4">
38
+ {tiles.length > 0 ? (
39
+ <dl className="grid grid-cols-2 gap-3 sm:grid-cols-4">
40
+ {tiles.map(([k, v]) => (
41
+ <div key={k} className="rounded-md border p-3">
42
+ <dt className="text-xs capitalize text-muted-foreground">{k}</dt>
43
+ <dd className="text-xl font-semibold">{v}</dd>
44
+ </div>
45
+ ))}
46
+ </dl>
47
+ ) : null}
48
+ {groups.map(([name, group]) => (
49
+ <section key={name} aria-label={name.replace(/_/g, " ")}>
50
+ <h4 className="mb-1 text-sm font-medium capitalize">{name.replace(/_/g, " ")}</h4>
51
+ <Tiles data={group} />
52
+ </section>
53
+ ))}
54
+ </div>
55
+ );
56
+ }
57
+
58
+ function Metrics({ campaignId, days: initialDays, className }: { campaignId: string; days: number; className?: string }) {
59
+ const [days, setDays] = useState(initialDays);
60
+ const metrics = useG8Query((signal) => g8Get(`/campaigns/${campaignId}/metrics`, { days }, { signal }), [campaignId, days]);
61
+ const data = objectOf(metrics.data);
62
+ return (
63
+ <section aria-label="Campaign metrics" className={`space-y-3 ${className ?? ""}`}>
64
+ <div className="flex items-center justify-between">
65
+ <h3 className="font-medium">Metrics</h3>
66
+ <div role="group" aria-label="Window" className="flex gap-1">
67
+ {WINDOWS.map((d) => (
68
+ <button key={d} type="button" aria-pressed={d === days} className={`rounded border px-2 py-0.5 text-xs ${d === days ? "bg-accent font-medium" : ""}`} onClick={() => setDays(d)}>
69
+ {d}d
70
+ </button>
71
+ ))}
72
+ </div>
73
+ </div>
74
+ {metrics.error ? <ApiError error={metrics.error} title="Could not load metrics" onRetry={() => void metrics.refetch()} /> : null}
75
+ {metrics.loading && !metrics.data ? <Skeleton className="h-24 w-full" /> : null}
76
+ {!metrics.loading && !metrics.error && Object.keys(data).length === 0 ? (
77
+ <EmptyState title="No metrics yet" description="Numbers appear once the campaign has launched and sent its first messages." />
78
+ ) : null}
79
+ {Object.keys(data).length > 0 ? <Tiles data={data} /> : null}
80
+ </section>
81
+ );
82
+ }
83
+
84
+ export function CampaignMetrics({ campaignId, days = 30, className }: CampaignMetricsProps) {
85
+ const [picked, setPicked] = useState<string | null>(null);
86
+ const id = campaignId ?? picked;
87
+ return (
88
+ <div className={`space-y-3 ${className ?? ""}`}>
89
+ {campaignId ? null : <CampaignSelect value={picked} onChange={setPicked} />}
90
+ {id ? <Metrics key={id} campaignId={id} days={days} /> : null}
91
+ </div>
92
+ );
93
+ }
@@ -0,0 +1,52 @@
1
+ "use client";
2
+
3
+ /**
4
+ * CampaignSelect: pick one of the workspace's campaigns. Used by CampaignAssets and CampaignMetrics when they
5
+ * are rendered without a `campaignId`, so each works as a standalone page.
6
+ *
7
+ * SDK operations:
8
+ * 1. listCampaignsForPicker GET /campaigns/picker (read; free; every campaign, id and name only)
9
+ *
10
+ * Tiers: read only.
11
+ */
12
+
13
+ import { Skeleton } from "@graph8/sdk/ui";
14
+ import { ApiError } from "@/g8/components/api-error";
15
+ import { EmptyState } from "@/g8/components/empty-state";
16
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
17
+ import { g8Get } from "@/g8/lib/g8-client";
18
+ import { listOf } from "../lib/campaigns-utils";
19
+ import type { CampaignRow } from "../lib/campaigns-utils";
20
+
21
+ export interface CampaignSelectProps {
22
+ value?: string | null;
23
+ onChange: (campaignId: string | null) => void;
24
+ label?: string;
25
+ className?: string;
26
+ }
27
+
28
+ export function CampaignSelect({ value, onChange, label = "Campaign", className }: CampaignSelectProps) {
29
+ const picker = useG8Query((signal) => g8Get("/campaigns/picker", undefined, { signal }), []);
30
+ if (picker.error) return <ApiError error={picker.error} title="Could not load campaigns" onRetry={() => void picker.refetch()} />;
31
+ if (picker.loading && !picker.data) return <Skeleton className="h-9 w-64" />;
32
+ const rows = listOf<CampaignRow>(picker.data);
33
+ if (rows.length === 0) {
34
+ return <EmptyState title="No campaigns yet" description="Create one with the campaign wizard, then it appears here." />;
35
+ }
36
+ return (
37
+ <label className={`flex items-center gap-2 text-sm ${className ?? ""}`}>
38
+ <span className="font-medium">{label}</span>
39
+ <select
40
+ aria-label={label}
41
+ className="h-9 min-w-56 rounded-md border bg-background px-2"
42
+ value={value ?? ""}
43
+ onChange={(e) => onChange(e.target.value || null)}
44
+ >
45
+ <option value="">Choose a campaign</option>
46
+ {rows.map((c) => (
47
+ <option key={c.id} value={c.id}>{c.name ?? c.id}</option>
48
+ ))}
49
+ </select>
50
+ </label>
51
+ );
52
+ }
@@ -0,0 +1,158 @@
1
+ "use client";
2
+
3
+ /**
4
+ * CampaignSequenceStep: the touches a campaign will make (email, phone, SMS, social) and their copy. Add a
5
+ * step, let AI write one step's content (billable, confirmed), edit a step's name and day, or remove it.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. getCampaignSequence GET /campaigns/{campaign_id}/sequence (read)
9
+ * 2. createCampaignStep POST /campaigns/{campaign_id}/sequence/steps (external, ConfirmSpendDialog)
10
+ * 3. generateCampaignStepContent POST /campaigns/{campaign_id}/sequence/steps/{step_id}/generate (billable, ConfirmSpendDialog; replaces the step's placeholder copy)
11
+ * 4. updateCampaignStep PUT /campaigns/{campaign_id}/sequence/steps/{step_id} (external, ConfirmSpendDialog)
12
+ * 5. deleteCampaignStep DELETE /campaigns/{campaign_id}/sequence/steps/{step_id} (destructive, ConfirmSpendDialog)
13
+ *
14
+ * Tiers: every write here is `external` or stronger in the contract, so each is confirmed. Pick a mode from
15
+ * the channel -> mode matrix (email -> email; phone -> call|voicemail; sms -> sms; social -> connect_note|dm|
16
+ * comment|reply). The one-step API launch executes email only; build other channels in the sequencer block.
17
+ */
18
+
19
+ import { useEffect, useState } from "react";
20
+ import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
21
+ import { ApiError } from "@/g8/components/api-error";
22
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
23
+ import { EmptyState } from "@/g8/components/empty-state";
24
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
25
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
26
+ import { g8Delete, g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
27
+ import { CHANNEL_MODES, CREDIT_ESTIMATES, stepsOf } from "../lib/campaigns-utils";
28
+ import type { SequenceStep } from "../lib/campaigns-utils";
29
+ import { FormField } from "./form-field";
30
+
31
+ export interface CampaignSequenceStepProps {
32
+ campaignId: string;
33
+ /** Called with the step count after every change, so a wizard can gate "Launch". */
34
+ onStepsChange?: (count: number) => void;
35
+ className?: string;
36
+ }
37
+
38
+ type Action =
39
+ | { kind: "create"; body: { name: string; channel: string; mode: string; day: number } }
40
+ | { kind: "update"; step: SequenceStep; body: { name: string; day: number } }
41
+ | { kind: "generate"; step: SequenceStep }
42
+ | { kind: "delete"; step: SequenceStep };
43
+
44
+ const label = (s: SequenceStep) => s.name || s.step_id;
45
+
46
+ export function CampaignSequenceStep({ campaignId, onStepsChange, className }: CampaignSequenceStepProps) {
47
+ const sequence = useG8Query((signal) => g8Get(`/campaigns/${campaignId}/sequence`, undefined, { signal }), [campaignId]);
48
+ const [pending, setPending] = useState<Action | null>(null);
49
+ const [adding, setAdding] = useState(false);
50
+ const [editing, setEditing] = useState<SequenceStep | null>(null);
51
+ const [draft, setDraft] = useState({ name: "", channel: "email", mode: "email", day: "1" });
52
+ const [busyId, setBusyId] = useState<string | null>(null);
53
+
54
+ const run = useG8Mutation(async (a: Action) => {
55
+ const base = `/campaigns/${campaignId}/sequence/steps`;
56
+ if (a.kind !== "create") setBusyId(a.step.step_id);
57
+ try {
58
+ if (a.kind === "create") return await g8Post(base, a.body);
59
+ if (a.kind === "update") return await g8Put(`${base}/${a.step.step_id}`, a.body);
60
+ if (a.kind === "generate") return await g8Post(`${base}/${a.step.step_id}/generate`, {});
61
+ return await g8Delete(`${base}/${a.step.step_id}`);
62
+ } catch (err) {
63
+ // A step that is already gone is the outcome a delete wanted.
64
+ if (a.kind === "delete" && (err as { status?: number }).status === 404) return undefined;
65
+ throw err;
66
+ } finally {
67
+ setBusyId(null);
68
+ setPending(null);
69
+ }
70
+ }, { onSuccess: () => void sequence.refetch() });
71
+
72
+ const steps = stepsOf(sequence.data);
73
+ const stepCount = sequence.data ? steps.length : null;
74
+ useEffect(() => { if (stepCount !== null) onStepsChange?.(stepCount); }, [stepCount, onStepsChange]);
75
+ const openEdit = (s: SequenceStep) => { setDraft({ name: s.name ?? "", channel: s.channel ?? "email", mode: s.mode ?? "email", day: String(s.day ?? 1) }); setEditing(s); };
76
+ const submitDialog = () => {
77
+ const day = Number(draft.day);
78
+ const body = { name: draft.name.trim(), day: Number.isFinite(day) && day >= 0 ? day : 1 };
79
+ if (editing) setPending({ kind: "update", step: editing, body });
80
+ else setPending({ kind: "create", body: { ...body, channel: draft.channel, mode: draft.mode } });
81
+ setEditing(null);
82
+ setAdding(false);
83
+ };
84
+ const title = (a: Action | null) =>
85
+ !a ? "" : a.kind === "create" ? `Add step "${a.body.name}"` : a.kind === "update" ? `Update step "${label(a.step)}"` : a.kind === "generate" ? `Write the copy for "${label(a.step)}" with AI` : `Remove step "${label(a.step)}"`;
86
+
87
+ return (
88
+ <section aria-label="Sequence" className={`space-y-3 ${className ?? ""}`}>
89
+ <div className="flex items-center justify-between">
90
+ <h3 className="font-medium">Sequence steps</h3>
91
+ <Button size="sm" onClick={() => { setDraft({ name: "", channel: "email", mode: "email", day: "1" }); setAdding(true); }}>Add step</Button>
92
+ </div>
93
+ {sequence.error ? <ApiError error={sequence.error} title="Could not load the sequence" onRetry={() => void sequence.refetch()} /> : null}
94
+ {run.error ? <ApiError error={run.error} title="That change did not go through" /> : null}
95
+ <Table>
96
+ <TableHeader><TableRow><TableHead>Day</TableHead><TableHead>Step</TableHead><TableHead>Channel</TableHead><TableHead>Mode</TableHead><TableHead className="w-64" /></TableRow></TableHeader>
97
+ <TableBody>
98
+ {sequence.loading && !sequence.data ? <TableRow><TableCell colSpan={5}><Skeleton className="h-6 w-full" /></TableCell></TableRow> : null}
99
+ {steps.map((s) => (
100
+ <TableRow key={s.step_id}>
101
+ <TableCell>{s.day ?? ""}</TableCell>
102
+ <TableCell className="font-medium">{label(s)}</TableCell>
103
+ <TableCell>{s.channel}</TableCell>
104
+ <TableCell>{s.mode}</TableCell>
105
+ <TableCell className="space-x-2 text-right">
106
+ <Button size="sm" variant="outline" aria-label={`Write ${label(s)}`} loading={busyId === s.step_id} onClick={() => setPending({ kind: "generate", step: s })}>Write with AI</Button>
107
+ <Button size="sm" variant="outline" aria-label={`Edit ${label(s)}`} onClick={() => openEdit(s)}>Edit</Button>
108
+ <Button size="sm" variant="outline" aria-label={`Remove ${label(s)}`} onClick={() => setPending({ kind: "delete", step: s })}>Remove</Button>
109
+ </TableCell>
110
+ </TableRow>
111
+ ))}
112
+ </TableBody>
113
+ </Table>
114
+ {!sequence.loading && !sequence.error && steps.length === 0 ? <EmptyState title="No steps yet" description="Add the first touch, for example a day-1 email." /> : null}
115
+
116
+ <Dialog open={adding || editing !== null} onOpenChange={(o) => { if (!o) { setAdding(false); setEditing(null); } }}>
117
+ <DialogContent>
118
+ <DialogHeader><DialogTitle>{editing ? "Edit step" : "Add step"}</DialogTitle></DialogHeader>
119
+ <div className="space-y-3">
120
+ <FormField label="Name" htmlFor="step-name"><Input id="step-name" value={draft.name} placeholder="Day 1 email" onChange={(e) => setDraft({ ...draft, name: e.target.value })} /></FormField>
121
+ <FormField label="Day" htmlFor="step-day"><Input id="step-day" type="number" min={0} value={draft.day} onChange={(e) => setDraft({ ...draft, day: e.target.value })} /></FormField>
122
+ {!editing ? (
123
+ <div className="grid grid-cols-2 gap-3">
124
+ <FormField label="Channel" htmlFor="step-channel">
125
+ <select id="step-channel" className="h-9 rounded-md border bg-background px-2" value={draft.channel} onChange={(e) => setDraft({ ...draft, channel: e.target.value, mode: CHANNEL_MODES[e.target.value][0] })}>
126
+ {Object.keys(CHANNEL_MODES).map((c) => <option key={c} value={c}>{c}</option>)}
127
+ </select>
128
+ </FormField>
129
+ <FormField label="Mode" htmlFor="step-mode">
130
+ <select id="step-mode" className="h-9 rounded-md border bg-background px-2" value={draft.mode} onChange={(e) => setDraft({ ...draft, mode: e.target.value })}>
131
+ {(CHANNEL_MODES[draft.channel] ?? []).map((m) => <option key={m} value={m}>{m}</option>)}
132
+ </select>
133
+ </FormField>
134
+ </div>
135
+ ) : null}
136
+ </div>
137
+ <DialogFooter>
138
+ <Button variant="outline" onClick={() => { setAdding(false); setEditing(null); }}>Cancel</Button>
139
+ <Button disabled={!draft.name.trim()} onClick={submitDialog}>{editing ? "Review change" : "Review step"}</Button>
140
+ </DialogFooter>
141
+ </DialogContent>
142
+ </Dialog>
143
+
144
+ <ConfirmSpendDialog
145
+ open={pending !== null}
146
+ tier={pending?.kind === "generate" ? "billable" : pending?.kind === "delete" ? "destructive" : "external"}
147
+ title={title(pending)}
148
+ description={pending?.kind === "generate" ? "Replaces the step's current copy. Estimate only." : "Nothing is sent to contacts until you launch the campaign."}
149
+ estimatedCredits={pending?.kind === "generate" ? CREDIT_ESTIMATES.stepContent : undefined}
150
+ affected={pending?.kind === "delete" ? 1 : undefined}
151
+ confirmLabel={pending?.kind === "generate" ? "Write step" : pending?.kind === "delete" ? "Remove step" : "Save step"}
152
+ confirming={run.loading}
153
+ onConfirm={() => pending && void run.mutate(pending)}
154
+ onCancel={() => setPending(null)}
155
+ />
156
+ </section>
157
+ );
158
+ }