@graph8/sdk 0.370.0 → 0.374.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 (646) hide show
  1. package/README.md +22 -3
  2. package/blocks/_foundation/README.md +61 -0
  3. package/blocks/_foundation/block.json +57 -0
  4. package/blocks/_foundation/components/auth-gate.tsx +363 -0
  5. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  6. package/blocks/_foundation/components/empty-state.tsx +5 -2
  7. package/blocks/_foundation/components/list-select.tsx +112 -0
  8. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  9. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  10. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  11. package/blocks/_foundation/components/screen/index.ts +1 -0
  12. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  13. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  14. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  15. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  16. package/blocks/_foundation/lib/paging.ts +57 -0
  17. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  18. package/blocks/_foundation/server/auth.ts +815 -0
  19. package/blocks/agents/README.md +81 -4
  20. package/blocks/agents/block.json +311 -2
  21. package/blocks/agents/components/agent-activity.tsx +112 -0
  22. package/blocks/agents/components/agent-approvals.tsx +135 -0
  23. package/blocks/agents/components/agent-builder.tsx +17 -3
  24. package/blocks/agents/components/agent-conversations.tsx +143 -0
  25. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
  26. package/blocks/agents/components/agent-operators.tsx +164 -0
  27. package/blocks/agents/components/agent-run-detail.tsx +20 -3
  28. package/blocks/agents/components/agent-runs.tsx +3 -3
  29. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  30. package/blocks/agents/components/agent-test-history.tsx +121 -0
  31. package/blocks/agents/components/agents-screen.tsx +53 -11
  32. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  33. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  34. package/blocks/agents/components/from-template-button.tsx +65 -0
  35. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  36. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  37. package/blocks/agents/components/knowledge-form.tsx +69 -0
  38. package/blocks/agents/components/name-dialog.tsx +32 -0
  39. package/blocks/agents/components/receptionist-button.tsx +46 -0
  40. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  41. package/blocks/agents/components/skill-editor.tsx +147 -0
  42. package/blocks/agents/components/skill-library.tsx +111 -0
  43. package/blocks/agents/lib/agent-types.ts +7 -1
  44. package/blocks/agents/lib/facts.tsx +13 -0
  45. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  46. package/blocks/agents/lib/load-more.tsx +141 -0
  47. package/blocks/agents/lib/skill-utils.ts +91 -0
  48. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +16 -3
  50. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  51. package/blocks/analytics/components/report-builder.tsx +15 -2
  52. package/blocks/appointments/README.md +6 -0
  53. package/blocks/appointments/block.json +27 -0
  54. package/blocks/appointments/components/booking-page.tsx +2 -2
  55. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  56. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  57. package/blocks/appointments/components/meeting-list.tsx +29 -13
  58. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  59. package/blocks/campaigns/README.md +1 -1
  60. package/blocks/campaigns/block.json +34 -1
  61. package/blocks/campaigns/components/campaign-assets.tsx +1 -1
  62. package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
  63. package/blocks/campaigns/components/campaign-links.tsx +180 -0
  64. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  65. package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
  66. package/blocks/campaigns/components/icp-detail.tsx +11 -2
  67. package/blocks/campaigns/components/icp-editor.tsx +8 -3
  68. package/blocks/campaigns/components/persona-detail.tsx +11 -2
  69. package/blocks/campaigns/components/persona-editor.tsx +6 -1
  70. package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
  71. package/blocks/coach/components/call-scorecard.tsx +7 -7
  72. package/blocks/company-intelligence/README.md +132 -0
  73. package/blocks/company-intelligence/block.json +318 -0
  74. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  75. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  76. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  77. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  78. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  79. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  80. package/blocks/company-intelligence/components/research-run.tsx +165 -0
  81. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  82. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  83. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  84. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  85. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  86. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  87. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  88. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  89. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  90. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  91. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  92. package/blocks/copilot/components/copilot-panel.tsx +1 -1
  93. package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
  94. package/blocks/crm/README.md +25 -0
  95. package/blocks/crm/block.json +257 -9
  96. package/blocks/crm/components/bulk-update.tsx +119 -0
  97. package/blocks/crm/components/companies-screen.tsx +23 -9
  98. package/blocks/crm/components/company-detail.tsx +2 -2
  99. package/blocks/crm/components/contact-detail.tsx +4 -4
  100. package/blocks/crm/components/contact-documents-tab.tsx +144 -0
  101. package/blocks/crm/components/contact-insights-tab.tsx +118 -0
  102. package/blocks/crm/components/contact-lookup-link.tsx +85 -0
  103. package/blocks/crm/components/contact-row-actions.tsx +43 -0
  104. package/blocks/crm/components/contact-tag-chips.tsx +113 -0
  105. package/blocks/crm/components/contacts-screen.tsx +77 -18
  106. package/blocks/crm/components/dedupe-review.tsx +6 -1
  107. package/blocks/crm/components/import-wizard.tsx +3 -7
  108. package/blocks/crm/components/list-select.tsx +20 -0
  109. package/blocks/crm/components/owner-assign.tsx +132 -0
  110. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  111. package/blocks/crm/components/record-form.tsx +1 -1
  112. package/blocks/crm/components/record-open-links.tsx +79 -0
  113. package/blocks/crm/components/tag-assign.tsx +112 -0
  114. package/blocks/crm/components/tag-manager.tsx +178 -0
  115. package/blocks/crm/components/tasks-screen.tsx +35 -15
  116. package/blocks/crm/components/unlock-info-button.tsx +82 -0
  117. package/blocks/crm/components/verify-email-button.tsx +93 -0
  118. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  119. package/blocks/crm/hooks/use-contact-tags.ts +29 -0
  120. package/blocks/crm/hooks/use-page-query.ts +3 -1
  121. package/blocks/crm/lib/crm-types.ts +14 -0
  122. package/blocks/crm/lib/read-lists.ts +16 -0
  123. package/blocks/crm/lib/record-actions.ts +102 -0
  124. package/blocks/deals/README.md +25 -6
  125. package/blocks/deals/block.json +168 -2
  126. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  127. package/blocks/deals/components/create-deal-button.tsx +109 -0
  128. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  129. package/blocks/deals/components/deal-detail.tsx +7 -5
  130. package/blocks/deals/components/deal-filters.tsx +65 -0
  131. package/blocks/deals/components/deal-line-items.tsx +120 -0
  132. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  133. package/blocks/deals/components/deal-related.tsx +30 -9
  134. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  135. package/blocks/deals/components/deals-screen.tsx +57 -22
  136. package/blocks/deals/components/forecast-view.tsx +4 -4
  137. package/blocks/deals/components/pipeline-board.tsx +8 -6
  138. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  139. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  140. package/blocks/deals/components/stage-editor.tsx +1 -1
  141. package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
  142. package/blocks/deals/lib/deal-extras.ts +156 -0
  143. package/blocks/dialer/README.md +14 -5
  144. package/blocks/dialer/block.json +17 -2
  145. package/blocks/dialer/components/call-contact-button.tsx +26 -9
  146. package/blocks/dialer/components/call-log-screen.tsx +10 -2
  147. package/blocks/dialer/components/call-log.tsx +16 -4
  148. package/blocks/dialer/components/call-queue.tsx +3 -3
  149. package/blocks/dialer/components/dialer-screen.tsx +69 -27
  150. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  151. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  152. package/blocks/dialer/components/session-start-form.tsx +58 -11
  153. package/blocks/dialer/components/softphone.tsx +1 -1
  154. package/blocks/documents/README.md +86 -0
  155. package/blocks/documents/block.json +378 -0
  156. package/blocks/documents/components/account-bar.tsx +48 -0
  157. package/blocks/documents/components/correspondence-form.tsx +95 -0
  158. package/blocks/documents/components/correspondence-import.tsx +106 -0
  159. package/blocks/documents/components/correspondence-panel.tsx +191 -0
  160. package/blocks/documents/components/documents-screen.tsx +76 -0
  161. package/blocks/documents/components/library-document-detail.tsx +175 -0
  162. package/blocks/documents/components/library-document-form.tsx +105 -0
  163. package/blocks/documents/components/library-folder-form.tsx +74 -0
  164. package/blocks/documents/components/library-panel.tsx +235 -0
  165. package/blocks/documents/components/library-upload.tsx +160 -0
  166. package/blocks/documents/components/question-row.tsx +130 -0
  167. package/blocks/documents/components/questionnaire-detail.tsx +197 -0
  168. package/blocks/documents/components/questionnaires-panel.tsx +173 -0
  169. package/blocks/documents/components/record-documents-tab.tsx +109 -0
  170. package/blocks/documents/lib/documents-types.ts +252 -0
  171. package/blocks/documents/lib/download.ts +64 -0
  172. package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
  173. package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
  174. package/blocks/documents/recipes/organize-the-library.ts +25 -0
  175. package/blocks/email-signatures/README.md +123 -0
  176. package/blocks/email-signatures/block.json +341 -0
  177. package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
  178. package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
  179. package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
  180. package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
  181. package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
  182. package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
  183. package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
  184. package/blocks/email-signatures/components/signature-policy.tsx +130 -0
  185. package/blocks/email-signatures/components/signature-preview.tsx +119 -0
  186. package/blocks/email-signatures/components/signature-report.tsx +77 -0
  187. package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
  188. package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
  189. package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
  190. package/blocks/email-signatures/lib/signatures-types.ts +392 -0
  191. package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
  192. package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
  193. package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
  194. package/blocks/enrichment/README.md +38 -10
  195. package/blocks/enrichment/block.json +164 -12
  196. package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
  197. package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
  198. package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
  199. package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
  200. package/blocks/enrichment/components/enrich-button.tsx +1 -1
  201. package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
  202. package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
  203. package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
  204. package/blocks/enrichment/components/list-pipelines.tsx +97 -0
  205. package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
  206. package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
  207. package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
  208. package/blocks/enrichment/components/waterfall-config.tsx +27 -7
  209. package/blocks/enrichment/lib/enrichment-types.ts +51 -0
  210. package/blocks/enrichment/lib/pipeline-types.ts +234 -0
  211. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  212. package/blocks/forms/README.md +10 -0
  213. package/blocks/forms/block.json +107 -0
  214. package/blocks/forms/components/form-activity.tsx +71 -0
  215. package/blocks/forms/components/form-backfill.tsx +108 -0
  216. package/blocks/forms/components/form-builder.tsx +1 -1
  217. package/blocks/forms/components/form-embed.tsx +1 -1
  218. package/blocks/forms/components/form-groups.tsx +151 -0
  219. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  220. package/blocks/forms/components/form-route-history.tsx +72 -0
  221. package/blocks/forms/components/form-saved-views.tsx +81 -0
  222. package/blocks/forms/components/forms-screen.tsx +57 -20
  223. package/blocks/forms/components/more-rows.tsx +30 -0
  224. package/blocks/forms/components/submissions-export.tsx +67 -0
  225. package/blocks/forms/components/submissions-table.tsx +15 -6
  226. package/blocks/forms/lib/forms-extra.ts +67 -0
  227. package/blocks/inbox/README.md +45 -4
  228. package/blocks/inbox/block.json +145 -8
  229. package/blocks/inbox/components/blocked-senders.tsx +103 -0
  230. package/blocks/inbox/components/compose-message.tsx +193 -0
  231. package/blocks/inbox/components/inbox-screen.tsx +54 -6
  232. package/blocks/inbox/components/inbox-search.tsx +106 -0
  233. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  234. package/blocks/inbox/components/tag-manager.tsx +136 -0
  235. package/blocks/inbox/components/thread-actions.tsx +133 -0
  236. package/blocks/inbox/components/thread-badge.tsx +2 -2
  237. package/blocks/inbox/components/thread-view.tsx +3 -1
  238. package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
  239. package/blocks/inbox/lib/inbox-manage.ts +227 -0
  240. package/blocks/integrations/README.md +54 -1
  241. package/blocks/integrations/block.json +189 -15
  242. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  243. package/blocks/integrations/components/connection-health.tsx +59 -0
  244. package/blocks/integrations/components/crm-detail.tsx +20 -1
  245. package/blocks/integrations/components/crm-records.tsx +103 -0
  246. package/blocks/integrations/components/field-mappings.tsx +61 -0
  247. package/blocks/integrations/components/integration-list.tsx +4 -4
  248. package/blocks/integrations/components/integrations-screen.tsx +20 -3
  249. package/blocks/integrations/components/migration-panel.tsx +133 -0
  250. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  251. package/blocks/integrations/components/sync-status.tsx +6 -6
  252. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  253. package/blocks/integrations/components/webhook-detail.tsx +26 -7
  254. package/blocks/integrations/components/webhook-manager.tsx +30 -17
  255. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  256. package/blocks/integrations/lib/crm-utils.ts +39 -0
  257. package/blocks/integrations/lib/facts.tsx +13 -0
  258. package/blocks/integrations/lib/integrations-utils.ts +5 -0
  259. package/blocks/integrations/lib/load-more.tsx +141 -0
  260. package/blocks/intent/components/account-intent-score.tsx +5 -5
  261. package/blocks/intent/components/intent-feed.tsx +8 -8
  262. package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
  263. package/blocks/intent/lib/intent-utils.ts +4 -4
  264. package/blocks/lifecycle/components/account-related.tsx +8 -1
  265. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  266. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  267. package/blocks/lists/README.md +7 -1
  268. package/blocks/lists/block.json +31 -3
  269. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  270. package/blocks/lists/components/add-to-list-button.tsx +17 -7
  271. package/blocks/lists/components/list-picker.tsx +13 -3
  272. package/blocks/lists/components/list-table.tsx +1 -1
  273. package/blocks/lists/components/lists-screen.tsx +4 -2
  274. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  275. package/blocks/lists/components/suppression-manager.tsx +27 -6
  276. package/blocks/lists/components/suppressions-screen.tsx +21 -12
  277. package/blocks/mailboxes/block.json +4 -0
  278. package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
  279. package/blocks/mailboxes/components/domain-detail.tsx +10 -4
  280. package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
  281. package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
  282. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  283. package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
  284. package/blocks/mailboxes/components/warmup-health.tsx +20 -20
  285. package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
  286. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  287. package/blocks/newsletter/README.md +12 -0
  288. package/blocks/newsletter/block.json +117 -0
  289. package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
  290. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  291. package/blocks/newsletter/components/more-rows.tsx +30 -0
  292. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  293. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  294. package/blocks/newsletter/components/newsletter-screen.tsx +38 -13
  295. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  296. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  297. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  298. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  299. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  300. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  301. package/blocks/nurture/components/nurture-builder.tsx +10 -2
  302. package/blocks/nurture/components/nurture-enroll.tsx +6 -7
  303. package/blocks/nurture/components/nurture-form.tsx +5 -8
  304. package/blocks/nurture/components/nurture-screen.tsx +16 -5
  305. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  306. package/blocks/objects/README.md +1 -1
  307. package/blocks/objects/block.json +13 -5
  308. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  309. package/blocks/objects/components/object-record-detail.tsx +8 -1
  310. package/blocks/objects/components/object-schema-editor.tsx +1 -1
  311. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  312. package/blocks/pages/README.md +14 -0
  313. package/blocks/pages/block.json +123 -2
  314. package/blocks/pages/components/more-rows.tsx +30 -0
  315. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  316. package/blocks/pages/components/page-analytics.tsx +176 -0
  317. package/blocks/pages/components/page-clone.tsx +99 -0
  318. package/blocks/pages/components/page-domains.tsx +167 -0
  319. package/blocks/pages/components/page-editor.tsx +1 -1
  320. package/blocks/pages/components/page-generation-status.tsx +48 -0
  321. package/blocks/pages/components/page-tracking.tsx +137 -0
  322. package/blocks/pages/components/pages-screen.tsx +57 -16
  323. package/blocks/pages/components/seo-report.tsx +6 -1
  324. package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
  325. package/blocks/pages/lib/studio.ts +7 -0
  326. package/blocks/phone-numbers/README.md +74 -0
  327. package/blocks/phone-numbers/block.json +262 -0
  328. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  329. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  330. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  331. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  332. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  333. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  334. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  335. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  336. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  337. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  338. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  339. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  340. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  341. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  342. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  343. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  344. package/blocks/pickers/README.md +23 -0
  345. package/blocks/pickers/block.json +103 -0
  346. package/blocks/pickers/components/company-picker.tsx +90 -0
  347. package/blocks/pickers/components/contact-picker.tsx +131 -0
  348. package/blocks/pickers/components/member-picker.tsx +99 -0
  349. package/blocks/pickers/components/search-picker.tsx +98 -0
  350. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  351. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  352. package/blocks/pickers/lib/use-debounced.ts +13 -0
  353. package/blocks/programmatic-pages/README.md +82 -0
  354. package/blocks/programmatic-pages/block.json +292 -0
  355. package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
  356. package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
  357. package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
  358. package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
  359. package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
  360. package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
  361. package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
  362. package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
  363. package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
  364. package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
  365. package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
  366. package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
  367. package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
  368. package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
  369. package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
  370. package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
  371. package/blocks/quotes/README.md +38 -1
  372. package/blocks/quotes/block.json +170 -6
  373. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  374. package/blocks/quotes/components/manual-products.tsx +144 -0
  375. package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
  376. package/blocks/quotes/components/quote-builder.tsx +1 -1
  377. package/blocks/quotes/components/quote-documents.tsx +94 -0
  378. package/blocks/quotes/components/quote-evidence.tsx +95 -0
  379. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  380. package/blocks/quotes/components/quote-list.tsx +28 -8
  381. package/blocks/quotes/components/quote-related.tsx +3 -2
  382. package/blocks/quotes/components/quote-settings.tsx +184 -0
  383. package/blocks/quotes/components/quote-templates.tsx +189 -0
  384. package/blocks/quotes/components/quote-viewer.tsx +23 -5
  385. package/blocks/quotes/components/quotes-screen.tsx +15 -5
  386. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  387. package/blocks/quotes/lib/quote-admin.ts +264 -0
  388. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  389. package/blocks/radar/components/radar-overview.tsx +4 -4
  390. package/blocks/radar/components/radar-screen.tsx +2 -2
  391. package/blocks/sequencer/README.md +61 -0
  392. package/blocks/sequencer/block.json +200 -6
  393. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  394. package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
  395. package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
  396. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  397. package/blocks/sequencer/components/sequence-builder.tsx +32 -23
  398. package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
  399. package/blocks/sequencer/components/sequence-detail.tsx +10 -2
  400. package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
  401. package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
  402. package/blocks/sequencer/components/sequence-list.tsx +1 -1
  403. package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
  404. package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
  405. package/blocks/sequencer/components/sequence-stats.tsx +8 -8
  406. package/blocks/sequencer/components/sequence-templates.tsx +160 -0
  407. package/blocks/sequencer/components/sequences-screen.tsx +39 -6
  408. package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
  409. package/blocks/sequencer/components/step-editor.tsx +21 -12
  410. package/blocks/sequencer/components/step-templates.tsx +224 -0
  411. package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
  412. package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
  413. package/blocks/signals/README.md +39 -0
  414. package/blocks/signals/block.json +287 -0
  415. package/blocks/signals/components/auto-seed-panel.tsx +111 -0
  416. package/blocks/signals/components/listener-form.tsx +184 -0
  417. package/blocks/signals/components/matched-posts.tsx +195 -0
  418. package/blocks/signals/components/post-engagements.tsx +114 -0
  419. package/blocks/signals/components/record-social-tab.tsx +49 -0
  420. package/blocks/signals/components/social-listeners-screen.tsx +250 -0
  421. package/blocks/signals/components/social-settings.tsx +128 -0
  422. package/blocks/signals/components/source-lists.tsx +83 -0
  423. package/blocks/signals/components/top-engagers.tsx +68 -0
  424. package/blocks/signals/components/x-integration.tsx +129 -0
  425. package/blocks/signals/lib/signals-types.ts +228 -0
  426. package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
  427. package/blocks/signals/recipes/spend-against-caps.ts +14 -0
  428. package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
  429. package/blocks/social-ads/README.md +8 -0
  430. package/blocks/social-ads/block.json +137 -4
  431. package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
  432. package/blocks/social-ads/components/audience-sync.tsx +6 -2
  433. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  434. package/blocks/social-ads/components/campaign-detail.tsx +6 -1
  435. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  436. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  437. package/blocks/social-ads/components/creative-detail.tsx +6 -1
  438. package/blocks/social-ads/components/creative-gallery.tsx +20 -2
  439. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  440. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  441. package/blocks/social-ads/components/post-composer.tsx +4 -2
  442. package/blocks/social-ads/components/post-detail.tsx +3 -0
  443. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  444. package/blocks/social-ads/components/social-performance.tsx +80 -0
  445. package/blocks/social-ads/components/social-screen.tsx +33 -6
  446. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  447. package/blocks/surveys/README.md +91 -0
  448. package/blocks/surveys/block.json +299 -0
  449. package/blocks/surveys/components/account-nps.tsx +69 -0
  450. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  451. package/blocks/surveys/components/org-summary.tsx +82 -0
  452. package/blocks/surveys/components/sending-domains.tsx +113 -0
  453. package/blocks/surveys/components/survey-form.tsx +126 -0
  454. package/blocks/surveys/components/survey-responses.tsx +145 -0
  455. package/blocks/surveys/components/survey-results.tsx +72 -0
  456. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  457. package/blocks/surveys/components/survey-send.tsx +162 -0
  458. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  459. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  460. package/blocks/surveys/lib/surveys-types.ts +228 -0
  461. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  462. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  463. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  464. package/blocks/visitors/components/snippet-install.tsx +18 -9
  465. package/blocks/visitors/components/traffic-chart.tsx +7 -6
  466. package/blocks/visitors/components/visitor-company-card.tsx +24 -14
  467. package/blocks/visitors/components/visitor-feed.tsx +5 -5
  468. package/blocks/web-analytics/README.md +102 -0
  469. package/blocks/web-analytics/block.json +292 -0
  470. package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
  471. package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
  472. package/blocks/web-analytics/components/audience-tab.tsx +135 -0
  473. package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
  474. package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
  475. package/blocks/web-analytics/components/overview-tab.tsx +102 -0
  476. package/blocks/web-analytics/components/pages-tab.tsx +148 -0
  477. package/blocks/web-analytics/components/ranked-table.tsx +103 -0
  478. package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
  479. package/blocks/web-analytics/components/report-card.tsx +71 -0
  480. package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
  481. package/blocks/web-analytics/hooks/use-report.ts +48 -0
  482. package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
  483. package/blocks/web-analytics/lib/page-path.ts +14 -0
  484. package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
  485. package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
  486. package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
  487. package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
  488. package/blocks/webchat/components/chat-widget-config.tsx +1 -1
  489. package/blocks/webchat/components/conversation-list.tsx +1 -1
  490. package/blocks/workbench/README.md +54 -2
  491. package/blocks/workbench/block.json +234 -5
  492. package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
  493. package/blocks/workbench/components/run-progress.tsx +1 -1
  494. package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
  495. package/blocks/workbench/components/workbench-automation.tsx +36 -0
  496. package/blocks/workbench/components/workbench-connections.tsx +136 -0
  497. package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
  498. package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
  499. package/blocks/workbench/components/workbench-grid.tsx +71 -8
  500. package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
  501. package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
  502. package/blocks/workbench/components/workbench-schedule.tsx +191 -0
  503. package/blocks/workbench/components/workbench-screen.tsx +65 -22
  504. package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
  505. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  506. package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
  507. package/blocks/workbench/components/workbench-triggers.tsx +178 -0
  508. package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
  509. package/blocks/workbench/lib/api-status.ts +8 -0
  510. package/blocks/workbench/lib/automation-types.ts +257 -0
  511. package/blocks/workflows/components/workflow-editor.tsx +1 -1
  512. package/dist/cli.js +744 -394
  513. package/dist/index.browser.js +1 -1
  514. package/dist/index.browser.js.map +1 -1
  515. package/dist/index.browser.mjs +1 -1
  516. package/dist/index.browser.mjs.map +1 -1
  517. package/dist/index.d.mts +2 -2
  518. package/dist/index.d.ts +2 -2
  519. package/dist/index.js +1 -1
  520. package/dist/index.js.map +1 -1
  521. package/dist/index.mjs +1 -1
  522. package/dist/index.mjs.map +1 -1
  523. package/dist/react.browser.js +1 -1
  524. package/dist/react.browser.js.map +1 -1
  525. package/dist/react.browser.mjs +1 -1
  526. package/dist/react.browser.mjs.map +1 -1
  527. package/dist/react.d.mts +2 -2
  528. package/dist/react.d.ts +2 -2
  529. package/dist/react.js +1 -1
  530. package/dist/react.js.map +1 -1
  531. package/dist/react.mjs +1 -1
  532. package/dist/react.mjs.map +1 -1
  533. package/dist/types/blocks/auth.d.ts +45 -0
  534. package/dist/types/blocks/brand.d.ts +11 -2
  535. package/dist/types/blocks/create.d.ts +11 -4
  536. package/dist/types/blocks/doctor.d.ts +1 -0
  537. package/dist/types/blocks/flows.d.ts +7 -1
  538. package/dist/types/blocks/orgBrand.d.ts +34 -0
  539. package/dist/types/cli.d.ts +3 -0
  540. package/dist/types/version.d.ts +2 -2
  541. package/docs/api/desk.d.ts +2 -2
  542. package/docs/api/desk.md +3 -3
  543. package/docs/api/forms.md +1 -1
  544. package/package.json +1 -1
  545. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  546. package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
  547. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  548. package/stacks/6sense.json +37 -4
  549. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  550. package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
  551. package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
  552. package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
  553. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  554. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  555. package/stacks/apollo/pages/lists.tsx.tmpl +24 -0
  556. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  557. package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
  558. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  559. package/stacks/apollo.json +68 -9
  560. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  561. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  562. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  563. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  564. package/stacks/calendly.json +8 -4
  565. package/stacks/clay/pages/companies.tsx.tmpl +17 -1
  566. package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
  567. package/stacks/clay/pages/lists.tsx.tmpl +24 -0
  568. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  569. package/stacks/clay.json +16 -3
  570. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  571. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  572. package/stacks/gong/pages/contacts.tsx.tmpl +34 -0
  573. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  574. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  575. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  576. package/stacks/gong.json +23 -6
  577. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  578. package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
  579. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  580. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  581. package/stacks/hootsuite.json +19 -4
  582. package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
  583. package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
  584. package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
  585. package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
  586. package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
  587. package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
  588. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  589. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  590. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  591. package/stacks/hubspot/pages/sequences.tsx.tmpl +18 -0
  592. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  593. package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
  594. package/stacks/hubspot.json +66 -7
  595. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  596. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  597. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  598. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  599. package/stacks/intercom.json +18 -4
  600. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  601. package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
  602. package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
  603. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  604. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  605. package/stacks/mailchimp.json +22 -6
  606. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  607. package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
  608. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  609. package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
  610. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  611. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  612. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  613. package/stacks/outreach/pages/sequences.tsx.tmpl +18 -0
  614. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  615. package/stacks/outreach.json +38 -9
  616. package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
  617. package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
  618. package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
  619. package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
  620. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  621. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  622. package/stacks/pipedrive.json +14 -2
  623. package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
  624. package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
  625. package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
  626. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  627. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  628. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  629. package/stacks/salesforce.json +17 -3
  630. package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
  631. package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
  632. package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
  633. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  634. package/stacks/zoominfo.json +36 -3
  635. package/styles/styles.css +1 -1
  636. package/templates/auth/auth-route.ts.tmpl +16 -0
  637. package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
  638. package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
  639. package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
  640. package/templates/auth/g8.auth.password.ts.tmpl +18 -0
  641. package/templates/auth/g8.auth.session.ts.tmpl +23 -0
  642. package/templates/auth/middleware.ts.tmpl +14 -0
  643. package/templates/next-app/README.md.tmpl +5 -0
  644. package/templates/next-app/app/globals.css.tmpl +349 -64
  645. package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
  646. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
