@graph8/sdk 0.362.0 → 0.373.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 (615) hide show
  1. package/README.md +33 -10
  2. package/blocks/_foundation/README.md +78 -1
  3. package/blocks/_foundation/block.json +123 -6
  4. package/blocks/_foundation/components/api-error.tsx +9 -7
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  7. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  8. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  9. package/blocks/_foundation/components/empty-state.tsx +5 -2
  10. package/blocks/_foundation/components/list-select.tsx +112 -0
  11. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  12. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  13. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  14. package/blocks/_foundation/components/screen/index.ts +2 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  17. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  18. package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
  19. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  20. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  21. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  22. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  23. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  24. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  25. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  26. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  27. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  28. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  29. package/blocks/_foundation/lib/g8-client.ts +1 -1
  30. package/blocks/_foundation/lib/paging.ts +57 -0
  31. package/blocks/_foundation/lib/plural.ts +23 -0
  32. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  33. package/blocks/_foundation/server/connect-route.ts +15 -0
  34. package/blocks/_foundation/server/connect.ts +127 -0
  35. package/blocks/_foundation/server/local-key.ts +70 -0
  36. package/blocks/_foundation/server/proxy.ts +103 -3
  37. package/blocks/_foundation/server/route.ts +16 -4
  38. package/blocks/agents/README.md +93 -2
  39. package/blocks/agents/block.json +343 -2
  40. package/blocks/agents/components/agent-activity.tsx +112 -0
  41. package/blocks/agents/components/agent-approvals.tsx +135 -0
  42. package/blocks/agents/components/agent-builder.tsx +29 -10
  43. package/blocks/agents/components/agent-conversations.tsx +143 -0
  44. package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
  45. package/blocks/agents/components/agent-operators.tsx +164 -0
  46. package/blocks/agents/components/agent-run-detail.tsx +119 -0
  47. package/blocks/agents/components/agent-runs.tsx +19 -10
  48. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  49. package/blocks/agents/components/agent-test-history.tsx +121 -0
  50. package/blocks/agents/components/agents-screen.tsx +226 -0
  51. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  52. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  53. package/blocks/agents/components/from-template-button.tsx +65 -0
  54. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  55. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  56. package/blocks/agents/components/knowledge-form.tsx +69 -0
  57. package/blocks/agents/components/name-dialog.tsx +32 -0
  58. package/blocks/agents/components/receptionist-button.tsx +46 -0
  59. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  60. package/blocks/agents/components/skill-editor.tsx +147 -0
  61. package/blocks/agents/components/skill-library.tsx +111 -0
  62. package/blocks/agents/lib/agent-types.ts +7 -1
  63. package/blocks/agents/lib/facts.tsx +13 -0
  64. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  65. package/blocks/agents/lib/load-more.tsx +141 -0
  66. package/blocks/agents/lib/skill-utils.ts +91 -0
  67. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  68. package/blocks/analytics/README.md +13 -0
  69. package/blocks/analytics/block.json +45 -1
  70. package/blocks/analytics/components/analytics-screen.tsx +222 -0
  71. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  72. package/blocks/analytics/components/leaderboard.tsx +14 -7
  73. package/blocks/analytics/components/report-builder.tsx +38 -11
  74. package/blocks/analytics/components/report-detail.tsx +84 -0
  75. package/blocks/analytics/components/trend-chart.tsx +62 -0
  76. package/blocks/appointments/README.md +6 -0
  77. package/blocks/appointments/block.json +33 -2
  78. package/blocks/appointments/components/availability-editor.tsx +17 -5
  79. package/blocks/appointments/components/booking-page.tsx +2 -2
  80. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  81. package/blocks/appointments/components/event-type-related.tsx +4 -2
  82. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  83. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  84. package/blocks/appointments/components/meeting-list.tsx +38 -18
  85. package/blocks/appointments/components/meetings-screen.tsx +7 -4
  86. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  87. package/blocks/appointments/components/scheduling-screen.tsx +2 -1
  88. package/blocks/block.schema.json +18 -0
  89. package/blocks/block.types.ts +17 -0
  90. package/blocks/campaigns/README.md +41 -11
  91. package/blocks/campaigns/block.json +105 -42
  92. package/blocks/campaigns/components/campaign-assets.tsx +17 -6
  93. package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
  94. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  95. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  96. package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
  97. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  98. package/blocks/campaigns/components/icp-detail.tsx +163 -0
  99. package/blocks/campaigns/components/icp-editor.tsx +22 -17
  100. package/blocks/campaigns/components/persona-detail.tsx +181 -0
  101. package/blocks/campaigns/components/persona-editor.tsx +19 -11
  102. package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
  103. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  104. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  105. package/blocks/coach/README.md +10 -0
  106. package/blocks/coach/block.json +16 -1
  107. package/blocks/coach/components/call-scorecard.tsx +7 -7
  108. package/blocks/coach/components/coach-screen.tsx +222 -0
  109. package/blocks/company-intelligence/README.md +132 -0
  110. package/blocks/company-intelligence/block.json +318 -0
  111. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  112. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  113. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  114. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  115. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  116. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  117. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  118. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  119. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  120. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  121. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  122. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  123. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  124. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  125. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  126. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  127. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  128. package/blocks/copilot/README.md +13 -0
  129. package/blocks/copilot/block.json +14 -1
  130. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  131. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  132. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  133. package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
  134. package/blocks/crm/README.md +34 -0
  135. package/blocks/crm/block.json +107 -2
  136. package/blocks/crm/components/companies-screen.tsx +359 -0
  137. package/blocks/crm/components/company-detail.tsx +10 -8
  138. package/blocks/crm/components/company-table.tsx +11 -4
  139. package/blocks/crm/components/contact-detail.tsx +10 -7
  140. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  141. package/blocks/crm/components/contact-table.tsx +10 -3
  142. package/blocks/crm/components/contacts-screen.tsx +377 -0
  143. package/blocks/crm/components/dedupe-review.tsx +13 -6
  144. package/blocks/crm/components/import-wizard.tsx +7 -9
  145. package/blocks/crm/components/list-select.tsx +20 -0
  146. package/blocks/crm/components/notes-panel.tsx +4 -4
  147. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  148. package/blocks/crm/components/record-form.tsx +6 -4
  149. package/blocks/crm/components/task-list.tsx +4 -4
  150. package/blocks/crm/components/tasks-screen.tsx +399 -0
  151. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  152. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  153. package/blocks/crm/hooks/use-page-query.ts +9 -1
  154. package/blocks/crm/lib/crm-types.ts +26 -2
  155. package/blocks/crm/lib/read-lists.ts +16 -0
  156. package/blocks/crm/lib/screen-parts.tsx +105 -0
  157. package/blocks/deals/README.md +24 -6
  158. package/blocks/deals/block.json +160 -5
  159. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  160. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  161. package/blocks/deals/components/deal-detail.tsx +25 -13
  162. package/blocks/deals/components/deal-filters.tsx +65 -0
  163. package/blocks/deals/components/deal-line-items.tsx +120 -0
  164. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  165. package/blocks/deals/components/deal-related.tsx +31 -10
  166. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  167. package/blocks/deals/components/deals-screen.tsx +66 -26
  168. package/blocks/deals/components/forecast-view.tsx +4 -4
  169. package/blocks/deals/components/pipeline-board.tsx +8 -6
  170. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  171. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  172. package/blocks/deals/components/stage-editor.tsx +5 -5
  173. package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
  174. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  175. package/blocks/deals/lib/deal-extras.ts +156 -0
  176. package/blocks/dialer/README.md +45 -4
  177. package/blocks/dialer/block.json +84 -3
  178. package/blocks/dialer/components/call-contact-button.tsx +269 -0
  179. package/blocks/dialer/components/call-log-screen.tsx +208 -0
  180. package/blocks/dialer/components/call-log.tsx +23 -7
  181. package/blocks/dialer/components/call-queue.tsx +4 -4
  182. package/blocks/dialer/components/dialer-screen.tsx +361 -0
  183. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  184. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  185. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  186. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  187. package/blocks/dialer/components/session-start-form.tsx +61 -12
  188. package/blocks/dialer/components/softphone.tsx +4 -2
  189. package/blocks/enrichment/README.md +29 -7
  190. package/blocks/enrichment/block.json +49 -9
  191. package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
  192. package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
  193. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  194. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  195. package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
  196. package/blocks/enrichment/components/waterfall-config.tsx +66 -17
  197. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  198. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  199. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  200. package/blocks/forms/README.md +8 -0
  201. package/blocks/forms/block.json +139 -18
  202. package/blocks/forms/components/form-activity.tsx +71 -0
  203. package/blocks/forms/components/form-backfill.tsx +108 -0
  204. package/blocks/forms/components/form-builder.tsx +4 -2
  205. package/blocks/forms/components/form-embed.tsx +4 -2
  206. package/blocks/forms/components/form-groups.tsx +151 -0
  207. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  208. package/blocks/forms/components/form-route-history.tsx +72 -0
  209. package/blocks/forms/components/form-saved-views.tsx +81 -0
  210. package/blocks/forms/components/forms-screen.tsx +56 -19
  211. package/blocks/forms/components/more-rows.tsx +30 -0
  212. package/blocks/forms/components/submissions-export.tsx +67 -0
  213. package/blocks/forms/components/submissions-table.tsx +17 -6
  214. package/blocks/forms/lib/forms-extra.ts +67 -0
  215. package/blocks/inbox/README.md +32 -3
  216. package/blocks/inbox/block.json +29 -2
  217. package/blocks/inbox/components/inbox-list.tsx +7 -3
  218. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  219. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  220. package/blocks/inbox/components/reply-composer.tsx +65 -9
  221. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  222. package/blocks/inbox/components/thread-badge.tsx +2 -2
  223. package/blocks/inbox/components/thread-view.tsx +1 -1
  224. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  225. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  226. package/blocks/inbox/lib/inbox-types.ts +47 -1
  227. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  228. package/blocks/integrations/README.md +63 -1
  229. package/blocks/integrations/block.json +234 -15
  230. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  231. package/blocks/integrations/components/connection-health.tsx +59 -0
  232. package/blocks/integrations/components/crm-detail.tsx +119 -0
  233. package/blocks/integrations/components/crm-records.tsx +103 -0
  234. package/blocks/integrations/components/field-mappings.tsx +61 -0
  235. package/blocks/integrations/components/integration-list.tsx +13 -8
  236. package/blocks/integrations/components/integrations-screen.tsx +152 -0
  237. package/blocks/integrations/components/migration-panel.tsx +133 -0
  238. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  239. package/blocks/integrations/components/sync-status.tsx +11 -9
  240. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  241. package/blocks/integrations/components/webhook-detail.tsx +179 -0
  242. package/blocks/integrations/components/webhook-manager.tsx +43 -25
  243. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  244. package/blocks/integrations/lib/crm-utils.ts +39 -0
  245. package/blocks/integrations/lib/facts.tsx +13 -0
  246. package/blocks/integrations/lib/integrations-utils.ts +11 -1
  247. package/blocks/integrations/lib/load-more.tsx +141 -0
  248. package/blocks/intent/README.md +14 -0
  249. package/blocks/intent/block.json +27 -1
  250. package/blocks/intent/components/account-intent-score.tsx +5 -5
  251. package/blocks/intent/components/intent-feed.tsx +13 -10
  252. package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
  253. package/blocks/intent/components/intent-screen.tsx +265 -0
  254. package/blocks/intent/components/keyword-detail.tsx +124 -0
  255. package/blocks/intent/lib/intent-utils.ts +4 -4
  256. package/blocks/lifecycle/block.json +13 -8
  257. package/blocks/lifecycle/components/account-related.tsx +8 -1
  258. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  259. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  260. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  261. package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
  262. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  263. package/blocks/lists/README.md +20 -1
  264. package/blocks/lists/block.json +69 -3
  265. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  266. package/blocks/lists/components/add-to-list-button.tsx +20 -9
  267. package/blocks/lists/components/list-picker.tsx +17 -3
  268. package/blocks/lists/components/list-table.tsx +12 -6
  269. package/blocks/lists/components/lists-screen.tsx +346 -0
  270. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  271. package/blocks/lists/components/suppression-manager.tsx +33 -8
  272. package/blocks/lists/components/suppressions-screen.tsx +264 -0
  273. package/blocks/mailboxes/README.md +17 -0
  274. package/blocks/mailboxes/block.json +69 -17
  275. package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
  276. package/blocks/mailboxes/components/domain-detail.tsx +148 -0
  277. package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
  278. package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
  279. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  280. package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
  281. package/blocks/mailboxes/components/warmup-health.tsx +24 -22
  282. package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
  283. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  284. package/blocks/newsletter/README.md +12 -0
  285. package/blocks/newsletter/block.json +157 -24
  286. package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
  287. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  288. package/blocks/newsletter/components/more-rows.tsx +30 -0
  289. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  290. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  291. package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
  292. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  293. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  294. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  295. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  296. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  297. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  298. package/blocks/nurture/block.json +28 -14
  299. package/blocks/nurture/components/nurture-builder.tsx +13 -3
  300. package/blocks/nurture/components/nurture-enroll.tsx +12 -10
  301. package/blocks/nurture/components/nurture-form.tsx +27 -10
  302. package/blocks/nurture/components/nurture-screen.tsx +26 -11
  303. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  304. package/blocks/nurture/lib/nurture-types.ts +1 -1
  305. package/blocks/objects/README.md +18 -1
  306. package/blocks/objects/block.json +42 -21
  307. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  308. package/blocks/objects/components/object-not-found.tsx +3 -1
  309. package/blocks/objects/components/object-record-detail.tsx +10 -2
  310. package/blocks/objects/components/object-schema-editor.tsx +4 -2
  311. package/blocks/objects/components/object-table.tsx +13 -3
  312. package/blocks/objects/components/objects-screen.tsx +147 -0
  313. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  314. package/blocks/objects/lib/object-types.ts +1 -1
  315. package/blocks/pages/README.md +14 -0
  316. package/blocks/pages/block.json +140 -23
  317. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  318. package/blocks/pages/components/content-library.tsx +9 -4
  319. package/blocks/pages/components/more-rows.tsx +30 -0
  320. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  321. package/blocks/pages/components/page-analytics.tsx +176 -0
  322. package/blocks/pages/components/page-clone.tsx +99 -0
  323. package/blocks/pages/components/page-domains.tsx +167 -0
  324. package/blocks/pages/components/page-editor.tsx +1 -1
  325. package/blocks/pages/components/page-generation-status.tsx +48 -0
  326. package/blocks/pages/components/page-list.tsx +2 -1
  327. package/blocks/pages/components/page-tracking.tsx +137 -0
  328. package/blocks/pages/components/pages-screen.tsx +57 -16
  329. package/blocks/pages/components/seo-report.tsx +6 -1
  330. package/blocks/pages/lib/studio.ts +7 -0
  331. package/blocks/phone-numbers/README.md +74 -0
  332. package/blocks/phone-numbers/block.json +262 -0
  333. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  334. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  335. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  336. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  337. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  338. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  339. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  340. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  341. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  342. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  343. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  344. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  345. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  346. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  347. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  348. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  349. package/blocks/pickers/README.md +23 -0
  350. package/blocks/pickers/block.json +103 -0
  351. package/blocks/pickers/components/company-picker.tsx +90 -0
  352. package/blocks/pickers/components/contact-picker.tsx +131 -0
  353. package/blocks/pickers/components/member-picker.tsx +99 -0
  354. package/blocks/pickers/components/search-picker.tsx +98 -0
  355. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  356. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  357. package/blocks/pickers/lib/use-debounced.ts +13 -0
  358. package/blocks/quotes/README.md +13 -0
  359. package/blocks/quotes/block.json +65 -13
  360. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  361. package/blocks/quotes/components/product-picker.tsx +3 -1
  362. package/blocks/quotes/components/quote-builder.tsx +1 -1
  363. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  364. package/blocks/quotes/components/quote-list.tsx +5 -3
  365. package/blocks/quotes/components/quote-related.tsx +8 -4
  366. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  367. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  368. package/blocks/radar/README.md +10 -0
  369. package/blocks/radar/block.json +41 -1
  370. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  371. package/blocks/radar/components/competitor-detail.tsx +117 -0
  372. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  373. package/blocks/radar/components/radar-overview.tsx +4 -4
  374. package/blocks/radar/components/radar-report.tsx +4 -2
  375. package/blocks/radar/components/radar-screen.tsx +214 -0
  376. package/blocks/radar/components/report-detail.tsx +104 -0
  377. package/blocks/search/README.md +10 -0
  378. package/blocks/search/block.json +16 -1
  379. package/blocks/search/components/filter-builder.tsx +3 -3
  380. package/blocks/search/components/prospect-results.tsx +5 -3
  381. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  382. package/blocks/search/components/saved-searches.tsx +4 -1
  383. package/blocks/search/hooks/use-filter-options.ts +20 -6
  384. package/blocks/search/lib/search-types.ts +2 -0
  385. package/blocks/sequencer/README.md +55 -9
  386. package/blocks/sequencer/block.json +126 -19
  387. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  388. package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
  389. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  390. package/blocks/sequencer/components/sequence-builder.tsx +224 -60
  391. package/blocks/sequencer/components/sequence-detail.tsx +261 -0
  392. package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
  393. package/blocks/sequencer/components/sequence-list.tsx +34 -9
  394. package/blocks/sequencer/components/sequence-stats.tsx +11 -10
  395. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  396. package/blocks/sequencer/components/sequences-screen.tsx +162 -0
  397. package/blocks/sequencer/components/step-editor.tsx +14 -12
  398. package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
  399. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  400. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  401. package/blocks/social-ads/README.md +19 -0
  402. package/blocks/social-ads/block.json +233 -1
  403. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  404. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  405. package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
  406. package/blocks/social-ads/components/audience-sync.tsx +17 -9
  407. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  408. package/blocks/social-ads/components/campaign-detail.tsx +138 -0
  409. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  410. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  411. package/blocks/social-ads/components/creative-detail.tsx +155 -0
  412. package/blocks/social-ads/components/creative-gallery.tsx +35 -7
  413. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  414. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  415. package/blocks/social-ads/components/post-composer.tsx +7 -3
  416. package/blocks/social-ads/components/post-detail.tsx +163 -0
  417. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  418. package/blocks/social-ads/components/social-performance.tsx +80 -0
  419. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  420. package/blocks/social-ads/components/social-screen.tsx +187 -0
  421. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  422. package/blocks/surveys/README.md +91 -0
  423. package/blocks/surveys/block.json +299 -0
  424. package/blocks/surveys/components/account-nps.tsx +69 -0
  425. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  426. package/blocks/surveys/components/org-summary.tsx +82 -0
  427. package/blocks/surveys/components/sending-domains.tsx +113 -0
  428. package/blocks/surveys/components/survey-form.tsx +126 -0
  429. package/blocks/surveys/components/survey-responses.tsx +145 -0
  430. package/blocks/surveys/components/survey-results.tsx +72 -0
  431. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  432. package/blocks/surveys/components/survey-send.tsx +162 -0
  433. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  434. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  435. package/blocks/surveys/lib/surveys-types.ts +228 -0
  436. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  437. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  438. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  439. package/blocks/visitors/README.md +10 -0
  440. package/blocks/visitors/block.json +25 -1
  441. package/blocks/visitors/components/snippet-install.tsx +21 -10
  442. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  443. package/blocks/visitors/components/visitor-company-card.tsx +26 -16
  444. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  445. package/blocks/visitors/components/visitor-feed.tsx +16 -7
  446. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  447. package/blocks/webchat/README.md +10 -0
  448. package/blocks/webchat/block.json +21 -7
  449. package/blocks/webchat/components/chat-widget-config.tsx +8 -5
  450. package/blocks/webchat/components/conversation-list.tsx +8 -4
  451. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  452. package/blocks/workbench/README.md +11 -0
  453. package/blocks/workbench/block.json +48 -1
  454. package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
  455. package/blocks/workbench/components/run-progress.tsx +1 -1
  456. package/blocks/workbench/components/workbench-grid.tsx +10 -4
  457. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  458. package/blocks/workbench/components/workbench-screen.tsx +202 -0
  459. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  460. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  461. package/blocks/workflows/README.md +13 -0
  462. package/blocks/workflows/block.json +28 -9
  463. package/blocks/workflows/components/run-history.tsx +31 -9
  464. package/blocks/workflows/components/trigger-config.tsx +3 -1
  465. package/blocks/workflows/components/workflow-editor.tsx +4 -2
  466. package/blocks/workflows/components/workflow-list.tsx +13 -6
  467. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  468. package/dist/cli.js +3109 -1977
  469. package/dist/errors.d.mts +34 -0
  470. package/dist/errors.d.ts +34 -0
  471. package/dist/errors.js +46 -0
  472. package/dist/errors.js.map +1 -0
  473. package/dist/errors.mjs +21 -0
  474. package/dist/errors.mjs.map +1 -0
  475. package/dist/index.browser.js +11 -5
  476. package/dist/index.browser.js.map +1 -1
  477. package/dist/index.browser.mjs +11 -5
  478. package/dist/index.browser.mjs.map +1 -1
  479. package/dist/index.d.mts +164 -25
  480. package/dist/index.d.ts +164 -25
  481. package/dist/index.js +11 -5
  482. package/dist/index.js.map +1 -1
  483. package/dist/index.mjs +11 -5
  484. package/dist/index.mjs.map +1 -1
  485. package/dist/react.browser.js +10 -4
  486. package/dist/react.browser.js.map +1 -1
  487. package/dist/react.browser.mjs +10 -4
  488. package/dist/react.browser.mjs.map +1 -1
  489. package/dist/react.d.mts +142 -5
  490. package/dist/react.d.ts +142 -5
  491. package/dist/react.js +10 -4
  492. package/dist/react.js.map +1 -1
  493. package/dist/react.mjs +10 -4
  494. package/dist/react.mjs.map +1 -1
  495. package/dist/types/blocks/add.d.ts +11 -4
  496. package/dist/types/blocks/brand.d.ts +122 -0
  497. package/dist/types/blocks/compose.d.ts +53 -0
  498. package/dist/types/blocks/create.d.ts +27 -20
  499. package/dist/types/blocks/doctor.d.ts +6 -1
  500. package/dist/types/blocks/flows.d.ts +7 -1
  501. package/dist/types/blocks/navfile.d.ts +35 -0
  502. package/dist/types/blocks/orgBrand.d.ts +34 -0
  503. package/dist/types/blocks/paths.d.ts +4 -0
  504. package/dist/types/blocks/prefixes.d.ts +37 -0
  505. package/dist/types/blocks/registry.d.ts +17 -0
  506. package/dist/types/blocks/screens.d.ts +53 -0
  507. package/dist/types/cli.d.ts +5 -0
  508. package/dist/types/errors.d.ts +33 -0
  509. package/dist/types/http.d.ts +2 -18
  510. package/dist/types/version.d.ts +2 -2
  511. package/docs/agents.md +15 -4
  512. package/docs/api/README.md +2 -2
  513. package/docs/api/by-path.md +1 -0
  514. package/docs/api/desk.d.ts +98 -2
  515. package/docs/api/desk.md +123 -1
  516. package/docs/api/inbox.d.ts +10 -2
  517. package/docs/api/inbox.md +10 -2
  518. package/docs/api/seo.d.ts +19 -1
  519. package/docs/api/seo.md +24 -1
  520. package/package.json +16 -2
  521. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  522. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  523. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  524. package/stacks/6sense.json +55 -17
  525. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  526. package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
  527. package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
  528. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  529. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  530. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  531. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  532. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  533. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  534. package/stacks/apollo.json +147 -37
  535. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  536. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  537. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  538. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  539. package/stacks/calendly.json +64 -0
  540. package/stacks/clay/pages/companies.tsx.tmpl +31 -0
  541. package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
  542. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  543. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  544. package/stacks/clay.json +73 -27
  545. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  546. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  547. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  548. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  549. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  550. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  551. package/stacks/gong.json +98 -35
  552. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  553. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  554. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  555. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  556. package/stacks/hootsuite.json +104 -0
  557. package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
  558. package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
  559. package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
  560. package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
  561. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  562. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  563. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  564. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  565. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  566. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  567. package/stacks/hubspot.json +181 -64
  568. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  569. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  570. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  571. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  572. package/stacks/intercom.json +52 -20
  573. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  574. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  575. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  576. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  577. package/stacks/mailchimp.json +68 -34
  578. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  579. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  580. package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
  581. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  582. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  583. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  584. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  585. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  586. package/stacks/outreach.json +106 -25
  587. package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
  588. package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
  589. package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
  590. package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
  591. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  592. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  593. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  594. package/stacks/pipedrive.json +117 -0
  595. package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
  596. package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
  597. package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
  598. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  599. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  600. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  601. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  602. package/stacks/salesforce.json +85 -31
  603. package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
  604. package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
  605. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  606. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  607. package/stacks/zoominfo.json +115 -0
  608. package/styles/styles.css +1 -1
  609. package/templates/next-app/README.md.tmpl +40 -9
  610. package/templates/next-app/app/globals.css.tmpl +349 -64
  611. package/templates/next-app/app/layout.tsx.tmpl +7 -2
  612. package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
  613. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
  614. package/templates/next-app/dot-gitignore.tmpl +2 -0
  615. package/templates/next-app/package.json.tmpl +1 -1
