@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
+ * CreativeTools: AI help and housekeeping for one ad creative. Describe a change and the creative is edited, rewrite the
5
+ * copy or one field, review it and apply the exact fixes, make the other sizes, retry a failed generation, send it back
6
+ * to draft, or keep it as a template.
7
+ *
8
+ * SDK operations, in the order the screen uses them:
9
+ * 1. chatEditAdCreative POST /ad-creatives/{id}/chat-edit { instruction } (BILLABLE, confirmed)
10
+ * 2. regenerateAdCreativeCopy POST /ad-creatives/{id}/regenerate-copy (BILLABLE, confirmed)
11
+ * 3. rewriteAdCreativeField POST /ad-creatives/{id}/rewrite-field { field, instruction } (BILLABLE, confirmed; the previous text is shown so it can be put back)
12
+ * 4. reviewAdCreative POST /ad-creatives/{id}/ai-review { deep } (BILLABLE, confirmed; changes nothing)
13
+ * 5. applyAdCreativeReviewFindings POST /ad-creatives/{id}/ai-review/apply { finding_ids } (write, free)
14
+ * 6. createAdCreativeSizeSet POST /ad-creatives/{id}/size-set { formats? } (BILLABLE, confirmed: one new creative per format)
15
+ * 7. retryAdCreativeGeneration POST /ad-creatives/{id}/retry (BILLABLE, confirmed; only for a failed creative)
16
+ * 8. moveAdCreativeToDraft POST /ad-creatives/{id}/move-to-draft (write)
17
+ * 9. saveAdCreativeAsTemplate POST /ad-creatives/{id}/save-as-template (write)
18
+ * 10. updateAdCreative PATCH /ad-creatives/{id} { field } (write; "Put the old text back")
19
+ *
20
+ * The API quotes no price in advance and the balance is not checked here, so each dialog says what runs and how many
21
+ * AI passes it is; a quick check runs no model (0 credits). Nothing here publishes or launches an ad.
22
+ */
23
+ import { useState } from "react";
24
+ import { Button, Input, Label, Textarea } from "@graph8/sdk/ui";
25
+ import { ApiError } from "@/g8/components/api-error";
26
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
27
+ import { useConfirmAction } from "@/g8/components/confirmed-action";
28
+ import { EmptyState } from "@/g8/components/empty-state";
29
+ import { SectionCard } from "@/g8/components/screen/section-card";
30
+ import { StatusPill } from "@/g8/components/screen/status-pill";
31
+ import type { Tone } from "@/g8/components/screen/status-pill";
32
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
33
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
34
+ import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
35
+ import { asRec, num, rowsOf, str } from "../lib/ads-extra";
36
+ import type { Rec } from "../lib/ads-extra";
37
+
38
+ export interface CreativeToolsProps {
39
+ creativeId: string;
40
+ platform: string;
41
+ /** The creative's state: "failed" offers Retry, anything but "draft" offers Move to draft. */
42
+ status: string;
43
+ /** Called after the creative changed, so the detail reloads. */
44
+ onChanged?: () => void;
45
+ className?: string;
46
+ }
47
+
48
+ const FIELDS = [
49
+ { key: "headline", label: "Headline" },
50
+ { key: "primary_text", label: "Primary text" },
51
+ { key: "description", label: "Description" },
52
+ ] as const;
53
+ const SEVERITY: Record<string, Tone> = { error: "negative", critical: "negative", high: "negative", warning: "warning", medium: "warning", info: "neutral", low: "neutral" };
54
+ const PASS = "Uses credits; the API gives no price in advance and your balance is not checked here.";
55
+
56
+ export function CreativeTools({ creativeId, platform, status, onChanged, className }: CreativeToolsProps) {
57
+ const base = `/ad-creatives/${encodeURIComponent(creativeId)}`;
58
+ const { ask, dialog, failure } = useConfirmAction();
59
+ const [instruction, setInstruction] = useState("");
60
+ const [field, setField] = useState<(typeof FIELDS)[number]["key"]>("headline");
61
+ const [fieldInstruction, setFieldInstruction] = useState("");
62
+ const [reply, setReply] = useState("");
63
+ const [previous, setPrevious] = useState<{ field: string; value: string } | null>(null);
64
+ const [findings, setFindings] = useState<Rec[] | null>(null);
65
+ const [picked, setPicked] = useState<string[]>([]);
66
+ const [made, setMade] = useState("");
67
+ const [formats, setFormats] = useState<string[]>([]);
68
+ const specs = useG8Query((signal) => g8Get("/ad-creatives/specs", undefined, { signal }), [creativeId]);
69
+ const platformFormats = Object.entries(asRec(asRec(asRec(specs.data?.data).specs)[platform])).map(([key, v]) => ({ key, label: str(asRec(v).label) || key }));
70
+
71
+ const changed = (): void => onChanged?.();
72
+ const restore = useG8Mutation(() => g8Patch(base, { [previous?.field ?? "headline"]: previous?.value ?? "" }), { onSuccess: () => { setPrevious(null); changed(); } });
73
+ const apply = useG8Mutation(() => g8Post(`${base}/ai-review/apply`, { finding_ids: picked }), {
74
+ onSuccess: (res) => {
75
+ const d = asRec(res.data);
76
+ const left = new Set(rowsOf<Rec>(asRec(d.review).findings).map((f) => str(f.id)));
77
+ setFindings((cur) => (cur ?? []).filter((f) => left.has(str(f.id))));
78
+ setPicked([]);
79
+ setReply(`Applied ${num(d.applied) ?? 0} fix${num(d.applied) === 1 ? "" : "es"}.`);
80
+ changed();
81
+ },
82
+ });
83
+ const move = useG8Mutation(() => g8Post(`${base}/move-to-draft`), { onSuccess: changed });
84
+ const template = useG8Mutation(() => g8Post(`${base}/save-as-template`), { onSuccess: () => setReply("Saved as a template.") });
85
+
86
+ const chat = (): void => ask({
87
+ tier: "billable", title: "Edit this creative with AI", confirmLabel: "Edit with AI",
88
+ description: `One AI pass applies your instruction to the copy and image. ${PASS}`,
89
+ run: async () => { const d = asRec((await g8Post(`${base}/chat-edit`, { instruction: instruction.trim() })).data); setReply(str(d.reply) || "Edit applied."); setInstruction(""); }, onDone: changed,
90
+ });
91
+ const newCopy = (): void => ask({
92
+ tier: "billable", title: "Write new copy for this creative", confirmLabel: "Write new copy",
93
+ description: `One AI pass replaces the headline, primary text and description. The old text is not kept here (use the Revisions tab to go back). ${PASS}`,
94
+ run: async () => { await g8Post(`${base}/regenerate-copy`); setReply("New copy written."); }, onDone: changed,
95
+ });
96
+ const rewrite = (): void => ask({
97
+ tier: "billable", title: `Rewrite the ${FIELDS.find((f) => f.key === field)?.label.toLowerCase()}`, confirmLabel: "Rewrite",
98
+ description: `One AI pass rewrites this field only. ${PASS}`,
99
+ run: async () => { const d = asRec((await g8Post(`${base}/rewrite-field`, { field, instruction: fieldInstruction.trim() })).data); setPrevious({ field, value: str(d.previous_value) }); setFieldInstruction(""); }, onDone: changed,
100
+ });
101
+ const review = (deep: boolean): void => ask({
102
+ tier: "billable", title: deep ? "Review this creative with AI" : "Quick check of this creative", confirmLabel: deep ? "Review with AI" : "Run quick check",
103
+ estimatedCredits: deep ? undefined : 0,
104
+ description: deep ? `One AI pass checks the copy quality on top of the built-in checks. ${PASS} Nothing changes.` : "Runs the built-in checks only, no AI model: 0 credits. Nothing changes.",
105
+ run: async () => { const d = asRec((await g8Post(`${base}/ai-review`, { deep })).data); setFindings(rowsOf<Rec>(d.findings).filter((f) => str(f.id) !== "")); setPicked([]); },
106
+ });
107
+ const sizes = (): void => ask({
108
+ tier: "billable", title: formats.length ? `Make ${formats.length} more size${formats.length === 1 ? "" : "s"}` : `Make every other ${platform} size`, confirmLabel: "Make sizes",
109
+ description: `Creates one new creative per size with this copy kept word for word; each one generates an image. ${PASS}`,
110
+ run: async () => { const d = asRec((await g8Post(`${base}/size-set`, formats.length ? { formats } : {})).data); setMade(`${num(d.total) ?? rowsOf(d.ad_creatives).length} new creatives are being made. They appear in the gallery.`); }, onDone: changed,
111
+ });
112
+ const retry = (): void => ask({
113
+ tier: "billable", title: "Retry generating this creative", confirmLabel: "Retry",
114
+ description: `Runs the generation again from the same brief. ${PASS}`, run: () => g8Post(`${base}/retry`), onDone: changed,
115
+ });
116
+
117
+ return (
118
+ <div className={className}>
119
+ <div className="grid gap-4">
120
+ <SectionCard title="Edit with AI" description="Say what to change, for example: make it punchier and add the free trial.">
121
+ <form className="grid gap-2" onSubmit={(e) => { e.preventDefault(); if (instruction.trim()) chat(); }}>
122
+ <Label htmlFor="ct-chat" className="sr-only">What should change</Label>
123
+ <Textarea id="ct-chat" rows={2} value={instruction} onChange={(e) => setInstruction(e.target.value)} />
124
+ <div className="flex flex-wrap gap-2"><Button type="submit" disabled={!instruction.trim()}>Edit with AI</Button><Button type="button" variant="outline" onClick={newCopy}>Write all new copy</Button></div>
125
+ </form>
126
+ </SectionCard>
127
+ <SectionCard title="Rewrite one field">
128
+ <form className="flex flex-wrap items-end gap-2" onSubmit={(e) => { e.preventDefault(); if (fieldInstruction.trim()) rewrite(); }}>
129
+ <select aria-label="Field to rewrite" className="border-input bg-background h-9 rounded-md border px-2 text-sm" value={field} onChange={(e) => setField(e.target.value as typeof field)}>{FIELDS.map((f) => <option key={f.key} value={f.key}>{f.label}</option>)}</select>
130
+ <div className="min-w-48 flex-1"><Input aria-label="How to rewrite it" placeholder="Shorter, with a number" value={fieldInstruction} onChange={(e) => setFieldInstruction(e.target.value)} /></div>
131
+ <Button type="submit" disabled={!fieldInstruction.trim()}>Rewrite</Button>
132
+ </form>
133
+ {previous ? (
134
+ <div className="mt-2 flex items-center gap-2 text-sm" role="status"><span>Rewritten. Before: "{previous.value}"</span><Button size="sm" variant="outline" loading={restore.loading} onClick={() => void restore.mutate(undefined)}>Put the old text back</Button></div>
135
+ ) : null}
136
+ {restore.error ? <ApiError error={restore.error} title="Could not put the text back" /> : null}
137
+ </SectionCard>
138
+ <SectionCard title="Review" description="Check the creative against ad rules and copy quality."
139
+ actions={<div className="flex gap-1"><Button size="sm" variant="outline" onClick={() => review(false)}>Quick check</Button><Button size="sm" onClick={() => review(true)}>Review with AI</Button></div>}>
140
+ {findings === null ? <EmptyState compact title="No review yet" /> : findings.length === 0 ? <EmptyState compact title="Nothing to fix" description="The review found no remaining problems." /> : (
141
+ <div className="grid gap-2">
142
+ <ul className="grid gap-2">{findings.map((f) => (
143
+ <li key={str(f.id)} className="flex items-start gap-3 rounded-md border p-3">
144
+ <input type="checkbox" className="mt-1" aria-label={`Select ${str(f.title) || str(f.id)}`} checked={picked.includes(str(f.id))} onChange={() => setPicked((c) => (c.includes(str(f.id)) ? c.filter((x) => x !== str(f.id)) : [...c, str(f.id)]))} />
145
+ <div className="min-w-0 flex-1"><div className="flex items-center gap-2"><span className="text-sm font-medium">{str(f.title) || str(f.message) || str(f.id)}</span><StatusPill tone={SEVERITY[str(f.severity).toLowerCase()] ?? "neutral"}>{str(f.severity, "note")}</StatusPill></div>{str(f.description) ? <p className="text-muted-foreground text-xs">{str(f.description)}</p> : null}</div>
146
+ </li>
147
+ ))}</ul>
148
+ <div><Button size="sm" loading={apply.loading} disabled={picked.length === 0} onClick={() => void apply.mutate(undefined)}>Apply {picked.length || ""} selected (free)</Button></div>
149
+ </div>
150
+ )}
151
+ {apply.error ? <ApiError error={apply.error} title="Could not apply the fixes" /> : null}
152
+ </SectionCard>
153
+ <SectionCard title="Other sizes" description={`Make this creative in the other ${platform} formats with the same copy.`}>
154
+ {platformFormats.length === 0 ? <p className="text-muted-foreground text-sm">No other formats are listed for {platform}.</p> : (
155
+ <div className="grid gap-2">
156
+ <div className="flex flex-wrap gap-3">{platformFormats.map((f) => <label key={f.key} className="flex items-center gap-1 text-sm"><input type="checkbox" checked={formats.includes(f.key)} onChange={() => setFormats((c) => (c.includes(f.key) ? c.filter((x) => x !== f.key) : [...c, f.key]))} />{f.label}</label>)}</div>
157
+ <div><Button variant="outline" onClick={sizes}>{formats.length ? `Make ${formats.length} selected` : "Make all other sizes"}</Button></div>
158
+ </div>
159
+ )}
160
+ {made ? <p role="status" className="mt-2 text-sm">{made}</p> : null}
161
+ </SectionCard>
162
+ <SectionCard title="Housekeeping">
163
+ <div className="flex flex-wrap gap-2">
164
+ {status === "failed" ? <Button variant="outline" onClick={retry}>Retry generation</Button> : null}
165
+ {status !== "draft" && status !== "generating" ? <Button variant="outline" loading={move.loading} onClick={() => void move.mutate(undefined)}>Move back to draft</Button> : null}
166
+ <Button variant="outline" loading={template.loading} onClick={() => void template.mutate(undefined)}>Save as a template</Button>
167
+ </div>
168
+ {[move.error, template.error].filter(Boolean).map((e, i) => <ApiError key={i} error={e} title="That did not work" />)}
169
+ </SectionCard>
170
+ {reply ? <p role="status" className="text-sm">{reply}</p> : null}
171
+ {failure ? <ApiError error={failure} title="That did not work" /> : null}
172
+ </div>
173
+ <ConfirmSpendDialog {...dialog} />
174
+ </div>
175
+ );
176
+ }
@@ -26,8 +26,11 @@ import { g8Patch, g8Post } from "@/g8/lib/g8-client";
26
26
  import { useSocialAccounts } from "../hooks/use-social-accounts";
