@graph8/sdk 0.362.0 → 0.370.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 (349) hide show
  1. package/README.md +14 -10
  2. package/blocks/_foundation/README.md +64 -1
  3. package/blocks/_foundation/block.json +79 -6
  4. package/blocks/_foundation/components/api-error.tsx +9 -7
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  7. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  8. package/blocks/_foundation/components/screen/index.ts +1 -0
  9. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  10. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  11. package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
  12. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  13. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  14. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  15. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  16. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  17. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  18. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  19. package/blocks/_foundation/lib/g8-client.ts +1 -1
  20. package/blocks/_foundation/lib/plural.ts +23 -0
  21. package/blocks/_foundation/server/connect-route.ts +15 -0
  22. package/blocks/_foundation/server/connect.ts +127 -0
  23. package/blocks/_foundation/server/local-key.ts +70 -0
  24. package/blocks/_foundation/server/proxy.ts +103 -3
  25. package/blocks/_foundation/server/route.ts +16 -4
  26. package/blocks/agents/README.md +14 -0
  27. package/blocks/agents/block.json +33 -1
  28. package/blocks/agents/components/agent-builder.tsx +13 -8
  29. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
  30. package/blocks/agents/components/agent-run-detail.tsx +102 -0
  31. package/blocks/agents/components/agent-runs.tsx +16 -7
  32. package/blocks/agents/components/agents-screen.tsx +184 -0
  33. package/blocks/analytics/README.md +13 -0
  34. package/blocks/analytics/block.json +45 -1
  35. package/blocks/analytics/components/analytics-screen.tsx +209 -0
  36. package/blocks/analytics/components/leaderboard.tsx +14 -7
  37. package/blocks/analytics/components/report-builder.tsx +23 -9
  38. package/blocks/analytics/components/report-detail.tsx +84 -0
  39. package/blocks/analytics/components/trend-chart.tsx +62 -0
  40. package/blocks/appointments/block.json +6 -2
  41. package/blocks/appointments/components/availability-editor.tsx +17 -5
  42. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  43. package/blocks/appointments/components/event-type-related.tsx +4 -2
  44. package/blocks/appointments/components/meeting-list.tsx +9 -5
  45. package/blocks/appointments/components/meetings-screen.tsx +7 -4
  46. package/blocks/appointments/components/scheduling-screen.tsx +2 -1
  47. package/blocks/block.schema.json +18 -0
  48. package/blocks/block.types.ts +17 -0
  49. package/blocks/campaigns/README.md +41 -11
  50. package/blocks/campaigns/block.json +105 -42
  51. package/blocks/campaigns/components/campaign-assets.tsx +16 -5
  52. package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
  53. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  54. package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
  55. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  56. package/blocks/campaigns/components/icp-detail.tsx +154 -0
  57. package/blocks/campaigns/components/icp-editor.tsx +15 -15
  58. package/blocks/campaigns/components/persona-detail.tsx +172 -0
  59. package/blocks/campaigns/components/persona-editor.tsx +14 -11
  60. package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
  61. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  62. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  63. package/blocks/coach/README.md +10 -0
  64. package/blocks/coach/block.json +16 -1
  65. package/blocks/coach/components/coach-screen.tsx +222 -0
  66. package/blocks/copilot/README.md +13 -0
  67. package/blocks/copilot/block.json +14 -1
  68. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  69. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  70. package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
  71. package/blocks/crm/README.md +28 -0
  72. package/blocks/crm/block.json +58 -2
  73. package/blocks/crm/components/companies-screen.tsx +345 -0
  74. package/blocks/crm/components/company-detail.tsx +8 -6
  75. package/blocks/crm/components/company-table.tsx +11 -4
  76. package/blocks/crm/components/contact-detail.tsx +6 -3
  77. package/blocks/crm/components/contact-table.tsx +10 -3
  78. package/blocks/crm/components/contacts-screen.tsx +359 -0
  79. package/blocks/crm/components/dedupe-review.tsx +7 -5
  80. package/blocks/crm/components/import-wizard.tsx +4 -2
  81. package/blocks/crm/components/notes-panel.tsx +4 -4
  82. package/blocks/crm/components/record-form.tsx +5 -3
  83. package/blocks/crm/components/task-list.tsx +4 -4
  84. package/blocks/crm/components/tasks-screen.tsx +379 -0
  85. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  86. package/blocks/crm/hooks/use-page-query.ts +7 -1
  87. package/blocks/crm/lib/crm-types.ts +12 -2
  88. package/blocks/crm/lib/screen-parts.tsx +105 -0
  89. package/blocks/deals/block.json +9 -3
  90. package/blocks/deals/components/deal-detail.tsx +23 -11
  91. package/blocks/deals/components/deal-related.tsx +1 -1
  92. package/blocks/deals/components/deals-screen.tsx +10 -5
  93. package/blocks/deals/components/stage-editor.tsx +4 -4
  94. package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
  95. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  96. package/blocks/dialer/README.md +34 -2
  97. package/blocks/dialer/block.json +67 -1
  98. package/blocks/dialer/components/call-contact-button.tsx +252 -0
  99. package/blocks/dialer/components/call-log-screen.tsx +205 -0
  100. package/blocks/dialer/components/call-log.tsx +7 -3
  101. package/blocks/dialer/components/call-queue.tsx +1 -1
  102. package/blocks/dialer/components/dialer-screen.tsx +319 -0
  103. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  104. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  105. package/blocks/dialer/components/session-start-form.tsx +4 -2
  106. package/blocks/dialer/components/softphone.tsx +3 -1
  107. package/blocks/enrichment/README.md +14 -2
  108. package/blocks/enrichment/block.json +32 -4
  109. package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
  110. package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
  111. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  112. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  113. package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
  114. package/blocks/enrichment/components/waterfall-config.tsx +39 -10
  115. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  116. package/blocks/forms/block.json +32 -18
  117. package/blocks/forms/components/form-builder.tsx +3 -1
  118. package/blocks/forms/components/form-embed.tsx +3 -1
  119. package/blocks/forms/components/forms-screen.tsx +5 -2
  120. package/blocks/forms/components/submissions-table.tsx +3 -1
  121. package/blocks/inbox/README.md +27 -3
  122. package/blocks/inbox/block.json +14 -2
  123. package/blocks/inbox/components/inbox-list.tsx +7 -3
  124. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  125. package/blocks/inbox/components/reply-composer.tsx +65 -9
  126. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  127. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  128. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  129. package/blocks/inbox/lib/inbox-types.ts +47 -1
  130. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  131. package/blocks/integrations/README.md +9 -0
  132. package/blocks/integrations/block.json +46 -1
  133. package/blocks/integrations/components/crm-detail.tsx +100 -0
  134. package/blocks/integrations/components/integration-list.tsx +9 -4
  135. package/blocks/integrations/components/integrations-screen.tsx +135 -0
  136. package/blocks/integrations/components/sync-status.tsx +5 -3
  137. package/blocks/integrations/components/webhook-detail.tsx +160 -0
  138. package/blocks/integrations/components/webhook-manager.tsx +13 -8
  139. package/blocks/integrations/lib/integrations-utils.ts +6 -1
  140. package/blocks/intent/README.md +14 -0
  141. package/blocks/intent/block.json +27 -1
  142. package/blocks/intent/components/intent-feed.tsx +5 -2
  143. package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
  144. package/blocks/intent/components/intent-screen.tsx +265 -0
  145. package/blocks/intent/components/keyword-detail.tsx +124 -0
  146. package/blocks/lifecycle/block.json +13 -8
  147. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  148. package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
  149. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  150. package/blocks/lists/README.md +13 -0
  151. package/blocks/lists/block.json +39 -1
  152. package/blocks/lists/components/add-to-list-button.tsx +3 -2
  153. package/blocks/lists/components/list-picker.tsx +5 -1
  154. package/blocks/lists/components/list-table.tsx +11 -5
  155. package/blocks/lists/components/lists-screen.tsx +344 -0
  156. package/blocks/lists/components/suppression-manager.tsx +6 -2
  157. package/blocks/lists/components/suppressions-screen.tsx +255 -0
  158. package/blocks/mailboxes/README.md +17 -0
  159. package/blocks/mailboxes/block.json +65 -17
  160. package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
  161. package/blocks/mailboxes/components/domain-detail.tsx +142 -0
  162. package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
  163. package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
  164. package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
  165. package/blocks/mailboxes/components/warmup-health.tsx +4 -2
  166. package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
  167. package/blocks/newsletter/block.json +40 -24
  168. package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
  169. package/blocks/newsletter/components/newsletter-screen.tsx +3 -1
  170. package/blocks/nurture/block.json +28 -14
  171. package/blocks/nurture/components/nurture-builder.tsx +3 -1
  172. package/blocks/nurture/components/nurture-enroll.tsx +6 -3
  173. package/blocks/nurture/components/nurture-form.tsx +23 -3
  174. package/blocks/nurture/components/nurture-screen.tsx +10 -6
  175. package/blocks/nurture/lib/nurture-types.ts +1 -1
  176. package/blocks/objects/README.md +17 -0
  177. package/blocks/objects/block.json +29 -16
  178. package/blocks/objects/components/object-not-found.tsx +3 -1
  179. package/blocks/objects/components/object-record-detail.tsx +2 -1
  180. package/blocks/objects/components/object-schema-editor.tsx +3 -1
  181. package/blocks/objects/components/object-table.tsx +13 -3
  182. package/blocks/objects/components/objects-screen.tsx +147 -0
  183. package/blocks/objects/lib/object-types.ts +1 -1
  184. package/blocks/pages/block.json +28 -23
  185. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  186. package/blocks/pages/components/content-library.tsx +9 -4
  187. package/blocks/pages/components/page-list.tsx +2 -1
  188. package/blocks/quotes/block.json +17 -12
  189. package/blocks/quotes/components/product-picker.tsx +3 -1
  190. package/blocks/quotes/components/quote-related.tsx +5 -2
  191. package/blocks/radar/README.md +10 -0
  192. package/blocks/radar/block.json +41 -1
  193. package/blocks/radar/components/competitor-detail.tsx +117 -0
  194. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  195. package/blocks/radar/components/radar-report.tsx +4 -2
  196. package/blocks/radar/components/radar-screen.tsx +214 -0
  197. package/blocks/radar/components/report-detail.tsx +104 -0
  198. package/blocks/search/README.md +10 -0
  199. package/blocks/search/block.json +16 -1
  200. package/blocks/search/components/filter-builder.tsx +3 -3
  201. package/blocks/search/components/prospect-results.tsx +5 -3
  202. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  203. package/blocks/search/components/saved-searches.tsx +4 -1
  204. package/blocks/search/hooks/use-filter-options.ts +20 -6
  205. package/blocks/search/lib/search-types.ts +2 -0
  206. package/blocks/sequencer/README.md +49 -9
  207. package/blocks/sequencer/block.json +99 -19
  208. package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
  209. package/blocks/sequencer/components/sequence-builder.tsx +211 -46
  210. package/blocks/sequencer/components/sequence-detail.tsx +259 -0
  211. package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
  212. package/blocks/sequencer/components/sequence-list.tsx +33 -8
  213. package/blocks/sequencer/components/sequence-stats.tsx +3 -2
  214. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  215. package/blocks/sequencer/components/sequences-screen.tsx +159 -0
  216. package/blocks/sequencer/components/step-editor.tsx +3 -1
  217. package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
  218. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  219. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  220. package/blocks/social-ads/README.md +11 -0
  221. package/blocks/social-ads/block.json +101 -2
  222. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  223. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  224. package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
  225. package/blocks/social-ads/components/audience-sync.tsx +11 -7
  226. package/blocks/social-ads/components/campaign-detail.tsx +133 -0
  227. package/blocks/social-ads/components/creative-detail.tsx +150 -0
  228. package/blocks/social-ads/components/creative-gallery.tsx +15 -5
  229. package/blocks/social-ads/components/post-composer.tsx +3 -1
  230. package/blocks/social-ads/components/post-detail.tsx +160 -0
  231. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  232. package/blocks/social-ads/components/social-screen.tsx +160 -0
  233. package/blocks/visitors/README.md +10 -0
  234. package/blocks/visitors/block.json +25 -1
  235. package/blocks/visitors/components/snippet-install.tsx +3 -1
  236. package/blocks/visitors/components/visitor-company-card.tsx +2 -2
  237. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  238. package/blocks/visitors/components/visitor-feed.tsx +11 -2
  239. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  240. package/blocks/webchat/README.md +10 -0
  241. package/blocks/webchat/block.json +21 -7
  242. package/blocks/webchat/components/chat-widget-config.tsx +7 -4
  243. package/blocks/webchat/components/conversation-list.tsx +7 -3
  244. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  245. package/blocks/workbench/README.md +11 -0
  246. package/blocks/workbench/block.json +48 -1
  247. package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
  248. package/blocks/workbench/components/workbench-grid.tsx +9 -3
  249. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  250. package/blocks/workbench/components/workbench-screen.tsx +192 -0
  251. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  252. package/blocks/workflows/README.md +13 -0
  253. package/blocks/workflows/block.json +28 -9
  254. package/blocks/workflows/components/run-history.tsx +31 -9
  255. package/blocks/workflows/components/trigger-config.tsx +3 -1
  256. package/blocks/workflows/components/workflow-editor.tsx +3 -1
  257. package/blocks/workflows/components/workflow-list.tsx +13 -6
  258. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  259. package/dist/cli.js +2709 -1721
  260. package/dist/errors.d.mts +34 -0
  261. package/dist/errors.d.ts +34 -0
  262. package/dist/errors.js +46 -0
  263. package/dist/errors.js.map +1 -0
  264. package/dist/errors.mjs +21 -0
  265. package/dist/errors.mjs.map +1 -0
  266. package/dist/index.browser.js +11 -5
  267. package/dist/index.browser.js.map +1 -1
  268. package/dist/index.browser.mjs +11 -5
  269. package/dist/index.browser.mjs.map +1 -1
  270. package/dist/index.d.mts +164 -25
  271. package/dist/index.d.ts +164 -25
  272. package/dist/index.js +11 -5
  273. package/dist/index.js.map +1 -1
  274. package/dist/index.mjs +11 -5
  275. package/dist/index.mjs.map +1 -1
  276. package/dist/react.browser.js +10 -4
  277. package/dist/react.browser.js.map +1 -1
  278. package/dist/react.browser.mjs +10 -4
  279. package/dist/react.browser.mjs.map +1 -1
  280. package/dist/react.d.mts +142 -5
  281. package/dist/react.d.ts +142 -5
  282. package/dist/react.js +10 -4
  283. package/dist/react.js.map +1 -1
  284. package/dist/react.mjs +10 -4
  285. package/dist/react.mjs.map +1 -1
  286. package/dist/types/blocks/add.d.ts +11 -4
  287. package/dist/types/blocks/brand.d.ts +113 -0
  288. package/dist/types/blocks/compose.d.ts +53 -0
  289. package/dist/types/blocks/create.d.ts +22 -19
  290. package/dist/types/blocks/doctor.d.ts +5 -1
  291. package/dist/types/blocks/navfile.d.ts +35 -0
  292. package/dist/types/blocks/paths.d.ts +4 -0
  293. package/dist/types/blocks/prefixes.d.ts +37 -0
  294. package/dist/types/blocks/registry.d.ts +17 -0
  295. package/dist/types/blocks/screens.d.ts +53 -0
  296. package/dist/types/cli.d.ts +2 -0
  297. package/dist/types/errors.d.ts +33 -0
  298. package/dist/types/http.d.ts +2 -18
  299. package/dist/types/version.d.ts +2 -2
  300. package/docs/agents.md +15 -4
  301. package/docs/api/README.md +2 -2
  302. package/docs/api/by-path.md +1 -0
  303. package/docs/api/desk.d.ts +98 -2
  304. package/docs/api/desk.md +123 -1
  305. package/docs/api/inbox.d.ts +10 -2
  306. package/docs/api/inbox.md +10 -2
  307. package/docs/api/seo.d.ts +19 -1
  308. package/docs/api/seo.md +24 -1
  309. package/package.json +16 -2
  310. package/stacks/6sense.json +41 -16
  311. package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
  312. package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
  313. package/stacks/apollo.json +118 -36
  314. package/stacks/calendly.json +60 -0
  315. package/stacks/clay/pages/companies.tsx.tmpl +15 -0
  316. package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
  317. package/stacks/clay.json +59 -26
  318. package/stacks/gong.json +81 -35
  319. package/stacks/hootsuite.json +99 -0
  320. package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
  321. package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
  322. package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
  323. package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
  324. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  325. package/stacks/hubspot.json +167 -66
  326. package/stacks/intercom.json +48 -20
  327. package/stacks/mailchimp.json +52 -33
  328. package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
  329. package/stacks/outreach.json +88 -25
  330. package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
  331. package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
  332. package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
  333. package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
  334. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  335. package/stacks/pipedrive.json +115 -0
  336. package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
  337. package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
  338. package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
  339. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  340. package/stacks/salesforce.json +81 -31
  341. package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
  342. package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
  343. package/stacks/zoominfo.json +102 -0
  344. package/styles/styles.css +1 -1
  345. package/templates/next-app/README.md.tmpl +40 -9
  346. package/templates/next-app/app/layout.tsx.tmpl +7 -2
  347. package/templates/next-app/components/app-shell.tsx.tmpl +99 -89
  348. package/templates/next-app/dot-gitignore.tmpl +2 -0
  349. package/templates/next-app/package.json.tmpl +1 -1
