@graph8/sdk 0.362.0 → 0.373.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (615) hide show
  1. package/README.md +33 -10
  2. package/blocks/_foundation/README.md +78 -1
  3. package/blocks/_foundation/block.json +123 -6
  4. package/blocks/_foundation/components/api-error.tsx +9 -7
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  7. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  8. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  9. package/blocks/_foundation/components/empty-state.tsx +5 -2
  10. package/blocks/_foundation/components/list-select.tsx +112 -0
  11. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  12. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  13. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  14. package/blocks/_foundation/components/screen/index.ts +2 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  17. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  18. package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
  19. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  20. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  21. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  22. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  23. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  24. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  25. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  26. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  27. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  28. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  29. package/blocks/_foundation/lib/g8-client.ts +1 -1
  30. package/blocks/_foundation/lib/paging.ts +57 -0
  31. package/blocks/_foundation/lib/plural.ts +23 -0
  32. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  33. package/blocks/_foundation/server/connect-route.ts +15 -0
  34. package/blocks/_foundation/server/connect.ts +127 -0
  35. package/blocks/_foundation/server/local-key.ts +70 -0
  36. package/blocks/_foundation/server/proxy.ts +103 -3
  37. package/blocks/_foundation/server/route.ts +16 -4
  38. package/blocks/agents/README.md +93 -2
  39. package/blocks/agents/block.json +343 -2
  40. package/blocks/agents/components/agent-activity.tsx +112 -0
  41. package/blocks/agents/components/agent-approvals.tsx +135 -0
  42. package/blocks/agents/components/agent-builder.tsx +29 -10
  43. package/blocks/agents/components/agent-conversations.tsx +143 -0
  44. package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
  45. package/blocks/agents/components/agent-operators.tsx +164 -0
  46. package/blocks/agents/components/agent-run-detail.tsx +119 -0
  47. package/blocks/agents/components/agent-runs.tsx +19 -10
  48. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  49. package/blocks/agents/components/agent-test-history.tsx +121 -0
  50. package/blocks/agents/components/agents-screen.tsx +226 -0
  51. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  52. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  53. package/blocks/agents/components/from-template-button.tsx +65 -0
  54. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  55. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  56. package/blocks/agents/components/knowledge-form.tsx +69 -0
  57. package/blocks/agents/components/name-dialog.tsx +32 -0
  58. package/blocks/agents/components/receptionist-button.tsx +46 -0
  59. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  60. package/blocks/agents/components/skill-editor.tsx +147 -0
  61. package/blocks/agents/components/skill-library.tsx +111 -0
  62. package/blocks/agents/lib/agent-types.ts +7 -1
  63. package/blocks/agents/lib/facts.tsx +13 -0
  64. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  65. package/blocks/agents/lib/load-more.tsx +141 -0
  66. package/blocks/agents/lib/skill-utils.ts +91 -0
  67. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  68. package/blocks/analytics/README.md +13 -0
  69. package/blocks/analytics/block.json +45 -1
  70. package/blocks/analytics/components/analytics-screen.tsx +222 -0
  71. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  72. package/blocks/analytics/components/leaderboard.tsx +14 -7
  73. package/blocks/analytics/components/report-builder.tsx +38 -11
  74. package/blocks/analytics/components/report-detail.tsx +84 -0
  75. package/blocks/analytics/components/trend-chart.tsx +62 -0
  76. package/blocks/appointments/README.md +6 -0
  77. package/blocks/appointments/block.json +33 -2
  78. package/blocks/appointments/components/availability-editor.tsx +17 -5
  79. package/blocks/appointments/components/booking-page.tsx +2 -2
  80. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  81. package/blocks/appointments/components/event-type-related.tsx +4 -2
  82. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  83. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  84. package/blocks/appointments/components/meeting-list.tsx +38 -18
  85. package/blocks/appointments/components/meetings-screen.tsx +7 -4
  86. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  87. package/blocks/appointments/components/scheduling-screen.tsx +2 -1
  88. package/blocks/block.schema.json +18 -0
  89. package/blocks/block.types.ts +17 -0
  90. package/blocks/campaigns/README.md +41 -11
  91. package/blocks/campaigns/block.json +105 -42
  92. package/blocks/campaigns/components/campaign-assets.tsx +17 -6
  93. package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
  94. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  95. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  96. package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
  97. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  98. package/blocks/campaigns/components/icp-detail.tsx +163 -0
  99. package/blocks/campaigns/components/icp-editor.tsx +22 -17
  100. package/blocks/campaigns/components/persona-detail.tsx +181 -0
  101. package/blocks/campaigns/components/persona-editor.tsx +19 -11
  102. package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
  103. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  104. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  105. package/blocks/coach/README.md +10 -0
  106. package/blocks/coach/block.json +16 -1
  107. package/blocks/coach/components/call-scorecard.tsx +7 -7
  108. package/blocks/coach/components/coach-screen.tsx +222 -0
  109. package/blocks/company-intelligence/README.md +132 -0
  110. package/blocks/company-intelligence/block.json +318 -0
  111. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  112. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  113. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  114. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  115. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  116. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  117. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  118. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  119. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  120. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  121. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  122. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  123. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  124. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  125. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  126. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  127. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  128. package/blocks/copilot/README.md +13 -0
  129. package/blocks/copilot/block.json +14 -1
  130. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  131. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  132. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  133. package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
  134. package/blocks/crm/README.md +34 -0
  135. package/blocks/crm/block.json +107 -2
  136. package/blocks/crm/components/companies-screen.tsx +359 -0
  137. package/blocks/crm/components/company-detail.tsx +10 -8
  138. package/blocks/crm/components/company-table.tsx +11 -4
  139. package/blocks/crm/components/contact-detail.tsx +10 -7
  140. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  141. package/blocks/crm/components/contact-table.tsx +10 -3
  142. package/blocks/crm/components/contacts-screen.tsx +377 -0
  143. package/blocks/crm/components/dedupe-review.tsx +13 -6
  144. package/blocks/crm/components/import-wizard.tsx +7 -9
  145. package/blocks/crm/components/list-select.tsx +20 -0
  146. package/blocks/crm/components/notes-panel.tsx +4 -4
  147. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  148. package/blocks/crm/components/record-form.tsx +6 -4
  149. package/blocks/crm/components/task-list.tsx +4 -4
  150. package/blocks/crm/components/tasks-screen.tsx +399 -0
  151. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  152. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  153. package/blocks/crm/hooks/use-page-query.ts +9 -1
  154. package/blocks/crm/lib/crm-types.ts +26 -2
  155. package/blocks/crm/lib/read-lists.ts +16 -0
  156. package/blocks/crm/lib/screen-parts.tsx +105 -0
  157. package/blocks/deals/README.md +24 -6
  158. package/blocks/deals/block.json +160 -5
  159. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  160. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  161. package/blocks/deals/components/deal-detail.tsx +25 -13
  162. package/blocks/deals/components/deal-filters.tsx +65 -0
  163. package/blocks/deals/components/deal-line-items.tsx +120 -0
  164. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  165. package/blocks/deals/components/deal-related.tsx +31 -10
  166. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  167. package/blocks/deals/components/deals-screen.tsx +66 -26
  168. package/blocks/deals/components/forecast-view.tsx +4 -4
  169. package/blocks/deals/components/pipeline-board.tsx +8 -6
  170. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  171. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  172. package/blocks/deals/components/stage-editor.tsx +5 -5
  173. package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
  174. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  175. package/blocks/deals/lib/deal-extras.ts +156 -0
  176. package/blocks/dialer/README.md +45 -4
  177. package/blocks/dialer/block.json +84 -3
  178. package/blocks/dialer/components/call-contact-button.tsx +269 -0
  179. package/blocks/dialer/components/call-log-screen.tsx +208 -0
  180. package/blocks/dialer/components/call-log.tsx +23 -7
  181. package/blocks/dialer/components/call-queue.tsx +4 -4
  182. package/blocks/dialer/components/dialer-screen.tsx +361 -0
  183. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  184. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  185. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  186. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  187. package/blocks/dialer/components/session-start-form.tsx +61 -12
  188. package/blocks/dialer/components/softphone.tsx +4 -2
  189. package/blocks/enrichment/README.md +29 -7
  190. package/blocks/enrichment/block.json +49 -9
  191. package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
  192. package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
  193. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  194. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  195. package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
  196. package/blocks/enrichment/components/waterfall-config.tsx +66 -17
  197. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  198. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  199. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  200. package/blocks/forms/README.md +8 -0
  201. package/blocks/forms/block.json +139 -18
  202. package/blocks/forms/components/form-activity.tsx +71 -0
  203. package/blocks/forms/components/form-backfill.tsx +108 -0
  204. package/blocks/forms/components/form-builder.tsx +4 -2
  205. package/blocks/forms/components/form-embed.tsx +4 -2
  206. package/blocks/forms/components/form-groups.tsx +151 -0
  207. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  208. package/blocks/forms/components/form-route-history.tsx +72 -0
  209. package/blocks/forms/components/form-saved-views.tsx +81 -0
  210. package/blocks/forms/components/forms-screen.tsx +56 -19
  211. package/blocks/forms/components/more-rows.tsx +30 -0
  212. package/blocks/forms/components/submissions-export.tsx +67 -0
  213. package/blocks/forms/components/submissions-table.tsx +17 -6
  214. package/blocks/forms/lib/forms-extra.ts +67 -0
  215. package/blocks/inbox/README.md +32 -3
  216. package/blocks/inbox/block.json +29 -2
  217. package/blocks/inbox/components/inbox-list.tsx +7 -3
  218. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  219. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  220. package/blocks/inbox/components/reply-composer.tsx +65 -9
  221. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  222. package/blocks/inbox/components/thread-badge.tsx +2 -2
  223. package/blocks/inbox/components/thread-view.tsx +1 -1
  224. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  225. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  226. package/blocks/inbox/lib/inbox-types.ts +47 -1
  227. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  228. package/blocks/integrations/README.md +63 -1
  229. package/blocks/integrations/block.json +234 -15
  230. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  231. package/blocks/integrations/components/connection-health.tsx +59 -0
  232. package/blocks/integrations/components/crm-detail.tsx +119 -0
  233. package/blocks/integrations/components/crm-records.tsx +103 -0
  234. package/blocks/integrations/components/field-mappings.tsx +61 -0
  235. package/blocks/integrations/components/integration-list.tsx +13 -8
  236. package/blocks/integrations/components/integrations-screen.tsx +152 -0
  237. package/blocks/integrations/components/migration-panel.tsx +133 -0
  238. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  239. package/blocks/integrations/components/sync-status.tsx +11 -9
  240. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  241. package/blocks/integrations/components/webhook-detail.tsx +179 -0
  242. package/blocks/integrations/components/webhook-manager.tsx +43 -25
  243. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  244. package/blocks/integrations/lib/crm-utils.ts +39 -0
  245. package/blocks/integrations/lib/facts.tsx +13 -0
  246. package/blocks/integrations/lib/integrations-utils.ts +11 -1
  247. package/blocks/integrations/lib/load-more.tsx +141 -0
  248. package/blocks/intent/README.md +14 -0
  249. package/blocks/intent/block.json +27 -1
  250. package/blocks/intent/components/account-intent-score.tsx +5 -5
  251. package/blocks/intent/components/intent-feed.tsx +13 -10
  252. package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
  253. package/blocks/intent/components/intent-screen.tsx +265 -0
  254. package/blocks/intent/components/keyword-detail.tsx +124 -0
  255. package/blocks/intent/lib/intent-utils.ts +4 -4
  256. package/blocks/lifecycle/block.json +13 -8
  257. package/blocks/lifecycle/components/account-related.tsx +8 -1
  258. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  259. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  260. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  261. package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
  262. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  263. package/blocks/lists/README.md +20 -1
  264. package/blocks/lists/block.json +69 -3
  265. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  266. package/blocks/lists/components/add-to-list-button.tsx +20 -9
  267. package/blocks/lists/components/list-picker.tsx +17 -3
  268. package/blocks/lists/components/list-table.tsx +12 -6
  269. package/blocks/lists/components/lists-screen.tsx +346 -0
  270. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  271. package/blocks/lists/components/suppression-manager.tsx +33 -8
  272. package/blocks/lists/components/suppressions-screen.tsx +264 -0
  273. package/blocks/mailboxes/README.md +17 -0
  274. package/blocks/mailboxes/block.json +69 -17
  275. package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
  276. package/blocks/mailboxes/components/domain-detail.tsx +148 -0
  277. package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
  278. package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
  279. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  280. package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
  281. package/blocks/mailboxes/components/warmup-health.tsx +24 -22
  282. package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
  283. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  284. package/blocks/newsletter/README.md +12 -0
  285. package/blocks/newsletter/block.json +157 -24
  286. package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
  287. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  288. package/blocks/newsletter/components/more-rows.tsx +30 -0
  289. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  290. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  291. package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
  292. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  293. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  294. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  295. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  296. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  297. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  298. package/blocks/nurture/block.json +28 -14
  299. package/blocks/nurture/components/nurture-builder.tsx +13 -3
  300. package/blocks/nurture/components/nurture-enroll.tsx +12 -10
  301. package/blocks/nurture/components/nurture-form.tsx +27 -10
  302. package/blocks/nurture/components/nurture-screen.tsx +26 -11
  303. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  304. package/blocks/nurture/lib/nurture-types.ts +1 -1
  305. package/blocks/objects/README.md +18 -1
  306. package/blocks/objects/block.json +42 -21
  307. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  308. package/blocks/objects/components/object-not-found.tsx +3 -1
  309. package/blocks/objects/components/object-record-detail.tsx +10 -2
  310. package/blocks/objects/components/object-schema-editor.tsx +4 -2
  311. package/blocks/objects/components/object-table.tsx +13 -3
  312. package/blocks/objects/components/objects-screen.tsx +147 -0
  313. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  314. package/blocks/objects/lib/object-types.ts +1 -1
  315. package/blocks/pages/README.md +14 -0
  316. package/blocks/pages/block.json +140 -23
  317. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  318. package/blocks/pages/components/content-library.tsx +9 -4
  319. package/blocks/pages/components/more-rows.tsx +30 -0
  320. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  321. package/blocks/pages/components/page-analytics.tsx +176 -0
  322. package/blocks/pages/components/page-clone.tsx +99 -0
  323. package/blocks/pages/components/page-domains.tsx +167 -0
  324. package/blocks/pages/components/page-editor.tsx +1 -1
  325. package/blocks/pages/components/page-generation-status.tsx +48 -0
  326. package/blocks/pages/components/page-list.tsx +2 -1
  327. package/blocks/pages/components/page-tracking.tsx +137 -0
  328. package/blocks/pages/components/pages-screen.tsx +57 -16
  329. package/blocks/pages/components/seo-report.tsx +6 -1
  330. package/blocks/pages/lib/studio.ts +7 -0
  331. package/blocks/phone-numbers/README.md +74 -0
  332. package/blocks/phone-numbers/block.json +262 -0
  333. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  334. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  335. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  336. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  337. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  338. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  339. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  340. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  341. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  342. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  343. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  344. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  345. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  346. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  347. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  348. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  349. package/blocks/pickers/README.md +23 -0
  350. package/blocks/pickers/block.json +103 -0
  351. package/blocks/pickers/components/company-picker.tsx +90 -0
  352. package/blocks/pickers/components/contact-picker.tsx +131 -0
  353. package/blocks/pickers/components/member-picker.tsx +99 -0
  354. package/blocks/pickers/components/search-picker.tsx +98 -0
  355. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  356. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  357. package/blocks/pickers/lib/use-debounced.ts +13 -0
  358. package/blocks/quotes/README.md +13 -0
  359. package/blocks/quotes/block.json +65 -13
  360. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  361. package/blocks/quotes/components/product-picker.tsx +3 -1
  362. package/blocks/quotes/components/quote-builder.tsx +1 -1
  363. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  364. package/blocks/quotes/components/quote-list.tsx +5 -3
  365. package/blocks/quotes/components/quote-related.tsx +8 -4
  366. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  367. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  368. package/blocks/radar/README.md +10 -0
  369. package/blocks/radar/block.json +41 -1
  370. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  371. package/blocks/radar/components/competitor-detail.tsx +117 -0
  372. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  373. package/blocks/radar/components/radar-overview.tsx +4 -4
  374. package/blocks/radar/components/radar-report.tsx +4 -2
  375. package/blocks/radar/components/radar-screen.tsx +214 -0
  376. package/blocks/radar/components/report-detail.tsx +104 -0
  377. package/blocks/search/README.md +10 -0
  378. package/blocks/search/block.json +16 -1
  379. package/blocks/search/components/filter-builder.tsx +3 -3
  380. package/blocks/search/components/prospect-results.tsx +5 -3
  381. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  382. package/blocks/search/components/saved-searches.tsx +4 -1
  383. package/blocks/search/hooks/use-filter-options.ts +20 -6
  384. package/blocks/search/lib/search-types.ts +2 -0
  385. package/blocks/sequencer/README.md +55 -9
  386. package/blocks/sequencer/block.json +126 -19
  387. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  388. package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
  389. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  390. package/blocks/sequencer/components/sequence-builder.tsx +224 -60
  391. package/blocks/sequencer/components/sequence-detail.tsx +261 -0
  392. package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
  393. package/blocks/sequencer/components/sequence-list.tsx +34 -9
  394. package/blocks/sequencer/components/sequence-stats.tsx +11 -10
  395. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  396. package/blocks/sequencer/components/sequences-screen.tsx +162 -0
  397. package/blocks/sequencer/components/step-editor.tsx +14 -12
  398. package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
  399. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  400. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  401. package/blocks/social-ads/README.md +19 -0
  402. package/blocks/social-ads/block.json +233 -1
  403. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  404. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  405. package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
  406. package/blocks/social-ads/components/audience-sync.tsx +17 -9
  407. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  408. package/blocks/social-ads/components/campaign-detail.tsx +138 -0
  409. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  410. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  411. package/blocks/social-ads/components/creative-detail.tsx +155 -0
  412. package/blocks/social-ads/components/creative-gallery.tsx +35 -7
  413. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  414. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  415. package/blocks/social-ads/components/post-composer.tsx +7 -3
  416. package/blocks/social-ads/components/post-detail.tsx +163 -0
  417. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  418. package/blocks/social-ads/components/social-performance.tsx +80 -0
  419. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  420. package/blocks/social-ads/components/social-screen.tsx +187 -0
  421. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  422. package/blocks/surveys/README.md +91 -0
  423. package/blocks/surveys/block.json +299 -0
  424. package/blocks/surveys/components/account-nps.tsx +69 -0
  425. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  426. package/blocks/surveys/components/org-summary.tsx +82 -0
  427. package/blocks/surveys/components/sending-domains.tsx +113 -0
  428. package/blocks/surveys/components/survey-form.tsx +126 -0
  429. package/blocks/surveys/components/survey-responses.tsx +145 -0
  430. package/blocks/surveys/components/survey-results.tsx +72 -0
  431. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  432. package/blocks/surveys/components/survey-send.tsx +162 -0
  433. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  434. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  435. package/blocks/surveys/lib/surveys-types.ts +228 -0
  436. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  437. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  438. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  439. package/blocks/visitors/README.md +10 -0
  440. package/blocks/visitors/block.json +25 -1
  441. package/blocks/visitors/components/snippet-install.tsx +21 -10
  442. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  443. package/blocks/visitors/components/visitor-company-card.tsx +26 -16
  444. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  445. package/blocks/visitors/components/visitor-feed.tsx +16 -7
  446. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  447. package/blocks/webchat/README.md +10 -0
  448. package/blocks/webchat/block.json +21 -7
  449. package/blocks/webchat/components/chat-widget-config.tsx +8 -5
  450. package/blocks/webchat/components/conversation-list.tsx +8 -4
  451. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  452. package/blocks/workbench/README.md +11 -0
  453. package/blocks/workbench/block.json +48 -1
  454. package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
  455. package/blocks/workbench/components/run-progress.tsx +1 -1
  456. package/blocks/workbench/components/workbench-grid.tsx +10 -4
  457. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  458. package/blocks/workbench/components/workbench-screen.tsx +202 -0
  459. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  460. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  461. package/blocks/workflows/README.md +13 -0
  462. package/blocks/workflows/block.json +28 -9
  463. package/blocks/workflows/components/run-history.tsx +31 -9
  464. package/blocks/workflows/components/trigger-config.tsx +3 -1
  465. package/blocks/workflows/components/workflow-editor.tsx +4 -2
  466. package/blocks/workflows/components/workflow-list.tsx +13 -6
  467. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  468. package/dist/cli.js +3109 -1977
  469. package/dist/errors.d.mts +34 -0
  470. package/dist/errors.d.ts +34 -0
  471. package/dist/errors.js +46 -0
  472. package/dist/errors.js.map +1 -0
  473. package/dist/errors.mjs +21 -0
  474. package/dist/errors.mjs.map +1 -0
  475. package/dist/index.browser.js +11 -5
  476. package/dist/index.browser.js.map +1 -1
  477. package/dist/index.browser.mjs +11 -5
  478. package/dist/index.browser.mjs.map +1 -1
  479. package/dist/index.d.mts +164 -25
  480. package/dist/index.d.ts +164 -25
  481. package/dist/index.js +11 -5
  482. package/dist/index.js.map +1 -1
  483. package/dist/index.mjs +11 -5
  484. package/dist/index.mjs.map +1 -1
  485. package/dist/react.browser.js +10 -4
  486. package/dist/react.browser.js.map +1 -1
  487. package/dist/react.browser.mjs +10 -4
  488. package/dist/react.browser.mjs.map +1 -1
  489. package/dist/react.d.mts +142 -5
  490. package/dist/react.d.ts +142 -5
  491. package/dist/react.js +10 -4
  492. package/dist/react.js.map +1 -1
  493. package/dist/react.mjs +10 -4
  494. package/dist/react.mjs.map +1 -1
  495. package/dist/types/blocks/add.d.ts +11 -4
  496. package/dist/types/blocks/brand.d.ts +122 -0
  497. package/dist/types/blocks/compose.d.ts +53 -0
  498. package/dist/types/blocks/create.d.ts +27 -20
  499. package/dist/types/blocks/doctor.d.ts +6 -1
  500. package/dist/types/blocks/flows.d.ts +7 -1
  501. package/dist/types/blocks/navfile.d.ts +35 -0
  502. package/dist/types/blocks/orgBrand.d.ts +34 -0
  503. package/dist/types/blocks/paths.d.ts +4 -0
  504. package/dist/types/blocks/prefixes.d.ts +37 -0
  505. package/dist/types/blocks/registry.d.ts +17 -0
  506. package/dist/types/blocks/screens.d.ts +53 -0
  507. package/dist/types/cli.d.ts +5 -0
  508. package/dist/types/errors.d.ts +33 -0
  509. package/dist/types/http.d.ts +2 -18
  510. package/dist/types/version.d.ts +2 -2
  511. package/docs/agents.md +15 -4
  512. package/docs/api/README.md +2 -2
  513. package/docs/api/by-path.md +1 -0
  514. package/docs/api/desk.d.ts +98 -2
  515. package/docs/api/desk.md +123 -1
  516. package/docs/api/inbox.d.ts +10 -2
  517. package/docs/api/inbox.md +10 -2
  518. package/docs/api/seo.d.ts +19 -1
  519. package/docs/api/seo.md +24 -1
  520. package/package.json +16 -2
  521. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  522. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  523. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  524. package/stacks/6sense.json +55 -17
  525. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  526. package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
  527. package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
  528. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  529. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  530. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  531. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  532. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  533. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  534. package/stacks/apollo.json +147 -37
  535. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  536. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  537. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  538. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  539. package/stacks/calendly.json +64 -0
  540. package/stacks/clay/pages/companies.tsx.tmpl +31 -0
  541. package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
  542. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  543. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  544. package/stacks/clay.json +73 -27
  545. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  546. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  547. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  548. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  549. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  550. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  551. package/stacks/gong.json +98 -35
  552. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  553. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  554. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  555. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  556. package/stacks/hootsuite.json +104 -0
  557. package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
  558. package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
  559. package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
  560. package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
  561. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  562. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  563. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  564. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  565. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  566. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  567. package/stacks/hubspot.json +181 -64
  568. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  569. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  570. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  571. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  572. package/stacks/intercom.json +52 -20
  573. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  574. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  575. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  576. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  577. package/stacks/mailchimp.json +68 -34
  578. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  579. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  580. package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
  581. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  582. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  583. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  584. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  585. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  586. package/stacks/outreach.json +106 -25
  587. package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
  588. package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
  589. package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
  590. package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
  591. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  592. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  593. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  594. package/stacks/pipedrive.json +117 -0
  595. package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
  596. package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
  597. package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
  598. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  599. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  600. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  601. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  602. package/stacks/salesforce.json +85 -31
  603. package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
  604. package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
  605. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  606. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  607. package/stacks/zoominfo.json +115 -0
  608. package/styles/styles.css +1 -1
  609. package/templates/next-app/README.md.tmpl +40 -9
  610. package/templates/next-app/app/globals.css.tmpl +349 -64
  611. package/templates/next-app/app/layout.tsx.tmpl +7 -2
  612. package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
  613. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
  614. package/templates/next-app/dot-gitignore.tmpl +2 -0
  615. package/templates/next-app/package.json.tmpl +1 -1
