@graph8/sdk 0.361.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 (388) hide show
  1. package/README.md +14 -10
  2. package/blocks/_foundation/README.md +172 -1
  3. package/blocks/_foundation/block.json +234 -13
  4. package/blocks/_foundation/components/api-error.tsx +71 -25
  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/empty-state.tsx +39 -7
  9. package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
  10. package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
  11. package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
  12. package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
  13. package/blocks/_foundation/components/screen/index.ts +22 -0
  14. package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
  17. package/blocks/_foundation/components/screen/section-card.tsx +47 -0
  18. package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
  19. package/blocks/_foundation/components/screen/split-view.tsx +125 -0
  20. package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
  21. package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
  22. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  23. package/blocks/_foundation/components/screen/time-series-chart.tsx +32 -0
  24. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  25. package/blocks/_foundation/components/screen/toolbar.tsx +144 -0
  26. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  27. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  28. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  29. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  30. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  31. package/blocks/_foundation/hooks/use-url-state.ts +74 -0
  32. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  33. package/blocks/_foundation/lib/g8-client.ts +1 -1
  34. package/blocks/_foundation/lib/plural.ts +23 -0
  35. package/blocks/_foundation/server/connect-route.ts +15 -0
  36. package/blocks/_foundation/server/connect.ts +127 -0
  37. package/blocks/_foundation/server/local-key.ts +70 -0
  38. package/blocks/_foundation/server/proxy.ts +103 -3
  39. package/blocks/_foundation/server/route.ts +16 -4
  40. package/blocks/agents/README.md +14 -0
  41. package/blocks/agents/block.json +33 -1
  42. package/blocks/agents/components/agent-builder.tsx +13 -8
  43. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
  44. package/blocks/agents/components/agent-run-detail.tsx +102 -0
  45. package/blocks/agents/components/agent-runs.tsx +16 -7
  46. package/blocks/agents/components/agents-screen.tsx +184 -0
  47. package/blocks/analytics/README.md +13 -0
  48. package/blocks/analytics/block.json +45 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +209 -0
  50. package/blocks/analytics/components/leaderboard.tsx +14 -7
  51. package/blocks/analytics/components/report-builder.tsx +23 -9
  52. package/blocks/analytics/components/report-detail.tsx +84 -0
  53. package/blocks/analytics/components/trend-chart.tsx +62 -0
  54. package/blocks/appointments/README.md +21 -1
  55. package/blocks/appointments/block.json +43 -2
  56. package/blocks/appointments/components/availability-editor.tsx +17 -5
  57. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  58. package/blocks/appointments/components/event-type-related.tsx +77 -0
  59. package/blocks/appointments/components/meeting-list.tsx +14 -6
  60. package/blocks/appointments/components/meetings-screen.tsx +157 -0
  61. package/blocks/appointments/components/scheduling-screen.tsx +122 -0
  62. package/blocks/appointments/hooks/use-event-types.ts +17 -0
  63. package/blocks/block.schema.json +18 -0
  64. package/blocks/block.types.ts +26 -0
  65. package/blocks/campaigns/README.md +41 -11
  66. package/blocks/campaigns/block.json +105 -42
  67. package/blocks/campaigns/components/campaign-assets.tsx +16 -5
  68. package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
  69. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  70. package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
  71. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  72. package/blocks/campaigns/components/icp-detail.tsx +154 -0
  73. package/blocks/campaigns/components/icp-editor.tsx +15 -15
  74. package/blocks/campaigns/components/persona-detail.tsx +172 -0
  75. package/blocks/campaigns/components/persona-editor.tsx +14 -11
  76. package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
  77. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  78. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  79. package/blocks/coach/README.md +10 -0
  80. package/blocks/coach/block.json +16 -1
  81. package/blocks/coach/components/coach-screen.tsx +222 -0
  82. package/blocks/copilot/README.md +13 -0
  83. package/blocks/copilot/block.json +14 -1
  84. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  85. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  86. package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
  87. package/blocks/crm/README.md +28 -0
  88. package/blocks/crm/block.json +58 -2
  89. package/blocks/crm/components/companies-screen.tsx +345 -0
  90. package/blocks/crm/components/company-detail.tsx +8 -6
  91. package/blocks/crm/components/company-table.tsx +11 -4
  92. package/blocks/crm/components/contact-detail.tsx +6 -3
  93. package/blocks/crm/components/contact-table.tsx +10 -3
  94. package/blocks/crm/components/contacts-screen.tsx +359 -0
  95. package/blocks/crm/components/dedupe-review.tsx +7 -5
  96. package/blocks/crm/components/import-wizard.tsx +4 -2
  97. package/blocks/crm/components/notes-panel.tsx +4 -4
  98. package/blocks/crm/components/record-form.tsx +5 -3
  99. package/blocks/crm/components/task-list.tsx +4 -4
  100. package/blocks/crm/components/tasks-screen.tsx +379 -0
  101. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  102. package/blocks/crm/hooks/use-page-query.ts +7 -1
  103. package/blocks/crm/lib/crm-types.ts +12 -2
  104. package/blocks/crm/lib/screen-parts.tsx +105 -0
  105. package/blocks/deals/README.md +34 -3
  106. package/blocks/deals/block.json +45 -4
  107. package/blocks/deals/components/deal-detail.tsx +27 -13
  108. package/blocks/deals/components/deal-related.tsx +87 -0
  109. package/blocks/deals/components/deals-screen.tsx +298 -0
  110. package/blocks/deals/components/pipeline-board.tsx +4 -2
  111. package/blocks/deals/components/stage-editor.tsx +4 -4
  112. package/blocks/deals/hooks/use-default-owner.ts +60 -0
  113. package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
  114. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  115. package/blocks/deals/lib/deals-types.ts +14 -0
  116. package/blocks/dialer/README.md +34 -2
  117. package/blocks/dialer/block.json +67 -1
  118. package/blocks/dialer/components/call-contact-button.tsx +252 -0
  119. package/blocks/dialer/components/call-log-screen.tsx +205 -0
  120. package/blocks/dialer/components/call-log.tsx +7 -3
  121. package/blocks/dialer/components/call-queue.tsx +1 -1
  122. package/blocks/dialer/components/dialer-screen.tsx +319 -0
  123. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  124. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  125. package/blocks/dialer/components/session-start-form.tsx +4 -2
  126. package/blocks/dialer/components/softphone.tsx +3 -1
  127. package/blocks/enrichment/README.md +14 -2
  128. package/blocks/enrichment/block.json +32 -4
  129. package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
  130. package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
  131. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  132. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  133. package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
  134. package/blocks/enrichment/components/waterfall-config.tsx +39 -10
  135. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  136. package/blocks/forms/README.md +12 -0
  137. package/blocks/forms/block.json +39 -17
  138. package/blocks/forms/components/form-builder.tsx +3 -1
  139. package/blocks/forms/components/form-embed.tsx +3 -1
  140. package/blocks/forms/components/forms-screen.tsx +353 -0
  141. package/blocks/forms/components/submissions-table.tsx +16 -6
  142. package/blocks/inbox/README.md +27 -3
  143. package/blocks/inbox/block.json +14 -2
  144. package/blocks/inbox/components/inbox-list.tsx +7 -3
  145. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  146. package/blocks/inbox/components/reply-composer.tsx +65 -9
  147. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  148. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  149. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  150. package/blocks/inbox/lib/inbox-types.ts +47 -1
  151. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  152. package/blocks/integrations/README.md +9 -0
  153. package/blocks/integrations/block.json +46 -1
  154. package/blocks/integrations/components/crm-detail.tsx +100 -0
  155. package/blocks/integrations/components/integration-list.tsx +9 -4
  156. package/blocks/integrations/components/integrations-screen.tsx +135 -0
  157. package/blocks/integrations/components/sync-status.tsx +5 -3
  158. package/blocks/integrations/components/webhook-detail.tsx +160 -0
  159. package/blocks/integrations/components/webhook-manager.tsx +13 -8
  160. package/blocks/integrations/lib/integrations-utils.ts +6 -1
  161. package/blocks/intent/README.md +14 -0
  162. package/blocks/intent/block.json +27 -1
  163. package/blocks/intent/components/intent-feed.tsx +5 -2
  164. package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
  165. package/blocks/intent/components/intent-screen.tsx +265 -0
  166. package/blocks/intent/components/keyword-detail.tsx +124 -0
  167. package/blocks/lifecycle/README.md +16 -0
  168. package/blocks/lifecycle/block.json +34 -7
  169. package/blocks/lifecycle/components/account-related.tsx +66 -0
  170. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  171. package/blocks/lifecycle/components/lifecycle-screen.tsx +149 -0
  172. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  173. package/blocks/lists/README.md +13 -0
  174. package/blocks/lists/block.json +39 -1
  175. package/blocks/lists/components/add-to-list-button.tsx +3 -2
  176. package/blocks/lists/components/list-picker.tsx +5 -1
  177. package/blocks/lists/components/list-table.tsx +11 -5
  178. package/blocks/lists/components/lists-screen.tsx +344 -0
  179. package/blocks/lists/components/suppression-manager.tsx +6 -2
  180. package/blocks/lists/components/suppressions-screen.tsx +255 -0
  181. package/blocks/mailboxes/README.md +17 -0
  182. package/blocks/mailboxes/block.json +65 -17
  183. package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
  184. package/blocks/mailboxes/components/domain-detail.tsx +142 -0
  185. package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
  186. package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
  187. package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
  188. package/blocks/mailboxes/components/warmup-health.tsx +4 -2
  189. package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
  190. package/blocks/newsletter/README.md +13 -0
  191. package/blocks/newsletter/block.json +47 -23
  192. package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
  193. package/blocks/newsletter/components/newsletter-screen.tsx +404 -0
  194. package/blocks/newsletter/components/subscriber-table.tsx +12 -6
  195. package/blocks/nurture/README.md +12 -0
  196. package/blocks/nurture/block.json +35 -13
  197. package/blocks/nurture/components/nurture-builder.tsx +3 -1
  198. package/blocks/nurture/components/nurture-enroll.tsx +6 -3
  199. package/blocks/nurture/components/nurture-form.tsx +23 -3
  200. package/blocks/nurture/components/nurture-screen.tsx +283 -0
  201. package/blocks/nurture/lib/nurture-types.ts +1 -1
  202. package/blocks/objects/README.md +17 -0
  203. package/blocks/objects/block.json +29 -16
  204. package/blocks/objects/components/object-not-found.tsx +3 -1
  205. package/blocks/objects/components/object-record-detail.tsx +2 -1
  206. package/blocks/objects/components/object-schema-editor.tsx +3 -1
  207. package/blocks/objects/components/object-table.tsx +13 -3
  208. package/blocks/objects/components/objects-screen.tsx +147 -0
  209. package/blocks/objects/lib/object-types.ts +1 -1
  210. package/blocks/pages/README.md +12 -0
  211. package/blocks/pages/block.json +35 -22
  212. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  213. package/blocks/pages/components/content-library.tsx +46 -11
  214. package/blocks/pages/components/page-list.tsx +6 -3
  215. package/blocks/pages/components/pages-screen.tsx +154 -0
  216. package/blocks/quotes/README.md +17 -0
  217. package/blocks/quotes/block.json +38 -9
  218. package/blocks/quotes/components/product-picker.tsx +3 -1
  219. package/blocks/quotes/components/quote-list.tsx +7 -3
  220. package/blocks/quotes/components/quote-related.tsx +123 -0
  221. package/blocks/quotes/components/quotes-screen.tsx +169 -0
  222. package/blocks/radar/README.md +10 -0
  223. package/blocks/radar/block.json +41 -1
  224. package/blocks/radar/components/competitor-detail.tsx +117 -0
  225. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  226. package/blocks/radar/components/radar-report.tsx +4 -2
  227. package/blocks/radar/components/radar-screen.tsx +214 -0
  228. package/blocks/radar/components/report-detail.tsx +104 -0
  229. package/blocks/search/README.md +10 -0
  230. package/blocks/search/block.json +16 -1
  231. package/blocks/search/components/filter-builder.tsx +3 -3
  232. package/blocks/search/components/prospect-results.tsx +5 -3
  233. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  234. package/blocks/search/components/saved-searches.tsx +4 -1
  235. package/blocks/search/hooks/use-filter-options.ts +20 -6
  236. package/blocks/search/lib/search-types.ts +2 -0
  237. package/blocks/sequencer/README.md +49 -9
  238. package/blocks/sequencer/block.json +99 -19
  239. package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
  240. package/blocks/sequencer/components/sequence-builder.tsx +211 -46
  241. package/blocks/sequencer/components/sequence-detail.tsx +259 -0
  242. package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
  243. package/blocks/sequencer/components/sequence-list.tsx +33 -8
  244. package/blocks/sequencer/components/sequence-stats.tsx +3 -2
  245. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  246. package/blocks/sequencer/components/sequences-screen.tsx +159 -0
  247. package/blocks/sequencer/components/step-editor.tsx +3 -1
  248. package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
  249. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  250. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  251. package/blocks/social-ads/README.md +11 -0
  252. package/blocks/social-ads/block.json +101 -2
  253. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  254. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  255. package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
  256. package/blocks/social-ads/components/audience-sync.tsx +11 -7
  257. package/blocks/social-ads/components/campaign-detail.tsx +133 -0
  258. package/blocks/social-ads/components/creative-detail.tsx +150 -0
  259. package/blocks/social-ads/components/creative-gallery.tsx +15 -5
  260. package/blocks/social-ads/components/post-composer.tsx +3 -1
  261. package/blocks/social-ads/components/post-detail.tsx +160 -0
  262. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  263. package/blocks/social-ads/components/social-screen.tsx +160 -0
  264. package/blocks/visitors/README.md +10 -0
  265. package/blocks/visitors/block.json +25 -1
  266. package/blocks/visitors/components/snippet-install.tsx +3 -1
  267. package/blocks/visitors/components/visitor-company-card.tsx +2 -2
  268. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  269. package/blocks/visitors/components/visitor-feed.tsx +11 -2
  270. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  271. package/blocks/webchat/README.md +10 -0
  272. package/blocks/webchat/block.json +21 -7
  273. package/blocks/webchat/components/chat-widget-config.tsx +7 -4
  274. package/blocks/webchat/components/conversation-list.tsx +7 -3
  275. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  276. package/blocks/workbench/README.md +11 -0
  277. package/blocks/workbench/block.json +48 -1
  278. package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
  279. package/blocks/workbench/components/workbench-grid.tsx +9 -3
  280. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  281. package/blocks/workbench/components/workbench-screen.tsx +192 -0
  282. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  283. package/blocks/workflows/README.md +13 -0
  284. package/blocks/workflows/block.json +28 -9
  285. package/blocks/workflows/components/run-history.tsx +31 -9
  286. package/blocks/workflows/components/trigger-config.tsx +3 -1
  287. package/blocks/workflows/components/workflow-editor.tsx +3 -1
  288. package/blocks/workflows/components/workflow-list.tsx +13 -6
  289. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  290. package/dist/cli.js +2785 -1637
  291. package/dist/errors.d.mts +34 -0
  292. package/dist/errors.d.ts +34 -0
  293. package/dist/errors.js +46 -0
  294. package/dist/errors.js.map +1 -0
  295. package/dist/errors.mjs +21 -0
  296. package/dist/errors.mjs.map +1 -0
  297. package/dist/index.browser.js +11 -5
  298. package/dist/index.browser.js.map +1 -1
  299. package/dist/index.browser.mjs +11 -5
  300. package/dist/index.browser.mjs.map +1 -1
  301. package/dist/index.d.mts +164 -25
  302. package/dist/index.d.ts +164 -25
  303. package/dist/index.js +11 -5
  304. package/dist/index.js.map +1 -1
  305. package/dist/index.mjs +11 -5
  306. package/dist/index.mjs.map +1 -1
  307. package/dist/react.browser.js +10 -4
  308. package/dist/react.browser.js.map +1 -1
  309. package/dist/react.browser.mjs +10 -4
  310. package/dist/react.browser.mjs.map +1 -1
  311. package/dist/react.d.mts +142 -5
  312. package/dist/react.d.ts +142 -5
  313. package/dist/react.js +10 -4
  314. package/dist/react.js.map +1 -1
  315. package/dist/react.mjs +10 -4
  316. package/dist/react.mjs.map +1 -1
  317. package/dist/types/blocks/add.d.ts +11 -4
  318. package/dist/types/blocks/brand.d.ts +113 -0
  319. package/dist/types/blocks/compose.d.ts +53 -0
  320. package/dist/types/blocks/create.d.ts +25 -8
  321. package/dist/types/blocks/doctor.d.ts +6 -1
  322. package/dist/types/blocks/flows.d.ts +20 -0
  323. package/dist/types/blocks/navfile.d.ts +35 -0
  324. package/dist/types/blocks/paths.d.ts +4 -0
  325. package/dist/types/blocks/prefixes.d.ts +37 -0
  326. package/dist/types/blocks/registry.d.ts +32 -0
  327. package/dist/types/blocks/screens.d.ts +53 -0
  328. package/dist/types/cli.d.ts +2 -0
  329. package/dist/types/errors.d.ts +33 -0
  330. package/dist/types/http.d.ts +2 -18
  331. package/dist/types/version.d.ts +2 -2
  332. package/docs/agents.md +15 -4
  333. package/docs/api/README.md +2 -2
  334. package/docs/api/by-path.md +1 -0
  335. package/docs/api/desk.d.ts +98 -2
  336. package/docs/api/desk.md +123 -1
  337. package/docs/api/inbox.d.ts +10 -2
  338. package/docs/api/inbox.md +10 -2
  339. package/docs/api/seo.d.ts +19 -1
  340. package/docs/api/seo.md +24 -1
  341. package/package.json +16 -2
  342. package/stacks/6sense.json +41 -16
  343. package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
  344. package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
  345. package/stacks/apollo.json +118 -36
  346. package/stacks/calendly.json +60 -0
  347. package/stacks/clay/pages/companies.tsx.tmpl +15 -0
  348. package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
  349. package/stacks/clay.json +59 -26
  350. package/stacks/gong.json +81 -35
  351. package/stacks/hootsuite.json +99 -0
  352. package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
  353. package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
  354. package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
  355. package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
  356. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  357. package/stacks/hubspot.json +167 -66
  358. package/stacks/intercom.json +48 -20
  359. package/stacks/mailchimp.json +52 -33
  360. package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
  361. package/stacks/outreach.json +88 -25
  362. package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
  363. package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
  364. package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
  365. package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
  366. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  367. package/stacks/pipedrive.json +115 -0
  368. package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
  369. package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
  370. package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
  371. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  372. package/stacks/salesforce.json +81 -31
  373. package/stacks/stack.schema.json +6 -1
  374. package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
  375. package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
  376. package/stacks/zoominfo.json +102 -0
  377. package/styles/styles.css +1 -1
  378. package/templates/next-app/README.md.tmpl +55 -8
  379. package/templates/next-app/app/globals.css.tmpl +105 -0
  380. package/templates/next-app/app/layout.tsx.tmpl +18 -4
  381. package/templates/next-app/components/app-shell.tsx.tmpl +248 -23
  382. package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
  383. package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
  384. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
  385. package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
  386. package/templates/next-app/dot-gitignore.tmpl +2 -0
  387. package/templates/next-app/lib/nav.ts.tmpl +1 -1
  388. package/templates/next-app/package.json.tmpl +1 -1
