@graph8/sdk 0.362.0 → 0.373.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (615) hide show
  1. package/README.md +33 -10
  2. package/blocks/_foundation/README.md +78 -1
  3. package/blocks/_foundation/block.json +123 -6
  4. package/blocks/_foundation/components/api-error.tsx +9 -7
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  7. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  8. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  9. package/blocks/_foundation/components/empty-state.tsx +5 -2
  10. package/blocks/_foundation/components/list-select.tsx +112 -0
  11. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  12. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  13. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  14. package/blocks/_foundation/components/screen/index.ts +2 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  17. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  18. package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
  19. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  20. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  21. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  22. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  23. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  24. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  25. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  26. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  27. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  28. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  29. package/blocks/_foundation/lib/g8-client.ts +1 -1
  30. package/blocks/_foundation/lib/paging.ts +57 -0
  31. package/blocks/_foundation/lib/plural.ts +23 -0
  32. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  33. package/blocks/_foundation/server/connect-route.ts +15 -0
  34. package/blocks/_foundation/server/connect.ts +127 -0
  35. package/blocks/_foundation/server/local-key.ts +70 -0
  36. package/blocks/_foundation/server/proxy.ts +103 -3
  37. package/blocks/_foundation/server/route.ts +16 -4
  38. package/blocks/agents/README.md +93 -2
  39. package/blocks/agents/block.json +343 -2
  40. package/blocks/agents/components/agent-activity.tsx +112 -0
  41. package/blocks/agents/components/agent-approvals.tsx +135 -0
  42. package/blocks/agents/components/agent-builder.tsx +29 -10
  43. package/blocks/agents/components/agent-conversations.tsx +143 -0
  44. package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
  45. package/blocks/agents/components/agent-operators.tsx +164 -0
  46. package/blocks/agents/components/agent-run-detail.tsx +119 -0
  47. package/blocks/agents/components/agent-runs.tsx +19 -10
  48. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  49. package/blocks/agents/components/agent-test-history.tsx +121 -0
  50. package/blocks/agents/components/agents-screen.tsx +226 -0
  51. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  52. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  53. package/blocks/agents/components/from-template-button.tsx +65 -0
  54. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  55. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  56. package/blocks/agents/components/knowledge-form.tsx +69 -0
  57. package/blocks/agents/components/name-dialog.tsx +32 -0
  58. package/blocks/agents/components/receptionist-button.tsx +46 -0
  59. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  60. package/blocks/agents/components/skill-editor.tsx +147 -0
  61. package/blocks/agents/components/skill-library.tsx +111 -0
  62. package/blocks/agents/lib/agent-types.ts +7 -1
  63. package/blocks/agents/lib/facts.tsx +13 -0
  64. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  65. package/blocks/agents/lib/load-more.tsx +141 -0
  66. package/blocks/agents/lib/skill-utils.ts +91 -0
  67. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  68. package/blocks/analytics/README.md +13 -0
  69. package/blocks/analytics/block.json +45 -1
  70. package/blocks/analytics/components/analytics-screen.tsx +222 -0
  71. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  72. package/blocks/analytics/components/leaderboard.tsx +14 -7
  73. package/blocks/analytics/components/report-builder.tsx +38 -11
  74. package/blocks/analytics/components/report-detail.tsx +84 -0
  75. package/blocks/analytics/components/trend-chart.tsx +62 -0
  76. package/blocks/appointments/README.md +6 -0
  77. package/blocks/appointments/block.json +33 -2
  78. package/blocks/appointments/components/availability-editor.tsx +17 -5
  79. package/blocks/appointments/components/booking-page.tsx +2 -2
  80. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  81. package/blocks/appointments/components/event-type-related.tsx +4 -2
  82. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  83. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  84. package/blocks/appointments/components/meeting-list.tsx +38 -18
  85. package/blocks/appointments/components/meetings-screen.tsx +7 -4
  86. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  87. package/blocks/appointments/components/scheduling-screen.tsx +2 -1
  88. package/blocks/block.schema.json +18 -0
  89. package/blocks/block.types.ts +17 -0
  90. package/blocks/campaigns/README.md +41 -11
  91. package/blocks/campaigns/block.json +105 -42
  92. package/blocks/campaigns/components/campaign-assets.tsx +17 -6
  93. package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
  94. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  95. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  96. package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
  97. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  98. package/blocks/campaigns/components/icp-detail.tsx +163 -0
  99. package/blocks/campaigns/components/icp-editor.tsx +22 -17
  100. package/blocks/campaigns/components/persona-detail.tsx +181 -0
  101. package/blocks/campaigns/components/persona-editor.tsx +19 -11
  102. package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
  103. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  104. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  105. package/blocks/coach/README.md +10 -0
  106. package/blocks/coach/block.json +16 -1
  107. package/blocks/coach/components/call-scorecard.tsx +7 -7
  108. package/blocks/coach/components/coach-screen.tsx +222 -0
  109. package/blocks/company-intelligence/README.md +132 -0
  110. package/blocks/company-intelligence/block.json +318 -0
  111. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  112. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  113. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  114. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  115. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  116. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  117. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  118. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  119. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  120. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  121. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  122. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  123. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  124. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  125. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  126. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  127. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  128. package/blocks/copilot/README.md +13 -0
  129. package/blocks/copilot/block.json +14 -1
  130. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  131. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  132. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  133. package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
  134. package/blocks/crm/README.md +34 -0
  135. package/blocks/crm/block.json +107 -2
  136. package/blocks/crm/components/companies-screen.tsx +359 -0
  137. package/blocks/crm/components/company-detail.tsx +10 -8
  138. package/blocks/crm/components/company-table.tsx +11 -4
  139. package/blocks/crm/components/contact-detail.tsx +10 -7
  140. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  141. package/blocks/crm/components/contact-table.tsx +10 -3
  142. package/blocks/crm/components/contacts-screen.tsx +377 -0
  143. package/blocks/crm/components/dedupe-review.tsx +13 -6
  144. package/blocks/crm/components/import-wizard.tsx +7 -9
  145. package/blocks/crm/components/list-select.tsx +20 -0
  146. package/blocks/crm/components/notes-panel.tsx +4 -4
  147. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  148. package/blocks/crm/components/record-form.tsx +6 -4
  149. package/blocks/crm/components/task-list.tsx +4 -4
  150. package/blocks/crm/components/tasks-screen.tsx +399 -0
  151. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  152. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  153. package/blocks/crm/hooks/use-page-query.ts +9 -1
  154. package/blocks/crm/lib/crm-types.ts +26 -2
  155. package/blocks/crm/lib/read-lists.ts +16 -0
  156. package/blocks/crm/lib/screen-parts.tsx +105 -0
  157. package/blocks/deals/README.md +24 -6
  158. package/blocks/deals/block.json +160 -5
  159. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  160. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  161. package/blocks/deals/components/deal-detail.tsx +25 -13
  162. package/blocks/deals/components/deal-filters.tsx +65 -0
  163. package/blocks/deals/components/deal-line-items.tsx +120 -0
  164. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  165. package/blocks/deals/components/deal-related.tsx +31 -10
  166. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  167. package/blocks/deals/components/deals-screen.tsx +66 -26
  168. package/blocks/deals/components/forecast-view.tsx +4 -4
  169. package/blocks/deals/components/pipeline-board.tsx +8 -6
  170. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  171. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  172. package/blocks/deals/components/stage-editor.tsx +5 -5
  173. package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
  174. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  175. package/blocks/deals/lib/deal-extras.ts +156 -0
  176. package/blocks/dialer/README.md +45 -4
  177. package/blocks/dialer/block.json +84 -3
  178. package/blocks/dialer/components/call-contact-button.tsx +269 -0
  179. package/blocks/dialer/components/call-log-screen.tsx +208 -0
  180. package/blocks/dialer/components/call-log.tsx +23 -7
  181. package/blocks/dialer/components/call-queue.tsx +4 -4
  182. package/blocks/dialer/components/dialer-screen.tsx +361 -0
  183. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  184. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  185. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  186. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  187. package/blocks/dialer/components/session-start-form.tsx +61 -12
  188. package/blocks/dialer/components/softphone.tsx +4 -2
  189. package/blocks/enrichment/README.md +29 -7
  190. package/blocks/enrichment/block.json +49 -9
  191. package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
  192. package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
  193. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  194. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  195. package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
  196. package/blocks/enrichment/components/waterfall-config.tsx +66 -17
  197. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  198. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  199. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  200. package/blocks/forms/README.md +8 -0
  201. package/blocks/forms/block.json +139 -18
  202. package/blocks/forms/components/form-activity.tsx +71 -0
  203. package/blocks/forms/components/form-backfill.tsx +108 -0
  204. package/blocks/forms/components/form-builder.tsx +4 -2
  205. package/blocks/forms/components/form-embed.tsx +4 -2
  206. package/blocks/forms/components/form-groups.tsx +151 -0
  207. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  208. package/blocks/forms/components/form-route-history.tsx +72 -0
  209. package/blocks/forms/components/form-saved-views.tsx +81 -0
  210. package/blocks/forms/components/forms-screen.tsx +56 -19
  211. package/blocks/forms/components/more-rows.tsx +30 -0
  212. package/blocks/forms/components/submissions-export.tsx +67 -0
  213. package/blocks/forms/components/submissions-table.tsx +17 -6
  214. package/blocks/forms/lib/forms-extra.ts +67 -0
  215. package/blocks/inbox/README.md +32 -3
  216. package/blocks/inbox/block.json +29 -2
  217. package/blocks/inbox/components/inbox-list.tsx +7 -3
  218. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  219. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  220. package/blocks/inbox/components/reply-composer.tsx +65 -9
  221. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  222. package/blocks/inbox/components/thread-badge.tsx +2 -2
  223. package/blocks/inbox/components/thread-view.tsx +1 -1
  224. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  225. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  226. package/blocks/inbox/lib/inbox-types.ts +47 -1
  227. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  228. package/blocks/integrations/README.md +63 -1
  229. package/blocks/integrations/block.json +234 -15
  230. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  231. package/blocks/integrations/components/connection-health.tsx +59 -0
  232. package/blocks/integrations/components/crm-detail.tsx +119 -0
  233. package/blocks/integrations/components/crm-records.tsx +103 -0
  234. package/blocks/integrations/components/field-mappings.tsx +61 -0
  235. package/blocks/integrations/components/integration-list.tsx +13 -8
  236. package/blocks/integrations/components/integrations-screen.tsx +152 -0
  237. package/blocks/integrations/components/migration-panel.tsx +133 -0
  238. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  239. package/blocks/integrations/components/sync-status.tsx +11 -9
  240. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  241. package/blocks/integrations/components/webhook-detail.tsx +179 -0
  242. package/blocks/integrations/components/webhook-manager.tsx +43 -25
  243. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  244. package/blocks/integrations/lib/crm-utils.ts +39 -0
  245. package/blocks/integrations/lib/facts.tsx +13 -0
  246. package/blocks/integrations/lib/integrations-utils.ts +11 -1
  247. package/blocks/integrations/lib/load-more.tsx +141 -0
  248. package/blocks/intent/README.md +14 -0
  249. package/blocks/intent/block.json +27 -1
  250. package/blocks/intent/components/account-intent-score.tsx +5 -5
  251. package/blocks/intent/components/intent-feed.tsx +13 -10
  252. package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
  253. package/blocks/intent/components/intent-screen.tsx +265 -0
  254. package/blocks/intent/components/keyword-detail.tsx +124 -0
  255. package/blocks/intent/lib/intent-utils.ts +4 -4
  256. package/blocks/lifecycle/block.json +13 -8
  257. package/blocks/lifecycle/components/account-related.tsx +8 -1
  258. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  259. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  260. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  261. package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
  262. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  263. package/blocks/lists/README.md +20 -1
  264. package/blocks/lists/block.json +69 -3
  265. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  266. package/blocks/lists/components/add-to-list-button.tsx +20 -9
  267. package/blocks/lists/components/list-picker.tsx +17 -3
  268. package/blocks/lists/components/list-table.tsx +12 -6
  269. package/blocks/lists/components/lists-screen.tsx +346 -0
  270. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  271. package/blocks/lists/components/suppression-manager.tsx +33 -8
  272. package/blocks/lists/components/suppressions-screen.tsx +264 -0
  273. package/blocks/mailboxes/README.md +17 -0
  274. package/blocks/mailboxes/block.json +69 -17
  275. package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
  276. package/blocks/mailboxes/components/domain-detail.tsx +148 -0
  277. package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
  278. package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
  279. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  280. package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
  281. package/blocks/mailboxes/components/warmup-health.tsx +24 -22
  282. package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
  283. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  284. package/blocks/newsletter/README.md +12 -0
  285. package/blocks/newsletter/block.json +157 -24
  286. package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
  287. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  288. package/blocks/newsletter/components/more-rows.tsx +30 -0
  289. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  290. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  291. package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
  292. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  293. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  294. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  295. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  296. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  297. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  298. package/blocks/nurture/block.json +28 -14
  299. package/blocks/nurture/components/nurture-builder.tsx +13 -3
  300. package/blocks/nurture/components/nurture-enroll.tsx +12 -10
  301. package/blocks/nurture/components/nurture-form.tsx +27 -10
  302. package/blocks/nurture/components/nurture-screen.tsx +26 -11
  303. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  304. package/blocks/nurture/lib/nurture-types.ts +1 -1
  305. package/blocks/objects/README.md +18 -1
  306. package/blocks/objects/block.json +42 -21
  307. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  308. package/blocks/objects/components/object-not-found.tsx +3 -1
  309. package/blocks/objects/components/object-record-detail.tsx +10 -2
  310. package/blocks/objects/components/object-schema-editor.tsx +4 -2
  311. package/blocks/objects/components/object-table.tsx +13 -3
  312. package/blocks/objects/components/objects-screen.tsx +147 -0
  313. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  314. package/blocks/objects/lib/object-types.ts +1 -1
  315. package/blocks/pages/README.md +14 -0
  316. package/blocks/pages/block.json +140 -23
  317. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  318. package/blocks/pages/components/content-library.tsx +9 -4
  319. package/blocks/pages/components/more-rows.tsx +30 -0
  320. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  321. package/blocks/pages/components/page-analytics.tsx +176 -0
  322. package/blocks/pages/components/page-clone.tsx +99 -0
  323. package/blocks/pages/components/page-domains.tsx +167 -0
  324. package/blocks/pages/components/page-editor.tsx +1 -1
  325. package/blocks/pages/components/page-generation-status.tsx +48 -0
  326. package/blocks/pages/components/page-list.tsx +2 -1
  327. package/blocks/pages/components/page-tracking.tsx +137 -0
  328. package/blocks/pages/components/pages-screen.tsx +57 -16
  329. package/blocks/pages/components/seo-report.tsx +6 -1
  330. package/blocks/pages/lib/studio.ts +7 -0
  331. package/blocks/phone-numbers/README.md +74 -0
  332. package/blocks/phone-numbers/block.json +262 -0
  333. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  334. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  335. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  336. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  337. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  338. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  339. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  340. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  341. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  342. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  343. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  344. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  345. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  346. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  347. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  348. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  349. package/blocks/pickers/README.md +23 -0
  350. package/blocks/pickers/block.json +103 -0
  351. package/blocks/pickers/components/company-picker.tsx +90 -0
  352. package/blocks/pickers/components/contact-picker.tsx +131 -0
  353. package/blocks/pickers/components/member-picker.tsx +99 -0
  354. package/blocks/pickers/components/search-picker.tsx +98 -0
  355. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  356. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  357. package/blocks/pickers/lib/use-debounced.ts +13 -0
  358. package/blocks/quotes/README.md +13 -0
  359. package/blocks/quotes/block.json +65 -13
  360. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  361. package/blocks/quotes/components/product-picker.tsx +3 -1
  362. package/blocks/quotes/components/quote-builder.tsx +1 -1
  363. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  364. package/blocks/quotes/components/quote-list.tsx +5 -3
  365. package/blocks/quotes/components/quote-related.tsx +8 -4
  366. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  367. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  368. package/blocks/radar/README.md +10 -0
  369. package/blocks/radar/block.json +41 -1
  370. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  371. package/blocks/radar/components/competitor-detail.tsx +117 -0
  372. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  373. package/blocks/radar/components/radar-overview.tsx +4 -4
  374. package/blocks/radar/components/radar-report.tsx +4 -2
  375. package/blocks/radar/components/radar-screen.tsx +214 -0
  376. package/blocks/radar/components/report-detail.tsx +104 -0
  377. package/blocks/search/README.md +10 -0
  378. package/blocks/search/block.json +16 -1
  379. package/blocks/search/components/filter-builder.tsx +3 -3
  380. package/blocks/search/components/prospect-results.tsx +5 -3
  381. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  382. package/blocks/search/components/saved-searches.tsx +4 -1
  383. package/blocks/search/hooks/use-filter-options.ts +20 -6
  384. package/blocks/search/lib/search-types.ts +2 -0
  385. package/blocks/sequencer/README.md +55 -9
  386. package/blocks/sequencer/block.json +126 -19
  387. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  388. package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
  389. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  390. package/blocks/sequencer/components/sequence-builder.tsx +224 -60
  391. package/blocks/sequencer/components/sequence-detail.tsx +261 -0
  392. package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
  393. package/blocks/sequencer/components/sequence-list.tsx +34 -9
  394. package/blocks/sequencer/components/sequence-stats.tsx +11 -10
  395. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  396. package/blocks/sequencer/components/sequences-screen.tsx +162 -0
  397. package/blocks/sequencer/components/step-editor.tsx +14 -12
  398. package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
  399. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  400. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  401. package/blocks/social-ads/README.md +19 -0
  402. package/blocks/social-ads/block.json +233 -1
  403. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  404. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  405. package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
  406. package/blocks/social-ads/components/audience-sync.tsx +17 -9
  407. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  408. package/blocks/social-ads/components/campaign-detail.tsx +138 -0
  409. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  410. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  411. package/blocks/social-ads/components/creative-detail.tsx +155 -0
  412. package/blocks/social-ads/components/creative-gallery.tsx +35 -7
  413. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  414. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  415. package/blocks/social-ads/components/post-composer.tsx +7 -3
  416. package/blocks/social-ads/components/post-detail.tsx +163 -0
  417. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  418. package/blocks/social-ads/components/social-performance.tsx +80 -0
  419. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  420. package/blocks/social-ads/components/social-screen.tsx +187 -0
  421. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  422. package/blocks/surveys/README.md +91 -0
  423. package/blocks/surveys/block.json +299 -0
  424. package/blocks/surveys/components/account-nps.tsx +69 -0
  425. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  426. package/blocks/surveys/components/org-summary.tsx +82 -0
  427. package/blocks/surveys/components/sending-domains.tsx +113 -0
  428. package/blocks/surveys/components/survey-form.tsx +126 -0
  429. package/blocks/surveys/components/survey-responses.tsx +145 -0
  430. package/blocks/surveys/components/survey-results.tsx +72 -0
  431. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  432. package/blocks/surveys/components/survey-send.tsx +162 -0
  433. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  434. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  435. package/blocks/surveys/lib/surveys-types.ts +228 -0
  436. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  437. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  438. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  439. package/blocks/visitors/README.md +10 -0
  440. package/blocks/visitors/block.json +25 -1
  441. package/blocks/visitors/components/snippet-install.tsx +21 -10
  442. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  443. package/blocks/visitors/components/visitor-company-card.tsx +26 -16
  444. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  445. package/blocks/visitors/components/visitor-feed.tsx +16 -7
  446. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  447. package/blocks/webchat/README.md +10 -0
  448. package/blocks/webchat/block.json +21 -7
  449. package/blocks/webchat/components/chat-widget-config.tsx +8 -5
  450. package/blocks/webchat/components/conversation-list.tsx +8 -4
  451. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  452. package/blocks/workbench/README.md +11 -0
  453. package/blocks/workbench/block.json +48 -1
  454. package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
  455. package/blocks/workbench/components/run-progress.tsx +1 -1
  456. package/blocks/workbench/components/workbench-grid.tsx +10 -4
  457. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  458. package/blocks/workbench/components/workbench-screen.tsx +202 -0
  459. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  460. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  461. package/blocks/workflows/README.md +13 -0
  462. package/blocks/workflows/block.json +28 -9
  463. package/blocks/workflows/components/run-history.tsx +31 -9
  464. package/blocks/workflows/components/trigger-config.tsx +3 -1
  465. package/blocks/workflows/components/workflow-editor.tsx +4 -2
  466. package/blocks/workflows/components/workflow-list.tsx +13 -6
  467. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  468. package/dist/cli.js +3109 -1977
  469. package/dist/errors.d.mts +34 -0
  470. package/dist/errors.d.ts +34 -0
  471. package/dist/errors.js +46 -0
  472. package/dist/errors.js.map +1 -0
  473. package/dist/errors.mjs +21 -0
  474. package/dist/errors.mjs.map +1 -0
  475. package/dist/index.browser.js +11 -5
  476. package/dist/index.browser.js.map +1 -1
  477. package/dist/index.browser.mjs +11 -5
  478. package/dist/index.browser.mjs.map +1 -1
  479. package/dist/index.d.mts +164 -25
  480. package/dist/index.d.ts +164 -25
  481. package/dist/index.js +11 -5
  482. package/dist/index.js.map +1 -1
  483. package/dist/index.mjs +11 -5
  484. package/dist/index.mjs.map +1 -1
  485. package/dist/react.browser.js +10 -4
  486. package/dist/react.browser.js.map +1 -1
  487. package/dist/react.browser.mjs +10 -4
  488. package/dist/react.browser.mjs.map +1 -1
  489. package/dist/react.d.mts +142 -5
  490. package/dist/react.d.ts +142 -5
  491. package/dist/react.js +10 -4
  492. package/dist/react.js.map +1 -1
  493. package/dist/react.mjs +10 -4
  494. package/dist/react.mjs.map +1 -1
  495. package/dist/types/blocks/add.d.ts +11 -4
  496. package/dist/types/blocks/brand.d.ts +122 -0
  497. package/dist/types/blocks/compose.d.ts +53 -0
  498. package/dist/types/blocks/create.d.ts +27 -20
  499. package/dist/types/blocks/doctor.d.ts +6 -1
  500. package/dist/types/blocks/flows.d.ts +7 -1
  501. package/dist/types/blocks/navfile.d.ts +35 -0
  502. package/dist/types/blocks/orgBrand.d.ts +34 -0
  503. package/dist/types/blocks/paths.d.ts +4 -0
  504. package/dist/types/blocks/prefixes.d.ts +37 -0
  505. package/dist/types/blocks/registry.d.ts +17 -0
  506. package/dist/types/blocks/screens.d.ts +53 -0
  507. package/dist/types/cli.d.ts +5 -0
  508. package/dist/types/errors.d.ts +33 -0
  509. package/dist/types/http.d.ts +2 -18
  510. package/dist/types/version.d.ts +2 -2
  511. package/docs/agents.md +15 -4
  512. package/docs/api/README.md +2 -2
  513. package/docs/api/by-path.md +1 -0
  514. package/docs/api/desk.d.ts +98 -2
  515. package/docs/api/desk.md +123 -1
  516. package/docs/api/inbox.d.ts +10 -2
  517. package/docs/api/inbox.md +10 -2
  518. package/docs/api/seo.d.ts +19 -1
  519. package/docs/api/seo.md +24 -1
  520. package/package.json +16 -2
  521. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  522. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  523. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  524. package/stacks/6sense.json +55 -17
  525. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  526. package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
  527. package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
  528. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  529. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  530. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  531. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  532. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  533. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  534. package/stacks/apollo.json +147 -37
  535. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  536. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  537. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  538. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  539. package/stacks/calendly.json +64 -0
  540. package/stacks/clay/pages/companies.tsx.tmpl +31 -0
  541. package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
  542. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  543. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  544. package/stacks/clay.json +73 -27
  545. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  546. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  547. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  548. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  549. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  550. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  551. package/stacks/gong.json +98 -35
  552. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  553. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  554. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  555. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  556. package/stacks/hootsuite.json +104 -0
  557. package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
  558. package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
  559. package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
  560. package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
  561. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  562. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  563. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  564. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  565. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  566. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  567. package/stacks/hubspot.json +181 -64
  568. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  569. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  570. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  571. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  572. package/stacks/intercom.json +52 -20
  573. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  574. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  575. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  576. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  577. package/stacks/mailchimp.json +68 -34
  578. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  579. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  580. package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
  581. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  582. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  583. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  584. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  585. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  586. package/stacks/outreach.json +106 -25
  587. package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
  588. package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
  589. package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
  590. package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
  591. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  592. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  593. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  594. package/stacks/pipedrive.json +117 -0
  595. package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
  596. package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
  597. package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
  598. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  599. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  600. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  601. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  602. package/stacks/salesforce.json +85 -31
  603. package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
  604. package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
  605. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  606. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  607. package/stacks/zoominfo.json +115 -0
  608. package/styles/styles.css +1 -1
  609. package/templates/next-app/README.md.tmpl +40 -9
  610. package/templates/next-app/app/globals.css.tmpl +349 -64
  611. package/templates/next-app/app/layout.tsx.tmpl +7 -2
  612. package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
  613. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
  614. package/templates/next-app/dot-gitignore.tmpl +2 -0
  615. package/templates/next-app/package.json.tmpl +1 -1