@@ -0,0 +1,103 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SequenceStepsPanel: the steps of one sequence as records. A list with each step's channel, wait and
5
+ * progress; open one (deep link via `stepId`) for every field, how many contacts are at each state on it, and
6
+ * the editor (change the copy, delete it, add steps).
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. listSequenceSteps GET /sequences/{id}/steps (read; the steps in order)
10
+ * 2. getSequenceStats GET /sequences/{id}/stats (read; per-step contact counts)
11
+ * Editing opens SequenceBuilder, which owns addSequenceSteps (write), updateSequenceStep (external:
12
+ * confirmed) and deleteSequenceStep (destructive: confirmed).
13
+ */
14
+
15
+ import { useState } from "react";
16
+ import { Button, Skeleton } from "@graph8/sdk/ui";
17
+ import { ApiError } from "@/g8/components/api-error";
18
+ import { EmptyState } from "@/g8/components/empty-state";
19
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
20
+ import { SectionCard } from "@/g8/components/screen/section-card";
21
+ import { StatusPill } from "@/g8/components/screen/status-pill";
22
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
23
+ import { g8Get } from "@/g8/lib/g8-client";
24
+ import { fieldItems, fromApiStep, KIND_LABEL, listOf, objectOf, stepSummary } from "../lib/sequencer-utils";
25
+ import type { ApiStep } from "../lib/sequencer-utils";
26
+ import { SequenceBuilder } from "./sequence-builder";
27
+
28
+ export interface SequenceStepsPanelProps {
29
+ sequenceId: string;
30
+ /** The open step (the API step id). Controlled so a screen can deep-link it. */
31
+ stepId?: string;
32
+ onStepChange?: (stepId: string | null) => void;
33
+ /** Called after any edit so the sequence header can refresh. */
34
+ onChanged?: () => void;
35
+ className?: string;
36
+ }
37
+
38
+ interface StepStat { step_id?: string; step_order: number; total_contacts?: number; counts?: Record<string, number> }
39
+
40
+ export function SequenceStepsPanel({ sequenceId, stepId, onStepChange, onChanged, className }: SequenceStepsPanelProps) {
41
+ const [editing, setEditing] = useState(false);
42
+ const [version, setVersion] = useState(0);
43
+ const steps = useG8Query((signal) => g8Get(`/sequences/${sequenceId}/steps`, undefined, { signal }), [sequenceId, version], { key: ["sequences", "steps", sequenceId] });
44
+ const stats = useG8Query((signal) => g8Get(`/sequences/${sequenceId}/stats`, undefined, { signal }), [sequenceId, version], { key: ["sequences", "stats", sequenceId] });
45
+ const rows = listOf<ApiStep>(steps.data).sort((a, b) => a.step_order - b.step_order);
46
+ const statRows = (objectOf(stats.data).step_stats as StepStat[] | undefined) ?? [];
47
+ const statFor = (s: ApiStep) => statRows.find((x) => (x.step_id && x.step_id === s.id) || x.step_order === s.step_order);
48
+ const open = rows.find((s) => s.id === stepId);
49
+ const refresh = () => { setVersion((v) => v + 1); onChanged?.(); };
50
+
51
+ if (steps.error) return <ApiError error={steps.error} title="Could not load the steps" onRetry={() => void steps.refetch()} className={className} />;
52
+ if (steps.loading && !steps.data) return <Skeleton className={`h-40 w-full ${className ?? ""}`} />;
53
+
54
+ return (
55
+ <div className={`space-y-4 ${className ?? ""}`}>
56
+ <div className="flex items-center justify-between">
57
+ <h3 className="font-medium">Steps</h3>
58
+ <Button size="sm" variant="outline" onClick={() => setEditing((e) => !e)}>{editing ? "Done editing" : "Edit or add steps"}</Button>
59
+ </div>
60
+ {editing ? (
61
+ <SequenceBuilder sequenceId={sequenceId} onSaved={refresh} />
62
+ ) : rows.length === 0 ? (
63
+ <EmptyState title="No steps yet" description="Add the first email, LinkedIn touch or call." action={<Button onClick={() => setEditing(true)}>Add a step</Button>} />
64
+ ) : (
65
+ <ol aria-label="Steps" className="divide-y rounded-md border">
66
+ {rows.map((s) => {
67
+ const draft = fromApiStep(s);
68
+ const st = statFor(s);
69
+ return (
70
+ <li key={s.id ?? s.step_order} className={`flex items-center gap-3 px-3 py-2 text-sm ${s.id && s.id === stepId ? "bg-muted" : ""}`}>
71
+ <span className="w-6 text-muted-foreground tabular-nums">{s.step_order}</span>
72
+ <StatusPill tone="neutral">{KIND_LABEL[draft.kind]}</StatusPill>
73
+ <button type="button" className="min-w-0 flex-1 truncate text-left font-medium hover:underline" aria-label={`Open step ${s.step_order}`} onClick={() => onStepChange?.(s.id ?? null)}>
74
+ {stepSummary(draft)}
75
+ </button>
76
+ <span className="text-xs text-muted-foreground">after {draft.wait_days} d</span>
77
+ {st?.total_contacts != null ? <span className="text-xs tabular-nums">{st.total_contacts} contacts</span> : null}
78
+ </li>
79
+ );
80
+ })}
81
+ </ol>
82
+ )}
83
+ {!editing && open ? (
84
+ <SectionCard
85
+ title={`Step ${open.step_order}: ${stepSummary(fromApiStep(open))}`}
86
+ actions={<Button size="sm" variant="ghost" onClick={() => onStepChange?.(null)}>Close step</Button>}
87
+ >
88
+ <KeyValueList
89
+ columns={2}
90
+ items={[
91
+ ...fieldItems({ step_type: open.step_type, input_type: open.input_type, wait_days: fromApiStep(open).wait_days }),
92
+ ...fieldItems((open.step_data ?? {}) as Record<string, unknown>),
93
+ ]}
94
+ />
95
+ {statFor(open)?.counts ? (
96
+ <KeyValueList columns={2} items={Object.entries(statFor(open)!.counts!).map(([k, v]) => ({ label: `Contacts ${k.replace(/_/g, " ")}`, value: String(v) }))} />
97
+ ) : null}
98
+ <Button size="sm" onClick={() => setEditing(true)}>Edit this sequence's steps</Button>
99
+ </SectionCard>
100
+ ) : null}
101
+ </div>
102
+ );
103
+ }
@@ -0,0 +1,162 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SequencesScreen: the finished "Sequences" page (the Apollo / Outreach sequencer).
5
+ *
6
+ * Composes: SequenceList (status, launch, pause, resume, duplicate, archive), SequenceBuilder (create a draft,
7
+ * edit steps), SequenceStats (totals, per-step progress, per-contact pause/resume), EnrollDialog, plus the
8
+ * lists block ListPicker and the crm block ContactTable to choose who to enroll.
9
+ *
10
+ * Every record has a complete detail, beside the list or as a full page (`?view=full`): the sequence
11
+ * (SequenceDetail), each step (`?step=`) and each enrollment (`?enrollment=`).
12
+ *
13
+ * User flows:
14
+ * - Browse sequences, filter by status, see live/paused/draft counts at a glance.
15
+ * - "New sequence" opens a sheet with the builder (sender, audience list, email/LinkedIn/call steps); the new
16
+ * draft opens right away. Nothing is sent until it is launched.
17
+ * - Launch (after a preview that checks each step's copy and sender; a launch that would 400 is refused with the
18
+ * reasons), pause, resume, duplicate, archive from the list (all confirmed).
19
+ * - Open a sequence (deep link `?id=`): Results (stats, contacts, pause/resume one), Steps (edit copy), and
20
+ * "Enroll contacts": pick a list, tick contacts, confirm the recipient count.
21
+ *
22
+ * SDK operations of its own (both read):
23
+ * 1. listSequences GET /sequences?limit=100 (counts for the stat strip)
24
+ * 2. getSequence GET /sequences/{id} (header of the open sequence; also gives associated_list_id)
25
+ * Everything that reaches real people is confirmed in the composed parts.
26
+ */
27
+
28
+ import { useState } from "react";
29
+ import type { ReactNode } from "react";
30
+ import { ListOrdered, Plus } from "lucide-react";
31
+ import { Button } from "@graph8/sdk/ui";
32
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
33
+ import { ScreenLayout } from "@/g8/components/screen/screen-layout";
34
+ import { SplitView } from "@/g8/components/screen/split-view";
35
+ import type { StatItem } from "@/g8/components/screen/stat-strip";
36
+ import { Toolbar } from "@/g8/components/screen/toolbar";
37
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
38
+ import { useUrlState } from "@/g8/hooks/use-url-state";
39
+ import { g8Get } from "@/g8/lib/g8-client";
40
+ import { listOf, statusGroup } from "../lib/sequencer-utils";
41
+ import type { SequenceRow } from "../lib/sequencer-utils";
42
+ import { SequenceBuilder } from "./sequence-builder";
43
+ import { SequenceDetail } from "./sequence-detail";
44
+ import { SequenceList } from "./sequence-list";
45
+
46
+ export interface SequencesScreenProps {
47
+ /** Owner email sent with a new sequence (`user_email`). */
48
+ ownerEmail?: string;
49
+ /** Extra buttons in the toolbar. */
50
+ toolbarActions?: ReactNode;
51
+ /** Extra buttons in the open sequence's header. */
52
+ renderDetailActions?: (sequence: SequenceRow) => ReactNode;
53
+ /** Extra tabs in the open sequence's panel. */
54
+ extraDetailTabs?: { id: string; label: string; render: (sequence: SequenceRow) => ReactNode }[];
55
+ /** Open an enrolled person in your contacts screen; without it the Contacts tab has no such button. */
56
+ onOpenContact?: (contactId: number) => void;
57
+ className?: string;
58
+ }
59
+
60
+ const FILTERS = [
61
+ { value: "", label: "All statuses" },
62
+ { value: "drafted", label: "Drafts" },
63
+ { value: "live", label: "Live" },
64
+ { value: "paused", label: "Paused" },
65
+ ];
66
+ const SUMMARY_LIMIT = 100;
67
+
68
+ /** Counts by status group for the stat strip. Exported for tests. */
69
+ export function sequenceStats(rows: SequenceRow[]): StatItem[] {
70
+ const n = (g: ReturnType<typeof statusGroup>) => rows.filter((r) => statusGroup(r.status) === g).length;
71
+ const hint = rows.length >= SUMMARY_LIMIT ? `First ${SUMMARY_LIMIT} sequences` : undefined;
72
+ const live = rows.filter((r) => statusGroup(r.status) === "live");
73
+ return [
74
+ { label: "Live", value: n("live"), tone: "positive", hint },
75
+ { label: "Paused", value: n("paused"), tone: "warning", hint },
76
+ { label: "Drafts", value: n("draft"), hint },
77
+ { label: "Contacts in live", value: live.reduce((a, r) => a + (r.contact_count ?? 0), 0).toLocaleString(), hint },
78
+ ];
79
+ }
80
+
81
+ export function SequencesScreen(props: SequencesScreenProps) {
82
+ const { ownerEmail, toolbarActions, className } = props;
83
+ const [selectedId, setSelectedId] = useUrlState("id", "");
84
+ const [detailTab, setDetailTab] = useUrlState("dtab", "");
85
+ const [view, setView] = useUrlState("view", "");
86
+ const [stepId, setStepId] = useUrlState("step", "");
87
+ const [contactId, setContactId] = useUrlState("enrollment", "");
88
+ const [status, setStatus] = useUrlState("status", "");
89
+ const [creating, setCreating] = useState(false);
90
+ const [version, setVersion] = useState(0);
91
+ const [listKey, setListKey] = useState(0);
92
+ const summary = useG8Query((signal) => g8Get("/sequences", { limit: SUMMARY_LIMIT }, { signal }), [version], { key: ["sequences", "summary"] });
93
+ const stats = summary.data ? sequenceStats(listOf<SequenceRow>(summary.data)) : undefined;
94
+ const bump = () => setVersion((v) => v + 1);
95
+ const open = (id: string) => { setSelectedId(id); setDetailTab(null); setStepId(null); setContactId(null); };
96
+ const close = () => { setSelectedId(null); setDetailTab(null); setStepId(null); setContactId(null); setView(null); };
97
+ const refreshList = () => { setListKey((k) => k + 1); bump(); };
98
+
99
+ return (
100
+ <ScreenLayout
101
+ className={className}
102
+ title="Sequences"
103
+ description="Multi-channel outreach that runs on its own: build steps, enroll contacts, watch replies."
104
+ icon={<ListOrdered className="h-5 w-5" aria-hidden />}
105
+ actions={
106
+ <Button onClick={() => setCreating(true)}>
107
+ <Plus className="mr-1 h-4 w-4" aria-hidden /> New sequence
108
+ </Button>
109
+ }
110
+ stats={stats}
111
+ >
112
+ <div className="space-y-3">
113
+ <Toolbar
114
+ actions={toolbarActions}
115
+ filters={
116
+ <select aria-label="Status" className="h-9 rounded-md border bg-background px-2 text-sm" value={status} onChange={(e) => setStatus(e.target.value || null)}>
117
+ {FILTERS.map((f) => <option key={f.value} value={f.value}>{f.label}</option>)}
118
+ </select>
119
+ }
120
+ />
121
+ <SplitView
122
+ expanded={view === "full"} onExpandedChange={(e) => setView(e ? "full" : null)}
123
+ detailOpen={Boolean(selectedId)}
124
+ onCloseDetail={close}
125
+ list={
126
+ <SequenceList key={`${listKey}:${status}`} status={status || undefined} onOpen={(s) => open(s.id)} onCreate={() => setCreating(true)} onChanged={bump} />
127
+ }
128
+ detail={
129
+ selectedId ? (
130
+ <SequenceDetail
131
+ key={selectedId}
132
+ sequenceId={selectedId}
133
+ onExpand={view === "full" ? undefined : () => setView("full")} onCollapse={view === "full" ? () => setView(null) : undefined}
134
+ tab={detailTab || undefined}
135
+ onTabChange={(t) => setDetailTab(t === "details" ? null : t)}
136
+ stepId={stepId || undefined}
137
+ onStepChange={(v) => setStepId(v)}
138
+ contactId={contactId || undefined}
139
+ onContactChange={(v) => setContactId(v)}
140
+ onOpenContact={props.onOpenContact}
141
+ onClose={close}
142
+ onChanged={refreshList}
143
+ refreshKey={version}
144
+ onOpenSequence={open}
145
+ renderActions={props.renderDetailActions}
146
+ extraTabs={props.extraDetailTabs}
147
+ />
148
+ ) : null
149
+ }
150
+ />
151
+ </div>
152
+ <FormSheet open={creating} onOpenChange={setCreating} width="xl" title="New sequence" description="Saved as a draft. Nobody is contacted until you launch it.">
153
+ {creating ? (
154
+ <SequenceBuilder
155
+ ownerEmail={ownerEmail}
156
+ onSaved={(id) => { setCreating(false); refreshList(); open(id); }}
157
+ />
158
+ ) : null}
159
+ </FormSheet>
160
+ </ScreenLayout>
161
+ );
162
+ }
@@ -20,6 +20,7 @@ import { g8Get } from "@/g8/lib/g8-client";
20
20
  import { useG8Query } from "@/g8/hooks/use-g8-query";