@@ -0,0 +1,192 @@
1
+ "use client";
2
+
3
+ /**
4
+ * WorkbenchScreen: the finished Workbench page. Without `?id=` it is the list of workbenches (search, row and column
5
+ * counts, People or Companies, New workbench in a side sheet, confirmed delete). With `?id=<table>` it opens that table:
6
+ * a header with its facts and the full WorkbenchGrid (edit cells, paste or add rows, add AI / waterfall / formula
7
+ * columns, quote then run a column, watch progress, unlock locked rows, send rows to a list, delete rows).
8
+ *
9
+ * Composes: ScreenLayout, StatStrip, Toolbar, FormSheet, StatusPill, EntityAvatar (screen kit), WorkbenchGrid and the
10
+ * parts it composes (WorkbenchAddRows, ColumnRecipeEditor, RunProgress, WorkbenchRowActions), ConfirmSpendDialog.
11
+ *
12
+ * SDK operations, in order:
13
+ * 1. listWorkbenchTables GET /workbench/tables?page&limit&filter_name (read)
14
+ * 2. createWorkbenchTable POST /workbench/tables { name, entity_type } (write); the new table opens at once
15
+ * 3. deleteWorkbenchTable DELETE /workbench/tables/{table_id} (destructive, ConfirmSpendDialog)
16
+ * 4. getWorkbenchTable GET /workbench/tables/{table_id} (read, via useWorkbenchTable)
17
+ * 5. Import people / Scrape the web: previewWorkbenchSearchImport, importWorkbenchFromSearch (billable, confirmed),
18
+ * estimateWorkbenchScraperCredits, runWorkbenchScraper (billable, confirmed), getWorkbenchScraperRun (see workbench-sources.tsx)
19
+ * 6. everything inside the grid: see WorkbenchGrid (runs and unlocks are billable and confirmed)
20
+ */
21
+
22
+ import { useEffect, useState } from "react";
23
+ import type { ReactNode } from "react";
24
+ import { ArrowLeft, Globe, Plus, Table2, Trash2, UsersRound } from "lucide-react";
25
+ import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
26
+ import { ApiError } from "@/g8/components/api-error";
27
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
28
+ import { EmptyState } from "@/g8/components/empty-state";
29
+ import { EntityAvatar } from "@/g8/components/screen/entity-avatar";
30
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
31
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
32
+ import { SplitView } from "@/g8/components/screen/split-view";
33
+ import { ScreenLayout } from "@/g8/components/screen/screen-layout";
34
+ import { StatusPill } from "@/g8/components/screen/status-pill";
35
+ import { Toolbar } from "@/g8/components/screen/toolbar";
36
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
37
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
38
+ import { useUrlState } from "@/g8/hooks/use-url-state";
39
+ import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
40
+ import { useWorkbenchTable } from "../hooks/use-workbench-table";
41
+ import { pageItems, pageTotal } from "../lib/workbench-types";
42
+ import type { WorkbenchTable } from "../lib/workbench-types";
43
+ import { WorkbenchGrid } from "./workbench-grid";
44
+ import { WorkbenchRowDetail } from "./workbench-row-detail";
45
+ import { ScraperPanel, SearchImportPanel } from "./workbench-sources";
46
+
47
+ export interface WorkbenchScreenProps {
48
+ toolbarActions?: ReactNode;
49
+ renderRowActions?: (table: WorkbenchTable) => ReactNode;
50
+ /** Extra buttons in an opened table's header. */
51
+ renderDetailActions?: (table: WorkbenchTable) => ReactNode;
52
+ className?: string;
53
+ }
54
+
55
+ const noun = (t: { entity_type?: string | null }) => (t.entity_type === "company" ? "Companies" : "People");
56
+
57
+ function OpenTable({ tableId, onBack, renderDetailActions, className }: { tableId: string; onBack: () => void; renderDetailActions?: (t: WorkbenchTable) => ReactNode; className?: string }) {
58
+ const { table, columns, loading, error, refetch } = useWorkbenchTable(tableId);
59
+ const [source, setSource] = useState<"import" | "scrape" | null>(null);
60
+ const [rowParam, setRowParam] = useUrlState("row");
61
+ const [view, setView] = useUrlState("view");
62
+ const full = view === "full";
63
+ const rowId = rowParam ? Number(rowParam) : null;
64
+ // New rows arrive from outside the grid (import, scraper): remount it so it reloads, and re-read the counts.
65
+ const [gridKey, setGridKey] = useState(0);
66
+ const changed = () => { setGridKey((k) => k + 1); void refetch(); };
67
+ return (
68
+ <ScreenLayout
69
+ className={className}
70
+ eyebrow={<Button variant="ghost" size="sm" onClick={onBack}><ArrowLeft className="mr-1 size-4" aria-hidden />Back to workbenches</Button>}
71
+ title={table?.name ?? "Workbench"}
72
+ description={table ? `${noun(table)} table. Add columns that look things up or write with AI, price a run, then send finished rows to a list.` : undefined}
73
+ icon={<Table2 className="size-5" aria-hidden />}
74
+ actions={table ? (<>
75
+ <Button size="sm" variant="outline" onClick={() => setSource("import")}><UsersRound className="mr-1 size-4" aria-hidden />Import people</Button>
76
+ <Button size="sm" variant="outline" onClick={() => setSource("scrape")}><Globe className="mr-1 size-4" aria-hidden />Scrape the web</Button>
77
+ {renderDetailActions?.(table)}
78
+ </>) : undefined}
79
+ stats={table ? [
80
+ { label: "Rows", value: (table.row_count ?? 0).toLocaleString() },
81
+ { label: "Columns", value: columns.length.toLocaleString() },
82
+ { label: "Recipe columns", value: columns.filter((c) => c.enrichment_type).length },
83
+ ] : undefined}
84
+ >
85
+ {error ? <ApiError error={error} title="Could not open the workbench" onRetry={() => void refetch()} /> : null}
86
+ {loading && !table ? <Skeleton className="h-8 w-64" /> : null}
87
+ <SplitView
88
+ expanded={full} onExpandedChange={(e) => setView(e ? "full" : null)}
89
+ list={<WorkbenchGrid key={gridKey} tableId={tableId} activeRowId={rowId} onOpenRow={(r) => setRowParam(String(r.id))} />}
90
+ detailOpen={rowId != null && Number.isFinite(rowId)}
91
+ onCloseDetail={() => setRowParam(null)}
92
+ detailWidth="lg"
93
+ detailTitle="Row"
94
+ detail={rowId != null ? (
95
+ <DetailPanel key={rowId} onExpand={full ? undefined : () => setView("full")} onCollapse={full ? () => setView(null) : undefined} title={`Row ${rowId}`} subtitle={table?.name} onClose={() => setRowParam(null)}>
96
+ <WorkbenchRowDetail tableId={tableId} rowId={rowId} columns={columns} entityType={table?.entity_type === "company" ? "company" : "contact"}
97
+ onChanged={(what) => { if (what === "deleted") setRowParam(null); changed(); }} />
98
+ </DetailPanel>
99
+ ) : null}
100
+ />
101
+ <FormSheet open={source === "import"} onOpenChange={(o) => !o && setSource(null)} width="lg" title="Import people from a search" description="Preview for free, then import. Imported rows are locked until you unlock them.">
102
+ <SearchImportPanel tableId={tableId} onChanged={changed} />
103
+ </FormSheet>
104
+ <FormSheet open={source === "scrape"} onOpenChange={(o) => !o && setSource(null)} width="lg" title="Scrape the web into this table" description="Describe what to collect. You see the credit estimate before anything runs.">
105
+ <ScraperPanel tableId={tableId} onChanged={changed} />
106
+ </FormSheet>
107
+ </ScreenLayout>
108
+ );
109
+ }
110
+
111
+ export function WorkbenchScreen({ toolbarActions, renderRowActions, renderDetailActions, className }: WorkbenchScreenProps) {
112
+ const [openId, setOpenId] = useUrlState("id");
113
+ const [search, setSearch] = useState("");
114
+ const [term, setTerm] = useState("");
115
+ useEffect(() => { const t = setTimeout(() => setTerm(search.trim()), 250); return () => clearTimeout(t); }, [search]);
116
+ const [creating, setCreating] = useState(false);
117
+ const [name, setName] = useState("");
118
+ const [entity, setEntity] = useState<"contact" | "company">("contact");
119
+ const [deleting, setDeleting] = useState<WorkbenchTable | null>(null);
120
+
121
+ const tables = useG8Query(async (signal) => {
122
+ const data = (await g8Get("/workbench/tables", { page: 1, limit: 100, filter_name: term || undefined }, { signal })).data;
123
+ const rows = pageItems<WorkbenchTable>(data);
124
+ return { rows, total: pageTotal(data, -1) >= 0 ? pageTotal(data, -1) : null };
125
+ }, [term], { enabled: !openId });
126
+ const create = useG8Mutation(
127
+ async () => (await g8Post<WorkbenchTable>("/workbench/tables", { name: name.trim(), entity_type: entity })).data,
128
+ { onSuccess: (t) => { setCreating(false); setName(""); setOpenId(t.id); } },
129
+ );
130
+ const remove = useG8Mutation((id: string) => g8Delete(`/workbench/tables/${id}`), { onSuccess: () => { setDeleting(null); void tables.refetch(); } });
131
+
132
+ if (openId) return <OpenTable key={openId} tableId={openId} onBack={() => setOpenId(null)} renderDetailActions={renderDetailActions} className={className} />;
133
+
134
+ const items = tables.data?.rows ?? [];
135
+ const partial = tables.data?.total == null || tables.data.total > items.length;
136
+ const hint = partial ? "On this page" : undefined;
137
+ return (
138
+ <ScreenLayout
139
+ className={className}
140
+ title="Workbench"
141
+ description="Spreadsheets that fill themselves in: paste or import records, add AI and enrichment columns, then send the finished rows to a list."
142
+ icon={<Table2 className="size-5" aria-hidden />}
143
+ actions={<>{toolbarActions}<Button size="sm" onClick={() => setCreating(true)}><Plus className="mr-1 size-4" aria-hidden />New workbench</Button></>}
144
+ stats={[
145
+ { label: "Workbenches", value: (tables.data?.total ?? items.length).toLocaleString(), hint: tables.data?.total == null ? "On this page" : undefined },
146
+ { label: "Rows", value: items.reduce((n, t) => n + (t.row_count ?? 0), 0).toLocaleString(), hint },
147
+ { label: "People tables", value: items.filter((t) => t.entity_type !== "company").length, hint },
148
+ { label: "Company tables", value: items.filter((t) => t.entity_type === "company").length, hint },
149
+ ]}
150
+ toolbar={<Toolbar search={{ value: search, onChange: setSearch, placeholder: "Search workbenches" }} />}
151
+ >
152
+ {tables.error ? <ApiError error={tables.error} title="Could not load workbenches" onRetry={() => void tables.refetch()} /> : null}
153
+ {remove.error ? <ApiError error={remove.error} title="Could not delete the workbench" /> : null}
154
+ {tables.loading && !tables.data ? <Skeleton className="h-24 w-full" /> : null}
155
+ {!tables.loading && !tables.error && items.length === 0 ? (
156
+ <EmptyState icon={<Table2 className="size-6" aria-hidden />} title={term ? "No workbench matches" : "No workbenches yet"} description={term ? "Try another name." : "Create one, then paste rows or import people."}
157
+ action={!term ? <Button size="sm" onClick={() => setCreating(true)}>Create your first workbench</Button> : undefined} />
158
+ ) : null}
159
+ <ul className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
160
+ {items.map((t) => (
161
+ <li key={t.id} data-testid={`workbench-${t.id}`} className="flex items-center gap-3 rounded-md border p-3">
162
+ <EntityAvatar name={t.name} kind={t.entity_type === "company" ? "company" : "person"} />
163
+ <button type="button" className="flex min-w-0 flex-1 flex-col text-left" onClick={() => setOpenId(t.id)}>
164
+ <span className="truncate text-sm font-medium">{t.name}</span>
165
+ <span className="text-xs text-muted-foreground">{(t.row_count ?? 0).toLocaleString()} rows</span>
166
+ </button>
167
+ <StatusPill tone="neutral">{noun(t)}</StatusPill>
168
+ {renderRowActions?.(t)}
169
+ <Button size="sm" variant="ghost" aria-label={`Delete ${t.name}`} onClick={() => setDeleting(t)}><Trash2 className="size-4" aria-hidden /></Button>
170
+ </li>
171
+ ))}
172
+ </ul>
173
+
174
+ <FormSheet open={creating} onOpenChange={setCreating} title="New workbench" description="Choose what each row is. You can paste rows or import people next."
175
+ footer={(<><Button variant="ghost" onClick={() => setCreating(false)}>Cancel</Button><Button loading={create.loading} disabled={!name.trim()} onClick={() => void create.mutate(undefined)}>Create workbench</Button></>)}>
176
+ <div className="flex flex-col gap-3">
177
+ <div className="flex flex-col gap-1"><Label htmlFor="wb-new-name">Name</Label><Input id="wb-new-name" placeholder="Series B fintech accounts" value={name} onChange={(e) => setName(e.target.value)} /></div>
178
+ <div className="flex flex-col gap-1"><Label htmlFor="wb-new-kind">Rows are</Label>
179
+ <select id="wb-new-kind" className="h-9 rounded-md border bg-background px-2 text-sm" value={entity} onChange={(e) => setEntity(e.target.value as "contact" | "company")}>
180
+ <option value="contact">People</option><option value="company">Companies</option>
181
+ </select>
182
+ </div>
183
+ {create.error ? <ApiError error={create.error} title="Could not create the workbench" /> : null}
184
+ </div>
185
+ </FormSheet>
186
+
187
+ <ConfirmSpendDialog open={deleting != null} tier="destructive" title={`Delete "${deleting?.name ?? ""}"`} description="The table, its columns and all of its rows are removed."
188
+ affected={deleting?.row_count ?? undefined} confirmLabel="Delete workbench" confirming={remove.loading}
189
+ onConfirm={() => deleting && void remove.mutate(deleting.id)} onCancel={() => setDeleting(null)} />
190
+ </ScreenLayout>
191
+ );
192
+ }
@@ -0,0 +1,194 @@
1
+ "use client";
2
+
3
+ /**
4
+ * WorkbenchSourcePanels: two ways to fill a workbench table with records.
5
+ * - SearchImportPanel: describe the people you want (titles, industries, countries, seniority, company domains),
6
+ * preview a free sample and the match count, then import them. Imported rows land LOCKED (names and companies
7
+ * visible, emails and phones masked): unlock them from the table's row actions.
8
+ * - ScraperPanel: describe what to collect in words ("seed-stage fintechs in Berlin"), see the credit estimate and
9
+ * your available credits, run the built-in web scraper and follow its progress.
10
+ *
11
+ * SDK operations, in order:
12
+ * SearchImportPanel
13
+ * 1. previewWorkbenchSearchImport POST /workbench/tables/{table_id}/import/search/preview { filters, sample_size, max_result } (read, free)
14
+ * -> { contacts, count }
15
+ * 2. importWorkbenchFromSearch POST /workbench/tables/{table_id}/import/search { filters, max_result, skip_duplicates } (billable)
16
+ * Goes through ConfirmSpendDialog first. -> { inserted, duplicates, columns_created, processed }
17
+ * ScraperPanel
18
+ * 1. estimateWorkbenchScraperCredits GET /workbench/scraper/credit-estimate?table_id&max_results&auto_enrich (read, free)
19
+ * -> { total_estimated_credits, available_credits, ... }
20
+ * 2. runWorkbenchScraper POST /workbench/tables/{table_id}/scraper/runs { prompt, max_results, auto_enrich, approved_max_credits, quote_expires_at } (billable)
21
+ * Goes through ConfirmSpendDialog first with the estimate; the estimate is also the cap sent back. -> { run_id, status }
22
+ * 3. getWorkbenchScraperRun GET /workbench/tables/{table_id}/scraper/runs/{run_id} (read) polled with useJobPoll
23
+ * -> { status, results_count, progress_pct, progress_message, error_message }
24
+ *
25
+ * Nothing runs without the confirmation. There is no stream, so the scraper run is polled with backoff.
26
+ */
27
+
28
+ import { useState } from "react";
29
+ import { Button, Input, Label } from "@graph8/sdk/ui";
30
+ import { ApiError } from "@/g8/components/api-error";
31
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
32
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
33
+ import { useJobPoll } from "@/g8/hooks/use-job-poll";
34
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
35
+
36
+ export interface WorkbenchSourcePanelProps {
37
+ tableId: string;
38
+ /** Called after rows were added (import done, scraper run finished) so the grid can reload. */
39
+ onChanged?: () => void;
40
+ className?: string;
41
+ }
42
+
43
+ const csv = (text: string): string[] => text.split(",").map((s) => s.trim()).filter(Boolean);
44
+
45
+ interface Preview { contacts?: Array<Record<string, unknown>>; count?: number }
46
+ interface ImportResult { inserted?: number; duplicates?: number; columns_created?: number; processed?: number }
47
+ interface Estimate { estimated_credits?: number; total_estimated_credits?: number; available_credits?: number; quote_expires_at?: string }
48
+ interface RunStarted { run_id?: string; status?: string }
49
+ interface ScraperRun { status?: string; results_count?: number; progress_pct?: number | null; progress_message?: string | null; error_message?: string | null }
50
+
51
+ const FIELDS = [
52
+ { key: "job_title", label: "Job titles", placeholder: "VP Sales, Head of Sales" },
53
+ { key: "seniority_level", label: "Seniority", placeholder: "vp, director" },
54
+ { key: "company_industry", label: "Industries", placeholder: "computer software" },
55
+ { key: "country", label: "Countries", placeholder: "United States" },
56
+ { key: "company_domain", label: "Company domains", placeholder: "acme.com, globex.com" },
57
+ ] as const;
58
+
59
+ /** Display name of a sampled person. Locked values arrive as "***" and are never shown. */
60
+ function sampleName(c: Record<string, unknown>): string {
61
+ const text = (v: unknown) => (typeof v === "string" && v !== "***" ? v : "");
62
+ return [text(c.first_name), text(c.last_name)].filter(Boolean).join(" ") || "Locked contact";
63
+ }
64
+
65
+ export function SearchImportPanel({ tableId, onChanged, className }: WorkbenchSourcePanelProps) {
66
+ const [values, setValues] = useState<Record<string, string>>({});
67
+ const [max, setMax] = useState("100");
68
+ const [confirmOpen, setConfirmOpen] = useState(false);
69
+ const filters = FIELDS.map((f) => ({ field: f.key, operator: "any_of", value: csv(values[f.key] ?? "") })).filter((f) => f.value.length > 0);
70
+ const key = JSON.stringify(filters);
71
+ const limit = Math.max(1, Math.min(Number(max) || 0, 10000));
72
+
73
+ const preview = useG8Mutation(async () => (await g8Post<Preview>(`/workbench/tables/${tableId}/import/search/preview`, { filters, sample_size: 10, max_result: limit })).data);
74
+ const run = useG8Mutation(
75
+ async () => (await g8Post<ImportResult>(`/workbench/tables/${tableId}/import/search`, { filters, max_result: limit, skip_duplicates: true })).data,
76
+ { onSuccess: () => { setConfirmOpen(false); onChanged?.(); } },
77
+ );
78
+ // A preview describes one set of filters: forget it when they change.
79
+ const [previewKey, setPreviewKey] = useState("");
80
+ const shown = preview.data && previewKey === key ? preview.data : null;
81
+ const done = run.data;
82
+
83
+ return (
84
+ <div className={`flex flex-col gap-3 ${className ?? ""}`}>
85
+ <p className="text-sm text-muted-foreground">Describe the people you want. Separate several values with commas. All filters must match.</p>
86
+ {FIELDS.map((f) => (
87
+ <div key={f.key} className="flex flex-col gap-1">
88
+ <Label htmlFor={`imp-${f.key}`}>{f.label}</Label>
89
+ <Input id={`imp-${f.key}`} placeholder={f.placeholder} value={values[f.key] ?? ""} onChange={(e) => setValues({ ...values, [f.key]: e.target.value })} />
90
+ </div>
91
+ ))}
92
+ <div className="flex flex-col gap-1">
93
+ <Label htmlFor="imp-max">Import at most</Label>
94
+ <Input id="imp-max" inputMode="numeric" className="w-32" value={max} onChange={(e) => setMax(e.target.value)} />
95
+ </div>
96
+ <div className="flex gap-2">
97
+ <Button variant="outline" size="sm" loading={preview.loading} disabled={filters.length === 0} onClick={async () => { setPreviewKey(key); await preview.mutate(undefined); }}>Preview sample</Button>
98
+ <Button size="sm" disabled={filters.length === 0 || !shown || (shown.count ?? 0) === 0} onClick={() => setConfirmOpen(true)}>Import {shown ? Math.min(shown.count ?? 0, limit).toLocaleString() : ""} people</Button>
99
+ </div>
100
+ {preview.error ? <ApiError error={preview.error} title="Could not preview the search" /> : null}
101
+ {shown ? (
102
+ <div role="status" className="rounded-md border p-3 text-sm">
103
+ <p><strong>{(shown.count ?? 0).toLocaleString()}</strong> people match. Sample:</p>
104
+ <ul className="mt-1 list-disc pl-5 text-muted-foreground">
105
+ {(shown.contacts ?? []).slice(0, 10).map((c, i) => <li key={i}>{sampleName(c)}{typeof c.job_title === "string" ? `, ${c.job_title}` : ""}{typeof c.company_name === "string" ? ` at ${c.company_name}` : ""}</li>)}
106
+ </ul>
107
+ <p className="mt-2 text-xs text-muted-foreground">Emails and phones stay locked until you unlock the rows (1 credit per row).</p>
108
+ </div>
109
+ ) : null}
110
+ {run.error ? <ApiError error={run.error} title="Could not import" /> : null}
111
+ {done ? <p role="status" className="text-sm">Imported {(done.inserted ?? 0).toLocaleString()} people{done.duplicates ? `, skipped ${done.duplicates.toLocaleString()} duplicates` : ""}. Their contact details are locked: select the rows and unlock them.</p> : null}
112
+ <ConfirmSpendDialog open={confirmOpen} tier="billable" title={`Import up to ${shown ? Math.min(shown.count ?? 0, limit).toLocaleString() : limit.toLocaleString()} people`}
113
+ description="They are added to this table with names and companies visible and contact details locked. Unlocking costs 1 credit per row."
114
+ affected={shown ? Math.min(shown.count ?? 0, limit) : undefined} confirmLabel="Import" confirming={run.loading}
115
+ onConfirm={() => void run.mutate(undefined)} onCancel={() => setConfirmOpen(false)} />
116
+ </div>
117
+ );
118
+ }
119
+
120
+ const RUNNING = new Set(["pending", "queued", "starting", "running", "in_progress", "processing"]);
121
+
122
+ export function ScraperPanel({ tableId, onChanged, className }: WorkbenchSourcePanelProps) {
123
+ const [prompt, setPrompt] = useState("");
124
+ const [maxResults, setMaxResults] = useState("100");
125
+ const [autoEnrich, setAutoEnrich] = useState(false);
126
+ const [confirmOpen, setConfirmOpen] = useState(false);
127
+ const [runId, setRunId] = useState<string | null>(null);
128
+ const count = Math.max(10, Math.min(Number(maxResults) || 0, 2000));
129
+ const scope = `${count}:${autoEnrich}`;
130
+
131
+ const [estimateScope, setEstimateScope] = useState("");
132
+ const estimate = useG8Mutation(async () => (await g8Get<Estimate>("/workbench/scraper/credit-estimate", { table_id: tableId, max_results: count, auto_enrich: autoEnrich })).data);
133
+ const quote = estimate.data && estimateScope === scope ? estimate.data : null;
134
+ const credits = quote ? (quote.total_estimated_credits ?? quote.estimated_credits ?? 0) : 0;
135
+
136
+ const [stale, setStale] = useState(false);
137
+ const start = useG8Mutation(
138
+ async () => (await g8Post<RunStarted>(`/workbench/tables/${tableId}/scraper/runs`, {
139
+ prompt: prompt.trim(), max_results: count, auto_enrich: autoEnrich, approved_max_credits: credits, quote_expires_at: quote?.quote_expires_at,
140
+ })).data,
141
+ { onSuccess: (r) => { setConfirmOpen(false); if (r.run_id) setRunId(r.run_id); } },
142
+ );
143
+ /** The server prices a run against the estimate's own expiry. Never invent one: an expired or missing quote is re-estimated. */
144
+ const confirmRun = () => {
145
+ const expires = quote?.quote_expires_at ? Date.parse(quote.quote_expires_at) : NaN;
146
+ if (!Number.isFinite(expires) || expires <= Date.now()) {
147
+ setConfirmOpen(false); setEstimateScope(""); estimate.reset(); setStale(true);
148
+ return;
149
+ }
150
+ void start.mutate(undefined);
151
+ };
152
+ const progress = useJobPoll<ScraperRun>(
153
+ async (signal) => (await g8Get<ScraperRun>(`/workbench/tables/${tableId}/scraper/runs/${runId}`, undefined, { signal })).data,
154
+ { enabled: runId != null, intervalMs: 3000, maxIntervalMs: 20000, isTerminal: (r) => !RUNNING.has(String(r.status ?? "").toLowerCase()), onDone: () => onChanged?.() },
155
+ );
156
+ const run = progress.status;
157
+
158
+ if (runId) {
159
+ return (
160
+ <div className={`flex flex-col gap-3 ${className ?? ""}`} role="status">
161
+ <h3 className="text-sm font-medium">{progress.done ? `Scraper ${run?.status ?? "finished"}` : "Scraping the web..."}</h3>
162
+ <div className="h-2 overflow-hidden rounded bg-muted" aria-label="Progress"><div className="h-full bg-primary" style={{ width: `${progress.done ? 100 : (run?.progress_pct ?? 0)}%` }} /></div>
163
+ <p className="text-sm">{(run?.results_count ?? 0).toLocaleString()} results collected.{run?.progress_message ? ` ${run.progress_message}` : ""}</p>
164
+ {run?.error_message ? <p role="alert" className="text-sm text-destructive">{run.error_message}</p> : null}
165
+ {progress.error ? <ApiError error={progress.error} title="Could not read the run" onRetry={() => progress.start()} /> : null}
166
+ {progress.done ? <Button size="sm" variant="outline" onClick={() => { setRunId(null); progress.stop(); start.reset(); }}>Start another run</Button> : null}
167
+ </div>
168
+ );
169
+ }
170
+
171
+ return (
172
+ <div className={`flex flex-col gap-3 ${className ?? ""}`}>
173
+ <div className="flex flex-col gap-1">
174
+ <Label htmlFor="scr-prompt">What should it collect?</Label>
175
+ <Input id="scr-prompt" placeholder="Seed-stage fintech companies in Berlin" value={prompt} onChange={(e) => setPrompt(e.target.value)} />
176
+ </div>
177
+ <div className="flex flex-col gap-1">
178
+ <Label htmlFor="scr-max">How many results (10 to 2,000)</Label>
179
+ <Input id="scr-max" inputMode="numeric" className="w-32" value={maxResults} onChange={(e) => setMaxResults(e.target.value)} />
180
+ </div>
181
+ <label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={autoEnrich} onChange={(e) => setAutoEnrich(e.target.checked)} /> Enrich every result with the table's enrichment columns (costs more)</label>
182
+ <div className="flex gap-2">
183
+ <Button variant="outline" size="sm" loading={estimate.loading} disabled={!prompt.trim()} onClick={async () => { setStale(false); setEstimateScope(scope); await estimate.mutate(undefined); }}>Estimate credits</Button>
184
+ <Button size="sm" disabled={!prompt.trim() || !quote} onClick={() => setConfirmOpen(true)}>Run scraper</Button>
185
+ </div>
186
+ {estimate.error ? <ApiError error={estimate.error} title="Could not estimate the cost" /> : null}
187
+ {quote ? <p role="status" className="text-sm">Up to <strong>{credits.toLocaleString()}</strong> credits{quote.available_credits != null ? `. You have ${quote.available_credits.toLocaleString()} available.` : "."}{quote.available_credits != null && credits > quote.available_credits ? " That is more than your balance, so the run would be refused." : ""}</p> : null}
188
+ {stale ? <p role="status" className="text-sm">That estimate has expired. Estimate the credits again before running the scraper.</p> : null}
189
+ {start.error ? <ApiError error={start.error} title="Could not start the scraper" /> : null}
190
+ <ConfirmSpendDialog open={confirmOpen} tier="billable" title={`Scrape up to ${count.toLocaleString()} results`} description={`"${prompt.trim()}". The scraper charges per result collected; the estimate is the most this run can charge.`}
191
+ estimatedCredits={credits} confirmLabel="Run scraper" confirming={start.loading} onConfirm={confirmRun} onCancel={() => setConfirmOpen(false)} />
192
+ </div>
193
+ );
194
+ }
@@ -3,6 +3,19 @@
3
3
  Editable React screens for automations: a workflow list, a step-list editor (not a canvas), trigger
