@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,176 @@
1
+ "use client";
2
+
3
+ /**
4
+ * PageAnalytics: how landing pages perform. Visits, identified people and companies, form submissions and sign-ups
5
+ * for a window and a traffic source, per page and in total, plus who visited. Pass `pageId` to scope everything to
6
+ * one page (the page detail tab); omit it for the whole portfolio.
7
+ *
8
+ * SDK operations, in the order the screen uses them (all read, free):
9
+ * 1. getLandingPageReportingOverview GET /landing-pages/reporting/overview?days&source&page_id
10
+ * 2. listLandingPageReportingPeople GET /landing-pages/reporting/people?cursor&limit&days&source&page_id (paged, "Load more")
11
+ * 3. listLandingPageReportingCompanies GET /landing-pages/reporting/companies?cursor&limit&days&source&page_id (paged, "Load more")
12
+ *
13
+ * The payloads are open-shaped in the contract: only values that are really numbers are shown, a missing one is a
14
+ * dash (never a zero). When graph8 cannot identify visitors right now it says so instead of showing an empty list.
15
+ */
16
+ import { useState } from "react";
17
+ import { Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
18
+ import { ApiError } from "@/g8/components/api-error";
19
+ import { EmptyState } from "@/g8/components/empty-state";
20
+ import { SectionCard } from "@/g8/components/screen/section-card";
21
+ import { StatStrip } from "@/g8/components/screen/stat-strip";
22
+ import type { StatItem } from "@/g8/components/screen/stat-strip";
23
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
24
+ import { usePaginated } from "@/g8/hooks/use-paginated";
25
+ import { g8Get } from "@/g8/lib/g8-client";
26
+ import { asRec, num, pageOf, rowsOf, shortDate, text } from "../lib/studio";
27
+ import type { Rec } from "../lib/studio";
28
+ import { MoreRows } from "./more-rows";
29
+
30
+ export interface PageAnalyticsProps {
31
+ /** Scope every number to one page. Omit for all pages. */
32
+ pageId?: string;
33
+ className?: string;
34
+ }
35
+
36
+ const SOURCES = ["all", "paid", "email", "organic", "referral", "direct"] as const;
37
+ const SELECT = "border-input bg-background h-9 rounded-md border px-2 text-sm";
38
+ const PAGE = 50;
39
+
40
+ const count = (v: unknown): string => (num(v) == null ? "-" : (v as number).toLocaleString());
41
+ const rate = (v: unknown): string => (num(v) == null ? "-" : `${((v as number) * 100).toFixed(1)}%`);
42
+
43
+ function PeopleList({ query }: { query: Record<string, string | number | string[]> }) {
44
+ const list = usePaginated<Rec>(async (cursor, signal) => {
45
+ const res = await g8Get("/landing-pages/reporting/people", { ...query, cursor: cursor ? Number(cursor) : 0, limit: PAGE }, { signal });
46
+ const d = asRec(res.data);
47
+ return pageOf(rowsOf<Rec>(d, "people"), d.next_cursor ?? res.pagination?.next_cursor);
48
+ }, [JSON.stringify(query)]);
49
+ if (list.error) return <ApiError error={list.error} title="Could not load visitors" onRetry={() => void list.reload()} />;
50
+ if (list.loading) return <Skeleton className="h-24 w-full" aria-busy="true" />;
51
+ if (list.items.length === 0) return <EmptyState compact title="No identified people yet" description="People appear here once a visitor is matched to a contact, for example after they submit a form." />;
52
+ return (
53
+ <>
54
+ <Table>
55
+ <TableHeader><TableRow><TableHead>Person</TableHead><TableHead>Visits</TableHead><TableHead>Top source</TableHead><TableHead>Last seen</TableHead></TableRow></TableHeader>
56
+ <TableBody>
57
+ {list.items.map((p, i) => (
58
+ <TableRow key={text(p.contact_id) || i}>
59
+ <TableCell className="font-medium">{text(p.name, "Identified contact")}</TableCell>
60
+ <TableCell>{count(p.visits)}</TableCell>
61
+ <TableCell>{text(p.top_source) || "-"}</TableCell>
62
+ <TableCell>{shortDate(p.last_seen) || "-"}</TableCell>
63
+ </TableRow>
64
+ ))}
65
+ </TableBody>
66
+ </Table>
67
+ <MoreRows shown={list.items.length} noun="people" hasMore={list.hasMore} loading={list.loadingMore} onMore={() => void list.loadMore()} />
68
+ </>
69
+ );
70
+ }
71
+
72
+ function CompaniesList({ query }: { query: Record<string, string | number | string[]> }) {
73
+ const list = usePaginated<Rec>(async (cursor, signal) => {
74
+ const res = await g8Get("/landing-pages/reporting/companies", { ...query, cursor: cursor ? Number(cursor) : 0, limit: PAGE }, { signal });
75
+ const d = asRec(res.data);
76
+ return pageOf(rowsOf<Rec>(d, "companies"), d.next_cursor ?? res.pagination?.next_cursor);
77
+ }, [JSON.stringify(query)]);
78
+ if (list.error) return <ApiError error={list.error} title="Could not load companies" onRetry={() => void list.reload()} />;
79
+ if (list.loading) return <Skeleton className="h-24 w-full" aria-busy="true" />;
80
+ if (list.items.length === 0) return <EmptyState compact title="No identified companies yet" description="Companies appear when a visit can be resolved to the organization behind it." />;
81
+ return (
82
+ <>
83
+ <Table>
84
+ <TableHeader><TableRow><TableHead>Company</TableHead><TableHead>Visits</TableHead><TableHead>Top source</TableHead><TableHead>Last seen</TableHead></TableRow></TableHeader>
85
+ <TableBody>
86
+ {list.items.map((c, i) => (
87
+ <TableRow key={text(c.company_id) || i}>
88
+ <TableCell className="font-medium">{text(c.company_name) || text(c.name) || text(c.domain) || "Unknown company"}</TableCell>
89
+ <TableCell>{count(c.visits)}</TableCell>
90
+ <TableCell>{text(c.top_source) || "-"}</TableCell>
91
+ <TableCell>{shortDate(c.last_seen) || "-"}</TableCell>
92
+ </TableRow>
93
+ ))}
94
+ </TableBody>
95
+ </Table>
96
+ <MoreRows shown={list.items.length} noun="companies" hasMore={list.hasMore} loading={list.loadingMore} onMore={() => void list.loadMore()} />
97
+ </>
98
+ );
99
+ }
100
+
101
+ export function PageAnalytics({ pageId, className }: PageAnalyticsProps) {
102
+ const [days, setDays] = useState(30);
103
+ const [source, setSource] = useState<(typeof SOURCES)[number]>("all");
104
+ const query: Record<string, string | number | string[]> = { days, source, ...(pageId ? { page_id: [pageId] } : {}) };
105
+ const overview = useG8Query((signal) => g8Get("/landing-pages/reporting/overview", query, { signal }), [days, source, pageId]);
106
+
107
+ const o = asRec(overview.data?.data);
108
+ const totals = asRec(o.totals);
109
+ const pages = rowsOf<Rec>(o.pages);
110
+ const sources = rowsOf<Rec>(o.sources);
111
+ const stats: StatItem[] = [
112
+ { label: "Page views", value: count(totals.page_views) },
113
+ { label: "Unique visitors", value: count(totals.unique_visitors) },
114
+ { label: "Identified people", value: o.people_resolution_available === false ? "-" : count(totals.identified_people), hint: o.people_resolution_available === false ? "Visitor identification is not available right now." : undefined },
115
+ { label: "Identified companies", value: o.company_resolution_available === false ? "-" : count(totals.identified_companies), hint: o.company_resolution_available === false ? "Company matching is catching up. This is not zero." : undefined },
116
+ { label: "Form submissions", value: count(totals.form_submissions) },
117
+ { label: "Form conversion", value: rate(totals.form_conversion_rate) },
118
+ ];
119
+
120
+ return (
121
+ <div className={className}>
122
+ <div className="mb-4 flex flex-wrap items-center gap-2">
123
+ <select aria-label="Reporting window" className={SELECT} value={days} onChange={(e) => setDays(Number(e.target.value))}>
124
+ {[7, 30, 90].map((d) => <option key={d} value={d}>Last {d} days</option>)}
125
+ </select>
126
+ <select aria-label="Traffic source" className={SELECT} value={source} onChange={(e) => setSource(e.target.value as (typeof SOURCES)[number])}>
127
+ {SOURCES.map((s) => <option key={s} value={s}>{s === "all" ? "All sources" : s[0].toUpperCase() + s.slice(1)}</option>)}
128
+ </select>
129
+ </div>
130
+ {overview.error ? <ApiError error={overview.error} title="Could not load page analytics" onRetry={() => void overview.refetch()} /> : (
131
+ <div className="grid gap-4">
132
+ <StatStrip items={stats} loading={overview.loading && !overview.data} />
133
+ {o.stale === true ? <p role="note" className="text-muted-foreground text-xs">These numbers may be a few minutes old.</p> : null}
134
+ {!pageId ? (
135
+ <SectionCard title="By page" flush>
136
+ {pages.length === 0 ? <EmptyState compact title="No published pages in this window" description="Only published pages with a URL are measured." /> : (
137
+ <Table>
138
+ <TableHeader><TableRow><TableHead>Page</TableHead><TableHead>Views</TableHead><TableHead>Visitors</TableHead><TableHead>Forms</TableHead><TableHead>Top source</TableHead></TableRow></TableHeader>
139
+ <TableBody>
140
+ {pages.map((p, i) => (
141
+ <TableRow key={text(p.id) || i}>
142
+ <TableCell className="font-medium">{text(p.name) || text(p.title) || text(p.id)}</TableCell>
143
+ <TableCell>{count(p.page_views)}</TableCell>
144
+ <TableCell>{count(p.unique_visitors)}</TableCell>
145
+ <TableCell>{count(p.form_submissions)}</TableCell>
146
+ <TableCell>{text(p.top_source) || "-"}</TableCell>
147
+ </TableRow>
148
+ ))}
149
+ </TableBody>
150
+ </Table>
151
+ )}
152
+ </SectionCard>
153
+ ) : null}
154
+ {sources.length > 0 ? (
155
+ <SectionCard title="Traffic sources" flush>
156
+ <Table>
157
+ <TableHeader><TableRow><TableHead>Source</TableHead><TableHead>Views</TableHead><TableHead>Visitors</TableHead></TableRow></TableHeader>
158
+ <TableBody>
159
+ {sources.slice(0, 10).map((s, i) => (
160
+ <TableRow key={i}><TableCell className="font-medium">{text(s.channel) || "Unknown"}</TableCell><TableCell>{count(s.page_views)}</TableCell><TableCell>{count(s.unique_visitors)}</TableCell></TableRow>
161
+ ))}
162
+ </TableBody>
163
+ </Table>
164
+ </SectionCard>
165
+ ) : null}
166
+ <SectionCard title="Identified people" description="Visitors matched to a contact.">
167
+ <PeopleList query={query} />
168
+ </SectionCard>
169
+ <SectionCard title="Identified companies" description="Organizations behind the visits.">
170
+ <CompaniesList query={query} />
171
+ </SectionCard>
172
+ </div>
173
+ )}
174
+ </div>
175
+ );
176
+ }
@@ -0,0 +1,99 @@
1
+ "use client";
2
+
3
+ /**
4
+ * PageClone: turn an existing web page into an editable landing page, from its public URL or from HTML you paste.
5
+ *
6
+ * SDK operations, in the order the screen uses them:
7
+ * 1. cloneUrlToLandingPage POST /landing-pages/clone-url { url, name?, use_vision } (BILLABLE, confirmed)
8
+ * 2. cloneHtmlToLandingPage POST /landing-pages/clone-html { html, name?, source_url?, use_vision } (BILLABLE, confirmed)
9
+ *
10
+ * Both run AI passes; "match the look from a screenshot" is the expensive half and is on by default, as in the API.
11
+ * The API quotes no price in advance and the balance is not checked here, so the dialog says what runs. The new page
12
+ * is a draft: nothing is published. Pasted HTML is limited to 4 MiB (the proxy rejects more than 5 MiB).
13
+ */
14
+ import { useState } from "react";
15
+ import { Button, Input, Label, Textarea } from "@graph8/sdk/ui";
16
+ import { ApiError } from "@/g8/components/api-error";
17
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
18
+ import { useConfirmAction } from "@/g8/components/confirmed-action";
19
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
20
+ import { g8Post } from "@/g8/lib/g8-client";
21
+ import { asRec, text } from "../lib/studio";
22
+
23
+ export interface PageCloneProps {
24
+ /** Called with the new page's id once the clone is made. */
25
+ onCreated?: (pageId: string) => void;
26
+ /** The button label. */
27
+ label?: string;
28
+ className?: string;
29
+ }
30
+
31
+ export const MAX_HTML_BYTES = 4 * 1024 * 1024;
32
+
33
+ /** True for an http(s) URL with a host: what the clone endpoint can fetch. */
34
+ export function isPublicUrl(raw: string): boolean {
35
+ try {
36
+ const u = new URL(raw.trim());
37
+ return (u.protocol === "http:" || u.protocol === "https:") && u.hostname.includes(".");
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ export function PageClone({ onCreated, label = "Clone a page", className }: PageCloneProps) {
44
+ const { ask, dialog, failure } = useConfirmAction();
45
+ const [open, setOpen] = useState(false);
46
+ const [mode, setMode] = useState<"url" | "html">("url");
47
+ const [url, setUrl] = useState("");
48
+ const [html, setHtml] = useState("");
49
+ const [name, setName] = useState("");
50
+ const [vision, setVision] = useState(true);
51
+ const htmlBytes = new TextEncoder().encode(html).length;
52
+ const ready = mode === "url" ? isPublicUrl(url) : html.trim() !== "" && htmlBytes <= MAX_HTML_BYTES;
53
+
54
+ const reset = (): void => { setUrl(""); setHtml(""); setName(""); setVision(true); };
55
+ const go = (): void => ask({
56
+ tier: "billable",
57
+ title: mode === "url" ? `Clone ${url.trim()} into an editable page` : "Clone the pasted HTML into an editable page",
58
+ confirmLabel: "Clone page",
59
+ description: `Runs AI passes to rebuild the page as editable sections${vision ? ", including a screenshot pass so it looks like the original" : ""}. Uses credits; the API gives no price in advance and your balance is not checked here. The new page is a draft.`,
60
+ run: async () => {
61
+ const common = { name: name.trim() || undefined, use_vision: vision };
62
+ const res = mode === "url" ? await g8Post("/landing-pages/clone-url", { url: url.trim(), ...common }) : await g8Post("/landing-pages/clone-html", { html, ...common });
63
+ const id = text(asRec(res.data).id);
64
+ reset();
65
+ setOpen(false);
66
+ if (id) onCreated?.(id);
67
+ },
68
+ });
69
+
70
+ return (
71
+ <div className={className}>
72
+ <Button size="sm" variant="outline" onClick={() => setOpen(true)}>{label}</Button>
73
+ <FormSheet
74
+ open={open}
75
+ onOpenChange={setOpen}
76
+ title="Clone a page"
77
+ description="Rebuild a page you already have as an editable landing page. It starts as a draft."
78
+ width="lg"
79
+ footer={<><Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button><Button disabled={!ready} onClick={go}>Clone page</Button></>}
80
+ >
81
+ <div className="flex gap-1" role="tablist" aria-label="Clone source">
82
+ <Button size="sm" role="tab" aria-selected={mode === "url"} variant={mode === "url" ? "default" : "outline"} onClick={() => setMode("url")}>From a URL</Button>
83
+ <Button size="sm" role="tab" aria-selected={mode === "html"} variant={mode === "html" ? "default" : "outline"} onClick={() => setMode("html")}>From HTML</Button>
84
+ </div>
85
+ {mode === "url" ? (
86
+ <div><Label htmlFor="pc-url" className="mb-1">Page URL</Label><Input id="pc-url" type="url" placeholder="https://example.com/pricing" value={url} onChange={(e) => setUrl(e.target.value)} aria-invalid={url !== "" && !isPublicUrl(url)} />
87
+ {url !== "" && !isPublicUrl(url) ? <p role="alert" className="text-error-foreground mt-1 text-xs">Enter a full address starting with https://.</p> : null}</div>
88
+ ) : (
89
+ <div><Label htmlFor="pc-html" className="mb-1">HTML</Label><Textarea id="pc-html" rows={10} className="font-mono text-xs" value={html} onChange={(e) => setHtml(e.target.value)} />
90
+ {htmlBytes > MAX_HTML_BYTES ? <p role="alert" className="text-error-foreground mt-1 text-xs">This is {(htmlBytes / 1048576).toFixed(1)} MiB. The limit is 4 MiB.</p> : null}</div>
91
+ )}
92
+ <div><Label htmlFor="pc-name" className="mb-1">Name (optional)</Label><Input id="pc-name" value={name} onChange={(e) => setName(e.target.value)} /></div>
93
+ <label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={vision} onChange={(e) => setVision(e.target.checked)} />Match the look from a screenshot (uses more credits)</label>
94
+ {failure ? <ApiError error={failure} title="The clone did not work" /> : null}
95
+ </FormSheet>
96
+ <ConfirmSpendDialog {...dialog} />
97
+ </div>
98
+ );
99
+ }
@@ -0,0 +1,167 @@
1
+ "use client";
2
+
3
+ /**
4
+ * PageDomains: where one landing page is live. Lists every URL the page is bound to, takes one off the internet
5
+ * without unpublishing the page, and publishes the page to another of your verified domains.
6
+ *
7
+ * SDK operations, in the order the screen uses them:
8
+ * 1. listLandingPageBindings GET /landing-pages/{id}/bindings (read)
9
+ * 2. listLandingPageDomains GET /landing-pages/domains (read; the domains a page can be published on)
10
+ * 3. publishLandingPage POST /landing-pages/{id}/publish { domain_id, subdomain, path } (external, confirmed: a public URL)
11
+ * 4. removeLandingPageBinding DELETE /landing-pages/{id}/bindings?hostname&path (destructive, confirmed: that URL stops resolving)
12
+ * 5. exportLandingPage GET /landing-pages/{id}/export (read; downloads the page as one self-contained HTML file)
13
+ *
14
+ * Publishing to a domain needs the domain to be connected for hosting (the shared default domain is published from the
15
+ * Edit tab). A domain that is not ready is listed but cannot be chosen, with the reason. A URL already used by another
16
+ * page is refused by the API; nothing is taken over from here.
17
+ */
18
+ import { useState } from "react";
19
+ import { Download } from "lucide-react";
20
+ import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
21
+ import { ApiError } from "@/g8/components/api-error";
22
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
23
+ import { useConfirmAction } from "@/g8/components/confirmed-action";
24
+ import { EmptyState } from "@/g8/components/empty-state";
25
+ import { SectionCard } from "@/g8/components/screen/section-card";
26
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
27
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
28
+ import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
29
+ import { asRec, num, rowsOf, shortDate, text } from "../lib/studio";
30
+ import type { Rec } from "../lib/studio";
31
+
32
+ export interface PageDomainsProps {
33
+ pageId: string;
34
+ /** Called after a URL was added or removed, so the page list and status refresh. */
35
+ onChanged?: () => void;
36
+ className?: string;
37
+ }
38
+
39
+ const SELECT = "border-input bg-background h-9 rounded-md border px-2 text-sm";
40
+
41
+ /** The slash-led path the API stores: "" and "/" are the root. */
42
+ export function normalizePath(raw: string): string {
43
+ const p = raw.trim().replace(/\/+$/, "");
44
+ return p ? (p.startsWith("/") ? p : `/${p}`) : "/";
45
+ }
46
+
47
+ /** A subdomain label: lowercase letters, digits and hyphens, not starting or ending with a hyphen. Empty means the apex. */
48
+ export function validSubdomain(raw: string): boolean {
49
+ const s = raw.trim();
50
+ return s === "" || s === "@" || /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i.test(s);
51
+ }
52
+
53
+ export function PageDomains({ pageId, onChanged, className }: PageDomainsProps) {
54
+ const base = `/landing-pages/${encodeURIComponent(pageId)}`;
55
+ const { ask, dialog, failure } = useConfirmAction();
56
+ const bindings = useG8Query((signal) => g8Get(`${base}/bindings`, undefined, { signal }), [pageId]);
57
+ const domains = useG8Query((signal) => g8Get("/landing-pages/domains", undefined, { signal }), []);
58
+ const [domainId, setDomainId] = useState("");
59
+ const [subdomain, setSubdomain] = useState("");
60
+ const [path, setPath] = useState("");
61
+
62
+ const exportHtml = useG8Mutation(async () => {
63
+ const d = asRec((await g8Get(`${base}/export`)).data);
64
+ const html = text(d.html) || text(d.html_output);
65
+ if (!html) throw new Error("This page has no HTML to download yet.");
66
+ const url = URL.createObjectURL(new Blob([html], { type: "text/html" }));
67
+ const a = document.createElement("a");
68
+ a.href = url;
69
+ a.download = `${(text(d.name) || pageId).replace(/[^a-z0-9-_]+/gi, "-").toLowerCase()}.html`;
70
+ a.click();
71
+ URL.revokeObjectURL(url);
72
+ });
73
+ const changed = (): void => { void bindings.refetch(); onChanged?.(); };
74
+ const rows = rowsOf<Rec>(bindings.data?.data, "bindings");
75
+ const primary = asRec(asRec(bindings.data?.data).primary);
76
+ const available = rowsOf<Rec>(domains.data?.data, "domains");
77
+ const chosen = available.find((d) => String(d.id) === domainId);
78
+ const publishable = available.filter((d) => num(d.id) != null && d.inboxkit_connected === true);
79
+ const formOk = Boolean(chosen) && validSubdomain(subdomain);
80
+
81
+ const publish = (): void => {
82
+ if (!chosen) return;
83
+ const host = [subdomain.trim() && subdomain.trim() !== "@" ? subdomain.trim() : "", text(chosen.domain)].filter(Boolean).join(".");
84
+ const url = `https://${host}${normalizePath(path)}`;
85
+ ask({
86
+ tier: "external",
87
+ title: `Publish this page at ${url}`,
88
+ confirmLabel: "Publish",
89
+ description: "The page goes live at this address and anyone with the link can see it. A URL already used by another page is refused, nothing is replaced.",
90
+ run: () => g8Post(`${base}/publish`, { domain_id: Number(chosen.id), subdomain: subdomain.trim() === "@" ? "" : subdomain.trim(), path: normalizePath(path) }),
91
+ onDone: () => { setSubdomain(""); setPath(""); changed(); },
92
+ });
93
+ };
94
+
95
+ const remove = (b: Rec): void => {
96
+ const hostname = text(b.hostname);
97
+ const bindingPath = text(b.path, "/");
98
+ ask({
99
+ tier: "destructive",
100
+ title: `Take down ${hostname}${bindingPath === "/" ? "" : bindingPath}`,
101
+ confirmLabel: "Remove URL",
102
+ affected: 1,
103
+ description: "This URL stops serving the page. Links to it in ads and emails will 404. The page and its other URLs stay; DNS records are not touched.",
104
+ tolerateStatus: [404],
105
+ run: () => g8Delete(`${base}/bindings`, { query: { hostname, path: bindingPath } }),
106
+ onDone: changed,
107
+ });
108
+ };
109
+
110
+ return (
111
+ <div className={className}>
112
+ <div className="grid gap-4">
113
+ <SectionCard title="Live URLs" description="Every address that serves this page."
114
+ actions={<Button size="sm" variant="outline" loading={exportHtml.loading} onClick={() => void exportHtml.mutate(undefined)}><Download className="size-4" />Download HTML</Button>}>
115
+ {exportHtml.error ? <ApiError error={exportHtml.error} title="Could not download the page" /> : null}
116
+ {bindings.error ? <ApiError error={bindings.error} title="Could not load the URLs" onRetry={() => void bindings.refetch()} /> : bindings.loading && !bindings.data ? (
117
+ <Skeleton className="h-16 w-full" aria-busy="true" />
118
+ ) : rows.length === 0 ? (
119
+ <EmptyState compact title="No custom URLs" description={text(primary.published_url) ? `Live at ${text(primary.published_url)}.` : "This page is not published to a custom domain yet."} />
120
+ ) : (
121
+ <ul className="grid gap-2">
122
+ {rows.map((b) => (
123
+ <li key={`${text(b.hostname)}${text(b.path)}`} className="flex items-center justify-between gap-3 rounded-md border p-3">
124
+ <div className="min-w-0">
125
+ <span className="block truncate text-sm font-medium">{text(b.published_url) || `${text(b.hostname)}${text(b.path)}`}</span>
126
+ <span className="text-muted-foreground block text-xs">{b.published_at ? `Published ${shortDate(b.published_at)}` : "Published"}</span>
127
+ </div>
128
+ <Button size="sm" variant="ghost" aria-label={`Remove ${text(b.hostname)}`} onClick={() => remove(b)}>Remove</Button>
129
+ </li>
130
+ ))}
131
+ </ul>
132
+ )}
133
+ </SectionCard>
134
+
135
+ <SectionCard title="Publish to a domain" description="Choose one of your connected domains, then an optional subdomain and path.">
136
+ {domains.error ? <ApiError error={domains.error} title="Could not load your domains" onRetry={() => void domains.refetch()} /> : domains.loading && !domains.data ? (
137
+ <Skeleton className="h-16 w-full" aria-busy="true" />
138
+ ) : publishable.length === 0 ? (
139
+ <EmptyState compact title="No connected domains" description="Connect a domain for hosting first. The shared default domain is published from the Edit tab." />
140
+ ) : (
141
+ <form className="grid gap-3 sm:grid-cols-3" onSubmit={(e) => { e.preventDefault(); if (formOk) publish(); }}>
142
+ <div>
143
+ <Label htmlFor="pd-domain" className="mb-1">Domain</Label>
144
+ <select id="pd-domain" className={`${SELECT} w-full`} value={domainId} onChange={(e) => setDomainId(e.target.value)}>
145
+ <option value="">Choose a domain</option>
146
+ {publishable.map((d) => <option key={String(d.id)} value={String(d.id)}>{text(d.domain)}</option>)}
147
+ </select>
148
+ </div>
149
+ <div>
150
+ <Label htmlFor="pd-sub" className="mb-1">Subdomain (optional)</Label>
151
+ <Input id="pd-sub" value={subdomain} placeholder="www or empty for the root" onChange={(e) => setSubdomain(e.target.value)} aria-invalid={!validSubdomain(subdomain)} />
152
+ </div>
153
+ <div>
154
+ <Label htmlFor="pd-path" className="mb-1">Path (optional)</Label>
155
+ <Input id="pd-path" value={path} placeholder="/pricing" onChange={(e) => setPath(e.target.value)} />
156
+ </div>
157
+ {!validSubdomain(subdomain) ? <p role="alert" className="text-error-foreground text-xs sm:col-span-3">A subdomain can only hold letters, numbers and hyphens.</p> : null}
158
+ <div className="sm:col-span-3"><Button type="submit" disabled={!formOk}>Publish to this domain</Button></div>
159
+ </form>
160
+ )}
161
+ </SectionCard>
162
+ {failure ? <ApiError error={failure} title="That did not work" /> : null}
163
+ </div>
164
+ <ConfirmSpendDialog {...dialog} />
165
+ </div>
166
+ );
167
+ }
@@ -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
+ }
@@ -25,6 +25,7 @@ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
25
25
  import type { ConfirmTier } from "@/g8/components/confirm-spend-dialog";
26
26
  import { asRec, num, rowsOf, shortDate, text } from "../lib/studio";
27
27
  import type { LandingPageRow } from "../lib/studio";
28
+ import { plural } from "@/g8/lib/plural";
28
29
 
29
30
  export interface PageListProps {
30
31
  /** Called with the page id when a row is opened. Wire it to your PageEditor route. */
@@ -145,7 +146,7 @@ export function PageList({ onOpen, onDeleted, pageSize = 25, className }: PageLi
145
146
  )}
146
147
 
147
148
  <div className="mt-3 flex items-center justify-between text-sm">
148
- <span className="text-muted-foreground">{total} {total === 1 ? "page" : "pages"}</span>
149
+ <span className="text-muted-foreground">{plural(total, "page")}</span>
149
150
  <div className="flex gap-2">
150
151
  <Button size="sm" variant="outline" disabled={offset === 0} onClick={() => setOffset(Math.max(0, offset - pageSize))}>Previous</Button>
151
152
  <Button size="sm" variant="outline" disabled={offset + pageSize >= total} onClick={() => setOffset(offset + pageSize)}>Next</Button>