@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
@@ -7,21 +7,31 @@
7
7
  *
8
8
  * SDK operations, in order:
9
9
  * 1. listEnrichmentProviders GET /enrichment/providers (read) provider choices, graph8-funded vs own key
10
+ * 1b. listWaterfallConfigs GET /enrichment/waterfall/configs?list_id (read) the list's own pipeline for the field, if any:
11
+ * a run reuses it with ITS providers, so the quote and the confirm are priced with those, not the chosen ones.
10
12
  * 2. preflightEnrichment POST /enrichment/preflight (read, free; nothing is written or charged)
11
13
  * { list_id, contact_ids, mode: "waterfall", providers, target_field } -> contact_count, not_in_list,
12
14
  * missing_credentials, warnings, credits_per_record, max_total_credits
13
- * 3. startEnrichment POST /enrichment/enrich (billable) 202 { job_id, status }
14
- * Goes through ConfirmSpendDialog first with `max_total_credits` (or 2 credits x contacts when the
15
- * preflight could not price it). There is no balance read, so this is an estimate of the most it can charge.
16
- * 4. getEnrichmentJob GET /enrichment/jobs/{job_id} (read) polled with useJobPoll (no streams), backoff;
15
+ * After ConfirmSpendDialog (with `max_total_credits`; when the preflight could not price the run the confirm shows NO total
16
+ * and says so, never a guess; there is no balance read, so a quote is the most it can charge):
17
+ * 3. createDefaultWaterfallConfig POST /enrichment/waterfall/configs { list_id, name, field, providers } (billable tier, free)
18
+ * Creates the list's pipeline for that field, or reuses the one it already has (`created: false`; it then runs
19
+ * with ITS providers). The column_id is kept, so a retry after step 4 failed does not create it again.
20
+ * 4. runWaterfallPipeline POST /enrichment/waterfall/enrich { column_id, list_id, record_ids } (billable) 202 { job_id, status }
21
+ * A 202 whose status is "error" (or a 502) means the job never started: shown as a failure, never polled.
22
+ * 5. getEnrichmentJob GET /enrichment/jobs/{job_id} (read) polled with useJobPoll (no streams), backoff;
17
23
  * read once more with ?include_provider_errors=true when nothing was enriched or some records failed.
