@graph8/sdk 0.303.0 → 0.357.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 (664) hide show
  1. package/README.md +81 -0
  2. package/blocks/README.md +32 -0
  3. package/blocks/_foundation/README.md +78 -0
  4. package/blocks/_foundation/block.json +79 -0
  5. package/blocks/_foundation/components/api-error.tsx +62 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +142 -0
  7. package/blocks/_foundation/components/empty-state.tsx +24 -0
  8. package/blocks/_foundation/hooks/use-g8-mutation.ts +88 -0
  9. package/blocks/_foundation/hooks/use-g8-query.ts +87 -0
  10. package/blocks/_foundation/hooks/use-job-poll.ts +130 -0
  11. package/blocks/_foundation/hooks/use-paginated.ts +105 -0
  12. package/blocks/_foundation/lib/g8-client.ts +128 -0
  13. package/blocks/_foundation/recipes/verify-proxy-key.ts +18 -0
  14. package/blocks/_foundation/server/proxy.ts +233 -0
  15. package/blocks/_foundation/server/route.ts +30 -0
  16. package/blocks/agents/README.md +133 -0
  17. package/blocks/agents/block.json +223 -0
  18. package/blocks/agents/components/agent-builder.tsx +199 -0
  19. package/blocks/agents/components/agent-knowledge-panel.tsx +144 -0
  20. package/blocks/agents/components/agent-runs.tsx +175 -0
  21. package/blocks/agents/components/agent-skills-panel.tsx +126 -0
  22. package/blocks/agents/components/agent-test-console.tsx +175 -0
  23. package/blocks/agents/lib/agent-types.ts +114 -0
  24. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +50 -0
  25. package/blocks/agents/recipes/run-and-follow-agent-run.ts +46 -0
  26. package/blocks/agents/recipes/test-agent-before-activating.ts +36 -0
  27. package/blocks/analytics/README.md +73 -0
  28. package/blocks/analytics/block.json +178 -0
  29. package/blocks/analytics/components/funnel-chart.tsx +79 -0
  30. package/blocks/analytics/components/kpi-tiles.tsx +74 -0
  31. package/blocks/analytics/components/leaderboard.tsx +86 -0
  32. package/blocks/analytics/components/report-builder.tsx +200 -0
  33. package/blocks/analytics/hooks/use-report.ts +26 -0
  34. package/blocks/analytics/lib/analytics-types.ts +108 -0
  35. package/blocks/analytics/lib/use-debounced.ts +16 -0
  36. package/blocks/analytics/recipes/01-weekly-outbound-report.ts +32 -0
  37. package/blocks/analytics/recipes/02-revenue-snapshot.ts +22 -0
  38. package/blocks/analytics/recipes/03-saved-report.ts +30 -0
  39. package/blocks/appointments/README.md +80 -0
  40. package/blocks/appointments/block.json +218 -0
  41. package/blocks/appointments/components/availability-editor.tsx +192 -0
  42. package/blocks/appointments/components/booking-page.tsx +154 -0
  43. package/blocks/appointments/components/event-type-editor.tsx +214 -0
  44. package/blocks/appointments/components/meeting-detail.tsx +147 -0
  45. package/blocks/appointments/components/meeting-list.tsx +148 -0
  46. package/blocks/appointments/hooks/use-available-slots.ts +29 -0
  47. package/blocks/appointments/hooks/use-schedules.ts +18 -0
  48. package/blocks/appointments/lib/appointments-types.ts +159 -0
  49. package/blocks/appointments/lib/idempotency.ts +30 -0
  50. package/blocks/appointments/recipes/01-create-event-type-and-hours.ts +27 -0
  51. package/blocks/appointments/recipes/02-find-slots-and-book.ts +34 -0
  52. package/blocks/appointments/recipes/03-bookings-digest.ts +22 -0
  53. package/blocks/appointments/recipes/04-calendar-invite.ts +21 -0
  54. package/blocks/block.schema.json +130 -0
  55. package/blocks/block.types.ts +109 -0
  56. package/blocks/campaigns/README.md +118 -0
  57. package/blocks/campaigns/block.json +361 -0
  58. package/blocks/campaigns/components/campaign-assets.tsx +180 -0
  59. package/blocks/campaigns/components/campaign-audience-step.tsx +91 -0
  60. package/blocks/campaigns/components/campaign-launch-step.tsx +147 -0
  61. package/blocks/campaigns/components/campaign-list.tsx +120 -0
  62. package/blocks/campaigns/components/campaign-metrics.tsx +93 -0
  63. package/blocks/campaigns/components/campaign-select.tsx +52 -0
  64. package/blocks/campaigns/components/campaign-sequence-step.tsx +158 -0
  65. package/blocks/campaigns/components/campaign-wizard.tsx +238 -0
  66. package/blocks/campaigns/components/form-field.tsx +29 -0
  67. package/blocks/campaigns/components/icp-editor.tsx +230 -0
  68. package/blocks/campaigns/components/persona-editor.tsx +210 -0
  69. package/blocks/campaigns/hooks/use-company-profile.ts +34 -0
  70. package/blocks/campaigns/lib/campaigns-utils.ts +155 -0
  71. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +33 -0
  72. package/blocks/campaigns/recipes/campaign-report.ts +22 -0
  73. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +51 -0
  74. package/blocks/campaigns/recipes/set-up-icp-and-personas.ts +37 -0
  75. package/blocks/coach/README.md +93 -0
  76. package/blocks/coach/block.json +125 -0
  77. package/blocks/coach/components/call-scorecard.tsx +126 -0
  78. package/blocks/coach/components/coaching-feed.tsx +140 -0
  79. package/blocks/coach/lib/coach-types.ts +116 -0
  80. package/blocks/coach/recipes/read-a-scorecard.ts +27 -0
  81. package/blocks/coach/recipes/weekly-coaching-digest.ts +53 -0
  82. package/blocks/copilot/README.md +99 -0
  83. package/blocks/copilot/block.json +151 -0
  84. package/blocks/copilot/components/copilot-memory.tsx +87 -0
  85. package/blocks/copilot/components/copilot-panel.tsx +189 -0
  86. package/blocks/copilot/components/copilot-team-chat.tsx +111 -0
  87. package/blocks/copilot/lib/copilot-types.ts +111 -0
  88. package/blocks/copilot/recipes/ask-the-copilot.ts +35 -0
  89. package/blocks/copilot/recipes/post-in-a-work-channel.ts +33 -0
  90. package/blocks/copilot/recipes/remember-and-recall.ts +19 -0
  91. package/blocks/crm/README.md +122 -0
  92. package/blocks/crm/block.json +378 -0
  93. package/blocks/crm/components/activity-timeline.tsx +99 -0
  94. package/blocks/crm/components/company-detail.tsx +118 -0
  95. package/blocks/crm/components/company-table.tsx +173 -0
  96. package/blocks/crm/components/contact-detail.tsx +135 -0
  97. package/blocks/crm/components/contact-table.tsx +217 -0
  98. package/blocks/crm/components/dedupe-review.tsx +164 -0
  99. package/blocks/crm/components/import-wizard.tsx +193 -0
  100. package/blocks/crm/components/notes-panel.tsx +112 -0
  101. package/blocks/crm/components/record-form.tsx +191 -0
  102. package/blocks/crm/components/task-list.tsx +107 -0
  103. package/blocks/crm/hooks/use-custom-fields.ts +18 -0
  104. package/blocks/crm/hooks/use-debounced.ts +13 -0
  105. package/blocks/crm/hooks/use-page-query.ts +52 -0
  106. package/blocks/crm/lib/crm-types.ts +136 -0
  107. package/blocks/crm/recipes/custom-fields-and-values.ts +28 -0
  108. package/blocks/crm/recipes/dedupe-and-merge.ts +30 -0
  109. package/blocks/crm/recipes/notes-tasks-and-activity.ts +23 -0
  110. package/blocks/crm/recipes/upsert-contact-with-company.ts +33 -0
  111. package/blocks/deals/README.md +78 -0
  112. package/blocks/deals/block.json +198 -0
  113. package/blocks/deals/components/deal-detail.tsx +134 -0
  114. package/blocks/deals/components/forecast-view.tsx +112 -0
  115. package/blocks/deals/components/pipeline-board.tsx +142 -0
  116. package/blocks/deals/components/stage-editor.tsx +96 -0
  117. package/blocks/deals/hooks/use-pipeline-deals.ts +67 -0
  118. package/blocks/deals/hooks/use-pipelines.ts +19 -0
  119. package/blocks/deals/lib/deals-types.ts +128 -0
  120. package/blocks/deals/recipes/01-create-deal-and-advance.ts +32 -0
  121. package/blocks/deals/recipes/02-pipeline-report.ts +35 -0
  122. package/blocks/deals/recipes/03-stale-deals-sweep.ts +20 -0
  123. package/blocks/dialer/README.md +114 -0
  124. package/blocks/dialer/block.json +260 -0
  125. package/blocks/dialer/components/call-log.tsx +78 -0
  126. package/blocks/dialer/components/call-queue.tsx +188 -0
  127. package/blocks/dialer/components/dialer-workspace.tsx +47 -0
  128. package/blocks/dialer/components/disposition-panel.tsx +75 -0
  129. package/blocks/dialer/components/live-calls-table.tsx +107 -0
  130. package/blocks/dialer/components/session-start-form.tsx +111 -0
  131. package/blocks/dialer/components/softphone.tsx +122 -0
  132. package/blocks/dialer/components/transcript-viewer.tsx +82 -0
  133. package/blocks/dialer/hooks/use-dialer-numbers.ts +19 -0
  134. package/blocks/dialer/hooks/use-dialer-session.ts +68 -0
  135. package/blocks/dialer/hooks/use-dispositions.ts +29 -0
  136. package/blocks/dialer/lib/dialer-types.ts +158 -0
  137. package/blocks/dialer/lib/softphone-transport.ts +67 -0
  138. package/blocks/dialer/lib/twilio-transport.ts +74 -0
  139. package/blocks/dialer/recipes/01-create-session-and-dial.ts +39 -0
  140. package/blocks/dialer/recipes/02-disposition-calls.ts +26 -0
  141. package/blocks/dialer/recipes/03-review-a-call.ts +23 -0
  142. package/blocks/enrichment/README.md +105 -0
  143. package/blocks/enrichment/block.json +204 -0
  144. package/blocks/enrichment/components/bulk-enrich-panel.tsx +205 -0
  145. package/blocks/enrichment/components/email-verify-badge.tsx +82 -0
  146. package/blocks/enrichment/components/enrich-button.tsx +136 -0
  147. package/blocks/enrichment/components/waterfall-config.tsx +127 -0
  148. package/blocks/enrichment/hooks/use-providers.ts +18 -0
  149. package/blocks/enrichment/lib/enrichment-types.ts +104 -0
  150. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +41 -0
  151. package/blocks/enrichment/recipes/lookup-and-verify.ts +25 -0
  152. package/blocks/enrichment/recipes/waterfall-pipeline.ts +26 -0
  153. package/blocks/forms/README.md +112 -0
  154. package/blocks/forms/block.json +200 -0
  155. package/blocks/forms/components/form-builder.tsx +150 -0
  156. package/blocks/forms/components/form-embed.tsx +103 -0
  157. package/blocks/forms/components/form-mapping-editor.tsx +125 -0
  158. package/blocks/forms/components/site-audit-panel.tsx +73 -0
  159. package/blocks/forms/components/submissions-table.tsx +158 -0
  160. package/blocks/forms/lib/form-types.ts +127 -0
  161. package/blocks/forms/recipes/install-snippet-and-form.ts +21 -0
  162. package/blocks/forms/recipes/retry-failed-deliveries.ts +26 -0
  163. package/blocks/forms/recipes/route-form-to-list.ts +44 -0
  164. package/blocks/inbox/README.md +72 -0
  165. package/blocks/inbox/block.json +194 -0
  166. package/blocks/inbox/components/inbox-list.tsx +156 -0
  167. package/blocks/inbox/components/inbox-screen.tsx +49 -0
  168. package/blocks/inbox/components/reply-composer.tsx +183 -0
  169. package/blocks/inbox/components/tag-and-assign.tsx +189 -0
  170. package/blocks/inbox/components/thread-badge.tsx +17 -0
  171. package/blocks/inbox/components/thread-view.tsx +108 -0
  172. package/blocks/inbox/hooks/use-inbox-thread.ts +24 -0
  173. package/blocks/inbox/hooks/use-inbox-threads.ts +40 -0
  174. package/blocks/inbox/lib/idempotency.ts +30 -0
  175. package/blocks/inbox/lib/inbox-types.ts +100 -0
  176. package/blocks/inbox/recipes/01-triage-and-reply.ts +43 -0
  177. package/blocks/inbox/recipes/02-tag-and-assign.ts +31 -0
  178. package/blocks/inbox/recipes/03-hot-contacts-digest.ts +16 -0
  179. package/blocks/integrations/README.md +75 -0
  180. package/blocks/integrations/block.json +172 -0
  181. package/blocks/integrations/components/integration-list.tsx +165 -0
  182. package/blocks/integrations/components/sync-status.tsx +223 -0
  183. package/blocks/integrations/components/webhook-manager.tsx +300 -0
  184. package/blocks/integrations/lib/integrations-utils.ts +62 -0
  185. package/blocks/integrations/recipes/crm-sync-health.ts +19 -0
  186. package/blocks/integrations/recipes/push-contacts-to-crm.ts +20 -0
  187. package/blocks/integrations/recipes/register-webhook.ts +18 -0
  188. package/blocks/integrations/recipes/verify-webhook-route.ts +33 -0
  189. package/blocks/intent/README.md +57 -0
  190. package/blocks/intent/block.json +138 -0
  191. package/blocks/intent/components/account-intent-score.tsx +91 -0
  192. package/blocks/intent/components/intent-feed.tsx +215 -0
  193. package/blocks/intent/components/intent-keyword-manager.tsx +331 -0
  194. package/blocks/intent/hooks/use-account-intent.ts +46 -0
  195. package/blocks/intent/lib/intent-utils.ts +95 -0
  196. package/blocks/intent/recipes/rank-intent-accounts.ts +36 -0
  197. package/blocks/intent/recipes/track-competitor-domain.ts +23 -0
  198. package/blocks/intent/recipes/who-visited-url.ts +18 -0
  199. package/blocks/lifecycle/README.md +79 -0
  200. package/blocks/lifecycle/block.json +198 -0
  201. package/blocks/lifecycle/components/churn-list.tsx +92 -0
  202. package/blocks/lifecycle/components/customer-table.tsx +119 -0
  203. package/blocks/lifecycle/components/lead-queue.tsx +127 -0
  204. package/blocks/lifecycle/components/lifecycle-board.tsx +102 -0
  205. package/blocks/lifecycle/components/stage-move-dialog.tsx +75 -0
  206. package/blocks/lifecycle/hooks/use-debounced.ts +13 -0
  207. package/blocks/lifecycle/hooks/use-lifecycle-stages.ts +27 -0
  208. package/blocks/lifecycle/lib/lifecycle.ts +99 -0
  209. package/blocks/lifecycle/recipes/customer-brief.ts +23 -0
  210. package/blocks/lifecycle/recipes/move-company-stage.ts +17 -0
  211. package/blocks/lifecycle/recipes/read-lifecycle-board.ts +21 -0
  212. package/blocks/lifecycle/recipes/unlock-stage-contacts.ts +25 -0
  213. package/blocks/lists/README.md +96 -0
  214. package/blocks/lists/block.json +166 -0
  215. package/blocks/lists/components/add-to-list-button.tsx +111 -0
  216. package/blocks/lists/components/list-picker.tsx +94 -0
  217. package/blocks/lists/components/list-table.tsx +160 -0
  218. package/blocks/lists/components/suppression-manager.tsx +153 -0
  219. package/blocks/lists/recipes/create-list-and-add-contacts.ts +35 -0
  220. package/blocks/lists/recipes/refresh-and-check-lists.ts +21 -0
  221. package/blocks/lists/recipes/suppress-and-reinstate.ts +30 -0
  222. package/blocks/mailboxes/README.md +71 -0
  223. package/blocks/mailboxes/block.json +187 -0
  224. package/blocks/mailboxes/components/connect-mailbox.tsx +284 -0
  225. package/blocks/mailboxes/components/domain-purchase.tsx +271 -0
  226. package/blocks/mailboxes/components/mailbox-list.tsx +185 -0
  227. package/blocks/mailboxes/components/warmup-health.tsx +232 -0
  228. package/blocks/mailboxes/lib/mailboxes-utils.ts +69 -0
  229. package/blocks/mailboxes/recipes/buy-domain-and-mailboxes.ts +64 -0
  230. package/blocks/mailboxes/recipes/connect-imap-mailbox.ts +38 -0
  231. package/blocks/mailboxes/recipes/is-it-safe-to-send.ts +17 -0
  232. package/blocks/mailboxes/recipes/warmup-report.ts +31 -0
  233. package/blocks/newsletter/README.md +122 -0
  234. package/blocks/newsletter/block.json +224 -0
  235. package/blocks/newsletter/components/issue-form.tsx +124 -0
  236. package/blocks/newsletter/components/issue-scheduler.tsx +152 -0
  237. package/blocks/newsletter/components/newsletter-editor.tsx +131 -0
  238. package/blocks/newsletter/components/newsletter-stats.tsx +93 -0
  239. package/blocks/newsletter/components/subscriber-table.tsx +173 -0
  240. package/blocks/newsletter/lib/newsletter-types.ts +97 -0
  241. package/blocks/newsletter/recipes/create-newsletter-and-draft-issue.ts +27 -0
  242. package/blocks/newsletter/recipes/import-audience-and-send.ts +43 -0
  243. package/blocks/newsletter/recipes/issue-performance-report.ts +26 -0
  244. package/blocks/nurture/README.md +110 -0
  245. package/blocks/nurture/block.json +191 -0
  246. package/blocks/nurture/components/nurture-builder.tsx +114 -0
  247. package/blocks/nurture/components/nurture-enroll.tsx +107 -0
  248. package/blocks/nurture/components/nurture-form.tsx +120 -0
  249. package/blocks/nurture/components/nurture-stats.tsx +81 -0
  250. package/blocks/nurture/lib/nurture-types.ts +109 -0
  251. package/blocks/nurture/recipes/create-a-nurture.ts +34 -0
  252. package/blocks/nurture/recipes/enroll-contacts-and-launch.ts +24 -0
  253. package/blocks/nurture/recipes/nurture-health-check.ts +28 -0
  254. package/blocks/objects/README.md +120 -0
  255. package/blocks/objects/block.json +243 -0
  256. package/blocks/objects/components/attribute-field-input.tsx +82 -0
  257. package/blocks/objects/components/attribute-form-dialog.tsx +159 -0
  258. package/blocks/objects/components/object-not-found.tsx +34 -0
  259. package/blocks/objects/components/object-record-detail.tsx +143 -0
  260. package/blocks/objects/components/object-schema-editor.tsx +197 -0
  261. package/blocks/objects/components/object-table.tsx +130 -0
  262. package/blocks/objects/components/record-form-dialog.tsx +112 -0
  263. package/blocks/objects/components/record-roles-panel.tsx +106 -0
  264. package/blocks/objects/hooks/use-object-schema.ts +36 -0
  265. package/blocks/objects/lib/object-types.ts +214 -0
  266. package/blocks/objects/recipes/define-object-and-add-records.ts +24 -0
  267. package/blocks/objects/recipes/update-audit-and-archive-records.ts +39 -0
  268. package/blocks/objects/recipes/upsert-records-from-your-system.ts +30 -0
  269. package/blocks/pages/README.md +104 -0
  270. package/blocks/pages/block.json +223 -0
  271. package/blocks/pages/components/brand-kit-editor.tsx +152 -0
  272. package/blocks/pages/components/content-library.tsx +163 -0
  273. package/blocks/pages/components/page-editor.tsx +253 -0
  274. package/blocks/pages/components/page-list.tsx +159 -0
  275. package/blocks/pages/components/seo-report.tsx +159 -0
  276. package/blocks/pages/lib/studio.ts +77 -0
  277. package/blocks/pages/recipes/audit-site-seo.ts +28 -0
  278. package/blocks/pages/recipes/create-and-publish-page.ts +26 -0
  279. package/blocks/pages/recipes/edit-page-section.ts +25 -0
  280. package/blocks/pages/recipes/refresh-brand-kit.ts +22 -0
  281. package/blocks/quotes/README.md +79 -0
  282. package/blocks/quotes/block.json +183 -0
  283. package/blocks/quotes/components/product-picker.tsx +80 -0
  284. package/blocks/quotes/components/quote-builder.tsx +195 -0
  285. package/blocks/quotes/components/quote-list.tsx +79 -0
  286. package/blocks/quotes/components/quote-viewer.tsx +135 -0
  287. package/blocks/quotes/components/signature-status.tsx +77 -0
  288. package/blocks/quotes/hooks/use-debounced.ts +13 -0
  289. package/blocks/quotes/lib/quotes.ts +142 -0
  290. package/blocks/quotes/recipes/create-and-send-quote.ts +44 -0
  291. package/blocks/quotes/recipes/revise-sent-quote.ts +18 -0
  292. package/blocks/quotes/recipes/track-quote-signature.ts +22 -0
  293. package/blocks/radar/README.md +106 -0
  294. package/blocks/radar/block.json +189 -0
  295. package/blocks/radar/components/competitor-battle-card.tsx +53 -0
  296. package/blocks/radar/components/competitor-tracker.tsx +158 -0
  297. package/blocks/radar/components/radar-overview.tsx +67 -0
  298. package/blocks/radar/components/radar-report.tsx +86 -0
  299. package/blocks/radar/hooks/use-competitors.ts +23 -0
  300. package/blocks/radar/lib/radar-types.ts +124 -0
  301. package/blocks/radar/recipes/battle-card.ts +28 -0
  302. package/blocks/radar/recipes/deep-dive-report.ts +32 -0
  303. package/blocks/radar/recipes/marketing-snapshot.ts +18 -0
  304. package/blocks/radar/recipes/track-and-scan.ts +39 -0
  305. package/blocks/search/README.md +129 -0
  306. package/blocks/search/block.json +185 -0
  307. package/blocks/search/components/filter-builder.tsx +149 -0
  308. package/blocks/search/components/prospect-results.tsx +175 -0
  309. package/blocks/search/components/save-to-list-dialog.tsx +135 -0
  310. package/blocks/search/components/saved-searches.tsx +95 -0
  311. package/blocks/search/hooks/use-filter-options.ts +41 -0
  312. package/blocks/search/lib/search-types.ts +149 -0
  313. package/blocks/search/recipes/explore-filter-values.ts +27 -0
  314. package/blocks/search/recipes/saved-searches.ts +34 -0
  315. package/blocks/search/recipes/search-and-save-to-list.ts +37 -0
  316. package/blocks/sequencer/README.md +87 -0
  317. package/blocks/sequencer/block.json +214 -0
  318. package/blocks/sequencer/components/enroll-dialog.tsx +93 -0
  319. package/blocks/sequencer/components/sequence-builder.tsx +306 -0
  320. package/blocks/sequencer/components/sequence-list.tsx +200 -0
  321. package/blocks/sequencer/components/sequence-stats.tsx +256 -0
  322. package/blocks/sequencer/components/step-editor.tsx +213 -0
  323. package/blocks/sequencer/lib/sequencer-utils.ts +168 -0
  324. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +51 -0
  325. package/blocks/sequencer/recipes/launch-and-enroll.ts +44 -0
  326. package/blocks/sequencer/recipes/pause-when-unsafe.ts +30 -0
  327. package/blocks/sequencer/recipes/sequence-report.ts +22 -0
  328. package/blocks/social-ads/README.md +128 -0
  329. package/blocks/social-ads/block.json +256 -0
  330. package/blocks/social-ads/components/ad-campaign-list.tsx +136 -0
  331. package/blocks/social-ads/components/audience-sync.tsx +150 -0
  332. package/blocks/social-ads/components/creative-gallery.tsx +121 -0
  333. package/blocks/social-ads/components/post-composer.tsx +122 -0
  334. package/blocks/social-ads/components/social-post-list.tsx +116 -0
  335. package/blocks/social-ads/hooks/use-social-accounts.ts +16 -0
  336. package/blocks/social-ads/lib/social-ads-types.ts +167 -0
  337. package/blocks/social-ads/recipes/creative-to-gallery.ts +41 -0
  338. package/blocks/social-ads/recipes/schedule-post.ts +51 -0
  339. package/blocks/social-ads/recipes/sync-audience-and-launch.ts +41 -0
  340. package/blocks/visitors/README.md +56 -0
  341. package/blocks/visitors/block.json +139 -0
  342. package/blocks/visitors/components/snippet-install.tsx +169 -0
  343. package/blocks/visitors/components/traffic-chart.tsx +156 -0
  344. package/blocks/visitors/components/visitor-company-card.tsx +92 -0
  345. package/blocks/visitors/components/visitor-feed.tsx +157 -0
  346. package/blocks/visitors/lib/visitors-utils.ts +78 -0
  347. package/blocks/visitors/recipes/check-snippet-installed.ts +24 -0
  348. package/blocks/visitors/recipes/recent-company-visits.ts +27 -0
  349. package/blocks/visitors/recipes/traffic-summary.ts +24 -0
  350. package/blocks/webchat/README.md +87 -0
  351. package/blocks/webchat/block.json +134 -0
  352. package/blocks/webchat/components/chat-widget-config.tsx +160 -0
  353. package/blocks/webchat/components/conversation-list.tsx +74 -0
  354. package/blocks/webchat/lib/webchat-types.ts +83 -0
  355. package/blocks/webchat/recipes/create-bot-and-style-widget.ts +24 -0
  356. package/blocks/webchat/recipes/list-chat-visitors.ts +24 -0
  357. package/blocks/workbench/README.md +135 -0
  358. package/blocks/workbench/block.json +259 -0
  359. package/blocks/workbench/components/column-recipe-editor.tsx +183 -0
  360. package/blocks/workbench/components/run-progress.tsx +132 -0
  361. package/blocks/workbench/components/workbench-add-rows.tsx +100 -0
  362. package/blocks/workbench/components/workbench-grid.tsx +159 -0
  363. package/blocks/workbench/components/workbench-row-actions.tsx +102 -0
  364. package/blocks/workbench/components/workbench-table-list.tsx +105 -0
  365. package/blocks/workbench/hooks/use-workbench-table.ts +30 -0
  366. package/blocks/workbench/lib/waterfall-presets.ts +119 -0
  367. package/blocks/workbench/lib/workbench-types.ts +191 -0
  368. package/blocks/workbench/recipes/build-enriched-table.ts +65 -0
  369. package/blocks/workbench/recipes/import-from-search.ts +42 -0
  370. package/blocks/workbench/recipes/scrape-into-table.ts +36 -0
  371. package/blocks/workbench/recipes/waterfall-column.ts +55 -0
  372. package/blocks/workflows/README.md +100 -0
  373. package/blocks/workflows/block.json +188 -0
  374. package/blocks/workflows/components/run-history.tsx +136 -0
  375. package/blocks/workflows/components/trigger-config.tsx +134 -0
  376. package/blocks/workflows/components/workflow-editor.tsx +142 -0
  377. package/blocks/workflows/components/workflow-list.tsx +153 -0
  378. package/blocks/workflows/hooks/use-workflow.ts +51 -0
  379. package/blocks/workflows/lib/graph.ts +187 -0
  380. package/blocks/workflows/recipes/create-workflow.ts +35 -0
  381. package/blocks/workflows/recipes/find-failed-runs.ts +15 -0
  382. package/blocks/workflows/recipes/run-workflow-and-wait.ts +32 -0
  383. package/blocks/workflows/recipes/set-trigger.ts +22 -0
  384. package/dist/cli.js +5330 -29
  385. package/dist/index.browser.js +7740 -0
  386. package/dist/index.browser.js.map +1 -0
  387. package/dist/index.browser.mjs +7717 -0
  388. package/dist/index.browser.mjs.map +1 -0
  389. package/dist/index.d.mts +2220 -23
  390. package/dist/index.d.ts +2220 -23
  391. package/dist/index.js +64 -3
  392. package/dist/index.js.map +1 -1
  393. package/dist/index.mjs +64 -3
  394. package/dist/index.mjs.map +1 -1
  395. package/dist/react.browser.js +7598 -0
  396. package/dist/react.browser.js.map +1 -0
  397. package/dist/react.browser.mjs +7568 -0
  398. package/dist/react.browser.mjs.map +1 -0
  399. package/dist/react.d.mts +2082 -20
  400. package/dist/react.d.ts +2082 -20
  401. package/dist/react.js +63 -2
  402. package/dist/react.js.map +1 -1
  403. package/dist/react.mjs +63 -2
  404. package/dist/react.mjs.map +1 -1
  405. package/dist/types/agentSetup.d.ts +6 -6
  406. package/dist/types/blocks/add.d.ts +53 -0
  407. package/dist/types/blocks/create.d.ts +46 -0
  408. package/dist/types/blocks/doctor.d.ts +26 -0
  409. package/dist/types/blocks/registry.d.ts +134 -0
  410. package/dist/types/cli.d.ts +16 -0
  411. package/dist/types/core.d.ts +1 -1
  412. package/dist/types/crypto-browser.d.ts +2 -0
  413. package/dist/types/crypto-node.d.ts +6 -0
  414. package/dist/types/react.d.ts +1 -1
  415. package/dist/types/version.d.ts +2 -2
  416. package/dist/types/webhooks.d.ts +2 -2
  417. package/dist/ui/index.d.mts +54 -0
  418. package/dist/ui/index.d.ts +54 -0
  419. package/dist/ui/index.js +105 -0
  420. package/dist/ui/index.js.map +1 -0
  421. package/dist/ui/index.mjs +44 -0
  422. package/dist/ui/index.mjs.map +1 -0
  423. package/dist/ui/lib/compose-refs.d.mts +19 -0
  424. package/dist/ui/lib/compose-refs.d.ts +19 -0
  425. package/dist/ui/lib/compose-refs.js +76 -0
  426. package/dist/ui/lib/compose-refs.js.map +1 -0
  427. package/dist/ui/lib/compose-refs.mjs +41 -0
  428. package/dist/ui/lib/compose-refs.mjs.map +1 -0
  429. package/dist/ui/lib/use-mobile.d.mts +4 -0
  430. package/dist/ui/lib/use-mobile.d.ts +4 -0
  431. package/dist/ui/lib/use-mobile.js +57 -0
  432. package/dist/ui/lib/use-mobile.js.map +1 -0
  433. package/dist/ui/lib/use-mobile.mjs +22 -0
  434. package/dist/ui/lib/use-mobile.mjs.map +1 -0
  435. package/dist/ui/lib/utils.d.mts +10 -0
  436. package/dist/ui/lib/utils.d.ts +10 -0
  437. package/dist/ui/lib/utils.js +33 -0
  438. package/dist/ui/lib/utils.js.map +1 -0
  439. package/dist/ui/lib/utils.mjs +9 -0
  440. package/dist/ui/lib/utils.mjs.map +1 -0
  441. package/dist/ui/primitives/alert-dialog.d.mts +22 -0
  442. package/dist/ui/primitives/alert-dialog.d.ts +22 -0
  443. package/dist/ui/primitives/alert-dialog.js +163 -0
  444. package/dist/ui/primitives/alert-dialog.js.map +1 -0
  445. package/dist/ui/primitives/alert-dialog.mjs +119 -0
  446. package/dist/ui/primitives/alert-dialog.mjs.map +1 -0
  447. package/dist/ui/primitives/avatar.d.mts +8 -0
  448. package/dist/ui/primitives/avatar.d.ts +8 -0
  449. package/dist/ui/primitives/avatar.js +91 -0
  450. package/dist/ui/primitives/avatar.js.map +1 -0
  451. package/dist/ui/primitives/avatar.mjs +55 -0
  452. package/dist/ui/primitives/avatar.mjs.map +1 -0
  453. package/dist/ui/primitives/breadcrumb.d.mts +13 -0
  454. package/dist/ui/primitives/breadcrumb.d.ts +13 -0
  455. package/dist/ui/primitives/breadcrumb.js +127 -0
  456. package/dist/ui/primitives/breadcrumb.js.map +1 -0
  457. package/dist/ui/primitives/breadcrumb.mjs +97 -0
  458. package/dist/ui/primitives/breadcrumb.mjs.map +1 -0
  459. package/dist/ui/primitives/button.d.mts +15 -0
  460. package/dist/ui/primitives/button.d.ts +15 -0
  461. package/dist/ui/primitives/button.js +91 -0
  462. package/dist/ui/primitives/button.js.map +1 -0
  463. package/dist/ui/primitives/button.mjs +56 -0
  464. package/dist/ui/primitives/button.mjs.map +1 -0
  465. package/dist/ui/primitives/card.d.mts +10 -0
  466. package/dist/ui/primitives/card.d.ts +10 -0
  467. package/dist/ui/primitives/card.js +101 -0
  468. package/dist/ui/primitives/card.js.map +1 -0
  469. package/dist/ui/primitives/card.mjs +62 -0
  470. package/dist/ui/primitives/card.mjs.map +1 -0
  471. package/dist/ui/primitives/carousel.d.mts +45 -0
  472. package/dist/ui/primitives/carousel.d.ts +45 -0
  473. package/dist/ui/primitives/carousel.js +300 -0
  474. package/dist/ui/primitives/carousel.js.map +1 -0
  475. package/dist/ui/primitives/carousel.mjs +261 -0
  476. package/dist/ui/primitives/carousel.mjs.map +1 -0
  477. package/dist/ui/primitives/chart.d.mts +68 -0
  478. package/dist/ui/primitives/chart.d.ts +68 -0
  479. package/dist/ui/primitives/chart.js +280 -0
  480. package/dist/ui/primitives/chart.js.map +1 -0
  481. package/dist/ui/primitives/chart.mjs +241 -0
  482. package/dist/ui/primitives/chart.mjs.map +1 -0
  483. package/dist/ui/primitives/collapsible.d.mts +8 -0
  484. package/dist/ui/primitives/collapsible.d.ts +8 -0
  485. package/dist/ui/primitives/collapsible.js +72 -0
  486. package/dist/ui/primitives/collapsible.js.map +1 -0
  487. package/dist/ui/primitives/collapsible.mjs +36 -0
  488. package/dist/ui/primitives/collapsible.mjs.map +1 -0
  489. package/dist/ui/primitives/context-menu.d.mts +29 -0
  490. package/dist/ui/primitives/context-menu.d.ts +29 -0
  491. package/dist/ui/primitives/context-menu.js +203 -0
  492. package/dist/ui/primitives/context-menu.js.map +1 -0
  493. package/dist/ui/primitives/context-menu.mjs +155 -0
  494. package/dist/ui/primitives/context-menu.mjs.map +1 -0
  495. package/dist/ui/primitives/dialog.d.mts +26 -0
  496. package/dist/ui/primitives/dialog.d.ts +26 -0
  497. package/dist/ui/primitives/dialog.js +192 -0
  498. package/dist/ui/primitives/dialog.js.map +1 -0
  499. package/dist/ui/primitives/dialog.mjs +148 -0
  500. package/dist/ui/primitives/dialog.mjs.map +1 -0
  501. package/dist/ui/primitives/drawer.d.mts +26 -0
  502. package/dist/ui/primitives/drawer.d.ts +26 -0
  503. package/dist/ui/primitives/drawer.js +146 -0
  504. package/dist/ui/primitives/drawer.js.map +1 -0
  505. package/dist/ui/primitives/drawer.mjs +103 -0
  506. package/dist/ui/primitives/drawer.mjs.map +1 -0
  507. package/dist/ui/primitives/input.d.mts +5 -0
  508. package/dist/ui/primitives/input.d.ts +5 -0
  509. package/dist/ui/primitives/input.js +46 -0
  510. package/dist/ui/primitives/input.js.map +1 -0
  511. package/dist/ui/primitives/input.mjs +22 -0
  512. package/dist/ui/primitives/input.mjs.map +1 -0
  513. package/dist/ui/primitives/item.d.mts +27 -0
  514. package/dist/ui/primitives/item.d.ts +27 -0
  515. package/dist/ui/primitives/item.js +221 -0
  516. package/dist/ui/primitives/item.js.map +1 -0
  517. package/dist/ui/primitives/item.mjs +188 -0
  518. package/dist/ui/primitives/item.mjs.map +1 -0
  519. package/dist/ui/primitives/kbd.d.mts +15 -0
  520. package/dist/ui/primitives/kbd.d.ts +15 -0
  521. package/dist/ui/primitives/kbd.js +45 -0
  522. package/dist/ui/primitives/kbd.js.map +1 -0
  523. package/dist/ui/primitives/kbd.mjs +21 -0
  524. package/dist/ui/primitives/kbd.mjs.map +1 -0
  525. package/dist/ui/primitives/label.d.mts +6 -0
  526. package/dist/ui/primitives/label.d.ts +6 -0
  527. package/dist/ui/primitives/label.js +55 -0
  528. package/dist/ui/primitives/label.js.map +1 -0
  529. package/dist/ui/primitives/label.mjs +21 -0
  530. package/dist/ui/primitives/label.mjs.map +1 -0
  531. package/dist/ui/primitives/resizable.d.mts +10 -0
  532. package/dist/ui/primitives/resizable.d.ts +10 -0
  533. package/dist/ui/primitives/resizable.js +68 -0
  534. package/dist/ui/primitives/resizable.js.map +1 -0
  535. package/dist/ui/primitives/resizable.mjs +42 -0
  536. package/dist/ui/primitives/resizable.mjs.map +1 -0
  537. package/dist/ui/primitives/save-status.d.mts +31 -0
  538. package/dist/ui/primitives/save-status.d.ts +31 -0
  539. package/dist/ui/primitives/save-status.js +150 -0
  540. package/dist/ui/primitives/save-status.js.map +1 -0
  541. package/dist/ui/primitives/save-status.mjs +126 -0
  542. package/dist/ui/primitives/save-status.mjs.map +1 -0
  543. package/dist/ui/primitives/separator.d.mts +6 -0
  544. package/dist/ui/primitives/separator.d.ts +6 -0
  545. package/dist/ui/primitives/separator.js +62 -0
  546. package/dist/ui/primitives/separator.js.map +1 -0
  547. package/dist/ui/primitives/separator.mjs +28 -0
  548. package/dist/ui/primitives/separator.mjs.map +1 -0
  549. package/dist/ui/primitives/sheet.d.mts +16 -0
  550. package/dist/ui/primitives/sheet.d.ts +16 -0
  551. package/dist/ui/primitives/sheet.js +170 -0
  552. package/dist/ui/primitives/sheet.js.map +1 -0
  553. package/dist/ui/primitives/sheet.mjs +129 -0
  554. package/dist/ui/primitives/sheet.mjs.map +1 -0
  555. package/dist/ui/primitives/sidebar.d.mts +74 -0
  556. package/dist/ui/primitives/sidebar.d.ts +74 -0
  557. package/dist/ui/primitives/sidebar.js +697 -0
  558. package/dist/ui/primitives/sidebar.js.map +1 -0
  559. package/dist/ui/primitives/sidebar.mjs +651 -0
  560. package/dist/ui/primitives/sidebar.mjs.map +1 -0
  561. package/dist/ui/primitives/skeleton.d.mts +5 -0
  562. package/dist/ui/primitives/skeleton.d.ts +5 -0
  563. package/dist/ui/primitives/skeleton.js +40 -0
  564. package/dist/ui/primitives/skeleton.js.map +1 -0
  565. package/dist/ui/primitives/skeleton.mjs +16 -0
  566. package/dist/ui/primitives/skeleton.mjs.map +1 -0
  567. package/dist/ui/primitives/slider.d.mts +20 -0
  568. package/dist/ui/primitives/slider.d.ts +20 -0
  569. package/dist/ui/primitives/slider.js +112 -0
  570. package/dist/ui/primitives/slider.js.map +1 -0
  571. package/dist/ui/primitives/slider.mjs +78 -0
  572. package/dist/ui/primitives/slider.mjs.map +1 -0
  573. package/dist/ui/primitives/sortable.d.mts +50 -0
  574. package/dist/ui/primitives/sortable.d.ts +50 -0
  575. package/dist/ui/primitives/sortable.js +456 -0
  576. package/dist/ui/primitives/sortable.js.map +1 -0
  577. package/dist/ui/primitives/sortable.mjs +435 -0
  578. package/dist/ui/primitives/sortable.mjs.map +1 -0
  579. package/dist/ui/primitives/switch.d.mts +6 -0
  580. package/dist/ui/primitives/switch.d.ts +6 -0
  581. package/dist/ui/primitives/switch.js +67 -0
  582. package/dist/ui/primitives/switch.js.map +1 -0
  583. package/dist/ui/primitives/switch.mjs +33 -0
  584. package/dist/ui/primitives/switch.mjs.map +1 -0
  585. package/dist/ui/primitives/table.d.mts +12 -0
  586. package/dist/ui/primitives/table.d.ts +12 -0
  587. package/dist/ui/primitives/table.js +130 -0
  588. package/dist/ui/primitives/table.js.map +1 -0
  589. package/dist/ui/primitives/table.mjs +89 -0
  590. package/dist/ui/primitives/table.mjs.map +1 -0
  591. package/dist/ui/primitives/tabs.d.mts +9 -0
  592. package/dist/ui/primitives/tabs.d.ts +9 -0
  593. package/dist/ui/primitives/tabs.js +86 -0
  594. package/dist/ui/primitives/tabs.js.map +1 -0
  595. package/dist/ui/primitives/tabs.mjs +49 -0
  596. package/dist/ui/primitives/tabs.mjs.map +1 -0
  597. package/dist/ui/primitives/textarea.d.mts +5 -0
  598. package/dist/ui/primitives/textarea.d.ts +5 -0
  599. package/dist/ui/primitives/textarea.js +43 -0
  600. package/dist/ui/primitives/textarea.js.map +1 -0
  601. package/dist/ui/primitives/textarea.mjs +19 -0
  602. package/dist/ui/primitives/textarea.mjs.map +1 -0
  603. package/dist/ui/primitives/tooltip.d.mts +9 -0
  604. package/dist/ui/primitives/tooltip.d.ts +9 -0
  605. package/dist/ui/primitives/tooltip.js +94 -0
  606. package/dist/ui/primitives/tooltip.js.map +1 -0
  607. package/dist/ui/primitives/tooltip.mjs +57 -0
  608. package/dist/ui/primitives/tooltip.mjs.map +1 -0
  609. package/docs/agents.md +72 -29
  610. package/docs/api/README.md +7 -3
  611. package/docs/api/appObjects.d.ts +3 -1
  612. package/docs/api/appObjects.md +2 -1
  613. package/docs/api/by-path.md +51 -0
  614. package/docs/api/dataPipelines.d.ts +149 -0
  615. package/docs/api/dataPipelines.md +232 -0
  616. package/docs/api/forms.d.ts +16 -0
  617. package/docs/api/forms.md +8 -0
  618. package/docs/api/objects.d.ts +705 -5
  619. package/docs/api/objects.md +1099 -14
  620. package/docs/api/ops.d.ts +1 -1
  621. package/docs/api/ops.md +1 -1
  622. package/docs/api/processLists.d.ts +247 -0
  623. package/docs/api/processLists.md +409 -0
  624. package/docs/api/quotes.d.ts +3 -1
  625. package/docs/api/quotes.md +7 -2
  626. package/docs/api/radar.d.ts +4 -0
  627. package/docs/api/radar.md +1 -0
  628. package/docs/api/reports.d.ts +476 -0
  629. package/docs/api/reports.md +539 -0
  630. package/docs/api/scheduleBuy.d.ts +12 -8
  631. package/docs/api/scheduleBuy.md +13 -10
  632. package/docs/api/seo.d.ts +406 -3
  633. package/docs/api/seo.md +505 -1
  634. package/docs/api/workspaceBackup.d.ts +324 -0
  635. package/docs/api/workspaceBackup.md +427 -0
  636. package/package.json +174 -5
  637. package/stacks/6sense.json +46 -0
  638. package/stacks/README.md +20 -0
  639. package/stacks/apollo.json +128 -0
  640. package/stacks/clay.json +59 -0
  641. package/stacks/gong.json +72 -0
  642. package/stacks/hubspot.json +162 -0
  643. package/stacks/intercom.json +53 -0
  644. package/stacks/mailchimp.json +71 -0
  645. package/stacks/outreach.json +86 -0
  646. package/stacks/salesforce.json +94 -0
  647. package/stacks/stack.schema.json +32 -0
  648. package/stacks/starter.json +22 -0
  649. package/styles/styles.css +2 -0
  650. package/styles/theme.css +181 -0
  651. package/styles/tokens.css +284 -0
  652. package/templates/next-app/README.md.tmpl +35 -0
  653. package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
  654. package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
  655. package/templates/next-app/app/globals.css.tmpl +8 -0
  656. package/templates/next-app/app/layout.tsx.tmpl +19 -0
  657. package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
  658. package/templates/next-app/dot-env.local.example.tmpl +6 -0
  659. package/templates/next-app/dot-gitignore.tmpl +7 -0
  660. package/templates/next-app/lib/nav.ts.tmpl +2 -0
  661. package/templates/next-app/next.config.mjs.tmpl +4 -0
  662. package/templates/next-app/package.json.tmpl +25 -0
  663. package/templates/next-app/postcss.config.mjs.tmpl +4 -0
  664. package/templates/next-app/tsconfig.json.tmpl +21 -0
