@graph8/sdk 0.373.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 (279) hide show
  1. package/README.md +3 -3
  2. package/blocks/_foundation/README.md +47 -0
  3. package/blocks/_foundation/block.json +13 -0
  4. package/blocks/_foundation/components/auth-gate.tsx +363 -0
  5. package/blocks/_foundation/server/auth.ts +815 -0
  6. package/blocks/campaigns/README.md +1 -1
  7. package/blocks/campaigns/block.json +34 -1
  8. package/blocks/campaigns/components/campaign-links.tsx +180 -0
  9. package/blocks/company-intelligence/components/research-run.tsx +6 -2
  10. package/blocks/crm/README.md +19 -0
  11. package/blocks/crm/block.json +205 -6
  12. package/blocks/crm/components/bulk-update.tsx +119 -0
  13. package/blocks/crm/components/contact-documents-tab.tsx +144 -0
  14. package/blocks/crm/components/contact-insights-tab.tsx +118 -0
  15. package/blocks/crm/components/contact-lookup-link.tsx +47 -6
  16. package/blocks/crm/components/contact-row-actions.tsx +43 -0
  17. package/blocks/crm/components/contact-tag-chips.tsx +113 -0
  18. package/blocks/crm/components/contacts-screen.tsx +46 -5
  19. package/blocks/crm/components/owner-assign.tsx +132 -0
  20. package/blocks/crm/components/record-open-links.tsx +79 -0
  21. package/blocks/crm/components/tag-assign.tsx +112 -0
  22. package/blocks/crm/components/tag-manager.tsx +178 -0
  23. package/blocks/crm/components/unlock-info-button.tsx +82 -0
  24. package/blocks/crm/components/verify-email-button.tsx +93 -0
  25. package/blocks/crm/hooks/use-contact-tags.ts +29 -0
  26. package/blocks/crm/lib/record-actions.ts +102 -0
  27. package/blocks/deals/README.md +1 -0
  28. package/blocks/deals/block.json +17 -0
  29. package/blocks/deals/components/create-deal-button.tsx +109 -0
  30. package/blocks/deals/components/deal-detail.tsx +5 -3
  31. package/blocks/dialer/components/call-log-screen.tsx +6 -1
  32. package/blocks/documents/README.md +86 -0
  33. package/blocks/documents/block.json +378 -0
  34. package/blocks/documents/components/account-bar.tsx +48 -0
  35. package/blocks/documents/components/correspondence-form.tsx +95 -0
  36. package/blocks/documents/components/correspondence-import.tsx +106 -0
  37. package/blocks/documents/components/correspondence-panel.tsx +191 -0
  38. package/blocks/documents/components/documents-screen.tsx +76 -0
  39. package/blocks/documents/components/library-document-detail.tsx +175 -0
  40. package/blocks/documents/components/library-document-form.tsx +105 -0
  41. package/blocks/documents/components/library-folder-form.tsx +74 -0
  42. package/blocks/documents/components/library-panel.tsx +235 -0
  43. package/blocks/documents/components/library-upload.tsx +160 -0
  44. package/blocks/documents/components/question-row.tsx +130 -0
  45. package/blocks/documents/components/questionnaire-detail.tsx +197 -0
  46. package/blocks/documents/components/questionnaires-panel.tsx +173 -0
  47. package/blocks/documents/components/record-documents-tab.tsx +109 -0
  48. package/blocks/documents/lib/documents-types.ts +252 -0
  49. package/blocks/documents/lib/download.ts +64 -0
  50. package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
  51. package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
  52. package/blocks/documents/recipes/organize-the-library.ts +25 -0
  53. package/blocks/email-signatures/README.md +123 -0
  54. package/blocks/email-signatures/block.json +341 -0
  55. package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
  56. package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
  57. package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
  58. package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
  59. package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
  60. package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
  61. package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
  62. package/blocks/email-signatures/components/signature-policy.tsx +130 -0
  63. package/blocks/email-signatures/components/signature-preview.tsx +119 -0
  64. package/blocks/email-signatures/components/signature-report.tsx +77 -0
  65. package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
  66. package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
  67. package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
  68. package/blocks/email-signatures/lib/signatures-types.ts +392 -0
  69. package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
  70. package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
  71. package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
  72. package/blocks/enrichment/README.md +22 -4
  73. package/blocks/enrichment/block.json +145 -5
  74. package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
  75. package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
  76. package/blocks/enrichment/components/bulk-enrich-panel.tsx +7 -7
  77. package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
  78. package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
  79. package/blocks/enrichment/components/enrichment-screen.tsx +34 -3
  80. package/blocks/enrichment/components/list-pipelines.tsx +97 -0
  81. package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
  82. package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
  83. package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
  84. package/blocks/enrichment/lib/enrichment-types.ts +12 -0
  85. package/blocks/enrichment/lib/pipeline-types.ts +234 -0
  86. package/blocks/forms/README.md +2 -0
  87. package/blocks/forms/components/forms-screen.tsx +6 -3
  88. package/blocks/inbox/README.md +40 -4
  89. package/blocks/inbox/block.json +130 -8
  90. package/blocks/inbox/components/blocked-senders.tsx +103 -0
  91. package/blocks/inbox/components/compose-message.tsx +193 -0
  92. package/blocks/inbox/components/inbox-screen.tsx +54 -6
  93. package/blocks/inbox/components/inbox-search.tsx +106 -0
  94. package/blocks/inbox/components/tag-manager.tsx +136 -0
  95. package/blocks/inbox/components/thread-actions.tsx +133 -0
  96. package/blocks/inbox/components/thread-view.tsx +2 -0
  97. package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
  98. package/blocks/inbox/lib/inbox-manage.ts +227 -0
  99. package/blocks/newsletter/components/newsletter-screen.tsx +3 -1
  100. package/blocks/pages/block.json +10 -1
  101. package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
  102. package/blocks/programmatic-pages/README.md +82 -0
  103. package/blocks/programmatic-pages/block.json +292 -0
  104. package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
  105. package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
  106. package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
  107. package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
  108. package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
  109. package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
  110. package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
  111. package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
  112. package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
  113. package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
  114. package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
  115. package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
  116. package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
  117. package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
  118. package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
  119. package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
  120. package/blocks/quotes/README.md +25 -1
  121. package/blocks/quotes/block.json +123 -6
  122. package/blocks/quotes/components/manual-products.tsx +144 -0
  123. package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
  124. package/blocks/quotes/components/quote-documents.tsx +94 -0
  125. package/blocks/quotes/components/quote-evidence.tsx +95 -0
  126. package/blocks/quotes/components/quote-list.tsx +23 -5
  127. package/blocks/quotes/components/quote-settings.tsx +184 -0
  128. package/blocks/quotes/components/quote-templates.tsx +189 -0
  129. package/blocks/quotes/components/quote-viewer.tsx +16 -3
  130. package/blocks/quotes/components/quotes-screen.tsx +15 -5
  131. package/blocks/quotes/lib/quote-admin.ts +264 -0
  132. package/blocks/sequencer/README.md +55 -0
  133. package/blocks/sequencer/block.json +172 -5
  134. package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
  135. package/blocks/sequencer/components/sequence-builder.tsx +13 -3
  136. package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
  137. package/blocks/sequencer/components/sequence-detail.tsx +6 -0
  138. package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
  139. package/blocks/sequencer/components/sequence-enrollments.tsx +2 -1
  140. package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
  141. package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
  142. package/blocks/sequencer/components/sequence-templates.tsx +160 -0
  143. package/blocks/sequencer/components/sequences-screen.tsx +36 -6
  144. package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
  145. package/blocks/sequencer/components/step-editor.tsx +10 -1
  146. package/blocks/sequencer/components/step-templates.tsx +224 -0
  147. package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
  148. package/blocks/sequencer/lib/sequencer-utils.ts +4 -0
  149. package/blocks/signals/README.md +39 -0
  150. package/blocks/signals/block.json +287 -0
  151. package/blocks/signals/components/auto-seed-panel.tsx +111 -0
  152. package/blocks/signals/components/listener-form.tsx +184 -0
  153. package/blocks/signals/components/matched-posts.tsx +195 -0
  154. package/blocks/signals/components/post-engagements.tsx +114 -0
  155. package/blocks/signals/components/record-social-tab.tsx +49 -0
  156. package/blocks/signals/components/social-listeners-screen.tsx +250 -0
  157. package/blocks/signals/components/social-settings.tsx +128 -0
  158. package/blocks/signals/components/source-lists.tsx +83 -0
  159. package/blocks/signals/components/top-engagers.tsx +68 -0
  160. package/blocks/signals/components/x-integration.tsx +129 -0
  161. package/blocks/signals/lib/signals-types.ts +228 -0
  162. package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
  163. package/blocks/signals/recipes/spend-against-caps.ts +14 -0
  164. package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
  165. package/blocks/visitors/components/traffic-chart.tsx +3 -2
  166. package/blocks/web-analytics/README.md +102 -0
  167. package/blocks/web-analytics/block.json +292 -0
  168. package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
  169. package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
  170. package/blocks/web-analytics/components/audience-tab.tsx +135 -0
  171. package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
  172. package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
  173. package/blocks/web-analytics/components/overview-tab.tsx +102 -0
  174. package/blocks/web-analytics/components/pages-tab.tsx +148 -0
  175. package/blocks/web-analytics/components/ranked-table.tsx +103 -0
  176. package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
  177. package/blocks/web-analytics/components/report-card.tsx +71 -0
  178. package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
  179. package/blocks/web-analytics/hooks/use-report.ts +48 -0
  180. package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
  181. package/blocks/web-analytics/lib/page-path.ts +14 -0
  182. package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
  183. package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
  184. package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
  185. package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
  186. package/blocks/workbench/README.md +54 -2
  187. package/blocks/workbench/block.json +234 -5
  188. package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
  189. package/blocks/workbench/components/workbench-automation.tsx +36 -0
  190. package/blocks/workbench/components/workbench-connections.tsx +136 -0
  191. package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
  192. package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
  193. package/blocks/workbench/components/workbench-grid.tsx +70 -7
  194. package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
  195. package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
  196. package/blocks/workbench/components/workbench-schedule.tsx +191 -0
  197. package/blocks/workbench/components/workbench-screen.tsx +50 -17
  198. package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
  199. package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
  200. package/blocks/workbench/components/workbench-triggers.tsx +178 -0
  201. package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
  202. package/blocks/workbench/lib/api-status.ts +8 -0
  203. package/blocks/workbench/lib/automation-types.ts +257 -0
  204. package/dist/cli.js +257 -51
  205. package/dist/index.browser.js +1 -1
  206. package/dist/index.browser.js.map +1 -1
  207. package/dist/index.browser.mjs +1 -1
  208. package/dist/index.browser.mjs.map +1 -1
  209. package/dist/index.js +1 -1
  210. package/dist/index.js.map +1 -1
  211. package/dist/index.mjs +1 -1
  212. package/dist/index.mjs.map +1 -1
  213. package/dist/react.browser.js +1 -1
  214. package/dist/react.browser.js.map +1 -1
  215. package/dist/react.browser.mjs +1 -1
  216. package/dist/react.browser.mjs.map +1 -1
  217. package/dist/react.js +1 -1
  218. package/dist/react.js.map +1 -1
  219. package/dist/react.mjs +1 -1
  220. package/dist/react.mjs.map +1 -1
  221. package/dist/types/blocks/auth.d.ts +45 -0
  222. package/dist/types/blocks/create.d.ts +5 -2
  223. package/dist/types/version.d.ts +2 -2
  224. package/docs/api/forms.md +1 -1
  225. package/package.json +1 -1
  226. package/stacks/6sense/pages/contacts.tsx.tmpl +2 -0
  227. package/stacks/6sense.json +20 -0
  228. package/stacks/apollo/pages/calls.tsx.tmpl +9 -9
  229. package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
  230. package/stacks/apollo/pages/contacts.tsx.tmpl +6 -0
  231. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +3 -3
  232. package/stacks/apollo/pages/lists.tsx.tmpl +14 -2
  233. package/stacks/apollo/pages/sequences.tsx.tmpl +8 -2
  234. package/stacks/apollo.json +33 -2
  235. package/stacks/clay/pages/contacts.tsx.tmpl +2 -0
  236. package/stacks/clay/pages/lists.tsx.tmpl +14 -2
  237. package/stacks/gong/pages/calls.tsx.tmpl +9 -9
  238. package/stacks/gong/pages/contacts.tsx.tmpl +2 -0
  239. package/stacks/hootsuite/pages/contacts.tsx.tmpl +2 -0
  240. package/stacks/hootsuite.json +10 -0
  241. package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
  242. package/stacks/hubspot/pages/companies.tsx.tmpl +2 -0
  243. package/stacks/hubspot/pages/contacts.tsx.tmpl +6 -0
  244. package/stacks/hubspot/pages/deals.tsx.tmpl +4 -0
  245. package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
  246. package/stacks/hubspot/pages/inbox.tsx.tmpl +2 -2
  247. package/stacks/hubspot/pages/sequences.tsx.tmpl +8 -2
  248. package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
  249. package/stacks/hubspot.json +45 -2
  250. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +3 -3
  251. package/stacks/intercom.json +10 -0
  252. package/stacks/mailchimp/pages/contacts.tsx.tmpl +2 -0
  253. package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
  254. package/stacks/mailchimp.json +2 -1
  255. package/stacks/outreach/pages/calls.tsx.tmpl +9 -9
  256. package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
  257. package/stacks/outreach/pages/contacts.tsx.tmpl +2 -0
  258. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +3 -3
  259. package/stacks/outreach/pages/sequences.tsx.tmpl +8 -2
  260. package/stacks/outreach.json +13 -2
  261. package/stacks/pipedrive/pages/contacts.tsx.tmpl +2 -0
  262. package/stacks/pipedrive/pages/inbox.tsx.tmpl +2 -2
  263. package/stacks/pipedrive.json +10 -0
  264. package/stacks/salesforce/pages/companies.tsx.tmpl +2 -0
  265. package/stacks/salesforce/pages/contacts.tsx.tmpl +6 -0
  266. package/stacks/salesforce/pages/deals.tsx.tmpl +4 -0
  267. package/stacks/salesforce.json +10 -0
  268. package/stacks/zoominfo/pages/contacts.tsx.tmpl +4 -0
  269. package/stacks/zoominfo/pages/lists.tsx.tmpl +14 -2
  270. package/stacks/zoominfo.json +20 -0
  271. package/styles/styles.css +1 -1
  272. package/templates/auth/auth-route.ts.tmpl +16 -0
  273. package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
  274. package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
  275. package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
  276. package/templates/auth/g8.auth.password.ts.tmpl +18 -0
  277. package/templates/auth/g8.auth.session.ts.tmpl +23 -0
  278. package/templates/auth/middleware.ts.tmpl +14 -0
  279. package/templates/next-app/README.md.tmpl +5 -0