18
- * 5. cancelEnrichmentJob POST /enrichment/jobs/{job_id}/cancel (write, free; records already done keep their data and charge)
24
+ * 6. cancelEnrichmentJob POST /enrichment/jobs/{job_id}/cancel (write, free; records already done keep their data and charge)
25
+ *
26
+ * When the job ends, every cached `contacts` query is refreshed (and the page mode's ContactTable reloads) so the
27
+ * filled values show. The panel does not use POST /enrichment/enrich (startEnrichment): before the API fix of
28
+ * 2026-10-16 that route dispatched the field name instead of a pipeline and silently enriched nothing.
19
29
  *
20
30
  * The contacts must be on `listId`; ids not on it are skipped and not charged. Locked contacts are enriched
21
31
  * like any other once they sit in a list.
22
32
  */
23
33
 
24
- import { useState } from "react";
34
+ import { useRef, useState } from "react";
25
35
  import { Button } from "@graph8/sdk/ui";
26
36
  import { ApiError } from "@/g8/components/api-error";
27
37
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
@@ -29,19 +39,23 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
29
39
  import { useG8Query } from "@/g8/hooks/use-g8-query";
30
40
  import { useJobPoll } from "@/g8/hooks/use-job-poll";
31
41
  import { g8Get, g8Post } from "@/g8/lib/g8-client";
42
+ import { g8QueryCache } from "@/g8/lib/g8-cache";
32
43
  import { ContactTable } from "@/g8/crm/components/contact-table";
33
44
  import { ListPicker } from "@/g8/lists/components/list-picker";
34
45
  import { plural } from "@/g8/lib/plural";
35
46
  import { useProviders } from "../hooks/use-providers";
36
- import { CREDITS_PER_RECORD_ESTIMATE, TARGET_FIELDS, estimateCredits, isJobTerminal } from "../lib/enrichment-types";
37
- import type { EnrichJob, Preflight, TargetField } from "../lib/enrichment-types";
47
+ import { FIELD_PROVIDERS, TARGET_FIELDS, isJobTerminal, providersForField, quotedCredits } from "../lib/enrichment-types";
48
+ import type { EnrichJob, Preflight, TargetField, WaterfallConfigSummary, WaterfallPipelineResult } from "../lib/enrichment-types";
38
49
  import { useBrand } from "@/g8/hooks/use-brand";
39
50
 
40
51
  export interface BulkEnrichPanelProps {
41
52
  /** With `listId` and `contactIds` it is just the enrichment panel. Omit them and it is a whole page: pick a list, tick contacts, enrich. */
42
53
  listId?: number;
43
54
  contactIds?: number[];
44
- /** Providers tried, in order, when the person has not chosen. Default: prospeo, dropcontact (the API's own default). */
55
+ /**
56
+ * Providers tried, in order, when the person has not chosen. Kept to the providers the field supports. Default: the
57
+ * API's own per field (work email: prospeo, dropcontact; phones: leadmagic, prospeo, apollo).
58
+ */
45
59
  defaultProviders?: string[];
46
60
  defaultField?: TargetField;
47
61
  /** Called when the job reached a final state. */
@@ -51,37 +65,64 @@ export interface BulkEnrichPanelProps {
51
65
  className?: string;
52
66
  }
53
67
 
54
- const DEFAULT_PROVIDERS = ["prospeo", "dropcontact"];
68
+ type StartedRun = { job: EnrichJob; pipeline: WaterfallPipelineResult; asked: string[] };
55
69
 
56
- function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDERS, defaultField = "work_email", onDone, onStarted, className }: BulkEnrichPanelProps & { listId: number; contactIds: number[] }) {
70
+ function BulkEnrichCore({ listId, contactIds, defaultProviders, defaultField = "work_email", onDone, onStarted, className }: BulkEnrichPanelProps & { listId: number; contactIds: number[] }) {
57
71
  const brand = useBrand();
58
72
  const [field, setField] = useState<TargetField>(defaultField);
59
73
  const [chosen, setChosen] = useState<string[] | null>(null);
60
74
  const [confirmOpen, setConfirmOpen] = useState(false);
61
75
  const [jobId, setJobId] = useState<string | null>(null);
76
+ const [run, setRun] = useState<StartedRun | null>(null);
77
+ // The pipeline step 3 created or reused, kept per list/field/providers so a retry does not create it twice.
78
+ const pipelineRef = useRef<{ key: string; pipeline: WaterfallPipelineResult } | null>(null);
62
79
  const catalog = useProviders();
63
80
 
64
81
  const known = new Set(catalog.providers.map((p) => p.provider));
65
- const providers = chosen ?? defaultProviders.filter((p) => known.size === 0 || known.has(p));
82
+ const providers = chosen ?? providersForField(field, defaultProviders).filter((p) => known.size === 0 || known.has(p));
83
+ const offered = catalog.providers.filter((p) => FIELD_PROVIDERS[field].includes(p.provider) && !providers.includes(p.provider));
66
84
  const column = TARGET_FIELDS.find((f) => f.value === field)!;
67
85
  const ids = [...new Set(contactIds)];
68
86
 
87
+ // The list's own pipeline for this field wins over the chosen providers (the API reuses it), so price that one.
88
+ const saved = useG8Query(
89
+ async (signal) => (await g8Get<{ configs: WaterfallConfigSummary[] }>("/enrichment/waterfall/configs", { list_id: listId }, { signal })).data.configs ?? [],
90
+ [listId],
91
+ { enabled: !jobId },
92
+ );
93
+ const existing = saved.data?.find((c) => c.field_to_enrich === column.target && c.providers.length > 0) ?? null;
94
+ const runProviders = existing ? existing.providers : providers;
95
+
69
96
  const preflight = useG8Query(
70
- async (signal) => (await g8Post<Preflight>("/enrichment/preflight", { list_id: listId, contact_ids: ids, mode: "waterfall", providers, target_field: column.target }, { signal })).data,
71
- [listId, ids.join(","), providers.join(","), field],
97
+ async (signal) => (await g8Post<Preflight>("/enrichment/preflight", { list_id: listId, contact_ids: ids, mode: "waterfall", providers: runProviders, target_field: column.target }, { signal })).data,
98
+ [listId, ids.join(","), runProviders.join(","), field],
72
99
  { enabled: ids.length > 0 && !jobId },
73
100
  );
74
101
  const quote = preflight.data;
75
- const credits = estimateCredits(quote, ids.length);
102
+ const credits = quotedCredits(quote); // null: the API could not price this run, so no total is shown
76
103
  const runnable = quote ? quote.contact_count - quote.not_in_list.length : ids.length;
77
104
 
78
- const start = useG8Mutation(() => g8Post<{ job_id: string }>("/enrichment/enrich", { contact_ids: ids, list_id: listId, fields_config: { [column.column]: providers } }), {
79
- onSuccess: (res) => { setConfirmOpen(false); setJobId(res.data.job_id); onStarted?.(res.data.job_id); },
105
+ const start = useG8Mutation(async (): Promise<StartedRun> => {
106
+ const key = `${listId}|${column.target}|${providers.join(",")}`;
107
+ let pipeline = pipelineRef.current?.key === key ? pipelineRef.current.pipeline : null;
108
+ if (!pipeline) {
109
+ pipeline = (await g8Post<WaterfallPipelineResult>("/enrichment/waterfall/configs", { list_id: listId, name: `${column.label} enrichment`, field: column.target, providers })).data;
110
+ pipelineRef.current = { key, pipeline };
111
+ }
112
+ const job = (await g8Post<EnrichJob>("/enrichment/waterfall/enrich", { column_id: pipeline.column_id, list_id: listId, record_ids: ids })).data;
113
+ if (!job?.job_id || String(job.status).toLowerCase() === "error") throw new Error("The enrichment did not start, so nothing was charged. Try again in a minute.");
114
+ return { job, pipeline, asked: providers };
115
+ }, {
116
+ onSuccess: (res) => { setConfirmOpen(false); setRun(res); setJobId(res.job.job_id); onStarted?.(res.job.job_id); },
80
117
  });
81
118
  const job = useJobPoll<EnrichJob>(
82
119
  async (signal) => (await g8Get<EnrichJob>(`/enrichment/jobs/${jobId}`, undefined, { signal })).data,
83
- { enabled: jobId != null, intervalMs: 2000, isTerminal: (j) => isJobTerminal(j.status), onDone: (j) => onDone?.(j) },
120
+ {
121
+ enabled: jobId != null, intervalMs: 2000, isTerminal: (j) => isJobTerminal(j.status),
122
+ onDone: (j) => { void g8QueryCache.invalidate(["contacts"]); onDone?.(j); },
123
+ },
84
124
  );
125
+ const reused = run && !run.pipeline.created && run.pipeline.providers?.length && run.pipeline.providers.join() !== run.asked.join() ? run.pipeline.providers : null;
85
126
  const cancel = useG8Mutation(() => g8Post(`/enrichment/jobs/${jobId}/cancel`, {}));
86
127
  const finished = job.status;
87
128
  const nothingWritten = job.done && ((finished?.successful_enrichments ?? 0) === 0 || (finished?.failed_enrichments ?? 0) > 0);
@@ -106,9 +147,10 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
106
147
  const pct = total ? Math.round(((j?.completed ?? 0) / total) * 100) : 0;
107
148
  return (
108
149
  <div className={`flex flex-col gap-3 ${className ?? ""}`} role="status">
109
- <h3 className="text-base font-semibold">{job.done ? (j?.status === "completed" ? "Enrichment finished" : `Enrichment ${j?.status}`) : "Enriching..."}</h3>
150
+ <h3 className="text-base font-medium">{job.done ? (j?.status === "completed" ? "Enrichment finished" : `Enrichment ${j?.status}`) : "Enriching..."}</h3>
110
151
  <div className="h-2 overflow-hidden rounded bg-muted" aria-label="Progress"><div className="h-full bg-primary transition-all" style={{ width: `${job.done ? 100 : pct}%` }} /></div>
111
152
  <p className="text-sm">{j?.completed ?? 0} of {total} contacts processed{j?.successful_enrichments != null ? `, ${j.successful_enrichments} enriched, ${j.failed_enrichments ?? 0} failed` : ""}.{j?.total_credits_used != null ? ` ${j.total_credits_used} credits used.` : ""}</p>
153
+ {reused ? <p className="text-xs text-muted-foreground">This list already had a {column.label.toLowerCase()} pipeline, so it ran with its providers: {reused.join(" > ")}.</p> : null}
112
154
  {j?.warnings?.map((w) => <p key={w} className="text-xs text-muted-foreground">{w}</p>)}
113
155
  {job.error ? <ApiError error={job.error} title="Could not read the job" onRetry={() => job.start()} /> : null}
114
156
  {errors.data?.length ? (
@@ -119,7 +161,7 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
119
161
  {cancel.error ? <ApiError error={cancel.error} title="Could not cancel" /> : null}
120
162
  <div className="flex gap-2">
121
163
  {!job.done ? <Button variant="outline" size="sm" loading={cancel.loading} onClick={() => void cancel.mutate(undefined)}>Cancel</Button> : null}
122
- {job.done ? <Button size="sm" variant="outline" onClick={() => { setJobId(null); job.stop(); }}>Enrich more</Button> : null}
164
+ {job.done ? <Button size="sm" variant="outline" onClick={() => { setJobId(null); setRun(null); job.stop(); }}>Enrich more</Button> : null}
123
165
  </div>
124
166
  </div>
125
167
  );
@@ -129,7 +171,7 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
129
171
  <div className={`flex flex-col gap-4 ${className ?? ""}`}>
130
172
  <div className="flex flex-wrap items-center gap-3">
131
173
  <label className="flex items-center gap-2 text-sm">Fill
132
- <select aria-label="Field to fill" className="h-9 rounded-md border bg-background px-2" value={field} onChange={(e) => setField(e.target.value as TargetField)}>
174
+ <select aria-label="Field to fill" className="h-9 rounded-md border bg-background px-2" value={field} onChange={(e) => { setField(e.target.value as TargetField); setChosen(null); }}>
133
175
  {TARGET_FIELDS.map((f) => <option key={f.value} value={f.value}>{f.label}</option>)}
134
176
  </select>
135
177
  </label>
@@ -139,7 +181,11 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
139
181
  <fieldset className="flex flex-col gap-1">
140
182
  <legend className="mb-1 text-sm font-medium">Providers, tried in this order</legend>
141
183
  {catalog.error ? <ApiError error={catalog.error} title="Could not load providers" onRetry={() => void catalog.refetch()} /> : null}
142
- {providers.map((p, i) => (
184
+ {existing ? (
185
+ <p className="text-sm text-muted-foreground" data-testid="saved-pipeline">
186
+ This list already has a {column.label.toLowerCase()} pipeline (&quot;{existing.name}&quot;), so a run uses its providers: {existing.providers.join(" > ")}. Delete it under Pipelines to choose others.
187
+ </p>
188
+ ) : providers.map((p, i) => (
143
189
  <div key={p} className="flex items-center gap-2 text-sm">
144
190
  <span className="w-6 text-muted-foreground">{i + 1}.</span><span className="w-32 font-medium">{p}</span>
145
191
  <Button variant="ghost" size="sm" aria-label={`Move ${p} up`} disabled={i === 0} onClick={() => move(p, -1)}>Up</Button>
@@ -148,7 +194,7 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
148
194
  </div>
149
195
  ))}
150
196
  <div className="flex flex-wrap gap-1">
151
- {catalog.providers.filter((p) => !providers.includes(p.provider)).map((p) => (
197
+ {(existing ? [] : offered).map((p) => (
152
198
  <Button key={p.provider} variant="outline" size="sm" onClick={() => toggleProvider(p.provider)}>Add {p.provider}{p.byok_configured ? " (your key)" : p.system_funded ? "" : " (needs a key)"}</Button>
153
199
  ))}
154
200
  </div>
@@ -158,15 +204,15 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
158
204
  {quote ? (
159
205
  <div className="rounded-md border p-3 text-sm" aria-label="Preflight">
160
206
  <p><strong>{runnable.toLocaleString()}</strong> of {quote.contact_count.toLocaleString()} contacts will be enriched{quote.not_in_list.length ? `; ${quote.not_in_list.length} are not on this list and are skipped (not charged)` : ""}{quote.duplicates_removed ? `; ${quote.duplicates_removed} duplicates removed` : ""}.</p>
161
- <p className="mt-1">{quote.max_total_credits != null ? `At most ${quote.max_total_credits.toLocaleString()} credits (${quote.credits_per_record} per record).` : `About ${credits.toLocaleString()} credits (${CREDITS_PER_RECORD_ESTIMATE} per record, the API's flat estimate).`} Records that already have a value or find nothing cost less.</p>
162
- {quote.missing_credentials.length ? <p role="alert" className="mt-1 text-amber-700">No API key for {quote.missing_credentials.join(", ")}: those steps are skipped. Add the keys in {brand.linkLabel} settings.</p> : null}
207
+ <p className="mt-1">{quote.max_total_credits != null ? `At most ${quote.max_total_credits.toLocaleString()} credits (${quote.credits_per_record} per record). Records that already have a value or find nothing cost less.` : `The API could not price this run, so no total is shown. Providers on ${brand.linkLabel}'s key spend credits per record; providers on your own key cost none.`}</p>
208
+ {quote.missing_credentials.length ? <p role="alert" className="mt-1 text-warning-foreground">No API key for {quote.missing_credentials.join(", ")}: those steps are skipped. Add the keys in {brand.linkLabel} settings.</p> : null}
163
209
  {quote.warnings.map((w) => <p key={w} className="mt-1 text-xs text-muted-foreground">{w}</p>)}
164
210
  </div>
165
211
  ) : null}
166
212
  {start.error ? <ApiError error={start.error} title="Could not start the enrichment" /> : null}
167
213
 
168
214
  <div className="flex justify-end">
169
- <Button disabled={ids.length === 0 || providers.length === 0 || preflight.loading || runnable === 0 || Boolean(preflight.error)} onClick={() => setConfirmOpen(true)}>
215
+ <Button disabled={ids.length === 0 || runProviders.length === 0 || preflight.loading || runnable === 0 || Boolean(preflight.error)} onClick={() => setConfirmOpen(true)}>
170
216
  Enrich {plural(runnable, "contact")}
171
217
  </Button>
172
218
  </div>
@@ -175,8 +221,8 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
175
221
  open={confirmOpen}
176
222
  tier="billable"
177
223
  title={`Enrich ${runnable.toLocaleString()} contacts: ${column.label.toLowerCase()}`}
178
- description={`Providers in order: ${providers.join(", ")}. The estimate is the most this run can charge; your balance is not checked here.`}
179
- estimatedCredits={credits}
224
+ description={`Providers in order: ${runProviders.join(", ")}${existing ? ` (the list's saved ${column.label.toLowerCase()} pipeline)` : ""}. ${credits != null ? "The estimate is the most this run can charge; your balance is not checked here." : "The API could not price this run, so no total is shown; your balance is not checked here."}`}
225
+ estimatedCredits={credits ?? undefined}
180
226
  affected={runnable}
181
227
  confirmLabel="Start enrichment"
182
228
  confirming={start.loading}
@@ -194,15 +240,18 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
194
240
  export function BulkEnrichPanel(props: BulkEnrichPanelProps) {
195
241
  const [listId, setListId] = useState<number | undefined>(undefined);
196
242
  const [picked, setPicked] = useState<number[]>([]);
243
+ const [tableVersion, setTableVersion] = useState(0);
197
244
  if (props.listId != null && props.contactIds) return <BulkEnrichCore {...props} listId={props.listId} contactIds={props.contactIds} />;
198
245
  const { listId: _l, contactIds: _c, ...rest } = props;
246
+ // A finished run reloads the table so the filled values show (this also clears the ticked contacts).
247
+ const done = (job: EnrichJob) => { setTableVersion((v) => v + 1); props.onDone?.(job); };
199
248
  return (
200
249
  <div className={`flex flex-col gap-4 ${props.className ?? ""}`}>
201
250
  <ListPicker value={listId} allowCreate={false} onChange={(l) => { setListId(l.id); setPicked([]); }} />
202
251
  {listId != null ? (
203
252
  <>
204
- <ContactTable listId={listId} selectable pageSize={10} onSelectionChange={(ids) => setPicked(ids)} />
205
- <BulkEnrichCore {...rest} className={undefined} listId={listId} contactIds={picked} />
253
+ <ContactTable listId={listId} selectable pageSize={10} refreshKey={tableVersion} onSelectionChange={(ids) => setPicked(ids)} />
254
+ <BulkEnrichCore {...rest} className={undefined} listId={listId} contactIds={picked} onDone={done} />
206
255
  </>
207
256
  ) : <p className="text-sm text-muted-foreground">Choose a list to enrich its contacts.</p>}
208
257
  </div>
@@ -63,9 +63,9 @@ export function classifyVerification(status: string | null | undefined, isValid?
63
63
 
64
64
  const STYLE: Record<VerifyState, string> = {
65
65
  unverified: "bg-muted text-muted-foreground",
66
- valid: "bg-emerald-100 text-emerald-800",
67
- risky: "bg-amber-100 text-amber-800",
68
- invalid: "bg-red-100 text-red-800",
66
+ valid: "bg-positive text-positive-foreground",
67
+ risky: "bg-warning text-warning-foreground",
68
+ invalid: "bg-error text-error-foreground",
69
69
  unverifiable: "bg-muted text-muted-foreground",
70
70
  };
71
71
  const LABEL: Record<VerifyState, string> = { unverified: "Unverified", valid: "Valid", risky: "Risky", invalid: "Invalid", unverifiable: "Couldn't verify" };
@@ -138,7 +138,7 @@ export function EnrichButton({ kind, lookup, record, recordLoaded = false, label
138
138
  <li key={k} className="flex items-center gap-2">
139
139
  {target ? <input type="checkbox" aria-label={`Apply ${k}`} checked={ticked.has(k)} onChange={() => setTicked((t) => { const n = new Set(t); if (!n.delete(k)) n.add(k); return n; })} /> : null}
140
140
  <span className="w-36 text-muted-foreground">{k.replace(/_/g, " ")}</span><span className="truncate">{v}</span>
141
- {target?.checked && target.existing[k] && !isMaskedValue(target.existing[k]) && target.existing[k] !== v ? <span className="text-xs text-amber-600">replaces {String(target.existing[k])}</span> : null}
141
+ {target?.checked && target.existing[k] && !isMaskedValue(target.existing[k]) && target.existing[k] !== v ? <span className="text-xs text-warning-foreground">replaces {String(target.existing[k])}</span> : null}
142
142
  </li>
143
143
  ))}
144
144
  {Object.keys(fields).length === 0 ? <li className="text-muted-foreground">The match had no fields to show.</li> : null}
@@ -0,0 +1,93 @@
1
+ "use client";
2
+
3
+ /**
4
+ * EnrichmentActivity: how much enrichment ran, how often it worked, and which fields it filled, for a chosen window.
5
+ * All four reads are free.
6
+ *
7
+ * SDK operations (all read, free):
8
+ * 1. getEnrichmentMetrics GET /enrichment/analytics/metrics?period -> { total_enrichments, success_rate (a percent, 0 to 100) }
9
+ * 2. getEnrichmentTrends GET /enrichment/analytics/trends?period&granularity -> { data_points[{ date, successful_enrichments, failed_enrichments }] }
10
+ * 3. getEnrichmentDailyActivity GET /enrichment/analytics/daily-activity?period -> { activity_points[{ date, enrichments, new_contacts }] }
11
+ * 4. getEnrichmentFieldDistribution GET /enrichment/analytics/field-distribution?period -> { field_types[{ field_type, count, percentage }], total_enrichments }
12
+ *
13
+ * The API counts COMPLETED jobs only (running, paused and failed jobs are not in these numbers), says so here, and the
14
+ * cost it returns is the API's own estimate at a fixed rate rather than credits charged, so no cost is shown.
15
+ * Every panel clears its numbers while a new window loads and when a read fails; a rate with nothing behind it shows
16
+ * a dash, never 0%.
17
+ */
18
+
19
+ import { useState } from "react";
20
+ import type { ReactNode } from "react";
21
+ import { ApiError } from "@/g8/components/api-error";
22
+ import { EmptyState } from "@/g8/components/empty-state";
23
+ import { BarList } from "@/g8/components/screen/bar-list";
24
+ import { SectionCard } from "@/g8/components/screen/section-card";
25
+ import { StatStrip } from "@/g8/components/screen/stat-strip";
26
+ import { TimeSeriesChart } from "@/g8/components/screen/time-series-chart";
27
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
28
+ import { g8Get } from "@/g8/lib/g8-client";
29
+ import { GRANULARITIES, PERIODS } from "../lib/pipeline-types";
30
+ import type { Granularity, Period } from "../lib/pipeline-types";
31
+
32
+ interface Metrics { total_enrichments: number; success_rate: number }
33
+ interface Trend { data_points: Array<{ date: string; total_enrichments: number; successful_enrichments: number; failed_enrichments: number }> }
34
+ interface Daily { activity_points: Array<{ date: string; enrichments: number; new_contacts: number }> }
35
+ interface Distribution { field_types: Array<{ field_type: string; count: number; percentage: number }>; total_enrichments: number }
36
+
37
+ const SELECT = "h-9 rounded-md border bg-background px-2 text-sm";
38
+ const humanField = (t: string) => t.replace(/[_-]+/g, " ").replace(/^./, (c) => c.toUpperCase());
39
+
40
+ function Panel({ title, description, q, empty, children }: { title: string; description?: string; q: { error: unknown; loading: boolean; refetch: () => Promise<unknown> }; empty: boolean; children: ReactNode }) {
41
+ return (
42
+ <SectionCard title={title} description={description}>
43
+ {q.error ? <ApiError error={q.error} title={`Could not load ${title.toLowerCase()}`} onRetry={() => void q.refetch()} /> : null}
44
+ {!q.error && q.loading ? <p className="text-sm text-muted-foreground" role="status">Loading...</p> : null}
45
+ {!q.error && !q.loading && empty ? <EmptyState title="Nothing in this window" description="No completed enrichment jobs fall in these dates." /> : null}
46
+ {!q.error && !q.loading && !empty ? children : null}
47
+ </SectionCard>
48
+ );
49
+ }
50
+
51
+ export function EnrichmentActivity({ className }: { className?: string }) {
52
+ const [period, setPeriod] = useState<Period>("last_30_days");
53
+ const [granularity, setGranularity] = useState<Granularity>("day");
54
+ const metrics = useG8Query(async (signal) => (await g8Get<Metrics>("/enrichment/analytics/metrics", { period }, { signal })).data, [period]);
55
+ const trends = useG8Query(async (signal) => (await g8Get<Trend>("/enrichment/analytics/trends", { period, granularity }, { signal })).data, [period, granularity]);
56
+ const daily = useG8Query(async (signal) => (await g8Get<Daily>("/enrichment/analytics/daily-activity", { period }, { signal })).data, [period]);
57
+ const fields = useG8Query(async (signal) => (await g8Get<Distribution>("/enrichment/analytics/field-distribution", { period }, { signal })).data, [period]);
58
+
59
+ const m = metrics.error || metrics.loading ? undefined : metrics.data;
60
+ const points = trends.error || trends.loading ? [] : trends.data?.data_points ?? [];
61
+ const days = daily.error || daily.loading ? [] : daily.data?.activity_points ?? [];
62
+ const kinds = fields.error || fields.loading ? [] : fields.data?.field_types ?? [];
63
+
64
+ return (
65
+ <div className={`flex flex-col gap-4 ${className ?? ""}`}>
66
+ <div className="flex flex-wrap items-end gap-3">
67
+ <label className="flex flex-col gap-1 text-sm">Window
68
+ <select className={SELECT} value={period} onChange={(e) => setPeriod(e.target.value as Period)}>{PERIODS.map((p) => <option key={p.value} value={p.value}>{p.label}</option>)}</select>
69
+ </label>
70
+ <label className="flex flex-col gap-1 text-sm">Group trends by
71
+ <select className={SELECT} value={granularity} onChange={(e) => setGranularity(e.target.value as Granularity)}>{GRANULARITIES.map((g) => <option key={g} value={g}>{g === "day" ? "Day" : g === "week" ? "Week" : "Month"}</option>)}</select>
72
+ </label>
73
+ <p className="pb-2 text-xs text-muted-foreground">Counts completed jobs only. Running, paused and failed jobs are not included.</p>
74
+ </div>
75
+ <StatStrip loading={metrics.loading} items={[
76
+ { label: "Records enriched", value: m ? m.total_enrichments.toLocaleString() : "-" },
77
+ { label: "Success rate", value: m && m.total_enrichments > 0 ? `${Math.round(m.success_rate)}%` : "-", hint: "Share of records the providers found a value for." },
78
+ ]} />
79
+ {metrics.error ? <ApiError error={metrics.error} title="Could not load the totals" onRetry={() => void metrics.refetch()} /> : null}
80
+ <div className="grid gap-4 lg:grid-cols-2">
81
+ <Panel title="Results over time" description="Records enriched and failed in each period." q={trends} empty={points.length === 0}>
82
+ <TimeSeriesChart kind="bar" stacked xKey="date" data={points} series={[{ key: "successful_enrichments", label: "Enriched", tone: "positive" }, { key: "failed_enrichments", label: "Failed", tone: "negative" }]} />
83
+ </Panel>
84
+ <Panel title="Daily activity" description="Records enriched and new contacts each day." q={daily} empty={days.length === 0}>
85
+ <TimeSeriesChart kind="line" xKey="date" data={days} series={[{ key: "enrichments", label: "Enriched" }, { key: "new_contacts", label: "New contacts", tone: "prior" }]} />
86
+ </Panel>
87
+ </div>
88
+ <Panel title="Fields filled" description="Which fields enrichment filled in this window." q={fields} empty={kinds.length === 0}>
89
+ <BarList items={kinds.map((k) => ({ label: humanField(k.field_type), value: k.count, display: `${k.count.toLocaleString()} (${Math.round(k.percentage)}%)` }))} />
90
+ </Panel>
91
+ </div>
92
+ );
93
+ }
@@ -10,10 +10,15 @@
10
10
  * read once more with ?include_provider_errors=true when nothing was enriched or some records failed
11
11
  * -> { status, total, completed, successful_enrichments, failed_enrichments, total_credits_used, warnings, provider_errors }
12
12
  * 2. cancelEnrichmentJob POST /enrichment/jobs/{job_id}/cancel (write, free; records already done keep their data and charge)
13
+ * 3. pauseEnrichmentJob POST /enrichment/jobs/{job_id}/pause (write, free; stops further charges until resumed)
14
+ * 4. resumeEnrichmentJob POST /enrichment/jobs/{job_id}/resume (billable, ConfirmSpendDialog; carries on with, and charges for, the records not yet reached)
15
+ * The state shown is always the server's: after Pause or Resume the job is read again, never assumed.
13
16
  */
14
17
 
18
+ import { useState } from "react";
15
19
  import { Button } from "@graph8/sdk/ui";
16
20
  import { ApiError } from "@/g8/components/api-error";
21
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
17
22
  import { KeyValueList } from "@/g8/components/screen/key-value-list";
18
23
  import { SectionCard } from "@/g8/components/screen/section-card";
19
24
  import { StatusPill, humanizeStatus, toneForStatus } from "@/g8/components/screen/status-pill";
@@ -37,6 +42,9 @@ export function EnrichmentJobDetail({ jobId, onDone, className }: EnrichmentJobD
37
42
  { intervalMs: 2000, isTerminal: (j) => isJobTerminal(j.status), onDone },
38
43
  );
39
44
  const cancel = useG8Mutation(() => g8Post(`/enrichment/jobs/${jobId}/cancel`, {}));
45
+ const [resuming, setResuming] = useState(false);
46
+ const pause = useG8Mutation(() => g8Post(`/enrichment/jobs/${jobId}/pause`, {}), { onSuccess: () => job.start() });
47
+ const resume = useG8Mutation(() => g8Post(`/enrichment/jobs/${jobId}/resume`, {}), { onSuccess: () => { setResuming(false); job.start(); }, onError: () => setResuming(false) });
40
48
  const j = job.status;
41
49
  const problems = job.done && j != null && ((j.successful_enrichments ?? 0) === 0 || (j.failed_enrichments ?? 0) > 0);
42
50
  const errors = useG8Query(
@@ -45,6 +53,8 @@ export function EnrichmentJobDetail({ jobId, onDone, className }: EnrichmentJobD
45
53
  { enabled: problems },
46
54
  );
47
55
  const total = j?.total ?? 0;
56
+ const state = String(j?.status ?? "").toLowerCase();
57
+ const remaining = Math.max(0, total - (j?.completed ?? 0));
48
58
  const pct = total ? Math.min(100, Math.round(((j?.completed ?? 0) / total) * 100)) : 0;
49
59
 
50
60
  if (job.error && !j) return <ApiError error={job.error} title="Could not read the job" onRetry={() => job.start()} />;
@@ -52,10 +62,15 @@ export function EnrichmentJobDetail({ jobId, onDone, className }: EnrichmentJobD
52
62
  <div className={`flex flex-col gap-4 ${className ?? ""}`} role="status">
53
63
  <div className="flex items-center gap-2">
54
64
  {j ? <StatusPill tone={toneForStatus(j.status)} dot>{humanizeStatus(j.status)}</StatusPill> : <span className="text-sm text-muted-foreground">Loading...</span>}
65
+ {!job.done && state === "paused" ? <Button size="sm" onClick={() => setResuming(true)}>Resume job</Button> : null}
66
+ {!job.done && (state === "running" || state === "pending") ? <Button size="sm" variant="outline" loading={pause.loading} onClick={() => void pause.mutate(undefined)}>Pause job</Button> : null}
55
67
  {!job.done && j ? <Button size="sm" variant="outline" loading={cancel.loading} onClick={() => void cancel.mutate(undefined)}>Cancel job</Button> : null}
56
68
  </div>
57
69
  <div className="h-2 overflow-hidden rounded bg-muted" aria-label="Progress"><div className="h-full bg-primary" style={{ width: `${job.done ? 100 : pct}%` }} /></div>
70
+ {j?.paused_at && state === "paused" ? <p className="text-sm text-muted-foreground">Paused {new Date(j.paused_at).toLocaleString()}. Nothing more is enriched or charged until you resume.</p> : null}
58
71
  {cancel.error ? <ApiError error={cancel.error} title="Could not cancel" /> : null}
72
+ {pause.error ? <ApiError error={pause.error} title="Could not pause" /> : null}
73
+ {resume.error ? <ApiError error={resume.error} title="Could not resume" /> : null}
59
74
  <SectionCard title="Result">
60
75
  <KeyValueList columns={2} items={[
61
76
  { label: "Job id", value: jobId, mono: true, wide: true },
@@ -73,6 +88,9 @@ export function EnrichmentJobDetail({ jobId, onDone, className }: EnrichmentJobD
73
88
  </ul>
74
89
  </SectionCard>
75
90
  ) : null}
91
+ <ConfirmSpendDialog open={resuming} tier="billable" title="Resume this job" affected={remaining}
92
+ description="The job carries on with the records it had not reached and charges for them as it goes. Records already enriched are not charged again. The API does not quote a total for a resume, so no credit figure is shown."
93
+ confirmLabel="Resume" confirming={resume.loading} onConfirm={() => void resume.mutate(undefined)} onCancel={() => setResuming(false)} />
76
94
  </div>
77
95
  );
78
96
  }