@@ -0,0 +1,100 @@
1
+ "use client";
2
+
3
+ /**
4
+ * WorkbenchAddRows: put records into a workbench table by hand. Two tabs: one row typed into the table's own
5
+ * columns, or many rows pasted from a spreadsheet (tab or comma separated, first line = headers).
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. addWorkbenchRow POST /workbench/tables/{table_id}/rows (write) { data: { <column machine name>: value } }
9
+ * 2. pasteImportWorkbenchRows POST /workbench/tables/{table_id}/import/paste (write) { headers, rows }; new headers become columns
10
+ *
11
+ * Both are plain writes on rows the person typed, so neither needs ConfirmSpendDialog. Searching for people to
12
+ * import (which lands locked rows and costs credits) is a separate, billable flow: see recipes/import-from-search.ts.
13
+ */
14
+
15
+ import { useState } from "react";
16
+ import { Button, Input, Label, Textarea } from "@graph8/sdk/ui";
17
+ import { ApiError } from "@/g8/components/api-error";
18
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
19
+ import { g8Post } from "@/g8/lib/g8-client";
20
+ import { parsePasted } from "../lib/workbench-types";
21
+ import type { WorkbenchColumn } from "../lib/workbench-types";
22
+
23
+ /** The proxy rejects request bodies over 5 MiB; stay under it with room for the JSON wrapping. */
24
+ export const MAX_PASTE_BYTES = 4 * 1024 * 1024;
25
+
26
+ export interface WorkbenchAddRowsProps {
27
+ tableId: string;
28
+ columns: WorkbenchColumn[];
29
+ /** Called after rows were added so the grid can reload. */
30
+ onAdded?: (count: number) => void;
31
+ className?: string;
32
+ }
33
+
34
+ export function WorkbenchAddRows({ tableId, columns, onAdded, className }: WorkbenchAddRowsProps) {
35
+ const [tab, setTab] = useState<"row" | "paste">("row");
36
+ const [values, setValues] = useState<Record<string, string>>({});
37
+ const [pasted, setPasted] = useState("");
38
+ const [notice, setNotice] = useState<string | null>(null);
39
+
40
+ const typeable = columns.filter((c) => (c.column_type ?? "data") === "data" && !c.enrichment_type);
41
+ const parsed = parsePasted(pasted);
42
+ const width = parsed.headers.length;
43
+ const tooBig = new Blob([pasted]).size > MAX_PASTE_BYTES;
44
+ const ragged = parsed.rows.filter((r) => r.length !== width).length;
45
+
46
+ const addRow = useG8Mutation(
47
+ () => {
48
+ const data = Object.fromEntries(Object.entries(values).filter(([, v]) => v.trim() !== ""));
49
+ return g8Post(`/workbench/tables/${tableId}/rows`, { data });
50
+ },
51
+ { onSuccess: () => { setValues({}); setNotice("Row added."); onAdded?.(1); } },
52
+ );
53
+ const paste = useG8Mutation(
54
+ () => g8Post(`/workbench/tables/${tableId}/import/paste`, { headers: parsed.headers, rows: parsed.rows }),
55
+ { onSuccess: () => { setPasted(""); setNotice(`Imported ${parsed.rows.length} rows.`); onAdded?.(parsed.rows.length); } },
56
+ );
57
+
58
+ return (
59
+ <div className={`flex flex-col gap-3 rounded-md border p-3 ${className ?? ""}`}>
60
+ <div role="tablist" aria-label="Add rows" className="flex gap-2">
61
+ {(["row", "paste"] as const).map((t) => (
62
+ <Button key={t} role="tab" aria-selected={tab === t} size="sm" variant={tab === t ? "default" : "outline"} onClick={() => { setTab(t); setNotice(null); }}>
63
+ {t === "row" ? "Add a row" : "Paste from a spreadsheet"}
64
+ </Button>
65
+ ))}
66
+ </div>
67
+
68
+ {tab === "row" ? (
69
+ <div className="flex flex-col gap-2">
70
+ {typeable.length === 0 ? <p className="text-sm text-muted-foreground">This table has no plain data columns yet. Paste a spreadsheet to create them from its headers.</p> : null}
71
+ <div className="grid gap-2 sm:grid-cols-2">
72
+ {typeable.map((c) => (
73
+ <div key={c.id} className="flex flex-col gap-1">
74
+ <Label htmlFor={`wb-add-${c.name}`}>{c.display_name || c.name}</Label>
75
+ <Input id={`wb-add-${c.name}`} value={values[c.name] ?? ""} onChange={(e) => setValues({ ...values, [c.name]: e.target.value })} />
76
+ </div>
77
+ ))}
78
+ </div>
79
+ {addRow.error ? <ApiError error={addRow.error} title="Could not add the row" /> : null}
80
+ <div><Button size="sm" loading={addRow.loading} disabled={Object.values(values).every((v) => !v.trim())} onClick={() => void addRow.mutate(undefined)}>Add row</Button></div>
81
+ </div>
82
+ ) : (
83
+ <div className="flex flex-col gap-2">
84
+ <Label htmlFor="wb-paste">Paste rows (the first line is the header)</Label>
85
+ <Textarea id="wb-paste" rows={6} value={pasted} onChange={(e) => setPasted(e.target.value)} placeholder={"Company\tDomain\nAcme\tacme.com"} />
86
+ {width > 0 ? (
87
+ <p className="text-sm text-muted-foreground">
88
+ {parsed.rows.length.toLocaleString()} rows, {width} columns: {parsed.headers.join(", ")}.
89
+ {ragged ? ` ${ragged} rows have a different number of cells than the header; check them before importing.` : ""}
90
+ </p>
91
+ ) : null}
92
+ {tooBig ? <p role="alert" className="text-sm text-destructive">This is over 4 MB, more than one request can carry. Paste it in smaller parts.</p> : null}
93
+ {paste.error ? <ApiError error={paste.error} title="Could not import the rows" /> : null}
94
+ <div><Button size="sm" loading={paste.loading} disabled={parsed.rows.length === 0 || tooBig} onClick={() => void paste.mutate(undefined)}>Import {parsed.rows.length.toLocaleString()} rows</Button></div>
95
+ </div>
96
+ )}
97
+ {notice ? <p role="status" className="text-sm text-muted-foreground">{notice}</p> : null}
98
+ </div>
99
+ );
100
+ }
@@ -0,0 +1,159 @@
1
+ "use client";
2
+
3
+ /**
4
+ * WorkbenchGrid: the Clay-style table. Rows are records, columns are data or recipes (AI prompt, enrichment
5
+ * waterfall, formula). Select rows, edit a cell, add rows or columns, run a recipe column and watch it finish,
6
+ * unlock/export/delete the selection. It composes WorkbenchAddRows, ColumnRecipeEditor, RunProgress and
7
+ * WorkbenchRowActions around one table.
8
+ *
9
+ * SDK operations, in order:
10
+ * 1. getWorkbenchTable GET /workbench/tables/{table_id} (read) name, entity_type, row_count
11
+ * 2. listWorkbenchColumns GET /workbench/tables/{table_id}/columns (read) machine names rows are keyed by
12
+ * 3. listWorkbenchRows GET /workbench/tables/{table_id}/rows?start_row&end_row (read) windows of 100; "Load more" widens it
13
+ * 4. updateWorkbenchRow PATCH /workbench/tables/{table_id}/rows/{row_id} { field, value } (write) double-click a cell; Enter saves
14
+ * (the panels it opens call: addWorkbenchRow, pasteImportWorkbenchRows, addWorkbenchColumn, saveWorkbenchColumnAiConfig,
15
+ * saveWorkbenchWaterfall, createWorkbenchFormulaColumns, mapWorkbenchColumn, quote*Run + run* (billable, confirmed),
16
+ * getWorkbenchHistory, getWorkbenchAnalytics, quoteWorkbenchUnlock, unlockWorkbenchRows (billable, confirmed),
17
+ * exportWorkbenchTable, bulkDeleteWorkbenchRows (destructive, confirmed))
18
+ *
19
+ * A locked row (credit_charged false) shows masked cells and cannot be edited: unlock it first (1 credit per row).
20
+ * Cells show `extracted_data` (what a recipe wrote) before `raw_data` (what was imported).
21
+ */
22
+
23
+ import { useEffect, useState } from "react";
24
+ import { Button, Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
25
+ import { ApiError } from "@/g8/components/api-error";
26
+ import { EmptyState } from "@/g8/components/empty-state";
27
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
28
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
29
+ import { g8Get, g8Patch } from "@/g8/lib/g8-client";
30
+ import { useWorkbenchTable } from "../hooks/use-workbench-table";
31
+ import { cellValue, formatCell, isLocked, isMasked, pageItems, pageTotal } from "../lib/workbench-types";
32
+ import type { RunTarget, WorkbenchColumn, WorkbenchRow } from "../lib/workbench-types";
33
+ import { ColumnRecipeEditor } from "./column-recipe-editor";
34
+ import { RunProgress } from "./run-progress";
35
+ import { WorkbenchAddRows } from "./workbench-add-rows";
36
+ import { WorkbenchRowActions } from "./workbench-row-actions";
37
+
38
+ export interface WorkbenchGridProps {
39
+ tableId: string;
40
+ /** Rows per window. Default 100. */
41
+ pageSize?: number;
42
+ className?: string;
43
+ }
44
+
45
+ type Panel = "rows" | "columns" | "selection" | null;
46
+
47
+ function runTarget(column: WorkbenchColumn): RunTarget | null {
48
+ if (column.enrichment_type === "waterfall") return { kind: "waterfall", columnId: column.id };
49
+ if (column.enrichment_type === "ai") return { kind: "ai-column", columnId: column.id };
50
+ return null;
51
+ }
52
+
53
+ export function WorkbenchGrid({ tableId, pageSize = 100, className }: WorkbenchGridProps) {
54
+ const [end, setEnd] = useState(pageSize);
55
+ const [panel, setPanel] = useState<Panel>(null);
56
+ const [selected, setSelected] = useState<number[]>([]);
57
+ const [editing, setEditing] = useState<{ rowId: number; column: string; value: string } | null>(null);
58
+ const [running, setRunning] = useState<WorkbenchColumn | null>(null);
59
+ const { table, columns, loading: metaLoading, error: metaError, refetch: refetchMeta } = useWorkbenchTable(tableId);
60
+
61
+ const rows = useG8Query(async (signal) => (await g8Get(`/workbench/tables/${tableId}/rows`, { start_row: 0, end_row: end }, { signal })).data, [tableId, end]);
62
+ const items = pageItems<WorkbenchRow>(rows.data);
63
+ const total = pageTotal(rows.data, items.length);
64
+ const save = useG8Mutation(
65
+ (v: { rowId: number; column: string; value: string }) => g8Patch(`/workbench/tables/${tableId}/rows/${v.rowId}`, { field: v.column, value: v.value }),
66
+ { onSuccess: () => { setEditing(null); void rows.refetch(); } },
67
+ );
68
+ // Selection and open panels belong to one table and one set of rows: drop them when the table changes,
69
+ // and prune the selection to rows that still exist after any reload (delete, import, run).
70
+ useEffect(() => { setSelected([]); setEditing(null); setRunning(null); setEnd(pageSize); }, [tableId, pageSize]);
71
+ useEffect(() => {
72
+ if (rows.loading || !rows.data) return;
73
+ const alive = new Set(items.map((r) => r.id));
74
+ setSelected((cur) => (cur.every((id) => alive.has(id)) ? cur : cur.filter((id) => alive.has(id))));
75
+ }, [rows.data, rows.loading]); // eslint-disable-line react-hooks/exhaustive-deps
76
+ const reload = () => { void rows.refetch(); void refetchMeta(); };
77
+ const toggle = (id: number) => setSelected(selected.includes(id) ? selected.filter((x) => x !== id) : [...selected, id]);
78
+ const allOn = items.length > 0 && items.every((r) => selected.includes(r.id));
79
+ const entityType = table?.entity_type === "company" ? "company" : "contact";
80
+
81
+ if (metaError) return <ApiError error={metaError} title="Could not load the table" onRetry={() => void refetchMeta()} />;
82
+
83
+ return (
84
+ <div className={`flex flex-col gap-3 ${className ?? ""}`}>
85
+ <div className="flex flex-wrap items-center justify-between gap-2">
86
+ <div>
87
+ <h2 className="text-base font-semibold">{table?.name ?? "Workbench"}</h2>
88
+ <p className="text-xs text-muted-foreground">{total.toLocaleString()} rows, {columns.length} columns</p>
89
+ </div>
90
+ <div className="flex gap-2" role="toolbar" aria-label="Workbench tools">
91
+ {([["rows", "Add rows"], ["columns", "Add column"], ["selection", `Selection (${selected.length})`]] as const).map(([id, label]) => (
92
+ <Button key={id} size="sm" variant={panel === id ? "default" : "outline"} onClick={() => setPanel(panel === id ? null : id)}>{label}</Button>
93
+ ))}
94
+ </div>
95
+ </div>
96
+
97
+ {panel === "rows" ? <WorkbenchAddRows tableId={tableId} columns={columns} onAdded={reload} /> : null}
98
+ {panel === "columns" ? <ColumnRecipeEditor tableId={tableId} columns={columns} entityType={entityType} onSaved={reload} /> : null}
99
+ {panel === "selection" ? <WorkbenchRowActions tableId={tableId} rowIds={selected} entityType={entityType} onChanged={(what) => { if (what !== "exported") setSelected([]); reload(); }} /> : null}
100
+ {running ? (
101
+ <div className="rounded-md border p-3">
102
+ <RunProgress key={running.id} tableId={tableId} target={runTarget(running)!} rowIds={selected.length ? selected : undefined} label={running.display_name || running.name} onFinished={reload} />
103
+ <Button size="sm" variant="ghost" onClick={() => setRunning(null)}>Close</Button>
104
+ </div>
105
+ ) : null}
106
+ {save.error ? <ApiError error={save.error} title="Could not save the cell" /> : null}
107
+ {rows.error ? <ApiError error={rows.error} title="Could not load the rows" onRetry={() => void rows.refetch()} /> : null}
108
+
109
+ {(metaLoading || rows.loading) && items.length === 0 ? <Skeleton className="h-40 w-full" /> : null}
110
+ {!rows.loading && !rows.error && items.length === 0 ? <EmptyState title="No rows yet" description="Add a row, paste a spreadsheet, or import people from a search." /> : null}
111
+ {items.length > 0 ? (
112
+ <div className="overflow-x-auto rounded-md border">
113
+ <Table>
114
+ <TableHeader>
115
+ <TableRow>
116
+ <TableHead className="w-8"><input type="checkbox" aria-label="Select all rows" checked={allOn} onChange={() => setSelected(allOn ? [] : items.map((r) => r.id))} /></TableHead>
117
+ {columns.map((c) => {
118
+ const target = runTarget(c);
119
+ return (
120
+ <TableHead key={c.id}>
121
+ <div className="flex items-center gap-1">
122
+ <span>{c.display_name || c.name}</span>
123
+ {c.enrichment_type ? <span className="rounded bg-muted px-1 text-xs">{c.enrichment_type}</span> : null}
124
+ {target ? <Button size="sm" variant="ghost" aria-label={`Run ${c.display_name || c.name}`} onClick={() => setRunning(c)}>Run</Button> : null}
125
+ </div>
126
+ </TableHead>
127
+ );
128
+ })}
129
+ </TableRow>
130
+ </TableHeader>
131
+ <TableBody>
132
+ {items.map((row) => (
133
+ <TableRow key={row.id} data-state={selected.includes(row.id) ? "selected" : undefined}>
134
+ <TableCell><input type="checkbox" aria-label={`Select row ${row.id}`} checked={selected.includes(row.id)} onChange={() => toggle(row.id)} /></TableCell>
135
+ {columns.map((c) => {
136
+ const value = cellValue(row, c);
137
+ const isEdit = editing?.rowId === row.id && editing.column === c.name;
138
+ const editable = !isLocked(row) && (c.column_type ?? "data") === "data" && !c.enrichment_type;
139
+ return (
140
+ <TableCell key={c.id} onDoubleClick={() => editable && setEditing({ rowId: row.id, column: c.name, value: formatCell(value) })}>
141
+ {isEdit && editing ? (
142
+ <Input autoFocus aria-label={`Edit ${c.name}`} value={editing.value} onChange={(e) => setEditing({ ...editing, value: e.target.value })}
143
+ onKeyDown={(e) => { if (e.key === "Enter" && !save.loading) void save.mutate(editing); if (e.key === "Escape") setEditing(null); }} />
144
+ ) : (
145
+ <span className={isMasked(value) || (isLocked(row) && value == null) ? "text-muted-foreground" : ""} title={isLocked(row) ? "Locked: unlock the row to see this" : undefined}>{formatCell(value) || (isLocked(row) ? "locked" : "")}</span>
146
+ )}
147
+ </TableCell>
148
+ );
149
+ })}
150
+ </TableRow>
151
+ ))}
152
+ </TableBody>
153
+ </Table>
154
+ </div>
155
+ ) : null}
156
+ {items.length < total ? <div><Button size="sm" variant="outline" loading={rows.loading} onClick={() => setEnd(end + pageSize)}>Load more ({items.length.toLocaleString()} of {total.toLocaleString()})</Button></div> : null}
157
+ </div>
158
+ );
159
+ }
@@ -0,0 +1,102 @@
1
+ "use client";
2
+
3
+ /**
4
+ * WorkbenchRowActions: what to do with the selected rows of a workbench table. Unlock locked rows (reveal real
5
+ * emails and phones), send the rows to a graph8 list, or delete them.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. quoteWorkbenchUnlock POST .../unlock-quote { row_ids } (read, free) -> operation_id, locked_count, required_credits,
9
+ * available_credits, can_unlock, quote_expires_at. Locked rows come from prospect search imports and find-people.
10
+ * 2. unlockWorkbenchRows POST .../unlock-rows { row_ids, approved_max_credits, quote_expires_at, operation_id } (billable)
11
+ * ONE CREDIT PER ROW. Goes through ConfirmSpendDialog first with the quote's required_credits.
12
+ * 3. exportWorkbenchTable POST .../export { row_ids, list_id | create_new_list + new_list_name, entity_type } (write)
13
+ * The way finished rows leave the workbench for a list (ListPicker from the lists block).
14
+ * 4. bulkDeleteWorkbenchRows POST .../rows/bulk-delete { row_ids } (destructive) ConfirmSpendDialog first.
15
+ */
16
+
17
+ import { useEffect, useState } from "react";
18
+ import { Button } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
21
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
22
+ import { g8Post } from "@/g8/lib/g8-client";
23
+ import { ListPicker } from "@/g8/lists/components/list-picker";
24
+ import type { ListSummary } from "@/g8/lists/components/list-picker";
25
+ import type { CreditQuote } from "../lib/workbench-types";
26
+
27
+ export interface WorkbenchRowActionsProps {
28
+ tableId: string;
29
+ /** Row ids selected in the grid. The actions are disabled until at least one is selected. */
30
+ rowIds: number[];
31
+ entityType?: "contact" | "company";
32
+ /** Called after rows were unlocked, deleted or exported so the grid can reload. */
33
+ onChanged?: (what: "unlocked" | "deleted" | "exported") => void;
34
+ className?: string;
35
+ }
36
+
37
+ export function WorkbenchRowActions({ tableId, rowIds, entityType = "contact", onChanged, className }: WorkbenchRowActionsProps) {
38
+ const [quote, setQuote] = useState<CreditQuote | null>(null);
39
+ const [confirmDelete, setConfirmDelete] = useState(false);
40
+ const [listId, setListId] = useState<number | null>(null);
41
+ const [notice, setNotice] = useState<string | null>(null);
42
+ const none = rowIds.length === 0;
43
+ // A quote prices one selection: drop it (and its "nothing to unlock" note) when the selection changes.
44
+ const scope = rowIds.join(",");
45
+ useEffect(() => { setQuote(null); }, [scope]);
46
+
47
+ const getQuote = useG8Mutation(async () => (await g8Post<CreditQuote>(`/workbench/tables/${tableId}/unlock-quote`, { row_ids: rowIds })).data, { onSuccess: (q) => setQuote(q) });
48
+ const unlock = useG8Mutation(
49
+ async (q: CreditQuote) => (await g8Post<{ unlocked_count?: number }>(`/workbench/tables/${tableId}/unlock-rows`, {
50
+ row_ids: rowIds, approved_max_credits: q.required_credits, quote_expires_at: q.quote_expires_at, operation_id: q.operation_id,
51
+ })).data,
52
+ { onSuccess: (r) => { setQuote(null); setNotice(`Unlocked ${r.unlocked_count ?? "the"} rows.`); onChanged?.("unlocked"); } },
53
+ );
54
+ const remove = useG8Mutation(() => g8Post(`/workbench/tables/${tableId}/rows/bulk-delete`, { row_ids: rowIds }), {
55
+ onSuccess: () => { setConfirmDelete(false); setNotice(`Deleted ${rowIds.length} rows.`); onChanged?.("deleted"); },
56
+ });
57
+ const exportRows = useG8Mutation(
58
+ (list: number) => g8Post(`/workbench/tables/${tableId}/export`, { row_ids: rowIds, list_id: list, entity_type: entityType }),
59
+ { onSuccess: () => { setNotice(`Sent ${rowIds.length} rows to the list.`); onChanged?.("exported"); } },
60
+ );
61
+ const pick = (list: ListSummary) => setListId(list.id);
62
+ const noLocked = quote != null && (quote.locked_count ?? 0) === 0;
63
+
64
+ return (
65
+ <div className={`flex flex-col gap-3 rounded-md border p-3 ${className ?? ""}`}>
66
+ <p className="text-sm font-medium">{none ? "Select rows to act on them" : `${rowIds.length.toLocaleString()} rows selected`}</p>
67
+ <div className="flex flex-wrap items-end gap-3">
68
+ <ListPicker value={listId} onChange={pick} type={entityType === "company" ? "companies" : "contacts"} allowCreate />
69
+ <Button size="sm" variant="outline" disabled={none || listId == null} loading={exportRows.loading} onClick={() => listId != null && void exportRows.mutate(listId)}>Send to list</Button>
70
+ <Button size="sm" variant="outline" disabled={none} loading={getQuote.loading} onClick={() => void getQuote.mutate(undefined)}>Unlock locked rows</Button>
71
+ <Button size="sm" variant="outline" disabled={none} onClick={() => setConfirmDelete(true)}>Delete rows</Button>
72
+ </div>
73
+ {[getQuote.error, exportRows.error, remove.error, unlock.error].filter(Boolean).map((e, i) => <ApiError key={i} error={e} title="That did not work" />)}
74
+ {noLocked ? <p role="status" className="text-sm text-muted-foreground">None of the selected rows are locked. Nothing to unlock.</p> : null}
75
+ {notice ? <p role="status" className="text-sm text-muted-foreground">{notice}</p> : null}
76
+
77
+ <ConfirmSpendDialog
78
+ open={quote != null && !noLocked}
79
+ tier="billable"
80
+ title={`Unlock ${quote?.locked_count ?? 0} rows`}
81
+ description={quote?.can_unlock === false ? `Not enough credits: you need ${quote.required_credits} and have ${quote.available_credits}.` : "One credit per locked row. Rows that are already unlocked are not charged."}
82
+ estimatedCredits={quote?.required_credits}
83
+ affected={quote?.locked_count}
84
+ confirmLabel={quote?.can_unlock === false ? "Not enough credits" : "Unlock rows"}
85
+ confirming={unlock.loading}
86
+ onConfirm={() => { if (quote && quote.can_unlock !== false) void unlock.mutate(quote); }}
87
+ onCancel={() => setQuote(null)}
88
+ />
89
+ <ConfirmSpendDialog
90
+ open={confirmDelete}
91
+ tier="destructive"
92
+ title={`Delete ${rowIds.length} rows`}
93
+ description="The rows and everything in their cells are removed from this table."
94
+ affected={rowIds.length}
95
+ confirmLabel="Delete rows"
96
+ confirming={remove.loading}
97
+ onConfirm={() => void remove.mutate(undefined)}
98
+ onCancel={() => setConfirmDelete(false)}
99
+ />
100
+ </div>
101
+ );
102
+ }
@@ -0,0 +1,105 @@
1
+ "use client";
2
+
3
+ /**
4
+ * WorkbenchTableList: the workbenches of the workspace, a form to create one, and delete. Pick one to open it
5
+ * in a WorkbenchGrid. Pass `onOpen` to handle the pick yourself; OMIT it (for example as a stack nav page) and the
6
+ * component opens the chosen table in a WorkbenchGrid itself, with a back button.
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. listWorkbenchTables GET /workbench/tables?page&limit&filter_name (read) { tables, total }
10
+ * 2. createWorkbenchTable POST /workbench/tables { name, entity_type } (write) entity_type is "contact" or "company"; the id is a UUID string
11
+ * 3. deleteWorkbenchTable DELETE /workbench/tables/{table_id} (destructive) ConfirmSpendDialog first; removes the rows too
12
+ */
13
+
14
+ import { useEffect, useState } from "react";
15
+ import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
16
+ import { ApiError } from "@/g8/components/api-error";
17
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
18
+ import { EmptyState } from "@/g8/components/empty-state";
19
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
20
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
21
+ import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
22
+ import { WorkbenchGrid } from "./workbench-grid";
23
+ import { pageItems } from "../lib/workbench-types";
24
+ import type { WorkbenchTable } from "../lib/workbench-types";
25
+
26
+ export interface WorkbenchTableListProps {
27
+ /** Called when a person opens a table (or right after one is created). Omit it and the table opens in a WorkbenchGrid inside this component. */
28
+ onOpen?: (table: WorkbenchTable) => void;
29
+ className?: string;
30
+ }
31
+
32
+ export function WorkbenchTableList({ onOpen, className }: WorkbenchTableListProps) {
33
+ const [name, setName] = useState("");
34
+ const [entity, setEntity] = useState<"contact" | "company">("contact");
35
+ const [search, setSearch] = useState("");
36
+ const [term, setTerm] = useState("");
37
+ useEffect(() => { const t = setTimeout(() => setTerm(search), 250); return () => clearTimeout(t); }, [search]);
38
+ const [deleting, setDeleting] = useState<WorkbenchTable | null>(null);
39
+ const [opened, setOpened] = useState<WorkbenchTable | null>(null);
40
+ const open = (t: WorkbenchTable) => { if (onOpen) onOpen(t); else setOpened(t); };
41
+
42
+ const tables = useG8Query(async (signal) => pageItems<WorkbenchTable>((await g8Get("/workbench/tables", { page: 1, limit: 100, filter_name: term || undefined }, { signal })).data), [term]);
43
+ const create = useG8Mutation(
44
+ async () => (await g8Post<WorkbenchTable>("/workbench/tables", { name: name.trim(), entity_type: entity })).data,
45
+ { onSuccess: (t) => { setName(""); void tables.refetch(); open(t); } },
46
+ );
47
+ const remove = useG8Mutation((id: string) => g8Delete(`/workbench/tables/${id}`), { onSuccess: () => { setDeleting(null); void tables.refetch(); } });
48
+
49
+ if (!onOpen && opened) {
50
+ return (
51
+ <div className={`flex flex-col gap-4 ${className ?? ""}`}>
52
+ <div className="flex items-center gap-2">
53
+ <Button size="sm" variant="ghost" onClick={() => setOpened(null)}>Back to workbenches</Button>
54
+ <span className="text-sm font-medium">{opened.name}</span>
55
+ </div>
56
+ <WorkbenchGrid tableId={opened.id} />
57
+ </div>
58
+ );
59
+ }
60
+
61
+ return (
62
+ <div className={`flex flex-col gap-4 ${className ?? ""}`}>
63
+ <form className="flex flex-wrap items-end gap-2" onSubmit={(e) => { e.preventDefault(); if (name.trim()) void create.mutate(undefined); }}>
64
+ <div className="flex flex-col gap-1">
65
+ <Label htmlFor="wb-new-name">New workbench</Label>
66
+ <Input id="wb-new-name" value={name} onChange={(e) => setName(e.target.value)} placeholder="Series B fintech accounts" />
67
+ </div>
68
+ <select aria-label="Records are" className="h-9 rounded-md border bg-background px-2" value={entity} onChange={(e) => setEntity(e.target.value as "contact" | "company")}>
69
+ <option value="contact">People</option>
70
+ <option value="company">Companies</option>
71
+ </select>
72
+ <Button type="submit" loading={create.loading} disabled={!name.trim()}>Create</Button>
73
+ </form>
74
+ {create.error ? <ApiError error={create.error} title="Could not create the workbench" /> : null}
75
+
76
+ <Input aria-label="Search workbenches" placeholder="Search by name" value={search} onChange={(e) => setSearch(e.target.value)} />
77
+ {tables.error ? <ApiError error={tables.error} title="Could not load workbenches" onRetry={() => void tables.refetch()} /> : null}
78
+ {remove.error ? <ApiError error={remove.error} title="Could not delete the workbench" /> : null}
79
+ {tables.loading && !tables.data ? <Skeleton className="h-24 w-full" /> : null}
80
+ {!tables.loading && !tables.error && (tables.data ?? []).length === 0 ? <EmptyState title="No workbenches yet" description="Create one above, then paste rows or import people." /> : null}
81
+ <ul className="flex flex-col gap-2">
82
+ {(tables.data ?? []).map((t) => (
83
+ <li key={t.id} className="flex items-center justify-between gap-2 rounded-md border p-3">
84
+ <button type="button" className="flex flex-col text-left" onClick={() => open(t)}>
85
+ <span className="text-sm font-medium">{t.name}</span>
86
+ <span className="text-xs text-muted-foreground">{(t.row_count ?? 0).toLocaleString()} rows, {t.entity_type === "company" ? "companies" : "people"}</span>
87
+ </button>
88
+ <Button size="sm" variant="ghost" onClick={() => setDeleting(t)}>Delete</Button>
89
+ </li>
90
+ ))}
91
+ </ul>
92
+ <ConfirmSpendDialog
93
+ open={deleting != null}
94
+ tier="destructive"
95
+ title={`Delete "${deleting?.name ?? ""}"`}
96
+ description="The table, its columns and all of its rows are removed."
97
+ affected={deleting?.row_count ?? undefined}
98
+ confirmLabel="Delete workbench"
99
+ confirming={remove.loading}
100
+ onConfirm={() => deleting && void remove.mutate(deleting.id)}
101
+ onCancel={() => setDeleting(null)}
102
+ />
103
+ </div>
104
+ );
105
+ }
@@ -0,0 +1,30 @@
1
+ "use client";
2
+
3
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
4
+ import { g8Get } from "@/g8/lib/g8-client";
5
+ import { pageItems } from "../lib/workbench-types";
6
+ import type { WorkbenchColumn, WorkbenchTable } from "../lib/workbench-types";
7
+
8
+ /**
9
+ * A workbench table and its columns (`GET /workbench/tables/{id}` then `GET /workbench/tables/{id}/columns`).
10
+ * Rows are loaded by the grid in windows; columns are small, so they load whole. `refetch` re-reads both,
11
+ * which is what to call after a column was added or a run finished.
12
+ */
13
+ export function useWorkbenchTable(tableId: string | null | undefined) {
14
+ const enabled = Boolean(tableId);
15
+ const table = useG8Query(async (signal) => (await g8Get<WorkbenchTable>(`/workbench/tables/${tableId}`, undefined, { signal })).data, [tableId], { enabled });
16
+ const columns = useG8Query(
17
+ async (signal) => pageItems<WorkbenchColumn>((await g8Get(`/workbench/tables/${tableId}/columns`, undefined, { signal })).data),
18
+ [tableId],
19
+ { enabled },
20
+ );
21
+ return {
22
+ table: table.data,
23
+ columns: columns.data ?? [],
24
+ loading: table.loading || columns.loading,
25
+ error: table.error ?? columns.error,
26
+ refetch: async () => {
27
+ await Promise.all([table.refetch(), columns.refetch()]);
28
+ },
29
+ };
30
+ }
@@ -0,0 +1,119 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Ready-made waterfall recipes for the fields people enrich most. Each step is one provider call; the
5
+ * waterfall tries them in order and stops at the first value. The provider/action/output path triples come
6
+ * from graph8's own waterfall templates (the same ones the app seeds), so a step the person does not touch
7
+ * works the way the app's would.
8
+ *
9
+ * `input_mapping` maps a provider's parameter to a graph8 target field (CONTACT_FIRST_NAME, COMPANY_WEBSITE...).
10
+ * The table's own columns must be mapped to those targets first (ColumnRecipeEditor, "Map columns" tab):
11
+ * a waterfall step whose inputs are not mapped has nothing to look up.
12
+ */
13
+
14
+ export interface PresetStep {
15
+ provider: string;
16
+ action: string;
17
+ actionDisplayName: string;
18
+ outputField: string;
19
+ inputMapping: Record<string, string>;
20
+ }
21
+
22
+ export interface WaterfallPreset {
23
+ id: string;
24
+ label: string;
25
+ /** Default name for the new column. */
26
+ columnName: string;
27
+ steps: PresetStep[];
28
+ }
29
+
30
+ const PERSON_BY_NAME = { first_name: "CONTACT_FIRST_NAME", last_name: "CONTACT_LAST_NAME" } as const;
31
+ const WEB = { domain: "COMPANY_WEBSITE" } as const;
32
+
33
+ export const WATERFALL_PRESETS: WaterfallPreset[] = [
34
+ {
35
+ id: "work_email",
36
+ label: "Work email",
37
+ columnName: "Work email",
38
+ steps: [
39
+ { provider: "apollo", action: "enrich_person", actionDisplayName: "Enrich Person", outputField: "person.email", inputMapping: { ...PERSON_BY_NAME, ...WEB, linkedin_url: "CONTACT_LINKEDIN_URL" } },
40
+ { provider: "leadmagic", action: "email_finder", actionDisplayName: "Find Email", outputField: "email", inputMapping: { ...PERSON_BY_NAME, ...WEB, company_name: "COMPANY_NAME" } },
41
+ { provider: "prospeo", action: "find_email", actionDisplayName: "Find Email", outputField: "result.email", inputMapping: { linkedin_url: "CONTACT_LINKEDIN_URL", ...WEB, ...PERSON_BY_NAME } },
42
+ { provider: "hunter", action: "find_email", actionDisplayName: "Find Email", outputField: "data.email", inputMapping: { ...PERSON_BY_NAME, ...WEB, company_name: "COMPANY_NAME" } },
43
+ { provider: "dropcontact", action: "find_work_email", actionDisplayName: "Find Work Email", outputField: "result.email", inputMapping: { ...PERSON_BY_NAME, ...WEB, linkedin_url: "CONTACT_LINKEDIN_URL" } },
44
+ ],
45
+ },
46
+ {
47
+ id: "mobile_phone",
48
+ label: "Mobile phone",
49
+ columnName: "Mobile phone",
50
+ steps: [
51
+ { provider: "leadmagic", action: "mobile_finder", actionDisplayName: "Mobile Finder", outputField: "mobile_number", inputMapping: { ...PERSON_BY_NAME, linkedin_url: "CONTACT_LINKEDIN_URL" } },
52
+ { provider: "prospeo", action: "mobile_finder", actionDisplayName: "Mobile Finder", outputField: "phone.raw_format", inputMapping: { url: "CONTACT_LINKEDIN_URL" } },
53
+ { provider: "apollo", action: "enrich_person", actionDisplayName: "Enrich Person (Mobile)", outputField: "person.phone_numbers.raw_number", inputMapping: { linkedin_url: "CONTACT_LINKEDIN_URL", ...PERSON_BY_NAME, ...WEB } },
54
+ ],
55
+ },
56
+ {
57
+ id: "linkedin_url",
58
+ label: "Person LinkedIn URL",
59
+ columnName: "LinkedIn URL",
60
+ steps: [
61
+ { provider: "icypeas", action: "find_profile_urls", actionDisplayName: "Find Profile URLs", outputField: "profile.linkedin_url", inputMapping: { ...PERSON_BY_NAME, ...WEB } },
62
+ { provider: "apollo", action: "enrich_person", actionDisplayName: "Enrich Person", outputField: "person.linkedin_url", inputMapping: { ...PERSON_BY_NAME, ...WEB } },
63
+ ],
64
+ },
65
+ {
66
+ id: "company_phone",
67
+ label: "Company phone",
68
+ columnName: "Company phone",
69
+ steps: [{ provider: "apollo", action: "enrich_company", actionDisplayName: "Enrich Company", outputField: "organization.phone", inputMapping: { ...WEB } }],
70
+ },
71
+ ];
72
+
73
+ /** The graph8 target fields a step reads, for example CONTACT_FIRST_NAME. */
74
+ export function requiredTargets(steps: PresetStep[]): string[] {
75
+ return [...new Set(steps.flatMap((s) => Object.values(s.inputMapping)))].sort();
76
+ }
77
+
78
+ export interface ProviderInfo { provider: string; system_funded: boolean; byok_configured: boolean }
79
+
80
+ /** The save body for `POST /workbench/tables/{id}/waterfall/configs`. Saving never runs or charges anything. */
81
+ export function buildWaterfallConfig(name: string, steps: PresetStep[], catalog: ProviderInfo[], id: () => string) {
82
+ const known = new Map(catalog.map((p) => [p.provider, p]));
83
+ const columnId = `waterfall_${id().replace(/-/g, "_")}`;
84
+ return {
85
+ config: {
86
+ id: id(),
87
+ name,
88
+ column_id: columnId,
89
+ field_to_enrich: name,
90
+ data_type: "text",
91
+ is_existing_column: false,
92
+ steps: steps.map((s, order) => {
93
+ const p = known.get(s.provider);
94
+ return {
95
+ id: `step_${s.provider}_${order}`,
96
+ provider: s.provider,
97
+ action: s.action,
98
+ action_display_name: s.actionDisplayName,
99
+ output_field: s.outputField,
100
+ output_mapping: {},
101
+ input_mapping: s.inputMapping,
102
+ order,
103
+ config: {},
104
+ enabled: true,
105
+ // Own key wins; otherwise graph8's key (and its credits).
106
+ is_system_provider: p ? !p.byok_configured && p.system_funded : true,
107
+ stop_on_success: true,
108
+ confidence_threshold: 0,
109
+ max_retries: 0,
110
+ };
111
+ }),
112
+ output_successful_provider: false,
113
+ hide_provider_columns: true,
114
+ skip_existing_values: true,
115
+ skip_recently_enriched: true,
116
+ auto_update: false,
117
+ },
118
+ };
119
+ }