@@ -0,0 +1,234 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SurveysScreen: the finished NPS and CSAT workspace. No props needed.
5
+ *
6
+ * Composes: ScreenLayout (title, tabs, stats), Toolbar (search, type, status and channel filters), SplitView + DetailPanel
7
+ * (the selected survey, deep link `?id=`), FormSheet (new survey via SurveyForm), SurveyResults, SurveyResponses, SurveySend,
8
+ * SurveyTriggers, SurveyForm in edit mode, OrgSummary, SendingDomains, AccountNps and ContactSurveyHistory.
9
+ *
10
+ * Tabs (`?tab=`): Surveys (list and detail), Overview (org-wide scores), Sending domains, Accounts and people.
11
+ * A survey's detail (`?id=`) has Results, Responses, Send, Automatic sends, Settings and Details; `&view=full` shows it as a page.
12
+ *
13
+ * User flows: see every survey with its type, state and channel; search and filter; create an NPS or CSAT survey; open one to read
14
+ * its score, response rate, split and trend; read, search, filter and export its answers (CSV); send it to a list now or later
15
+ * (confirmed with the audience size); add or remove automatic sends; edit its question, thank-you line and channel; close it
16
+ * (stops new sends, keeps every answer) and reopen it; delete it (confirmed); verify a sending domain; read org-wide NPS and CSAT;
17
+ * look up one company's NPS and one person's answers. The API has no share link, embed code, AI question writer or question
18
+ * list: a survey is one question (see the README for the gaps).
19
+ *
20
+ * SDK operations: listSurveys, updateSurvey (close and reopen), deleteSurvey (destructive, confirmed); everything else lives in
21
+ * the composed parts, which confirm every send, domain verification and removal.
22
+ */
23
+
24
+ import { useState } from "react";
25
+ import type { ReactNode } from "react";
26
+ import { ClipboardCheck, Lock, LockOpen, Plus, Trash2 } from "lucide-react";
27
+ import { Button, Skeleton } from "@graph8/sdk/ui";
28
+ import { ApiError } from "@/g8/components/api-error";
29
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
30
+ import { EmptyState } from "@/g8/components/empty-state";
31
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
32
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
33
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
34
+ import { ScreenLayout } from "@/g8/components/screen/screen-layout";
35
+ import { SplitView } from "@/g8/components/screen/split-view";
36
+ import { StatusPill } from "@/g8/components/screen/status-pill";
37
+ import { Toolbar } from "@/g8/components/screen/toolbar";
38
+ import { useUrlState } from "@/g8/hooks/use-url-state";
39
+ import { g8Delete, g8Get, g8Patch } from "@/g8/lib/g8-client";
40
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
41
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
42
+ import { plural } from "@/g8/lib/plural";
43
+ import { COMMENT_MODES, channelLabel, isNps, statusOf, typeLabel, when } from "../lib/surveys-types";
44
+ import type { ResponseRow, Survey } from "../lib/surveys-types";
45
+ import { AccountNps } from "./account-nps";
46
+ import { ContactSurveyHistory } from "./contact-survey-history";
47
+ import { OrgSummary } from "./org-summary";
48
+ import { SendingDomains } from "./sending-domains";
49
+ import { SurveyForm } from "./survey-form";
50
+ import { SurveyResponses } from "./survey-responses";
51
+ import { SurveyResults } from "./survey-results";
52
+ import { SurveySend } from "./survey-send";
53
+ import { SurveyTriggers } from "./survey-triggers";
54
+
55
+ export interface SurveysScreenProps {
56
+ renderRowActions?: (survey: Survey) => ReactNode;
57
+ renderDetailActions?: (survey: Survey) => ReactNode;
58
+ /** Render a link to the person behind an answer (for example to your contact page). */
59
+ renderContact?: (row: ResponseRow) => ReactNode;
60
+ extraDetailTabs?: { id: string; label: string; render: (survey: Survey) => ReactNode }[];
61
+ toolbarActions?: ReactNode;
62
+ className?: string;
63
+ }
64
+
65
+ type Tab = "surveys" | "overview" | "domains" | "accounts";
66
+ const TABS: Tab[] = ["surveys", "overview", "domains", "accounts"];
67
+ const SELECT = "border-input bg-background h-9 rounded-md border px-2 text-sm";
68
+
69
+ export function SurveysScreen({ renderRowActions, renderDetailActions, renderContact, extraDetailTabs, toolbarActions, className }: SurveysScreenProps) {
70
+ const [id, setId] = useUrlState("id", "");
71
+ const [view, setView] = useUrlState("view", "");
72
+ const [tabParam, setTabParam] = useUrlState("tab", "surveys");
73
+ const tab = (TABS.includes(tabParam as Tab) ? tabParam : "surveys") as Tab;
74
+ const expandProps = { onExpand: () => setView("full"), onCollapse: view === "full" ? () => setView(null) : undefined };
75
+ const [search, setSearch] = useState("");
76
+ const [typeFilter, setTypeFilter] = useState("");
77
+ const [statusFilter, setStatusFilter] = useState("");
78
+ const [channelFilter, setChannelFilter] = useState("");
79
+ const [creating, setCreating] = useState(false);
80
+ const [toDelete, setToDelete] = useState<Survey | null>(null);
81
+ const [busy, setBusy] = useState<string | null>(null);
82
+
83
+ const list = useG8Query((signal) => g8Get<Survey[]>("/feedback/surveys", {}, { signal }).then((r) => r.data ?? []), [], { key: ["surveys", "list"] });
84
+ const toggle = useG8Mutation((s: Survey) => g8Patch<Survey>(`/feedback/surveys/${encodeURIComponent(s.id)}`, { is_active: !s.is_active }), {
85
+ invalidates: (_r, s) => [["surveys", "list"], ["surveys", "detail", s.id]],
86
+ });
87
+ const remove = useG8Mutation((s: Survey) => g8Delete(`/feedback/surveys/${encodeURIComponent(s.id)}`), {
88
+ onSuccess: (_r, s) => { setToDelete(null); if (s.id === id) { setId(null); setView(null); } },
89
+ invalidates: [["surveys"]],
90
+ });
91
+ const flip = async (s: Survey): Promise<void> => {
92
+ setBusy(s.id);
93
+ await toggle.mutate(s);
94
+ setBusy(null);
95
+ };
96
+
97
+ const all = list.data ?? [];
98
+ const q = search.trim().toLowerCase();
99
+ const shown = all.filter((s) => (!typeFilter || s.type === typeFilter) && (!statusFilter || statusOf(s) === statusFilter) && (!channelFilter || s.delivery_channel === channelFilter) && (!q || `${s.name} ${s.question_text}`.toLowerCase().includes(q)));
100
+ const selected = all.find((s) => s.id === id);
101
+ const closeDetail = (): void => { setId(null); setView(null); };
102
+
103
+ const rows = list.error ? <ApiError error={list.error} title="Could not load surveys" onRetry={() => void list.refetch()} /> : list.loading && !list.data ? (
104
+ <div aria-busy="true" className="grid gap-2"><Skeleton className="h-16 w-full" /><Skeleton className="h-16 w-full" /><Skeleton className="h-16 w-full" /></div>
105
+ ) : shown.length === 0 ? (
106
+ <EmptyState icon={<ClipboardCheck className="size-8" />} title={all.length === 0 ? "No surveys yet" : "No surveys match"} description={all.length === 0 ? "Ask customers how likely they are to recommend you (NPS) or how satisfied they are (CSAT), and read the answers here." : "Try another search or filter."} action={all.length === 0 ? <Button onClick={() => setCreating(true)}>Create your first survey</Button> : undefined} />
107
+ ) : (
108
+ <>
109
+ <p role="status" className="text-muted-foreground mb-2 text-xs">{shown.length === all.length ? `All ${plural(all.length, "survey")}` : `${shown.length.toLocaleString()} of ${plural(all.length, "survey")}`}</p>
110
+ <ul className="grid gap-2">
111
+ {shown.map((s) => (
112
+ <li key={s.id} className={`flex flex-wrap items-center justify-between gap-2 rounded-md border p-3 ${s.id === id ? "bg-muted" : ""}`}>
113
+ <button type="button" className="min-w-0 flex-1 text-left" onClick={() => setId(s.id)}>
114
+ <span className="block truncate font-medium">{s.name}</span>
115
+ <span className="text-muted-foreground block truncate text-xs">{s.question_text} · {channelLabel(s.delivery_channel)}</span>
116
+ </button>
117
+ <StatusPill tone="info">{typeLabel(s)}</StatusPill>
118
+ <StatusPill tone={s.is_active ? "positive" : "neutral"}>{statusOf(s)}</StatusPill>
119
+ <div className="flex gap-1">
120
+ <Button size="sm" variant="ghost" aria-label={`${s.is_active ? "Close" : "Reopen"} ${s.name}`} loading={busy === s.id} disabled={busy !== null} onClick={() => void flip(s)}>{s.is_active ? <Lock className="size-4" /> : <LockOpen className="size-4" />}</Button>
121
+ <Button size="sm" variant="ghost" aria-label={`Delete ${s.name}`} onClick={() => setToDelete(s)}><Trash2 className="size-4" /></Button>
122
+ {renderRowActions?.(s)}
123
+ </div>
124
+ </li>
125
+ ))}
126
+ </ul>
127
+ </>
128
+ );
129
+
130
+ const detail = selected ? (
131
+ <DetailPanel
132
+ title={selected.name}
133
+ subtitle={selected.question_text}
134
+ badges={<><StatusPill tone="info">{typeLabel(selected)}</StatusPill><StatusPill tone={selected.is_active ? "positive" : "neutral"}>{statusOf(selected)}</StatusPill></>}
135
+ actions={<>{renderDetailActions?.(selected)}<Button size="sm" variant="outline" loading={busy === selected.id} disabled={busy !== null} onClick={() => void flip(selected)}>{selected.is_active ? <Lock className="size-4" /> : <LockOpen className="size-4" />}{selected.is_active ? "Close survey" : "Reopen survey"}</Button></>}
136
+ onClose={closeDetail}
137
+ {...expandProps}
138
+ tabs={[
139
+ { id: "results", label: "Results", content: <SurveyResults key={selected.id} survey={selected} /> },
140
+ { id: "responses", label: "Responses", content: <SurveyResponses key={selected.id} survey={selected} renderContact={renderContact} /> },
141
+ { id: "send", label: "Send", content: <SurveySend key={selected.id} survey={selected} /> },
142
+ { id: "automatic", label: "Automatic sends", content: <SurveyTriggers key={selected.id} survey={selected} /> },
143
+ { id: "settings", label: "Settings", content: <SurveyForm key={selected.id} survey={selected} onSaved={() => void list.refetch()} /> },
144
+ { id: "details", label: "Details", content: <SurveyDetails survey={selected} /> },
145
+ ...(extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(selected) })),
146
+ ]}
147
+ />
148
+ ) : null;
149
+
150
+ const surveysTab = (
151
+ <SplitView
152
+ list={(
153
+ <div className="grid gap-3">
154
+ <Toolbar
155
+ search={{ value: search, onChange: setSearch, placeholder: "Search surveys" }}
156
+ filters={(
157
+ <>
158
+ <select aria-label="Filter by type" className={SELECT} value={typeFilter} onChange={(e) => setTypeFilter(e.target.value)}><option value="">All types</option><option value="nps">NPS</option><option value="csat">CSAT</option></select>
159
+ <select aria-label="Filter by status" className={SELECT} value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}><option value="">Open and closed</option><option value="open">Open</option><option value="closed">Closed</option></select>
160
+ <select aria-label="Filter by channel" className={SELECT} value={channelFilter} onChange={(e) => setChannelFilter(e.target.value)}><option value="">All channels</option><option value="email">Email</option><option value="bridge">Chat message</option><option value="in_app">In-app</option></select>
161
+ </>
162
+ )}
163
+ actions={toolbarActions}
164
+ />
165
+ {rows}
166
+ </div>
167
+ )}
168
+ detail={detail}
169
+ detailOpen={Boolean(selected)}
170
+ onCloseDetail={closeDetail}
171
+ expanded={view === "full"}
172
+ onExpandedChange={(e) => setView(e ? "full" : null)}
173
+ detailWidth="xl"
174
+ />
175
+ );
176
+
177
+ return (
178
+ <ScreenLayout
179
+ className={className}
180
+ title="Surveys"
181
+ description="Ask customers how likely they are to recommend you (NPS) or how satisfied they are (CSAT)."
182
+ icon={<ClipboardCheck className="size-5" />}
183
+ actions={<Button size="sm" onClick={() => setCreating(true)}><Plus className="size-4" />New survey</Button>}
184
+ tabs={[{ id: "surveys", label: "Surveys", count: list.data ? all.length : undefined }, { id: "overview", label: "Overview" }, { id: "domains", label: "Sending domains" }, { id: "accounts", label: "Accounts and people" }]}
185
+ activeTab={tab}
186
+ onTabChange={(next) => { setTabParam(next === "surveys" ? null : next); closeDetail(); }}
187
+ stats={tab === "surveys" ? [
188
+ { label: "Surveys", value: list.data ? all.length.toLocaleString() : "..." },
189
+ { label: "Open", value: all.filter((s) => s.is_active).length.toLocaleString(), tone: "positive" },
190
+ { label: "Closed", value: all.filter((s) => !s.is_active).length.toLocaleString() },
191
+ { label: "NPS / CSAT", value: `${all.filter((s) => isNps(s)).length} / ${all.filter((s) => !isNps(s)).length}` },
192
+ ] : undefined}
193
+ >
194
+ {tab === "surveys" ? surveysTab : tab === "overview" ? <OrgSummary onOpenSurvey={(sid) => { setTabParam(null); setId(sid); }} /> : tab === "domains" ? <SendingDomains /> : (
195
+ <div className="grid gap-4 lg:grid-cols-2"><AccountNps /><ContactSurveyHistory /></div>
196
+ )}
197
+ <FormSheet open={creating} onOpenChange={setCreating} title="New survey" description="One question, scored. Nothing is sent until you send it.">
198
+ {creating ? <SurveyForm onCancel={() => setCreating(false)} onSaved={(created) => { setCreating(false); setTabParam(null); void list.refetch(); setId(created.id); }} /> : null}
199
+ </FormSheet>
200
+ <ConfirmSpendDialog
201
+ open={toDelete !== null}
202
+ tier="destructive"
203
+ title={`Delete ${toDelete?.name ?? "this survey"}?`}
204
+ affected={1}
205
+ description={<><p>The survey, its automatic sends and every answer collected are removed. To stop asking but keep the answers, close it instead.</p>{remove.error ? <ApiError error={remove.error} title="Could not delete the survey" /> : null}</>}
206
+ confirmLabel="Delete"
207
+ confirming={remove.loading}
208
+ onCancel={() => setToDelete(null)}
209
+ onConfirm={() => { if (toDelete) void remove.mutate(toDelete); }}
210
+ />
211
+ {toggle.error ? <ApiError error={toggle.error} title="Could not change the survey" /> : null}
212
+ </ScreenLayout>
213
+ );
214
+ }
215
+
216
+ function SurveyDetails({ survey }: { survey: Survey }) {
217
+ return (
218
+ <KeyValueList
219
+ columns={2}
220
+ items={[
221
+ { label: "Name", value: survey.name },
222
+ { label: "Type", value: typeLabel(survey) },
223
+ { label: "Status", value: statusOf(survey) },
224
+ { label: "Question", value: survey.question_text, wide: true },
225
+ { label: "Scale", value: `${survey.scale_min} to ${survey.scale_max}` },
226
+ { label: "Shown through", value: channelLabel(survey.delivery_channel) },
227
+ { label: "Comments", value: COMMENT_MODES.find((m) => m.value === survey.comment_mode)?.label ?? "Score only" },
228
+ { label: "Thank-you line", value: survey.thank_you_copy || "", wide: true },
229
+ { label: "Created", value: when(survey.created_at) },
230
+ { label: "Survey id", value: survey.id, mono: true },
231
+ ]}
232
+ />
233
+ );
234
+ }
@@ -0,0 +1,228 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Types, vocabularies and pure helpers for the surveys block.
5
+ *
6
+ * A graph8 survey is ONE question with a numeric answer (plus an optional comment): NPS (0 to 10) or CSAT (1 to 5).
7
+ * The vocabularies below are copied from the API's request patterns (`^(nps|csat)$`, `^(email|bridge|in_app)$`,
8
+ * `^(none|link|html)$`, `^(email|bridge)$`, `^(scheduled|list_entry|stage_entry)$`), not from memory: the API refuses
9
+ * anything else. Calls nothing.
10
+ */
11
+
12
+ export type SurveyType = "nps" | "csat";
13
+ export type DeliveryChannel = "email" | "bridge" | "in_app";
14
+ export type CommentMode = "none" | "link" | "html";
15
+ export type SendChannel = "email" | "bridge";
16
+ export type TriggerKind = "scheduled" | "list_entry" | "stage_entry";
17
+ export type Bucket = "day" | "week" | "month";
18
+
19
+ export const SURVEY_TYPES: { value: SurveyType; label: string; hint: string; min: number; max: number }[] = [
20
+ { value: "nps", label: "NPS", hint: "How likely are you to recommend us? Scored 0 to 10.", min: 0, max: 10 },
21
+ { value: "csat", label: "CSAT", hint: "How satisfied were you? Scored 1 to 5.", min: 1, max: 5 },
22
+ ];
23
+ export const DELIVERY_CHANNELS: { value: DeliveryChannel; label: string }[] = [
24
+ { value: "email", label: "Email" },
25
+ { value: "bridge", label: "Chat message (messaging bridge)" },
26
+ { value: "in_app", label: "In-app widget" },
27
+ ];
28
+ export const COMMENT_MODES: { value: CommentMode; label: string }[] = [
29
+ { value: "none", label: "Score only" },
30
+ { value: "link", label: "Score, then a comment form" },
31
+ { value: "html", label: "Comment box inside the email" },
32
+ ];
33
+ export const SEND_CHANNELS: { value: SendChannel; label: string }[] = [
34
+ { value: "email", label: "Email" },
35
+ { value: "bridge", label: "Chat message" },
36
+ ];
37
+ export const TRIGGER_KINDS: { value: TriggerKind; label: string; hint: string }[] = [
38
+ { value: "scheduled", label: "On a schedule", hint: "Re-sends to the list every N days." },
39
+ { value: "list_entry", label: "When someone joins a list", hint: "Sends to each person who enters the watched list." },
40
+ { value: "stage_entry", label: "When a record reaches a stage", hint: "Sends when a record moves into the stage you name." },
41
+ ];
42
+ export const BUCKETS: { value: Bucket; label: string }[] = [
43
+ { value: "day", label: "Daily" },
44
+ { value: "week", label: "Weekly" },
45
+ { value: "month", label: "Monthly" },
46
+ ];
47
+ export const CADENCE_DAYS = [30, 60, 90] as const;
48
+
49
+ /** One survey definition (SurveyResponseDTO). */
50
+ export interface Survey {
51
+ id: string;
52
+ name: string;
53
+ type: string;
54
+ question_text: string;
55
+ scale_min: number;
56
+ scale_max: number;
57
+ delivery_channel: string;
58
+ is_active: boolean;
59
+ comment_mode?: string | null;
60
+ thank_you_copy?: string | null;
61
+ template_id?: number | null;
62
+ score_field_column_id?: string | null;
63
+ created_at?: string | null;
64
+ }
65
+
66
+ export interface TrendBar { period: string; total: number; promoters: number; passives: number; detractors: number; nps_score?: number | null; csat_avg?: number | null }
67
+ export interface Summary {
68
+ survey_id: string;
69
+ type: string;
70
+ bucket: string;
71
+ delivery_channel: string;
72
+ total_respondents: number;
73
+ nps_score?: number | null;
74
+ promoters: number;
75
+ passives: number;
76
+ detractors: number;
77
+ csat_avg?: number | null;
78
+ satisfied?: number | null;
79
+ neutral?: number | null;
80
+ dissatisfied?: number | null;
81
+ response_rate: { rate: number; responded: number; sent: number };
82
+ time_series: TrendBar[];
83
+ }
84
+ export interface Trends { survey_id: string; bars: TrendBar[]; bucket: string; type: string; delivery_channel: string }
85
+
86
+ export interface ResponseRow {
87
+ contact_id: number;
88
+ first_name?: string | null;
89
+ last_name?: string | null;
90
+ score: number;
91
+ classification?: string | null;
92
+ comment?: string | null;
93
+ channel?: string | null;
94
+ responded_at?: string | null;
95
+ response_count?: number;
96
+ trend?: string | null;
97
+ history?: { score: number; responded_at?: string | null; send_id?: number | null }[];
98
+ }
99
+ export interface AppUserResponseRow { app_user_id: string; channel: string; score: number; comment?: string | null; responded_at?: string | null }
100
+
101
+ export interface SendRow {
102
+ id: number;
103
+ status: string;
104
+ channel: string;
105
+ audience_ref?: string | null;
106
+ audience_name?: string | null;
107
+ recipient_count?: number;
108
+ responded_count?: number;
109
+ scheduled_at?: string | null;
110
+ created_at?: string | null;
111
+ trigger_kind?: string | null;
112
+ delivery_breakdown?: { bounced?: number; delivered?: number; failed?: number; opened?: number; pending?: number; sent?: number };
113
+ }
114
+ export interface SendAccepted { send_id: number; status: string; recipient_count: number }
115
+ export interface SendDetail { id: number; status: string; channel: string; recipient_count: number; scheduled_at?: string | null; audience_ref?: string | null; survey_id: string }
116
+
117
+ export interface Trigger {
118
+ id: string;
119
+ survey_id: string;
120
+ trigger_kind: string;
121
+ is_active: boolean;
122
+ sender_email: string;
123
+ sender_name?: string | null;
124
+ reply_to?: string | null;
125
+ subject: string;
126
+ audience_ref?: string | null;
127
+ stage_value?: string | null;
128
+ cadence_days?: number | null;
129
+ }
130
+ export interface SendingDomain { id: number; domain: string; status: "verified" | "pending" | "not_set_up" }
131
+
132
+ export interface OrgSummary {
133
+ bucket: string;
134
+ org_nps: { nps_score?: number | null; promoters: number; passives: number; detractors: number; total_respondents: number };
135
+ org_csat: { csat_avg?: number | null; total_respondents: number };
136
+ in_app_csat_avg?: number | null;
137
+ in_app_respondents?: number;
138
+ per_survey: { survey_id: string; name?: string | null; type: string; status?: string | null; is_active?: boolean; sent_count?: number; total_respondents: number; nps_score?: number | null; csat_avg?: number | null; promoters: number; passives: number; detractors: number }[];
139
+ time_series: TrendBar[];
140
+ }
141
+
142
+ export const isNps = (s: { type?: string | null }): boolean => (s.type ?? "").toLowerCase() === "nps";
143
+ export const typeLabel = (s: { type?: string | null }): string => (isNps(s) ? "NPS" : "CSAT");
144
+ export const scaleOf = (type: SurveyType): { min: number; max: number } => {
145
+ const t = SURVEY_TYPES.find((x) => x.value === type) ?? SURVEY_TYPES[0];
146
+ return { min: t.min, max: t.max };
147
+ };
148
+ export const channelLabel = (v: string | null | undefined): string => DELIVERY_CHANNELS.find((c) => c.value === v)?.label ?? (v || "Not set");
149
+ /** A survey receives answers from the people it is sent to (or the in-app widget) while it is open. */
150
+ export const statusOf = (s: Survey): "open" | "closed" => (s.is_active ? "open" : "closed");
151
+
152
+ /** "42", "-8", or a faint dash when the API has no score (too few answers is not zero). */
153
+ export function scoreText(v: number | null | undefined, digits = 0): string {
154
+ return v == null || !Number.isFinite(v) ? "-" : v.toFixed(digits).replace(/^-0$/, "0");
155
+ }
156
+ export function percent(part: number | null | undefined, whole: number | null | undefined): string {
157
+ return !whole || part == null ? "-" : `${Math.round((part / whole) * 100)}%`;
158
+ }
159
+ export const fullName = (r: { first_name?: string | null; last_name?: string | null; contact_id?: number }): string =>
160
+ [r.first_name, r.last_name].filter(Boolean).join(" ") || (r.contact_id != null ? `Contact ${r.contact_id}` : "Unknown");
161
+ export const when = (v: string | null | undefined): string => (v ? new Date(v).toLocaleString() : "");
162
+
163
+ /** The classification the API may omit, derived from the survey type's published bands (NPS 9-10 / 7-8 / 0-6; CSAT 4-5 / 3 / 1-2). */
164
+ export function bandOf(type: string | null | undefined, score: number, given?: string | null): string {
165
+ if (given) return given;
166
+ if ((type ?? "").toLowerCase() === "csat") return score >= 4 ? "satisfied" : score === 3 ? "neutral" : "dissatisfied";
167
+ return score >= 9 ? "promoter" : score >= 7 ? "passive" : "detractor";
168
+ }
169
+
170
+ /** The audience size a send will reach, at most: the list's contact count, null when the list is unknown. */
171
+ export function audienceSize(list: { contact_count?: number | null; total?: number | null } | null | undefined): number | null {
172
+ const n = list?.contact_count ?? list?.total;
173
+ return typeof n === "number" ? n : null;
174
+ }
175
+
176
+ /** Fields a person can change after creating a survey (the API does not take type, scale or comment mode on update). */
177
+ export interface SurveyFormValues { name: string; question_text: string; thank_you_copy: string; delivery_channel: DeliveryChannel }
178
+
179
+ /**
180
+ * Only the fields that changed. A cleared thank-you line is sent as "" (the API ignores null on update, so null would
181
+ * silently keep the old text); an empty name or question never reaches here because the form blocks saving it.
182
+ */
183
+ export function surveyDiff(before: Survey, next: SurveyFormValues): Record<string, unknown> {
184
+ const diff: Record<string, unknown> = {};
185
+ if (next.name.trim() !== before.name) diff.name = next.name.trim();
186
+ if (next.question_text.trim() !== before.question_text) diff.question_text = next.question_text.trim();
187
+ if (next.thank_you_copy.trim() !== (before.thank_you_copy ?? "").trim()) diff.thank_you_copy = next.thank_you_copy.trim();
188
+ if (next.delivery_channel !== before.delivery_channel) diff.delivery_channel = next.delivery_channel;
189
+ return diff;
190
+ }
191
+
192
+ /** One CSV cell: quoted, quotes doubled, and a leading = + - @ tab or CR neutralised so a spreadsheet never runs a comment as a formula. */
193
+ export function csvCell(value: unknown): string {
194
+ let text = value == null ? "" : String(value);
195
+ if (/^[=+\-@\t\r]/.test(text) && !/^-?\d+(\.\d+)?$/.test(text)) text = `'${text}`;
196
+ return `"${text.replace(/"/g, '""')}"`;
197
+ }
198
+
199
+ export function responsesCsv(type: string, rows: ResponseRow[]): string {
200
+ const head = ["contact_id", "first_name", "last_name", "score", "classification", "comment", "channel", "responded_at", "response_count"];
201
+ const lines = rows.map((r) => [r.contact_id, r.first_name, r.last_name, r.score, bandOf(type, r.score, r.classification), r.comment, r.channel, r.responded_at, r.response_count ?? 1].map(csvCell).join(","));
202
+ return [head.map(csvCell).join(","), ...lines].join("\r\n");
203
+ }
204
+
205
+ /** Hand a text file to the browser. */
206
+ export function downloadText(filename: string, text: string, mime = "text/csv;charset=utf-8"): void {
207
+ const url = URL.createObjectURL(new Blob([text], { type: mime }));
208
+ const a = document.createElement("a");
209
+ a.href = url;
210
+ a.download = filename;
211
+ document.body.appendChild(a);
212
+ a.click();
213
+ a.remove();
214
+ setTimeout(() => URL.revokeObjectURL(url), 0);
215
+ }
216
+
217
+ export const slugName = (name: string): string => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "survey";
218
+
219
+ /** The email part of a sender after the @, lower-cased: the domain a send must be verified for. */
220
+ export const domainOf = (email: string): string => email.split("@")[1]?.trim().toLowerCase() ?? "";
221
+ export const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
222
+
223
+ /** The API's `scheduled_at` is a date-time: a `datetime-local` value is read as local time and sent as ISO. */
224
+ export function scheduleIso(local: string): string | undefined {
225
+ if (!local) return undefined;
226
+ const d = new Date(local);
227
+ return Number.isNaN(d.getTime()) ? undefined : d.toISOString();
228
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @title Create an NPS (or CSAT) survey and read back what the API stored
3
+ * @when Setting up the question you will ask. A survey is ONE question with a numeric answer: `type` is "nps" (0 to 10) or
4
+ * "csat" (1 to 5), fixed at creation. Creating it sends nothing; people are asked only when you call sendSurvey.
5
+ */
6
+ import { g8 } from "@graph8/sdk";
7
+
8
+ g8.init({ apiKey: process.env.G8_API_KEY! });
9
+
10
+ const created = await g8.api.surveys.createSurvey({
11
+ body: {
12
+ name: "Relationship NPS",
13
+ type: "nps",
14
+ question_text: "How likely are you to recommend us to a friend or colleague?",
15
+ delivery_channel: "email",
16
+ comment_mode: "link",
17
+ thank_you_copy: "Thank you, that helps us a lot.",
18
+ },
19
+ });
20
+
21
+ const survey = await g8.api.surveys.getSurvey({ path: { survey_id: created.data.id } });
22
+ console.log(survey.data.id, survey.data.type, `${survey.data.scale_min}-${survey.data.scale_max}`, survey.data.is_active);
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @title Send a survey to a list and follow the send
3
+ * @when Asking an audience now or later. This EMAILS REAL PEOPLE: have a person confirm the audience size first. The sender
4
+ * address must be on a verified sending domain (listSurveySendingDomains), and `scheduled_at` queues the send for later.
5
+ * The API answers 202 with a send id; dispatch is asynchronous, so poll getSurveySend.
6
+ */
7
+ import { g8 } from "@graph8/sdk";
8
+
9
+ g8.init({ apiKey: process.env.G8_API_KEY! });
10
+
11
+ export async function sendSurvey(surveyId: string, listId: number): Promise<{ sendId: number; status: string; recipients: number }> {
12
+ const domains = await g8.api.surveys.listSurveySendingDomains({});
13
+ const verified = domains.data.find((d) => d.status === "verified");
14
+ if (!verified) throw new Error("No verified sending domain. Verify one first with verifySurveyDomain.");
15
+
16
+ const accepted = await g8.api.surveys.sendSurvey({
17
+ path: { survey_id: surveyId },
18
+ body: { audience_ref: String(listId), channel: "email", sender_email: `survey@${verified.domain}`, subject: "Quick question" },
19
+ });
20
+
21
+ let status = accepted.data.status;
22
+ for (let i = 0; i < 20 && !["sent", "failed", "scheduled"].includes(status); i++) {
23
+ await new Promise((r) => setTimeout(r, 3000));
24
+ status = (await g8.api.surveys.getSurveySend({ path: { send_id: accepted.data.send_id } })).data.status;
25
+ }
26
+ return { sendId: accepted.data.send_id, status, recipients: accepted.data.recipient_count };
27
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @title Report every survey's score, response rate and latest comments
3
+ * @when A weekly digest or a dashboard backend. A null score means too few answers to publish, which is not zero. The
4
+ * responses endpoint returns every answer in one read (it takes no paging), so a very large survey is a large response.
5
+ */
6
+ import { g8 } from "@graph8/sdk";
7
+
8
+ g8.init({ apiKey: process.env.G8_API_KEY! });
9
+
10
+ export async function scoreReport(): Promise<Array<{ name: string; type: string; score: number | null; answered: number; asked: number; comments: string[] }>> {
11
+ const surveys = await g8.api.surveys.listSurveys({});
12
+ const out: Array<{ name: string; type: string; score: number | null; answered: number; asked: number; comments: string[] }> = [];
13
+ for (const s of surveys.data) {
14
+ const summary = await g8.api.surveys.getSurveySummary({ path: { survey_id: s.id }, query: { bucket: "month" } });
15
+ const answers = await g8.api.surveys.getSurveyResponses({ path: { survey_id: s.id } });
16
+ const d = summary.data;
17
+ out.push({
18
+ name: s.name,
19
+ type: s.type,
20
+ score: (s.type === "nps" ? d.nps_score : d.csat_avg) ?? null,
21
+ answered: d.response_rate.responded,
22
+ asked: d.response_rate.sent,
23
+ comments: answers.data.responses.filter((r) => r.comment).slice(0, 5).map((r) => r.comment as string),
24
+ });
25
+ }
26
+ return out;
27
+ }
@@ -5,6 +5,16 @@ and chart traffic. The Apollo / 6sense "website visitors" screen.
5
5
 
6
6
  Depends on: `crm`. Install: `npx @graph8/sdk add visitors`.
7
7
 
8
+ ## Screens
9
+
10
+ `VisitorsScreen` (`components/visitors-screen.tsx`) is the finished page: copy it as your route. No props are required.
11
+
12
+ - **Visitors tab**: bots/internal-traffic toggles, 7-day stat strip (visitors, sessions, page views with sparklines), the polled company/people feed, and a detail
13
+ panel for a clicked person with the pages they viewed. Company cards expand to the full journey.
14
+ - **Traffic tab**: 7/30/90 day window, trend chart, top pages, "on the site now". **Install tab**: the snippet and an install check.
15
+ - Slots: `toolbarActions`, `renderDetailActions(visit, kind)`, `extraDetailTabs` (each `{ id, label, render(visit, kind) }`; shown once the visitor row is known). Everything is read tier.
16
+ - Prerequisite: the snippet pasted into your site (only a person can do this); People also needs `contacts:read`.
17
+
8
18
  ## Components
9
19
 
10
20
  | Component | What it does | Tiers |
@@ -71,6 +71,29 @@
71
71
  "tiers": [
72
72
  "read"
73
73
  ]
74
+ },
75
+ {
76
+ "name": "VisitorsScreen",
77
+ "file": "components/visitors-screen.tsx",
78
+ "purpose": "Finished website-visitors page: week stat strip, company and people feed with person detail, traffic charts and the snippet install check.",
79
+ "operations": [
80
+ "get_audience_trend_analytics_audience_trend_get"
81
+ ],
82
+ "tiers": [
83
+ "read"
84
+ ]
85
+ },
86
+ {
87
+ "name": "VisitorDetail",
88
+ "file": "components/visitor-detail.tsx",
89
+ "purpose": "Complete screen for one visiting company or identified person: fields, pages viewed, full journey.",
90
+ "operations": [
91
+ "list_company_visitor_events_visitors_companies_events_get",
92
+ "list_contact_visitor_events_visitors_contacts_events_get"
93
+ ],
94
+ "tiers": [
95
+ "read"
96
+ ]
74
97
  }
75
98
  ],
76
99
  "hooks": [],
@@ -134,6 +157,7 @@
134
157
  }
135
158
  ],
136
159
  "peerDependencies": {
137
- "react": ">=18"
160
+ "react": ">=18",
161
+ "lucide-react": ">=0.400"
138
162
  }
139
163
  }