@@ -8,7 +8,10 @@
8
8
  "email verification chip with a one-click verify",
9
9
  "bulk enrichment with a free preflight, credit estimate confirmation, polled progress and cancel",
10
10
  "waterfall pipeline manager with provider order and credential readiness",
11
- "finished EnrichmentScreen page"
11
+ "finished EnrichmentScreen page",
12
+ "list pipelines: templates, build, edit, price (free estimate), run behind a confirm, watch, stop, on/off, delete, run history",
13
+ "automatic enrichment of a list's new records and the workspace-wide switch and merge rule, each confirmed with the real billing effect",
14
+ "pause and resume a running enrichment job; enrichment activity analytics (totals, success rate, trends, fields filled)"
12
15
  ],
13
16
  "dependsOn": [
14
17
  "lists",
@@ -16,27 +19,53 @@
16
19
  ],
17
20
  "operations": [
18
21
  "cancel_enrichment_job_enrichment_jobs__job_id__cancel_post",
22
+ "cancel_list_pipeline_run_enrichment_lists__list_id__pipelines__pipeline_id__cancel_post",
23
+ "create_auto_enrichment_config_enrichment_auto_enrichment_configs_post",
19
24
  "create_default_waterfall_config_enrichment_waterfall_configs_post",
20
25
  "create_list_lists_post",
26
+ "create_list_pipeline_enrichment_lists__list_id__pipelines_post",
27
+ "create_list_pipeline_from_template_enrichment_lists__list_id__pipelines_from_template_post",
28
+ "delete_auto_enrichment_config_enrichment_auto_enrichment_configs__config_id__delete",
21
29
  "delete_contact_contacts__contact_id__delete",
30
+ "delete_list_pipeline_enrichment_lists__list_id__pipelines__pipeline_id__delete",
22
31
  "delete_waterfall_config_enrichment_waterfall_configs__column_id__delete",
32
+ "estimate_list_pipeline_enrichment_lists__list_id__pipelines__pipeline_id__estimate_post",
33
+ "get_auto_enrichment_config_enrichment_auto_enrichment_configs__config_id__get",
34
+ "get_auto_enrichment_settings_enrichment_auto_enrichment_settings_get",
23
35
  "get_company_companies__company_id__get",
24
36
  "get_contact_contacts__contact_id__get",
37
+ "get_enrichment_daily_activity_enrichment_analytics_daily_activity_get",
38
+ "get_enrichment_field_distribution_enrichment_analytics_field_distribution_get",
25
39
  "get_enrichment_job_enrichment_jobs__job_id__get",
40
+ "get_enrichment_metrics_enrichment_analytics_metrics_get",
41
+ "get_enrichment_trends_enrichment_analytics_trends_get",
42
+ "get_pipeline_run_enrichment_lists_pipeline_runs__run_id__get",
26
43
  "list_active_enrichment_jobs_enrichment_active_jobs_get",
44
+ "list_auto_enrichment_configs_enrichment_auto_enrichment_configs_get",
27
45
  "list_companies_companies_get",
28
46
  "list_contact_fields_fields_get",
29
47
  "list_contacts_contacts_get",
30
48
  "list_enrichment_providers_enrichment_providers_get",
49
+ "list_list_pipeline_runs_enrichment_lists__list_id__pipelines__pipeline_id__runs_get",
50
+ "list_list_pipelines_enrichment_lists__list_id__pipelines_get",
31
51
  "list_lists_lists_get",
52
+ "list_pipeline_step_options_enrichment_lists__list_id__pipeline_step_options_get",
53
+ "list_pipeline_templates_enrichment_lists_pipeline_templates_get",
32
54
  "list_waterfall_configs_enrichment_waterfall_configs_get",
33
55
  "lookup_company_enrichment_lookup_company_post",
34
56
  "lookup_person_enrichment_lookup_person_post",
57
+ "pause_enrichment_job_enrichment_jobs__job_id__pause_post",
35
58
  "preflight_enrichment_enrichment_preflight_post",
59
+ "resume_enrichment_job_enrichment_jobs__job_id__resume_post",
60
+ "run_list_pipeline_enrichment_lists__list_id__pipelines__pipeline_id__runs_post",
36
61
  "run_waterfall_pipeline_enrichment_waterfall_enrich_post",
62
+ "set_pipeline_enabled_enrichment_lists__list_id__pipelines__pipeline_id__enabled_post",
37
63
  "start_enrichment_enrichment_enrich_post",
64
+ "update_auto_enrichment_config_enrichment_auto_enrichment_configs__config_id__patch",
65
+ "update_auto_enrichment_settings_enrichment_auto_enrichment_settings__entity_type__put",
38
66
  "update_company_companies__company_id__patch",
39
67
  "update_contact_contacts__contact_id__patch",
68
+ "update_list_pipeline_enrichment_lists__list_id__pipelines__pipeline_id__put",
40
69
  "verify_email_enrichment_verify_email_post"
41
70
  ],
