@graph8/sdk 0.303.0 → 0.357.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (664) hide show
  1. package/README.md +81 -0
  2. package/blocks/README.md +32 -0
  3. package/blocks/_foundation/README.md +78 -0
  4. package/blocks/_foundation/block.json +79 -0
  5. package/blocks/_foundation/components/api-error.tsx +62 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +142 -0
  7. package/blocks/_foundation/components/empty-state.tsx +24 -0
  8. package/blocks/_foundation/hooks/use-g8-mutation.ts +88 -0
  9. package/blocks/_foundation/hooks/use-g8-query.ts +87 -0
  10. package/blocks/_foundation/hooks/use-job-poll.ts +130 -0
  11. package/blocks/_foundation/hooks/use-paginated.ts +105 -0
  12. package/blocks/_foundation/lib/g8-client.ts +128 -0
  13. package/blocks/_foundation/recipes/verify-proxy-key.ts +18 -0
  14. package/blocks/_foundation/server/proxy.ts +233 -0
  15. package/blocks/_foundation/server/route.ts +30 -0
  16. package/blocks/agents/README.md +133 -0
  17. package/blocks/agents/block.json +223 -0
  18. package/blocks/agents/components/agent-builder.tsx +199 -0
  19. package/blocks/agents/components/agent-knowledge-panel.tsx +144 -0
  20. package/blocks/agents/components/agent-runs.tsx +175 -0
  21. package/blocks/agents/components/agent-skills-panel.tsx +126 -0
  22. package/blocks/agents/components/agent-test-console.tsx +175 -0
  23. package/blocks/agents/lib/agent-types.ts +114 -0
  24. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +50 -0
  25. package/blocks/agents/recipes/run-and-follow-agent-run.ts +46 -0
  26. package/blocks/agents/recipes/test-agent-before-activating.ts +36 -0
  27. package/blocks/analytics/README.md +73 -0
  28. package/blocks/analytics/block.json +178 -0
  29. package/blocks/analytics/components/funnel-chart.tsx +79 -0
  30. package/blocks/analytics/components/kpi-tiles.tsx +74 -0
  31. package/blocks/analytics/components/leaderboard.tsx +86 -0
  32. package/blocks/analytics/components/report-builder.tsx +200 -0
  33. package/blocks/analytics/hooks/use-report.ts +26 -0
  34. package/blocks/analytics/lib/analytics-types.ts +108 -0
  35. package/blocks/analytics/lib/use-debounced.ts +16 -0
  36. package/blocks/analytics/recipes/01-weekly-outbound-report.ts +32 -0
  37. package/blocks/analytics/recipes/02-revenue-snapshot.ts +22 -0
  38. package/blocks/analytics/recipes/03-saved-report.ts +30 -0
  39. package/blocks/appointments/README.md +80 -0
  40. package/blocks/appointments/block.json +218 -0
  41. package/blocks/appointments/components/availability-editor.tsx +192 -0
  42. package/blocks/appointments/components/booking-page.tsx +154 -0
  43. package/blocks/appointments/components/event-type-editor.tsx +214 -0
  44. package/blocks/appointments/components/meeting-detail.tsx +147 -0
  45. package/blocks/appointments/components/meeting-list.tsx +148 -0
  46. package/blocks/appointments/hooks/use-available-slots.ts +29 -0
  47. package/blocks/appointments/hooks/use-schedules.ts +18 -0
  48. package/blocks/appointments/lib/appointments-types.ts +159 -0
  49. package/blocks/appointments/lib/idempotency.ts +30 -0
  50. package/blocks/appointments/recipes/01-create-event-type-and-hours.ts +27 -0
  51. package/blocks/appointments/recipes/02-find-slots-and-book.ts +34 -0
  52. package/blocks/appointments/recipes/03-bookings-digest.ts +22 -0
  53. package/blocks/appointments/recipes/04-calendar-invite.ts +21 -0
  54. package/blocks/block.schema.json +130 -0
  55. package/blocks/block.types.ts +109 -0
  56. package/blocks/campaigns/README.md +118 -0
  57. package/blocks/campaigns/block.json +361 -0
  58. package/blocks/campaigns/components/campaign-assets.tsx +180 -0
  59. package/blocks/campaigns/components/campaign-audience-step.tsx +91 -0
  60. package/blocks/campaigns/components/campaign-launch-step.tsx +147 -0
  61. package/blocks/campaigns/components/campaign-list.tsx +120 -0
  62. package/blocks/campaigns/components/campaign-metrics.tsx +93 -0
  63. package/blocks/campaigns/components/campaign-select.tsx +52 -0
  64. package/blocks/campaigns/components/campaign-sequence-step.tsx +158 -0
  65. package/blocks/campaigns/components/campaign-wizard.tsx +238 -0
  66. package/blocks/campaigns/components/form-field.tsx +29 -0
  67. package/blocks/campaigns/components/icp-editor.tsx +230 -0
  68. package/blocks/campaigns/components/persona-editor.tsx +210 -0
  69. package/blocks/campaigns/hooks/use-company-profile.ts +34 -0
  70. package/blocks/campaigns/lib/campaigns-utils.ts +155 -0
  71. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +33 -0
  72. package/blocks/campaigns/recipes/campaign-report.ts +22 -0
  73. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +51 -0
  74. package/blocks/campaigns/recipes/set-up-icp-and-personas.ts +37 -0
  75. package/blocks/coach/README.md +93 -0
  76. package/blocks/coach/block.json +125 -0
  77. package/blocks/coach/components/call-scorecard.tsx +126 -0
  78. package/blocks/coach/components/coaching-feed.tsx +140 -0
  79. package/blocks/coach/lib/coach-types.ts +116 -0
  80. package/blocks/coach/recipes/read-a-scorecard.ts +27 -0
  81. package/blocks/coach/recipes/weekly-coaching-digest.ts +53 -0
  82. package/blocks/copilot/README.md +99 -0
  83. package/blocks/copilot/block.json +151 -0
  84. package/blocks/copilot/components/copilot-memory.tsx +87 -0
  85. package/blocks/copilot/components/copilot-panel.tsx +189 -0
  86. package/blocks/copilot/components/copilot-team-chat.tsx +111 -0
  87. package/blocks/copilot/lib/copilot-types.ts +111 -0
  88. package/blocks/copilot/recipes/ask-the-copilot.ts +35 -0
  89. package/blocks/copilot/recipes/post-in-a-work-channel.ts +33 -0
  90. package/blocks/copilot/recipes/remember-and-recall.ts +19 -0
  91. package/blocks/crm/README.md +122 -0
  92. package/blocks/crm/block.json +378 -0
  93. package/blocks/crm/components/activity-timeline.tsx +99 -0
  94. package/blocks/crm/components/company-detail.tsx +118 -0
  95. package/blocks/crm/components/company-table.tsx +173 -0
  96. package/blocks/crm/components/contact-detail.tsx +135 -0
  97. package/blocks/crm/components/contact-table.tsx +217 -0
  98. package/blocks/crm/components/dedupe-review.tsx +164 -0
  99. package/blocks/crm/components/import-wizard.tsx +193 -0
  100. package/blocks/crm/components/notes-panel.tsx +112 -0
  101. package/blocks/crm/components/record-form.tsx +191 -0
  102. package/blocks/crm/components/task-list.tsx +107 -0
  103. package/blocks/crm/hooks/use-custom-fields.ts +18 -0
  104. package/blocks/crm/hooks/use-debounced.ts +13 -0
  105. package/blocks/crm/hooks/use-page-query.ts +52 -0
  106. package/blocks/crm/lib/crm-types.ts +136 -0
  107. package/blocks/crm/recipes/custom-fields-and-values.ts +28 -0
  108. package/blocks/crm/recipes/dedupe-and-merge.ts +30 -0
  109. package/blocks/crm/recipes/notes-tasks-and-activity.ts +23 -0
  110. package/blocks/crm/recipes/upsert-contact-with-company.ts +33 -0
  111. package/blocks/deals/README.md +78 -0
  112. package/blocks/deals/block.json +198 -0
  113. package/blocks/deals/components/deal-detail.tsx +134 -0
  114. package/blocks/deals/components/forecast-view.tsx +112 -0
  115. package/blocks/deals/components/pipeline-board.tsx +142 -0
  116. package/blocks/deals/components/stage-editor.tsx +96 -0
  117. package/blocks/deals/hooks/use-pipeline-deals.ts +67 -0
  118. package/blocks/deals/hooks/use-pipelines.ts +19 -0
  119. package/blocks/deals/lib/deals-types.ts +128 -0
  120. package/blocks/deals/recipes/01-create-deal-and-advance.ts +32 -0
  121. package/blocks/deals/recipes/02-pipeline-report.ts +35 -0
  122. package/blocks/deals/recipes/03-stale-deals-sweep.ts +20 -0
  123. package/blocks/dialer/README.md +114 -0
  124. package/blocks/dialer/block.json +260 -0
  125. package/blocks/dialer/components/call-log.tsx +78 -0
  126. package/blocks/dialer/components/call-queue.tsx +188 -0
  127. package/blocks/dialer/components/dialer-workspace.tsx +47 -0
  128. package/blocks/dialer/components/disposition-panel.tsx +75 -0
  129. package/blocks/dialer/components/live-calls-table.tsx +107 -0
  130. package/blocks/dialer/components/session-start-form.tsx +111 -0
  131. package/blocks/dialer/components/softphone.tsx +122 -0
  132. package/blocks/dialer/components/transcript-viewer.tsx +82 -0
  133. package/blocks/dialer/hooks/use-dialer-numbers.ts +19 -0
  134. package/blocks/dialer/hooks/use-dialer-session.ts +68 -0
  135. package/blocks/dialer/hooks/use-dispositions.ts +29 -0
  136. package/blocks/dialer/lib/dialer-types.ts +158 -0
  137. package/blocks/dialer/lib/softphone-transport.ts +67 -0
  138. package/blocks/dialer/lib/twilio-transport.ts +74 -0
  139. package/blocks/dialer/recipes/01-create-session-and-dial.ts +39 -0
  140. package/blocks/dialer/recipes/02-disposition-calls.ts +26 -0
  141. package/blocks/dialer/recipes/03-review-a-call.ts +23 -0
  142. package/blocks/enrichment/README.md +105 -0
  143. package/blocks/enrichment/block.json +204 -0
  144. package/blocks/enrichment/components/bulk-enrich-panel.tsx +205 -0
  145. package/blocks/enrichment/components/email-verify-badge.tsx +82 -0
  146. package/blocks/enrichment/components/enrich-button.tsx +136 -0
  147. package/blocks/enrichment/components/waterfall-config.tsx +127 -0
  148. package/blocks/enrichment/hooks/use-providers.ts +18 -0
  149. package/blocks/enrichment/lib/enrichment-types.ts +104 -0
  150. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +41 -0
  151. package/blocks/enrichment/recipes/lookup-and-verify.ts +25 -0
  152. package/blocks/enrichment/recipes/waterfall-pipeline.ts +26 -0
  153. package/blocks/forms/README.md +112 -0
  154. package/blocks/forms/block.json +200 -0
  155. package/blocks/forms/components/form-builder.tsx +150 -0
  156. package/blocks/forms/components/form-embed.tsx +103 -0
  157. package/blocks/forms/components/form-mapping-editor.tsx +125 -0
  158. package/blocks/forms/components/site-audit-panel.tsx +73 -0
  159. package/blocks/forms/components/submissions-table.tsx +158 -0
  160. package/blocks/forms/lib/form-types.ts +127 -0
  161. package/blocks/forms/recipes/install-snippet-and-form.ts +21 -0
  162. package/blocks/forms/recipes/retry-failed-deliveries.ts +26 -0
  163. package/blocks/forms/recipes/route-form-to-list.ts +44 -0
  164. package/blocks/inbox/README.md +72 -0
  165. package/blocks/inbox/block.json +194 -0
  166. package/blocks/inbox/components/inbox-list.tsx +156 -0
  167. package/blocks/inbox/components/inbox-screen.tsx +49 -0
  168. package/blocks/inbox/components/reply-composer.tsx +183 -0
  169. package/blocks/inbox/components/tag-and-assign.tsx +189 -0
  170. package/blocks/inbox/components/thread-badge.tsx +17 -0
  171. package/blocks/inbox/components/thread-view.tsx +108 -0
  172. package/blocks/inbox/hooks/use-inbox-thread.ts +24 -0
  173. package/blocks/inbox/hooks/use-inbox-threads.ts +40 -0
  174. package/blocks/inbox/lib/idempotency.ts +30 -0
  175. package/blocks/inbox/lib/inbox-types.ts +100 -0
  176. package/blocks/inbox/recipes/01-triage-and-reply.ts +43 -0
  177. package/blocks/inbox/recipes/02-tag-and-assign.ts +31 -0
  178. package/blocks/inbox/recipes/03-hot-contacts-digest.ts +16 -0
  179. package/blocks/integrations/README.md +75 -0
  180. package/blocks/integrations/block.json +172 -0
  181. package/blocks/integrations/components/integration-list.tsx +165 -0
  182. package/blocks/integrations/components/sync-status.tsx +223 -0
  183. package/blocks/integrations/components/webhook-manager.tsx +300 -0
  184. package/blocks/integrations/lib/integrations-utils.ts +62 -0
  185. package/blocks/integrations/recipes/crm-sync-health.ts +19 -0
  186. package/blocks/integrations/recipes/push-contacts-to-crm.ts +20 -0
  187. package/blocks/integrations/recipes/register-webhook.ts +18 -0
  188. package/blocks/integrations/recipes/verify-webhook-route.ts +33 -0
  189. package/blocks/intent/README.md +57 -0
  190. package/blocks/intent/block.json +138 -0
  191. package/blocks/intent/components/account-intent-score.tsx +91 -0
  192. package/blocks/intent/components/intent-feed.tsx +215 -0
  193. package/blocks/intent/components/intent-keyword-manager.tsx +331 -0
  194. package/blocks/intent/hooks/use-account-intent.ts +46 -0
  195. package/blocks/intent/lib/intent-utils.ts +95 -0
  196. package/blocks/intent/recipes/rank-intent-accounts.ts +36 -0
  197. package/blocks/intent/recipes/track-competitor-domain.ts +23 -0
  198. package/blocks/intent/recipes/who-visited-url.ts +18 -0
  199. package/blocks/lifecycle/README.md +79 -0
  200. package/blocks/lifecycle/block.json +198 -0
  201. package/blocks/lifecycle/components/churn-list.tsx +92 -0
  202. package/blocks/lifecycle/components/customer-table.tsx +119 -0
  203. package/blocks/lifecycle/components/lead-queue.tsx +127 -0
  204. package/blocks/lifecycle/components/lifecycle-board.tsx +102 -0
  205. package/blocks/lifecycle/components/stage-move-dialog.tsx +75 -0
  206. package/blocks/lifecycle/hooks/use-debounced.ts +13 -0
  207. package/blocks/lifecycle/hooks/use-lifecycle-stages.ts +27 -0
  208. package/blocks/lifecycle/lib/lifecycle.ts +99 -0
  209. package/blocks/lifecycle/recipes/customer-brief.ts +23 -0
  210. package/blocks/lifecycle/recipes/move-company-stage.ts +17 -0
  211. package/blocks/lifecycle/recipes/read-lifecycle-board.ts +21 -0
  212. package/blocks/lifecycle/recipes/unlock-stage-contacts.ts +25 -0
  213. package/blocks/lists/README.md +96 -0
  214. package/blocks/lists/block.json +166 -0
  215. package/blocks/lists/components/add-to-list-button.tsx +111 -0
  216. package/blocks/lists/components/list-picker.tsx +94 -0
  217. package/blocks/lists/components/list-table.tsx +160 -0
  218. package/blocks/lists/components/suppression-manager.tsx +153 -0
  219. package/blocks/lists/recipes/create-list-and-add-contacts.ts +35 -0
  220. package/blocks/lists/recipes/refresh-and-check-lists.ts +21 -0
  221. package/blocks/lists/recipes/suppress-and-reinstate.ts +30 -0
  222. package/blocks/mailboxes/README.md +71 -0
  223. package/blocks/mailboxes/block.json +187 -0
  224. package/blocks/mailboxes/components/connect-mailbox.tsx +284 -0
  225. package/blocks/mailboxes/components/domain-purchase.tsx +271 -0
  226. package/blocks/mailboxes/components/mailbox-list.tsx +185 -0
  227. package/blocks/mailboxes/components/warmup-health.tsx +232 -0
  228. package/blocks/mailboxes/lib/mailboxes-utils.ts +69 -0
  229. package/blocks/mailboxes/recipes/buy-domain-and-mailboxes.ts +64 -0
  230. package/blocks/mailboxes/recipes/connect-imap-mailbox.ts +38 -0
  231. package/blocks/mailboxes/recipes/is-it-safe-to-send.ts +17 -0
  232. package/blocks/mailboxes/recipes/warmup-report.ts +31 -0
  233. package/blocks/newsletter/README.md +122 -0
  234. package/blocks/newsletter/block.json +224 -0
  235. package/blocks/newsletter/components/issue-form.tsx +124 -0
  236. package/blocks/newsletter/components/issue-scheduler.tsx +152 -0
  237. package/blocks/newsletter/components/newsletter-editor.tsx +131 -0
  238. package/blocks/newsletter/components/newsletter-stats.tsx +93 -0
  239. package/blocks/newsletter/components/subscriber-table.tsx +173 -0
  240. package/blocks/newsletter/lib/newsletter-types.ts +97 -0
  241. package/blocks/newsletter/recipes/create-newsletter-and-draft-issue.ts +27 -0
  242. package/blocks/newsletter/recipes/import-audience-and-send.ts +43 -0
  243. package/blocks/newsletter/recipes/issue-performance-report.ts +26 -0
  244. package/blocks/nurture/README.md +110 -0
  245. package/blocks/nurture/block.json +191 -0
  246. package/blocks/nurture/components/nurture-builder.tsx +114 -0
  247. package/blocks/nurture/components/nurture-enroll.tsx +107 -0
  248. package/blocks/nurture/components/nurture-form.tsx +120 -0
  249. package/blocks/nurture/components/nurture-stats.tsx +81 -0
  250. package/blocks/nurture/lib/nurture-types.ts +109 -0
  251. package/blocks/nurture/recipes/create-a-nurture.ts +34 -0
  252. package/blocks/nurture/recipes/enroll-contacts-and-launch.ts +24 -0
  253. package/blocks/nurture/recipes/nurture-health-check.ts +28 -0
  254. package/blocks/objects/README.md +120 -0
  255. package/blocks/objects/block.json +243 -0
  256. package/blocks/objects/components/attribute-field-input.tsx +82 -0
  257. package/blocks/objects/components/attribute-form-dialog.tsx +159 -0
  258. package/blocks/objects/components/object-not-found.tsx +34 -0
  259. package/blocks/objects/components/object-record-detail.tsx +143 -0
  260. package/blocks/objects/components/object-schema-editor.tsx +197 -0
  261. package/blocks/objects/components/object-table.tsx +130 -0
  262. package/blocks/objects/components/record-form-dialog.tsx +112 -0
  263. package/blocks/objects/components/record-roles-panel.tsx +106 -0
  264. package/blocks/objects/hooks/use-object-schema.ts +36 -0
  265. package/blocks/objects/lib/object-types.ts +214 -0
  266. package/blocks/objects/recipes/define-object-and-add-records.ts +24 -0
  267. package/blocks/objects/recipes/update-audit-and-archive-records.ts +39 -0
  268. package/blocks/objects/recipes/upsert-records-from-your-system.ts +30 -0
  269. package/blocks/pages/README.md +104 -0
  270. package/blocks/pages/block.json +223 -0
  271. package/blocks/pages/components/brand-kit-editor.tsx +152 -0
  272. package/blocks/pages/components/content-library.tsx +163 -0
  273. package/blocks/pages/components/page-editor.tsx +253 -0
  274. package/blocks/pages/components/page-list.tsx +159 -0
  275. package/blocks/pages/components/seo-report.tsx +159 -0
  276. package/blocks/pages/lib/studio.ts +77 -0
  277. package/blocks/pages/recipes/audit-site-seo.ts +28 -0
  278. package/blocks/pages/recipes/create-and-publish-page.ts +26 -0
  279. package/blocks/pages/recipes/edit-page-section.ts +25 -0
  280. package/blocks/pages/recipes/refresh-brand-kit.ts +22 -0
  281. package/blocks/quotes/README.md +79 -0
  282. package/blocks/quotes/block.json +183 -0
  283. package/blocks/quotes/components/product-picker.tsx +80 -0
  284. package/blocks/quotes/components/quote-builder.tsx +195 -0
  285. package/blocks/quotes/components/quote-list.tsx +79 -0
  286. package/blocks/quotes/components/quote-viewer.tsx +135 -0
  287. package/blocks/quotes/components/signature-status.tsx +77 -0
  288. package/blocks/quotes/hooks/use-debounced.ts +13 -0
  289. package/blocks/quotes/lib/quotes.ts +142 -0
  290. package/blocks/quotes/recipes/create-and-send-quote.ts +44 -0
  291. package/blocks/quotes/recipes/revise-sent-quote.ts +18 -0
  292. package/blocks/quotes/recipes/track-quote-signature.ts +22 -0
  293. package/blocks/radar/README.md +106 -0
  294. package/blocks/radar/block.json +189 -0
  295. package/blocks/radar/components/competitor-battle-card.tsx +53 -0
  296. package/blocks/radar/components/competitor-tracker.tsx +158 -0
  297. package/blocks/radar/components/radar-overview.tsx +67 -0
  298. package/blocks/radar/components/radar-report.tsx +86 -0
  299. package/blocks/radar/hooks/use-competitors.ts +23 -0
  300. package/blocks/radar/lib/radar-types.ts +124 -0
  301. package/blocks/radar/recipes/battle-card.ts +28 -0
  302. package/blocks/radar/recipes/deep-dive-report.ts +32 -0
  303. package/blocks/radar/recipes/marketing-snapshot.ts +18 -0
  304. package/blocks/radar/recipes/track-and-scan.ts +39 -0
  305. package/blocks/search/README.md +129 -0
  306. package/blocks/search/block.json +185 -0
  307. package/blocks/search/components/filter-builder.tsx +149 -0
  308. package/blocks/search/components/prospect-results.tsx +175 -0
  309. package/blocks/search/components/save-to-list-dialog.tsx +135 -0
  310. package/blocks/search/components/saved-searches.tsx +95 -0
  311. package/blocks/search/hooks/use-filter-options.ts +41 -0
  312. package/blocks/search/lib/search-types.ts +149 -0
  313. package/blocks/search/recipes/explore-filter-values.ts +27 -0
  314. package/blocks/search/recipes/saved-searches.ts +34 -0
  315. package/blocks/search/recipes/search-and-save-to-list.ts +37 -0
  316. package/blocks/sequencer/README.md +87 -0
  317. package/blocks/sequencer/block.json +214 -0
  318. package/blocks/sequencer/components/enroll-dialog.tsx +93 -0
  319. package/blocks/sequencer/components/sequence-builder.tsx +306 -0
  320. package/blocks/sequencer/components/sequence-list.tsx +200 -0
  321. package/blocks/sequencer/components/sequence-stats.tsx +256 -0
  322. package/blocks/sequencer/components/step-editor.tsx +213 -0
  323. package/blocks/sequencer/lib/sequencer-utils.ts +168 -0
  324. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +51 -0
  325. package/blocks/sequencer/recipes/launch-and-enroll.ts +44 -0
  326. package/blocks/sequencer/recipes/pause-when-unsafe.ts +30 -0
  327. package/blocks/sequencer/recipes/sequence-report.ts +22 -0
  328. package/blocks/social-ads/README.md +128 -0
  329. package/blocks/social-ads/block.json +256 -0
  330. package/blocks/social-ads/components/ad-campaign-list.tsx +136 -0
  331. package/blocks/social-ads/components/audience-sync.tsx +150 -0
  332. package/blocks/social-ads/components/creative-gallery.tsx +121 -0
  333. package/blocks/social-ads/components/post-composer.tsx +122 -0
  334. package/blocks/social-ads/components/social-post-list.tsx +116 -0
  335. package/blocks/social-ads/hooks/use-social-accounts.ts +16 -0
  336. package/blocks/social-ads/lib/social-ads-types.ts +167 -0
  337. package/blocks/social-ads/recipes/creative-to-gallery.ts +41 -0
  338. package/blocks/social-ads/recipes/schedule-post.ts +51 -0
  339. package/blocks/social-ads/recipes/sync-audience-and-launch.ts +41 -0
  340. package/blocks/visitors/README.md +56 -0
  341. package/blocks/visitors/block.json +139 -0
  342. package/blocks/visitors/components/snippet-install.tsx +169 -0
  343. package/blocks/visitors/components/traffic-chart.tsx +156 -0
  344. package/blocks/visitors/components/visitor-company-card.tsx +92 -0
  345. package/blocks/visitors/components/visitor-feed.tsx +157 -0
  346. package/blocks/visitors/lib/visitors-utils.ts +78 -0
  347. package/blocks/visitors/recipes/check-snippet-installed.ts +24 -0
  348. package/blocks/visitors/recipes/recent-company-visits.ts +27 -0
  349. package/blocks/visitors/recipes/traffic-summary.ts +24 -0
  350. package/blocks/webchat/README.md +87 -0
  351. package/blocks/webchat/block.json +134 -0
  352. package/blocks/webchat/components/chat-widget-config.tsx +160 -0
  353. package/blocks/webchat/components/conversation-list.tsx +74 -0
  354. package/blocks/webchat/lib/webchat-types.ts +83 -0
  355. package/blocks/webchat/recipes/create-bot-and-style-widget.ts +24 -0
  356. package/blocks/webchat/recipes/list-chat-visitors.ts +24 -0
  357. package/blocks/workbench/README.md +135 -0
  358. package/blocks/workbench/block.json +259 -0
  359. package/blocks/workbench/components/column-recipe-editor.tsx +183 -0
  360. package/blocks/workbench/components/run-progress.tsx +132 -0
  361. package/blocks/workbench/components/workbench-add-rows.tsx +100 -0
  362. package/blocks/workbench/components/workbench-grid.tsx +159 -0
  363. package/blocks/workbench/components/workbench-row-actions.tsx +102 -0
  364. package/blocks/workbench/components/workbench-table-list.tsx +105 -0
  365. package/blocks/workbench/hooks/use-workbench-table.ts +30 -0
  366. package/blocks/workbench/lib/waterfall-presets.ts +119 -0
  367. package/blocks/workbench/lib/workbench-types.ts +191 -0
  368. package/blocks/workbench/recipes/build-enriched-table.ts +65 -0
  369. package/blocks/workbench/recipes/import-from-search.ts +42 -0
  370. package/blocks/workbench/recipes/scrape-into-table.ts +36 -0
  371. package/blocks/workbench/recipes/waterfall-column.ts +55 -0
  372. package/blocks/workflows/README.md +100 -0
  373. package/blocks/workflows/block.json +188 -0
  374. package/blocks/workflows/components/run-history.tsx +136 -0
  375. package/blocks/workflows/components/trigger-config.tsx +134 -0
  376. package/blocks/workflows/components/workflow-editor.tsx +142 -0
  377. package/blocks/workflows/components/workflow-list.tsx +153 -0
  378. package/blocks/workflows/hooks/use-workflow.ts +51 -0
  379. package/blocks/workflows/lib/graph.ts +187 -0
  380. package/blocks/workflows/recipes/create-workflow.ts +35 -0
  381. package/blocks/workflows/recipes/find-failed-runs.ts +15 -0
  382. package/blocks/workflows/recipes/run-workflow-and-wait.ts +32 -0
  383. package/blocks/workflows/recipes/set-trigger.ts +22 -0
  384. package/dist/cli.js +5330 -29
  385. package/dist/index.browser.js +7740 -0
  386. package/dist/index.browser.js.map +1 -0
  387. package/dist/index.browser.mjs +7717 -0
  388. package/dist/index.browser.mjs.map +1 -0
  389. package/dist/index.d.mts +2220 -23
  390. package/dist/index.d.ts +2220 -23
  391. package/dist/index.js +64 -3
  392. package/dist/index.js.map +1 -1
  393. package/dist/index.mjs +64 -3
  394. package/dist/index.mjs.map +1 -1
  395. package/dist/react.browser.js +7598 -0
  396. package/dist/react.browser.js.map +1 -0
  397. package/dist/react.browser.mjs +7568 -0
  398. package/dist/react.browser.mjs.map +1 -0
  399. package/dist/react.d.mts +2082 -20
  400. package/dist/react.d.ts +2082 -20
  401. package/dist/react.js +63 -2
  402. package/dist/react.js.map +1 -1
  403. package/dist/react.mjs +63 -2
  404. package/dist/react.mjs.map +1 -1
  405. package/dist/types/agentSetup.d.ts +6 -6
  406. package/dist/types/blocks/add.d.ts +53 -0
  407. package/dist/types/blocks/create.d.ts +46 -0
  408. package/dist/types/blocks/doctor.d.ts +26 -0
  409. package/dist/types/blocks/registry.d.ts +134 -0
  410. package/dist/types/cli.d.ts +16 -0
  411. package/dist/types/core.d.ts +1 -1
  412. package/dist/types/crypto-browser.d.ts +2 -0
  413. package/dist/types/crypto-node.d.ts +6 -0
  414. package/dist/types/react.d.ts +1 -1
  415. package/dist/types/version.d.ts +2 -2
  416. package/dist/types/webhooks.d.ts +2 -2
  417. package/dist/ui/index.d.mts +54 -0
  418. package/dist/ui/index.d.ts +54 -0
  419. package/dist/ui/index.js +105 -0
  420. package/dist/ui/index.js.map +1 -0
  421. package/dist/ui/index.mjs +44 -0
  422. package/dist/ui/index.mjs.map +1 -0
  423. package/dist/ui/lib/compose-refs.d.mts +19 -0
  424. package/dist/ui/lib/compose-refs.d.ts +19 -0
  425. package/dist/ui/lib/compose-refs.js +76 -0
  426. package/dist/ui/lib/compose-refs.js.map +1 -0
  427. package/dist/ui/lib/compose-refs.mjs +41 -0
  428. package/dist/ui/lib/compose-refs.mjs.map +1 -0
  429. package/dist/ui/lib/use-mobile.d.mts +4 -0
  430. package/dist/ui/lib/use-mobile.d.ts +4 -0
  431. package/dist/ui/lib/use-mobile.js +57 -0
  432. package/dist/ui/lib/use-mobile.js.map +1 -0
  433. package/dist/ui/lib/use-mobile.mjs +22 -0
  434. package/dist/ui/lib/use-mobile.mjs.map +1 -0
  435. package/dist/ui/lib/utils.d.mts +10 -0
  436. package/dist/ui/lib/utils.d.ts +10 -0
  437. package/dist/ui/lib/utils.js +33 -0
  438. package/dist/ui/lib/utils.js.map +1 -0
  439. package/dist/ui/lib/utils.mjs +9 -0
  440. package/dist/ui/lib/utils.mjs.map +1 -0
  441. package/dist/ui/primitives/alert-dialog.d.mts +22 -0
  442. package/dist/ui/primitives/alert-dialog.d.ts +22 -0
  443. package/dist/ui/primitives/alert-dialog.js +163 -0
  444. package/dist/ui/primitives/alert-dialog.js.map +1 -0
  445. package/dist/ui/primitives/alert-dialog.mjs +119 -0
  446. package/dist/ui/primitives/alert-dialog.mjs.map +1 -0
  447. package/dist/ui/primitives/avatar.d.mts +8 -0
  448. package/dist/ui/primitives/avatar.d.ts +8 -0
  449. package/dist/ui/primitives/avatar.js +91 -0
  450. package/dist/ui/primitives/avatar.js.map +1 -0
  451. package/dist/ui/primitives/avatar.mjs +55 -0
  452. package/dist/ui/primitives/avatar.mjs.map +1 -0
  453. package/dist/ui/primitives/breadcrumb.d.mts +13 -0
  454. package/dist/ui/primitives/breadcrumb.d.ts +13 -0
  455. package/dist/ui/primitives/breadcrumb.js +127 -0
  456. package/dist/ui/primitives/breadcrumb.js.map +1 -0
  457. package/dist/ui/primitives/breadcrumb.mjs +97 -0
  458. package/dist/ui/primitives/breadcrumb.mjs.map +1 -0
  459. package/dist/ui/primitives/button.d.mts +15 -0
  460. package/dist/ui/primitives/button.d.ts +15 -0
  461. package/dist/ui/primitives/button.js +91 -0
  462. package/dist/ui/primitives/button.js.map +1 -0
  463. package/dist/ui/primitives/button.mjs +56 -0
  464. package/dist/ui/primitives/button.mjs.map +1 -0
  465. package/dist/ui/primitives/card.d.mts +10 -0
  466. package/dist/ui/primitives/card.d.ts +10 -0
  467. package/dist/ui/primitives/card.js +101 -0
  468. package/dist/ui/primitives/card.js.map +1 -0
  469. package/dist/ui/primitives/card.mjs +62 -0
  470. package/dist/ui/primitives/card.mjs.map +1 -0
  471. package/dist/ui/primitives/carousel.d.mts +45 -0
  472. package/dist/ui/primitives/carousel.d.ts +45 -0
  473. package/dist/ui/primitives/carousel.js +300 -0
  474. package/dist/ui/primitives/carousel.js.map +1 -0
  475. package/dist/ui/primitives/carousel.mjs +261 -0
  476. package/dist/ui/primitives/carousel.mjs.map +1 -0
  477. package/dist/ui/primitives/chart.d.mts +68 -0
  478. package/dist/ui/primitives/chart.d.ts +68 -0
  479. package/dist/ui/primitives/chart.js +280 -0
  480. package/dist/ui/primitives/chart.js.map +1 -0
  481. package/dist/ui/primitives/chart.mjs +241 -0
  482. package/dist/ui/primitives/chart.mjs.map +1 -0
  483. package/dist/ui/primitives/collapsible.d.mts +8 -0
  484. package/dist/ui/primitives/collapsible.d.ts +8 -0
  485. package/dist/ui/primitives/collapsible.js +72 -0
  486. package/dist/ui/primitives/collapsible.js.map +1 -0
  487. package/dist/ui/primitives/collapsible.mjs +36 -0
  488. package/dist/ui/primitives/collapsible.mjs.map +1 -0
  489. package/dist/ui/primitives/context-menu.d.mts +29 -0
  490. package/dist/ui/primitives/context-menu.d.ts +29 -0
  491. package/dist/ui/primitives/context-menu.js +203 -0
  492. package/dist/ui/primitives/context-menu.js.map +1 -0
  493. package/dist/ui/primitives/context-menu.mjs +155 -0
  494. package/dist/ui/primitives/context-menu.mjs.map +1 -0
  495. package/dist/ui/primitives/dialog.d.mts +26 -0
  496. package/dist/ui/primitives/dialog.d.ts +26 -0
  497. package/dist/ui/primitives/dialog.js +192 -0
  498. package/dist/ui/primitives/dialog.js.map +1 -0
  499. package/dist/ui/primitives/dialog.mjs +148 -0
  500. package/dist/ui/primitives/dialog.mjs.map +1 -0
  501. package/dist/ui/primitives/drawer.d.mts +26 -0
  502. package/dist/ui/primitives/drawer.d.ts +26 -0
  503. package/dist/ui/primitives/drawer.js +146 -0
  504. package/dist/ui/primitives/drawer.js.map +1 -0
  505. package/dist/ui/primitives/drawer.mjs +103 -0
  506. package/dist/ui/primitives/drawer.mjs.map +1 -0
  507. package/dist/ui/primitives/input.d.mts +5 -0
  508. package/dist/ui/primitives/input.d.ts +5 -0
  509. package/dist/ui/primitives/input.js +46 -0
  510. package/dist/ui/primitives/input.js.map +1 -0
  511. package/dist/ui/primitives/input.mjs +22 -0
  512. package/dist/ui/primitives/input.mjs.map +1 -0
  513. package/dist/ui/primitives/item.d.mts +27 -0
  514. package/dist/ui/primitives/item.d.ts +27 -0
  515. package/dist/ui/primitives/item.js +221 -0
  516. package/dist/ui/primitives/item.js.map +1 -0
  517. package/dist/ui/primitives/item.mjs +188 -0
  518. package/dist/ui/primitives/item.mjs.map +1 -0
  519. package/dist/ui/primitives/kbd.d.mts +15 -0
  520. package/dist/ui/primitives/kbd.d.ts +15 -0
  521. package/dist/ui/primitives/kbd.js +45 -0
  522. package/dist/ui/primitives/kbd.js.map +1 -0
  523. package/dist/ui/primitives/kbd.mjs +21 -0
  524. package/dist/ui/primitives/kbd.mjs.map +1 -0
  525. package/dist/ui/primitives/label.d.mts +6 -0
  526. package/dist/ui/primitives/label.d.ts +6 -0
  527. package/dist/ui/primitives/label.js +55 -0
  528. package/dist/ui/primitives/label.js.map +1 -0
  529. package/dist/ui/primitives/label.mjs +21 -0
  530. package/dist/ui/primitives/label.mjs.map +1 -0
  531. package/dist/ui/primitives/resizable.d.mts +10 -0
  532. package/dist/ui/primitives/resizable.d.ts +10 -0
  533. package/dist/ui/primitives/resizable.js +68 -0
  534. package/dist/ui/primitives/resizable.js.map +1 -0
  535. package/dist/ui/primitives/resizable.mjs +42 -0
  536. package/dist/ui/primitives/resizable.mjs.map +1 -0
  537. package/dist/ui/primitives/save-status.d.mts +31 -0
  538. package/dist/ui/primitives/save-status.d.ts +31 -0
  539. package/dist/ui/primitives/save-status.js +150 -0
  540. package/dist/ui/primitives/save-status.js.map +1 -0
  541. package/dist/ui/primitives/save-status.mjs +126 -0
  542. package/dist/ui/primitives/save-status.mjs.map +1 -0
  543. package/dist/ui/primitives/separator.d.mts +6 -0
  544. package/dist/ui/primitives/separator.d.ts +6 -0
  545. package/dist/ui/primitives/separator.js +62 -0
  546. package/dist/ui/primitives/separator.js.map +1 -0
  547. package/dist/ui/primitives/separator.mjs +28 -0
  548. package/dist/ui/primitives/separator.mjs.map +1 -0
  549. package/dist/ui/primitives/sheet.d.mts +16 -0
  550. package/dist/ui/primitives/sheet.d.ts +16 -0
  551. package/dist/ui/primitives/sheet.js +170 -0
  552. package/dist/ui/primitives/sheet.js.map +1 -0
  553. package/dist/ui/primitives/sheet.mjs +129 -0
  554. package/dist/ui/primitives/sheet.mjs.map +1 -0
  555. package/dist/ui/primitives/sidebar.d.mts +74 -0
  556. package/dist/ui/primitives/sidebar.d.ts +74 -0
  557. package/dist/ui/primitives/sidebar.js +697 -0
  558. package/dist/ui/primitives/sidebar.js.map +1 -0
  559. package/dist/ui/primitives/sidebar.mjs +651 -0
  560. package/dist/ui/primitives/sidebar.mjs.map +1 -0
  561. package/dist/ui/primitives/skeleton.d.mts +5 -0
  562. package/dist/ui/primitives/skeleton.d.ts +5 -0
  563. package/dist/ui/primitives/skeleton.js +40 -0
  564. package/dist/ui/primitives/skeleton.js.map +1 -0
  565. package/dist/ui/primitives/skeleton.mjs +16 -0
  566. package/dist/ui/primitives/skeleton.mjs.map +1 -0
  567. package/dist/ui/primitives/slider.d.mts +20 -0
  568. package/dist/ui/primitives/slider.d.ts +20 -0
  569. package/dist/ui/primitives/slider.js +112 -0
  570. package/dist/ui/primitives/slider.js.map +1 -0
  571. package/dist/ui/primitives/slider.mjs +78 -0
  572. package/dist/ui/primitives/slider.mjs.map +1 -0
  573. package/dist/ui/primitives/sortable.d.mts +50 -0
  574. package/dist/ui/primitives/sortable.d.ts +50 -0
  575. package/dist/ui/primitives/sortable.js +456 -0
  576. package/dist/ui/primitives/sortable.js.map +1 -0
  577. package/dist/ui/primitives/sortable.mjs +435 -0
  578. package/dist/ui/primitives/sortable.mjs.map +1 -0
  579. package/dist/ui/primitives/switch.d.mts +6 -0
  580. package/dist/ui/primitives/switch.d.ts +6 -0
  581. package/dist/ui/primitives/switch.js +67 -0
  582. package/dist/ui/primitives/switch.js.map +1 -0
  583. package/dist/ui/primitives/switch.mjs +33 -0
  584. package/dist/ui/primitives/switch.mjs.map +1 -0
  585. package/dist/ui/primitives/table.d.mts +12 -0
  586. package/dist/ui/primitives/table.d.ts +12 -0
  587. package/dist/ui/primitives/table.js +130 -0
  588. package/dist/ui/primitives/table.js.map +1 -0
  589. package/dist/ui/primitives/table.mjs +89 -0
  590. package/dist/ui/primitives/table.mjs.map +1 -0
  591. package/dist/ui/primitives/tabs.d.mts +9 -0
  592. package/dist/ui/primitives/tabs.d.ts +9 -0
  593. package/dist/ui/primitives/tabs.js +86 -0
  594. package/dist/ui/primitives/tabs.js.map +1 -0
  595. package/dist/ui/primitives/tabs.mjs +49 -0
  596. package/dist/ui/primitives/tabs.mjs.map +1 -0
  597. package/dist/ui/primitives/textarea.d.mts +5 -0
  598. package/dist/ui/primitives/textarea.d.ts +5 -0
  599. package/dist/ui/primitives/textarea.js +43 -0
  600. package/dist/ui/primitives/textarea.js.map +1 -0
  601. package/dist/ui/primitives/textarea.mjs +19 -0
  602. package/dist/ui/primitives/textarea.mjs.map +1 -0
  603. package/dist/ui/primitives/tooltip.d.mts +9 -0
  604. package/dist/ui/primitives/tooltip.d.ts +9 -0
  605. package/dist/ui/primitives/tooltip.js +94 -0
  606. package/dist/ui/primitives/tooltip.js.map +1 -0
  607. package/dist/ui/primitives/tooltip.mjs +57 -0
  608. package/dist/ui/primitives/tooltip.mjs.map +1 -0
  609. package/docs/agents.md +72 -29
  610. package/docs/api/README.md +7 -3
  611. package/docs/api/appObjects.d.ts +3 -1
  612. package/docs/api/appObjects.md +2 -1
  613. package/docs/api/by-path.md +51 -0
  614. package/docs/api/dataPipelines.d.ts +149 -0
  615. package/docs/api/dataPipelines.md +232 -0
  616. package/docs/api/forms.d.ts +16 -0
  617. package/docs/api/forms.md +8 -0
  618. package/docs/api/objects.d.ts +705 -5
  619. package/docs/api/objects.md +1099 -14
  620. package/docs/api/ops.d.ts +1 -1
  621. package/docs/api/ops.md +1 -1
  622. package/docs/api/processLists.d.ts +247 -0
  623. package/docs/api/processLists.md +409 -0
  624. package/docs/api/quotes.d.ts +3 -1
  625. package/docs/api/quotes.md +7 -2
  626. package/docs/api/radar.d.ts +4 -0
  627. package/docs/api/radar.md +1 -0
  628. package/docs/api/reports.d.ts +476 -0
  629. package/docs/api/reports.md +539 -0
  630. package/docs/api/scheduleBuy.d.ts +12 -8
  631. package/docs/api/scheduleBuy.md +13 -10
  632. package/docs/api/seo.d.ts +406 -3
  633. package/docs/api/seo.md +505 -1
  634. package/docs/api/workspaceBackup.d.ts +324 -0
  635. package/docs/api/workspaceBackup.md +427 -0
  636. package/package.json +174 -5
  637. package/stacks/6sense.json +46 -0
  638. package/stacks/README.md +20 -0
  639. package/stacks/apollo.json +128 -0
  640. package/stacks/clay.json +59 -0
  641. package/stacks/gong.json +72 -0
  642. package/stacks/hubspot.json +162 -0
  643. package/stacks/intercom.json +53 -0
  644. package/stacks/mailchimp.json +71 -0
  645. package/stacks/outreach.json +86 -0
  646. package/stacks/salesforce.json +94 -0
  647. package/stacks/stack.schema.json +32 -0
  648. package/stacks/starter.json +22 -0
  649. package/styles/styles.css +2 -0
  650. package/styles/theme.css +181 -0
  651. package/styles/tokens.css +284 -0
  652. package/templates/next-app/README.md.tmpl +35 -0
  653. package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
  654. package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
  655. package/templates/next-app/app/globals.css.tmpl +8 -0
  656. package/templates/next-app/app/layout.tsx.tmpl +19 -0
  657. package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
  658. package/templates/next-app/dot-env.local.example.tmpl +6 -0
  659. package/templates/next-app/dot-gitignore.tmpl +7 -0
  660. package/templates/next-app/lib/nav.ts.tmpl +2 -0
  661. package/templates/next-app/next.config.mjs.tmpl +4 -0
  662. package/templates/next-app/package.json.tmpl +25 -0
  663. package/templates/next-app/postcss.config.mjs.tmpl +4 -0
  664. package/templates/next-app/tsconfig.json.tmpl +21 -0
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @title Sync a list to an ad platform and launch a campaign on it
3
+ * @when Retargeting or lookalike campaigns from a graph8 list. The ad account must already be connected in graph8
4
+ * (OAuth happens in the app only; the API cannot do it). Both steps are EXTERNAL: creating a sync sends the list's
5
+ * contacts to the platform, launching creates a campaign that spends from the ad account. Get a human to approve the
6
+ * list (and its size), the platform, the daily budget and the dates. Start and end are date-only: pass "YYYY-MM-DD".
7
+ * A 409 on create means that list is already synced to that platform: reuse the existing sync.
8
+ */
9
+ import { g8 } from "@graph8/sdk";
10
+
11
+ g8.init({ apiKey: process.env.G8_API_KEY! });
12
+
13
+ export async function syncList(audienceId: number, platform: "meta" | "linkedin" | "google" | "x") {
14
+ const res = await g8.api.audienceSyncs.createAudienceSync({ body: { audience_id: audienceId, platform, mode: "mirror", refresh_cadence_hours: 24 } });
15
+ return (res.data as { id: number }).id;
16
+ }
17
+
18
+ export async function pushNow(syncId: number) {
19
+ return g8.api.audienceSyncs.triggerAudienceSync({ path: { config_id: syncId } });
20
+ }
21
+
22
+ /**
23
+ * HTTP 200 does NOT mean launched: a refused launch answers 200 with `status: "error"` and a `message`. Check it.
24
+ * `currency` defaults to USD; Google needs `creative_image_url`.
25
+ */
26
+ export async function launch(syncId: number, name: string, dailyBudget: number, destinationUrl: string, startDate: string, endDate?: string, currency = "USD") {
27
+ const res = await g8.api.adCampaigns.launchAdCampaign({
28
+ path: { config_id: syncId },
29
+ body: { name, daily_budget: dailyBudget, currency, destination_url: destinationUrl, start_date: startDate, ...(endDate ? { end_date: endDate } : {}) },
30
+ });
31
+ const body = res.data as { status?: string; message?: string };
32
+ if (body.status === "error" || body.status === "failed") throw new Error(body.message ?? "The platform refused the launch");
33
+ return res.data;
34
+ }
35
+
36
+ /** Results by campaign for the last `days`; join on `platform_campaign_id` (one sync can carry several campaigns, so `sync_config_id` alone would sum them). Spend is in each campaign's currency. */
37
+ export async function results(days = 30) {
38
+ const [campaigns, metrics] = await Promise.all([g8.api.adCampaigns.listAdCampaigns(), g8.api.adCampaigns.getPerCampaignMetrics({ query: { days } })]);
39
+ const rows = ((metrics.data as { metrics?: Array<{ sync_config_id: number; platform_campaign_id?: string; impressions: number; clicks: number; spend: number }> }).metrics) ?? [];
40
+ return campaigns.data.map((c) => ({ id: c.id, name: c.name, status: c.status, metrics: rows.filter((m) => m.sync_config_id === c.sync_config_id && (!c.platform_campaign_id || m.platform_campaign_id === c.platform_campaign_id)) }));
41
+ }
@@ -0,0 +1,56 @@
1
+ # visitors
2
+
3
+ See which companies and people visit your website, install the graph8 tracking snippet and prove it works,
4
+ and chart traffic. The Apollo / 6sense "website visitors" screen.
5
+
6
+ Depends on: `crm`. Install: `npx @graph8/sdk add visitors`.
7
+
8
+ ## Components
9
+
10
+ | Component | What it does | Tiers |
11
+ |---|---|---|
12
+ | `VisitorFeed` | Polled feed of company visits (default) and identified people. Paging, refresh, bots excluded. | read |
13
+ | `VisitorCompanyCard` | One visiting company with its page views; "Full journey" loads every page view. | read |
14
+ | `SnippetInstall` | Streams picker, React and script-tag snippets with copy, and an install check that polls until events arrive. | read |
15
+ | `TrafficChart` | Visitors, sessions or page views per day (inline SVG), top pages, "N on the site now". | read |
16
+
17
+ Everything here is read tier: no confirmation dialog is needed. No component spends credits or contacts anyone.
18
+
19
+ ## The flow, in order
20
+
21
+ 1. **Get the snippet.** `SnippetInstall` -> `listOrgStreams` (the websites) -> `getOrgSnippet` with the chosen
22
+ `stream_id`. The `write_key` it returns is PUBLIC and goes in page source.
23
+ 2. **The human installs it** (paste into the site or the Next.js layout). This is the one step the agent cannot do.
24
+ 3. **Prove it.** "Check installation" polls `checkTrackingInstallation` with that `write_key` until
25
+ `status` is `receiving` (also `no_events`, `unknown`). It backs off and gives up after about 5 minutes.
26
+ 4. **Show visitors.** `VisitorFeed` -> `listCompanyVisitorEvents` (`show_mode=grouped`) and, on the People tab,
27
+ `listContactVisitorEvents`. Each grouped row's `COMPANY_ID` feeds the card's journey call
28
+ (`filter={"company_id": id}`, `show_mode=raw`).
29
+ 5. **Chart traffic.** `TrafficChart` -> `getAudienceTrend`, `getTopPages`, `getRealtime` (re-read every 10 s).
30
+
31
+ ## Prerequisites
32
+
33
+ - Tracking snippet on the site (`where: "app"`). Verify: Check installation reports events received.
34
+ - A website stream exists in the workspace (`where: "api"`): `GET /streams` is non-empty.
35
+ - The People tab joins visits to contacts, so the API key also needs the contacts read scope.
36
+
37
+ ## Defaults (do not ask the human)
38
+
39
+ - Poll the feed every 30 s; realtime every 10 s (there are no streams or sockets).
40
+ - Companies tab first; bots and internal traffic excluded; 25 rows per page.
41
+ - The org's main stream unless the person picks another.
42
+
43
+ ## Operations
44
+
45
+ | Operation | Method and path | Tier | Used by |
46
+ |---|---|---|---|
47
+ | `list_company_visitor_events_visitors_companies_events_get` | GET /visitors/companies/events | read | VisitorFeed, VisitorCompanyCard |
48
+ | `list_contact_visitor_events_visitors_contacts_events_get` | GET /visitors/contacts/events | read | VisitorFeed |
49
+ | `list_org_streams_streams_get` | GET /streams | read | SnippetInstall |
50
+ | `get_org_snippet_snippet_get` | GET /snippet | read | SnippetInstall |
51
+ | `check_tracking_installation_visitors_installation_check_get` | GET /visitors/installation-check | read | SnippetInstall |
52
+ | `get_audience_trend_analytics_audience_trend_get` | GET /analytics/audience/trend | read | TrafficChart |
53
+ | `get_realtime_analytics_realtime_get` | GET /analytics/realtime | read | TrafficChart |
54
+ | `get_top_pages_analytics_behavior_pages_get` | GET /analytics/behavior/pages | read | TrafficChart |
55
+
56
+ Recipes (`recipes/`): `check-snippet-installed.ts`, `recent-company-visits.ts`, `traffic-summary.ts`.
@@ -0,0 +1,139 @@
1
+ {
2
+ "$schema": "../block.schema.json",
3
+ "id": "visitors",
4
+ "title": "Website visitors",
5
+ "summary": "See which companies and people visit your site, install the tracking snippet, and chart traffic.",
6
+ "features": [
7
+ "live-ish company and people visitor feed",
8
+ "company visit journey",
9
+ "tracking snippet with install check",
10
+ "traffic trend, realtime and top pages"
11
+ ],
12
+ "dependsOn": [
13
+ "crm"
14
+ ],
15
+ "operations": [
16
+ "list_company_visitor_events_visitors_companies_events_get",
17
+ "list_contact_visitor_events_visitors_contacts_events_get",
18
+ "list_org_streams_streams_get",
19
+ "get_org_snippet_snippet_get",
20
+ "check_tracking_installation_visitors_installation_check_get",
21
+ "get_audience_trend_analytics_audience_trend_get",
22
+ "get_realtime_analytics_realtime_get",
23
+ "get_top_pages_analytics_behavior_pages_get"
24
+ ],
25
+ "components": [
26
+ {
27
+ "name": "VisitorFeed",
28
+ "file": "components/visitor-feed.tsx",
29
+ "purpose": "Polled feed of company and identified-person visits with paging and refresh.",
30
+ "operations": [
31
+ "list_company_visitor_events_visitors_companies_events_get",
32
+ "list_contact_visitor_events_visitors_contacts_events_get"
33
+ ],
34
+ "tiers": [
35
+ "read"
36
+ ]
37
+ },
38
+ {
39
+ "name": "VisitorCompanyCard",
40
+ "file": "components/visitor-company-card.tsx",
41
+ "purpose": "One visiting company with its page views and an on-demand full journey.",
42
+ "operations": [
43
+ "list_company_visitor_events_visitors_companies_events_get"
44
+ ],
45
+ "tiers": [
46
+ "read"
47
+ ]
48
+ },
49
+ {
50
+ "name": "SnippetInstall",
51
+ "file": "components/snippet-install.tsx",
52
+ "purpose": "Show the React and script-tag snippets for a website stream and poll until events arrive.",
53
+ "operations": [
54
+ "list_org_streams_streams_get",
55
+ "get_org_snippet_snippet_get",
56
+ "check_tracking_installation_visitors_installation_check_get"
57
+ ],
58
+ "tiers": [
59
+ "read"
60
+ ]
61
+ },
62
+ {
63
+ "name": "TrafficChart",
64
+ "file": "components/traffic-chart.tsx",
65
+ "purpose": "Daily visitors, sessions and page views as an SVG line chart, with top pages and a realtime counter.",
66
+ "operations": [
67
+ "get_audience_trend_analytics_audience_trend_get",
68
+ "get_realtime_analytics_realtime_get",
69
+ "get_top_pages_analytics_behavior_pages_get"
70
+ ],
71
+ "tiers": [
72
+ "read"
73
+ ]
74
+ }
75
+ ],
76
+ "hooks": [],
77
+ "lib": [
78
+ {
79
+ "file": "lib/visitors-utils.ts",
80
+ "purpose": "Visitor event types, label and time helpers, and envelope readers shared by the components."
81
+ }
82
+ ],
83
+ "recipes": [
84
+ {
85
+ "file": "recipes/check-snippet-installed.ts",
86
+ "title": "Fetch the tracking snippet and confirm events are arriving",
87
+ "when": "Onboarding a website."
88
+ },
89
+ {
90
+ "file": "recipes/recent-company-visits.ts",
91
+ "title": "List the companies that visited your site recently",
92
+ "when": "Building a who-is-on-our-site view or a daily digest."
93
+ },
94
+ {
95
+ "file": "recipes/traffic-summary.ts",
96
+ "title": "Summarise site traffic for the last N days",
97
+ "when": "A weekly report or dashboard header."
98
+ }
99
+ ],
100
+ "prerequisites": [
101
+ {
102
+ "id": "snippet-on-site",
103
+ "description": "The graph8 tracking snippet is pasted into the website the visitors feed should watch.",
104
+ "where": "app",
105
+ "verify": "SnippetInstall's Check installation reports events received (GET /visitors/installation-check status is receiving)."
106
+ },
107
+ {
108
+ "id": "website-stream",
109
+ "description": "The workspace has at least one website stream (it gives the write key).",
110
+ "where": "api",
111
+ "verify": "GET /streams returns at least one stream."
112
+ }
113
+ ],
114
+ "scopes": [
115
+ "analytics:read",
116
+ "integrations:read",
117
+ "visitors:read"
118
+ ],
119
+ "defaults": [
120
+ {
121
+ "decision": "Visitor feed refresh",
122
+ "default": "Poll every 30 seconds (no streams exist)",
123
+ "askHuman": false
124
+ },
125
+ {
126
+ "decision": "Bots and internal traffic",
127
+ "default": "Excluded",
128
+ "askHuman": false
129
+ },
130
+ {
131
+ "decision": "Which website stream to show",
132
+ "default": "The org's main stream",
133
+ "askHuman": false
134
+ }
135
+ ],
136
+ "peerDependencies": {
137
+ "react": ">=18"
138
+ }
139
+ }
@@ -0,0 +1,169 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SnippetInstall: shows the tracking snippet for a website stream, then watches for the first events so
5
+ * the person knows the install worked.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. listOrgStreams GET /streams (read; the org's websites and their write keys)
9
+ * 2. getOrgSnippet GET /snippet?stream_id= (read; write key + React and script-tag embeds)
10
+ * 3. checkTrackingInstallation GET /visitors/installation-check (read; polled until events arrive)
11
+ *
12
+ * Tiers: all read; no confirmation. The write key is the PUBLIC browser key, safe to ship in a page.
13
+ * Step 3 is a poll with backoff (there is no push): it stops at "receiving", or after about 5 minutes.
14
+ * The person must paste the snippet into their site (a `where: "app"` prerequisite) before step 3 can pass.
15
+ */
16
+
17
+ import { useState } from "react";
18
+ import { Button, Skeleton } from "@graph8/sdk/ui";
19
+ import { g8Get } from "@/g8/lib/g8-client";
20
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
21
+ import { useJobPoll } from "@/g8/hooks/use-job-poll";
22
+ import { ApiError } from "@/g8/components/api-error";
23
+ import { EmptyState } from "@/g8/components/empty-state";
24
+
25
+ export interface SnippetInstallProps {
26
+ /** Pre-select a stream id (from listOrgStreams). Default: the org's main stream. */
27
+ streamId?: string;
28
+ /** Called once events are seen. */
29
+ onInstalled?: () => void;
30
+ className?: string;
31
+ }
32
+
33
+ interface Stream {
34
+ id: string;
35
+ name: string;
36
+ domains?: string[];
37
+ is_default?: boolean;
38
+ }
39
+
40
+ interface SnippetPayload {
41
+ write_key: string;
42
+ react_snippet: string;
43
+ script_tag: string;
44
+ stream_id?: string | null;
45
+ stream_name?: string | null;
46
+ warning?: string | null;
47
+ action_required?: boolean;
48
+ }
49
+
50
+ interface InstallStatus {
51
+ status: "receiving" | "no_events" | "unknown";
52
+ event_count: number;
53
+ last_event_at: string | null;
54
+ }
55
+
56
+ function CodeBlock({ label, code }: { label: string; code: string }) {
57
+ const [copied, setCopied] = useState(false);
58
+ const copy = async () => {
59
+ try {
60
+ await navigator.clipboard.writeText(code);
61
+ setCopied(true);
62
+ } catch {
63
+ setCopied(false);
64
+ }
65
+ };
66
+ return (
67
+ <div className="space-y-1">
68
+ <div className="flex items-center justify-between">
69
+ <span className="text-sm font-medium">{label}</span>
70
+ <Button size="sm" variant="outline" onClick={() => void copy()}>
71
+ {copied ? "Copied" : "Copy"}
72
+ </Button>
73
+ </div>
74
+ <pre className="overflow-x-auto rounded bg-gray-950 p-3 text-xs text-gray-100">
75
+ <code>{code}</code>
76
+ </pre>
77
+ </div>
78
+ );
79
+ }
80
+
81
+ function InstallCheck({ writeKey, onInstalled }: { writeKey: string; onInstalled?: () => void }) {
82
+ const check = useJobPoll<InstallStatus>(
83
+ async (signal) => (await g8Get<InstallStatus>("/visitors/installation-check", { write_key: writeKey }, { signal })).data,
84
+ {
85
+ isTerminal: (s) => s.status === "receiving" || s.status === "unknown",
86
+ enabled: false,
87
+ intervalMs: 5000,
88
+ maxIntervalMs: 15000,
89
+ maxAttempts: 25,
90
+ onDone: (s) => s.status === "receiving" && onInstalled?.(),
91
+ },
92
+ );
93
+ const s = check.status;
94
+ return (
95
+ <div className="space-y-2" aria-live="polite">
96
+ <Button onClick={check.start} disabled={check.polling}>
97
+ {check.polling ? "Waiting for the first visit..." : "Check installation"}
98
+ </Button>
99
+ {check.error ? <ApiError error={check.error} /> : null}
100
+ {s?.status === "receiving" ? (
101
+ <p role="status" className="text-sm text-green-700">
102
+ Installed: {s.event_count.toLocaleString()} events received.
103
+ </p>
104
+ ) : null}
105
+ {s?.status === "unknown" ? (
106
+ <p role="status" className="text-sm text-amber-700">
107
+ Your workspace cannot be probed yet. Open your site once and check the Visitors feed.
108
+ </p>
109
+ ) : null}
110
+ {s?.status === "no_events" && !check.polling ? (
111
+ <p role="status" className="text-sm text-gray-600">
112
+ No events yet. Load a page that has the snippet, then check again.
113
+ </p>
114
+ ) : null}
115
+ {s?.status === "no_events" && check.polling ? (
116
+ <p className="text-sm text-gray-600">No events yet. Open a page that has the snippet.</p>
117
+ ) : null}
118
+ </div>
119
+ );
120
+ }
121
+
122
+ export function SnippetInstall({ streamId, onInstalled, className }: SnippetInstallProps) {
123
+ const [selected, setSelected] = useState<string | undefined>(streamId);
124
+ const streams = useG8Query((signal) => g8Get<Stream[]>("/streams", { limit: 50 }, { signal }), []);
125
+ const snippet = useG8Query((signal) => g8Get<SnippetPayload>("/snippet", { stream_id: selected }, { signal }), [selected]);
126
+ const list = streams.data?.data ?? [];
127
+
128
+ if (snippet.error) return <ApiError error={snippet.error} onRetry={() => void snippet.refetch()} className={className} />;
129
+ if (snippet.loading && !snippet.data) return <Skeleton className={`h-40 w-full ${className ?? ""}`} />;
130
+ const s = snippet.data?.data;
131
+ if (!s)
132
+ return (
133
+ <EmptyState
134
+ title="No tracking stream yet"
135
+ description="Create a website stream in graph8 first, then come back for its snippet."
136
+ className={className}
137
+ />
138
+ );
139
+
140
+ return (
141
+ <div className={`space-y-4 ${className ?? ""}`}>
142
+ {list.length > 1 ? (
143
+ <label className="flex items-center gap-2 text-sm">
144
+ Website
145
+ <select
146
+ className="rounded border border-gray-300 px-2 py-1"
147
+ value={selected ?? s.stream_id ?? ""}
148
+ onChange={(e) => setSelected(e.target.value || undefined)}
149
+ >
150
+ {list.map((st) => (
151
+ <option key={st.id} value={st.id}>
152
+ {st.name}
153
+ {st.domains?.length ? ` (${st.domains[0]})` : ""}
154
+ </option>
155
+ ))}
156
+ </select>
157
+ </label>
158
+ ) : null}
159
+ {s.warning ? (
160
+ <p role="alert" className="rounded border border-amber-200 bg-amber-50 p-2 text-sm text-amber-900">
161
+ {s.warning}
162
+ </p>
163
+ ) : null}
164
+ <CodeBlock label="React / Next.js" code={s.react_snippet} />
165
+ <CodeBlock label="Any website (script tag)" code={s.script_tag} />
166
+ <InstallCheck key={s.write_key} writeKey={s.write_key} onInstalled={onInstalled} />
167
+ </div>
168
+ );
169
+ }
@@ -0,0 +1,156 @@
1
+ "use client";
2
+
3
+ /**
4
+ * TrafficChart: visitors, sessions and page views over time, with who is on the site right now and the
5
+ * pages they are on. Pure inline SVG, so it needs no chart library.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. getAudienceTrend GET /analytics/audience/trend (read; one row per day: users, sessions, page_views)
9
+ * 2. getRealtime GET /analytics/realtime (read; active users now, polled every 10 s)
10
+ * 3. getTopPages GET /analytics/behavior/pages (read; top pages by views)
11
+ *
12
+ * Tiers: all read; no confirmation. Realtime has no stream: the component re-reads every `realtimeSeconds`
13
+ * (default 10, the endpoint's cache TTL; 0 turns it off). Pass `writeKey` to scope to one website stream.
14
+ */
15
+
16
+ import { useEffect, useState } from "react";
17
+ import { Button, Skeleton } from "@graph8/sdk/ui";
18
+ import { g8Get } from "@/g8/lib/g8-client";
19
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
20
+ import { ApiError } from "@/g8/components/api-error";
21
+ import { EmptyState } from "@/g8/components/empty-state";
22
+ import { seriesOf } from "../lib/visitors-utils";
23
+
24
+ export interface TrafficChartProps {
25
+ days?: number;
26
+ writeKey?: string;
27
+ realtimeSeconds?: number;
28
+ className?: string;
29
+ }
30
+
31
+ interface TrendPoint {
32
+ date: string;
33
+ users?: number;
34
+ new_users?: number;
35
+ sessions?: number;
36
+ page_views?: number;
37
+ }
38
+
39
+ interface PageRow {
40
+ page_path?: string;
41
+ path?: string;
42
+ url?: string;
43
+ page_title?: string;
44
+ views?: number;
45
+ page_views?: number;
46
+ unique_visitors?: number;
47
+ }
48
+
49
+ type Metric = "users" | "sessions" | "page_views";
50
+ const METRICS: Array<{ key: Metric; label: string }> = [
51
+ { key: "users", label: "Visitors" },
52
+ { key: "sessions", label: "Sessions" },
53
+ { key: "page_views", label: "Page views" },
54
+ ];
55
+
56
+ export function LineChart({ points, metric }: { points: TrendPoint[]; metric: Metric }) {
57
+ const width = 600;
58
+ const height = 160;
59
+ const values = points.map((p) => p[metric] ?? 0);
60
+ const max = Math.max(1, ...values);
61
+ const x = (i: number) => (points.length <= 1 ? width / 2 : (i / (points.length - 1)) * width);
62
+ const y = (v: number) => height - (v / max) * (height - 12) - 6;
63
+ const path = values.map((v, i) => `${i === 0 ? "M" : "L"}${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" ");
64
+ return (
65
+ <svg viewBox={`0 0 ${width} ${height}`} role="img" aria-label={`${metric} per day`} className="h-40 w-full text-blue-600">
66
+ <path d={path} fill="none" stroke="currentColor" strokeWidth="2" />
67
+ {values.map((v, i) => (
68
+ <circle key={points[i].date} cx={x(i)} cy={y(v)} r="2.5" fill="currentColor">
69
+ <title>{`${points[i].date}: ${v}`}</title>
70
+ </circle>
71
+ ))}
72
+ </svg>
73
+ );
74
+ }
75
+
76
+ function activeNow(envelope: unknown): number | undefined {
77
+ const data = (envelope as { data?: Record<string, unknown> } | null)?.data;
78
+ const value = data?.active_users_5min ?? data?.active_5min ?? data?.active_users ?? data?.five_min ?? data?.active_now;
79
+ return typeof value === "number" ? value : undefined;
80
+ }
81
+
82
+ export function TrafficChart({ days = 30, writeKey, realtimeSeconds = 10, className }: TrafficChartProps) {
83
+ const [metric, setMetric] = useState<Metric>("users");
84
+ const scope = { days, write_key: writeKey };
85
+ const trend = useG8Query((signal) => g8Get("/analytics/audience/trend", scope, { signal }), [days, writeKey]);
86
+ const pages = useG8Query((signal) => g8Get("/analytics/behavior/pages", { ...scope, limit: 8 }, { signal }), [days, writeKey]);
87
+ const live = useG8Query((signal) => g8Get("/analytics/realtime", { write_key: writeKey }, { signal }), [writeKey]);
88
+
89
+ useEffect(() => {
90
+ if (!realtimeSeconds) return;
91
+ const timer = setInterval(() => void live.refetch(), realtimeSeconds * 1000);
92
+ return () => clearInterval(timer);
93
+ }, [realtimeSeconds, live.refetch]);
94
+
95
+ const points = seriesOf<TrendPoint>(trend.data);
96
+ const topPages = seriesOf<PageRow>(pages.data);
97
+ const total = points.reduce((sum, p) => sum + (p[metric] ?? 0), 0);
98
+ const now = activeNow(live.data);
99
+
100
+ return (
101
+ <div className={`space-y-4 ${className ?? ""}`}>
102
+ <div className="flex flex-wrap items-center justify-between gap-2">
103
+ <div className="inline-flex gap-1" role="group" aria-label="Metric">
104
+ {METRICS.map((m) => (
105
+ <Button
106
+ key={m.key}
107
+ size="sm"
108
+ variant={metric === m.key ? "default" : "outline"}
109
+ aria-pressed={metric === m.key}
110
+ onClick={() => setMetric(m.key)}
111
+ >
112
+ {m.label}
113
+ </Button>
114
+ ))}
115
+ </div>
116
+ <p className="text-sm text-gray-600" aria-live="polite">
117
+ {now != null ? `${now} on the site now` : ""}
118
+ </p>
119
+ </div>
120
+
121
+ {trend.error ? (
122
+ <ApiError error={trend.error} onRetry={() => void trend.refetch()} />
123
+ ) : trend.loading && !trend.data ? (
124
+ <Skeleton className="h-40 w-full" />
125
+ ) : points.length === 0 ? (
126
+ <EmptyState title="No traffic yet" description="Install the snippet and visits will chart here." />
127
+ ) : (
128
+ <div>
129
+ <p className="text-sm text-gray-600">
130
+ {total.toLocaleString()} {METRICS.find((m) => m.key === metric)?.label.toLowerCase()} in {days} days
131
+ </p>
132
+ <LineChart points={points} metric={metric} />
133
+ </div>
134
+ )}
135
+
136
+ {topPages.length > 0 ? (
137
+ <table className="w-full text-sm">
138
+ <thead>
139
+ <tr className="text-left text-gray-500">
140
+ <th className="py-1">Top pages</th>
141
+ <th className="py-1 text-right">Views</th>
142
+ </tr>
143
+ </thead>
144
+ <tbody>
145
+ {topPages.map((p, i) => (
146
+ <tr key={i} className="border-t">
147
+ <td className="py-1">{p.page_path ?? p.path ?? p.url ?? p.page_title ?? "-"}</td>
148
+ <td className="py-1 text-right">{(p.views ?? p.page_views ?? 0).toLocaleString()}</td>
149
+ </tr>
150
+ ))}
151
+ </tbody>
152
+ </table>
153
+ ) : null}
154
+ </div>
155
+ );
156
+ }
@@ -0,0 +1,92 @@
1
+ "use client";
2
+
3
+ /**
4
+ * VisitorCompanyCard: one company that visited your site, with the pages it viewed and, on request,
5
+ * its full journey.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. listCompanyVisitorEvents GET /visitors/companies/events (read; only when "Full journey" is clicked,
9
+ * with filter={"company_id": <id>} and show_mode=raw)
10
+ *
11
+ * Tiers: read only; no confirmation. Pass a grouped visit from `VisitorFeed` (or any
12
+ * `/visitors/companies/events?show_mode=grouped` row) as `visit`. `actions` slot: put your own buttons
13
+ * ("Add to list", "Open company") next to the name.
14
+ */
15
+
16
+ import { useState } from "react";
17
+ import type { ReactNode } from "react";
18
+ import { Button, Card, CardContent, CardHeader, CardTitle, Skeleton } from "@graph8/sdk/ui";
19
+ import { g8Get } from "@/g8/lib/g8-client";
20
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
21
+ import { ApiError } from "@/g8/components/api-error";
22
+ import { companyLabel, pageLabel, pageOf, timeAgo } from "../lib/visitors-utils";
23
+ import type { FlowEvent, VisitorGroup } from "../lib/visitors-utils";
24
+
25
+ export interface VisitorCompanyCardProps {
26
+ visit: VisitorGroup;
27
+ /** Pages shown before "Full journey". Default 5. */
28
+ previewPages?: number;
29
+ actions?: ReactNode;
30
+ className?: string;
31
+ }
32
+
33
+ function PageList({ pages }: { pages: FlowEvent[] }) {
34
+ if (pages.length === 0) return <p className="text-sm text-gray-500">No page views recorded.</p>;
35
+ return (
36
+ <ol className="space-y-1 text-sm">
37
+ {pages.map((p, i) => (
38
+ <li key={p.message_id ?? `${p.received_at}-${i}`} className="flex justify-between gap-3">
39
+ <span className="truncate">
40
+ {pageLabel(p)}
41
+ {p.page_referrer ? <span className="ml-2 text-xs text-gray-400">from {p.page_referrer}</span> : null}
42
+ </span>
43
+ <span className="shrink-0 text-xs text-gray-500">{timeAgo(p.received_at)}</span>
44
+ </li>
45
+ ))}
46
+ </ol>
47
+ );
48
+ }
49
+
50
+ function Journey({ companyId }: { companyId: number }) {
51
+ const journey = useG8Query(
52
+ (signal) =>
53
+ g8Get("/visitors/companies/events", { show_mode: "raw", filter: JSON.stringify({ company_id: companyId }), limit: 100 }, { signal }),
54
+ [companyId],
55
+ );
56
+ if (journey.loading) return <Skeleton className="h-16 w-full" />;
57
+ if (journey.error) return <ApiError error={journey.error} onRetry={() => void journey.refetch()} />;
58
+ // Raw mode returns one row per page view.
59
+ const views = pageOf(journey.data).events as unknown as FlowEvent[];
60
+ return <PageList pages={views} />;
61
+ }
62
+
63
+ export function VisitorCompanyCard({ visit, previewPages = 5, actions, className }: VisitorCompanyCardProps) {
64
+ const [showJourney, setShowJourney] = useState(false);
65
+ const pages = visit.children ?? [];
66
+ return (
67
+ <Card className={className} data-testid="visitor-company-card">
68
+ <CardHeader className="flex flex-row items-start justify-between gap-3">
69
+ <div>
70
+ <CardTitle className="text-base">{companyLabel(visit)}</CardTitle>
71
+ <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"}
74
+ </p>
75
+ </div>
76
+ {actions ? <div className="flex gap-2">{actions}</div> : null}
77
+ </CardHeader>
78
+ <CardContent className="space-y-3">
79
+ {showJourney && visit.COMPANY_ID != null ? (
80
+ <Journey companyId={visit.COMPANY_ID} />
81
+ ) : (
82
+ <PageList pages={pages.slice(0, previewPages)} />
83
+ )}
84
+ {visit.COMPANY_ID != null ? (
85
+ <Button variant="link" size="sm" className="px-0" onClick={() => setShowJourney((v) => !v)}>
86
+ {showJourney ? "Show summary" : "Full journey"}
87
+ </Button>
88
+ ) : null}
89
+ </CardContent>
90
+ </Card>
91
+ );
92
+ }