@@ -7,7 +7,8 @@
7
7
  "single person/company lookup that can apply found fields to the record",
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
- "waterfall pipeline manager with provider order and credential readiness"
10
+ "waterfall pipeline manager with provider order and credential readiness",
11
+ "finished EnrichmentScreen page"
11
12
  ],
12
13
  "dependsOn": [
13
14
  "lists"
@@ -18,6 +19,8 @@
18
19
  "create_list_lists_post",
19
20
  "delete_contact_contacts__contact_id__delete",
20
21
  "delete_waterfall_config_enrichment_waterfall_configs__column_id__delete",
22
+ "get_company_companies__company_id__get",
23
+ "get_contact_contacts__contact_id__get",
21
24
  "get_enrichment_job_enrichment_jobs__job_id__get",
22
25
  "list_contact_fields_fields_get",
23
26
  "list_contacts_contacts_get",
@@ -41,9 +44,12 @@
41
44
  "lookup_person_enrichment_lookup_person_post",
42
45
  "lookup_company_enrichment_lookup_company_post",
43
46
  "update_contact_contacts__contact_id__patch",
44
- "update_company_companies__company_id__patch"
47
+ "update_company_companies__company_id__patch",
48
+ "get_contact_contacts__contact_id__get",
49
+ "get_company_companies__company_id__get"
45
50
  ],
