@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,181 @@
1
+ "use client";
2
+
3
+ /**
4
+ * PersonaDetail: one buyer persona, complete. Every field, the campaigns that target it, and every action:
5
+ * edit, pin or unpin, archive.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. getPersona GET /personas/{persona_id} (read; all fields)
9
+ * 2. listCampaigns GET /campaigns (read; campaigns whose target persona is this title)
10
+ * 3. updatePersona PATCH /personas/{persona_id} (write)
11
+ * 4. pinPersona / unpinPersona POST /personas/{persona_id}/pin|unpin (write)
12
+ * 5. archivePersona POST /personas/{persona_id}/archive (destructive: confirmed)
13
+ */
14
+
15
+ import { useState } from "react";
16
+ import type { ReactNode } from "react";
17
+ import { Button, Input, Skeleton, Textarea } from "@graph8/sdk/ui";
18
+ import { ApiError } from "@/g8/components/api-error";
19
+ import { useAllPages } from "@/g8/hooks/use-all-pages";
20
+ import { hasNextOf } from "@/g8/lib/paging";
21
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
22
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
23
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
24
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
25
+ import { SectionCard } from "@/g8/components/screen/section-card";
26
+ import { StatusPill } from "@/g8/components/screen/status-pill";
27
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
28
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
29
+ import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
30
+ import { fieldItems, listOf, objectOf, personaFormOf, personaPatch, text } from "../lib/campaigns-utils";
31
+ import type { CampaignRow } from "../lib/campaigns-utils";
32
+ import { FormField } from "./form-field";
33
+
34
+ export interface PersonaDetailProps {
35
+ personaId: string;
36
+ onClose?: () => void;
37
+ /** Full-page mode: show an "Open full page" (onExpand) or "Back to split view" (onCollapse) button. */
38
+ onExpand?: () => void;
39
+ onCollapse?: () => void;
40
+ onChanged?: () => void;
41
+ /** Open a campaign that targets this persona. */
42
+ onOpenCampaign?: (campaign: CampaignRow) => void;
43
+ renderActions?: (persona: Record<string, unknown>) => ReactNode;
44
+ className?: string;
45
+ }
46
+
47
+ interface Form { title: string; why: string; signals: string; approach: string; goal: string; priority: string; confidence: string }
48
+
49
+ /** The API returns `pinned` (`is_pinned` is accepted too, for older payloads). */
50
+ function isPinned(r: Record<string, unknown> | undefined): boolean {
51
+ return Boolean(r && (r.pinned ?? r.is_pinned));
52
+ }
53
+
54
+ /** Campaigns per read: every page is read, so no targeting campaign is missed. */
55
+ const CAMPAIGN_PAGE = 100;
56
+
57
+ export function PersonaDetail({ personaId, onClose, onExpand, onCollapse, onChanged, onOpenCampaign, renderActions, className }: PersonaDetailProps) {
58
+ const [form, setForm] = useState<Form | null>(null);
59
+ const [archiving, setArchiving] = useState(false);
60
+ const q = useG8Query((signal) => g8Get(`/personas/${personaId}`, undefined, { signal }), [personaId]);
61
+ const campaigns = useAllPages<CampaignRow>(async (page, signal) => {
62
+ const env = await g8Get("/campaigns", { page, limit: CAMPAIGN_PAGE }, { signal });
63
+ const rows = listOf<CampaignRow>(env);
64
+ return { rows, hasNext: hasNextOf(env, CAMPAIGN_PAGE, rows.length) };
65
+ }, [personaId]);
66
+ const p = objectOf(q.data) as Record<string, any>;
67
+
68
+ const save = useG8Mutation(async (f: Form) => {
69
+ const patch = personaPatch(p, f);
70
+ if (Object.keys(patch).length > 0) await g8Patch(`/personas/${personaId}`, patch); // nothing changed: no request
71
+ setForm(null);
72
+ await q.refetch();
73
+ onChanged?.();
74
+ });
75
+ const pin = useG8Mutation(async () => {
76
+ await g8Post(`/personas/${personaId}/${isPinned(p) ? "unpin" : "pin"}`, {});
77
+ await q.refetch();
78
+ onChanged?.();
79
+ });
80
+ const archive = useG8Mutation(async () => {
81
+ await g8Post(`/personas/${personaId}/archive`, {});
82
+ setArchiving(false);
83
+ onChanged?.();
84
+ onClose?.();
85
+ }, { onError: () => setArchiving(false) });
86
+
87
+ if (q.error) return <ApiError error={q.error} title="Could not load the persona" onRetry={() => void q.refetch()} className={className} />;
88
+ if (q.loading && !q.data) return <Skeleton className={`h-48 w-full ${className ?? ""}`} />;
89
+
90
+ const title = text(p.title) || `Persona ${personaId}`;
91
+ const targeting = campaigns.items.filter((c) => (c.target_persona ?? "").trim().toLowerCase() === title.trim().toLowerCase());
92
+ const startEdit = () => {
93
+ save.reset();
94
+ setForm(personaFormOf(p));
95
+ };
96
+ const set = (k: keyof Form) => (e: { target: { value: string } }) => setForm((cur) => (cur ? { ...cur, [k]: e.target.value } : cur));
97
+ return (
98
+ <DetailPanel
99
+ className={className}
100
+ title={title}
101
+ subtitle={p.why_target ?? undefined}
102
+ badges={<><StatusPill tone={p.status && p.status !== "active" ? "neutral" : "positive"} dot>{p.status ?? "active"}</StatusPill>{isPinned(p) ? <StatusPill tone="info">pinned</StatusPill> : null}</>}
103
+ actions={
104
+ <>
105
+ <Button size="sm" variant="outline" onClick={startEdit}>Edit persona</Button>
106
+ <Button size="sm" variant="outline" loading={pin.loading} onClick={() => void pin.mutate(undefined)}>{isPinned(p) ? "Unpin persona" : "Pin persona"}</Button>
107
+ <Button size="sm" variant="outline" onClick={() => setArchiving(true)}>Archive persona</Button>
108
+ {renderActions?.(p)}
109
+ <FormSheet
110
+ open={form !== null}
111
+ onOpenChange={(o) => !o && setForm(null)}
112
+ title="Edit persona"
113
+ footer={<><Button variant="outline" onClick={() => setForm(null)}>Cancel</Button><Button loading={save.loading} disabled={!form?.title.trim()} onClick={() => form && void save.mutate(form)}>Save persona</Button></>}
114
+ >
115
+ {form ? (
116
+ <>
117
+ <FormField label="Title" htmlFor="pd-title"><Input id="pd-title" value={form.title} onChange={set("title")} /></FormField>
118
+ <FormField label="Why target" htmlFor="pd-why"><Textarea id="pd-why" value={form.why} onChange={set("why")} /></FormField>
119
+ <FormField label="Key signals" htmlFor="pd-sig" hint="One per line."><Textarea id="pd-sig" value={form.signals} onChange={set("signals")} /></FormField>
120
+ <FormField label="Campaign approach" htmlFor="pd-app"><Textarea id="pd-app" value={form.approach} onChange={set("approach")} /></FormField>
121
+ <FormField label="Recommended goal" htmlFor="pd-goal"><Input id="pd-goal" value={form.goal} onChange={set("goal")} /></FormField>
122
+ <FormField label="Priority" htmlFor="pd-pri"><Input id="pd-pri" type="number" min={1} value={form.priority} onChange={set("priority")} /></FormField>
123
+ <FormField label="Confidence" htmlFor="pd-conf">
124
+ <select id="pd-conf" className="h-9 rounded-md border bg-background px-2" value={form.confidence} onChange={set("confidence")}>{["low", "medium", "high"].map((c) => <option key={c} value={c}>{c}</option>)}</select>
125
+ </FormField>
126
+ {save.error ? <ApiError error={save.error} title="Could not save the persona" /> : null}
127
+ </>
128
+ ) : null}
129
+ </FormSheet>
130
+ <ConfirmSpendDialog
131
+ open={archiving}
132
+ tier="destructive"
133
+ title={`Archive ${title}`}
134
+ description="It drops out of the default list. Existing campaigns keep the persona text."
135
+ affected={1}
136
+ confirmLabel="Archive persona"
137
+ confirming={archive.loading}
138
+ onCancel={() => setArchiving(false)}
139
+ onConfirm={() => void archive.mutate(undefined)}
140
+ />
141
+ </>
142
+ }
143
+ onClose={onClose}
144
+ onExpand={onExpand}
145
+ onCollapse={onCollapse}
146
+ tabs={[
147
+ {
148
+ id: "details",
149
+ label: "Details",
150
+ content: (
151
+ <div className="space-y-4">
152
+ {[pin.error, archive.error].map((e, i) => (e ? <ApiError key={i} error={e} /> : null))}
153
+ <KeyValueList columns={2} items={fieldItems(p)} />
154
+ </div>
155
+ ),
156
+ },
157
+ {
158
+ id: "campaigns",
159
+ label: "Campaigns",
160
+ count: targeting.length,
161
+ content: (
162
+ <SectionCard title="Campaigns targeting this persona">
163
+ {targeting.length === 0 ? (
164
+ <p className="text-sm text-muted-foreground">No campaign targets this persona yet.</p>
165
+ ) : (
166
+ <ul className="divide-y">
167
+ {targeting.map((c) => (
168
+ <li key={c.id} className="flex items-center justify-between py-2 text-sm">
169
+ <Button size="sm" variant="ghost" disabled={!onOpenCampaign} onClick={() => onOpenCampaign?.(c)}>{c.name}</Button>
170
+ <StatusPill tone="neutral">{c.status ?? "draft"}</StatusPill>
171
+ </li>
172
+ ))}
173
+ </ul>
174
+ )}
175
+ </SectionCard>
176
+ ),
177
+ },
178
+ ]}
179
+ />
180
+ );
181
+ }
@@ -19,13 +19,14 @@
19
19
  import { useState } from "react";