@@ -19,12 +19,13 @@ import { Button, Card, CardContent, CardHeader, CardTitle, Input, Label, Skeleto
19
19
  import { ApiError } from "@/g8/components/api-error";
20
20
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
21
21
  import { EmptyState } from "@/g8/components/empty-state";
22
- import { G8Error } from "@graph8/sdk";
22
+ import { G8Error } from "@graph8/sdk/errors";
23
23
  import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
24
24
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
25
25
  import { useG8Query } from "@/g8/hooks/use-g8-query";
26
26
  import { isCssLength, isHexColor, pickItems, readSettings } from "../lib/webchat-types";
27
27
  import type { ChatBot, StarterQuestion, WidgetSettings } from "../lib/webchat-types";
28
+ import { useBrand } from "@/g8/hooks/use-brand";
28
29
 
29
30
  export interface ChatWidgetConfigProps {
30
31
  initialBotId?: string;
@@ -45,6 +46,7 @@ function validate(s: WidgetSettings): string | null {
45
46
  }
46
47
 
47
48
  export function ChatWidgetConfig({ initialBotId, surface = "external" }: ChatWidgetConfigProps) {
49
+ const brand = useBrand();
48
50
  const bots = useG8Query((signal) => g8Get<unknown>("/webchat/chat-bots", {}, { signal }).then((r) => pickItems<ChatBot>(r.data)), []);
49
51
  const [botId, setBotId] = useState(initialBotId ?? "");
50
52
  const [newBot, setNewBot] = useState("");
@@ -69,7 +71,8 @@ export function ChatWidgetConfig({ initialBotId, surface = "external" }: ChatWid
69
71
  }, [id]);
70
72
 
71
73
  useEffect(() => {
72
- if (settings.loading) return;
74
+ // A failed load says nothing about whether settings exist: keep the draft and (below) keep Save off.
75
+ if (settings.loading || settings.error) return;
73
76
  setExists(settings.data != null);
74
77
  setDraft({ ...EMPTY, ...(settings.data ?? {}) });
75
78
  }, [settings.data, settings.loading, id]);
@@ -118,9 +121,9 @@ export function ChatWidgetConfig({ initialBotId, surface = "external" }: ChatWid
118
121
  <div><Label htmlFor="g8-wc-text" className="mb-1">Reply text</Label><Input id="g8-wc-text" placeholder="#111111" value={draft.assistantMessageTextColor ?? ""} onChange={(e) => set({ assistantMessageTextColor: e.target.value })} /></div>
119
122
  <label className="flex items-center gap-2 self-end text-sm"><input type="checkbox" checked={Boolean(draft.darkMode)} onChange={(e) => set({ darkMode: e.target.checked })} />Dark mode</label>
120
123
  </div>
121
- {problem ? <p role="alert" className="text-sm text-red-700">{problem}</p> : null}
124
+ {problem ? <p role="alert" className="text-sm text-error-foreground">{problem}</p> : null}
122
125
  {save.error ? <ApiError error={save.error} title="Could not save the widget settings" /> : null}
123
- <div><Button loading={save.loading} disabled={Boolean(problem) || settings.loading} onClick={() => void save.mutate(undefined)}>{exists ? "Save changes" : "Create widget settings"}</Button></div>
126
+ <div><Button loading={save.loading} disabled={Boolean(problem) || settings.loading || Boolean(settings.error)} onClick={() => void save.mutate(undefined)}>{exists ? "Save changes" : "Create widget settings"}</Button></div>
124
127
  </CardContent>
125
128
  </Card>
126
129
  <Card>
@@ -141,7 +144,7 @@ export function ChatWidgetConfig({ initialBotId, surface = "external" }: ChatWid
141
144
  <Card>
142
145
  <CardHeader><CardTitle>Put the widget on your site</CardTitle></CardHeader>
143
146
  <CardContent className="grid gap-2">
144
- <p className="text-muted-foreground text-sm">The widget loads through the graph8 tracking snippet. Add it to every page that should show the chat.</p>
147
+ <p className="text-muted-foreground text-sm">The widget loads through the {brand.linkLabel} tracking snippet. Add it to every page that should show the chat.</p>
145
148
  {snippet.error ? <ApiError error={snippet.error} title="Could not load the snippet" onRetry={() => void snippet.refetch()} /> : snippet.data?.script_tag ? <pre aria-label="Embed code" className="bg-muted overflow-auto rounded-md p-3 text-xs">{snippet.data.script_tag}</pre> : <Skeleton className="h-12 w-full" />}
146
149
  </CardContent>
147
150
  </Card>
@@ -18,15 +18,19 @@ import { g8Get } from "@/g8/lib/g8-client";
18
18
  import { useG8Query } from "@/g8/hooks/use-g8-query";
19
19
  import { pickItems, visitorCompany, visitorEmail, visitorLastSeen, visitorName } from "../lib/webchat-types";
20
20
  import type { Visitor } from "../lib/webchat-types";
21
+ import { rowActivationProps } from "@/g8/hooks/use-row-activation";
22
+ import { RecordLink } from "@/g8/components/screen/record-link";
21
23
 
22
24
  export interface ConversationListProps {
25
+ /** URL param the host screen reads for this record type. With it the name is a real link (Cmd-click opens it in a new tab); without it a plain button. */
26
+ linkParam?: string;
23
27
  /** Limit the funnel to one bot. */
24
28
  botId?: string;
25
29
  pageSize?: number;
26
30
  onSelectVisitor?: (visitor: Visitor) => void;
27
31
  }
28
32
 
29
- export function ConversationList({ botId, pageSize = 25, onSelectVisitor }: ConversationListProps) {
33
+ export function ConversationList({ linkParam, botId, pageSize = 25, onSelectVisitor }: ConversationListProps) {
30
34
  const [page, setPage] = useState(1);
31
35
  const [identifiedOnly, setIdentifiedOnly] = useState(false);
32
36
  const [text, setText] = useState("");
@@ -43,7 +47,7 @@ export function ConversationList({ botId, pageSize = 25, onSelectVisitor }: Conv
43
47
  <div className="grid gap-3">
44
48
  {counts.length > 0 ? (
45
49
  <dl className="grid grid-cols-2 gap-2 sm:grid-cols-4">
46
- {counts.map(([k, v]) => <div key={k} className="rounded-md border p-3"><dt className="text-muted-foreground text-xs">{k.replace(/_/g, " ")}</dt><dd className="text-lg font-semibold">{v.toLocaleString()}</dd></div>)}
50
+ {counts.map(([k, v]) => <div key={k} className="rounded-md border p-3"><dt className="text-muted-foreground text-xs">{k.replace(/_/g, " ")}</dt><dd className="text-lg font-medium">{v.toLocaleString()}</dd></div>)}
47
51
  </dl>
48
52
  ) : null}
49
53
  <form className="flex flex-wrap items-center gap-2" onSubmit={(e) => { e.preventDefault(); setPage(1); setSearch(text.trim()); }}>
@@ -58,8 +62,8 @@ export function ConversationList({ botId, pageSize = 25, onSelectVisitor }: Conv
58
62
  <TableHeader><TableRow><TableHead>Visitor</TableHead><TableHead>Email</TableHead><TableHead>Company</TableHead><TableHead>Last seen</TableHead></TableRow></TableHeader>
59
63
  <TableBody>
60
64
  {items.map((v, i) => (
61
- <TableRow key={String(v.id ?? i)} className={onSelectVisitor ? "cursor-pointer" : undefined} onClick={() => onSelectVisitor?.(v)}>
62
- <TableCell>{visitorName(v)}</TableCell><TableCell>{visitorEmail(v)}</TableCell><TableCell>{visitorCompany(v)}</TableCell><TableCell>{visitorLastSeen(v)}</TableCell>
65
+ <TableRow key={String(v.id ?? i)} className={onSelectVisitor ? "cursor-pointer" : undefined} {...rowActivationProps(() => onSelectVisitor?.(v))}>
66
+ <TableCell>{v.id != null && onSelectVisitor ? <RecordLink param={linkParam} id={String(v.id)} onOpen={() => onSelectVisitor(v)}>{visitorName(v)}</RecordLink> : visitorName(v)}</TableCell><TableCell>{visitorEmail(v)}</TableCell><TableCell>{visitorCompany(v)}</TableCell><TableCell>{visitorLastSeen(v)}</TableCell>
63
67
  </TableRow>
64
68
  ))}
65
69
  </TableBody>
@@ -0,0 +1,245 @@
1
+ "use client";
2
+
3
+ /**
4
+ * WebchatScreen: the finished web chat page with three tabs.
5
+ * Visitors: the people who used the widget (search, identified-only, paging), engagement funnel as stats, and a detail
6
+ * panel for the selected visitor (deep link `?id=`). Message threads live in the inbox block: pass
7
+ * `renderDetailActions` to link a visitor to their conversation.
8
+ * Widget: ChatWidgetConfig (pick or create a chat bot, style the widget, starter questions, delete a question with confirm).
9
+ * Install: the tracking snippet to paste on your pages, plus what must be set up in graph8 first.
10
+ *
11
+ * SDK operations (all through the composed parts): listWebchatVisitors, getWebchatVisitorFunnel, getOrgSnippet (read);
12
+ * ChatWidgetConfig's bot, settings and question calls (write); deleteWebchatQuestion (destructive, ConfirmSpendDialog).
13
+ * Saves in the Widget tab are live for the next visitor. Mapping an answering agent to a bot is done in the graph8 app.
14
+ */
15
+ import { useEffect, useMemo, useState, type ReactNode } from "react";
16
+ import { Copy, MessageSquare } from "lucide-react";
17
+ import { Button, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
18
+ import { ApiError } from "@/g8/components/api-error";
19
+ import { EmptyState } from "@/g8/components/empty-state";
20
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
21
+ import { EntityAvatar } from "@/g8/components/screen/entity-avatar";
22
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
23
+ import { ScreenLayout } from "@/g8/components/screen/screen-layout";
24
+ import { SectionCard } from "@/g8/components/screen/section-card";
25
+ import { SplitView } from "@/g8/components/screen/split-view";
26
+ import { StatusPill } from "@/g8/components/screen/status-pill";
27
+ import { Toolbar } from "@/g8/components/screen/toolbar";
28
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
29
+ import { useUrlState } from "@/g8/hooks/use-url-state";
30
+ import { g8Get } from "@/g8/lib/g8-client";
31
+ import { pickItems, visitorCompany, visitorEmail, visitorLastSeen, visitorName } from "../lib/webchat-types";
32
+ import type { Visitor } from "../lib/webchat-types";
33
+ import { ChatWidgetConfig } from "./chat-widget-config";
34
+ import { rowActivationProps } from "@/g8/hooks/use-row-activation";
35
+ import { RecordLink } from "@/g8/components/screen/record-link";
36
+ import { useBrand } from "@/g8/hooks/use-brand";
37
+
38
+ export interface WebchatScreenProps {
39
+ /** Limit the funnel and open the Widget tab on this bot. */
40
+ botId?: string;
41
+ pageSize?: number;
42
+ /** Extra buttons in the header. */
43
+ toolbarActions?: ReactNode;
44
+ /** Buttons in the visitor detail header (for example "Open in inbox"). */
45
+ renderDetailActions?: (visitor: Visitor) => ReactNode;
46
+ /** Extra tabs in the visitor detail panel. */
47
+ extraDetailTabs?: Array<{ id: string; label: string; render: (visitor: Visitor) => ReactNode }>;
48
+ renderRowActions?: (visitor: Visitor) => ReactNode;
49
+ className?: string;
50
+ }
51
+
52
+ const TABS = [
53
+ { id: "visitors", label: "Visitors" },
54
+ { id: "widget", label: "Widget" },
55
+ { id: "install", label: "Install" },
56
+ ];
57
+
58
+ function useDebounced<T>(value: T, ms = 300): T {
59
+ const [v, setV] = useState(value);
60
+ useEffect(() => {
61
+ const t = setTimeout(() => setV(value), ms);
62
+ return () => clearTimeout(t);
63
+ }, [value, ms]);
64
+ return v;
65
+ }
66
+
67
+ /** A stable, deep-linkable key: the visitor id, else the email. A row with neither is selectable but not linkable (never the row index, which points at different people per page). */
68
+ const idOf = (v: Visitor): string | null => {
69
+ if (v.id != null && String(v.id) !== "") return String(v.id);
70
+ const email = visitorEmail(v);
71
+ return email ? `email:${email}` : null;
72
+ };
73
+
74
+ function flatFields(v: Visitor): Array<{ label: string; value: ReactNode }> {
75
+ return Object.entries(v)
76
+ .filter(([, x]) => ["string", "number", "boolean"].includes(typeof x) && String(x) !== "")
77
+ .map(([k, x]) => ({ label: k.replace(/_/g, " "), value: String(x) }));
78
+ }
79
+
80
+ function VisitorDetail({ visitor, onClose, onExpand, onCollapse, props }: { visitor: Visitor; onClose: () => void; onExpand?: () => void; onCollapse?: () => void; props: WebchatScreenProps }) {
81
+ const name = visitorName(visitor);
82
+ const identified = Boolean(visitorEmail(visitor));
83
+ return (
84
+ <DetailPanel
85
+ title={name}
86
+ subtitle={[visitorEmail(visitor), visitorCompany(visitor)].filter(Boolean).join(" at ") || "Anonymous visitor"}
87
+ avatar={<EntityAvatar name={name} kind="person" />}
88
+ badges={<StatusPill tone={identified ? "positive" : "neutral"}>{identified ? "Identified" : "Anonymous"}</StatusPill>}
89
+ actions={props.renderDetailActions?.(visitor)}
90
+ onClose={onClose}
91
+ onExpand={onExpand}
92
+ onCollapse={onCollapse}
93
+ tabs={[
94
+ { id: "overview", label: "Overview", content: <KeyValueList columns={1} items={[{ label: "Last seen", value: visitorLastSeen(visitor) || "Not known" }, ...flatFields(visitor)]} /> },
95
+ ...(props.extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(visitor) })),
96
+ ]}
97
+ />
98
+ );
99
+ }
100
+
101
+ function VisitorsTab({ props }: { props: WebchatScreenProps }) {
102
+ const pageSize = props.pageSize ?? 25;
103
+ const [page, setPage] = useState(1);
104
+ const [identifiedOnly, setIdentifiedOnly] = useState(false);
105
+ const [text, setText] = useState("");
106
+ const search = useDebounced(text.trim());
107
+ const [openId, setOpenId] = useUrlState("id");
108
+ const [view, setView] = useUrlState("view");
109
+ const [localSel, setLocalSel] = useState<Visitor | null>(null);
110
+ const visitors = useG8Query(
111
+ (signal) =>
112
+ g8Get<unknown>("/webchat/visitors", { page, page_size: pageSize, identified_only: identifiedOnly || undefined, search: search || undefined }, { signal }).then((r) => ({
113
+ items: pickItems<Visitor>(r.data),
114
+ total: (r.data as { total?: number } | undefined)?.total,
115
+ })),
116
+ [page, pageSize, identifiedOnly, search],
117
+ );
118
+ const items = visitors.data?.items ?? [];
119
+ const selected = (openId ? items.find((v) => idOf(v) === openId) : undefined) ?? (localSel && items.includes(localSel) ? localSel : undefined);
120
+ const reset = () => { setPage(1); setOpenId(null); setView(null); setLocalSel(null); };
121
+ const pick = (v: Visitor) => { const id = idOf(v); setLocalSel(id ? null : v); setOpenId(id); };
122
+ const expanded = view === "full" && Boolean(selected);
123
+
124
+ const list = (
125
+ <div className="space-y-3">
126
+ <Toolbar
127
+ search={{ value: text, onChange: (v) => { setText(v); reset(); }, placeholder: "Search by name or email" }}
128
+ filters={<label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={identifiedOnly} onChange={(e) => { setIdentifiedOnly(e.target.checked); reset(); }} />Identified only</label>}
129
+ />
130
+ {visitors.error ? (
131
+ <ApiError error={visitors.error} title="Could not load visitors" onRetry={() => void visitors.refetch()} />
132
+ ) : visitors.loading && !visitors.data ? (
133
+ <div aria-busy="true" className="grid gap-2">{Array.from({ length: 4 }, (_, i) => <Skeleton key={i} className="h-9 w-full" />)}</div>
134
+ ) : items.length === 0 ? (
135
+ search || identifiedOnly ? (
136
+ <EmptyState title="No visitors match" description="Try a different search, or include anonymous visitors." action={<Button size="sm" variant="outline" onClick={() => { setText(""); setIdentifiedOnly(false); reset(); }}>Clear filters</Button>} />
137
+ ) : (
138
+ <EmptyState icon={<MessageSquare className="size-6" />} title="No chat visitors yet" description="People appear here after they use the widget on your site. Install the snippet first." action={<span className="text-sm text-muted-foreground">See the Install tab.</span>} />
139
+ )
140
+ ) : (
141
+ <Table>
142
+ <TableHeader><TableRow><TableHead>Visitor</TableHead><TableHead>Email</TableHead><TableHead>Company</TableHead><TableHead>Last seen</TableHead>{props.renderRowActions ? <TableHead /> : null}</TableRow></TableHeader>
143
+ <TableBody>
144
+ {items.map((v, i) => (
145
+ <TableRow key={idOf(v) ?? `row-${i}`} data-state={selected === v ? "selected" : undefined} className="cursor-pointer" {...rowActivationProps(() => pick(v))}>
146
+ <TableCell><span className="flex items-center gap-2"><EntityAvatar name={visitorName(v)} kind="person" size="sm" />{idOf(v) ? <RecordLink param="id" id={idOf(v) as string} onOpen={() => pick(v)}>{visitorName(v)}</RecordLink> : visitorName(v)}</span></TableCell>
147
+ <TableCell>{visitorEmail(v)}</TableCell>
148
+ <TableCell>{visitorCompany(v)}</TableCell>
149
+ <TableCell>{visitorLastSeen(v)}</TableCell>
150
+ {props.renderRowActions ? <TableCell onClick={(e) => e.stopPropagation()}>{props.renderRowActions(v)}</TableCell> : null}
151
+ </TableRow>
152
+ ))}
153
+ </TableBody>
154
+ </Table>
155
+ )}
156
+ <div className="flex items-center justify-between text-sm text-muted-foreground">
157
+ <span>{visitors.data?.total != null ? `${visitors.data.total.toLocaleString()} visitors` : `Page ${page}`}</span>
158
+ <div className="flex gap-2">
159
+ <Button size="sm" variant="outline" disabled={page <= 1} onClick={() => { setPage(page - 1); setOpenId(null); setLocalSel(null); }}>Previous</Button>
160
+ <Button size="sm" variant="outline" disabled={items.length < pageSize} onClick={() => { setPage(page + 1); setOpenId(null); setLocalSel(null); }}>Next</Button>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ );
165
+ const close = () => { setOpenId(null); setView(null); setLocalSel(null); };
166
+ return (
167
+ <SplitView
168
+ list={list}
169
+ detailOpen={Boolean(selected)}
170
+ onCloseDetail={close}
171
+ expanded={expanded}
172
+ onExpandedChange={(next) => setView(next ? "full" : null)}
173
+ detail={selected ? <VisitorDetail visitor={selected} onClose={close} onExpand={expanded ? undefined : () => setView("full")} onCollapse={expanded ? () => setView(null) : undefined} props={props} /> : null}
174
+ />
175
+ );
176
+ }
177
+
178
+ function InstallTab() {
179
+ const brand = useBrand();
180
+ const snippet = useG8Query((signal) => g8Get<{ script_tag?: string }>("/snippet", {}, { signal }).then((r) => r.data), []);
181
+ const [copied, setCopied] = useState(false);
182
+ const [copyFailed, setCopyFailed] = useState(false);
183
+ const tag = snippet.data?.script_tag;
184
+ const copy = () => {
185
+ setCopyFailed(false);
186
+ const write = navigator.clipboard?.writeText(tag ?? "");
187
+ if (!write) { setCopyFailed(true); return; }
188
+ write.then(() => setCopied(true)).catch(() => { setCopied(false); setCopyFailed(true); });
189
+ };
190
+ return (
191
+ <div className="grid gap-4">
192
+ <SectionCard title="Embed code" description={`The widget loads through the ${brand.linkLabel} tracking snippet. Add it to every page that should show the chat.`}>
193
+ {snippet.error ? (
194
+ <ApiError error={snippet.error} title="Could not load the snippet" onRetry={() => void snippet.refetch()} />
195
+ ) : tag ? (
196
+ <div className="space-y-2">
197
+ <pre aria-label="Snippet" className="overflow-auto rounded-md bg-muted p-3 text-xs">{tag}</pre>
198
+ <Button size="sm" variant="outline" onClick={copy}><Copy className="mr-1 size-4" />{copied ? "Copied" : "Copy snippet"}</Button>
199
+ {copyFailed ? <p role="alert" className="text-sm text-error-foreground">Copy failed. Select the code above and copy it.</p> : null}
200
+ </div>
201
+ ) : (
202
+ <Skeleton className="h-12 w-full" />
203
+ )}
204
+ </SectionCard>
205
+ <SectionCard title="Before it works">
206
+ <ul className="list-disc space-y-1 pl-5 text-sm">
207
+ <li>The snippet is on the pages that should show the chat.</li>
208
+ <li>An answering agent is mapped to the bot. A bot with none answers nothing. Set it up in {brand.linkLabel}.</li>
209
+ <li>Style the widget in the Widget tab. Saves are live for the next visitor.</li>
210
+ </ul>
211
+ <a className="mt-2 inline-block text-sm underline" href={brand.appUrl} target="_blank" rel="noreferrer">Open {brand.linkLabel}</a>
212
+ </SectionCard>
213
+ </div>
214
+ );
215
+ }
216
+
217
+ export function WebchatScreen(props: WebchatScreenProps) {
218
+ const { botId, toolbarActions, className } = props;
219
+ const [tab, setTab] = useUrlState("tab", "visitors");
220
+ const funnel = useG8Query((signal) => g8Get<Record<string, unknown>>("/webchat/visitors/funnel", { bot_id: botId }, { signal }).then((r) => r.data), [botId]);
221
+ const stats = useMemo(() => {
222
+ const counts = Object.entries((funnel.data?.counts ?? funnel.data?.events ?? {}) as Record<string, unknown>).filter(([, v]) => typeof v === "number") as Array<[string, number]>;
223
+ return counts.slice(0, 4).map(([k, v]) => ({ label: k.replace(/_/g, " "), value: v.toLocaleString() }));
224
+ }, [funnel.data]);
225
+ const active = TABS.some((t) => t.id === tab) ? tab : "visitors";
226
+
227
+ return (
228
+ <ScreenLayout
229
+ className={className}
230
+ title="Web chat"
231
+ description="Who talks to your site's AI chat, how it looks, and how to put it on your pages."
232
+ icon={<MessageSquare className="size-5" />}
233
+ actions={toolbarActions}
234
+ tabs={TABS}
235
+ activeTab={active}
236
+ onTabChange={(id) => setTab(id)}
237
+ stats={stats.length > 0 ? stats : undefined}
238
+ >
239
+ {funnel.error ? <p role="alert" className="mb-3 text-sm text-muted-foreground">Could not load the engagement numbers. <button type="button" className="underline" onClick={() => void funnel.refetch()}>Retry</button></p> : null}
240
+ {active === "visitors" ? <VisitorsTab props={props} /> : null}
241
+ {active === "widget" ? <ChatWidgetConfig initialBotId={botId} /> : null}
242
+ {active === "install" ? <InstallTab /> : null}
243
+ </ScreenLayout>
244
+ );
245
+ }
@@ -7,6 +7,17 @@ price, run it, and watch it finish. Finished rows go to a list. Builds on `enric
7
7
 
8
8
  Install: `npx @graph8/sdk add workbench` (installs `foundation`, `crm`, `enrichment` and `lists` first).
9
9
 
10
+ ## Screens
11
+
12
+ `WorkbenchScreen` is the finished Workbench page; mount it with no props. The list view shows every workbench with row and
13
+ column counts, search, **New workbench** (People or Companies) and a confirmed delete. `?id=<table>` opens a table: a
14
+ header with row, column and recipe-column counts and the full `WorkbenchGrid` (edit cells, paste or add rows, add AI,
15
+ waterfall and formula columns, quote then run, progress, unlock locked rows, send rows to a list, delete rows).
16
+ **Import people** (free preview and match count, then a confirmed import; rows land locked) and **Scrape the web** (credit estimate and balance, confirmed run, polled progress) are in the table header (`SearchImportPanel`, `ScraperPanel`). Open any row (`?row=`) for `WorkbenchRowDetail`: every cell, per-cell edit, unlock, send to a list, delete. `?view=full` opens a row full-page.
17
+ Slots: `toolbarActions`, `renderRowActions(table)`, `renderDetailActions(table)`.
18
+
19
+ Prerequisites: `enrichment` and `lists` installed, proxy allowlist `workbench`, `enrichment`, `lists`; provider keys and credits as below.
20
+
10
21
  ## What you get
11
22
 
12
23
  | Component | Use it for | Props |
@@ -8,7 +8,8 @@
8
8
  "add rows by typing or pasting a spreadsheet; locked rows from a search import show masked cells",
9
9
  "column recipes: AI prompt with {{column}} placeholders, enrichment waterfall presets, formulas, plain columns, target-field mapping",
10
10
  "runs are quoted for free, confirmed with the exact credits, started with the quote as the cap and followed to a final receipt",
11
- "unlock locked rows (1 credit each), send rows to a list, delete rows"
11
+ "unlock locked rows (1 credit each), send rows to a list, delete rows",
12
+ "finished WorkbenchScreen page"
12
13
  ],
13
14
  "dependsOn": [
14
15
  "enrichment",
@@ -25,6 +26,7 @@
25
26
  "export_workbench_table_workbench_tables__table_id__export_post",
26
27
  "get_workbench_analytics_workbench_tables__table_id__analytics_get",
27
28
  "get_workbench_history_workbench_tables__table_id__history_get",
29
+ "get_workbench_row_workbench_tables__table_id__rows__row_id__get",
28
30
  "get_workbench_scraper_run_workbench_tables__table_id__scraper_runs__run_id__get",
29
31
  "get_workbench_table_workbench_tables__table_id__get",
30
32
  "import_workbench_from_search_workbench_tables__table_id__import_search_post",
@@ -143,6 +145,50 @@
143
145
  "billable",
144
146
  "destructive"
145
147
  ]
148
+ },
149
+ {
150
+ "name": "WorkbenchScreen",
151
+ "file": "components/workbench-screen.tsx",
152
+ "purpose": "Finished Workbench page: workbench list with create/delete and the full table (WorkbenchGrid) with a stats header.",
153
+ "operations": [
154
+ "create_workbench_table_workbench_tables_post",
155
+ "delete_workbench_table_workbench_tables__table_id__delete",
156
+ "list_workbench_tables_workbench_tables_get"
157
+ ],
158
+ "tiers": [
159
+ "read",
160
+ "write",
161
+ "destructive"
162
+ ]
163
+ },
164
+ {
165
+ "name": "SearchImportPanel",
166
+ "file": "components/workbench-sources.tsx",
167
+ "purpose": "Import people from a search into a table: free preview and match count, then a confirmed billable import (rows land locked). The same file exports ScraperPanel: web scraper with credit estimate, confirmed billable run and polled progress.",
168
+ "operations": [
169
+ "estimate_workbench_scraper_credits_workbench_scraper_credit_estimate_get",
170
+ "get_workbench_scraper_run_workbench_tables__table_id__scraper_runs__run_id__get",
171
+ "import_workbench_from_search_workbench_tables__table_id__import_search_post",
172
+ "preview_workbench_search_import_workbench_tables__table_id__import_search_preview_post",
173
+ "run_workbench_scraper_workbench_tables__table_id__scraper_runs_post"
174
+ ],
175
+ "tiers": [
176
+ "read",
177
+ "billable"
178
+ ]
179
+ },
180
+ {
181
+ "name": "WorkbenchRowDetail",
182
+ "file": "components/workbench-row-detail.tsx",
183
+ "purpose": "One workbench row in full: every cell, status, per-cell edit, unlock, send to a list and delete.",
184
+ "operations": [
185
+ "get_workbench_row_workbench_tables__table_id__rows__row_id__get",
186
+ "update_workbench_row_workbench_tables__table_id__rows__row_id__patch"
187
+ ],
188
+ "tiers": [
189
+ "read",
190
+ "write"
191
+ ]
146
192
  }
147
193
  ],
148
194
  "hooks": [
@@ -254,6 +300,7 @@
254
300
  }
255
301
  ],
256
302
  "peerDependencies": {
303
+ "lucide-react": ">=0.400.0",
257
304
  "react": ">=18"
258
305
  }
259
306
  }
@@ -30,6 +30,7 @@ import { useProviders } from "@/g8/enrichment/hooks/use-providers";
30
30
  import { WATERFALL_PRESETS, buildWaterfallConfig, requiredTargets } from "../lib/waterfall-presets";
31
31
  import { pageItems, toMachineName } from "../lib/workbench-types";
32
32
  import type { WorkbenchColumn } from "../lib/workbench-types";
33
+ import { useBrand } from "@/g8/hooks/use-brand";
33
34
 
34
35
  export interface ColumnRecipeEditorProps {
35
36
  tableId: string;
@@ -52,6 +53,7 @@ const TABS: Array<{ id: Tab; label: string }> = [
52
53
  const newId = () => globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;
53
54
 
54
55
  export function ColumnRecipeEditor({ tableId, columns, entityType = "contact", onSaved, className }: ColumnRecipeEditorProps) {
56
+ const brand = useBrand();
55
57
  const [tab, setTab] = useState<Tab>("ai");
56
58
  const [name, setName] = useState("");
57
59
  const [prompt, setPrompt] = useState("");
@@ -132,12 +134,12 @@ export function ColumnRecipeEditor({ tableId, columns, entityType = "contact", o
132
134
  <label key={s.provider} className="flex items-center gap-2 text-sm">
133
135
  <input type="checkbox" aria-label={`Use ${s.provider}`} checked={!skipped.includes(s.provider)} onChange={() => setSkipped(skipped.includes(s.provider) ? skipped.filter((x) => x !== s.provider) : [...skipped, s.provider])} />
134
136
  <span className="w-6 text-muted-foreground">{i + 1}.</span><span className="font-medium">{s.provider}</span>
135
- <span className="text-xs text-muted-foreground">{info?.byok_configured ? "your key" : info?.system_funded ? "graph8 key, uses credits when run" : "needs your key"}</span>
137
+ <span className="text-xs text-muted-foreground">{info?.byok_configured ? "your key" : info?.system_funded ? `${brand.linkLabel} key, uses credits when run` : "needs your key"}</span>
136
138
  </label>
137
139
  );
138
140
  })}
139
141
  </fieldset>
140
- {unmapped.length ? <p role="alert" className="text-sm text-amber-700">Not mapped yet: {unmapped.join(", ")}. Use the Map columns tab, or the steps that need them find nothing.</p> : null}
142
+ {unmapped.length ? <p role="alert" className="text-sm text-warning-foreground">Not mapped yet: {unmapped.join(", ")}. Use the Map columns tab, or the steps that need them find nothing.</p> : null}
141
143
  {addWaterfall.error ? <ApiError error={addWaterfall.error} title="Could not save the enrichment column" /> : null}
142
144
  <div><Button size="sm" loading={addWaterfall.loading} disabled={steps.length === 0} onClick={() => void addWaterfall.mutate(undefined)}>Save enrichment column</Button></div>
143
145
  </div>
@@ -163,7 +165,7 @@ export function ColumnRecipeEditor({ tableId, columns, entityType = "contact", o
163
165
 
164
166
  {tab === "map" ? (
165
167
  <div className="flex flex-col gap-2">
166
- <p className="text-sm text-muted-foreground">Tell graph8 what each of your columns holds, so enrichment steps know where the first name, last name and website are.</p>
168
+ <p className="text-sm text-muted-foreground">Tell {brand.linkLabel} what each of your columns holds, so enrichment steps know where the first name, last name and website are.</p>
167
169
  {targets.error ? <ApiError error={targets.error} title="Could not load the fields" onRetry={() => void targets.refetch()} /> : null}
168
170
  {map.error ? <ApiError error={map.error} title="Could not save the mapping" /> : null}
169
171
  {columns.filter((c) => (c.column_type ?? "data") === "data" && !c.enrichment_type).map((c) => (
@@ -94,7 +94,7 @@ export function RunProgress({ tableId, target, rowIds, label = "this column", on
94
94
  if (job) {
95
95
  return (
96
96
  <div className={`flex flex-col gap-2 rounded-md border p-3 ${className ?? ""}`} role="status">
97
- <h3 className="text-sm font-semibold">{poll.done ? `Run ${receipt?.status ?? "finished"}` : "Running..."}</h3>
97
+ <h3 className="text-sm font-medium">{poll.done ? `Run ${receipt?.status ?? "finished"}` : "Running..."}</h3>
98
98
  <p className="text-sm text-muted-foreground">
99
99
  {receipt ? `${receipt.credits_charged ?? 0} credits charged so far.` : "Queued. The first update can take a few seconds."}
100
100
  {Object.entries(distribution).length ? ` Rows: ${Object.entries(distribution).map(([k, v]) => `${v} ${k}`).join(", ")}.` : ""}
@@ -39,6 +39,10 @@ export interface WorkbenchGridProps {
39
39
  tableId: string;
40
40
  /** Rows per window. Default 100. */
41
41
  pageSize?: number;
42
+ /** Adds an "Open" button on every row (a row detail screen). Omit it and the grid is unchanged. */
43
+ onOpenRow?: (row: WorkbenchRow) => void;
44
+ /** Highlights the open row. */
45
+ activeRowId?: number | null;
42
46
  className?: string;
43
47
  }
44
48
 
@@ -50,7 +54,7 @@ function runTarget(column: WorkbenchColumn): RunTarget | null {
50
54
  return null;
51
55
  }
52
56
 
53
- export function WorkbenchGrid({ tableId, pageSize = 100, className }: WorkbenchGridProps) {
57
+ export function WorkbenchGrid({ tableId, pageSize = 100, onOpenRow, activeRowId, className }: WorkbenchGridProps) {
54
58
  const [end, setEnd] = useState(pageSize);
55
59
  const [panel, setPanel] = useState<Panel>(null);
56
60
  const [selected, setSelected] = useState<number[]>([]);
@@ -84,7 +88,7 @@ export function WorkbenchGrid({ tableId, pageSize = 100, className }: WorkbenchG
84
88
  <div className={`flex flex-col gap-3 ${className ?? ""}`}>
85
89
  <div className="flex flex-wrap items-center justify-between gap-2">
86
90
  <div>
87
- <h2 className="text-base font-semibold">{table?.name ?? "Workbench"}</h2>
91
+ <h2 className="text-base font-medium">{table?.name ?? "Workbench"}</h2>
88
92
  <p className="text-xs text-muted-foreground">{total.toLocaleString()} rows, {columns.length} columns</p>
89
93
  </div>
90
94
  <div className="flex gap-2" role="toolbar" aria-label="Workbench tools">
@@ -114,6 +118,7 @@ export function WorkbenchGrid({ tableId, pageSize = 100, className }: WorkbenchG
114
118
  <TableHeader>
115
119
  <TableRow>
116
120
  <TableHead className="w-8"><input type="checkbox" aria-label="Select all rows" checked={allOn} onChange={() => setSelected(allOn ? [] : items.map((r) => r.id))} /></TableHead>
121
+ {onOpenRow ? <TableHead className="w-14"><span className="sr-only">Open</span></TableHead> : null}
117
122
  {columns.map((c) => {
118
123
  const target = runTarget(c);
119
124
  return (
@@ -130,8 +135,9 @@ export function WorkbenchGrid({ tableId, pageSize = 100, className }: WorkbenchG
130
135
  </TableHeader>
131
136
  <TableBody>
132
137
  {items.map((row) => (
133
- <TableRow key={row.id} data-state={selected.includes(row.id) ? "selected" : undefined}>
138
+ <TableRow key={row.id} data-state={selected.includes(row.id) || activeRowId === row.id ? "selected" : undefined}>
134
139
  <TableCell><input type="checkbox" aria-label={`Select row ${row.id}`} checked={selected.includes(row.id)} onChange={() => toggle(row.id)} /></TableCell>
140
+ {onOpenRow ? <TableCell><Button size="sm" variant="ghost" aria-label={`Open row ${row.id}`} onClick={() => onOpenRow(row)}>Open</Button></TableCell> : null}
135
141
  {columns.map((c) => {
136
142
  const value = cellValue(row, c);
137
143
  const isEdit = editing?.rowId === row.id && editing.column === c.name;
@@ -140,7 +146,7 @@ export function WorkbenchGrid({ tableId, pageSize = 100, className }: WorkbenchG
140
146
  <TableCell key={c.id} onDoubleClick={() => editable && setEditing({ rowId: row.id, column: c.name, value: formatCell(value) })}>
141
147
  {isEdit && editing ? (
142
148
  <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); }} />
149
+ onKeyDown={(e) => { if (e.key === "Enter" && !save.loading) { if (editing.value === formatCell(value)) setEditing(null); else void save.mutate(editing); } if (e.key === "Escape") setEditing(null); }} />
144
150
  ) : (
145
151
  <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
152
  )}