46
51
  "tiers": [
52
+ "read",
47
53
  "write",
48
54
  "billable"
49
55
  ]
@@ -51,7 +57,7 @@
51
57
  {
52
58
  "name": "EmailVerifyBadge",
53
59
  "file": "components/email-verify-badge.tsx",
54
- "purpose": "Status chip for an email (Unverified, Valid, Risky, Invalid) with a confirmed 1-credit Verify action; never verifies a locked email.",
60
+ "purpose": "Status chip for an email (Unverified, Valid, Risky, Invalid, Couldn't verify) from the verifier's own result codes, with a confirmed 1-credit Verify action; never verifies a locked email.",
55
61
  "operations": [
56
62
  "verify_email_enrichment_verify_email_post"
57
63
  ],
@@ -97,6 +103,26 @@
97
103
  "billable",
98
104
  "destructive"
99
105
  ]
106
+ },
107
+ {
108
+ "name": "EnrichmentScreen",
109
+ "file": "components/enrichment-screen.tsx",
110
+ "purpose": "Finished enrichment page: enrich a list, one-off lookups and email verification, pipelines and the provider catalog.",
111
+ "operations": [],
112
+ "tiers": []
113
+ },
114
+ {
115
+ "name": "EnrichmentJobDetail",
116
+ "file": "components/enrichment-job-detail.tsx",
117
+ "purpose": "One bulk enrichment job: live progress, counts, credits, warnings, provider errors and cancel.",
118
+ "operations": [
119
+ "cancel_enrichment_job_enrichment_jobs__job_id__cancel_post",
120
+ "get_enrichment_job_enrichment_jobs__job_id__get"
121
+ ],
122
+ "tiers": [
123
+ "read",
124
+ "write"
125
+ ]
100
126
  }
