@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
@@ -21,6 +21,7 @@ import { useG8Query } from "@/g8/hooks/use-g8-query";
21
21
  import { useJobPoll } from "@/g8/hooks/use-job-poll";
22
22
  import { ApiError } from "@/g8/components/api-error";
23
23
  import { EmptyState } from "@/g8/components/empty-state";
24
+ import { useBrand } from "@/g8/hooks/use-brand";
24
25
 
25
26
  export interface SnippetInstallProps {
26
27
  /** Pre-select a stream id (from listOrgStreams). Default: the org's main stream. */
@@ -120,6 +121,7 @@ function InstallCheck({ writeKey, onInstalled }: { writeKey: string; onInstalled
120
121
  }
121
122
 
122
123
  export function SnippetInstall({ streamId, onInstalled, className }: SnippetInstallProps) {
124
+ const brand = useBrand();
123
125
  const [selected, setSelected] = useState<string | undefined>(streamId);
124
126
  const streams = useG8Query((signal) => g8Get<Stream[]>("/streams", { limit: 50 }, { signal }), []);
125
127
  const snippet = useG8Query((signal) => g8Get<SnippetPayload>("/snippet", { stream_id: selected }, { signal }), [selected]);
@@ -132,7 +134,7 @@ export function SnippetInstall({ streamId, onInstalled, className }: SnippetInst
132
134
  return (
133
135
  <EmptyState
134
136
  title="No tracking stream yet"
135
- description="Create a website stream in graph8 first, then come back for its snippet."
137
+ description={`Create a website stream in ${brand.linkLabel} first, then come back for its snippet.`}
136
138
  className={className}
137
139
  />
138
140
  );
@@ -21,6 +21,7 @@ import { useG8Query } from "@/g8/hooks/use-g8-query";
21
21
  import { ApiError } from "@/g8/components/api-error";
22
22
  import { companyLabel, pageLabel, pageOf, timeAgo } from "../lib/visitors-utils";
23
23
  import type { FlowEvent, VisitorGroup } from "../lib/visitors-utils";
24
+ import { plural } from "@/g8/lib/plural";
24
25
 
25
26
  export interface VisitorCompanyCardProps {
26
27
  visit: VisitorGroup;
@@ -69,8 +70,7 @@ export function VisitorCompanyCard({ visit, previewPages = 5, actions, className
69
70
  <div>
70
71
  <CardTitle className="text-base">{companyLabel(visit)}</CardTitle>
71
72
  <p className="text-xs text-gray-500">
72
- {visit.COMPANY_DOMAIN ? `${visit.COMPANY_DOMAIN} - ` : ""}last seen {timeAgo(visit.received_at)} - {pages.length} page view
73
- {pages.length === 1 ? "" : "s"}
73
+ {visit.COMPANY_DOMAIN ? `${visit.COMPANY_DOMAIN} - ` : ""}last seen {timeAgo(visit.received_at)} - {plural(pages.length, "page view")}
74
74
  </p>
75
75
  </div>
76
76
  {actions ? <div className="flex gap-2">{actions}</div> : null}
@@ -0,0 +1,105 @@
1
+ "use client";
2
+
3
+ /**
4
+ * VisitorDetail: the complete screen for one visitor, a visiting company or an identified person. Shows every field the feed
5
+ * carries (name, domain, ids, last seen, visit count), every page viewed with its referrer, and, for a company, the full journey of
6
+ * raw page views. Pass the grouped row from VisitorFeed, or just `kind` and `id` (a deep link): the row is then found in the latest
7
+ * 100 grouped visits.
8
+ *
9
+ * SDK operations, in order (all read tier, nothing billable or external):
10
+ * 1. listCompanyVisitorEvents GET /visitors/companies/events (show_mode=grouped to find the row; show_mode=raw with filter={"company_id": id} for the journey)
11
+ * 2. listContactVisitorEvents GET /visitors/contacts/events (show_mode=grouped to find the row; needs contacts:read too)
12
+ */
13
+ import { Skeleton } from "@graph8/sdk/ui";
14
+ import { ApiError } from "@/g8/components/api-error";
15
+ import { EmptyState } from "@/g8/components/empty-state";
16
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
17
+ import { g8Get } from "@/g8/lib/g8-client";
18
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
19
+ import { EntityAvatar } from "@/g8/components/screen/entity-avatar";
20
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
21
+ import { SectionCard } from "@/g8/components/screen/section-card";
22
+ import { companyLabel, pageLabel, pageOf, personLabel, timeAgo } from "../lib/visitors-utils";
23
+ import type { FlowEvent, VisitorGroup } from "../lib/visitors-utils";
24
+ import { VisitorCompanyCard } from "./visitor-company-card";
25
+
26
+ export interface VisitorDetailProps {
27
+ kind: "company" | "person";
28
+ /** COMPANY_ID for a company, CONTACT_ID for a person. */
29
+ id: number;
30
+ /** The row from the feed; without it the screen looks it up. */
31
+ visit?: VisitorGroup | null;
32
+ /** Buttons in the header (for example "Open contact" or "Add to list"). */
33
+ actions?: React.ReactNode;
34
+ /** Extra tabs from the screen's `extraDetailTabs` slot, after the built-in ones. */
35
+ extraTabs?: { id: string; label: string; content: React.ReactNode }[];
36
+ onClose?: () => void;
37
+ onExpand?: () => void;
38
+ onCollapse?: () => void;
39
+ }
40
+
41
+ function Pages({ pages }: { pages: FlowEvent[] }) {
42
+ if (pages.length === 0) return <p className="text-sm text-muted-foreground">No page views recorded.</p>;
43
+ return (
44
+ <ol className="space-y-1.5 text-sm">
45
+ {pages.map((p, i) => (
46
+ <li key={p.message_id ?? `${p.received_at}-${i}`} className="flex justify-between gap-3">
47
+ <span className="min-w-0 truncate">
48
+ {pageLabel(p)}
49
+ {p.page_path ? <span className="ml-2 text-xs text-muted-foreground">{p.page_path}</span> : null}
50
+ {p.page_referrer ? <span className="ml-2 text-xs text-muted-foreground">from {p.page_referrer}</span> : null}
51
+ </span>
52
+ <span className="shrink-0 text-xs text-muted-foreground">{timeAgo(p.received_at)}</span>
53
+ </li>
54
+ ))}
55
+ </ol>
56
+ );
57
+ }
58
+
59
+ export function VisitorDetail({ kind, id, visit: given, actions, extraTabs, onClose, onExpand, onCollapse }: VisitorDetailProps) {
60
+ const path = kind === "company" ? "/visitors/companies/events" : "/visitors/contacts/events";
61
+ const lookup = useG8Query(
62
+ async (signal) => pageOf(await g8Get(path, { show_mode: "grouped", page: 1, limit: 100 }, { signal })).events,
63
+ [kind],
64
+ { enabled: !given },
65
+ );
66
+ const visit = given ?? (lookup.data ?? []).find((v) => (kind === "company" ? v.COMPANY_ID : v.CONTACT_ID) === id);
67
+
68
+ if (!given && lookup.error) return <ApiError error={lookup.error} title="Could not load the visitor" onRetry={() => void lookup.refetch()} />;
69
+ if (!visit && lookup.loading) return <Skeleton className="h-40 w-full" />;
70
+ if (!visit) {
71
+ if (kind === "company") {
72
+ return <DetailPanel title={`Company ${id}`} subtitle="Not among the latest visits" onClose={onClose} onExpand={onExpand} onCollapse={onCollapse}
73
+ tabs={[{ id: "journey", label: "Journey", content: <VisitorCompanyCard visit={{ COMPANY_ID: id }} /> }]} />;
74
+ }
75
+ return <EmptyState title="Visitor not found" description="They are not among the latest 100 identified visitors." />;
76
+ }
77
+
78
+ const name = kind === "company" ? companyLabel(visit) : personLabel(visit);
79
+ const pages = visit.children ?? [];
80
+ const tabs = [
81
+ {
82
+ id: "overview",
83
+ label: "Overview",
84
+ content: (
85
+ <div className="space-y-4">
86
+ <KeyValueList columns={2} items={[
87
+ ...(kind === "person" ? [{ label: "Person", value: name }, { label: "Contact id", value: visit.CONTACT_ID ?? "-" }] : []),
88
+ { label: "Company", value: visit.COMPANY_NAME ?? "-" },
89
+ { label: "Domain", value: visit.COMPANY_DOMAIN ?? "-" },
90
+ { label: "Company id", value: visit.COMPANY_ID ?? "-" },
91
+ { label: "Last seen", value: timeAgo(visit.received_at) },
92
+ { label: "Page views", value: pages.length },
93
+ ]} />
94
+ <SectionCard title="Pages viewed"><Pages pages={pages} /></SectionCard>
95
+ </div>
96
+ ),
97
+ },
98
+ ...(kind === "company" ? [{ id: "journey", label: "Full journey", content: <VisitorCompanyCard visit={visit} /> }] : []),
99
+ ];
100
+ return (
101
+ <DetailPanel title={name} subtitle={kind === "person" ? visit.COMPANY_NAME ?? visit.COMPANY_DOMAIN ?? undefined : visit.COMPANY_DOMAIN ?? undefined}
102
+ avatar={<EntityAvatar name={name} kind={kind === "company" ? "company" : "person"} size="lg" />} actions={actions}
103
+ tabs={extraTabs?.length ? [...tabs, ...extraTabs] : tabs} onClose={onClose} onExpand={onExpand} onCollapse={onCollapse} />
104
+ );
105
+ }
@@ -21,6 +21,7 @@ import { EmptyState } from "@/g8/components/empty-state";
21
21
  import { VisitorCompanyCard } from "./visitor-company-card";
22
22
  import { pageOf, personLabel, pageLabel, timeAgo } from "../lib/visitors-utils";
23
23
  import type { VisitorGroup } from "../lib/visitors-utils";
24
+ import { useBrand } from "@/g8/hooks/use-brand";
24
25
 
25
26
  export interface VisitorFeedProps {
26
27
  pageSize?: number;
@@ -30,6 +31,8 @@ export interface VisitorFeedProps {
30
31
  /** Which tab to open on. Default "companies". */
31
32
  initialView?: "companies" | "people";
32
33
  onSelectPerson?: (visit: VisitorGroup) => void;
34
+ /** Called when a person opens a visiting company (show a VisitorDetail). Adds an Open button to each company card. */
35
+ onSelectCompany?: (visit: VisitorGroup) => void;
33
36
  className?: string;
34
37
  }
35
38
 
@@ -42,8 +45,10 @@ export function VisitorFeed({
42
45
  includeInternal = false,
43
46
  initialView = "companies",
44
47
  onSelectPerson,
48
+ onSelectCompany,
45
49
  className,
46
50
  }: VisitorFeedProps) {
51
+ const brand = useBrand();
47
52
  const [view, setView] = useState<View>(initialView);
48
53
  // The page belongs to one set of inputs: a different tab, page size or bot/internal filter starts at page 1.
49
54
  const scope = `${view}|${pageSize}|${includeBots}|${includeInternal}`;
@@ -113,12 +118,16 @@ export function VisitorFeed({
113
118
  ) : events.length === 0 ? (
114
119
  <EmptyState
115
120
  title={view === "companies" ? "No company visits yet" : "No identified people yet"}
116
- description="Visits appear once the graph8 snippet is on your site. Check the install in SnippetInstall."
121
+ description={`Visits appear once the ${brand.linkLabel} snippet is on your site. Check the install in SnippetInstall.`}
117
122
  />
118
123
  ) : view === "companies" ? (
119
124
  <div className="space-y-3">
120
125
  {events.map((visit, i) => (
121
- <VisitorCompanyCard key={`${visit.COMPANY_ID}-${visit.received_at}-${i}`} visit={visit} />
126
+ <VisitorCompanyCard
127
+ key={`${visit.COMPANY_ID}-${visit.received_at}-${i}`}
128
+ visit={visit}
129
+ actions={onSelectCompany ? <Button size="sm" variant="outline" onClick={() => onSelectCompany(visit)}>Open</Button> : undefined}
130
+ />
122
131
  ))}
123
132
  </div>
124
133
  ) : (
@@ -0,0 +1,166 @@
1
+ "use client";
2
+
3
+ /**
4
+ * VisitorsScreen: the finished "website visitors" product page. Three tabs:
5
+ * Visitors companies (default) and identified people visiting your site (VisitorFeed, polled), a stat strip for the last
6
+ * 7 days, and a complete detail screen for any company or person (VisitorDetail: fields, pages viewed, full journey;
7
+ * deep link `?id=c-<company id>` or `?id=p-<contact id>`, full page with `?view=full`).
8
+ * Traffic visitors, sessions and page views per day, top pages, "N on the site now" (TrafficChart).
9
+ * Install the tracking snippet and an install check that polls until events arrive (SnippetInstall).
10
+ *
11
+ * User flows: get the snippet, prove it works, watch who visits, switch companies/people, open a person, expand a
12
+ * company's journey, chart traffic. A site with no events yet opens on a guided "install the snippet" empty state.
13
+ *
14
+ * SDK operations: none of its own. Composes VisitorFeed, SnippetInstall, TrafficChart (all read tier, nothing is
15
+ * billable or external) and one read of getAudienceTrend (7 days) for the stat strip.
16
+ * Prerequisite: the snippet pasted in the site (a step only a person can do); the People list also needs contacts:read.
17
+ */
18
+ import { useState } from "react";
19
+ import type { ReactNode } from "react";
20
+ import { Button } from "@graph8/sdk/ui";
21
+ import { Globe } from "lucide-react";
22
+ import { g8Get } from "@/g8/lib/g8-client";
23
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
24
+ import { useUrlState } from "@/g8/hooks/use-url-state";
25
+ import { ScreenLayout } from "@/g8/components/screen/screen-layout";
26
+ import type { StatItem } from "@/g8/components/screen/stat-strip";
27
+ import { Toolbar } from "@/g8/components/screen/toolbar";
28
+ import { SplitView } from "@/g8/components/screen/split-view";
29
+ import { seriesOf } from "../lib/visitors-utils";
30
+ import type { VisitorGroup } from "../lib/visitors-utils";
31
+ import { SnippetInstall } from "./snippet-install";
32
+ import { TrafficChart } from "./traffic-chart";
33
+ import { VisitorDetail } from "./visitor-detail";
34
+ import { VisitorFeed } from "./visitor-feed";
35
+
36
+ export interface VisitorsScreenProps {
37
+ /** Buttons in the visitor detail header (for example "Open contact" or "Add to list"). */
38
+ renderDetailActions?: (visit: VisitorGroup, kind: "company" | "person") => ReactNode;
39
+ /** Tabs added after the built-in ones on the open visitor (once its row is known). */
40
+ extraDetailTabs?: { id: string; label: string; render: (visit: VisitorGroup, kind: "company" | "person") => ReactNode }[];
41
+ toolbarActions?: ReactNode;
42
+ className?: string;
43
+ }
44
+
45
+ interface TrendPoint {
46
+ date: string;
47
+ users?: number;
48
+ sessions?: number;
49
+ page_views?: number;
50
+ }
51
+
52
+ type Tab = "visitors" | "traffic" | "install";
53
+ const TABS = ["visitors", "traffic", "install"] as const;
54
+
55
+ function useWeekStats(): { items: StatItem[] | undefined; loading: boolean } {
56
+ const q = useG8Query((signal) => g8Get("/analytics/audience/trend", { days: 7 }, { signal }), []);
57
+ const points = seriesOf<TrendPoint>(q.data);
58
+ if (q.error || points.length === 0) return { items: undefined, loading: q.loading };
59
+ // The trend payload is open-shaped in the contract: chart only the metrics the rows really carry.
60
+ const metrics = [
61
+ { key: "users", label: "Visitors" },
62
+ { key: "sessions", label: "Sessions" },
63
+ { key: "page_views", label: "Page views" },
64
+ ] as const;
65
+ const items = metrics
66
+ .filter((m) => points.some((p) => typeof p[m.key] === "number"))
67
+ .map((m): StatItem => ({
68
+ label: m.label,
69
+ value: points.reduce((s, p) => s + (p[m.key] ?? 0), 0).toLocaleString(),
70
+ hint: "last 7 days",
71
+ sparkline: points.map((p) => p[m.key] ?? 0),
72
+ }));
73
+ return { loading: q.loading, items: items.length ? items : undefined };
74
+ }
75
+
76
+ export function VisitorsScreen(props: VisitorsScreenProps) {
77
+ const [tabValue, setTab] = useUrlState("tab", "visitors");
78
+ const tab: Tab = (TABS as readonly string[]).includes(tabValue) ? (tabValue as Tab) : "visitors";
79
+ const [id, setId] = useUrlState("id", "");
80
+ const [view, setView] = useUrlState("view", "");
81
+ const [open, setOpen] = useState<VisitorGroup | null>(null);
82
+ const expanded = view === "full" && Boolean(id);
83
+ const close = () => {
84
+ setOpen(null);
85
+ setId("");
86
+ setView("");
87
+ };
88
+ const kind = id.startsWith("p-") ? "person" : "company";
89
+ // Only `c-<id>` and `p-<id>` open a record: `?id=123` must not be read as company 3.
90
+ const numeric = /^[cp]-\d+$/.test(id) ? Number(id.slice(2)) : NaN;
91
+ const stats = useWeekStats();
92
+ const [bots, setBots] = useState(false);
93
+ const [internal, setInternal] = useState(false);
94
+ const [days, setDays] = useState(30);
95
+
96
+ return (
97
+ <ScreenLayout
98
+ title="Website visitors"
99
+ description="Who is on your site, which companies they work for, and what they looked at."
100
+ icon={<Globe className="size-5" />}
101
+ actions={props.toolbarActions}
102
+ tabs={[
103
+ { id: "visitors", label: "Visitors" },
104
+ { id: "traffic", label: "Traffic" },
105
+ { id: "install", label: "Install" },
106
+ ]}
107
+ activeTab={tab}
108
+ onTabChange={(next) => {
109
+ setTab(next);
110
+ close();
111
+ }}
112
+ stats={tab === "visitors" ? stats.items : undefined}
113
+ className={props.className}
114
+ >
115
+ {tab === "visitors" ? (
116
+ <div className="space-y-3">
117
+ <Toolbar>
118
+ <Button size="sm" variant={bots ? "default" : "outline"} aria-pressed={bots} onClick={() => setBots((v) => !v)}>
119
+ Include bots
120
+ </Button>
121
+ <Button size="sm" variant={internal ? "default" : "outline"} aria-pressed={internal} onClick={() => setInternal((v) => !v)}>
122
+ Include internal traffic
123
+ </Button>
124
+ </Toolbar>
125
+ <SplitView
126
+ detailOpen={Boolean(id) && Number.isFinite(numeric)}
127
+ onCloseDetail={close}
128
+ expanded={expanded}
129
+ onExpandedChange={(next) => setView(next ? "full" : "")}
130
+ detailWidth="lg"
131
+ list={
132
+ <VisitorFeed
133
+ key={`${bots}-${internal}`}
134
+ includeBots={bots}
135
+ includeInternal={internal}
136
+ onSelectPerson={(v) => { if (v.CONTACT_ID == null) return; setOpen(v); setId(`p-${v.CONTACT_ID}`); }}
137
+ onSelectCompany={(v) => { if (v.COMPANY_ID == null) return; setOpen(v); setId(`c-${v.COMPANY_ID}`); }}
138
+ />
139
+ }
140
+ detail={id && Number.isFinite(numeric) ? (
141
+ <VisitorDetail key={id} kind={kind} id={numeric} visit={open && (kind === "company" ? open.COMPANY_ID : open.CONTACT_ID) === numeric ? open : null}
142
+ actions={open ? props.renderDetailActions?.(open, kind) : undefined}
143
+ extraTabs={open ? (props.extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(open, kind) })) : undefined}
144
+ onClose={close} onExpand={expanded ? undefined : () => setView("full")} onCollapse={expanded ? () => setView("") : undefined} />
145
+ ) : null}
146
+ />
147
+ </div>
148
+ ) : tab === "traffic" ? (
149
+ <div className="space-y-3">
150
+ <Toolbar>
151
+ <div className="inline-flex gap-1" role="group" aria-label="Traffic window">
152
+ {[7, 30, 90].map((d) => (
153
+ <Button key={d} size="sm" variant={days === d ? "default" : "outline"} aria-pressed={days === d} onClick={() => setDays(d)}>
154
+ {d} days
155
+ </Button>
156
+ ))}
157
+ </div>
158
+ </Toolbar>
159
+ <TrafficChart days={days} />
160
+ </div>
161
+ ) : (
162
+ <SnippetInstall />
163
+ )}
164
+ </ScreenLayout>
165
+ );
166
+ }
@@ -4,6 +4,14 @@ Configure the AI chat widget on your website and see who used it. `ChatWidgetCon
4
4
  look, starter questions and the embed code; `ConversationList` shows the widget's visitors and the engagement funnel.
5
5
  Message threads and replies live in the `inbox` block.
6
6
 
7
+ ## Screens
8
+
9
+ `WebchatScreen` (`components/webchat-screen.tsx`) needs no props. **Visitors**: funnel stats, search, identified only,
10
+ paging, visitor detail (`?id=`). **Widget**: pick or create a bot, style it, starter questions (delete is confirmed).
11
+ **Install**: the tracking snippet with copy, and what graph8 must have set up (an answering agent mapped to the bot).
12
+ Slots: `toolbarActions`, `renderRowActions`, `renderDetailActions`, `extraDetailTabs` (use them to link a visitor to the inbox).
13
+ Scopes: `inbox:read`, `inbox:write`, `inbox:delete`, `integrations:read`.
14
+
7
15
  ## Install
8
16
 
9
17
  ```
@@ -20,6 +28,8 @@ Installs `foundation` and `crm` first if missing. Everything lands under `g8/web
20
28
  | graph8 workspace | An **answering agent** mapped to the bot. A bot with none answers nothing. | `GET /api/v1/webchat/agents?chat_bot_id=` lists an active agent. |
21
29
  | Your app | `allowedPrefixes` must include `webchat` and `snippet`. | The route file lists both. |
22
30
 
31
+ Every detail opens as a full page with `&view=full` (the Open full page button).
32
+
23
33
  ## The flow, in order (IDs passed between steps)
24
34
 
25
35
  1. `listChatBots` -> **`bot.id`**. This is the `chat_bot_id` of nearly every other call. `createChatBot({ name })` makes one.
@@ -14,17 +14,17 @@
14
14
  "crm"
15
15
  ],
16
16
  "operations": [
17
- "list_chat_bots_webchat_chat_bots_get",
18
17
  "create_chat_bot_webchat_chat_bots_post",
19
- "get_webchat_settings_webchat_settings_get",
20
- "create_webchat_settings_webchat_settings_post",
21
- "update_webchat_settings_webchat_settings_patch",
22
- "list_webchat_questions_webchat_questions_get",
23
18
  "create_webchat_question_webchat_questions_post",
19
+ "create_webchat_settings_webchat_settings_post",
24
20
  "delete_webchat_question_webchat_questions__question_id__delete",
25
21
  "get_org_snippet_snippet_get",
22
+ "get_webchat_settings_webchat_settings_get",
23
+ "get_webchat_visitor_funnel_webchat_visitors_funnel_get",
24
+ "list_chat_bots_webchat_chat_bots_get",
25
+ "list_webchat_questions_webchat_questions_get",
26
26
  "list_webchat_visitors_webchat_visitors_get",
27
- "get_webchat_visitor_funnel_webchat_visitors_funnel_get"
27
+ "update_webchat_settings_webchat_settings_patch"
28
28
  ],
29
29
  "components": [
30
30
  {
@@ -59,6 +59,19 @@
59
59
  "tiers": [
60
60
  "read"
61
61
  ]
62
+ },
63
+ {
64
+ "name": "WebchatScreen",
65
+ "file": "components/webchat-screen.tsx",
66
+ "purpose": "Finished web chat page: visitors with funnel stats and detail, widget styling, and install snippet.",
67
+ "operations": [
68
+ "get_org_snippet_snippet_get",
69
+ "get_webchat_visitor_funnel_webchat_visitors_funnel_get",
70
+ "list_webchat_visitors_webchat_visitors_get"
71
+ ],
72
+ "tiers": [
73
+ "read"
74
+ ]
62
75
  }
63
76
  ],
64
77
  "hooks": [],
@@ -129,6 +142,7 @@
129
142
  }
130
143
  ],
131
144
  "peerDependencies": {
132
- "react": ">=18"
145
+ "react": ">=18",
146
+ "lucide-react": ">=0.400.0"
133
147
  }
134
148
  }
@@ -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]);
@@ -120,7 +123,7 @@ export function ChatWidgetConfig({ initialBotId, surface = "external" }: ChatWid
120
123
  </div>
121
124
  {problem ? <p role="alert" className="text-sm text-red-700">{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("");
@@ -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>