@@ -50,9 +50,9 @@ const STATES = ["", "waiting", "paused", "completed", "replied", "bounced", "ski
50
50
 
51
51
  function Tile({ label, value }: { label: string; value: string | number }) {
52
52
  return (
53
- <div className="rounded border border-gray-200 p-3">
54
- <div className="text-xs capitalize text-gray-500">{label}</div>
55
- <div className="text-xl font-semibold">{value}</div>
53
+ <div className="rounded border border-border p-3">
54
+ <div className="text-xs capitalize text-muted-foreground">{label}</div>
55
+ <div className="text-xl font-medium">{value}</div>
56
56
  </div>
57
57
  );
58
58
  }
@@ -145,7 +145,7 @@ export function SequenceStats({ sequenceId, pageSize = 25, refreshSeconds, class
145
145
  <TableCell>{st.step_order}</TableCell>
146
146
  <TableCell>{st.step_type}</TableCell>
147
147
  <TableCell>{st.total_contacts ?? 0}</TableCell>
148
- <TableCell className="text-xs text-gray-600">
148
+ <TableCell className="text-xs text-muted-foreground">
149
149
  {Object.entries(st.counts ?? {})
150
150
  .filter(([, n]) => n > 0)
151
151
  .map(([k, n]) => `${k}: ${n}`)
@@ -156,11 +156,11 @@ export function SequenceStats({ sequenceId, pageSize = 25, refreshSeconds, class
156
156
  </TableBody>
157
157
  </Table>
158
158
  ) : (
159
- <p className="text-sm text-gray-500">No step activity yet.</p>
159
+ <p className="text-sm text-muted-foreground">No step activity yet.</p>
160
160
  )}
161
161
 
162
162
  {skips.length > 0 ? (
163
- <p className="text-sm text-gray-600" data-testid="skip-reasons">
163
+ <p className="text-sm text-muted-foreground" data-testid="skip-reasons">
164
164
  Skipped contacts, counted once each: {skips.map(([k, n]) => `${k.replace(/^skipped_/, "").replace(/_/g, " ")} ${n}`).join(", ")}
165
165
  </p>
166
166
  ) : null}
@@ -171,7 +171,7 @@ export function SequenceStats({ sequenceId, pageSize = 25, refreshSeconds, class
171
171
  Contacts in
172
172
  <select
173
173
  aria-label="Contact state"
174
- className="ml-2 h-8 rounded border border-gray-300 px-1"
174
+ className="ml-2 h-8 rounded border border-input px-1"
175
175
  value={state}
176
176
  onChange={(e) => {
177
177
  setState(e.target.value);
@@ -201,7 +201,7 @@ export function SequenceStats({ sequenceId, pageSize = 25, refreshSeconds, class
201
201
  <TableBody>
202
202
  {rows.length === 0 ? (
203
203
  <TableRow>
204
- <TableCell colSpan={4} className="text-sm text-gray-500">
204
+ <TableCell colSpan={4} className="text-sm text-muted-foreground">
205
205
  {contacts.loading ? "Loading..." : "No contacts in this state."}
206
206
  </TableCell>
207
207
  </TableRow>
@@ -0,0 +1,160 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SequenceTemplates: reusable sequence skeletons (a named list of steps). List, create, edit, archive, restore and USE
5
+ * them: "Use template" hands the template's steps to `onUse`, which SequencesScreen opens in the builder so you only add
6
+ * the audience list and the senders.
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. GET /sequencer/templates/sequences (sequences.listSequenceTemplates) read (archived ones included; filtered here)
10
+ * 2. POST /sequencer/templates/sequences (sequences.createSequenceTemplate) write
11
+ * 3. PATCH /sequencer/templates/sequences/{id} (sequences.updateSequenceTemplate) write - ONLY the changed fields; the steps
12
+ * are sent (as the whole list, which replaces the old one) only when they changed
13
+ * 4. PATCH /sequencer/templates/sequences/{id}/archive (sequences.archiveSequenceTemplate) DESTRUCTIVE to archive -> ConfirmSpendDialog; restoring is a plain write
14
+ * A template is a COPY: using it, or changing it later, never touches a sequence built from it. The form is seeded from a
15
+ * LOADED template and Save stays disabled until something changed. A template can also be made from a finished sequence
16
+ * ("Save as template" in SequenceManageActions).
17
+ */
18
+ import { useState } from "react";
19
+ import { Archive, ArchiveRestore, FilePlus2, Pencil, Plus } from "lucide-react";
20
+ import { Button, Input, Skeleton, Textarea } 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 { StatusPill } from "@/g8/components/screen/status-pill";
25
+ import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
26
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
27
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
28
+ import { sequenceTemplates, templateBodyFromSteps, templateToDraftSteps } from "../lib/sequencer-manage";
29
+ import type { SequenceTemplate } from "../lib/sequencer-manage";
30
+ import { KIND_LABEL, KINDS, newStep, stepIssues, toApiStep } from "../lib/sequencer-utils";
31
+ import type { DraftStep, StepKind } from "../lib/sequencer-utils";
32
+ import { StepEditor } from "./step-editor";
33
+
34
+ export interface SequenceTemplatesProps {
35
+ /** Called when the person chooses "Use template": the template's name and an unsaved copy of its steps. */
36
+ onUse?: (template: { id: string; name: string; description: string; steps: DraftStep[] }) => void;
37
+ className?: string;
38
+ }
39
+
40
+ const stepsKey = (steps: DraftStep[]) => JSON.stringify(steps.map((s, i) => toApiStep(s, i + 1)));
41
+
42
+ function TemplateForm({ loaded, onDone }: { loaded: SequenceTemplate | null; onDone: (changed: boolean) => void }) {
43
+ const [name, setName] = useState(loaded?.name ?? "");
44
+ const [description, setDescription] = useState(loaded?.description ?? "");
45
+ const [steps, setSteps] = useState<DraftStep[]>(() => (loaded ? templateToDraftSteps(loaded) : []));
46
+ const stepsChanged = loaded ? stepsKey(steps) !== stepsKey(templateToDraftSteps(loaded)) : steps.length > 0;
47
+ const save = useG8Mutation(async () => {
48
+ // A loaded step keeps its own name (same position and same step type); only new or retyped steps get the automatic one.
49
+ const api = steps.map((s, i) => {
50
+ const was = loaded?.steps[i];
51
+ const a = toApiStep(s, i + 1);
52
+ return { ...a, id: undefined, name: was && was.step_type === a.step_type ? was.name : undefined };
53
+ });
54
+ const body = templateBodyFromSteps(name, description, api);
55
+ if (!loaded) {
56
+ await g8Post("/sequencer/templates/sequences", body);
57
+ return;
58
+ }
59
+ const changes: Record<string, unknown> = {};
60
+ if (name.trim() !== loaded.name) changes.name = name.trim();
61
+ if (description.trim() !== loaded.description) changes.description = description.trim();
62
+ if (stepsChanged) changes.steps = body.steps;
63
+ await g8Patch(`/sequencer/templates/sequences/${encodeURIComponent(loaded.id)}`, changes);
64
+ }, { invalidates: ["sequences"] });
65
+ const dirty = loaded ? name.trim() !== loaded.name || description.trim() !== loaded.description || stepsChanged : true;
66
+ const valid = name.trim().length > 0 && dirty && steps.every((s) => stepIssues(s).length === 0);
67
+ const submit = async () => {
68
+ try {
69
+ await save.mutateAsync(undefined);
70
+ onDone(true);
71
+ } catch {
72
+ /* shown below */
73
+ }
74
+ };
75
+ const add = (kind: StepKind) => setSteps((all) => [...all, newStep(kind)]);
76
+ return (
77
+ <form className="space-y-3 rounded-md border border-border p-3" aria-label={loaded ? `Edit ${loaded.name}` : "New sequence template"} onSubmit={(e) => { e.preventDefault(); if (valid) void submit(); }}>
78
+ <Input aria-label="Template name" placeholder="Template name" value={name} onChange={(e) => setName(e.target.value)} />
79
+ <Textarea aria-label="Description" rows={2} placeholder="What this sequence is for (optional)" value={description} onChange={(e) => setDescription(e.target.value)} />
80
+ {steps.map((s, i) => (
81
+ <StepEditor key={s.key} step={s} index={i + 1} onChange={(next) => setSteps((all) => all.map((x) => (x.key === s.key ? next : x)))} onRemove={() => setSteps((all) => all.filter((x) => x.key !== s.key))} />
82
+ ))}
83
+ <div className="flex flex-wrap gap-2">
84
+ {KINDS.map((k) => <Button key={k} type="button" size="sm" variant="outline" onClick={() => add(k)}>Add {KIND_LABEL[k].toLowerCase()} step</Button>)}
85
+ </div>
86
+ {save.error ? <ApiError error={save.error} title="Could not save the template" /> : null}
87
+ <div className="flex justify-end gap-2">
88
+ <Button type="button" variant="outline" size="sm" onClick={() => onDone(false)}>Cancel</Button>
89
+ <Button type="submit" size="sm" disabled={!valid || save.loading}>{loaded ? "Save changes" : "Create template"}</Button>
90
+ </div>
91
+ </form>
92
+ );
93
+ }
94
+
95
+ export function SequenceTemplates({ onUse, className }: SequenceTemplatesProps) {
96
+ const [editing, setEditing] = useState<SequenceTemplate | "new" | null>(null);
97
+ const [archiving, setArchiving] = useState<SequenceTemplate | null>(null);
98
+ const [showArchived, setShowArchived] = useState(false);
99
+ const [restoringId, setRestoringId] = useState<string | null>(null);
100
+ const list = useG8Query(async (signal) => sequenceTemplates(await g8Get("/sequencer/templates/sequences", undefined, { signal })), [], { key: ["sequences", "templates"] });
101
+ const archive = useG8Mutation(async (v: { template: SequenceTemplate; archived: boolean }) => {
102
+ await g8Patch(`/sequencer/templates/sequences/${encodeURIComponent(v.template.id)}/archive`, { is_archived: v.archived });
103
+ setArchiving(null);
104
+ }, { invalidates: ["sequences"], onError: () => setArchiving(null) });
105
+
106
+ if (list.error) return <ApiError error={list.error} title="Could not load the sequence templates" onRetry={() => void list.refetch()} className={className} />;
107
+ if (list.loading && !list.data) return <div aria-busy="true" className={`space-y-2 ${className ?? ""}`}><Skeleton className="h-12 w-full" /><Skeleton className="h-12 w-full" /></div>;
108
+ const rows = (list.data ?? []).filter((t) => showArchived || !t.archived);
109
+ const done = (changed: boolean) => { setEditing(null); if (changed) void list.refetch(); };
110
+
111
+ return (
112
+ <div className={`space-y-3 ${className ?? ""}`}>
113
+ <div className="flex flex-wrap items-center justify-between gap-2">
114
+ <h3 className="font-medium">Sequence templates</h3>
115
+ <div className="flex items-center gap-2">
116
+ <label className="flex items-center gap-1 text-xs"><input type="checkbox" checked={showArchived} onChange={(e) => setShowArchived(e.target.checked)} />Show archived</label>
117
+ <Button size="sm" disabled={editing != null} onClick={() => setEditing("new")}><Plus className="mr-1 size-4" aria-hidden />New template</Button>
118
+ </div>
119
+ </div>
120
+ {editing === "new" ? <TemplateForm loaded={null} onDone={done} /> : null}
121
+ {rows.length === 0 && editing !== "new" ? (
122
+ <EmptyState compact title="No sequence templates yet" description="Save the steps of a sequence that works, then start new ones from it." action={<Button size="sm" onClick={() => setEditing("new")}>Create a template</Button>} />
123
+ ) : (
124
+ <ul aria-label="Sequence templates" className="divide-y rounded-md border border-border">
125
+ {rows.map((t) => (
126
+ <li key={t.id} className="p-2 text-sm">
127
+ {editing !== "new" && editing?.id === t.id ? (
128
+ <TemplateForm loaded={t} onDone={done} />
129
+ ) : (
130
+ <div className="flex items-center gap-2">
131
+ <span className="min-w-0 flex-1"><span className="font-medium">{t.name}</span><span className="block truncate text-xs text-muted-foreground">{t.steps.length} {t.steps.length === 1 ? "step" : "steps"}{t.description ? ` - ${t.description}` : ""}</span></span>
132
+ {t.archived ? <StatusPill tone="warning">archived</StatusPill> : null}
133
+ {!t.archived && onUse ? <Button size="sm" variant="outline" onClick={() => onUse({ id: t.id, name: t.name, description: t.description, steps: templateToDraftSteps(t) })}><FilePlus2 className="mr-1 size-4" aria-hidden />Use template</Button> : null}
134
+ <Button size="sm" variant="ghost" aria-label={`Edit ${t.name}`} disabled={editing != null} onClick={() => setEditing(t)}><Pencil className="size-4" aria-hidden /></Button>
135
+ {t.archived ? (
136
+ <Button size="sm" variant="ghost" aria-label={`Restore ${t.name}`} loading={archive.loading && restoringId === t.id} onClick={() => { setRestoringId(t.id); void archive.mutate({ template: t, archived: false }); }}><ArchiveRestore className="size-4" aria-hidden /></Button>
137
+ ) : (
138
+ <Button size="sm" variant="ghost" aria-label={`Archive ${t.name}`} onClick={() => setArchiving(t)}><Archive className="size-4" aria-hidden /></Button>
139
+ )}
140
+ </div>
141
+ )}
142
+ </li>
143
+ ))}
144
+ </ul>
145
+ )}
146
+ {archive.error ? <ApiError error={archive.error} title="Could not update the template" /> : null}
147
+ <ConfirmSpendDialog
148
+ open={archiving != null}
149
+ tier="destructive"
150
+ title={`Archive "${archiving?.name ?? ""}"?`}
151
+ description="It leaves the template list. Sequences already built from it are not changed. You can restore it from Show archived."
152
+ confirmLabel="Archive template"
153
+ affected={1}
154
+ confirming={archive.loading}
155
+ onConfirm={() => archiving && void archive.mutate({ template: archiving, archived: true })}
156
+ onCancel={() => setArchiving(null)}
157
+ />
158
+ </div>
159
+ );
160
+ }
@@ -19,6 +19,10 @@
19
19
  * - Open a sequence (deep link `?id=`): Results (stats, contacts, pause/resume one), Steps (edit copy), and
20
20
  * "Enroll contacts": pick a list, tick contacts, confirm the recipient count.
21
21
  *
22
+ * - "Templates" (header): sequence templates (list, create, edit, archive, use) and step templates (the same, plus AI drafts);
23
+ * "Use template" opens the new-sequence sheet with the template's name and steps filled in. In an open sequence: Edit (rename and
24
+ * settings), Sync audience, Save as template, Delete, a Senders tab, and removing a contact from the sequence.
25
+ *
22
26
  * SDK operations of its own (both read):
23
27
  * 1. listSequences GET /sequences?limit=100 (counts for the stat strip)
24
28
  * 2. getSequence GET /sequences/{id} (header of the open sequence; also gives associated_list_id)
@@ -27,8 +31,8 @@
27
31
 
28
32
  import { useState } from "react";
29
33
  import type { ReactNode } from "react";
30
- import { ListOrdered, Plus } from "lucide-react";
31
- import { Button } from "@graph8/sdk/ui";
34
+ import { LayoutTemplate, ListOrdered, Plus } from "lucide-react";
35
+ import { Button, Tabs, TabsContent, TabsList, TabsTrigger } from "@graph8/sdk/ui";
32
36
  import { FormSheet } from "@/g8/components/screen/form-sheet";
33
37
  import { ScreenLayout } from "@/g8/components/screen/screen-layout";
34
38
  import { SplitView } from "@/g8/components/screen/split-view";
@@ -38,10 +42,12 @@ import { useG8Query } from "@/g8/hooks/use-g8-query";
38
42
  import { useUrlState } from "@/g8/hooks/use-url-state";
39
43
  import { g8Get } from "@/g8/lib/g8-client";
40
44
  import { listOf, statusGroup } from "../lib/sequencer-utils";
41
- import type { SequenceRow } from "../lib/sequencer-utils";
45
+ import type { DraftStep, SequenceRow } from "../lib/sequencer-utils";
42
46
  import { SequenceBuilder } from "./sequence-builder";
43
47
  import { SequenceDetail } from "./sequence-detail";
44
48
  import { SequenceList } from "./sequence-list";
49
+ import { SequenceTemplates } from "./sequence-templates";
50
+ import { StepTemplates } from "./step-templates";
45
51
 
46
52
  export interface SequencesScreenProps {
47
53
  /** Owner email sent with a new sequence (`user_email`). */
@@ -52,6 +58,8 @@ export interface SequencesScreenProps {
52
58
  renderDetailActions?: (sequence: SequenceRow) => ReactNode;
53
59
  /** Extra tabs in the open sequence's panel. */
54
60
  extraDetailTabs?: { id: string; label: string; render: (sequence: SequenceRow) => ReactNode }[];
61
+ /** Open an enrolled person in your contacts screen; without it the Contacts tab has no such button. */
62
+ onOpenContact?: (contactId: number) => void;
55
63
  className?: string;
56
64
  }
57
65
 
@@ -85,6 +93,8 @@ export function SequencesScreen(props: SequencesScreenProps) {
85
93
  const [contactId, setContactId] = useUrlState("enrollment", "");
86
94
  const [status, setStatus] = useUrlState("status", "");
87
95
  const [creating, setCreating] = useState(false);
96
+ const [templatesOpen, setTemplatesOpen] = useState(false);
97
+ const [prefill, setPrefill] = useState<{ name: string; steps: DraftStep[] } | null>(null);
88
98
  const [version, setVersion] = useState(0);
89
99
  const [listKey, setListKey] = useState(0);
90
100
  const summary = useG8Query((signal) => g8Get("/sequences", { limit: SUMMARY_LIMIT }, { signal }), [version], { key: ["sequences", "summary"] });
@@ -101,9 +111,14 @@ export function SequencesScreen(props: SequencesScreenProps) {
101
111
  description="Multi-channel outreach that runs on its own: build steps, enroll contacts, watch replies."
102
112
  icon={<ListOrdered className="h-5 w-5" aria-hidden />}
103
113
  actions={
104
- <Button onClick={() => setCreating(true)}>
105
- <Plus className="mr-1 h-4 w-4" aria-hidden /> New sequence
106
- </Button>
114
+ <>
115
+ <Button variant="outline" onClick={() => setTemplatesOpen(true)}>
116
+ <LayoutTemplate className="mr-1 h-4 w-4" aria-hidden /> Templates
117
+ </Button>
118
+ <Button onClick={() => { setPrefill(null); setCreating(true); }}>
119
+ <Plus className="mr-1 h-4 w-4" aria-hidden /> New sequence
120
+ </Button>
121
+ </>
107
122
  }
108
123
  stats={stats}
109
124
  >
@@ -135,6 +150,7 @@ export function SequencesScreen(props: SequencesScreenProps) {
135
150
  onStepChange={(v) => setStepId(v)}
136
151
  contactId={contactId || undefined}
137
152
  onContactChange={(v) => setContactId(v)}
153
+ onOpenContact={props.onOpenContact}
138
154
  onClose={close}
139
155
  onChanged={refreshList}
140
156
  refreshKey={version}
@@ -146,9 +162,26 @@ export function SequencesScreen(props: SequencesScreenProps) {
146
162
  }
147
163
  />
148
164
  </div>
165
+ <FormSheet open={templatesOpen} onOpenChange={setTemplatesOpen} width="xl" title="Templates" description="Reusable sequences and steps. A template is a copy: changing one never changes a sequence built from it.">
166
+ {templatesOpen ? (
167
+ <Tabs defaultValue="sequences">
168
+ <TabsList aria-label="Template kind">
169
+ <TabsTrigger value="sequences">Sequence templates</TabsTrigger>
170
+ <TabsTrigger value="steps">Step templates</TabsTrigger>
171
+ </TabsList>
172
+ <TabsContent value="sequences" className="pt-3">
173
+ <SequenceTemplates onUse={(t) => { setPrefill({ name: t.name, steps: t.steps }); setTemplatesOpen(false); setCreating(true); }} />
174
+ </TabsContent>
175
+ <TabsContent value="steps" className="pt-3"><StepTemplates /></TabsContent>
176
+ </Tabs>
177
+ ) : null}
178
+ </FormSheet>
149
179
  <FormSheet open={creating} onOpenChange={setCreating} width="xl" title="New sequence" description="Saved as a draft. Nobody is contacted until you launch it.">
150
180
  {creating ? (
151
181
  <SequenceBuilder
182
+ key={prefill ? `tpl:${prefill.name}` : "blank"}
183
+ initialName={prefill?.name}
184
+ initialSteps={prefill?.steps}
152
185
  ownerEmail={ownerEmail}
153
186
  onSaved={(id) => { setCreating(false); refreshList(); open(id); }}
154
187
  />
@@ -0,0 +1,196 @@
1
+ "use client";
2
+
3
+ /**
4
+ * StepAiAssist: AI help for the copy of ONE email or LinkedIn step. It is collapsed until opened and loads nothing
5
+ * until then. Plug it into StepEditor's `renderAssist` slot (SequenceBuilder does).
6
+ *
7
+ * SDK operations, in order:
8
+ * Improve the instructions (email steps the AI writes)
9
+ * POST /sequencer/content/instructions/improve (sequences.improveStepInstruction) BILLABLE -> ConfirmSpendDialog first.
10
+ * Returns a rewrite of your instruction; it is shown beside yours and only replaces it when you accept.
11
+ * Preview a draft for one contact
12
+ * GET /contacts?name= (contacts.listContacts) read - find the contact
13
+ * GET /sequencer/agents/textual (sequences.listSequencerTextualAgents) read - the agent that writes
14
+ * POST /sequencer/content/email/estimate (sequences.estimateEmailDraft) read, FREE - the credit price, shown in the dialog
15
+ * POST /sequencer/content/email/generate (sequences.draftEmail) BILLABLE -> ConfirmSpendDialog first
16
+ * POST /sequencer/content/linkedin/generate (sequences.draftLinkedinMessage) BILLABLE -> ConfirmSpendDialog first
17
+ * The draft is shown, never sent or saved. "Use as my copy" first warns that the draft is personalised for one contact while the step goes to everyone, and writes it only after a second click.
18
+ * Nothing here reaches a contact: it only drafts text for you to read.
19
+ */
20
+ import { useEffect, useState } from "react";
21
+ import { Sparkles } from "lucide-react";
22
+ import { Button, Input, Textarea } from "@graph8/sdk/ui";
23
+ import { ApiError } from "@/g8/components/api-error";
24
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
25
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
26
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
27
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
28
+ import { agentOptions, draftContact, readCreditEstimate, readEmailDraft, readImprovedInstruction, readLinkedinDraft, templateRows } from "../lib/sequencer-manage";
29
+ import type { DraftContact } from "../lib/sequencer-manage";
30
+ import type { DraftStep } from "../lib/sequencer-utils";
31
+
32
+ export interface StepAiAssistProps {
33
+ step: DraftStep;
34
+ onChange: (next: DraftStep) => void;
35
+ /** The saved sequence this step belongs to, when there is one (gives the drafter the earlier steps as context). */
36
+ sequenceId?: string;
37
+ /** 1-based position of the step, for the same reason. */
38
+ stepOrder?: number;
39
+ className?: string;
40
+ }
41
+
42
+ const LINKEDIN_COPY: Record<string, string | undefined> = { CONNECTION_REQUEST: "linkedin_connection_message", MESSAGE: "linkedin_message_text", INMAIL: "linkedin_inmail_body" };
43
+ const text = (v: unknown): string => (typeof v === "string" ? v : "");
44
+
45
+ function useDebounced<T>(value: T, ms = 300): T {
46
+ const [v, setV] = useState(value);
47
+ useEffect(() => {
48
+ const t = setTimeout(() => setV(value), ms);
49
+ return () => clearTimeout(t);
50
+ }, [value, ms]);
51
+ return v;
52
+ }
53
+
54
+ type Pending = { kind: "improve" } | { kind: "draft"; credits: number | null; cached: boolean } | null;
55
+
56
+ export function StepAiAssist({ step, onChange, sequenceId, stepOrder, className }: StepAiAssistProps) {
57
+ const [open, setOpen] = useState(false);
58
+ if (step.kind !== "email" && step.kind !== "linkedin") return null;
59
+ if (!open) {
60
+ return <div className={className}><Button type="button" size="sm" variant="ghost" onClick={() => setOpen(true)}><Sparkles className="mr-1 size-4" aria-hidden />AI help for this step</Button></div>;
61
+ }
62
+ return <AssistPanel step={step} onChange={onChange} sequenceId={sequenceId} stepOrder={stepOrder} onClose={() => setOpen(false)} className={className} />;
63
+ }
64
+
65
+ function AssistPanel({ step, onChange, sequenceId, stepOrder, onClose, className }: StepAiAssistProps & { onClose: () => void }) {
66
+ const isEmail = step.kind === "email";
67
+ const action = text(step.step_data.linkedin_action_type);
68
+ const copyKey = LINKEDIN_COPY[action];
69
+ const aiEmail = isEmail && step.input_type !== "MANUAL_TEMPLATE";
70
+ const [instructions, setInstructions] = useState(text(step.step_data.instructions));
71
+ const [agent, setAgent] = useState("");
72
+ const [search, setSearch] = useState("");
73
+ const [contact, setContact] = useState<DraftContact | null>(null);
74
+ const [pending, setPending] = useState<Pending>(null);
75
+ const [improved, setImproved] = useState<string | null>(null);
76
+ const [draft, setDraft] = useState<{ subject: string; body: string } | null>(null);
77
+ // The draft is written for ONE contact but the step is sent to every contact: ask before it replaces the step copy.
78
+ const [confirmingCopy, setConfirmingCopy] = useState(false);
79
+ const q = useDebounced(search.trim());
80
+
81
+ const agents = useG8Query(async (signal) => agentOptions(await g8Get("/sequencer/agents/textual", undefined, { signal })), []);
82
+ const found = useG8Query(
83
+ async (signal) => templateRows<Record<string, unknown>>(await g8Get("/contacts", { name: q, limit: 8 }, { signal })).map(draftContact).filter((c): c is DraftContact => c != null),
84
+ [q],
85
+ { enabled: q.length >= 2 },
86
+ );
87
+
88
+ const body = () =>
89
+ isEmail
90
+ ? { agent_name: agent, contact_id: contact!.id, contact_work_email: contact!.email, first_name: contact!.first_name, last_name: contact!.last_name, instructions, lead_info: contact!.lead_info, sequence_id: sequenceId, step_order: stepOrder }
91
+ : { agent_name: agent, contact_id: contact!.id, first_name: contact!.first_name, last_name: contact!.last_name, linkedin_action_type: action, instructions, lead_info: contact!.lead_info };
92
+
93
+ const prepare = useG8Mutation(async (kind: "improve" | "draft") => {
94
+ if (kind === "improve") return setPending({ kind: "improve" });
95
+ // The estimate is free and exact; if it is unavailable the dialog still asks, without a number.
96
+ const estimate = isEmail ? await g8Post("/sequencer/content/email/estimate", body()).then(readCreditEstimate, () => null) : null;
97
+ setPending({ kind: "draft", credits: estimate?.credits ?? null, cached: estimate?.cached ?? false });
98
+ });
99
+ const run = useG8Mutation(async (p: NonNullable<Pending>) => {
100
+ if (p.kind === "improve") {
101
+ setImproved(readImprovedInstruction(await g8Post("/sequencer/content/instructions/improve", { user_instruction: instructions })) || null);
102
+ } else if (isEmail) {
103
+ setDraft(readEmailDraft(await g8Post("/sequencer/content/email/generate", body())));
104
+ } else {
105
+ const li = readLinkedinDraft(await g8Post("/sequencer/content/linkedin/generate", body()));
106
+ setDraft(li ? { subject: li.subject, body: li.message } : null);
107
+ }
108
+ setPending(null);
109
+ }, { onError: () => setPending(null) });
110
+
111
+ const useCopy = () => {
112
+ if (!draft) return;
113
+ const data = isEmail ? { ...step.step_data, subject: draft.subject, body: draft.body } : { ...step.step_data, ...(copyKey ? { [copyKey]: draft.body } : {}), ...(action === "INMAIL" && draft.subject ? { linkedin_inmail_subject: draft.subject } : {}) };
114
+ onChange({ ...step, dirty: true, input_type: "MANUAL_TEMPLATE", step_data: data });
115
+ setDraft(null);
116
+ setConfirmingCopy(false);
117
+ };
118
+ const acceptInstructions = () => {
119
+ if (improved == null) return;
120
+ setInstructions(improved);
121
+ onChange({ ...step, dirty: true, step_data: { ...step.step_data, instructions: improved } });
122
+ setImproved(null);
123
+ };
124
+
125
+ const canDraft = Boolean(agent) && contact != null && instructions.trim().length > 0 && (isEmail ? Boolean(contact.email) : Boolean(copyKey));
126
+ const busy = prepare.loading || run.loading;
127
+ const confirmDraft = pending?.kind === "draft" ? pending : null;
128
+
129
+ return (
130
+ <div className={`space-y-3 rounded-md border border-border p-3 text-sm ${className ?? ""}`} aria-label="AI help">
131
+ <div className="flex items-center justify-between"><h4 className="font-medium">AI help</h4><Button type="button" size="sm" variant="ghost" onClick={onClose}>Close</Button></div>
132
+ <Textarea aria-label="What should it say" rows={3} placeholder="What should this message say, and to whom?" value={instructions} onChange={(e) => setInstructions(e.target.value)} />
133
+ {aiEmail ? <Button type="button" size="sm" variant="outline" disabled={!instructions.trim() || busy} onClick={() => void prepare.mutate("improve")}>Improve my instructions</Button> : null}
134
+ {improved != null ? (
135
+ <div role="group" aria-label="Improved instructions" className="space-y-2 rounded-md bg-muted p-2">
136
+ <p className="whitespace-pre-wrap">{improved}</p>
137
+ <div className="flex gap-2"><Button type="button" size="sm" onClick={acceptInstructions}>Use these instructions</Button><Button type="button" size="sm" variant="ghost" onClick={() => setImproved(null)}>Keep mine</Button></div>
138
+ </div>
139
+ ) : null}
140
+
141
+ <div className="space-y-2 border-t border-border pt-3">
142
+ <p className="font-medium">Preview a draft for one contact</p>
143
+ <select aria-label="Agent" className="h-9 w-full rounded-md border border-border bg-background px-2" value={agent} disabled={agents.loading} onChange={(e) => setAgent(e.target.value)}>
144
+ <option value="">{agents.loading ? "Loading agents..." : "Choose the agent that writes"}</option>
145
+ {(agents.data ?? []).map((a) => <option key={a.value} value={a.value}>{a.label}</option>)}
146
+ </select>
147
+ {agents.error ? <ApiError error={agents.error} title="Could not load the agents" onRetry={() => void agents.refetch()} /> : null}
148
+ {agents.data && agents.data.length === 0 ? <p className="text-muted-foreground">No agent is set up yet, so there is nobody to write the draft.</p> : null}
149
+ {contact ? (
150
+ <p className="flex items-center justify-between gap-2"><span>Draft for <strong>{contact.label}</strong></span><Button type="button" size="sm" variant="ghost" onClick={() => setContact(null)}>Change</Button></p>
151
+ ) : (
152
+ <>
153
+ <Input aria-label="Find a contact" placeholder="Search contacts by name" value={search} onChange={(e) => setSearch(e.target.value)} />
154
+ {found.error ? <ApiError error={found.error} compact onRetry={() => void found.refetch()} /> : null}
155
+ {found.data && found.data.length === 0 ? <p className="text-muted-foreground">No contact matches.</p> : null}
156
+ {found.data && found.data.length > 0 ? (
157
+ <ul aria-label="Contacts" className="divide-y rounded-md border border-border">
158
+ {found.data.map((c) => <li key={c.id}><button type="button" className="w-full p-2 text-left hover:bg-muted/50" onClick={() => setContact(c)}>{c.label}</button></li>)}
159
+ </ul>
160
+ ) : null}
161
+ </>
162
+ )}
163
+ {!isEmail && !copyKey ? <p className="text-muted-foreground">This LinkedIn action sends no text, so there is nothing to draft.</p> : null}
164
+ <Button type="button" size="sm" disabled={!canDraft || busy} loading={prepare.loading} onClick={() => void prepare.mutate("draft")}>Draft for this contact</Button>
165
+ </div>
166
+
167
+ {[prepare.error, run.error].map((e, i) => (e ? <ApiError key={i} error={e} title="The AI request failed" /> : null))}
168
+ {draft ? (
169
+ <div role="group" aria-label="Drafted copy" className="space-y-2 rounded-md bg-muted p-2">
170
+ {draft.subject ? <p className="font-medium">{draft.subject}</p> : null}
171
+ <p className="whitespace-pre-wrap">{draft.body}</p>
172
+ <div className="flex gap-2">{confirmingCopy ? (
173
+ <div role="alert" className="space-y-2 rounded-md border border-border bg-background p-2">
174
+ <p>This draft was written for {contact?.label ?? "one contact"}. The step is sent to every contact in the sequence, so names and details in it will go to people they do not fit. Replace the step copy only if you will make it generic (merge tokens such as {"{{first_name}}"}).</p>
175
+ <div className="flex gap-2"><Button type="button" size="sm" onClick={useCopy}>Replace the step copy</Button><Button type="button" size="sm" variant="ghost" onClick={() => setConfirmingCopy(false)}>Keep my copy</Button></div>
176
+ </div>
177
+ ) : (
178
+ <div className="flex gap-2"><Button type="button" size="sm" onClick={() => setConfirmingCopy(true)}>Use as my copy</Button><Button type="button" size="sm" variant="ghost" onClick={() => { setDraft(null); setConfirmingCopy(false); }}>Discard</Button></div>
179
+ )}</div>
180
+ </div>
181
+ ) : null}
182
+
183
+ <ConfirmSpendDialog
184
+ open={pending != null}
185
+ tier="billable"
186
+ title={pending?.kind === "improve" ? "Improve these instructions with AI?" : `Draft ${isEmail ? "an email" : "a LinkedIn message"} for ${contact?.label ?? "this contact"}?`}
187
+ description={pending?.kind === "improve" ? "The AI rewrites your instructions. It uses credits and nothing is sent to anyone." : confirmDraft?.cached ? "An identical draft is cached, so this one is free. Nothing is sent to anyone." : "The AI writes one draft for you to read. It uses credits each time and nothing is sent to anyone."}
188
+ estimatedCredits={confirmDraft?.credits ?? undefined}
189
+ confirmLabel={pending?.kind === "improve" ? "Improve instructions" : "Write draft"}
190
+ confirming={run.loading}
191
+ onConfirm={() => pending && void run.mutate(pending)}
192
+ onCancel={() => setPending(null)}
193
+ />
194
+ </div>
195
+ );
196
+ }
@@ -15,6 +15,7 @@
15
15
  * Problems show inline and `stepIssues(step)` returns the same list for gating a Save button.
16
16
  */
17
17
 
18
+ import type { ReactNode } from "react";
18
19
  import { Input, Textarea } from "@graph8/sdk/ui";
19
20
  import { g8Get } from "@/g8/lib/g8-client";
20
21
  import { useG8Query } from "@/g8/hooks/use-g8-query";
@@ -28,6 +29,10 @@ export interface StepEditorProps {
28
29
  index: number;
29
30
  onChange: (next: DraftStep) => void;
30
31
  onRemove?: () => void;
32
+ /** Optional slot under the copy, for AI help or other tools (SequenceBuilder passes StepAiAssist). Receives the step and its setter. */
33
+ /** Disable the Manual / AI input-type select (a saved step template: the API fixes its input type at creation). */
34
+ lockInputType?: boolean;
35
+ renderAssist?: (step: DraftStep, onChange: (next: DraftStep) => void) => ReactNode;
31
36
  disabled?: boolean;
32
37
  className?: string;
33
38
  }
@@ -52,15 +57,15 @@ function LinkedinReadiness() {
52
57
  const c = conn.data?.data;
53
58
  if (conn.loading || conn.error || !c) return null;
54
59
  if (c.connected && c.is_token_valid !== false && !c.requires_manual_login)
55
- return <p className="text-xs text-green-700">LinkedIn sender connected.</p>;
60
+ return <p className="text-xs text-positive-foreground">LinkedIn sender connected.</p>;
56
61
  return (
57
- <p role="alert" className="text-xs text-amber-800">
62
+ <p role="alert" className="text-xs text-warning-foreground">
58
63
  LinkedIn is not ready to send. Reconnect the LinkedIn account in the {brand.linkLabel} app before launching.
59
64
  </p>
60
65
  );
61
66
  }
62
67
 
63
- export function StepEditor({ step, index, onChange, onRemove, disabled, className }: StepEditorProps) {
68
+ export function StepEditor({ step, index, onChange, onRemove, renderAssist, lockInputType, disabled, className }: StepEditorProps) {
64
69
  const set = (patch: Record<string, unknown>) => onChange({ ...step, dirty: true, step_data: { ...step.step_data, ...patch } });
65
70
  const issues = stepIssues(step);
66
71
  const data = step.step_data;
@@ -72,13 +77,13 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
72
77
  const phoneOptions = Array.isArray(data.phone_options) ? (data.phone_options as string[]) : [];
73
78
 
74
79
  return (
75
- <fieldset disabled={disabled} className={`space-y-3 rounded border border-gray-200 p-3 ${className ?? ""}`} data-step-kind={step.kind}>
80
+ <fieldset disabled={disabled} className={`space-y-3 rounded border border-border p-3 ${className ?? ""}`} data-step-kind={step.kind}>
76
81
  <div className="flex items-center justify-between">
77
- <legend className="text-sm font-semibold">
82
+ <legend className="text-sm font-medium">
78
83
  Step {index}: {KIND_LABEL[step.kind]}
79
84
  </legend>
80
85
  {onRemove ? (
81
- <button type="button" className="text-xs text-red-700 hover:underline" onClick={onRemove}>
86
+ <button type="button" className="text-xs text-error-foreground hover:underline" onClick={onRemove}>
82
87
  Remove step
83
88
  </button>
84
89
  ) : null}
@@ -105,8 +110,9 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
105
110
  Copy
106
111
  <select
107
112
  aria-label="Email copy"
108
- className="ml-2 h-8 rounded border border-gray-300 px-1 text-sm"
113
+ className="ml-2 h-8 rounded border border-input px-1 text-sm"
109
114
  value={step.input_type}
115
+ disabled={lockInputType}
110
116
  onChange={(e) => onChange({ ...step, dirty: true, input_type: e.target.value as InputType })}
111
117
  >
112
118
  <option value="MANUAL_TEMPLATE">I write it</option>
@@ -114,6 +120,7 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
114
120
  <option value="ON_DEMAND">AI writes each email</option>
115
121
  </select>
116
122
  </label>
123
+ {lockInputType ? <p className="text-xs text-muted-foreground">The input type is fixed once a template is created. Create a new template to change it.</p> : null}
117
124
  {step.input_type === "MANUAL_TEMPLATE" ? (
118
125
  <>
119
126
  <Input aria-label="Subject" placeholder="Subject" value={str("subject")} onChange={(e) => set({ subject: e.target.value })} />
@@ -141,7 +148,7 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
141
148
  <>
142
149
  <select
143
150
  aria-label="LinkedIn action"
144
- className="h-8 rounded border border-gray-300 px-1 text-sm"
151
+ className="h-8 rounded border border-input px-1 text-sm"
145
152
  value={action}
146
153
  onChange={(e) => set({ linkedin_action_type: e.target.value })}
147
154
  >
@@ -160,7 +167,7 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
160
167
  onChange={(e) => set({ [copyKey]: e.target.value })}
161
168
  />
162
169
  ) : (
163
- <p className="text-xs text-gray-500">This action sends no text.</p>
170
+ <p className="text-xs text-muted-foreground">This action sends no text.</p>
164
171
  )}
165
172
  {action === "INMAIL" ? (
166
173
  <Input
@@ -176,7 +183,7 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
176
183
 
177
184
  {step.kind === "call" || step.kind === "task" ? (
178
185
  <div className="space-y-1 text-sm">
179
- <p className="text-gray-600">
186
+ <p className="text-muted-foreground">
180
187
  {step.kind === "call" ? "An automatic call attempt." : "Creates a call task for a person to work in the dialer."}
181
188
  </p>
182
189
  <div className="flex flex-wrap gap-3">
@@ -198,13 +205,15 @@ export function StepEditor({ step, index, onChange, onRemove, disabled, classNam
198
205
  </div>
199
206
  ) : null}
200
207
 
208
+ {renderAssist ? renderAssist(step, onChange) : null}
209
+
201
210
  {broken ? (
202
- <p role="alert" className="text-xs text-amber-800">
211
+ <p role="alert" className="text-xs text-warning-foreground">
203
212
  A {"{{merge token}}"} is not closed. Unclosed tokens are deleted when the email sends.
204
213
  </p>
205
214
  ) : null}
206
215
  {issues.length > 0 ? (
207
- <ul className="text-xs text-red-700" aria-label="Step problems">
216
+ <ul className="text-xs text-error-foreground" aria-label="Step problems">
208
217
  {issues.map((i) => (
209
218
  <li key={i}>{i}</li>
210
219
  ))}