101
127
  ],
102
128
  "hooks": [
@@ -129,7 +155,7 @@
129
155
  {
130
156
  "file": "recipes/waterfall-pipeline.ts",
131
157
  "title": "Create a waterfall pipeline for a list and see which providers are ready",
132
- "when": "Setting up \"try provider A, then B\" enrichment for a column once, then reusing it. Check the provider catalog first: graph8-funded providers spend credits when the pipeline runs, providers on your own key do not, and a provider without a key is skipped. Creating is idempotent by (list_id, name)."
158
+ "when": "Setting up \"try provider A, then B\" enrichment for a column once, then reusing it. Check the provider catalog first: graph8-funded providers spend credits when the pipeline runs, providers on your own key do not, and a provider without a key is skipped. Creating with `field` is idempotent per field and list: if the list already has a pipeline for that field it is returned with `created: false` and keeps its own providers, whatever name or providers you send (delete it first to change them). Without `field`, a pipeline is reused by (list_id, name)."
133
159
  }
134
160
  ],
135
161
  "prerequisites": [
@@ -159,6 +185,7 @@
159
185
  }
160
186
  ],
161
187
  "scopes": [
188
+ "companies:read",
162
189
  "companies:write",
163
190
  "contacts:delete",
164
191
  "contacts:read",
@@ -199,6 +226,7 @@
199
226
  }
200
227
  ],
201
228
  "peerDependencies": {
229
+ "lucide-react": ">=0.400.0",
202
230
  "react": ">=18"
203
231
  }
204
232
  }
@@ -31,9 +31,11 @@ import { useJobPoll } from "@/g8/hooks/use-job-poll";
31
31
  import { g8Get, g8Post } from "@/g8/lib/g8-client";
32
32
  import { ContactTable } from "@/g8/crm/components/contact-table";