4
4
  configuration and run history. Install with `npx @graph8/sdk add workflows`.
5
5
 
6
+ ## Screens
7
+
8
+ `WorkflowsScreen` (`components/workflows-screen.tsx`) is the finished automations workspace; mount it as the page.
9
+
10
+ - **Workflows tab**: stat strip (workflows, enabled, disabled, failed runs), search, status filter, the list with
11
+ Run (plan preview and run input first), Enable / Disable and Delete, and **New workflow**. Opening one (deep link
12
+ `?id=`) shows **Steps** (editor), **Trigger** (type, interval, cursor reset) and **Runs** for that workflow.
13
+ - **Runs tab**: every run as a list; opening one (`?run=`) shows `RunDetail` (status, timing, who started it, error, input, output) with pause / resume / stop / restart. `&view=full` opens any record as a full page.
14
+ - Create, run, enable and disable are `external`, delete is `destructive`: all go through `ConfirmSpendDialog`.
15
+ Needs `workflows:read|write|run|delete`.
16
+ - Slots: `renderRowActions`, `renderDetailActions`, `extraDetailTabs`, `toolbarActions`. `RunPlan` and `RunDetail` are now
17
+ exported; `RunHistory` gained `selectedId`, `onSelectRun`, `hideDetail`, `refreshKey`, `nameFor`.
18
+
6
19
  | Component | What it is |