20
20
  import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Textarea } from "@graph8/sdk/ui";
21
21
  import { ApiError } from "@/g8/components/api-error";
22
+ import { FirstNotice } from "@/g8/components/paging-notices";
22
23
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
23
24
  import { EmptyState } from "@/g8/components/empty-state";
24
25
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
25
26
  import { useG8Query } from "@/g8/hooks/use-g8-query";
26
27
  import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
27
28
  import { useCompanyProfile } from "../hooks/use-company-profile";
28
- import { CREDIT_ESTIMATES, lines, listOf, statusClass, text } from "../lib/campaigns-utils";
29
+ import { CREDIT_ESTIMATES, lines, listOf, personaFormOf, personaPatch, statusClass, text } from "../lib/campaigns-utils";
29
30
  import { FormField } from "./form-field";
30
31
 
31
32
  export interface PersonaEditorProps {
@@ -33,6 +34,10 @@ export interface PersonaEditorProps {
33
34
  websiteUrl?: string;
34
35
  /** Called after any change, so a wizard can refresh its persona choices. */
35
36
  onChange?: () => void;
37
+ /** Called when a persona's title is clicked (a screen opens its detail). Without it the title is plain text. */
38
+ onOpen?: (persona: { id: string; title?: string | null }) => void;
39
+ /** Change it to reload the list (the detail edited or archived a persona). */
40
+ refreshKey?: number;
36
41
  className?: string;
37
42
  }
38
43
 
@@ -61,16 +66,14 @@ interface Suggestion {
61
66
  interface PersonaForm { title: string; why: string; signals: string; approach: string; goal: string; priority: string; confidence: string }
62
67
  const emptyForm: PersonaForm = { title: "", why: "", signals: "", approach: "", goal: "", priority: "1", confidence: "medium" };
63
68
 
64
- function formOf(p: PersonaRow): PersonaForm {
65
- return {
66
- title: p.title ?? "", why: p.why_target ?? "", approach: p.campaign_approach ?? "", goal: p.recommended_goal ?? "",
67
- priority: String(p.priority ?? 1), confidence: p.confidence ?? "medium", signals: (p.key_signals ?? []).map((s) => text(s)).filter(Boolean).join("\n"),
68
- };
69
- }
69
+ const formOf = (p: PersonaRow): PersonaForm => personaFormOf(p as Record<string, any>);
70
+
71
+ /** The persona API has no paging: a full read is announced under the table. */
72
+ const PERSONA_LIMIT = 100;
70
73
 
71
- export function PersonaEditor({ websiteUrl, onChange, className }: PersonaEditorProps) {
74
+ export function PersonaEditor({ websiteUrl, onChange, onOpen, refreshKey, className }: PersonaEditorProps) {
72
75
  const profile = useCompanyProfile(websiteUrl);
73
- const personas = useG8Query((signal) => g8Get("/personas", { limit: 100 }, { signal }), []);
76
+ const personas = useG8Query((signal) => g8Get("/personas", { limit: PERSONA_LIMIT }, { signal }), [refreshKey]);
74
77
  const [editing, setEditing] = useState<PersonaRow | "new" | null>(null);
75
78
  const [form, setForm] = useState<PersonaForm>(emptyForm);
76
79
  const [archiving, setArchiving] = useState<PersonaRow | null>(null);
@@ -81,11 +84,15 @@ export function PersonaEditor({ websiteUrl, onChange, className }: PersonaEditor
81
84
 
82
85
  const done = () => { void personas.refetch(); onChange?.(); };
83
86
  const save = useG8Mutation(async () => {
87
+ if (editing && editing !== "new") {
88
+ const patch = personaPatch(editing as Record<string, any>, form);
89
+ return Object.keys(patch).length > 0 ? g8Patch(`/personas/${editing.id}`, patch) : undefined; // nothing changed: no request
90
+ }
84
91
  const body = {
85
92
  title: form.title.trim(), why_target: form.why, key_signals: lines(form.signals), campaign_approach: form.approach,
86
93
  recommended_goal: form.goal, priority: Number(form.priority) || 1, confidence: form.confidence,
87
94
  };
88
- return editing && editing !== "new" ? g8Patch(`/personas/${editing.id}`, body) : g8Post("/personas", { ...body, website_url: profile.websiteUrl });
95
+ return g8Post("/personas", { ...body, website_url: profile.websiteUrl });
89
96
  }, { onSuccess: () => { setEditing(null); done(); } });
90
97
  const archive = useG8Mutation(async (p: PersonaRow) => {
91
98
  setBusyId(p.id);
@@ -143,7 +150,7 @@ export function PersonaEditor({ websiteUrl, onChange, className }: PersonaEditor
143
150
  {personas.loading && !personas.data ? <TableRow><TableCell colSpan={5}><Skeleton className="h-6 w-full" /></TableCell></TableRow> : null}
144
151
  {rows.map((p) => (
145
152
  <TableRow key={p.id}>
146
- <TableCell className="font-medium">{p.title}</TableCell>
153
+ <TableCell className="font-medium">{onOpen ? <button type="button" className="text-left hover:underline" aria-label={`Open ${p.title}`} onClick={() => onOpen(p)}>{p.title}</button> : p.title}</TableCell>
147
154
  <TableCell className="max-w-md truncate text-muted-foreground">{p.why_target}</TableCell>
148
155
  <TableCell>{p.confidence}</TableCell>
149
156
  <TableCell><span className={`rounded px-2 py-0.5 text-xs ${statusClass(p.status)}`}>{p.status ?? "active"}</span></TableCell>
@@ -155,6 +162,7 @@ export function PersonaEditor({ websiteUrl, onChange, className }: PersonaEditor
155
162
  ))}
156
163
  </TableBody>
157
164
  </Table>
165
+ <FirstNotice shown={rows.length} limit={PERSONA_LIMIT} noun="personas" />
158
166
  {!personas.loading && !personas.error && rows.length === 0 ? <EmptyState title="No personas yet" description="Add the roles you sell to, or let AI recommend them." /> : null}
159
167
 
160
168
  <Dialog open={editing !== null} onOpenChange={(o) => { if (!o) setEditing(null); }}>
@@ -2,8 +2,13 @@
2
2
 
3
3
  /** Types and helpers shared by the campaign components. Payloads are open-shaped: every field is optional. */
4
4
 
5
+ import { DEFAULT_BRAND } from "@/g8/hooks/use-brand";
6
+
5
7
  /** Where the person manages mailboxes. Google and Microsoft OAuth consent can only run in the graph8 app. */
6
- export const GRAPH8_APP_MAILBOXES_URL = "https://app.graph8.com/studio/settings?tab=purchase&view=mailboxes";
8
+ /** The path on the platform's web app; components resolve it against the brand (`brandUrl(useBrand(), ...)`). */
9
+ export const GRAPH8_APP_MAILBOXES_PATH = "/studio/settings?tab=purchase&view=mailboxes";
10
+ /** The default (graph8) URL. Kept for existing imports; prefer GRAPH8_APP_MAILBOXES_PATH with useBrand. */
11
+ export const GRAPH8_APP_MAILBOXES_URL = `${DEFAULT_BRAND.appUrl}${GRAPH8_APP_MAILBOXES_PATH}`;
7
12
 
8
13
  /**
9
14
  * Rough credit estimates shown in ConfirmSpendDialog. The API has no price read and no balance read, so these
@@ -93,10 +98,10 @@ export function commaList(value: string): string[] {
93
98
 
94
99
  export function statusClass(status: string | null | undefined): string {
95
100
  const s = (status ?? "").toLowerCase();
96
- if (["active", "live", "completed", "approved", "complete", "done"].includes(s)) return "bg-green-100 text-green-800";
97
- if (["draft", "pending", "running", "generating", "in_progress", "copy_in_progress", "scheduling"].includes(s)) return "bg-amber-100 text-amber-800";
98
- if (s) return "bg-red-100 text-red-800";
99
- return "bg-gray-100 text-gray-700";
101
+ if (["active", "live", "completed", "approved", "complete", "done"].includes(s)) return "bg-positive text-positive-foreground";
102
+ if (["draft", "pending", "running", "generating", "in_progress", "copy_in_progress", "scheduling"].includes(s)) return "bg-warning text-warning-foreground";
103
+ if (s) return "bg-error text-error-foreground";
104
+ return "bg-muted text-foreground";
100
105
  }
101
106
 
102
107
  /** Timestamps are shown in UTC so the same value reads the same everywhere. */
@@ -150,6 +155,134 @@ export function audienceOf(envelope: unknown): { listId: string | null; name: st
150
155
  return { listId: id == null || id === "" ? null : String(id), name: text(a.name) || text(a.title), size };
151
156
  }
152
157
 
158
+ /** Document types that hold the sequence itself, not copy. The launch counts every other completed document as copy. */
159
+ export const NON_COPY_FILE_TYPES = new Set(["sequence", "step_catalog"]);
160
+
161
+ /** The email copy document a launch reads, and where the generator files it. */
162
+ export const EMAILS_DOCUMENT = { file_type: "emails", display_name: "Emails", folder_path: "campaign_copy/channels/email" } as const;
163
+
164
+ const GENERATING = new Set(["generating", "pending", "running", "in_progress", "queued", "processing", "regenerating"]);
165
+
166
+ export interface CopyState {
167
+ /** False when the payload has no `documents` list (an older API): readiness is then unknown, not missing. */
168
+ known: boolean;
169
+ /** At least one completed copy document: the launch's server-side check passes. */
170
+ ready: boolean;
171
+ /** A copy document is still being written, or the campaign is still in `copy_in_progress`. */
172
+ generating: boolean;
173
+ /** The existing email copy document, so writing the copy reuses it instead of creating a second one. */
174
+ emailsDocId: string | null;
175
+ }
176
+
177
+ /**
178
+ * Copy readiness from a GET /campaigns/{id}/full payload. Mirrors the launch's own check: a launch is refused
179
+ * (400 "no completed copy documents") unless some document other than the sequence and step catalog is completed.
180
+ */
181
+ export function copyStateOf(envelope: unknown): CopyState {
182
+ const d = objectOf(envelope);
183
+ if (!Array.isArray(d.documents)) return { known: false, ready: false, generating: false, emailsDocId: null };
184
+ const copy = (d.documents as CampaignDocRow[]).filter((doc) => doc && !NON_COPY_FILE_TYPES.has(text(doc.file_type)));
185
+ const ready = copy.some((doc) => text(doc.status).toLowerCase() === "completed");
186
+ const generating = !ready && (copy.some((doc) => GENERATING.has(text(doc.status).toLowerCase())) || text(d.status) === "copy_in_progress");
187
+ const emails = copy.find((doc) => doc.file_type === EMAILS_DOCUMENT.file_type);
188
+ return { known: true, ready, generating, emailsDocId: emails ? String(emails.id) : null };
189
+ }
190
+
191
+ /** New mailboxes send nothing for their first 14 days (the sequencer's warm-up hold, on by default). */
192
+ export const WARMUP_HOLD_DAYS = 14;
193
+
194
+ /** When a mailbox added less than 14 days ago may start sending, or null when it is past the hold (or unknown). */
195
+ export function warmupHoldUntil(createdAt: string | null | undefined, now: Date = new Date()): Date | null {
196
+ if (!createdAt) return null;
197
+ const created = new Date(createdAt.includes("T") ? createdAt : createdAt.replace(" ", "T"));
198
+ if (Number.isNaN(created.getTime())) return null;
199
+ const until = new Date(created.getTime() + WARMUP_HOLD_DAYS * 24 * 60 * 60 * 1000);
200
+ return until > now ? until : null;
201
+ }
202
+
153
203
  export function byteLength(s: string): number {
154
204
  return typeof Blob !== "undefined" ? new Blob([s]).size : s.length;
155
205
  }
206
+
207
+ const HIDDEN_FIELDS = /password|secret|token|credential/i;
208
+
209
+ /** Every readable field of an open-shaped record as label/value pairs (secrets are never shown). */
210
+ export function fieldItems(obj: Record<string, unknown>): Array<{ label: string; value: string }> {
211
+ return Object.entries(obj)
212
+ .filter(([k, v]) => !HIDDEN_FIELDS.test(k) && v !== null && v !== undefined && v !== "")
213
+ .map(([k, v]) => {
214
+ const label = k.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
215
+ if (typeof v === "string" && /_at$|date$/.test(k) && !Number.isNaN(new Date(v).getTime())) return { label, value: formatDate(v) };
216
+ if (typeof v === "boolean") return { label, value: v ? "Yes" : "No" };
217
+ if (Array.isArray(v) && v.every((x) => typeof x === "string")) return { label, value: v.join(", ") };
218
+ if (typeof v === "object") return { label, value: JSON.stringify(v) };
219
+ return { label, value: String(v) };
220
+ });
221
+ }
222
+
223
+ const isTextItem = (s: unknown): boolean => typeof s === "string" || typeof s === "number";
224
+
225
+ /**
226
+ * The stored list after the person edited its text lines: items the text box cannot show or edit (objects, nested
227
+ * lists) stay exactly where they were; the text items take the edited lines in order; extra lines go at the end.
228
+ */
229
+ function mergeList(original: unknown, edited: string[]): unknown[] {
230
+ const out: unknown[] = [];
231
+ let next = 0;
232
+ for (const item of Array.isArray(original) ? original : []) {
233
+ if (!isTextItem(item)) out.push(item);
234
+ else if (next < edited.length) out.push(edited[next++]);
235
+ }
236
+ return [...out, ...edited.slice(next)];
237
+ }
238
+
239
+ export interface IcpFormValues { name: string; description: string; industries: string; size: string; regions: string; signals: string }
240
+ export interface PersonaFormValues { title: string; why: string; signals: string; approach: string; goal: string; priority: string; confidence: string }
241
+
242
+ const csv = (v: unknown): string => (Array.isArray(v) ? v.join(", ") : text(v));
243
+ const textLines = (list: unknown): string => (Array.isArray(list) ? list.map((s) => text(s)).filter(Boolean).join("\n") : "");
244
+
245
+ /** The edit form for a stored ICP. */
246
+ export function icpFormOf(icp: Record<string, any>): IcpFormValues {
247
+ const f = (icp.firmographics ?? {}) as Record<string, unknown>;
248
+ return { name: icp.name ?? "", description: icp.description ?? "", industries: csv(f.industries), size: text(f.company_size), regions: csv(f.regions), signals: textLines(icp.buying_signals) };
249
+ }
250
+
251
+ /**
252
+ * PATCH body for an ICP edit: only what the person changed. The API replaces each key it receives, so an untouched
253
+ * list or object is never re-sent (structured buying signals or firmographics the form cannot show survive), and
254
+ * an edited signals list keeps the items the text box could not show.
255
+ */
256
+ export function icpPatch(icp: Record<string, any>, form: IcpFormValues): Record<string, unknown> {
257
+ const before = icpFormOf(icp);
258
+ const out: Record<string, unknown> = {};
259
+ if (form.name.trim() !== before.name.trim()) out.name = form.name.trim();
260
+ if (form.description !== before.description) out.description = form.description;
261
+ const firmographics: Record<string, unknown> = { ...(icp.firmographics ?? {}) };
262
+ let touched = false;
263
+ if (form.industries !== before.industries) { firmographics.industries = commaList(form.industries); touched = true; }
264
+ if (form.size !== before.size) { firmographics.company_size = form.size; touched = true; }
265
+ if (form.regions !== before.regions) { firmographics.regions = commaList(form.regions); touched = true; }
266
+ if (touched) out.firmographics = firmographics;
267
+ if (form.signals !== before.signals) out.buying_signals = mergeList(icp.buying_signals, lines(form.signals));
268
+ return out;
269
+ }
270
+
271
+ /** The edit form for a stored persona. */
272
+ export function personaFormOf(p: Record<string, any>): PersonaFormValues {
273
+ return { title: p.title ?? "", why: p.why_target ?? "", approach: p.campaign_approach ?? "", goal: p.recommended_goal ?? "", priority: String(p.priority ?? 1), confidence: p.confidence ?? "medium", signals: textLines(p.key_signals) };
274
+ }
275
+
276
+ /** PATCH body for a persona edit: only the changed keys (an untouched `key_signals` list is never re-sent). */
277
+ export function personaPatch(p: Record<string, any>, form: PersonaFormValues): Record<string, unknown> {
278
+ const before = personaFormOf(p);
279
+ const out: Record<string, unknown> = {};
280
+ if (form.title.trim() !== before.title.trim()) out.title = form.title.trim();
281
+ if (form.why !== before.why) out.why_target = form.why;
282
+ if (form.signals !== before.signals) out.key_signals = mergeList(p.key_signals, lines(form.signals));
283
+ if (form.approach !== before.approach) out.campaign_approach = form.approach;
284
+ if (form.goal !== before.goal) out.recommended_goal = form.goal;
285
+ if (form.priority !== before.priority) out.priority = Number(form.priority) || 1;
286
+ if (form.confidence !== before.confidence) out.confidence = form.confidence;
287
+ return out;
288
+ }
@@ -5,7 +5,10 @@
5
5
  * first. Readiness needs a non-empty audience, completed copy, at least one complete step and an ACTIVE
6
6
  * email mailbox (Google and Microsoft mailboxes are connected by the person in the graph8 app). An empty body
7
7
  * auto-picks the first active mailbox; pass `sender_mailbox_ids` to choose. Read the state with
8
- * `getCampaignFull` (plain `getCampaign` omits the audience): `is_launched` means do not launch again. If
8
+ * `getCampaignFull` (plain `getCampaign` omits the audience): `is_launched` (true only for a live launch, not a
9
+ * failed attempt) means do not launch again; copy is ready when a document other than `sequence` and
10
+ * `step_catalog` has `status: "completed"` (the launch refuses with 400 otherwise). The deliverability verdict is
11
+ * `safe` (older responses: `safe_to_send`). If
9
12
  * launch fails with 502 `draft_created_activation_failed` or `activation_state_uncertain`, a sequence may
10
13
  * exist: check it before retrying, never repeat blindly.
11
14
  */
@@ -17,13 +20,23 @@ export async function attachAndLaunch(campaignId: string, listId: number, sender
17
20
  await g8.api.gtmCampaigns.attachAudienceToCampaign({ path: { campaign_id: campaignId }, body: { list_id: String(listId) } });
18
21
 
19
22
  const verdict = await g8.api.deliverability.getSafeToSend();
20
- const safe = ((verdict.data ?? {}) as { safe_to_send?: boolean }).safe_to_send;
23
+ const v = (verdict.data ?? {}) as { safe?: boolean; safe_to_send?: boolean };
24
+ const safe = v.safe ?? v.safe_to_send;
21
25
  if (safe === false) return { launched: false as const, reason: "deliverability says do not send" };
22
26
 
23
27
  const full = await g8.api.gtmCampaigns.getCampaignFull({ path: { campaign_id: campaignId } });
24
- const state = (full.data ?? {}) as { is_launched?: boolean; audience?: { item_count?: number }; sequence?: { steps?: unknown[] } };
28
+ const state = (full.data ?? {}) as {
29
+ is_launched?: boolean;
30
+ audience?: { item_count?: number };
31
+ sequence?: { steps?: unknown[] };
32
+ documents?: Array<{ file_type?: string | null; status?: string | null }>;
33
+ };
25
34
  if (state.is_launched) return { launched: false as const, reason: "already launched" };
26
35
  if (!state.sequence?.steps?.length) return { launched: false as const, reason: "the campaign has no steps" };
36
+ const copyReady = (state.documents ?? []).some(
37
+ (d) => d.status === "completed" && !["sequence", "step_catalog"].includes(String(d.file_type)),
38
+ );
39
+ if (!copyReady) return { launched: false as const, reason: "the campaign has no completed copy yet" };
27
40
 
28
41
  const launched = await g8.api.gtmCampaigns.launchCampaign({
29
42
  path: { campaign_id: campaignId },
@@ -3,8 +3,12 @@
3
3
  * @when Starting a campaign from the company profile. `generateCampaignIdeasAsync` is BILLABLE (it scales with
4
4
  * `num_ideas`): a human approves the spend first. It returns a `task_id`; there are no streams, so poll
5
5
  * `getCampaignIdeasTaskStatus` every 2 to 3 seconds until `completed` or `failed`. `createCampaign` is tier
6
- * `external` in the contract but contacts nobody; with `auto_generate_documents: true` it also spends credits
7
- * writing the documents. Store the returned campaign id at once: a retry after a later failure must reuse it.
6
+ * `external` in the contract but contacts nobody; with `auto_generate_documents: true` (the default here) it also
7
+ * spends credits writing the documents. Keep it on: a campaign with no completed copy document is refused at launch
8
+ * (400 "no completed copy documents"). Pass `autoGenerateDocuments: false` only when you write the copy yourself
9
+ * (`createCampaignDocument` with `status: "completed"`). `generation_status: "failed_to_dispatch"` in the response
10
+ * means the writing never started: regenerate the documents before launching. Store the returned campaign id at
11
+ * once: a retry after a later failure must reuse it.
8
12
  */
9
13
  import { g8 } from "@graph8/sdk";
10
14
 
@@ -34,7 +38,7 @@ export async function generateIdeas(websiteUrl: string, goal: "awareness" | "con
34
38
  throw new Error(`idea task ${taskId} did not finish in time`);
35
39
  }
36
40
 
37
- export async function createCampaignFromIdea(idea: Idea, listId?: number) {
41
+ export async function createCampaignFromIdea(idea: Idea, listId?: number, autoGenerateDocuments = true) {
38
42
  const created = await g8.api.gtmCampaigns.createCampaign({
39
43
  body: {
40
44
  name: idea.campaign_name ?? "New campaign",
@@ -44,7 +48,7 @@ export async function createCampaignFromIdea(idea: Idea, listId?: number) {
44
48
  brief: idea.why_this_works,
45
49
  target_channels: ["email"],
46
50
  audience_list_id: listId == null ? undefined : String(listId),
47
- auto_generate_documents: false,
51
+ auto_generate_documents: autoGenerateDocuments,
48
52
  },
49
53
  });
50
54
  return String(created.data?.id);
@@ -5,6 +5,14 @@ the `dialer` block (the optional "Real calls" tab reuses its `CallLog`).
5
5
 
6
6
  Install: `npx @graph8/sdk add coach` (installs `foundation`, `lists`, `crm` and `dialer` first).
7
7
 
8
+ ## Screens
9
+
10
+ `CoachScreen` (`components/coach-screen.tsx`) needs no props. **Practice calls**: stats (calls, average and best score,
11
+ weakest stage), a feed with the first tip per call, scorecard / recording / transcript detail (`?id=`), delete (confirmed,
12
+ hide with `allowDelete={false}`), and a New practice call form (adds the row; the live conversation runs in graph8).
13
+ **Leaderboard** ranks reps. **Real calls** shows the rep's dialer calls once a rep email is set; managers type a rep
14
+ email in the toolbar (needs a manager key). Slots: `toolbarActions`, `renderRowActions`, `renderDetailActions`, `extraDetailTabs`.
15
+
8
16
  ## What you get
9
17
 
10
18
  | Component | Use it for | Embed props |
@@ -12,6 +20,8 @@ Install: `npx @graph8/sdk add coach` (installs `foundation`, `lists`, `crm` and
12
20
  | `CallScorecard` | One graded call: overall score, stages with checklist verdicts, strengths, improvements, recording, transcript | `callId`, `allowDelete`, `onDeleted` |
13
21
  | `CoachingFeed` | Practice calls newest first with the first tip, a leaderboard tab, optional real-calls tab | `userEmail`, `onOpenCall`, `allowCreate`, `showDialerCalls`, `allowDelete`, `pageSize` |
14
22
 
23
+ Every detail opens as a full page with `&view=full` (the Open full page button).
24
+
15
25
  ## The flow, with the ids that pass between steps
16
26
 
17
27
  1. **Feed**: `GET /sales-coach/calls?email=` returns the calls (newest first) with `scores` and transcript
@@ -55,6 +55,20 @@
55
55
  "read",
56
56
  "write"
57
57
  ]
58
+ },
59
+ {
60
+ "name": "CoachScreen",
61
+ "file": "components/coach-screen.tsx",
62
+ "purpose": "Finished sales coach page: practice calls with scorecards, leaderboard, real calls and a new-call form.",
63
+ "operations": [
64
+ "create_call_sales_coach_calls_post",
65
+ "get_sales_coach_leaderboard_sales_coach_leaderboard_get",
66
+ "list_calls_sales_coach_calls_get"
67
+ ],
68
+ "tiers": [
69
+ "read",
70
+ "write"
71
+ ]
58
72
  }
59
73
  ],
60
74
  "hooks": [],
@@ -120,6 +134,7 @@
120
134
  }
121
135
  ],
122
136
  "peerDependencies": {
123
- "react": ">=18"
137
+ "react": ">=18",
138
+ "lucide-react": ">=0.400.0"
124
139
  }
125
140
  }
@@ -34,7 +34,7 @@ export interface CallScorecardProps {
34
34
  className?: string;
35
35
  }
36
36
 
37
- const VERDICT_STYLE: Record<string, string> = { met: "text-green-700", partial: "text-amber-600", missed: "text-destructive" };
37
+ const VERDICT_STYLE: Record<string, string> = { met: "text-positive-foreground", partial: "text-warning-foreground", missed: "text-destructive" };
38
38
 
39
39
  export function CallScorecard({ callId, onDeleted, allowDelete = true, className }: CallScorecardProps) {
40
40
  const [audio, setAudio] = useState<{ callId: string; url: string } | null>(null);
@@ -67,13 +67,13 @@ export function CallScorecard({ callId, onDeleted, allowDelete = true, className
67
67
  <div className={`flex flex-col gap-4 ${className ?? ""}`}>
68
68
  <header className="flex flex-wrap items-start justify-between gap-3">
69
69
  <div>
70
- <h2 className="text-lg font-semibold">{row.prospect_name || "Practice call"}{row.prospect_company ? ` at ${row.prospect_company}` : ""}</h2>
70
+ <h2 className="text-lg font-medium">{row.prospect_name || "Practice call"}{row.prospect_company ? ` at ${row.prospect_company}` : ""}</h2>
71
71
  <p className="text-sm text-muted-foreground">
72
72
  {[row.user_email, row.call_mode, row.call_status, row.duration_ms ? clock(row.duration_ms) : null, row.date_of_call ? new Date(row.date_of_call).toLocaleDateString() : null].filter(Boolean).join(" - ")}
73
73
  </p>
74
74
  </div>
75
75
  <div className="text-right">
76
- <div data-testid="overall-score" className="text-3xl font-semibold">{card?.scorePct != null ? `${Math.round(card.scorePct)}%` : "-"}</div>
76
+ <div data-testid="overall-score" className="text-3xl font-medium">{card?.scorePct != null ? `${Math.round(card.scorePct)}%` : "-"}</div>
77
77
  <div className="text-xs text-muted-foreground">{card ? "overall" : "not graded yet"}</div>
78
78
  </div>
79
79
  </header>
@@ -94,20 +94,20 @@ export function CallScorecard({ callId, onDeleted, allowDelete = true, className
94
94
  ))}
95
95
  </ul>
96
96
  <div className="grid gap-3 sm:grid-cols-2">
97
- <div><h3 className="mb-1 text-sm font-semibold">Strengths</h3><ul className="list-disc pl-5 text-sm">{card.strengths.map((t, i) => <li key={i}>{t}</li>)}</ul></div>
98
- <div><h3 className="mb-1 text-sm font-semibold">To improve</h3><ul className="list-disc pl-5 text-sm">{card.improvements.map((t, i) => <li key={i}>{t}</li>)}</ul></div>
97
+ <div><h3 className="mb-1 text-sm font-medium">Strengths</h3><ul className="list-disc pl-5 text-sm">{card.strengths.map((t, i) => <li key={i}>{t}</li>)}</ul></div>
98
+ <div><h3 className="mb-1 text-sm font-medium">To improve</h3><ul className="list-disc pl-5 text-sm">{card.improvements.map((t, i) => <li key={i}>{t}</li>)}</ul></div>
99
99
  </div>
100
100
  </>
101
101
  )}
102
102
 
103
103
  <div className="flex flex-col gap-2">
104
- <h3 className="text-sm font-semibold">Recording</h3>
104
+ <h3 className="text-sm font-medium">Recording</h3>
105
105
  {row.audio_url ? (shownAudio ? <audio controls src={shownAudio} className="w-full" /> : <div><Button size="sm" variant="outline" loading={loadAudio.loading} onClick={() => void loadAudio.mutate(callId)}>Load recording</Button></div>) : <p className="text-sm text-muted-foreground">No recording.</p>}
106
106
  {loadAudio.error ? <ApiError error={loadAudio.error} title="Could not load the recording" /> : null}
107
107
  </div>
108
108
 
109
109
  <div className="flex flex-col gap-2">
110
- <h3 className="text-sm font-semibold">Transcript</h3>
110
+ <h3 className="text-sm font-medium">Transcript</h3>
111
111
  {transcript.error ? <ApiError error={transcript.error} title="Could not load the transcript" onRetry={() => void transcript.refetch()} /> : null}
112
112
  <ul className="flex max-h-96 flex-col gap-1 overflow-y-auto" data-testid="transcript">
113
113
  {segments.map((s, i) => (