@graph8/sdk 0.370.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 (466) hide show
  1. package/README.md +19 -0
  2. package/blocks/_foundation/README.md +14 -0
  3. package/blocks/_foundation/block.json +44 -0
  4. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  5. package/blocks/_foundation/components/empty-state.tsx +5 -2
  6. package/blocks/_foundation/components/list-select.tsx +112 -0
  7. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  8. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  9. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  10. package/blocks/_foundation/components/screen/index.ts +1 -0
  11. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  12. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  13. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  14. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  15. package/blocks/_foundation/lib/paging.ts +57 -0
  16. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  17. package/blocks/agents/README.md +81 -4
  18. package/blocks/agents/block.json +311 -2
  19. package/blocks/agents/components/agent-activity.tsx +112 -0
  20. package/blocks/agents/components/agent-approvals.tsx +135 -0
  21. package/blocks/agents/components/agent-builder.tsx +17 -3
  22. package/blocks/agents/components/agent-conversations.tsx +143 -0
  23. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
  24. package/blocks/agents/components/agent-operators.tsx +164 -0
  25. package/blocks/agents/components/agent-run-detail.tsx +20 -3
  26. package/blocks/agents/components/agent-runs.tsx +3 -3
  27. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  28. package/blocks/agents/components/agent-test-history.tsx +121 -0
  29. package/blocks/agents/components/agents-screen.tsx +53 -11
  30. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  31. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  32. package/blocks/agents/components/from-template-button.tsx +65 -0
  33. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  34. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  35. package/blocks/agents/components/knowledge-form.tsx +69 -0
  36. package/blocks/agents/components/name-dialog.tsx +32 -0
  37. package/blocks/agents/components/receptionist-button.tsx +46 -0
  38. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  39. package/blocks/agents/components/skill-editor.tsx +147 -0
  40. package/blocks/agents/components/skill-library.tsx +111 -0
  41. package/blocks/agents/lib/agent-types.ts +7 -1
  42. package/blocks/agents/lib/facts.tsx +13 -0
  43. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  44. package/blocks/agents/lib/load-more.tsx +141 -0
  45. package/blocks/agents/lib/skill-utils.ts +91 -0
  46. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  47. package/blocks/analytics/components/analytics-screen.tsx +16 -3
  48. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  49. package/blocks/analytics/components/report-builder.tsx +15 -2
  50. package/blocks/appointments/README.md +6 -0
  51. package/blocks/appointments/block.json +27 -0
  52. package/blocks/appointments/components/booking-page.tsx +2 -2
  53. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  54. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  55. package/blocks/appointments/components/meeting-list.tsx +29 -13
  56. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  57. package/blocks/campaigns/components/campaign-assets.tsx +1 -1
  58. package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
  59. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  60. package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
  61. package/blocks/campaigns/components/icp-detail.tsx +11 -2
  62. package/blocks/campaigns/components/icp-editor.tsx +8 -3
  63. package/blocks/campaigns/components/persona-detail.tsx +11 -2
  64. package/blocks/campaigns/components/persona-editor.tsx +6 -1
  65. package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
  66. package/blocks/coach/components/call-scorecard.tsx +7 -7
  67. package/blocks/company-intelligence/README.md +132 -0
  68. package/blocks/company-intelligence/block.json +318 -0
  69. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  70. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  71. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  72. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  73. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  74. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  75. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  76. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  77. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  78. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  79. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  80. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  81. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  82. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  83. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  84. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  85. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  86. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  87. package/blocks/copilot/components/copilot-panel.tsx +1 -1
  88. package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
  89. package/blocks/crm/README.md +6 -0
  90. package/blocks/crm/block.json +53 -4
  91. package/blocks/crm/components/companies-screen.tsx +23 -9
  92. package/blocks/crm/components/company-detail.tsx +2 -2
  93. package/blocks/crm/components/contact-detail.tsx +4 -4
  94. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  95. package/blocks/crm/components/contacts-screen.tsx +31 -13
  96. package/blocks/crm/components/dedupe-review.tsx +6 -1
  97. package/blocks/crm/components/import-wizard.tsx +3 -7
  98. package/blocks/crm/components/list-select.tsx +20 -0
  99. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  100. package/blocks/crm/components/record-form.tsx +1 -1
  101. package/blocks/crm/components/tasks-screen.tsx +35 -15
  102. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  103. package/blocks/crm/hooks/use-page-query.ts +3 -1
  104. package/blocks/crm/lib/crm-types.ts +14 -0
  105. package/blocks/crm/lib/read-lists.ts +16 -0
  106. package/blocks/deals/README.md +24 -6
  107. package/blocks/deals/block.json +151 -2
  108. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  109. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  110. package/blocks/deals/components/deal-detail.tsx +2 -2
  111. package/blocks/deals/components/deal-filters.tsx +65 -0
  112. package/blocks/deals/components/deal-line-items.tsx +120 -0
  113. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  114. package/blocks/deals/components/deal-related.tsx +30 -9
  115. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  116. package/blocks/deals/components/deals-screen.tsx +57 -22
  117. package/blocks/deals/components/forecast-view.tsx +4 -4
  118. package/blocks/deals/components/pipeline-board.tsx +8 -6
  119. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  120. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  121. package/blocks/deals/components/stage-editor.tsx +1 -1
  122. package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
  123. package/blocks/deals/lib/deal-extras.ts +156 -0
  124. package/blocks/dialer/README.md +14 -5
  125. package/blocks/dialer/block.json +17 -2
  126. package/blocks/dialer/components/call-contact-button.tsx +26 -9
  127. package/blocks/dialer/components/call-log-screen.tsx +4 -1
  128. package/blocks/dialer/components/call-log.tsx +16 -4
  129. package/blocks/dialer/components/call-queue.tsx +3 -3
  130. package/blocks/dialer/components/dialer-screen.tsx +69 -27
  131. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  132. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  133. package/blocks/dialer/components/session-start-form.tsx +58 -11
  134. package/blocks/dialer/components/softphone.tsx +1 -1
  135. package/blocks/enrichment/README.md +16 -6
  136. package/blocks/enrichment/block.json +19 -7
  137. package/blocks/enrichment/components/bulk-enrich-panel.tsx +76 -27
  138. package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
  139. package/blocks/enrichment/components/enrich-button.tsx +1 -1
  140. package/blocks/enrichment/components/enrichment-screen.tsx +70 -17
  141. package/blocks/enrichment/components/waterfall-config.tsx +27 -7
  142. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  143. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  144. package/blocks/forms/README.md +8 -0
  145. package/blocks/forms/block.json +107 -0
  146. package/blocks/forms/components/form-activity.tsx +71 -0
  147. package/blocks/forms/components/form-backfill.tsx +108 -0
  148. package/blocks/forms/components/form-builder.tsx +1 -1
  149. package/blocks/forms/components/form-embed.tsx +1 -1
  150. package/blocks/forms/components/form-groups.tsx +151 -0
  151. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  152. package/blocks/forms/components/form-route-history.tsx +72 -0
  153. package/blocks/forms/components/form-saved-views.tsx +81 -0
  154. package/blocks/forms/components/forms-screen.tsx +51 -17
  155. package/blocks/forms/components/more-rows.tsx +30 -0
  156. package/blocks/forms/components/submissions-export.tsx +67 -0
  157. package/blocks/forms/components/submissions-table.tsx +15 -6
  158. package/blocks/forms/lib/forms-extra.ts +67 -0
  159. package/blocks/inbox/README.md +5 -0
  160. package/blocks/inbox/block.json +15 -0
  161. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  162. package/blocks/inbox/components/thread-badge.tsx +2 -2
  163. package/blocks/inbox/components/thread-view.tsx +1 -1
  164. package/blocks/integrations/README.md +54 -1
  165. package/blocks/integrations/block.json +189 -15
  166. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  167. package/blocks/integrations/components/connection-health.tsx +59 -0
  168. package/blocks/integrations/components/crm-detail.tsx +20 -1
  169. package/blocks/integrations/components/crm-records.tsx +103 -0
  170. package/blocks/integrations/components/field-mappings.tsx +61 -0
  171. package/blocks/integrations/components/integration-list.tsx +4 -4
  172. package/blocks/integrations/components/integrations-screen.tsx +20 -3
  173. package/blocks/integrations/components/migration-panel.tsx +133 -0
  174. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  175. package/blocks/integrations/components/sync-status.tsx +6 -6
  176. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  177. package/blocks/integrations/components/webhook-detail.tsx +26 -7
  178. package/blocks/integrations/components/webhook-manager.tsx +30 -17
  179. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  180. package/blocks/integrations/lib/crm-utils.ts +39 -0
  181. package/blocks/integrations/lib/facts.tsx +13 -0
  182. package/blocks/integrations/lib/integrations-utils.ts +5 -0
  183. package/blocks/integrations/lib/load-more.tsx +141 -0
  184. package/blocks/intent/components/account-intent-score.tsx +5 -5
  185. package/blocks/intent/components/intent-feed.tsx +8 -8
  186. package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
  187. package/blocks/intent/lib/intent-utils.ts +4 -4
  188. package/blocks/lifecycle/components/account-related.tsx +8 -1
  189. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  190. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  191. package/blocks/lists/README.md +7 -1
  192. package/blocks/lists/block.json +31 -3
  193. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  194. package/blocks/lists/components/add-to-list-button.tsx +17 -7
  195. package/blocks/lists/components/list-picker.tsx +13 -3
  196. package/blocks/lists/components/list-table.tsx +1 -1
  197. package/blocks/lists/components/lists-screen.tsx +4 -2
  198. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  199. package/blocks/lists/components/suppression-manager.tsx +27 -6
  200. package/blocks/lists/components/suppressions-screen.tsx +21 -12
  201. package/blocks/mailboxes/block.json +4 -0
  202. package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
  203. package/blocks/mailboxes/components/domain-detail.tsx +10 -4
  204. package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
  205. package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
  206. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  207. package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
  208. package/blocks/mailboxes/components/warmup-health.tsx +20 -20
  209. package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
  210. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  211. package/blocks/newsletter/README.md +12 -0
  212. package/blocks/newsletter/block.json +117 -0
  213. package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
  214. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  215. package/blocks/newsletter/components/more-rows.tsx +30 -0
  216. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  217. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  218. package/blocks/newsletter/components/newsletter-screen.tsx +36 -13
  219. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  220. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  221. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  222. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  223. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  224. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  225. package/blocks/nurture/components/nurture-builder.tsx +10 -2
  226. package/blocks/nurture/components/nurture-enroll.tsx +6 -7
  227. package/blocks/nurture/components/nurture-form.tsx +5 -8
  228. package/blocks/nurture/components/nurture-screen.tsx +16 -5
  229. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  230. package/blocks/objects/README.md +1 -1
  231. package/blocks/objects/block.json +13 -5
  232. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  233. package/blocks/objects/components/object-record-detail.tsx +8 -1
  234. package/blocks/objects/components/object-schema-editor.tsx +1 -1
  235. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  236. package/blocks/pages/README.md +14 -0
  237. package/blocks/pages/block.json +113 -1
  238. package/blocks/pages/components/more-rows.tsx +30 -0
  239. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  240. package/blocks/pages/components/page-analytics.tsx +176 -0
  241. package/blocks/pages/components/page-clone.tsx +99 -0
  242. package/blocks/pages/components/page-domains.tsx +167 -0
  243. package/blocks/pages/components/page-editor.tsx +1 -1
  244. package/blocks/pages/components/page-generation-status.tsx +48 -0
  245. package/blocks/pages/components/page-tracking.tsx +137 -0
  246. package/blocks/pages/components/pages-screen.tsx +57 -16
  247. package/blocks/pages/components/seo-report.tsx +6 -1
  248. package/blocks/pages/lib/studio.ts +7 -0
  249. package/blocks/phone-numbers/README.md +74 -0
  250. package/blocks/phone-numbers/block.json +262 -0
  251. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  252. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  253. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  254. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  255. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  256. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  257. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  258. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  259. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  260. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  261. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  262. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  263. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  264. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  265. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  266. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  267. package/blocks/pickers/README.md +23 -0
  268. package/blocks/pickers/block.json +103 -0
  269. package/blocks/pickers/components/company-picker.tsx +90 -0
  270. package/blocks/pickers/components/contact-picker.tsx +131 -0
  271. package/blocks/pickers/components/member-picker.tsx +99 -0
  272. package/blocks/pickers/components/search-picker.tsx +98 -0
  273. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  274. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  275. package/blocks/pickers/lib/use-debounced.ts +13 -0
  276. package/blocks/quotes/README.md +13 -0
  277. package/blocks/quotes/block.json +48 -1
  278. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  279. package/blocks/quotes/components/quote-builder.tsx +1 -1
  280. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  281. package/blocks/quotes/components/quote-list.tsx +5 -3
  282. package/blocks/quotes/components/quote-related.tsx +3 -2
  283. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  284. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  285. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  286. package/blocks/radar/components/radar-overview.tsx +4 -4
  287. package/blocks/radar/components/radar-screen.tsx +2 -2
  288. package/blocks/sequencer/README.md +6 -0
  289. package/blocks/sequencer/block.json +28 -1
  290. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  291. package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
  292. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  293. package/blocks/sequencer/components/sequence-builder.tsx +19 -20
  294. package/blocks/sequencer/components/sequence-detail.tsx +4 -2
  295. package/blocks/sequencer/components/sequence-enrollments.tsx +4 -2
  296. package/blocks/sequencer/components/sequence-list.tsx +1 -1
  297. package/blocks/sequencer/components/sequence-stats.tsx +8 -8
  298. package/blocks/sequencer/components/sequences-screen.tsx +3 -0
  299. package/blocks/sequencer/components/step-editor.tsx +11 -11
  300. package/blocks/sequencer/lib/sequencer-utils.ts +5 -5
  301. package/blocks/social-ads/README.md +8 -0
  302. package/blocks/social-ads/block.json +137 -4
  303. package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
  304. package/blocks/social-ads/components/audience-sync.tsx +6 -2
  305. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  306. package/blocks/social-ads/components/campaign-detail.tsx +6 -1
  307. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  308. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  309. package/blocks/social-ads/components/creative-detail.tsx +6 -1
  310. package/blocks/social-ads/components/creative-gallery.tsx +20 -2
  311. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  312. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  313. package/blocks/social-ads/components/post-composer.tsx +4 -2
  314. package/blocks/social-ads/components/post-detail.tsx +3 -0
  315. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  316. package/blocks/social-ads/components/social-performance.tsx +80 -0
  317. package/blocks/social-ads/components/social-screen.tsx +33 -6
  318. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  319. package/blocks/surveys/README.md +91 -0
  320. package/blocks/surveys/block.json +299 -0
  321. package/blocks/surveys/components/account-nps.tsx +69 -0
  322. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  323. package/blocks/surveys/components/org-summary.tsx +82 -0
  324. package/blocks/surveys/components/sending-domains.tsx +113 -0
  325. package/blocks/surveys/components/survey-form.tsx +126 -0
  326. package/blocks/surveys/components/survey-responses.tsx +145 -0
  327. package/blocks/surveys/components/survey-results.tsx +72 -0
  328. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  329. package/blocks/surveys/components/survey-send.tsx +162 -0
  330. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  331. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  332. package/blocks/surveys/lib/surveys-types.ts +228 -0
  333. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  334. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  335. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  336. package/blocks/visitors/components/snippet-install.tsx +18 -9
  337. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  338. package/blocks/visitors/components/visitor-company-card.tsx +24 -14
  339. package/blocks/visitors/components/visitor-feed.tsx +5 -5
  340. package/blocks/webchat/components/chat-widget-config.tsx +1 -1
  341. package/blocks/webchat/components/conversation-list.tsx +1 -1
  342. package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
  343. package/blocks/workbench/components/run-progress.tsx +1 -1
  344. package/blocks/workbench/components/workbench-grid.tsx +1 -1
  345. package/blocks/workbench/components/workbench-screen.tsx +15 -5
  346. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  347. package/blocks/workflows/components/workflow-editor.tsx +1 -1
  348. package/dist/cli.js +536 -392
  349. package/dist/index.browser.js +1 -1
  350. package/dist/index.browser.js.map +1 -1
  351. package/dist/index.browser.mjs +1 -1
  352. package/dist/index.browser.mjs.map +1 -1
  353. package/dist/index.d.mts +2 -2
  354. package/dist/index.d.ts +2 -2
  355. package/dist/index.js +1 -1
  356. package/dist/index.js.map +1 -1
  357. package/dist/index.mjs +1 -1
  358. package/dist/index.mjs.map +1 -1
  359. package/dist/react.browser.js +1 -1
  360. package/dist/react.browser.js.map +1 -1
  361. package/dist/react.browser.mjs +1 -1
  362. package/dist/react.browser.mjs.map +1 -1
  363. package/dist/react.d.mts +2 -2
  364. package/dist/react.d.ts +2 -2
  365. package/dist/react.js +1 -1
  366. package/dist/react.js.map +1 -1
  367. package/dist/react.mjs +1 -1
  368. package/dist/react.mjs.map +1 -1
  369. package/dist/types/blocks/brand.d.ts +11 -2
  370. package/dist/types/blocks/create.d.ts +7 -3
  371. package/dist/types/blocks/doctor.d.ts +1 -0
  372. package/dist/types/blocks/flows.d.ts +7 -1
  373. package/dist/types/blocks/orgBrand.d.ts +34 -0
  374. package/dist/types/cli.d.ts +3 -0
  375. package/dist/types/version.d.ts +2 -2
  376. package/docs/api/desk.d.ts +2 -2
  377. package/docs/api/desk.md +3 -3
  378. package/package.json +1 -1
  379. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  380. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  381. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  382. package/stacks/6sense.json +17 -4
  383. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  384. package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
  385. package/stacks/apollo/pages/contacts.tsx.tmpl +29 -95
  386. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  387. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  388. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  389. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  390. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  391. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  392. package/stacks/apollo.json +36 -8
  393. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  394. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  395. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  396. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  397. package/stacks/calendly.json +8 -4
  398. package/stacks/clay/pages/companies.tsx.tmpl +17 -1
  399. package/stacks/clay/pages/contacts.tsx.tmpl +12 -0
  400. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  401. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  402. package/stacks/clay.json +16 -3
  403. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  404. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  405. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  406. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  407. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  408. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  409. package/stacks/gong.json +23 -6
  410. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  411. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  412. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  413. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  414. package/stacks/hootsuite.json +9 -4
  415. package/stacks/hubspot/pages/companies.tsx.tmpl +14 -35
  416. package/stacks/hubspot/pages/contacts.tsx.tmpl +29 -95
  417. package/stacks/hubspot/pages/deals.tsx.tmpl +10 -1
  418. package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
  419. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  420. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  421. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  422. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  423. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  424. package/stacks/hubspot.json +21 -5
  425. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  426. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  427. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  428. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  429. package/stacks/intercom.json +8 -4
  430. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  431. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  432. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  433. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  434. package/stacks/mailchimp.json +20 -5
  435. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  436. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  437. package/stacks/outreach/pages/contacts.tsx.tmpl +27 -66
  438. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  439. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  440. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  441. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  442. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  443. package/stacks/outreach.json +25 -7
  444. package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
  445. package/stacks/pipedrive/pages/contacts.tsx.tmpl +15 -35
  446. package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
  447. package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
  448. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  449. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  450. package/stacks/pipedrive.json +4 -2
  451. package/stacks/salesforce/pages/companies.tsx.tmpl +14 -35
  452. package/stacks/salesforce/pages/contacts.tsx.tmpl +20 -35
  453. package/stacks/salesforce/pages/deals.tsx.tmpl +10 -1
  454. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  455. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  456. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  457. package/stacks/salesforce.json +7 -3
  458. package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
  459. package/stacks/zoominfo/pages/contacts.tsx.tmpl +12 -0
  460. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  461. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  462. package/stacks/zoominfo.json +16 -3
  463. package/styles/styles.css +1 -1
  464. package/templates/next-app/app/globals.css.tmpl +349 -64
  465. package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
  466. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