42
71
  "components": [
@@ -113,7 +142,7 @@
113
142
  {
114
143
  "name": "EnrichmentScreen",
115
144
  "file": "components/enrichment-screen.tsx",
116
- "purpose": "Finished enrichment page: enrich a list, one-off lookups and email verification, pipelines and the provider catalog.",
145
+ "purpose": "Finished enrichment page: enrich a list, one-off lookups and email verification, pipelines, list pipelines, auto-enrichment, jobs, activity and the provider catalog.",
117
146
  "operations": [
118
147
  "list_contacts_contacts_get",
119
148
  "list_companies_companies_get",
@@ -126,15 +155,122 @@
126
155
  {
127
156
  "name": "EnrichmentJobDetail",
128
157
  "file": "components/enrichment-job-detail.tsx",
129
- "purpose": "One bulk enrichment job: live progress, counts, credits, warnings, provider errors and cancel.",
158
+ "purpose": "One bulk enrichment job: live progress, counts, credits, warnings, provider errors, cancel, pause and resume (resume is confirmed).",
130
159
  "operations": [
131
160
  "cancel_enrichment_job_enrichment_jobs__job_id__cancel_post",
132
- "get_enrichment_job_enrichment_jobs__job_id__get"
161
+ "get_enrichment_job_enrichment_jobs__job_id__get",
162
+ "pause_enrichment_job_enrichment_jobs__job_id__pause_post",
163
+ "resume_enrichment_job_enrichment_jobs__job_id__resume_post"
164
+ ],
165
+ "tiers": [
166
+ "read",
167
+ "write",
168
+ "billable"
169
+ ]
170
+ },
171
+ {
172
+ "name": "ListPipelines",
173
+ "file": "components/list-pipelines.tsx",
174
+ "purpose": "The pipelines of one list with the open one beside them: templates, new pipeline, open to price, run, watch, stop, edit and delete. The open pipeline is in the URL (?lp=).",
175
+ "operations": [
176
+ "list_list_pipelines_enrichment_lists__list_id__pipelines_get"
177
+ ],
178
+ "tiers": [
179
+ "read"
180
+ ]
181
+ },
182
+ {
183
+ "name": "PipelineDetail",
184
+ "file": "components/pipeline-detail.tsx",
185
+ "purpose": "One list pipeline: price the run for this exact list (free estimate), run it behind a confirm showing that price, poll the run with per-step progress, stop it, turn it on or off, delete it, read its run history.",
186
+ "operations": [
187
+ "estimate_list_pipeline_enrichment_lists__list_id__pipelines__pipeline_id__estimate_post",
188
+ "run_list_pipeline_enrichment_lists__list_id__pipelines__pipeline_id__runs_post",
189
+ "get_pipeline_run_enrichment_lists_pipeline_runs__run_id__get",
190
+ "cancel_list_pipeline_run_enrichment_lists__list_id__pipelines__pipeline_id__cancel_post",
191
+ "list_list_pipeline_runs_enrichment_lists__list_id__pipelines__pipeline_id__runs_get",
192
+ "set_pipeline_enabled_enrichment_lists__list_id__pipelines__pipeline_id__enabled_post",
193
+ "delete_list_pipeline_enrichment_lists__list_id__pipelines__pipeline_id__delete",
194
+ "list_pipeline_step_options_enrichment_lists__list_id__pipeline_step_options_get"
195
+ ],
196
+ "tiers": [
197
+ "read",
198
+ "write",
199
+ "billable",
200
+ "destructive"
201
+ ]
202
+ },
203
+ {
204
+ "name": "PipelineEditor",
205
+ "file": "components/pipeline-editor.tsx",
206
+ "purpose": "Create or edit a list pipeline in a side sheet: pick steps from the configs the list can use, order them, turn steps off, fill-only-empty and skip-recent options. Saving never runs it.",
207
+ "operations": [
208
+ "list_pipeline_step_options_enrichment_lists__list_id__pipeline_step_options_get",
209
+ "create_list_pipeline_enrichment_lists__list_id__pipelines_post",
210
+ "update_list_pipeline_enrichment_lists__list_id__pipelines__pipeline_id__put"
211
+ ],
212
+ "tiers": [
213
+ "read",
214
+ "billable"
215
+ ]
216
+ },
217
+ {
218
+ "name": "PipelineTemplates",
219
+ "file": "components/pipeline-templates.tsx",
220
+ "purpose": "Pick a ready-made pipeline for the list; saved turned off. Adding one the list already has resets it (confirmed).",
221
+ "operations": [
222
+ "list_pipeline_templates_enrichment_lists_pipeline_templates_get",
223
+ "create_list_pipeline_from_template_enrichment_lists__list_id__pipelines_from_template_post"
133
224
  ],
134
225
  "tiers": [
135
226
  "read",
136
227
  "write"
137
228
  ]
229
+ },
230
+ {
231
+ "name": "AutoEnrichmentConfigs",
232
+ "file": "components/auto-enrichment-configs.tsx",
233
+ "purpose": "What a list enriches by itself as records are added: list, add, turn on or off, delete. Every change is confirmed and states that it spends credits with nobody confirming each record.",
234
+ "operations": [
235
+ "list_auto_enrichment_configs_enrichment_auto_enrichment_configs_get",
236
+ "get_auto_enrichment_config_enrichment_auto_enrichment_configs__config_id__get",
237
+ "create_auto_enrichment_config_enrichment_auto_enrichment_configs_post",
238
+ "update_auto_enrichment_config_enrichment_auto_enrichment_configs__config_id__patch",
239
+ "delete_auto_enrichment_config_enrichment_auto_enrichment_configs__config_id__delete",
240
+ "list_pipeline_step_options_enrichment_lists__list_id__pipeline_step_options_get"
241
+ ],
242
+ "tiers": [
243
+ "read",
244
+ "billable",
245
+ "destructive"
246
+ ]
247
+ },
248
+ {
249
+ "name": "AutoEnrichmentSettings",
250
+ "file": "components/auto-enrichment-settings.tsx",
251
+ "purpose": "Workspace-wide automatic enrichment for contacts and companies: on or off and the merge rule (keep edits, never overwrite, matched data wins). Per-field overrides are kept.",
252
+ "operations": [
253
+ "get_auto_enrichment_settings_enrichment_auto_enrichment_settings_get",
254
+ "update_auto_enrichment_settings_enrichment_auto_enrichment_settings__entity_type__put"
255
+ ],
256
+ "tiers": [
257
+ "read",
258
+ "billable"
259
+ ]
260
+ },
261
+ {
262
+ "name": "EnrichmentActivity",
263
+ "file": "components/enrichment-activity.tsx",
264
+ "purpose": "Free analytics for a chosen window: records enriched, success rate, results over time, daily activity and the fields filled.",
265
+ "operations": [
266
+ "get_enrichment_metrics_enrichment_analytics_metrics_get",
267
+ "get_enrichment_trends_enrichment_analytics_trends_get",
268
+ "get_enrichment_daily_activity_enrichment_analytics_daily_activity_get",
269
+ "get_enrichment_field_distribution_enrichment_analytics_field_distribution_get"
270
+ ],
271
+ "tiers": [
272
+ "read"
273
+ ]
138
274
  }
139
275
  ],
140
276
  "hooks": [
@@ -151,6 +287,10 @@
151
287
  {
152
288
  "file": "lib/enrichment-types.ts",
153
289
  "purpose": "Credit prices, field and job vocabularies, preflight/job/pipeline shapes, estimateCredits and isJobTerminal."
290
+ },
291
+ {
292
+ "file": "lib/pipeline-types.ts",
293
+ "purpose": "List pipeline, run, estimate, template, auto-enrichment and analytics shapes plus pure helpers (step builders, ordering, change detection, percent)."
154
294
  }
155
295
  ],
156
296
  "recipes": [
@@ -192,7 +332,7 @@
192
332
  {
193
333
  "id": "credits",
194
334
  "where": "api",
195
- "description": "The workspace has credits for graph8-funded providers. The API has no balance read, so the UI shows estimates only; a run without enough credits fails with 402.",
335
+ "description": "The workspace has credits for graph8-funded providers. There is no standalone balance read; a pipeline's price estimate returns available_credits beside the quote, and a run without enough credits fails with 402.",
196
336
  "verify": "A one-contact enrichment succeeds."
197
337
  }
198
338
  ],
@@ -0,0 +1,134 @@
1
+ "use client";
2
+
3
+ /**
4
+ * AutoEnrichmentConfigs: the enrichments a list runs by itself. Each one enriches every record that is ADDED to the
5
+ * list from then on, with a provider waterfall or an AI group, and charges for it with nobody confirming each record.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. listAutoEnrichmentConfigs GET /enrichment/auto-enrichment/configs?audience_id (read, free) -> { configs[], total }
9
+ * 2. listPipelineStepOptions GET /enrichment/lists/{list_id}/pipeline-step-options (read, free) names the column each config uses
10
+ * (and is exactly the set the API accepts for a new config: active, and global or available on this list)
11
+ * 3. getAutoEnrichmentConfig GET /enrichment/auto-enrichment/configs/{config_id} (read, free) loaded before Turn off or Delete is offered
12
+ * 4. createAutoEnrichmentConfig POST /enrichment/auto-enrichment/configs { audience_id, enrichment_type, column_id, trigger_on_insert }
13
+ * (billable, ConfirmSpendDialog)
14
+ * 5. updateAutoEnrichmentConfig PATCH /enrichment/auto-enrichment/configs/{config_id} { trigger_on_insert } (billable tier, ConfirmSpendDialog)
15
+ * 6. deleteAutoEnrichmentConfig DELETE /enrichment/auto-enrichment/configs/{config_id} (destructive, ConfirmSpendDialog)
16
+ *
17
+ * What the confirm says is what the backend does: the trigger is an INSERT trigger. Records already on the list are
18
+ * not enriched when a config is created, and edits to a record do not re-run it. No per-record price is published for
19
+ * this call, so the confirm states that instead of showing a number.
20
+ */
21
+
22
+ import { useState } from "react";
23
+ import { Button, Label } from "@graph8/sdk/ui";
24
+ import { ApiError } from "@/g8/components/api-error";
25
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
26
+ import { useConfirmAction } from "@/g8/components/confirmed-action";
27
+ import { EmptyState } from "@/g8/components/empty-state";
28
+ import { SectionCard } from "@/g8/components/screen/section-card";
29
+ import { StatusPill } from "@/g8/components/screen/status-pill";
30
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
31
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
32
+ import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
33
+ import { itemsOf } from "../lib/pipeline-types";
34
+ import type { AutoConfig, StepOption } from "../lib/pipeline-types";
35
+
36
+ export interface AutoEnrichmentConfigsProps {
37
+ listId: number;
38
+ className?: string;
39
+ }
40
+
41
+ const day = (iso?: string | null) => (iso ? new Date(iso).toLocaleDateString(undefined, { dateStyle: "medium" }) : "");
42
+ const ONGOING = "From now on, every record added to this list is enriched automatically and charged, with nobody confirming each one. Records already on the list are not touched, and editing a record does not run it again. The API publishes no per-record price for this, so no total is shown: check the provider costs on the Providers tab.";
43
+
44
+ function ConfigRow({ config, label, onChanged }: { config: AutoConfig; label: string; onChanged: () => void }) {
45
+ const [open, setOpen] = useState(false);
46
+ const { ask, dialog, failure } = useConfirmAction();
47
+ const detail = useG8Query(async (signal) => (await g8Get<AutoConfig>(`/enrichment/auto-enrichment/configs/${config.id}`, undefined, { signal })).data, [config.id], { enabled: open });
48
+ const fresh = detail.data;
49
+ const toggle = (next: boolean) => ask({
50
+ tier: "billable",
51
+ title: `${next ? "Turn on" : "Turn off"} automatic enrichment with ${label}`,
52
+ confirmLabel: next ? "Turn on" : "Turn off",
53
+ description: next ? ONGOING : "New records added to this list will no longer be enriched with this. Records already enriched keep their values.",
54
+ run: () => g8Patch(`/enrichment/auto-enrichment/configs/${config.id}`, { trigger_on_insert: next }),
55
+ onDone: () => { void detail.refetch(); onChanged(); },
56
+ });
57
+ const remove = () => ask({
58
+ tier: "destructive",
59
+ title: `Delete automatic enrichment with ${label}`,
60
+ description: "New records will no longer be enriched with this. Values already written stay on the records.",
61
+ affected: 1,
62
+ tolerateStatus: [404],
63
+ run: () => g8Delete(`/enrichment/auto-enrichment/configs/${config.id}`),
64
+ onDone: onChanged,
65
+ });
66
+ return (
67
+ <li className="flex flex-col gap-2 px-4 py-3" data-testid="auto-config">
68
+ <div className="flex items-center justify-between gap-3">
69
+ <button type="button" className="min-w-0 text-left" aria-expanded={open} onClick={() => setOpen(!open)}>
70
+ <span className="block truncate text-sm font-medium">{label}</span>
71
+ <span className="block text-xs text-muted-foreground">{config.enrichment_type === "ai" ? "AI group" : "Provider waterfall"}</span>
72
+ </button>
73
+ <StatusPill tone={config.trigger_on_insert ? "positive" : "neutral"} dot>{config.trigger_on_insert ? "Enriching new records" : "Off"}</StatusPill>
74
+ </div>
75
+ {open ? (
76
+ <div className="flex flex-col gap-2 rounded-md border p-3">
77
+ {detail.error ? <ApiError error={detail.error} title="Could not load this setting" onRetry={() => void detail.refetch()} /> : null}
78
+ {detail.loading && !fresh ? <p className="text-sm text-muted-foreground">Loading...</p> : null}
79
+ {fresh ? <p className="text-xs text-muted-foreground">Added {day(fresh.created_at) || "on an unknown date"}. Applies to records added to the list; it does not re-run on edits.</p> : null}
80
+ <div className="flex gap-2">
81
+ <Button size="sm" variant="outline" disabled={!fresh} onClick={() => fresh && toggle(!fresh.trigger_on_insert)}>{fresh?.trigger_on_insert === false ? "Turn on" : "Turn off"}</Button>
82
+ <Button size="sm" variant="ghost" disabled={!fresh} onClick={remove}>Delete</Button>
83
+ </div>
84
+ {failure ? <ApiError error={failure} title="That did not go through" /> : null}
85
+ </div>
86
+ ) : null}
87
+ <ConfirmSpendDialog {...dialog} />
88
+ </li>
89
+ );
90
+ }
91
+
92
+ export function AutoEnrichmentConfigs({ listId, className }: AutoEnrichmentConfigsProps) {
93
+ const [pick, setPick] = useState("");
94
+ const [confirming, setConfirming] = useState(false);
95
+ const configs = useG8Query(async (signal) => itemsOf<AutoConfig>((await g8Get("/enrichment/auto-enrichment/configs", { audience_id: listId }, { signal })).data, "configs"), [listId]);
96
+ const options = useG8Query(async (signal) => itemsOf<StepOption>((await g8Get(`/enrichment/lists/${listId}/pipeline-step-options`, undefined, { signal })).data), [listId], { key: ["enrichment", "step-options", listId], staleTime: 30_000 });
97
+ const rows = configs.data ?? [];
98
+ const menu = options.data ?? [];
99
+ const labelOf = (c: AutoConfig) => menu.find((o) => o.config_ref === c.column_id && o.type === c.enrichment_type)?.name ?? "A column that is no longer available";
100
+ const free = menu.filter((o) => !rows.some((c) => c.column_id === o.config_ref && c.enrichment_type === o.type));
101
+ const chosen = free.find((o) => `${o.type}:${o.config_ref}` === pick);
102
+ const create = useG8Mutation(
103
+ async () => (await g8Post<AutoConfig>("/enrichment/auto-enrichment/configs", { audience_id: listId, enrichment_type: chosen?.type, column_id: chosen?.config_ref, trigger_on_insert: true })).data,
104
+ { onSuccess: () => { setConfirming(false); setPick(""); void configs.refetch(); }, onError: () => setConfirming(false) },
105
+ );
106
+
107
+ return (
108
+ <div className={`flex flex-col gap-4 ${className ?? ""}`}>
109
+ <SectionCard title="Automatic enrichment for this list" description="Enrich every new record as it arrives. This spends credits without asking each time." flush>
110
+ {configs.error ? <div className="p-3"><ApiError error={configs.error} title="Could not load automatic enrichment" onRetry={() => void configs.refetch()} /></div> : null}
111
+ {!configs.error && configs.loading && !configs.data ? <p className="p-4 text-sm text-muted-foreground">Loading...</p> : null}
112
+ {!configs.error && configs.data && rows.length === 0 ? <EmptyState title="Nothing runs automatically" description="Pick a column below and new records on this list will be enriched as they are added." /> : null}
113
+ <ul className="divide-y">{(configs.error ? [] : rows).map((c) => <ConfigRow key={c.id} config={c} label={labelOf(c)} onChanged={() => void configs.refetch()} />)}</ul>
114
+ </SectionCard>
115
+
116
+ <SectionCard title="Add automatic enrichment" description="Choose what to run on each new record.">
117
+ <div className="flex flex-col gap-3">
118
+ {options.error ? <ApiError error={options.error} title="Could not load the columns you can use" onRetry={() => void options.refetch()} /> : null}
119
+ <div className="flex flex-col gap-1">
120
+ <Label htmlFor="ae-pick">Run on every new record</Label>
121
+ <select id="ae-pick" className="h-9 rounded-md border bg-background px-2 text-sm" value={pick} disabled={options.loading} onChange={(e) => setPick(e.target.value)}>
122
+ <option value="">{options.loading ? "Loading..." : free.length ? "Choose a column" : "Nothing left to add"}</option>
123
+ {free.map((o) => <option key={`${o.type}:${o.config_ref}`} value={`${o.type}:${o.config_ref}`}>{o.name}</option>)}
124
+ </select>
125
+ </div>
126
+ {create.error ? <ApiError error={create.error} title="Could not turn it on" /> : null}
127
+ <div><Button disabled={!chosen} onClick={() => setConfirming(true)}>Turn on for new records</Button></div>
128
+ </div>
129
+ </SectionCard>
130
+ <ConfirmSpendDialog open={confirming} tier="billable" title={`Enrich new records automatically with ${chosen?.name ?? "this column"}`} description={ONGOING}
131
+ confirmLabel="Turn on" confirming={create.loading} onConfirm={() => void create.mutate(undefined)} onCancel={() => setConfirming(false)} />
132
+ </div>
133
+ );
134
+ }
@@ -0,0 +1,82 @@
1
+ "use client";
2
+
3
+ /**
4
+ * AutoEnrichmentSettings: the workspace-wide switch and merge rule for automatic enrichment, one card for contacts and
5
+ * one for companies. The rule decides who wins when enriched data disagrees with what is on a record.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. getAutoEnrichmentSettings GET /enrichment/auto-enrichment/settings (read, free) -> { contact, company } each
9
+ * { enabled, default_rule: sticky | user_wins | mashup_wins, per_field_overrides }
10
+ * 2. updateAutoEnrichmentSettings PUT /enrichment/auto-enrichment/settings/{entity_type} (billable, ConfirmSpendDialog)
11
+ * sends enabled + default_rule + the per-field overrides it loaded, so saving one never wipes the others. The API
12
+ * defaults `enabled` to true, so it is always sent explicitly.
13
+ *
14
+ * Save is disabled until the settings have loaded and while nothing changed. The confirm states the real effect (what
15
+ * the rule does, from the merge handler) and that graph8 grades turning this on as a billable change; the API publishes
16
+ * no price for it, so none is shown. Needs the settings permission: a 403 is shown as such, not as an empty card.
17
+ */
18
+
19
+ import { useState } from "react";
20
+ import { Button } from "@graph8/sdk/ui";
21
+ import { ApiError } from "@/g8/components/api-error";
22
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
23
+ import { SectionCard } from "@/g8/components/screen/section-card";
24
+ import { StatusPill } from "@/g8/components/screen/status-pill";
25
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
26
+ import { useBrand } from "@/g8/hooks/use-brand";
27
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
28
+ import { g8Get, g8Put } from "@/g8/lib/g8-client";
29
+ import { plural } from "@/g8/lib/plural";
30
+ import { MERGE_RULES, MERGE_RULE_COPY } from "../lib/pipeline-types";
31
+ import type { AutoSettings, MergeRule } from "../lib/pipeline-types";
32
+
33
+ const KINDS = [{ id: "contact", label: "Contacts" }, { id: "company", label: "Companies" }] as const;
34
+
35
+ function KindCard({ settings, label, onSaved }: { settings: AutoSettings; label: string; onSaved: () => void }) {
36
+ const brand = useBrand();
37
+ const [enabled, setEnabled] = useState(settings.enabled);
38
+ const [rule, setRule] = useState<MergeRule>(settings.default_rule);
39
+ const [confirming, setConfirming] = useState(false);
40
+ const overrides = Object.keys(settings.per_field_overrides ?? {}).length;
41
+ const save = useG8Mutation(
42
+ () => g8Put(`/enrichment/auto-enrichment/settings/${settings.entity_type}`, { enabled, default_rule: rule, per_field_overrides: settings.per_field_overrides ?? {} }),
43
+ { onSuccess: () => { setConfirming(false); onSaved(); }, onError: () => setConfirming(false) },
44
+ );
45
+ const changed = enabled !== settings.enabled || rule !== settings.default_rule;
46
+ return (
47
+ <SectionCard title={label} actions={<StatusPill tone={settings.enabled ? "positive" : "neutral"} dot>{settings.enabled ? "On" : "Off"}</StatusPill>}>
48
+ <div className="flex flex-col gap-3">
49
+ <label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={enabled} onChange={(e) => setEnabled(e.target.checked)} /> Enrich {label.toLowerCase()} automatically</label>
50
+ <fieldset className="flex flex-col gap-2" aria-label={`${label} merge rule`}>
51
+ <legend className="text-sm font-medium">When enriched data differs from the record</legend>
52
+ {MERGE_RULES.map((r) => (
53
+ <label key={r} className="flex items-start gap-2 text-sm">
54
+ <input type="radio" name={`rule-${settings.entity_type}`} className="mt-1" checked={rule === r} onChange={() => setRule(r)} />
55
+ <span><span className="font-medium">{MERGE_RULE_COPY[r].label}</span><span className="block text-xs text-muted-foreground">{MERGE_RULE_COPY[r].help}</span></span>
56
+ </label>
57
+ ))}
58
+ </fieldset>
59
+ {overrides ? <p className="text-xs text-muted-foreground">{plural(overrides, "field")} follow their own rule. Saving keeps them as they are.</p> : null}
60
+ {save.error ? <ApiError error={save.error} title={`Could not save ${label.toLowerCase()} settings`} /> : null}
61
+ <div><Button disabled={!changed} onClick={() => setConfirming(true)}>Save {label.toLowerCase()} settings</Button></div>
62
+ </div>
63
+ <ConfirmSpendDialog open={confirming} tier="billable" title={`Save ${label.toLowerCase()} settings`}
64
+ description={`${enabled ? `Automatic enrichment of ${label.toLowerCase()} will be ON` : `Automatic enrichment of ${label.toLowerCase()} will be OFF`}, with the rule "${MERGE_RULE_COPY[rule].label}": ${MERGE_RULE_COPY[rule].help} ${brand.linkLabel} treats turning this on as a billable change and publishes no price for it, so no total is shown.`}
65
+ confirmLabel="Save" confirming={save.loading} onConfirm={() => void save.mutate(undefined)} onCancel={() => setConfirming(false)} />
66
+ </SectionCard>
67
+ );
68
+ }
69
+
70
+ export function AutoEnrichmentSettings({ className }: { className?: string }) {
71
+ const q = useG8Query(async (signal) => (await g8Get<Partial<Record<"contact" | "company", AutoSettings>>>("/enrichment/auto-enrichment/settings", undefined, { signal })).data, []);
72
+ if (q.error) return <ApiError error={q.error} title="Could not load the workspace settings" onRetry={() => void q.refetch()} />;
73
+ if (!q.data) return <p className="text-sm text-muted-foreground">Loading settings...</p>;
74
+ return (
75
+ <div className={`grid gap-4 lg:grid-cols-2 ${className ?? ""}`}>
76
+ {KINDS.map((k) => {
77
+ const s = q.data?.[k.id];
78
+ return s ? <KindCard key={`${k.id}-${s.enabled}-${s.default_rule}`} settings={{ ...s, entity_type: k.id }} label={k.label} onSaved={() => void q.refetch()} /> : null;
79
+ })}
80
+ </div>
81
+ );
82
+ }
@@ -12,8 +12,8 @@
12
12
  * 2. preflightEnrichment POST /enrichment/preflight (read, free; nothing is written or charged)
13
13
  * { list_id, contact_ids, mode: "waterfall", providers, target_field } -> contact_count, not_in_list,
14
14
  * missing_credentials, warnings, credits_per_record, max_total_credits
15
- * After ConfirmSpendDialog (with `max_total_credits`, or 2 credits x contacts when the preflight could not price it;
16
- * there is no balance read, so this is an estimate of the most it can charge):
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
17
  * 3. createDefaultWaterfallConfig POST /enrichment/waterfall/configs { list_id, name, field, providers } (billable tier, free)
18
18
  * Creates the list's pipeline for that field, or reuses the one it already has (`created: false`; it then runs
19
19
  * with ITS providers). The column_id is kept, so a retry after step 4 failed does not create it again.
@@ -44,7 +44,7 @@ import { ContactTable } from "@/g8/crm/components/contact-table";
44
44
  import { ListPicker } from "@/g8/lists/components/list-picker";
45
45
  import { plural } from "@/g8/lib/plural";
46
46
  import { useProviders } from "../hooks/use-providers";
47
- import { CREDITS_PER_RECORD_ESTIMATE, FIELD_PROVIDERS, TARGET_FIELDS, estimateCredits, isJobTerminal, providersForField } from "../lib/enrichment-types";
47
+ import { FIELD_PROVIDERS, TARGET_FIELDS, isJobTerminal, providersForField, quotedCredits } from "../lib/enrichment-types";
48
48
  import type { EnrichJob, Preflight, TargetField, WaterfallConfigSummary, WaterfallPipelineResult } from "../lib/enrichment-types";
49
49
  import { useBrand } from "@/g8/hooks/use-brand";
50
50
 
@@ -99,7 +99,7 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders, defaultField = "
99
99
  { enabled: ids.length > 0 && !jobId },
100
100
  );