21
21
  import { hasBrokenTokens, KIND_LABEL, LINKEDIN_ACTIONS, stepIssues } from "../lib/sequencer-utils";
22
22
  import type { DraftStep, InputType } from "../lib/sequencer-utils";
23
+ import { useBrand } from "@/g8/hooks/use-brand";
23
24
 
24
25
  export interface StepEditorProps {
25
26
  step: DraftStep;
@@ -38,6 +39,7 @@ const COPY_KEY: Record<string, string | undefined> = {
38
39
  };
39
40
 
40
41
  function LinkedinReadiness() {
42
+ const brand = useBrand();
41
43
  const conn = useG8Query(
42
44
  (signal) =>
43
45
  g8Get<{ connected?: boolean; is_token_valid?: boolean; requires_manual_login?: boolean }>(
@@ -50,10 +52,10 @@ function LinkedinReadiness() {
50
52
  const c = conn.data?.data;
51
53
  if (conn.loading || conn.error || !c) return null;
52
54
  if (c.connected && c.is_token_valid !== false && !c.requires_manual_login)
53
- return <p className="text-xs text-green-700">LinkedIn sender connected.</p>;
55
+ return <p className="text-xs text-positive-foreground">LinkedIn sender connected.</p>;
54
56
  return (
55
- <p role="alert" className="text-xs text-amber-800">
56
- LinkedIn is not ready to send. Reconnect the LinkedIn account in the graph8 app before launching.
57
+ <p role="alert" className="text-xs text-warning-foreground">
58
+ LinkedIn is not ready to send. Reconnect the LinkedIn account in the {brand.linkLabel} app before launching.
57
59
  </p>
58
60
  );
59
61
  }
@@ -70,13 +72,13 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
70
72
  const phoneOptions = Array.isArray(data.phone_options) ? (data.phone_options as string[]) : [];
71
73
 
72
74
  return (
73
- <fieldset disabled={disabled} className={`space-y-3 rounded border border-gray-200 p-3 ${className ?? ""}`} data-step-kind={step.kind}>
75
+ <fieldset disabled={disabled} className={`space-y-3 rounded border border-border p-3 ${className ?? ""}`} data-step-kind={step.kind}>
74
76
  <div className="flex items-center justify-between">
75
- <legend className="text-sm font-semibold">
77
+ <legend className="text-sm font-medium">
76
78
  Step {index}: {KIND_LABEL[step.kind]}
77
79
  </legend>
78
80
  {onRemove ? (
79
- <button type="button" className="text-xs text-red-700 hover:underline" onClick={onRemove}>
81
+ <button type="button" className="text-xs text-error-foreground hover:underline" onClick={onRemove}>
80
82
  Remove step
81
83
  </button>
82
84
  ) : null}
@@ -103,7 +105,7 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
103
105
  Copy
104
106
  <select
105
107
  aria-label="Email copy"
106
- className="ml-2 h-8 rounded border border-gray-300 px-1 text-sm"
108
+ className="ml-2 h-8 rounded border border-input px-1 text-sm"
107
109
  value={step.input_type}
108
110
  onChange={(e) => onChange({ ...step, dirty: true, input_type: e.target.value as InputType })}
109
111
  >
@@ -139,7 +141,7 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
139
141
  <>
140
142
  <select
141
143
  aria-label="LinkedIn action"
142
- className="h-8 rounded border border-gray-300 px-1 text-sm"
144
+ className="h-8 rounded border border-input px-1 text-sm"
143
145
  value={action}
144
146
  onChange={(e) => set({ linkedin_action_type: e.target.value })}
145
147
  >
@@ -158,7 +160,7 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
158
160
  onChange={(e) => set({ [copyKey]: e.target.value })}
159
161
  />
160
162
  ) : (
161
- <p className="text-xs text-gray-500">This action sends no text.</p>
163
+ <p className="text-xs text-muted-foreground">This action sends no text.</p>
162
164
  )}
163
165
  {action === "INMAIL" ? (
164
166
  <Input
@@ -174,7 +176,7 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
174
176
 
175
177
  {step.kind === "call" || step.kind === "task" ? (
176
178
  <div className="space-y-1 text-sm">
177
- <p className="text-gray-600">
179
+ <p className="text-muted-foreground">
178
180
  {step.kind === "call" ? "An automatic call attempt." : "Creates a call task for a person to work in the dialer."}
179
181
  </p>
180
182
  <div className="flex flex-wrap gap-3">
@@ -197,12 +199,12 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
197
199
  ) : null}
198
200
 
199
201
  {broken ? (
200
- <p role="alert" className="text-xs text-amber-800">
202
+ <p role="alert" className="text-xs text-warning-foreground">
201
203
  A {"{{merge token}}"} is not closed. Unclosed tokens are deleted when the email sends.
202
204
  </p>
203
205
  ) : null}
204
206
  {issues.length > 0 ? (
205
- <ul className="text-xs text-red-700" aria-label="Step problems">
207
+ <ul className="text-xs text-error-foreground" aria-label="Step problems">
206
208
  {issues.map((i) => (
207
209
  <li key={i}>{i}</li>
208
210
  ))}
@@ -10,7 +10,8 @@ export type StepKind = "email" | "linkedin" | "call" | "task";
10
10
 
11
11
  /** UI step kind -> the API `step_type`. `linkedin` is HeyReach; `task` is a manual dialer task. */
12
12
  export const STEP_TYPE: Record<StepKind, string> = { email: "EMAIL", linkedin: "HEYREACH", call: "PHONE", task: "MANUAL_DIALER" };
13
- export const KIND_LABEL: Record<StepKind, string> = { email: "Email", linkedin: "LinkedIn", call: "Call", task: "Call task" };
13
+ /** `call` is an AI voice-agent call (PHONE); `task` is a call a rep makes from the dialer (MANUAL_DIALER). */
14
+ export const KIND_LABEL: Record<StepKind, string> = { email: "Email", linkedin: "LinkedIn", call: "AI call", task: "Call task" };
14
15
  export const KINDS = Object.keys(STEP_TYPE) as StepKind[];
15
16
 
16
17
  export const LINKEDIN_ACTIONS = ["CONNECTION_REQUEST", "MESSAGE", "INMAIL", "FOLLOW", "VIEW_PROFILE", "LIKE_POST"] as const;
@@ -29,6 +30,8 @@ export interface DraftStep {
29
30
  step_data: Record<string, unknown>;
30
31
  /** True once changed since it was loaded or saved. */
31
32
  dirty?: boolean;
33
+ /** The API `step_type` a loaded step had (for example NETRION), so saving it never rewrites the provider. */
34
+ step_type?: string;
32
35
  }
33
36
 
34
37
  export interface SequenceRow {
@@ -77,17 +80,23 @@ export function fromApiStep(s: ApiStep): DraftStep {
77
80
  key: s.id ?? newKey(),
78
81
  id: s.id,
79
82
  kind: kindOf(s.step_type),
83
+ step_type: s.step_type,
80
84
  input_type: (s.input_type as InputType) ?? "MANUAL_TEMPLATE",
81
85
  wait_days: (s.time_interval ?? 0) / 86400,
82
86
  step_data: { ...(s.step_data ?? {}) },
83
87
  };
84
88
  }
85
89
 
86
- /** The request shape for create and add-steps. `order` is 1-based. */
87
- export function toApiStep(step: DraftStep, order: number) {
90
+ /**
91
+ * The request shape for create, update and add-steps. `order` is 1-based. `linkedinStepType` is the LinkedIn
92
+ * provider step for NEW LinkedIn steps (`HEYREACH`, or `NETRION`); a loaded step keeps its own `step_type`.
93
+ */
94
+ export function toApiStep(step: DraftStep, order: number, linkedinStepType?: string) {
95
+ // A loaded step keeps its own type (a NETRION step stays NETRION) as long as its kind did not change.
96
+ const own = step.step_type && kindOf(step.step_type) === step.kind ? step.step_type : undefined;
88
97
  return {
89
98
  step_order: order,
90
- step_type: STEP_TYPE[step.kind],
99
+ step_type: own ?? (step.kind === "linkedin" && linkedinStepType ? linkedinStepType : STEP_TYPE[step.kind]),
91
100
  input_type: step.input_type,
92
101
  time_interval: Math.max(0, Math.round(step.wait_days * 86400)),
93
102
  step_data: step.step_data,
@@ -133,11 +142,11 @@ export function statusGroup(status: string | null | undefined): "draft" | "live"
133
142
  }
134
143
 
135
144
  export const STATUS_CLASS: Record<ReturnType<typeof statusGroup>, string> = {
136
- draft: "bg-gray-100 text-gray-700",
137
- live: "bg-green-100 text-green-800",
138
- paused: "bg-amber-100 text-amber-800",
139
- ended: "bg-slate-100 text-slate-600",
140
- busy: "bg-blue-100 text-blue-800",
145
+ draft: "bg-muted text-foreground",
146
+ live: "bg-positive text-positive-foreground",
147
+ paused: "bg-warning text-warning-foreground",
148
+ ended: "bg-muted text-muted-foreground",
149
+ busy: "bg-info text-info-foreground",
141
150
  };
142
151
 
143
152
  export function listOf<T>(envelope: unknown): T[] {
@@ -153,7 +162,96 @@ export function objectOf(envelope: unknown): Record<string, unknown> {
153
162
  return data && typeof data === "object" && !Array.isArray(data) ? (data as Record<string, unknown>) : {};
154
163
  }
155
164
 
156
- /** Map a mailbox's provider to the sequence channel type the API takes. */
165
+ export type EmailChannelType = "GMAIL" | "INBOXKIT" | "SMTP";
166
+
167
+ export interface MailboxLike {
168
+ provider?: string | null;
169
+ /** inboxkit | manual_google | manual_microsoft | manual_smtp | opensrs | purchased (GET /mailboxes). */
170
+ source?: string | null;
171
+ }
172
+
173
+ /**
174
+ * The sequence channel type for a mailbox, decided by `source` first, as the graph8 app does: an InboxKit mailbox
175
+ * is INBOXKIT whatever its provider (a Google-backed InboxKit mailbox attached as GMAIL fails at send time),
176
+ * Google is GMAIL, SMTP is SMTP. `null` = this mailbox cannot send from a sequence (see `mailboxUnsupportedReason`).
177
+ */
178
+ export function mailboxChannelType(mailbox: MailboxLike): EmailChannelType | null {
179
+ const source = (mailbox.source ?? "").toLowerCase();
180
+ const provider = (mailbox.provider ?? "").toLowerCase();
181
+ if (source === "inboxkit") return "INBOXKIT";
182
+ if (source === "manual_microsoft" || provider === "microsoft") return null;
183
+ if (provider === "google_mailbox" || source === "manual_google") return "GMAIL";
184
+ if (provider === "smtp" || source === "manual_smtp") return "SMTP";
185
+ // An API without `source` (older graph8): fall back to the provider guess.
186
+ return source ? null : channelTypeFor(provider);
187
+ }
188
+
189
+ /** Why a mailbox cannot be picked as a sequence sender, or null when it can. */
190
+ export function mailboxUnsupportedReason(mailbox: MailboxLike): string | null {
191
+ if (mailboxChannelType(mailbox)) return null;
192
+ const microsoft = (mailbox.source ?? "").toLowerCase() === "manual_microsoft" || (mailbox.provider ?? "").toLowerCase() === "microsoft";
193
+ return microsoft
194
+ ? "Directly connected Microsoft mailboxes cannot send from sequences yet"
195
+ : "This mailbox type cannot send from sequences yet";
196
+ }
197
+
198
+ /** The senders a set of steps needs. `call` (AI) needs an AI number, `task` a dialer number. */
199
+ export function requiredSenders(kinds: Iterable<StepKind>): { email: boolean; aiPhone: boolean; dialerPhone: boolean; linkedin: boolean } {
200
+ const set = new Set(kinds);
201
+ return { email: set.has("email"), aiPhone: set.has("call"), dialerPhone: set.has("task"), linkedin: set.has("linkedin") };
202
+ }
203
+
204
+ const EMAIL_TYPES = new Set(["SMTP", "GMAIL", "INBOXKIT"]);
205
+
206
+ /**
207
+ * What stops a sequence from launching, read from GET /sequences/{id}/preview (`steps[].step_type` and
208
+ * `channels[].channel_type`): the API refuses `/run` with 400 when a step type has no sender channel. Checked
209
+ * client-side so a launch is never offered that can only fail.
210
+ */
211
+ export function senderGaps(steps: Array<{ step_type?: string | null }>, channels: Array<{ channel_type?: string | null }>): string[] {
212
+ const types = new Set(steps.map((s) => (s.step_type ?? "").toUpperCase()));
213
+ const have = new Set(channels.map((c) => (c.channel_type ?? "").toUpperCase()));
214
+ const gaps: string[] = [];
215
+ if (types.has("EMAIL") && ![...have].some((t) => EMAIL_TYPES.has(t))) gaps.push("Email steps have no mailbox to send from.");
216
+ if ((types.has("PHONE") || types.has("MANUAL_DIALER")) && !have.has("PHONE"))
217
+ gaps.push("Call steps have no phone number to call from.");
218
+ if (types.has("HEYREACH") && !have.has("LINKEDIN")) gaps.push("LinkedIn steps have no LinkedIn account to send from.");
219
+ if (types.has("NETRION") && !have.has("LINKEDIN_NETRION")) gaps.push("LinkedIn steps have no LinkedIn account to send from.");
220
+ if (types.has("SMS") && !have.has("SMS")) gaps.push("SMS steps have no number to text from.");
221
+ return gaps;
222
+ }
223
+
224
+ interface PreviewIssue {
225
+ message?: string;
226
+ severity?: string;
227
+ }
228
+
229
+ /**
230
+ * Launch check from GET /sequences/{id}/preview. The verdict lives on each step: `steps[].rendered.will_send_as_shown`
231
+ * and `steps[].rendered.content_issues` (a top-level copy is read too, for older APIs). An error-severity issue
232
+ * makes `/run` answer 400, so it blocks; anything else is a warning. A missing sender also blocks.
233
+ */
234
+ export function launchCheck(preview: Record<string, unknown>): { warnings: string[]; blockers: string[] } {
235
+ const steps = (Array.isArray(preview.steps) ? preview.steps : []) as Array<Record<string, unknown>>;
236
+ const channels = (Array.isArray(preview.channels) ? preview.channels : []) as Array<{ channel_type?: string | null }>;
237
+ const warnings: string[] = [];
238
+ const blockers: string[] = [];
239
+ const read = (verdict: Record<string, unknown> | null | undefined, where: string) => {
240
+ if (!verdict) return;
241
+ const issues = (Array.isArray(verdict.content_issues) ? verdict.content_issues : []) as PreviewIssue[];
242
+ for (const issue of issues) {
243
+ const text = `${where}${issue.message ?? "The copy will not send as shown."}`;
244
+ ((issue.severity ?? "").toLowerCase() === "error" ? blockers : warnings).push(text);
245
+ }
246
+ if (verdict.will_send_as_shown === false && issues.length === 0) warnings.push(`${where}The copy will not send as shown.`);
247
+ };
248
+ steps.forEach((step, i) => read(step.rendered as Record<string, unknown> | undefined, `Step ${Number(step.step_order ?? i + 1)}: `));
249
+ read(preview, "");
250
+ blockers.push(...senderGaps(steps as Array<{ step_type?: string | null }>, channels));
251
+ return { warnings: [...new Set(warnings)], blockers: [...new Set(blockers)] };
252
+ }
253
+
254
+ /** Map a mailbox's provider to the sequence channel type the API takes. Prefer `mailboxChannelType`, which reads `source`. */
157
255
  export function channelTypeFor(provider: string | null | undefined): "GMAIL" | "INBOXKIT" | "SMTP" {
158
256
  const p = (provider ?? "").toLowerCase();
159
257
  if (p.includes("gmail") || p.includes("google")) return "GMAIL";
@@ -166,3 +264,28 @@ export function newIdempotencyKey(): string {
166
264
  ? crypto.randomUUID()
167
265
  : `g8-${Date.now()}-${Math.random().toString(36).slice(2)}`;
168
266
  }
267
+
268
+ const HIDDEN_FIELDS = /password|secret|token|credential/i;
269
+
270
+ /** Every readable field of an open-shaped record as label/value pairs (secrets are never shown). */
271
+ export function fieldItems(obj: Record<string, unknown>): Array<{ label: string; value: string }> {
272
+ return Object.entries(obj)
273
+ .filter(([k, v]) => !HIDDEN_FIELDS.test(k) && v !== null && v !== undefined && v !== "")
274
+ .map(([k, v]) => {
275
+ const label = k.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
276
+ if (typeof v === "string" && /_at$|date$/.test(k) && !Number.isNaN(new Date(v).getTime()))
277
+ return { label, value: new Date(v).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short", timeZone: "UTC" }) + " UTC" };
278
+ if (typeof v === "boolean") return { label, value: v ? "Yes" : "No" };
279
+ if (typeof v === "object") return { label, value: JSON.stringify(v) };
280
+ return { label, value: String(v) };
281
+ });
282
+ }
283
+
284
+ /** One line that says what a step does, for lists. */
285
+ export function stepSummary(step: DraftStep): string {
286
+ const d = step.step_data;
287
+ const str = (v: unknown) => (typeof v === "string" ? v.trim() : "");
288
+ if (step.kind === "email") return str(d.subject) || str(d.instructions) || "Email";
289
+ if (step.kind === "linkedin") return str(d.linkedin_action_type).replace(/_/g, " ").toLowerCase() || "LinkedIn";
290
+ return KIND_LABEL[step.kind];
291
+ }
@@ -1,10 +1,13 @@
1
1
  /**
2
- * @title Create a draft sequence with email, LinkedIn and a call
2
+ * @title Create a draft sequence with email, LinkedIn and an AI call
3
3
  * @when Building outreach. Creating the draft is tier `external` in the contract but contacts nobody; the
4
- * LAUNCH is what reaches people (see launch-and-enroll.ts). Three rules the API only enforces at send time:
4
+ * LAUNCH is what reaches people (see launch-and-enroll.ts). Rules the API only enforces at send or launch time:
5
5
  * 1) an EMAIL step needs `input_type: "MANUAL_TEMPLATE"` or its subject/body are ignored; 2) a LinkedIn
6
6
  * (HEYREACH) step needs `linkedin_action_type` and the action's own copy key, never `body`; 3) `time_interval`
7
- * is in SECONDS. Attach the sender mailbox as a channel; LinkedIn steps also need a LinkedIn sender (app).
7
+ * is in SECONDS; 4) EVERY step type needs its own sender channel or POST /sequences/{id}/run answers 400: the
8
+ * mailbox (channel type from its `source`), a Voice AI number for PHONE (an AI voice-agent call; a rep's call is
9
+ * MANUAL_DIALER with a dialer number), a LinkedIn account for HEYREACH; 5) `associated_list_id` is required to
10
+ * launch and cannot be set later.
8
11
  */
9
12
  import { g8 } from "@graph8/sdk";
10
13
 
@@ -12,7 +15,28 @@ g8.init({ apiKey: process.env.G8_API_KEY! });
12
15
 
13
16
  const DAY = 86400;
14
17
 
15
- export async function createDraftSequence(name: string, listId: number, mailbox: { id: number; email: string }) {
18
+ /** Sequence channel type for a mailbox from GET /mailboxes: by `source` first, as the graph8 app does. */
19
+ function mailboxChannelType(m: { source?: string | null; provider?: string | null }): "INBOXKIT" | "GMAIL" | "SMTP" {
20
+ if (m.source === "inboxkit") return "INBOXKIT";
21
+ if (m.source === "manual_microsoft" || m.provider === "microsoft")
22
+ throw new Error("Directly connected Microsoft mailboxes cannot send from sequences yet: pick another mailbox");
23
+ if (m.provider === "google_mailbox" || m.source === "manual_google") return "GMAIL";
24
+ return "SMTP";
25
+ }
26
+
27
+ export async function createDraftSequence(name: string, listId: number, sender: { id: number; email: string }) {
28
+ // The senders, one per step type. Rows are open-shaped (no response schema in the contract): read loosely.
29
+ const mailboxes = (await g8.api.gtmContext.listMailboxes()).data as Array<{ id: string; email: string; source?: string; provider?: string }>;
30
+ const mailbox = mailboxes.find((m) => Number(m.id) === sender.id);
31
+ if (!mailbox) throw new Error(`Mailbox ${sender.id} not found`);
32
+ const mailboxId = sender.id;
33
+ const phones = (await g8.api.sequences.listSequencerPhoneNumbers()).data as { phone_numbers?: Array<{ id: number; phone_number: string; inbound_type?: string }> };
34
+ const aiNumber = (phones.phone_numbers ?? []).find((p) => p.inbound_type === "ai");
35
+ const linkedin = (await g8.api.sequences.listLinkedinAccounts()).data as { linkedin_accounts?: Array<{ id: number; name?: string }>; provider?: string };
36
+ const account = linkedin.linkedin_accounts?.[0];
37
+ if (!aiNumber || !account || linkedin.provider !== "HEYREACH")
38
+ throw new Error("This recipe needs a Voice AI number and a HeyReach LinkedIn account (connect them in the graph8 app)");
39
+
16
40
  const created = await g8.api.sequenceLifecycle.createSequence({
17
41
  body: {
18
42
  name,
@@ -44,7 +68,11 @@ export async function createDraftSequence(name: string, listId: number, mailbox:
44
68
  step_data: { dial_dnc: false, phone_options: ["mobile", "direct"] },
45
69
  },
46
70
  ],
47
- channels: [{ channel_id: mailbox.id, channel_value: mailbox.email, channel_type: "SMTP" }],
71
+ channels: [
72
+ { channel_id: mailboxId, channel_value: mailbox.email, channel_type: mailboxChannelType(mailbox) },
73
+ { channel_id: aiNumber.id, channel_value: aiNumber.phone_number, channel_type: "PHONE" },
74
+ { channel_id: account.id, channel_value: account.name ?? String(account.id), channel_type: "LINKEDIN" },
75
+ ],
48
76
  },
49
77
  });
50
78
  return created.data?.id;
@@ -19,9 +19,26 @@ export async function launchAndEnroll(sequenceId: string, listId: number, contac
19
19
  if ((verdict.data as { safe_to_send?: boolean } | undefined)?.safe_to_send === false)
20
20
  throw new Error("Deliverability says not safe to send right now");
21
21
 
22
+ // The copy verdict is PER STEP (`steps[].rendered`), not on the preview itself. An error-severity issue makes
23
+ // /run answer 400; anything else is a warning to show the human. Every step type also needs a sender channel.
22
24
  const preview = await g8.api.sequenceLifecycle.previewSequence({ path: { sequence_id: sequenceId } });
23
- const issues = (preview.data as { content_issues?: unknown[]; will_send_as_shown?: boolean } | undefined)?.content_issues ?? [];
24
- if (issues.length) throw new Error(`Fix the copy first: ${JSON.stringify(issues)}`);
25
+ const steps = preview.data?.steps ?? [];
26
+ const issues = steps.flatMap((s) =>
27
+ (s.rendered?.content_issues ?? []).map((i) => ({ step: s.step_order, ...(i as { severity?: string; message?: string }) })),
28
+ );
29
+ const errors = issues.filter((i) => (i.severity ?? "").toLowerCase() === "error");
30
+ if (errors.length) throw new Error(`Fix the copy first: ${errors.map((e) => `step ${e.step}: ${e.message}`).join("; ")}`);
31
+ if (steps.some((s) => s.rendered?.will_send_as_shown === false))
32
+ console.warn("Some copy will not send exactly as previewed:", JSON.stringify(issues));
33
+ const types = new Set(steps.map((s) => s.step_type.toUpperCase()));
34
+ const senders = new Set((preview.data?.channels ?? []).map((c) => (c.channel_type ?? "").toUpperCase()));
35
+ const missing = [
36
+ types.has("EMAIL") && !["SMTP", "GMAIL", "INBOXKIT"].some((t) => senders.has(t)) && "a mailbox",
37
+ (types.has("PHONE") || types.has("MANUAL_DIALER")) && !senders.has("PHONE") && "a phone number",
38
+ types.has("HEYREACH") && !senders.has("LINKEDIN") && "a LinkedIn account",
39
+ types.has("NETRION") && !senders.has("LINKEDIN_NETRION") && "a LinkedIn account",
40
+ ].filter(Boolean);
41
+ if (missing.length) throw new Error(`Attach ${missing.join(", ")} first (POST /sequences/{id}/channels): /run refuses it`);
25
42
 
26
43
  const current = await g8.api.sequences.getSequence({ path: { sequence_id: sequenceId } });
27
44
  const status = (current.data as { status?: string } | undefined)?.status;