27
27
  import { formatInstant, isFuture, localInputToIso } from "../lib/social-ads-types";
28
28
  import type { PostVariant, SocialPost } from "../lib/social-ads-types";
29
+ import { useBrand } from "@/g8/hooks/use-brand";
29
30
 
30
31
  export interface PostComposerProps {
32
+ /** Start with this AI brief filled in (for example from a post idea). */
33
+ initialBrief?: string;
31
34
  /** Called after a post was saved, scheduled or published. */
32
35
  onSaved?: (post: SocialPost, action: "draft" | "scheduled" | "published") => void;
33
36
  className?: string;
@@ -36,13 +39,14 @@ export interface PostComposerProps {
36
39
  type Action = "schedule" | "publish";
37
40
  const newKey = () => globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;
38
41
 
39
- export function PostComposer({ onSaved, className }: PostComposerProps) {
42
+ export function PostComposer({ initialBrief, onSaved, className }: PostComposerProps) {
43
+ const brand = useBrand();
40
44
  const { accounts, loading, error, refetch } = useSocialAccounts();
41
45
  const [account, setAccount] = useState("");
42
46
  const [body, setBody] = useState("");
43
47
  const [imageUrl, setImageUrl] = useState("");
44
48
  const [when, setWhen] = useState("");
45
- const [brief, setBrief] = useState("");
49
+ const [brief, setBrief] = useState(initialBrief ?? "");
46
50
  const [variants, setVariants] = useState<PostVariant[]>([]);
47
51
  const [confirm, setConfirm] = useState<Action | "generate" | null>(null);
48
52
  const [notice, setNotice] = useState<string | null>(null);
@@ -76,7 +80,7 @@ export function PostComposer({ onSaved, className }: PostComposerProps) {
76
80
  });
77
81
 
78
82
  if (error) return <ApiError error={error} title="Could not load social accounts" onRetry={() => void refetch()} />;
79
- if (!loading && accounts.length === 0) return <EmptyState title="No social account connected" description="Connect a LinkedIn account in graph8 (Settings, Channels), then come back. It cannot be done through the API." />;
83
+ if (!loading && accounts.length === 0) return <EmptyState title="No social account connected" description={`Connect a LinkedIn account in ${brand.linkLabel} (Settings, Channels), then come back. It cannot be done through the API.`} />;
80
84
 
81
85
  const ready = body.trim().length > 0 && Boolean(chosen);
82
86
  const dialog = confirm === "publish"
@@ -0,0 +1,163 @@
1
+ "use client";
2
+
3
+ /**
4
+ * PostDetail: the complete screen for one social post. Loads the post by id (so `?id=` deep links work), shows every field
5
+ * (body, media, account, status, schedule, publish time, failure reason, link), and offers every action that fits its state:
6
+ * edit the draft, schedule, publish now, cancel a scheduled post, delete.
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. getSocialPost GET /social/posts/{post_id} (read)
10
+ * 2. updateSocialPost PATCH /social/posts/{post_id} (write; draft body)
11
+ * 3. scheduleSocialPost POST /social/posts/{post_id}/schedule (external, confirmed)
12
+ * 4. publishSocialPost POST /social/posts/{post_id}/publish (external, confirmed)
13
+ * 5. cancelSocialPost POST /social/posts/{post_id}/cancel (destructive, confirmed)
14
+ * 6. deleteSocialPost DELETE /social/posts/{post_id} (destructive, confirmed; 404 counts as done)
15
+ * 7. getSocialPostMetrics GET /social/posts/{post_id}/metrics (read; the Performance tab, only for a post that is live)
16
+ * Times are instants shown in the viewer's zone with its name.
17
+ */
18
+ import { useState } from "react";
19
+ import { Button, Input, Skeleton, Textarea } from "@graph8/sdk/ui";
20
+ import { ApiError } from "@/g8/components/api-error";
21
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
22
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
23
+ import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
24
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
25
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
26
+ import { SectionCard } from "@/g8/components/screen/section-card";
27
+ import { StatusPill } from "@/g8/components/screen/status-pill";
28
+ import { formatInstant, isFuture, localInputToIso } from "../lib/social-ads-types";
29
+ import type { SocialPost } from "../lib/social-ads-types";
30
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
31
+ import { AskButton, useConfirmAction } from "@/g8/components/confirmed-action";
32
+ import { PostMetrics } from "./social-performance";
33
+
34
+ export interface PostDetailProps {
35
+ postId: string;
36
+ onClose?: () => void;
37
+ onExpand?: () => void;
38
+ onCollapse?: () => void;
39
+ /** Called after any change (edit, publish, schedule, cancel, delete) so the list can reload. */
40
+ onChanged?: () => void;
41
+ /** Called after the post was deleted. */
42
+ onDeleted?: () => void;
43
+ }
44
+
45
+ const tone = (s: string) => (s === "live" ? "positive" : s === "failed" ? "negative" : s === "scheduled" ? "info" : "neutral") as "positive" | "negative" | "info" | "neutral";
46
+
47
+ export function PostDetail({ postId, onClose, onExpand, onCollapse, onChanged, onDeleted }: PostDetailProps) {
48
+ const { ask, dialog, failure } = useConfirmAction();
49
+ const query = useG8Query(async (signal) => (await g8Get<SocialPost>(`/social/posts/${postId}`, undefined, { signal })).data, [postId]);
50
+ const [body, setBody] = useState<string | null>(null);
51
+ const [when, setWhen] = useState("");
52
+ const save = useG8Mutation((text: string) => g8Patch(`/social/posts/${postId}`, { body: text }), {
53
+ onSuccess: () => {
54
+ setBody(null);
55
+ void query.refetch();
56
+ onChanged?.();
57
+ },
58
+ });
59
+ const post = query.data;
60
+ const changed = () => {
61
+ void query.refetch();
62
+ onChanged?.();
63
+ };
64
+
65
+ if (query.error) return <ApiError error={query.error} title="Could not load the post" onRetry={() => void query.refetch()} />;
66
+ if (!post) return <Skeleton className="h-40 w-full" />;
67
+ const editable = ["draft", "ready", "failed"].includes(post.status);
68
+ const preview = post.body.length > 60 ? `${post.body.slice(0, 60)}...` : post.body;
69
+
70
+ const actions = (
71
+ <div className="flex flex-wrap gap-1">
72
+ {editable ? (
73
+ <AskButton ask={ask} label="Publish now" tier="external" title={`Publish: "${preview}"`} confirmLabel="Publish"
74
+ description="The post goes live now on the connected account and cannot be recalled from here."
75
+ run={() => g8Post(`/social/posts/${postId}/publish`, {})} onDone={changed} />
76
+ ) : null}
77
+ {post.status === "scheduled" ? (
78
+ <AskButton ask={ask} label="Cancel schedule" tier="destructive" title={`Cancel: "${preview}"`} confirmLabel="Cancel post"
79
+ description="The scheduled post will not be published. It stays as a cancelled record."
80
+ run={() => g8Post(`/social/posts/${postId}/cancel`, {})} onDone={changed} />
81
+ ) : null}
82
+ {post.status !== "publishing" ? (
83
+ <AskButton ask={ask} label="Delete" variant="ghost" tier="destructive" title={`Delete: "${preview}"`} confirmLabel="Delete"
84
+ description="The post is removed. A post that is already live stays live on the network."
85
+ tolerateStatus={[404]} run={() => g8Delete(`/social/posts/${postId}`)} onDone={() => { onChanged?.(); onDeleted?.(); onClose?.(); }} />
86
+ ) : null}
87
+ </div>
88
+ );
89
+
90
+ const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
91
+ const tabs = [
92
+ {
93
+ id: "post",
94
+ label: "Post",
95
+ content: (
96
+ <div className="space-y-4">
97
+ <SectionCard title="Content">
98
+ {body == null ? (
99
+ <p className="whitespace-pre-wrap text-sm">{post.body}</p>
100
+ ) : (
101
+ <div className="space-y-2">
102
+ <Textarea aria-label="Post body" rows={8} value={body} onChange={(e) => setBody(e.target.value)} />
103
+ <div className="flex gap-2">
104
+ <Button size="sm" disabled={!body.trim() || save.loading} onClick={() => void save.mutate(body.trim())}>Save draft</Button>
105
+ <Button size="sm" variant="ghost" onClick={() => setBody(null)}>Cancel</Button>
106
+ </div>
107
+ {save.error ? <ApiError error={save.error} title="Could not save" /> : null}
108
+ </div>
109
+ )}
110
+ {editable && body == null ? <Button size="sm" variant="outline" className="mt-3" onClick={() => setBody(post.body)}>Edit text</Button> : null}
111
+ {(post.media ?? []).length > 0 ? (
112
+ <ul className="mt-3 grid grid-cols-2 gap-2">
113
+ {(post.media ?? []).map((m) => (
114
+ <li key={m.url}><img src={m.url} alt={m.alt ?? ""} className="aspect-video w-full rounded object-cover" /></li>
115
+ ))}
116
+ </ul>
117
+ ) : null}
118
+ </SectionCard>
119
+ <KeyValueList columns={2} items={[
120
+ { label: "Type", value: post.post_type },
121
+ { label: "Network", value: post.provider ?? "-" },
122
+ { label: "Account", value: post.account_ref ?? "-" },
123
+ { label: "Created", value: post.created_at ? formatInstant(post.created_at) : "-" },
124
+ { label: "Scheduled for", value: post.scheduled_at ? formatInstant(post.scheduled_at) : "-" },
125
+ { label: "Published", value: post.published_at ? formatInstant(post.published_at) : "-" },
126
+ ]} />
127
+ {post.failure_reason ? <p role="alert" className="text-sm text-error-foreground">{post.failure_reason}</p> : null}
128
+ {post.external_post_url ? <a className="text-sm underline" href={post.external_post_url} target="_blank" rel="noreferrer">View on the network</a> : null}
129
+ </div>
130
+ ),
131
+ },
132
+ ...(editable || post.status === "scheduled"
133
+ ? [{
134
+ id: "schedule",
135
+ label: "Schedule",
136
+ content: (
137
+ <SectionCard title="Schedule this post" description={`Pick a time in your zone (${zone}).`}>
138
+ <div className="flex flex-wrap items-center gap-2">
139
+ <Input type="datetime-local" aria-label="Publish at" value={when} onChange={(e) => setWhen(e.target.value)} className="w-56" />
140
+ {when && isFuture(when) ? (
141
+ <AskButton ask={ask} label="Schedule" tier="external" title={`Schedule: "${preview}"`} confirmLabel="Schedule"
142
+ description={`Publishes at ${formatInstant(localInputToIso(when))}.`}
143
+ run={() => g8Post(`/social/posts/${postId}/schedule`, { scheduled_at: localInputToIso(when) })} onDone={() => { setWhen(""); changed(); }} />
144
+ ) : (
145
+ <span className="text-xs text-muted-foreground">Choose a time at least a minute from now.</span>
146
+ )}
147
+ </div>
148
+ </SectionCard>
149
+ ),
150
+ }]
151
+ : []),
152
+ ...(post.status === "live" ? [{ id: "performance", label: "Performance", content: <PostMetrics postId={postId} /> }] : []),
153
+ ];
154
+
155
+ return (
156
+ <>
157
+ <DetailPanel title={preview || "Post"} subtitle={post.provider ?? undefined} badges={<StatusPill tone={tone(post.status)}>{post.status}</StatusPill>}
158
+ actions={actions} tabs={tabs} onClose={onClose} onExpand={onExpand} onCollapse={onCollapse} />
159
+ {failure ? <ApiError error={failure} title="That did not work" /> : null}
160
+ <ConfirmSpendDialog {...dialog} />
161
+ </>
162
+ );
163
+ }
@@ -0,0 +1,64 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SocialIdeas: post ideas mined from your own signals and content, with the reason each one fits. Pick one to start a
5
+ * draft with its hook as the brief.
6
+ *
7
+ * SDK operations:
8
+ * 1. listSocialIdeas GET /social/ideas?source (read; the ideas already mined. This screen never asks the API to mine new ones.)
9
+ *
10
+ * `onUse` receives a brief for the chosen idea; SocialScreen opens the composer with it. Nothing is written or spent here.
11
+ */
12
+ import { useState } from "react";
13
+ import { Button, Skeleton } from "@graph8/sdk/ui";
14
+ import { ApiError } from "@/g8/components/api-error";
15
+ import { EmptyState } from "@/g8/components/empty-state";
16
+ import { SectionCard } from "@/g8/components/screen/section-card";
17
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
18
+ import { g8Get } from "@/g8/lib/g8-client";
19
+ import { rowsOf, str } from "../lib/ads-extra";
20
+ import type { Rec } from "../lib/ads-extra";
21
+
22
+ export interface SocialIdeasProps {
23
+ /** Called with a brief built from the idea. */
24
+ onUse?: (brief: string, idea: Rec) => void;
25
+ className?: string;
26
+ }
27
+
28
+ const SOURCES = [
29
+ { value: "", label: "All sources" },
30
+ { value: "signal_mining", label: "Your signals" },
31
+ { value: "linkedin_trending", label: "Trending on LinkedIn" },
32
+ { value: "proof_freshness", label: "Fresh proof" },
33
+ { value: "repurpose", label: "Repurposed content" },
34
+ { value: "ai", label: "AI" },
35
+ ];
36
+
37
+ export function ideaBrief(idea: Rec): string {
38
+ return [str(idea.title), str(idea.primary_hook) ? `Hook: ${str(idea.primary_hook)}` : "", str(idea.concept), str(idea.proof_points) ? `Proof: ${str(idea.proof_points)}` : ""].filter(Boolean).join(". ");
39
+ }
40
+
41
+ export function SocialIdeas({ onUse, className }: SocialIdeasProps) {
42
+ const [source, setSource] = useState("");
43
+ const res = useG8Query((signal) => g8Get("/social/ideas", { source: source || undefined }, { signal }), [source]);
44
+ const ideas = rowsOf<Rec>(res.data?.data, "ideas");
45
+ return (
46
+ <SectionCard className={className} title="Post ideas" description="Starting points from your own signals and content."
47
+ actions={<select aria-label="Idea source" className="border-input bg-background h-8 rounded-md border px-2 text-sm" value={source} onChange={(e) => setSource(e.target.value)}>{SOURCES.map((s) => <option key={s.value} value={s.value}>{s.label}</option>)}</select>}>
48
+ {res.error ? <ApiError error={res.error} title="Could not load ideas" onRetry={() => void res.refetch()} /> : res.loading && !res.data ? <Skeleton className="h-24 w-full" aria-busy="true" /> : ideas.length === 0 ? (
49
+ <EmptyState compact title="No ideas yet" description="Ideas appear as signals and content worth posting about come in." />
50
+ ) : (
51
+ <ul className="grid gap-3">
52
+ {ideas.map((i) => (
53
+ <li key={str(i.id)} className="grid gap-1 rounded-md border p-3">
54
+ <span className="text-sm font-medium">{str(i.title)}</span>
55
+ {str(i.why) ? <p className="text-muted-foreground text-xs">{str(i.why)}</p> : null}
56
+ {str(i.primary_hook) ? <p className="text-sm">{str(i.primary_hook)}</p> : null}
57
+ {onUse ? <div><Button size="sm" variant="outline" aria-label={`Draft a post from ${str(i.title)}`} onClick={() => onUse(ideaBrief(i), i)}>Draft a post</Button></div> : null}
58
+ </li>
59
+ ))}
60
+ </ul>
61
+ )}
62
+ </SectionCard>
63
+ );
64
+ }
@@ -0,0 +1,80 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SocialPerformance: how your social posts perform. Totals for a window with a per-day table, and (as PostMetrics)
5
+ * the numbers for one post.
6
+ *
7
+ * SDK operations:
8
+ * 1. getSocialAnalytics GET /social/analytics?days (read; totals and per-day rows)
9
+ * 2. getSocialPostMetrics GET /social/posts/{id}/metrics (read; impressions, reactions, comments, shares)
10
+ *
11
+ * Both payloads are open: every numeric total is shown with its name, and a figure the network does not provide is a
12
+ * dash, never a zero. `supported: false` on a post means the network gives no numbers for it.
13
+ */
14
+ import { useState } from "react";
15
+ import { Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
16
+ import { ApiError } from "@/g8/components/api-error";
17
+ import { EmptyState } from "@/g8/components/empty-state";
18
+ import { SectionCard } from "@/g8/components/screen/section-card";
19
+ import { StatStrip } from "@/g8/components/screen/stat-strip";
20
+ import type { StatItem } from "@/g8/components/screen/stat-strip";
21
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
22
+ import { g8Get } from "@/g8/lib/g8-client";
23
+ import { asRec, num, rowsOf, str, whenText } from "../lib/ads-extra";
24
+ import type { Rec } from "../lib/ads-extra";
25
+
26
+ export interface SocialPerformanceProps {
27
+ className?: string;
28
+ }
29
+
30
+ const label = (k: string): string => k.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
31
+ const count = (v: unknown): string => (num(v) == null ? "-" : (v as number).toLocaleString());
32
+
33
+ export function SocialPerformance({ className }: SocialPerformanceProps) {
34
+ const [days, setDays] = useState(30);
35
+ const res = useG8Query((signal) => g8Get("/social/analytics", { days }, { signal }), [days]);
36
+ const d = asRec(res.data?.data);
37
+ const totals = Object.entries(asRec(d.totals)).filter(([, v]) => num(v) !== undefined);
38
+ const perDay = rowsOf<Rec>(d.per_day);
39
+ const columns = [...new Set(perDay.flatMap((r) => Object.entries(r).filter(([k, v]) => k !== "date" && num(v) !== undefined).map(([k]) => k)))];
40
+ const stats: StatItem[] = totals.map(([k, v]) => ({ label: label(k), value: count(v) }));
41
+ return (
42
+ <div className={className}>
43
+ <div className="mb-4"><select aria-label="Reporting window" className="border-input bg-background h-9 rounded-md border px-2 text-sm" value={days} onChange={(e) => setDays(Number(e.target.value))}>{[7, 30, 90].map((n) => <option key={n} value={n}>Last {n} days</option>)}</select></div>
44
+ {res.error ? <ApiError error={res.error} title="Could not load post performance" onRetry={() => void res.refetch()} /> : res.loading && !res.data ? <Skeleton className="h-24 w-full" aria-busy="true" /> : stats.length === 0 && perDay.length === 0 ? (
45
+ <EmptyState title="No performance numbers yet" description="Numbers appear after posts have been live for a while." />
46
+ ) : (
47
+ <div className="grid gap-4">
48
+ {stats.length ? <StatStrip items={stats} /> : null}
49
+ {perDay.length ? (
50
+ <SectionCard title="By day" flush>
51
+ <Table>
52
+ <TableHeader><TableRow><TableHead>Day</TableHead>{columns.map((c) => <TableHead key={c}>{label(c)}</TableHead>)}</TableRow></TableHeader>
53
+ <TableBody>{perDay.map((r, i) => <TableRow key={i}><TableCell className="font-medium">{str(r.date)}</TableCell>{columns.map((c) => <TableCell key={c}>{count(r[c])}</TableCell>)}</TableRow>)}</TableBody>
54
+ </Table>
55
+ </SectionCard>
56
+ ) : null}
57
+ </div>
58
+ )}
59
+ </div>
60
+ );
61
+ }
62
+
63
+ export function PostMetrics({ postId, className }: { postId: string; className?: string }) {
64
+ const res = useG8Query((signal) => g8Get(`/social/posts/${encodeURIComponent(postId)}/metrics`, undefined, { signal }), [postId]);
65
+ const d = asRec(res.data?.data);
66
+ return (
67
+ <SectionCard className={className} title="Post performance" description={d.fetched_at ? `Read ${whenText(d.fetched_at)}` : undefined}>
68
+ {res.error ? <ApiError error={res.error} title="Could not load the numbers" onRetry={() => void res.refetch()} /> : res.loading && !res.data ? <Skeleton className="h-16 w-full" aria-busy="true" /> : d.supported === false ? (
69
+ <EmptyState compact title="No numbers for this post" description="This network does not provide performance numbers for it." />
70
+ ) : (
71
+ <StatStrip items={[
72
+ { label: "Impressions", value: count(d.impressions) },
73
+ { label: "Reactions", value: count(d.reactions) },
74
+ { label: "Comments", value: count(d.comments) },
75
+ { label: "Shares", value: count(d.shares) },
76
+ ]} />
77
+ )}
78
+ </SectionCard>
79
+ );
80
+ }
@@ -28,6 +28,10 @@ export interface SocialPostListProps {
28
28
  /** Change to reload the list, for example after PostComposer saved a post. */
29
29
  refreshKey?: number;
30
30
  pageSize?: number;
31
+ /** Called when a person opens a post (show a PostDetail). */
32
+ onSelectPost?: (post: SocialPost) => void;
33
+ /** Highlights the open post. */
34
+ selectedId?: string | null;
31
35
  className?: string;
32
36
  }
33
37
 
@@ -39,7 +43,7 @@ const COPY: Record<Pending["kind"], { tier: ConfirmTier; verb: string; text: str
39
43
  delete: { tier: "destructive", verb: "Delete", text: "The post is removed. A post that is already live stays live on the network." },
40
44
  };
41
45
 
42
- export function SocialPostList({ refreshKey = 0, pageSize = 20, className }: SocialPostListProps) {
46
+ export function SocialPostList({ refreshKey = 0, pageSize = 20, onSelectPost, selectedId, className }: SocialPostListProps) {
43
47
  const [status, setStatus] = useState<(typeof STATUSES)[number]>("all");
44
48
  const [page, setPage] = useState(1);
45
49
  const [pending, setPending] = useState<Pending | null>(null);
@@ -87,9 +91,13 @@ export function SocialPostList({ refreshKey = 0, pageSize = 20, className }: Soc
87
91
  {!query.loading && !query.error && items.length === 0 ? <EmptyState title="No posts here" description="Write one with the composer." /> : null}
88
92
  <ul className="flex flex-col gap-2">
89
93
  {items.map((p) => (
90
- <li key={p.id} className="flex flex-wrap items-start justify-between gap-2 rounded-md border p-3" data-testid="post">
94
+ <li key={p.id} className={`flex flex-wrap items-start justify-between gap-2 rounded-md border p-3 ${selectedId === p.id ? "bg-muted/50" : ""}`} data-testid="post">
91
95
  <div className="flex min-w-0 flex-1 flex-col gap-1">
92
- <p className="line-clamp-3 text-sm">{p.body}</p>
96
+ {onSelectPost ? (
97
+ <button type="button" className="line-clamp-3 text-left text-sm hover:underline" onClick={() => onSelectPost(p)}>{p.body}</button>
98
+ ) : (
99
+ <p className="line-clamp-3 text-sm">{p.body}</p>
100
+ )}
93
101
  <p className="text-xs text-muted-foreground">
94
102
  {p.status}
95
103
  {p.status === "scheduled" && p.scheduled_at ? ` for ${formatInstant(p.scheduled_at)}` : ""}