7
20
  |---|---|
8
21
  | `WorkflowList` | Workflows with run (plan preview + JSON input), enable/disable, create, delete. |
@@ -13,24 +13,42 @@
13
13
  "foundation"
14
14
  ],
15
15
  "operations": [
16
- "list_workflows_workflows_get",
17
16
  "create_workflow_workflows_post",
18
- "plan_workflow_workflows__action_id__plan_post",
19
- "execute_workflow_workflows__action_id__execute_post",
20
- "update_workflow_workflows__action_id__put",
21
17
  "delete_workflow_workflows__action_id__delete",
22
- "get_workflow_workflows__action_id__get",
18
+ "execute_workflow_workflows__action_id__execute_post",
19
+ "get_execution_workflows_executions__execution_id__get",
23
20
  "get_node_types_schema_workflows_node_types_schema_get",
24
21
  "get_trigger_status_workflows__action_id__trigger_status_get",
25
- "reset_trigger_cursor_workflows__action_id__trigger_reset_post",
22
+ "get_workflow_workflows__action_id__get",
26
23
  "list_workflow_executions_workflows_executions_get",
27
- "get_execution_workflows_executions__execution_id__get",
24
+ "list_workflows_workflows_get",
28
25
  "pause_execution_workflows_executions__execution_id__pause_post",
26
+ "plan_workflow_workflows__action_id__plan_post",
27
+ "reset_trigger_cursor_workflows__action_id__trigger_reset_post",
28
+ "restart_execution_workflows_executions__execution_id__restart_post",
29
29
  "resume_execution_workflows_executions__execution_id__resume_post",
30
30
  "stop_execution_workflows_executions__execution_id__stop_post",
31
- "restart_execution_workflows_executions__execution_id__restart_post"
31
+ "update_workflow_workflows__action_id__put"
32
32
  ],