101
101
  const quote = preflight.data;
102
- const credits = estimateCredits(quote, ids.length);
102
+ const credits = quotedCredits(quote); // null: the API could not price this run, so no total is shown
103
103
  const runnable = quote ? quote.contact_count - quote.not_in_list.length : ids.length;
104
104
 
105
105
  const start = useG8Mutation(async (): Promise<StartedRun> => {
@@ -204,7 +204,7 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders, defaultField = "
204
204
  {quote ? (
205
205
  <div className="rounded-md border p-3 text-sm" aria-label="Preflight">
206
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>
207
- <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>
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
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}
209
209
  {quote.warnings.map((w) => <p key={w} className="mt-1 text-xs text-muted-foreground">{w}</p>)}
210
210
  </div>
@@ -221,8 +221,8 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders, defaultField = "
221
221
  open={confirmOpen}
222
222
  tier="billable"
223
223
  title={`Enrich ${runnable.toLocaleString()} contacts: ${column.label.toLowerCase()}`}
224
- description={`Providers in order: ${runProviders.join(", ")}${existing ? ` (the list's saved ${column.label.toLowerCase()} pipeline)` : ""}. The estimate is the most this run can charge; your balance is not checked here.`}
225
- 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}
226
226
  affected={runnable}
227
227
  confirmLabel="Start enrichment"
228
228
  confirming={start.loading}
@@ -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
+ }