33
33
  import { ListPicker } from "@/g8/lists/components/list-picker";
34
+ import { plural } from "@/g8/lib/plural";
34
35
  import { useProviders } from "../hooks/use-providers";
35
36
  import { CREDITS_PER_RECORD_ESTIMATE, TARGET_FIELDS, estimateCredits, isJobTerminal } from "../lib/enrichment-types";
36
37
  import type { EnrichJob, Preflight, TargetField } from "../lib/enrichment-types";
38
+ import { useBrand } from "@/g8/hooks/use-brand";
37
39
 
38
40
  export interface BulkEnrichPanelProps {
39
41
  /** With `listId` and `contactIds` it is just the enrichment panel. Omit them and it is a whole page: pick a list, tick contacts, enrich. */
@@ -44,12 +46,15 @@ export interface BulkEnrichPanelProps {
44
46
  defaultField?: TargetField;
45
47
  /** Called when the job reached a final state. */
46
48
  onDone?: (job: EnrichJob) => void;
49
+ /** Called with the job id as soon as a run starts (to list it in a job history). */
50
+ onStarted?: (jobId: string) => void;
47
51
  className?: string;
48
52
  }
49
53
 
50
54
  const DEFAULT_PROVIDERS = ["prospeo", "dropcontact"];
51
55
 
52
- function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDERS, defaultField = "work_email", onDone, className }: BulkEnrichPanelProps & { listId: number; contactIds: number[] }) {
56
+ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDERS, defaultField = "work_email", onDone, onStarted, className }: BulkEnrichPanelProps & { listId: number; contactIds: number[] }) {
57
+ const brand = useBrand();
53
58
  const [field, setField] = useState<TargetField>(defaultField);
54
59
  const [chosen, setChosen] = useState<string[] | null>(null);
55
60
  const [confirmOpen, setConfirmOpen] = useState(false);
@@ -71,7 +76,7 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
71
76
  const runnable = quote ? quote.contact_count - quote.not_in_list.length : ids.length;
72
77
 
73
78
  const start = useG8Mutation(() => g8Post<{ job_id: string }>("/enrichment/enrich", { contact_ids: ids, list_id: listId, fields_config: { [column.column]: providers } }), {
74
- onSuccess: (res) => { setConfirmOpen(false); setJobId(res.data.job_id); },
79
+ onSuccess: (res) => { setConfirmOpen(false); setJobId(res.data.job_id); onStarted?.(res.data.job_id); },
75
80
  });
76
81
  const job = useJobPoll<EnrichJob>(
77
82
  async (signal) => (await g8Get<EnrichJob>(`/enrichment/jobs/${jobId}`, undefined, { signal })).data,
@@ -128,7 +133,7 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
128
133
  {TARGET_FIELDS.map((f) => <option key={f.value} value={f.value}>{f.label}</option>)}
129
134
  </select>
130
135
  </label>
131
- <span className="text-sm text-muted-foreground">for {ids.length.toLocaleString()} contact{ids.length === 1 ? "" : "s"}</span>
136
+ <span className="text-sm text-muted-foreground">for {plural(ids.length, "contact")}</span>
132
137
  </div>
133
138
 
134
139
  <fieldset className="flex flex-col gap-1">
@@ -154,7 +159,7 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
154
159
  <div className="rounded-md border p-3 text-sm" aria-label="Preflight">
155
160
  <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>
156
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>
157
- {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 graph8 settings.</p> : null}
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}
158
163
  {quote.warnings.map((w) => <p key={w} className="mt-1 text-xs text-muted-foreground">{w}</p>)}
159
164
  </div>
160
165
  ) : null}
@@ -162,7 +167,7 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
162
167
 
163
168
  <div className="flex justify-end">
164
169
  <Button disabled={ids.length === 0 || providers.length === 0 || preflight.loading || runnable === 0 || Boolean(preflight.error)} onClick={() => setConfirmOpen(true)}>
165
- Enrich {runnable.toLocaleString()} contact{runnable === 1 ? "" : "s"}
170
+ Enrich {plural(runnable, "contact")}
166
171
  </Button>
167
172
  </div>
168
173
 
@@ -4,6 +4,12 @@
4
4
  * EmailVerifyBadge: a small status chip next to an email address (Unverified, Valid, Risky, Invalid) with
5
5
  * a "Verify" action.
6
6
  *
7
+ * The verdict comes from the verifier's own result code (EmailListVerify and friends; the backend's vocabulary is
8
+ * shared/services/email_verification_classifier.py): `ok`/`valid` is Valid; catch-all codes (`ok_for_all`,
9
+ * `accept_all`) are Risky; hard failures (`email_disabled`, `invalid_mx`, `dead_server`, `no_mx`, ...) are Invalid, so a
10
+ * known-bad address is never offered a second paid Verify; `unknown`/`error` (the verifier was inconclusive or the call
11
+ * failed) is "Couldn't verify", which can be retried.
12
+ *
7
13
  * SDK operations, in order:
8
14
  * 1. verifyEmail POST /enrichment/verify-email { email } (billable)
9
15
  * Goes through ConfirmSpendDialog first: 1 credit per verification.
@@ -20,7 +26,7 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
20
26
  import { g8Post } from "@/g8/lib/g8-client";
21
27
  import { CREDITS_PER_EMAIL_VERIFICATION, isMaskedValue } from "../lib/enrichment-types";
22
28
 
23
- export type VerifyState = "unverified" | "valid" | "risky" | "invalid";
29
+ export type VerifyState = "unverified" | "valid" | "risky" | "invalid" | "unverifiable";
24
30
 