33
33
  "components": [
34
+ {
35
+ "name": "WorkflowsScreen",
36
+ "file": "components/workflows-screen.tsx",
37
+ "purpose": "Finished automations workspace: stats, search, list with run/enable/delete, create, and a detail panel with steps, trigger and runs, plus a workspace-wide Runs tab.",
38
+ "operations": [
39
+ "create_workflow_workflows_post",
40
+ "delete_workflow_workflows__action_id__delete",
41
+ "execute_workflow_workflows__action_id__execute_post",
42
+ "list_workflow_executions_workflows_executions_get",
43
+ "list_workflows_workflows_get",
44
+ "update_workflow_workflows__action_id__put"
45
+ ],
46
+ "tiers": [
47
+ "read",
48
+ "external",
49
+ "destructive"
50
+ ]
51
+ },
34
52
  {
35
53
  "name": "WorkflowList",
36
54
  "file": "components/workflow-list.tsx",
@@ -183,6 +201,7 @@
183
201
  }
184
202
  ],
185
203
  "peerDependencies": {
186
- "react": ">=18"
204
+ "react": ">=18",
205
+ "lucide-react": ">=0.400.0"
187
206
  }
188
207
  }
@@ -28,6 +28,8 @@ import { EmptyState } from "@/g8/components/empty-state";
28
28
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
29
29
  import { TERMINAL_STATUSES, WAITING_STATUSES, payload, str } from "../lib/graph";