@@ -187,7 +187,7 @@ export function PageEditor({ pageId, onSaved, className }: PageEditorProps) {
187
187
  return (
188
188
  <div className={className}>
189
189
  <div className="mb-4 flex flex-wrap items-center gap-2">
190
- <h2 className="text-lg font-semibold">{text(data.name, "Untitled page")}</h2>
190
+ <h2 className="text-lg font-medium">{text(data.name, "Untitled page")}</h2>
191
191
  <span className="text-muted-foreground text-sm">
192
192
  {text(data.status, "draft")}
193
193
  {url ? ` · ${url}` : ""}
@@ -0,0 +1,48 @@
1
+ "use client";
2
+
3
+ /**
4
+ * PageGenerationStatus: shows how far an AI-built page is while it is still being made, and calls `onDone` when it
5
+ * finishes. Renders nothing for a page that is not generating. A page that failed shows its error.
6
+ *
7
+ * SDK operations:
8
+ * 1. getLandingPageStatus GET /landing-pages/{id}/status (read; polled every 4 s only while the page is generating)
9
+ */
10
+ import { useEffect, useRef } from "react";
11
+ import { ApiError } from "@/g8/components/api-error";
12
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
13
+ import { g8Get } from "@/g8/lib/g8-client";
14
+ import { asRec, num, text } from "../lib/studio";
15
+
16
+ export interface PageGenerationStatusProps {
17
+ pageId: string;
18
+ /** The status the page row already shows. Polling starts only when it is an in-progress one. */
19
+ status?: string;
20
+ onDone?: () => void;
21
+ className?: string;
22
+ }
23
+
24
+ const WORKING = new Set(["generating", "pending", "processing", "queued", "in_progress"]);
25
+ export const isGenerating = (status: string | undefined): boolean => WORKING.has(String(status ?? "").toLowerCase());
26
+
27
+ export function PageGenerationStatus({ pageId, status, onDone, className }: PageGenerationStatusProps) {
28
+ const res = useG8Query((signal) => g8Get(`/landing-pages/${encodeURIComponent(pageId)}/status`, undefined, { signal }), [pageId, status], { enabled: isGenerating(status) });
29
+ const d = asRec(res.data?.data);
30
+ const live = isGenerating(text(d.status) || status);
31
+ // The status read can say "done" while the row still says "generating" (a worker died): tell the parent once per page, never in a loop.
32
+ const told = useRef<string | null>(null);
33
+ useEffect(() => {
34
+ if (!isGenerating(status)) return;
35
+ if (res.data && !live) { if (told.current !== pageId) { told.current = pageId; onDone?.(); } return; }
36
+ const t = setTimeout(() => void res.refetch(), 4000);
37
+ return () => clearTimeout(t);
38
+ }, [status, res.data, live]); // eslint-disable-line react-hooks/exhaustive-deps
39
+ if (!isGenerating(status)) return null;
40
+ if (res.error) return <ApiError error={res.error} title="Could not read the page status" onRetry={() => void res.refetch()} />;
41
+ const progress = num(d.progress);
42
+ return (
43
+ <p role="status" className={className ?? "text-sm"}>
44
+ Generating this page{progress != null ? `: ${progress}%` : ""}
45
+ {num(d.sections_completed) ? `, ${d.sections_completed} sections written` : ""}. This view updates by itself.
46
+ </p>
47
+ );
48
+ }
@@ -0,0 +1,137 @@
1
+ "use client";
2
+
3
+ /**
4
+ * PageTracking: how one landing page is measured. Whether visits are recorded on the workspace stream, on a stream of
5
+ * its own or not at all, which form it reports, whether the chat widget shows, and what counts as a conversion.
6
+ * Changes apply the next time the page is published.
7
+ *
8
+ * SDK operations, in the order the screen uses them:
9
+ * 1. getLandingPageTracking GET /landing-pages/{id}/tracking (read)
10
+ * 2. updateLandingPageTracking PATCH /landing-pages/{id}/tracking (write; ONLY the fields you changed are sent)
11
+ * 3. provisionLandingPageTracking POST /landing-pages/{id}/tracking/provision (write; gives the page its own stream)
12
+ * 4. clearLandingPageConsentOverride DELETE /landing-pages/{id}/tracking/consent (destructive, confirmed: drops the page's own consent banner settings)
13
+ *
14
+ * The form is seeded from the loaded record only, and Save stays disabled until that record has arrived, so a blank
15
+ * form can never be written over real settings. "Dedicated" is reached through Provision (the API refuses a direct switch).
16
+ */
17
+ import { useState } from "react";
18
+ import { Button, Input, Label, Skeleton, Switch } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
21
+ import { useConfirmAction } from "@/g8/components/confirmed-action";
22
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
23
+ import { SectionCard } from "@/g8/components/screen/section-card";
24
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
25
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
26
+ import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
27
+ import { asRec, shortDate, text } from "../lib/studio";
28
+ import type { Rec } from "../lib/studio";
29
+
30
+ export interface PageTrackingProps {
31
+ pageId: string;
32
+ onChanged?: () => void;
33
+ className?: string;
34
+ }
35
+
36
+ const SELECT = "border-input bg-background h-9 rounded-md border px-2 text-sm";
37
+ const MODES = [
38
+ { value: "org_default", label: "Workspace stream (default)" },
39
+ { value: "disabled", label: "Off (no visits recorded)" },
40
+ ];
41
+ const GOALS = [
42
+ { value: "", label: "Not set" },
43
+ { value: "form_submission", label: "A form is submitted" },
44
+ { value: "workspace_signup", label: "Someone signs up" },
45
+ { value: "paid_account", label: "Someone pays" },
46
+ { value: "traffic_only", label: "Traffic only" },
47
+ ];
48
+
49
+ /** The fields that differ from the saved record: the body of the PATCH. Empty means nothing to save. */
50
+ export function trackingChanges(saved: Rec, draft: { mode: string; form_id: string; chat_enabled: boolean; conversion_goal: string }): Rec {
51
+ const out: Rec = {};
52
+ if (draft.mode !== text(saved.mode, "org_default") && draft.mode !== "dedicated") out.mode = draft.mode;
53
+ if (draft.form_id.trim() !== text(saved.form_id)) out.form_id = draft.form_id.trim();
54
+ if (draft.chat_enabled !== (saved.chat_enabled !== false)) out.chat_enabled = draft.chat_enabled;
55
+ if (draft.conversion_goal && draft.conversion_goal !== text(saved.conversion_goal)) out.conversion_goal = draft.conversion_goal;
56
+ return out;
57
+ }
58
+
59
+ function TrackingForm({ pageId, saved, onSaved }: { pageId: string; saved: Rec; onSaved: () => void }) {
60
+ const base = `/landing-pages/${encodeURIComponent(pageId)}/tracking`;
61
+ const { ask, dialog, failure } = useConfirmAction();
62
+ // Only what the person changed is held as `edits`, shown over the saved copy, so a reload never wipes typing.
63
+ const base0 = { mode: text(saved.mode, "org_default"), form_id: text(saved.form_id), chat_enabled: saved.chat_enabled !== false, conversion_goal: text(saved.conversion_goal) };
64
+ const [edits, setEdits] = useState<Partial<typeof base0>>({});
65
+ const draft = { ...base0, ...edits };
66
+ const setDraft = (next: typeof base0): void => setEdits(Object.fromEntries(Object.entries(next).filter(([k, v]) => v !== base0[k as keyof typeof base0])) as Partial<typeof base0>);
67
+ const changes = trackingChanges(saved, draft);
68
+ const dirty = Object.keys(changes).length > 0;
69
+ const save = useG8Mutation(() => g8Patch(base, changes), { onSuccess: () => { setEdits({}); onSaved(); } });
70
+ const provision = useG8Mutation(() => g8Post(`${base}/provision`), { onSuccess: onSaved });
71
+ const dedicated = saved.mode === "dedicated";
72
+ return (
73
+ <div className="grid gap-4">
74
+ <form className="grid gap-3 sm:grid-cols-2" onSubmit={(e) => { e.preventDefault(); if (dirty) void save.mutate(undefined); }}>
75
+ <div>
76
+ <Label htmlFor="pt-mode" className="mb-1">Measure visits</Label>
77
+ {dedicated ? <p className="text-sm">On a stream of its own</p> : (
78
+ <select id="pt-mode" className={`${SELECT} w-full`} value={draft.mode} onChange={(e) => setDraft({ ...draft, mode: e.target.value })}>
79
+ {MODES.map((m) => <option key={m.value} value={m.value}>{m.label}</option>)}
80
+ </select>
81
+ )}
82
+ </div>
83
+ <div>
84
+ <Label htmlFor="pt-goal" className="mb-1">A conversion is</Label>
85
+ <select id="pt-goal" className={`${SELECT} w-full`} value={draft.conversion_goal} onChange={(e) => setDraft({ ...draft, conversion_goal: e.target.value })}>
86
+ {GOALS.map((g) => <option key={g.value} value={g.value} disabled={g.value === "" && Boolean(saved.conversion_goal)}>{g.label}</option>)}
87
+ </select>
88
+ </div>
89
+ <div>
90
+ <Label htmlFor="pt-form" className="mb-1">Form label</Label>
91
+ <Input id="pt-form" value={draft.form_id} placeholder="demo-request" onChange={(e) => setDraft({ ...draft, form_id: e.target.value })} />
92
+ </div>
93
+ <div className="flex items-end gap-2">
94
+ <Switch id="pt-chat" checked={draft.chat_enabled} onCheckedChange={(v) => setDraft({ ...draft, chat_enabled: v })} aria-label="Show the chat widget" />
95
+ <Label htmlFor="pt-chat">Show the chat widget on this page</Label>
96
+ </div>
97
+ <div className="flex gap-2 sm:col-span-2">
98
+ <Button type="submit" loading={save.loading} disabled={!dirty}>Save tracking</Button>
99
+ {!dedicated ? <Button type="button" variant="outline" loading={provision.loading} onClick={() => void provision.mutate(undefined)}>Give this page its own stream</Button> : null}
100
+ </div>
101
+ </form>
102
+ {save.error ? <ApiError error={save.error} title="Could not save tracking" /> : null}
103
+ {save.data && !dirty ? <p role="status" className="text-sm">Tracking saved. It applies the next time the page is published.</p> : null}
104
+ {provision.error ? <ApiError error={provision.error} title="Could not set up the stream" /> : null}
105
+ <KeyValueList columns={2} items={[
106
+ { label: "Stream", value: text(saved.stream_name) || "Workspace stream" },
107
+ { label: "Provisioned", value: shortDate(saved.provisioned_at) },
108
+ { label: "Page has a form", value: saved.has_forms === true ? "Yes" : saved.has_forms === false ? "No" : "" },
109
+ { label: "Page has a sign-up", value: saved.has_graph8_signup === true ? "Yes" : saved.has_graph8_signup === false ? "No" : "" },
110
+ ]} />
111
+ <div>
112
+ <Button size="sm" variant="ghost" onClick={() => ask({
113
+ tier: "destructive",
114
+ title: "Use the workspace consent banner on this page",
115
+ confirmLabel: "Use workspace setting",
116
+ description: "Drops this page's own consent banner settings. The page follows the workspace default the next time it is published.",
117
+ run: () => g8Delete(`${base}/consent`),
118
+ onDone: onSaved,
119
+ })}>Reset consent to the workspace default</Button>
120
+ </div>
121
+ {failure ? <ApiError error={failure} title="That did not work" /> : null}
122
+ <ConfirmSpendDialog {...dialog} />
123
+ </div>
124
+ );
125
+ }
126
+
127
+ export function PageTracking({ pageId, onChanged, className }: PageTrackingProps) {
128
+ const res = useG8Query((signal) => g8Get(`/landing-pages/${encodeURIComponent(pageId)}/tracking`, undefined, { signal }), [pageId]);
129
+ const saved = asRec(res.data?.data);
130
+ return (
131
+ <SectionCard className={className} title="Tracking" description="Applies the next time the page is published.">
132
+ {res.error ? <ApiError error={res.error} title="Could not load tracking" onRetry={() => void res.refetch()} /> : !res.data ? <Skeleton className="h-32 w-full" aria-busy="true" /> : (
133
+ <TrackingForm key={pageId} pageId={pageId} saved={saved} onSaved={() => { void res.refetch(); onChanged?.(); }} />
134
+ )}
135
+ </SectionCard>
136
+ );
137
+ }
@@ -5,7 +5,9 @@
5
5
  *
6
6
  * Composes: ScreenLayout (title, tabs, stats), SplitView + DetailPanel (landing page list and the selected
7
7
  * page's editor, deep link `?id=`), PageList (list, filter, blank or AI create, publish, unpublish, delete),
8
- * PageEditor (details, sections, preview, versions, publish), ContentLibrary, BrandKitEditor, SeoReport.
8
+ * PageEditor (details, sections, preview, versions, publish), PageDomains (live URLs, publish to a domain, download),
9
+ * PageTracking, PageAiTools (edit, review, search metadata), PageAnalytics (portfolio tab and per page), PageClone,
10
+ * ContentLibrary, BrandKitEditor, SeoReport.
9
11
  *
10
12
  * Every record is deep-linkable: `?id=` opens a landing page (Edit and Details tabs) or, on the Content tab
11
13
  * (`?tab=content`), a content item with all of its fields and actions; the Brand kit tab is the one brand record.
@@ -16,30 +18,43 @@
16
18
  * (confirmed); manage the content inventory (add by URL, edit, SEO enrichment, delete); re-scan the brand kit and
17
19
  * choose page layouts; run an SEO audit and track keywords. The editor changes text fields of sections-mode pages;
18
20
  * freeform HTML pages can be previewed, published and versioned, and their HTML is edited in the graph8 app.
21
+ * Every page also has Publishing (its URLs, publish to a connected domain, remove a URL, download the HTML), Tracking,
22
+ * AI tools (edit by instruction, review and apply fixes, write search metadata: each AI pass is a confirmed spend) and
23
+ * Analytics tabs; "Clone a page" rebuilds a URL or pasted HTML as a draft (confirmed spend); the Analytics tab shows
24
+ * visits, people and companies across all pages.
19
25
  *
20
- * SDK operations: listLandingPages (for the headline numbers only); everything else lives in the composed parts,
21
- * which confirm every publish, unpublish, delete, AI generation and paid audit.
26
+ * SDK operations: listLandingPages (the headline numbers read the API's totals, so they stay right past one page),
27
+ * getLandingPage (the title of a page that is not in the loaded list); everything else lives in the composed parts,
28
+ * which confirm every publish, unpublish, delete, URL removal, AI pass and paid audit.
22
29
  */
23
30
 
24
- import { useState } from "react";
31
+ import { useRef, useState } from "react";
25
32
  import type { ReactNode } from "react";
26
33
  import { Globe } from "lucide-react";
27
34
  import { Skeleton } from "@graph8/sdk/ui";
28
35
  import { ApiError } from "@/g8/components/api-error";
36
+ import { FirstNotice } from "@/g8/components/paging-notices";
29
37
  import { DetailPanel } from "@/g8/components/screen/detail-panel";
30
38
  import { KeyValueList } from "@/g8/components/screen/key-value-list";
31
39
  import { ScreenLayout } from "@/g8/components/screen/screen-layout";
32
40
  import { SplitView } from "@/g8/components/screen/split-view";
33
41
  import { useUrlState } from "@/g8/hooks/use-url-state";
34
42
  import { g8Get } from "@/g8/lib/g8-client";
43
+ import { useAllPages } from "@/g8/hooks/use-all-pages";
35
44
  import { useG8Query } from "@/g8/hooks/use-g8-query";
36
45
  import { StatusPill } from "@/g8/components/screen/status-pill";
37
46
  import { asRec, num, rowsOf, shortDate, text } from "../lib/studio";
38
47
  import type { LandingPageRow } from "../lib/studio";
39
48
  import { BrandKitEditor } from "./brand-kit-editor";
40
49
  import { ContentLibrary } from "./content-library";
50
+ import { PageAiTools } from "./page-ai-tools";
51
+ import { PageAnalytics } from "./page-analytics";
52
+ import { PageClone } from "./page-clone";
53
+ import { PageDomains } from "./page-domains";
41
54
  import { PageEditor } from "./page-editor";
55
+ import { PageGenerationStatus } from "./page-generation-status";
42
56
  import { PageList } from "./page-list";
57
+ import { PageTracking } from "./page-tracking";
43
58
  import { SeoReport } from "./seo-report";
44
59
 
45
60
  export interface PagesScreenProps {
@@ -51,10 +66,10 @@ export interface PagesScreenProps {
51
66
  className?: string;
52
67
  }
53
68
 
54
- type Tab = "pages" | "content" | "brand" | "seo";
69
+ type Tab = "pages" | "content" | "brand" | "seo" | "analytics";
55
70
 
56
71
  /** Every field of one landing page (name and slug are edited in the Edit tab). */
57
- function PageDetails({ pageId }: { pageId: string }) {
72
+ function PageDetails({ pageId, onGenerated }: { pageId: string; onGenerated?: () => void }) {
58
73
  const res = useG8Query((signal) => g8Get(`/landing-pages/${encodeURIComponent(pageId)}`, undefined, { signal }), [pageId]);
59
74
  if (res.error) return <ApiError error={res.error} title="Could not load the page" onRetry={() => void res.refetch()} />;
60
75
  if (!res.data) return <Skeleton className="h-32 w-full" aria-busy="true" />;
@@ -62,6 +77,8 @@ function PageDetails({ pageId }: { pageId: string }) {
62
77
  const n = (k: string): string => (d[k] == null || d[k] === "" ? "Not set" : text(d[k]));
63
78
  const sections = Array.isArray(d.sections) ? d.sections.length : null;
64
79
  return (
80
+ <div className="grid gap-3">
81
+ <PageGenerationStatus pageId={pageId} status={text(d.status)} onDone={onGenerated} />
65
82
  <KeyValueList
66
83
  columns={2}
67
84
  items={[
@@ -78,23 +95,40 @@ function PageDetails({ pageId }: { pageId: string }) {
78
95
  ...(d.error_message ? [{ label: "Last error", value: text(d.error_message) }] : []),
79
96
  ]}
80
97
  />
98
+ </div>
81
99
  );
82
100
  }
83
101
 
102
+ /** Landing pages per read for the counts; the list itself pages through PageList. */
103
+ const LANDING_PAGE_SIZE = 100;
104
+
84
105
  export function PagesScreen({ defaultDomain, renderDetailActions, extraDetailTabs, toolbarActions, className }: PagesScreenProps) {
85
106
  const [tabParam, setTabParam] = useUrlState("tab", "pages");
86
- const tab = (["pages", "content", "brand", "seo"].includes(tabParam) ? tabParam : "pages") as Tab;
107
+ const tab = (["pages", "content", "brand", "seo", "analytics"].includes(tabParam) ? tabParam : "pages") as Tab;
87
108
  const [pageId, setPageId] = useUrlState("id", "");
88
109
  const [view, setView] = useUrlState("view", "");
89
110
  const expandProps = { onExpand: () => setView("full"), onCollapse: view === "full" ? () => setView(null) : undefined };
90
111
  // The list owns its own fetch; bumping this remounts it after the editor changed something (publish, rename).
91
112
  const [rev, setRev] = useState(0);
92
- const pages = useG8Query((signal) => g8Get("/landing-pages", { limit: 100 }, { signal }), [rev]);
93
- const data = asRec(pages.data?.data);
94
- const rows = rowsOf<LandingPageRow>(data, "landing_pages");
95
- const total = num(data.total) ?? rows.length;
113
+ // The stat strip and the open page's name read EVERY landing page (100 per read, offset paged), so the counts are
114
+ // never those of a capped first read. `total` is the API's own count.
115
+ const apiTotal = useRef<number | undefined>(undefined);
116
+ const pages = useAllPages<LandingPageRow>(async (page, signal) => {
117
+ const offset = (page - 1) * LANDING_PAGE_SIZE;
118
+ const data = asRec((await g8Get("/landing-pages", { limit: LANDING_PAGE_SIZE, offset: offset || undefined }, { signal })).data);
119
+ const rows = rowsOf<LandingPageRow>(data, "landing_pages");
120
+ apiTotal.current = num(data.total) ?? apiTotal.current;
121
+ return { rows, hasNext: apiTotal.current != null ? offset + rows.length < apiTotal.current : rows.length >= LANDING_PAGE_SIZE };
122
+ }, [rev]);
123
+ const rows = pages.items;
124
+ const total = apiTotal.current ?? rows.length;
96
125
  const published = rows.filter((p) => p.status === "published").length;
97
126
  const current = tab === "pages" ? rows.find((p) => p.id === pageId) : undefined;
127
+ const bump = (): void => setRev((r) => r + 1);
128
+ // Pages whose generation "finished" per /status: held HERE, above the child that remounts on every rev, so a page the list still
129
+ // calls generating (a dead worker) refreshes the list once instead of looping.
130
+ const generatedOnce = useRef<Set<string>>(new Set());
131
+ const onGenerated = (id: string): void => { if (generatedOnce.current.has(id)) return; generatedOnce.current.add(id); bump(); };
98
132
 
99
133
  const detail = tab === "pages" && pageId ? (
100
134
  <DetailPanel
@@ -106,7 +140,11 @@ export function PagesScreen({ defaultDomain, renderDetailActions, extraDetailTab
106
140
  {...expandProps}
107
141
  tabs={[
108
142
  { id: "edit", label: "Edit", content: <PageEditor key={pageId} pageId={pageId} onSaved={() => setRev((r) => r + 1)} /> },
109
- { id: "details", label: "Details", content: <PageDetails key={`${pageId}:${rev}`} pageId={pageId} /> },
143
+ { id: "details", label: "Details", content: <PageDetails key={`${pageId}:${rev}`} pageId={pageId} onGenerated={() => onGenerated(pageId)} /> },
144
+ { id: "publishing", label: "Publishing", content: <PageDomains key={pageId} pageId={pageId} onChanged={bump} /> },
145
+ { id: "tracking", label: "Tracking", content: <PageTracking key={pageId} pageId={pageId} onChanged={bump} /> },
146
+ { id: "ai", label: "AI tools", content: <PageAiTools key={pageId} pageId={pageId} onChanged={bump} /> },
147
+ { id: "analytics", label: "Analytics", content: <PageAnalytics key={pageId} pageId={pageId} /> },
110
148
  ...(extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(pageId) })),
111
149
  ]}
112
150
  />
@@ -118,9 +156,10 @@ export function PagesScreen({ defaultDomain, renderDetailActions, extraDetailTab
118
156
  title="Pages"
119
157
  description="Landing pages, your content library, the brand kit and SEO in one place."
120
158
  icon={<Globe className="size-5" />}
121
- actions={toolbarActions}
159
+ actions={<>{tab === "pages" ? <PageClone onCreated={(id) => { bump(); setPageId(id); }} /> : null}{toolbarActions}</>}
122
160
  tabs={[
123
161
  { id: "pages", label: "Landing pages", count: total },
162
+ { id: "analytics", label: "Analytics" },
124
163
  { id: "content", label: "Content" },
125
164
  { id: "brand", label: "Brand kit" },
126
165
  { id: "seo", label: "SEO" },
@@ -130,12 +169,13 @@ export function PagesScreen({ defaultDomain, renderDetailActions, extraDetailTab
130
169
  stats={tab === "pages" ? [
131
170
  { label: "Landing pages", value: total.toLocaleString() },
132
171
  { label: "Published", value: published.toLocaleString(), tone: "positive" },
133
- { label: "Drafts", value: Math.max(0, rows.length - published).toLocaleString() },
172
+ { label: "Drafts", value: Math.max(0, total - published).toLocaleString() },
134
173
  ] : undefined}
135
174
  >
136
175
  {tab === "pages" ? (
137
176
  <>
138
- {pages.error ? <ApiError error={pages.error} title="Could not load landing pages" onRetry={() => void pages.refetch()} /> : null}
177
+ <FirstNotice shown={rows.length} limit={rows.length} force={pages.truncated} noun="landing pages" />
178
+ {pages.error ? <ApiError error={pages.error} title="Could not load landing pages" onRetry={pages.refetch} /> : null}
139
179
  <SplitView
140
180
  detailOpen={Boolean(pageId)}
141
181
  onCloseDetail={() => { setPageId(null); setView(null); }}
@@ -146,7 +186,8 @@ export function PagesScreen({ defaultDomain, renderDetailActions, extraDetailTab
146
186
  detail={detail}
147
187
  />
148
188
  </>
149
- ) : tab === "content" ? <ContentLibrary selectedId={pageId || null} onSelectedChange={(id) => setPageId(id)} />
189
+ ) : tab === "analytics" ? <PageAnalytics />
190
+ : tab === "content" ? <ContentLibrary selectedId={pageId || null} onSelectedChange={(id) => setPageId(id)} />
150
191
  : tab === "brand" ? <BrandKitEditor />
151
192
  : <SeoReport defaultDomain={defaultDomain} />}
152
193
  </ScreenLayout>
@@ -26,6 +26,7 @@ import { useG8Query } from "@/g8/hooks/use-g8-query";
26
26
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
27
27
  import { useJobPoll } from "@/g8/hooks/use-job-poll";
28
28
  import { ApiError } from "@/g8/components/api-error";
29
+ import { FirstNotice } from "@/g8/components/paging-notices";
29
30
  import { EmptyState } from "@/g8/components/empty-state";
30
31
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
31
32
  import { asRec, num, rowsOf, shortDate, text } from "../lib/studio";
@@ -110,12 +111,15 @@ function Keywords({ domain }: { domain: string }) {
110
111
  );
111
112
  }
112
113
 
114
+ /** The audits API has no paging: a full read is announced under the audit chips. */
115
+ const AUDIT_LIMIT = 10;
116
+
113
117
  export function SeoReport({ defaultDomain = "", className }: SeoReportProps) {
114
118
  const [domain, setDomain] = useState(defaultDomain);
115
119
  const [maxPages, setMaxPages] = useState(100);
116
120
  const [confirm, setConfirm] = useState(false);
117
121
  const [selected, setSelected] = useState<string | null>(null);
118
- const audits = useG8Query((signal) => g8Get("/seo/audits", { limit: 10 }, { signal }), []);
122
+ const audits = useG8Query((signal) => g8Get("/seo/audits", { limit: AUDIT_LIMIT }, { signal }), []);
119
123
  const start = useG8Mutation(() => g8Post("/seo/audits", { domain: domain.trim(), max_pages: maxPages }), {
120
124
  onSuccess: (res) => {
121
125
  setConfirm(false);
@@ -143,6 +147,7 @@ export function SeoReport({ defaultDomain = "", className }: SeoReportProps) {
143
147
  {text(a.domain)} · {shortDate(a.created_at)}
144
148
  </Button>
145
149
  ))}
150
+ <FirstNotice className="w-full" shown={rows.length} limit={AUDIT_LIMIT} noun="audits" />
146
151
  </div>
147
152
  ) : null}
148
153
  <div className="space-y-4">
@@ -8,6 +8,8 @@
8
8
  * Nothing here knows more than that, so a field the API adds later never breaks a screen.
9
9
  */
10
10
 
11
+ import type { PaginatedResponse } from "@graph8/sdk";
12
+
11
13
  export type Rec = Record<string, unknown>;
12
14
 
13
15
  export function asRec(value: unknown): Rec {
@@ -75,3 +77,8 @@ export function liveUrl(data: unknown): string {
75
77
  const rec = asRec(data);
76
78
  return text(rec.published_url) || text(rec.url) || text(asRec(rec.landing_page).published_url);
77
79
  }
80
+
81
+ /** Wrap rows and the API's "next" marker (a cursor or an offset; null or absent when there is no more) for `usePaginated`. */
82
+ export function pageOf<T>(rows: T[], next: unknown): PaginatedResponse<T> {
83
+ return { data: rows, pagination: { next_cursor: next == null || next === "" ? null : String(next) } } as unknown as PaginatedResponse<T>;
84
+ }
@@ -0,0 +1,74 @@
1
+ # phone-numbers: buy, assign, configure and release numbers
2
+
3
+ Every phone number the workspace owns, in one finished screen: see them all, search a carrier for new ones, buy one,
4
+ give it to a teammate or a voice agent, change how it answers, watch its usage against the daily limit, register for text
5
+ messaging (A2P 10DLC), and release it. Install with `npx @graph8/sdk add phone-numbers` (depends on `pickers`, for the
6
+ assignee picker). The dialer, sequencer and voice agents all pick numbers from this inventory.
7
+
8
+ > Buying a number spends credits and real money, releasing one is irreversible, and submitting a text registration
9
+ > reaches the carriers. Each of those opens a confirm dialog first; nothing here buys, releases or submits by itself.
10
+
11
+ ## Screens
12
+
13
+ - `PhoneNumbersScreen` (`components/phone-numbers-screen.tsx`): no required props.
14
+ - **Numbers** tab: stat strip (numbers, assigned, unassigned, calls today), search, filters (use, carrier, assignment,
15
+ status, kept in the URL as `n_use=...`), and the list. The list is paged on the client with **Load more** ("Showing 25
16
+ of 140 numbers"): the two list endpoints return the whole inventory and take no `limit`, so nothing is silently cut off.
17
+ A row opens the number beside the list (`?id=%2B14155550123`, add `&view=full` for a full page, `&dtab=usage` for a tab).
18
+ - **Buy a number** (sheet): choose a carrier, number type, area code, and digits or a city; search is free. Choose a number,
19
+ say what it is for (dialer, voice AI, personal work phone) and who gets it, then **Buy for about 250 credits** opens the
20
+ confirm dialog with the price. A 402 says nothing was bought and nothing was charged.
21
+ - **Add a number you own** (sheet): register a number you already hold with the voice platform. It buys nothing.
22
+ - **Text registration** tab: the A2P 10DLC status (brand, campaign, carrier feedback), a form that saves a draft, a
23
+ confirmed **Submit registration**, and **Retry stuck numbers**.
24
+ - The open number has tabs: **Overview**, **Assign** (MemberPicker, Assign to me, Unassign, Re-sync voice record),
25
+ **Usage** (calls today against the daily limit, 7-day calls, connect rate, confirmed counter reset), **Settings**
26
+ (answering mode, Telnyx caller-ID verification, who answers) and **Release** (release, or remove voice routing only).
27
+ - Slots: `toolbarActions`, `renderRowActions`, `renderDetailActions`, `extraDetailTabs`.
28
+ - Parts you can compose yourself: `PhoneNumberList`, `PhoneNumberDetail`, `NumberSearch`, `AssignNumber`, `NumberSettings`,
29
+ `NumberUsage`, `ReleaseNumber`, `RegisterOwnedNumber`, `A2pRegistration`, and the `usePhoneNumbers` hook.
30
+
31
+ ## What the API gives, and what it does not
32
+
33
+ - **There is no "list my phone numbers" operation in the `phoneNumbers` namespace.** The inventory is two reads, merged:
34
+ `GET /voice/dialer/numbers` (every number except the ones a voice AI agent holds, with usage) and
35
+ `GET /voice/agent-phone-numbers` (AI-capable numbers and which agent holds each). `usePhoneNumbers` does the merge.
36
+ - **No price read.** Prices on the purchase dialog are estimates: 250 credits held on purchase, then about 250 credits a
37
+ month in rental. The API reports no rental per number, so the block never labels any number "billable". Numbers the
38
+ platform provisioned for you (the onboarding receptionist line) are never rental-billed.
39
+ - **No operations for call forwarding, voicemail greetings or ring-group routing.** Settings says so and links to the
40
+ platform app instead of showing a dead control. Porting a number in is also an app flow.
41
+ - **Admin-only writes.** Reassigning, switching the answering mode and resetting counters are admin-only through the API; a
42
+ 403 is explained on the screen.
43
+ - **Telnyx numbers must be verified as a caller ID** before they can place calls; an unverified one shows as "Cannot dial"
44
+ and stays out of the dialer rotation. Twilio numbers need nothing.
45
+
46
+ ## Operations
47
+
48
+ | Flow | Operation | Tier |
49
+ |---|---|---|
50
+ | Inventory | `listDialerNumbers`, `listAgentPhoneNumbers` | read |
51
+ | Search | `searchAvailablePhoneNumbers` | read (free) |
52
+ | Buy | `purchasePhoneNumber` (Idempotency-Key) | **billable**, confirmed |
53
+ | Assign | `updatePhoneAssignee` (CRM + voice), `updateVoiceAssignee` (voice only; `""` clears the voice assignment, the API cannot clear the CRM owner) | write |
54
+ | Answering mode | `updatePhoneCapabilities` (voice AI, dialer), `updateVoiceInboundType` (rep, work phone) | write |
55
+ | Caller ID, who answers | `verifyVoiceCallerId`, `getVoiceAnsweringAgent` | write, read |
56
+ | Register an owned number | `provisionVoiceNumber` | write (buys nothing) |
57
+ | Release | `releasePhoneNumber` | **destructive**, type the number, then confirmed |
58
+ | Remove voice routing | `deleteVoiceNumber` (keeps the number and its carrier bill) | **destructive**, confirmed |
59
+ | Reset counters | `resetVoiceDailyCounts` (every number, lifts the daily cap) | **destructive**, confirmed |
60
+ | Text registration | `getPhoneA2pStatus`, `saveA2pInputs`, `startA2pRegistration`, `reconcileA2pRegistration` | read, write, **external** (submit is confirmed), write |
61
+
62
+ Scopes: `voice:read`, `voice:write`, `voice:run` (buy, submit A2P), `voice:delete` (release, remove routing, reset
63
+ counters) and `team:read` (the assignee picker).
64
+
65
+ ## Defaults (no need to ask the human)
66
+
67
+ - Carrier: Twilio (Telnyx is one select away). A new number is an **outbound dialer** number, owned by the identity behind the key unless an assignee is chosen.
68
+ - A number's use maps to the API as: dialer = `dialer: true`; voice AI = `dialer: false`; personal work phone = `dialer: true, work: true`.
69
+ - Answering mode: "Voice AI agent" and "Outbound dialer" go through `update-capabilities`; "Rep answers" and "Personal work phone" go through `inbound-type`. Both rebuild the number's routing, so the change takes a second click on a button that says the number answers nothing for a moment.
70
+ - Releasing needs the number typed first and then a confirm. If the carrier is unknown (a number only the agents list knows), the person chooses it: the wrong carrier releases nothing.
71
+ - Saving A2P details sends the whole object, merged over what was saved, so a field the form does not edit (an address) is never wiped. The form appears only after the saved details loaded.
72
+
73
+ Recipes (`recipes/`): search and buy (human approval required), assign or clear a rep, audit the inventory (read-only), and
74
+ save A2P details and check registration.