25
31
  export interface EmailVerifyBadgeProps {
26
32
  email?: string | null;
@@ -32,13 +38,26 @@ export interface EmailVerifyBadgeProps {
32
38
  className?: string;
33
39
  }
34
40
 
35
- /** Collapse the verifier's vocabulary into the four states the chip shows. */
41
+ // The verifier's result codes, normalised to lowercase with "-" (shared/services/email_verification_classifier.py).
42
+ // `error` is EmailListVerify's "delivery failed" and the classifier counts it NOT_VALID, so it is Invalid here; a
43
+ // vendor failure reaches the client as `unknown` (the endpoint catches it and charges nothing). `role-based` and
44
+ // `timeout` are not in the backend vocabulary; they are kept as harmless aliases from other verifiers.
45
+ const VALID = new Set(["valid", "ok", "deliverable", "ultra-sure", "sure"]);
46
+ const RISKY = new Set(["catch-all", "accept-all", "ok-for-all", "role-based"]);
47
+ const INVALID = new Set([
48
+ "invalid", "fail", "undeliverable", "unlikely", "unsure", "email-disabled", "dead-server", "invalid-mx", "invalid-syntax",
49
+ "bad-mx", "no-mx", "abuse", "do-not-mail", "spamtrap", "disposable", "bounced", "error",
50
+ ]);
51
+ const UNVERIFIABLE = new Set(["unknown", "unverifiable", "smtp-protocol", "antispam-system", "timeout"]);
52
+
53
+ /** Collapse the verifier's vocabulary into the states the chip shows. */
36
54
  export function classifyVerification(status: string | null | undefined, isValid?: boolean): VerifyState {
37
- const s = String(status ?? "").toLowerCase().replace(/_/g, "-");
55
+ const s = String(status ?? "").trim().toLowerCase().replace(/[_\s]+/g, "-");
38
56
  if (!s) return "unverified";
39
- if (["catch-all", "unknown", "role-based", "accept-all"].includes(s)) return "risky";
40
- if (s === "valid") return "valid";
41
- if (["invalid", "do-not-mail", "spamtrap", "abuse", "disposable", "bounced"].includes(s)) return "invalid";
57
+ if (RISKY.has(s)) return "risky";
58
+ if (VALID.has(s)) return "valid";
59
+ if (INVALID.has(s)) return "invalid";
60
+ if (UNVERIFIABLE.has(s)) return "unverifiable";
42
61
  return isValid === true ? "valid" : "unverified"; // an unrecognised status is not proof of a bad address
43
62
  }
44
63
 
@@ -47,8 +66,9 @@ const STYLE: Record<VerifyState, string> = {
47
66
  valid: "bg-emerald-100 text-emerald-800",
48
67
  risky: "bg-amber-100 text-amber-800",
49
68
  invalid: "bg-red-100 text-red-800",
69
+ unverifiable: "bg-muted text-muted-foreground",
50
70
  };
51
- const LABEL: Record<VerifyState, string> = { unverified: "Unverified", valid: "Valid", risky: "Risky", invalid: "Invalid" };
71
+ const LABEL: Record<VerifyState, string> = { unverified: "Unverified", valid: "Valid", risky: "Risky", invalid: "Invalid", unverifiable: "Couldn't verify" };
52
72
 
53
73
  export function EmailVerifyBadge({ email, initialStatus, onVerified, readOnly, className }: EmailVerifyBadgeProps) {
54
74
  const [confirmOpen, setConfirmOpen] = useState(false);
@@ -72,7 +92,7 @@ export function EmailVerifyBadge({ email, initialStatus, onVerified, readOnly, c
72
92
  return (
73
93
  <span className={`inline-flex items-center gap-1 ${className ?? ""}`}>
74
94
  <span data-state={state} title={detail ?? undefined} className={`rounded-full px-2 py-0.5 text-xs font-medium ${STYLE[state]}`}>{LABEL[state]}</span>
75
- {!readOnly && state === "unverified" ? <Button variant="link" size="sm" className="h-auto p-0 text-xs" onClick={() => setConfirmOpen(true)}>Verify</Button> : null}
95
+ {!readOnly && (state === "unverified" || state === "unverifiable") ? <Button variant="link" size="sm" className="h-auto p-0 text-xs" onClick={() => setConfirmOpen(true)}>{state === "unverifiable" ? "Try again" : "Verify"}</Button> : null}
76
96
  {verify.error ? <ApiError error={verify.error} title="Could not verify" /> : null}
77
97
  <ConfirmSpendDialog open={confirmOpen} tier="billable" title={`Verify ${email}`} description="Checks whether the address can receive mail."
78
98
  estimatedCredits={CREDITS_PER_EMAIL_VERIFICATION} confirmLabel="Verify" confirming={verify.loading}
@@ -13,6 +13,11 @@
13
13
  *
14
14
  * Apply is opt-in per field: fields the record already has are shown unticked so a lookup never silently
15
15
  * overwrites good data. A locked ("***") value on the record counts as empty.
16
+ *
17
+ * "Already has" is judged on the FULL record. A list row can omit fields the record has, so unless the caller says
18
+ * `recordLoaded` (it passed the full record from GET /contacts/{id} or /companies/{id}), the button reads the full
19
+ * record itself right after the lookup (GET /contacts/{id} | /companies/{id}, read). If that read fails nothing is
20
+ * pre-ticked and the dialog says existing values were not checked. The record id is captured when the lookup runs.
16
21
  */
17
22
 
18
23
  import { useState } from "react";
@@ -20,7 +25,8 @@ import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogH
20
25
  import { ApiError } from "@/g8/components/api-error";
21
26
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
22
27
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
23
- import { g8Patch, g8Post } from "@/g8/lib/g8-client";
28
+ import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
29
+ import { plural } from "@/g8/lib/plural";
24
30
  import { CREDITS_PER_FOUND_LOOKUP, isMaskedValue } from "../lib/enrichment-types";
25
31
 
26
32
  export interface PersonLookup { email?: string; linkedin_url?: string; first_name?: string; last_name?: string; company_domain?: string }
@@ -32,6 +38,8 @@ export interface EnrichButtonProps {
32
38
  lookup: PersonLookup | CompanyLookup;
33
39
  /** The CRM record the result can be applied to (its current values decide what is pre-ticked). */
34
40
  record?: { id: number } & Record<string, unknown>;
41
+ /** True when `record` is the full record (not a list row). Default false: the button then reads the full record itself before pre-ticking. */
42
+ recordLoaded?: boolean;
35
43
  label?: string;
36
44
  variant?: "default" | "outline" | "secondary" | "ghost";
37
45
  size?: "default" | "sm";
@@ -60,25 +68,39 @@ function hasIdentity(kind: EnrichButtonProps["kind"], l: PersonLookup & CompanyL
60
68
  return kind === "person" ? Boolean(l.email || l.linkedin_url || (l.first_name && l.last_name && l.company_domain)) : Boolean(l.domain || l.name);
61
69
  }
62
70
 
63
- export function EnrichButton({ kind, lookup, record, label, variant = "outline", size = "sm", onResult, onApplied, className }: EnrichButtonProps) {
71
+ export function EnrichButton({ kind, lookup, record, recordLoaded = false, label, variant = "outline", size = "sm", onResult, onApplied, className }: EnrichButtonProps) {
64
72
  const [confirmOpen, setConfirmOpen] = useState(false);
65
73
  const [result, setResult] = useState<LookupResult | null>(null);
66
74
  const [ticked, setTicked] = useState<Set<string>>(new Set());
67
75
  const [applied, setApplied] = useState(false);
76
+ /** What the record held when the lookup ran: its id, its full values, and whether those values could be read. */
77
+ const [target, setTarget] = useState<{ id: number; existing: Record<string, unknown>; checked: boolean } | null>(null);
68
78
  const clean = Object.fromEntries(Object.entries(lookup).filter(([, v]) => typeof v === "string" && v.trim() && !isMaskedValue(v)));
69
79
  const ready = hasIdentity(kind, clean);
80
+ const path = (id: number): string => `/${kind === "person" ? "contacts" : "companies"}/${id}`;
70
81
 
71
- const run = useG8Mutation(() => g8Post<LookupResult>(`/enrichment/lookup/${kind}`, clean), {
72
- onSuccess: (res) => {
82
+ const run = useG8Mutation(async () => {
83
+ const res = await g8Post<LookupResult>(`/enrichment/lookup/${kind}`, clean);
84
+ if (!record) return { res, target: null };
85
+ if (recordLoaded) return { res, target: { id: record.id, existing: record, checked: true } };
86
+ try {
87
+ const full = await g8Get<Record<string, unknown>>(path(record.id));
88
+ return { res, target: { id: record.id, existing: full.data ?? {}, checked: true } };
89
+ } catch {
90
+ return { res, target: { id: record.id, existing: {}, checked: false } };
91
+ }
92
+ }, {
93
+ onSuccess: ({ res, target: t }) => {
73
94
  setConfirmOpen(false);
74
95
  setResult(res.data);
75
96
  setApplied(false);
97
+ setTarget(t);
76
98
  const fields = res.data.data ? normalise(kind, res.data.data) : {};
77
- setTicked(new Set(Object.keys(fields).filter((k) => !record || !record[k] || isMaskedValue(record[k]))));
99
+ setTicked(new Set(t?.checked ? Object.keys(fields).filter((k) => !t.existing[k] || isMaskedValue(t.existing[k])) : []));
78
100
  onResult?.(res.data);
79
101
  },
80
102
  });
81
- const apply = useG8Mutation((values: Record<string, string>) => g8Patch(`/${kind === "person" ? "contacts" : "companies"}/${record!.id}`, values), {
103
+ const apply = useG8Mutation((values: Record<string, string>) => g8Patch(path(target!.id), values), {
82
104
  onSuccess: (_r, values) => { setApplied(true); onApplied?.(values); },
83
105
  });
84
106
 
@@ -114,19 +136,20 @@ export function EnrichButton({ kind, lookup, record, label, variant = "outline",
114
136
  <ul className="flex flex-col gap-1 text-sm" aria-label="Found fields">
115
137
  {Object.entries(fields).map(([k, v]) => (
116
138
  <li key={k} className="flex items-center gap-2">
117
- {record ? <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}
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}
118
140
  <span className="w-36 text-muted-foreground">{k.replace(/_/g, " ")}</span><span className="truncate">{v}</span>
119
- {record && record[k] && !isMaskedValue(record[k]) && record[k] !== v ? <span className="text-xs text-amber-600">replaces {String(record[k])}</span> : null}
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}
120
142
  </li>
121
143
  ))}
122
144
  {Object.keys(fields).length === 0 ? <li className="text-muted-foreground">The match had no fields to show.</li> : null}
123
145
  </ul>
124
146
  ) : null}
147
+ {target && !target.checked && result?.found ? <p role="status" className="text-sm text-muted-foreground">Existing values were not checked, so nothing is ticked. Tick only what you want to write.</p> : null}
125
148
  {apply.error ? <ApiError error={apply.error} title="Could not update the record" /> : null}
126
149
  {applied ? <p role="status" className="text-sm">Updated the record.</p> : null}
127
150
  <DialogFooter>
128
151
  <Button variant="ghost" onClick={() => setResult(null)}>Close</Button>
129
- {result?.found && record && !applied ? <Button loading={apply.loading} disabled={Object.keys(chosen).length === 0} onClick={() => void apply.mutate(chosen)}>Apply {Object.keys(chosen).length} field{Object.keys(chosen).length === 1 ? "" : "s"}</Button> : null}
152
+ {result?.found && target && !applied ? <Button loading={apply.loading} disabled={Object.keys(chosen).length === 0} onClick={() => void apply.mutate(chosen)}>Apply {plural(Object.keys(chosen).length, "field")}</Button> : null}
130
153
  </DialogFooter>
131
154
  </DialogContent>
132
155
  </Dialog>
@@ -0,0 +1,78 @@
1
+ "use client";
2
+
3
+ /**
4
+ * EnrichmentJobDetail: one bulk enrichment job in full. Live status (polled with backoff, there are no streams),
5
+ * progress, how many records were enriched or failed, credits used, warnings, the provider errors that explain a
6
+ * failure, and Cancel while it runs.
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. getEnrichmentJob GET /enrichment/jobs/{job_id} (read) polled with useJobPoll until a final status;
10
+ * read once more with ?include_provider_errors=true when nothing was enriched or some records failed
11
+ * -> { status, total, completed, successful_enrichments, failed_enrichments, total_credits_used, warnings, provider_errors }
12
+ * 2. cancelEnrichmentJob POST /enrichment/jobs/{job_id}/cancel (write, free; records already done keep their data and charge)
13
+ */
14
+
15
+ import { Button } from "@graph8/sdk/ui";
16
+ import { ApiError } from "@/g8/components/api-error";
17
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
18
+ import { SectionCard } from "@/g8/components/screen/section-card";
19
+ import { StatusPill, humanizeStatus, toneForStatus } from "@/g8/components/screen/status-pill";
20
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
21
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
22
+ import { useJobPoll } from "@/g8/hooks/use-job-poll";
23
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
24
+ import { isJobTerminal } from "../lib/enrichment-types";
25
+ import type { EnrichJob } from "../lib/enrichment-types";
26
+
27
+ export interface EnrichmentJobDetailProps {
28
+ jobId: string;
29
+ /** Called when the job reached a final state. */
30
+ onDone?: (job: EnrichJob) => void;
31
+ className?: string;
32
+ }
33
+
34
+ export function EnrichmentJobDetail({ jobId, onDone, className }: EnrichmentJobDetailProps) {
35
+ const job = useJobPoll<EnrichJob>(
36
+ async (signal) => (await g8Get<EnrichJob>(`/enrichment/jobs/${jobId}`, undefined, { signal })).data,
37
+ { intervalMs: 2000, isTerminal: (j) => isJobTerminal(j.status), onDone },
38
+ );
39
+ const cancel = useG8Mutation(() => g8Post(`/enrichment/jobs/${jobId}/cancel`, {}));
40
+ const j = job.status;
41
+ const problems = job.done && j != null && ((j.successful_enrichments ?? 0) === 0 || (j.failed_enrichments ?? 0) > 0);
42
+ const errors = useG8Query(
43
+ async (signal) => (await g8Get<EnrichJob>(`/enrichment/jobs/${jobId}`, { include_provider_errors: true }, { signal })).data.provider_errors ?? [],
44
+ [jobId, job.done],
45
+ { enabled: problems },
46
+ );
47
+ const total = j?.total ?? 0;
48
+ const pct = total ? Math.min(100, Math.round(((j?.completed ?? 0) / total) * 100)) : 0;
49
+
50
+ if (job.error && !j) return <ApiError error={job.error} title="Could not read the job" onRetry={() => job.start()} />;
51
+ return (
52
+ <div className={`flex flex-col gap-4 ${className ?? ""}`} role="status">
53
+ <div className="flex items-center gap-2">
54
+ {j ? <StatusPill tone={toneForStatus(j.status)} dot>{humanizeStatus(j.status)}</StatusPill> : <span className="text-sm text-muted-foreground">Loading...</span>}
55
+ {!job.done && j ? <Button size="sm" variant="outline" loading={cancel.loading} onClick={() => void cancel.mutate(undefined)}>Cancel job</Button> : null}
56
+ </div>
57
+ <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>
58
+ {cancel.error ? <ApiError error={cancel.error} title="Could not cancel" /> : null}
59
+ <SectionCard title="Result">
60
+ <KeyValueList columns={2} items={[
61
+ { label: "Job id", value: jobId, mono: true, wide: true },
62
+ { label: "Processed", value: j ? `${(j.completed ?? 0).toLocaleString()} of ${total.toLocaleString()}` : "" },
63
+ { label: "Enriched", value: j?.successful_enrichments != null ? j.successful_enrichments.toLocaleString() : "" },
64
+ { label: "Failed", value: j?.failed_enrichments != null ? j.failed_enrichments.toLocaleString() : "" },
65
+ { label: "Credits used", value: j?.total_credits_used != null ? j.total_credits_used.toLocaleString() : "" },
66
+ ]} />
67
+ </SectionCard>
68
+ {j?.warnings?.length ? <SectionCard title="Warnings"><ul className="list-disc pl-5 text-sm">{j.warnings.map((w) => <li key={w}>{w}</li>)}</ul></SectionCard> : null}
69
+ {errors.data?.length ? (
70
+ <SectionCard title="Provider errors" description="Usually a missing or invalid provider key.">
71
+ <ul className="list-disc pl-5 text-sm text-destructive" aria-label="Provider errors">
72
+ {errors.data.slice(0, 20).map((e, i) => <li key={i}>{e.provider}: {e.error}</li>)}
73
+ </ul>
74
+ </SectionCard>
75
+ ) : null}
76
+ </div>
77
+ );
78
+ }