30
30
  import type { ExecutionRow, Rec } from "../lib/graph";
31
+ import { rowActivationProps } from "@/g8/hooks/use-row-activation";
32
+ import { pluralWord } from "@/g8/lib/plural";
31
33
 
32
34
  export interface RunHistoryProps {
33
35
  /** Only this workflow's runs. Omit for every workflow. */
@@ -35,6 +37,15 @@ export interface RunHistoryProps {
35
37
  /** Open this run's detail first (for example the id returned by WorkflowList's onRunStarted). */
36
38
  initialExecutionId?: string;
37
39
  pageSize?: number;
40
+ /** Controlled selection (a run id). With it, the host owns which run is open (for example from the URL). */
41
+ selectedId?: string | null;
42
+ onSelectRun?: (executionId: string | null) => void;
43
+ /** Hide the built-in detail column when the host shows the run elsewhere (see RunDetail). */
44
+ hideDetail?: boolean;
45
+ /** Change this number to reload the list (for example after RunDetail paused or stopped a run). */
46
+ refreshKey?: number;
47
+ /** Show the workflow's name next to each run when the list spans several workflows. */
48
+ nameFor?: (actionId: string) => string | undefined;
38
49
  className?: string;
39
50
  }
40
51
 
@@ -50,7 +61,8 @@ const VERB_COPY: Record<Verb, { title: string; description: string; label: strin
50
61
  const allowed = (status: string): Verb[] =>
51
62
  status === "running" ? ["pause", "stop"] : status === "paused" || status === "pending_approval" ? ["resume", "stop"] : TERMINAL_STATUSES.includes(status) ? ["restart"] : [];
52
63
 
53
- function Detail({ executionId, onChanged }: { executionId: string; onChanged: () => void }) {
64
+ /** One run in full: status, timing, who started it, error, input and output, and the controls its status allows. */
65
+ export function RunDetail({ executionId, onChanged }: { executionId: string; onChanged: () => void }) {
54
66
  const [verb, setVerb] = useState<Verb | null>(null);
55
67
  const poll = useJobPoll<Rec>(async (signal) => payload(await g8Get(`/workflows/executions/${encodeURIComponent(executionId)}`, undefined, { signal })), {
56
68
  isTerminal: (e) => TERMINAL_STATUSES.includes(str(e.status)) || WAITING_STATUSES.includes(str(e.status)),
@@ -67,6 +79,13 @@ function Detail({ executionId, onChanged }: { executionId: string; onChanged: ()
67
79
  {poll.error ? <ApiError error={poll.error} title="Could not read this run" onRetry={poll.start} /> : null}
68
80
  {act.error ? <ApiError error={act.error} title="That action failed" /> : null}
69
81
  <p>Status: <strong>{status.replace(/_/g, " ")}</strong>{e.duration_ms != null ? ` in ${Math.round(Number(e.duration_ms) / 100) / 10}s` : ""}</p>
82
+ <dl className="grid grid-cols-2 gap-x-3 gap-y-1 text-xs">
83
+ <dt className="text-muted-foreground">Run id</dt><dd className="font-mono break-all">{executionId}</dd>
84
+ <dt className="text-muted-foreground">Workflow</dt><dd className="font-mono break-all">{str(e.action_id)}</dd>
85
+ <dt className="text-muted-foreground">Started by</dt><dd>{str(e.triggered_by ?? e.trigger_type)}</dd>
86
+ <dt className="text-muted-foreground">Started</dt><dd>{e.started_at || e.created_at ? new Date(str(e.started_at ?? e.created_at)).toLocaleString() : ""}</dd>
87
+ <dt className="text-muted-foreground">Finished</dt><dd>{e.completed_at ? new Date(str(e.completed_at)).toLocaleString() : ""}</dd>
88
+ </dl>
70
89
  {e.error_message ? <p className="text-destructive">{str(e.error_message)}</p> : null}
71
90
  {e.input_data ? <details><summary>Input</summary><pre className="overflow-auto text-xs">{JSON.stringify(e.input_data, null, 2)}</pre></details> : null}
72
91
  {e.output_data ? <details open><summary>Output</summary><pre className="overflow-auto text-xs">{JSON.stringify(e.output_data, null, 2)}</pre></details> : null}
@@ -82,13 +101,15 @@ function Detail({ executionId, onChanged }: { executionId: string; onChanged: ()
82
101
  );
83
102
  }
84
103
 
85
- export function RunHistory({ actionId, initialExecutionId, pageSize = 25, className }: RunHistoryProps) {
104
+ export function RunHistory({ actionId, initialExecutionId, pageSize = 25, selectedId, onSelectRun, hideDetail, refreshKey, nameFor, className }: RunHistoryProps) {
86
105
  const [status, setStatus] = useState("");
87
106
  const [offset, setOffset] = useState(0);
88
- const [selected, setSelected] = useState<string | null>(initialExecutionId ?? null);
107
+ const [innerSel, setInnerSel] = useState<string | null>(initialExecutionId ?? null);
108
+ const selected = selectedId !== undefined ? selectedId : innerSel;
109
+ const setSelected = (id: string | null) => { setInnerSel(id); onSelectRun?.(id); };
89
110
  const list = useG8Query(
90
111
  (signal) => g8Get("/workflows/executions", { action_id: actionId, status_filter: status || undefined, limit: pageSize, offset }, { signal }),
91
- [actionId, status, offset, pageSize],
112
+ [actionId, status, offset, pageSize, refreshKey],
92
113
  );
93
114
  const body = payload(list.data);
94
115
  const rows = (Array.isArray(body.executions) ? body.executions : []) as ExecutionRow[];
@@ -103,17 +124,18 @@ export function RunHistory({ actionId, initialExecutionId, pageSize = 25, classN
103
124
  <Button size="sm" variant="outline" onClick={() => void list.refetch()}>Refresh</Button>
104
125
  </div>
105
126
  {list.error ? <ApiError error={list.error} onRetry={() => void list.refetch()} /> : null}
106
- <div className="grid gap-4 lg:grid-cols-2">
127
+ <div className={hideDetail ? "" : "grid gap-4 lg:grid-cols-2"}>
107
128
  <div>
108
129
  {list.loading && !list.data ? <div className="space-y-2" aria-busy="true">{[0, 1, 2].map((i) => <Skeleton key={i} className="h-10 w-full" />)}</div> : !list.error && rows.length === 0 ? (
109
130
  <EmptyState title="No runs yet" description="Runs appear here when a trigger fires or someone runs the workflow." />
110
131
  ) : (
111
132
  <Table>
112
- <TableHeader><TableRow><TableHead>Started</TableHead><TableHead>Status</TableHead><TableHead>By</TableHead></TableRow></TableHeader>
133
+ <TableHeader><TableRow><TableHead>Started</TableHead>{nameFor ? <TableHead>Workflow</TableHead> : null}<TableHead>Status</TableHead><TableHead>By</TableHead></TableRow></TableHeader>
113
134
  <TableBody>
114
135
  {rows.map((r) => (
115
- <TableRow key={r.execution_id} data-state={selected === r.execution_id ? "selected" : undefined}>
136
+ <TableRow key={r.execution_id} data-state={selected === r.execution_id ? "selected" : undefined} {...rowActivationProps(() => setSelected(r.execution_id))}>
116
137
  <TableCell><button type="button" className="hover:underline" onClick={() => setSelected(r.execution_id)}>{new Date(str(r.started_at ?? r.created_at)).toLocaleString()}</button></TableCell>
138
+ {nameFor ? <TableCell>{nameFor(r.action_id) ?? ""}</TableCell> : null}
117
139
  <TableCell className="capitalize">{r.status.replace(/_/g, " ")}</TableCell>
118
140
  <TableCell>{r.triggered_by ?? r.trigger_type ?? ""}</TableCell>
119
141
  </TableRow>
@@ -122,14 +144,14 @@ export function RunHistory({ actionId, initialExecutionId, pageSize = 25, classN
122
144
  </Table>
123
145
  )}
124
146
  <div className="mt-3 flex items-center justify-between text-sm">
125
- <span className="text-muted-foreground">{total} {total === 1 ? "run" : "runs"}</span>
147
+ <span className="text-muted-foreground">{total} {pluralWord(total, "run")}</span>
126
148
  <div className="flex gap-2">
127
149
  <Button size="sm" variant="outline" disabled={offset === 0} onClick={() => setOffset(Math.max(0, offset - pageSize))}>Previous</Button>
128
150
  <Button size="sm" variant="outline" disabled={offset + pageSize >= total} onClick={() => setOffset(offset + pageSize)}>Next</Button>
129
151
  </div>
130
152
  </div>
131
153
  </div>
132
- <div>{selected ? <Detail key={selected} executionId={selected} onChanged={() => void list.refetch()} /> : <p className="text-muted-foreground text-sm">Select a run to see its input, output and errors.</p>}</div>
154
+ {hideDetail ? null : <div>{selected ? <RunDetail key={selected} executionId={selected} onChanged={() => void list.refetch()} /> : <p className="text-muted-foreground text-sm">Select a run to see its input, output and errors.</p>}</div>}
133
155
  </div>
134
156
  </div>
135
157
  );
@@ -26,6 +26,7 @@ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
26
26
  import { useWorkflow } from "../hooks/use-workflow";
27
27
  import { asRec, nodeIdOf, nodesOf, payload, setStep, str, triggerOf } from "../lib/graph";
28
28
  import type { Rec } from "../lib/graph";
29
+ import { useBrand } from "@/g8/hooks/use-brand";
29
30
 
30
31
  export interface TriggerConfigProps {
31
32
  actionId: string;
@@ -100,6 +101,7 @@ function Form({ actionId, config, save, saveAndRefresh, trigger, types, onSaved
100
101
  }
101
102
 
102
103
  export function TriggerConfig({ actionId, onSaved, className }: TriggerConfigProps) {
104
+ const brand = useBrand();
103
105
  const wf = useWorkflow(actionId);
104
106
  const status = useG8Query((signal) => g8Get(`/workflows/${encodeURIComponent(actionId)}/trigger-status`, undefined, { signal }), [actionId]);
105
107
  const catalog = useG8Query((signal) => g8Get("/workflows/node-types/schema", { node_type: "trigger", detail: "slim" }, { signal }), []);
@@ -127,7 +129,7 @@ export function TriggerConfig({ actionId, onSaved, className }: TriggerConfigPro
127
129
  </Card>
128
130
  {catalog.error ? <p className="mb-2 text-xs">The trigger catalog could not be loaded; the current type is still editable.</p> : null}
129
131
  {trigger ? <Form key={JSON.stringify(trigger.config)} actionId={actionId} config={wf.config} save={wf.save} saveAndRefresh={wf.saveAndRefresh} trigger={trigger} types={types} onSaved={refresh} /> : (
130
- <p className="text-muted-foreground text-sm">There is no trigger node to configure ({nodesOf(wf.config).length} steps). Add a trigger step in the graph8 app.</p>
132
+ <p className="text-muted-foreground text-sm">There is no trigger node to configure ({nodesOf(wf.config).length} steps). Add a trigger step in the {brand.linkLabel} app.</p>
131
133
  )}
132
134
  </div>
133
135
  );
@@ -26,6 +26,7 @@ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
26
26
  import { useWorkflow } from "../hooks/use-workflow";
27
27
  import { addStep, asRec, canMove, isLinear, moveStep, newStep, nodeIdOf, nodeNameOf, nodeTypeOf, orderedSteps, payload, removeStep, setStep, str } from "../lib/graph";
28
28
  import type { Rec, WfNode } from "../lib/graph";
29
+ import { useBrand } from "@/g8/hooks/use-brand";
29
30
 
30
31
  export interface WorkflowEditorProps {
31
32
  actionId: string;
@@ -85,6 +86,7 @@ function AddStep({ onAdd }: { onAdd: (type: string) => void }) {
85
86
  }
86
87
 
87
88
  export function WorkflowEditor({ actionId, onSaved, className }: WorkflowEditorProps) {
89
+ const brand = useBrand();
88
90
  const wf = useWorkflow(actionId);
89
91
  // The draft remembers which workflow it belongs to, so switching `actionId` can never show or save the old one.
90
92
  const [draftState, setDraftState] = useState<{ id: string; config: Rec } | null>(null);
@@ -113,7 +115,7 @@ export function WorkflowEditor({ actionId, onSaved, className }: WorkflowEditorP
113
115
  <Button disabled={!draft} onClick={() => setConfirm(true)}>Save workflow</Button>
114
116
  </div>
115
117
  </div>
116
- {!linear ? <p className="text-muted-foreground mb-3 text-sm">This workflow branches, so steps can be renamed and reconfigured here but not added, moved or removed. Use the graph8 app to change its shape.</p> : null}
118
+ {!linear ? <p className="text-muted-foreground mb-3 text-sm">This workflow branches, so steps can be renamed and reconfigured here but not added, moved or removed. Use the {brand.linkLabel} app to change its shape.</p> : null}
117
119
  {wf.save.error ? <ApiError error={wf.save.error} title="Could not save the workflow" /> : null}
118
120
  <div className="mb-4 space-y-3">
119
121
  {steps.map((s, i) => (