@graph8/sdk 0.370.0 → 0.373.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (466) hide show
  1. package/README.md +19 -0
  2. package/blocks/_foundation/README.md +14 -0
  3. package/blocks/_foundation/block.json +44 -0
  4. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  5. package/blocks/_foundation/components/empty-state.tsx +5 -2
  6. package/blocks/_foundation/components/list-select.tsx +112 -0
  7. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  8. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  9. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  10. package/blocks/_foundation/components/screen/index.ts +1 -0
  11. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  12. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  13. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  14. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  15. package/blocks/_foundation/lib/paging.ts +57 -0
  16. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  17. package/blocks/agents/README.md +81 -4
  18. package/blocks/agents/block.json +311 -2
  19. package/blocks/agents/components/agent-activity.tsx +112 -0
  20. package/blocks/agents/components/agent-approvals.tsx +135 -0
  21. package/blocks/agents/components/agent-builder.tsx +17 -3
  22. package/blocks/agents/components/agent-conversations.tsx +143 -0
  23. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
  24. package/blocks/agents/components/agent-operators.tsx +164 -0
  25. package/blocks/agents/components/agent-run-detail.tsx +20 -3
  26. package/blocks/agents/components/agent-runs.tsx +3 -3
  27. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  28. package/blocks/agents/components/agent-test-history.tsx +121 -0
  29. package/blocks/agents/components/agents-screen.tsx +53 -11
  30. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  31. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  32. package/blocks/agents/components/from-template-button.tsx +65 -0
  33. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  34. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  35. package/blocks/agents/components/knowledge-form.tsx +69 -0
  36. package/blocks/agents/components/name-dialog.tsx +32 -0
  37. package/blocks/agents/components/receptionist-button.tsx +46 -0
  38. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  39. package/blocks/agents/components/skill-editor.tsx +147 -0
  40. package/blocks/agents/components/skill-library.tsx +111 -0
  41. package/blocks/agents/lib/agent-types.ts +7 -1
  42. package/blocks/agents/lib/facts.tsx +13 -0
  43. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  44. package/blocks/agents/lib/load-more.tsx +141 -0
  45. package/blocks/agents/lib/skill-utils.ts +91 -0
  46. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  47. package/blocks/analytics/components/analytics-screen.tsx +16 -3
  48. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  49. package/blocks/analytics/components/report-builder.tsx +15 -2
  50. package/blocks/appointments/README.md +6 -0
  51. package/blocks/appointments/block.json +27 -0
  52. package/blocks/appointments/components/booking-page.tsx +2 -2
  53. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  54. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  55. package/blocks/appointments/components/meeting-list.tsx +29 -13
  56. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  57. package/blocks/campaigns/components/campaign-assets.tsx +1 -1
  58. package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
  59. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  60. package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
  61. package/blocks/campaigns/components/icp-detail.tsx +11 -2
  62. package/blocks/campaigns/components/icp-editor.tsx +8 -3
  63. package/blocks/campaigns/components/persona-detail.tsx +11 -2
  64. package/blocks/campaigns/components/persona-editor.tsx +6 -1
  65. package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
  66. package/blocks/coach/components/call-scorecard.tsx +7 -7
  67. package/blocks/company-intelligence/README.md +132 -0
  68. package/blocks/company-intelligence/block.json +318 -0
  69. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  70. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  71. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  72. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  73. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  74. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  75. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  76. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  77. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  78. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  79. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  80. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  81. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  82. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  83. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  84. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  85. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  86. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  87. package/blocks/copilot/components/copilot-panel.tsx +1 -1
  88. package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
  89. package/blocks/crm/README.md +6 -0
  90. package/blocks/crm/block.json +53 -4
  91. package/blocks/crm/components/companies-screen.tsx +23 -9
  92. package/blocks/crm/components/company-detail.tsx +2 -2
  93. package/blocks/crm/components/contact-detail.tsx +4 -4
  94. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  95. package/blocks/crm/components/contacts-screen.tsx +31 -13
  96. package/blocks/crm/components/dedupe-review.tsx +6 -1
  97. package/blocks/crm/components/import-wizard.tsx +3 -7
  98. package/blocks/crm/components/list-select.tsx +20 -0
  99. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  100. package/blocks/crm/components/record-form.tsx +1 -1
  101. package/blocks/crm/components/tasks-screen.tsx +35 -15
  102. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  103. package/blocks/crm/hooks/use-page-query.ts +3 -1
  104. package/blocks/crm/lib/crm-types.ts +14 -0
  105. package/blocks/crm/lib/read-lists.ts +16 -0
  106. package/blocks/deals/README.md +24 -6
  107. package/blocks/deals/block.json +151 -2
  108. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  109. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  110. package/blocks/deals/components/deal-detail.tsx +2 -2
  111. package/blocks/deals/components/deal-filters.tsx +65 -0
  112. package/blocks/deals/components/deal-line-items.tsx +120 -0
  113. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  114. package/blocks/deals/components/deal-related.tsx +30 -9
  115. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  116. package/blocks/deals/components/deals-screen.tsx +57 -22
  117. package/blocks/deals/components/forecast-view.tsx +4 -4
  118. package/blocks/deals/components/pipeline-board.tsx +8 -6
  119. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  120. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  121. package/blocks/deals/components/stage-editor.tsx +1 -1
  122. package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
  123. package/blocks/deals/lib/deal-extras.ts +156 -0
  124. package/blocks/dialer/README.md +14 -5
  125. package/blocks/dialer/block.json +17 -2
  126. package/blocks/dialer/components/call-contact-button.tsx +26 -9
  127. package/blocks/dialer/components/call-log-screen.tsx +4 -1
  128. package/blocks/dialer/components/call-log.tsx +16 -4
  129. package/blocks/dialer/components/call-queue.tsx +3 -3
  130. package/blocks/dialer/components/dialer-screen.tsx +69 -27
  131. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  132. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  133. package/blocks/dialer/components/session-start-form.tsx +58 -11
  134. package/blocks/dialer/components/softphone.tsx +1 -1
  135. package/blocks/enrichment/README.md +16 -6
  136. package/blocks/enrichment/block.json +19 -7
  137. package/blocks/enrichment/components/bulk-enrich-panel.tsx +76 -27
  138. package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
  139. package/blocks/enrichment/components/enrich-button.tsx +1 -1
  140. package/blocks/enrichment/components/enrichment-screen.tsx +70 -17
  141. package/blocks/enrichment/components/waterfall-config.tsx +27 -7
  142. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  143. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  144. package/blocks/forms/README.md +8 -0
  145. package/blocks/forms/block.json +107 -0
  146. package/blocks/forms/components/form-activity.tsx +71 -0
  147. package/blocks/forms/components/form-backfill.tsx +108 -0
  148. package/blocks/forms/components/form-builder.tsx +1 -1
  149. package/blocks/forms/components/form-embed.tsx +1 -1
  150. package/blocks/forms/components/form-groups.tsx +151 -0
  151. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  152. package/blocks/forms/components/form-route-history.tsx +72 -0
  153. package/blocks/forms/components/form-saved-views.tsx +81 -0
  154. package/blocks/forms/components/forms-screen.tsx +51 -17
  155. package/blocks/forms/components/more-rows.tsx +30 -0
  156. package/blocks/forms/components/submissions-export.tsx +67 -0
  157. package/blocks/forms/components/submissions-table.tsx +15 -6
  158. package/blocks/forms/lib/forms-extra.ts +67 -0
  159. package/blocks/inbox/README.md +5 -0
  160. package/blocks/inbox/block.json +15 -0
  161. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  162. package/blocks/inbox/components/thread-badge.tsx +2 -2
  163. package/blocks/inbox/components/thread-view.tsx +1 -1
  164. package/blocks/integrations/README.md +54 -1
  165. package/blocks/integrations/block.json +189 -15
  166. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  167. package/blocks/integrations/components/connection-health.tsx +59 -0
  168. package/blocks/integrations/components/crm-detail.tsx +20 -1
  169. package/blocks/integrations/components/crm-records.tsx +103 -0
  170. package/blocks/integrations/components/field-mappings.tsx +61 -0
  171. package/blocks/integrations/components/integration-list.tsx +4 -4
  172. package/blocks/integrations/components/integrations-screen.tsx +20 -3
  173. package/blocks/integrations/components/migration-panel.tsx +133 -0
  174. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  175. package/blocks/integrations/components/sync-status.tsx +6 -6
  176. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  177. package/blocks/integrations/components/webhook-detail.tsx +26 -7
  178. package/blocks/integrations/components/webhook-manager.tsx +30 -17
  179. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  180. package/blocks/integrations/lib/crm-utils.ts +39 -0
  181. package/blocks/integrations/lib/facts.tsx +13 -0
  182. package/blocks/integrations/lib/integrations-utils.ts +5 -0
  183. package/blocks/integrations/lib/load-more.tsx +141 -0
  184. package/blocks/intent/components/account-intent-score.tsx +5 -5
  185. package/blocks/intent/components/intent-feed.tsx +8 -8
  186. package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
  187. package/blocks/intent/lib/intent-utils.ts +4 -4
  188. package/blocks/lifecycle/components/account-related.tsx +8 -1
  189. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  190. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  191. package/blocks/lists/README.md +7 -1
  192. package/blocks/lists/block.json +31 -3
  193. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  194. package/blocks/lists/components/add-to-list-button.tsx +17 -7
  195. package/blocks/lists/components/list-picker.tsx +13 -3
  196. package/blocks/lists/components/list-table.tsx +1 -1
  197. package/blocks/lists/components/lists-screen.tsx +4 -2
  198. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  199. package/blocks/lists/components/suppression-manager.tsx +27 -6
  200. package/blocks/lists/components/suppressions-screen.tsx +21 -12
  201. package/blocks/mailboxes/block.json +4 -0
  202. package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
  203. package/blocks/mailboxes/components/domain-detail.tsx +10 -4
  204. package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
  205. package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
  206. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  207. package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
  208. package/blocks/mailboxes/components/warmup-health.tsx +20 -20
  209. package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
  210. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  211. package/blocks/newsletter/README.md +12 -0
  212. package/blocks/newsletter/block.json +117 -0
  213. package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
  214. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  215. package/blocks/newsletter/components/more-rows.tsx +30 -0
  216. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  217. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  218. package/blocks/newsletter/components/newsletter-screen.tsx +36 -13
  219. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  220. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  221. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  222. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  223. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  224. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  225. package/blocks/nurture/components/nurture-builder.tsx +10 -2
  226. package/blocks/nurture/components/nurture-enroll.tsx +6 -7
  227. package/blocks/nurture/components/nurture-form.tsx +5 -8
  228. package/blocks/nurture/components/nurture-screen.tsx +16 -5
  229. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  230. package/blocks/objects/README.md +1 -1
  231. package/blocks/objects/block.json +13 -5
  232. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  233. package/blocks/objects/components/object-record-detail.tsx +8 -1
  234. package/blocks/objects/components/object-schema-editor.tsx +1 -1
  235. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  236. package/blocks/pages/README.md +14 -0
  237. package/blocks/pages/block.json +113 -1
  238. package/blocks/pages/components/more-rows.tsx +30 -0
  239. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  240. package/blocks/pages/components/page-analytics.tsx +176 -0
  241. package/blocks/pages/components/page-clone.tsx +99 -0
  242. package/blocks/pages/components/page-domains.tsx +167 -0
  243. package/blocks/pages/components/page-editor.tsx +1 -1
  244. package/blocks/pages/components/page-generation-status.tsx +48 -0
  245. package/blocks/pages/components/page-tracking.tsx +137 -0
  246. package/blocks/pages/components/pages-screen.tsx +57 -16
  247. package/blocks/pages/components/seo-report.tsx +6 -1
  248. package/blocks/pages/lib/studio.ts +7 -0
  249. package/blocks/phone-numbers/README.md +74 -0
  250. package/blocks/phone-numbers/block.json +262 -0
  251. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  252. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  253. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  254. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  255. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  256. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  257. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  258. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  259. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  260. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  261. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  262. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  263. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  264. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  265. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  266. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  267. package/blocks/pickers/README.md +23 -0
  268. package/blocks/pickers/block.json +103 -0
  269. package/blocks/pickers/components/company-picker.tsx +90 -0
  270. package/blocks/pickers/components/contact-picker.tsx +131 -0
  271. package/blocks/pickers/components/member-picker.tsx +99 -0
  272. package/blocks/pickers/components/search-picker.tsx +98 -0
  273. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  274. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  275. package/blocks/pickers/lib/use-debounced.ts +13 -0
  276. package/blocks/quotes/README.md +13 -0
  277. package/blocks/quotes/block.json +48 -1
  278. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  279. package/blocks/quotes/components/quote-builder.tsx +1 -1
  280. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  281. package/blocks/quotes/components/quote-list.tsx +5 -3
  282. package/blocks/quotes/components/quote-related.tsx +3 -2
  283. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  284. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  285. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  286. package/blocks/radar/components/radar-overview.tsx +4 -4
  287. package/blocks/radar/components/radar-screen.tsx +2 -2
  288. package/blocks/sequencer/README.md +6 -0
  289. package/blocks/sequencer/block.json +28 -1
  290. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  291. package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
  292. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  293. package/blocks/sequencer/components/sequence-builder.tsx +19 -20
  294. package/blocks/sequencer/components/sequence-detail.tsx +4 -2
  295. package/blocks/sequencer/components/sequence-enrollments.tsx +4 -2
  296. package/blocks/sequencer/components/sequence-list.tsx +1 -1
  297. package/blocks/sequencer/components/sequence-stats.tsx +8 -8
  298. package/blocks/sequencer/components/sequences-screen.tsx +3 -0
  299. package/blocks/sequencer/components/step-editor.tsx +11 -11
  300. package/blocks/sequencer/lib/sequencer-utils.ts +5 -5
  301. package/blocks/social-ads/README.md +8 -0
  302. package/blocks/social-ads/block.json +137 -4
  303. package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
  304. package/blocks/social-ads/components/audience-sync.tsx +6 -2
  305. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  306. package/blocks/social-ads/components/campaign-detail.tsx +6 -1
  307. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  308. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  309. package/blocks/social-ads/components/creative-detail.tsx +6 -1
  310. package/blocks/social-ads/components/creative-gallery.tsx +20 -2
  311. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  312. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  313. package/blocks/social-ads/components/post-composer.tsx +4 -2
  314. package/blocks/social-ads/components/post-detail.tsx +3 -0
  315. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  316. package/blocks/social-ads/components/social-performance.tsx +80 -0
  317. package/blocks/social-ads/components/social-screen.tsx +33 -6
  318. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  319. package/blocks/surveys/README.md +91 -0
  320. package/blocks/surveys/block.json +299 -0
  321. package/blocks/surveys/components/account-nps.tsx +69 -0
  322. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  323. package/blocks/surveys/components/org-summary.tsx +82 -0
  324. package/blocks/surveys/components/sending-domains.tsx +113 -0
  325. package/blocks/surveys/components/survey-form.tsx +126 -0
  326. package/blocks/surveys/components/survey-responses.tsx +145 -0
  327. package/blocks/surveys/components/survey-results.tsx +72 -0
  328. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  329. package/blocks/surveys/components/survey-send.tsx +162 -0
  330. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  331. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  332. package/blocks/surveys/lib/surveys-types.ts +228 -0
  333. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  334. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  335. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  336. package/blocks/visitors/components/snippet-install.tsx +18 -9
  337. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  338. package/blocks/visitors/components/visitor-company-card.tsx +24 -14
  339. package/blocks/visitors/components/visitor-feed.tsx +5 -5
  340. package/blocks/webchat/components/chat-widget-config.tsx +1 -1
  341. package/blocks/webchat/components/conversation-list.tsx +1 -1
  342. package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
  343. package/blocks/workbench/components/run-progress.tsx +1 -1
  344. package/blocks/workbench/components/workbench-grid.tsx +1 -1
  345. package/blocks/workbench/components/workbench-screen.tsx +15 -5
  346. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  347. package/blocks/workflows/components/workflow-editor.tsx +1 -1
  348. package/dist/cli.js +536 -392
  349. package/dist/index.browser.js +1 -1
  350. package/dist/index.browser.js.map +1 -1
  351. package/dist/index.browser.mjs +1 -1
  352. package/dist/index.browser.mjs.map +1 -1
  353. package/dist/index.d.mts +2 -2
  354. package/dist/index.d.ts +2 -2
  355. package/dist/index.js +1 -1
  356. package/dist/index.js.map +1 -1
  357. package/dist/index.mjs +1 -1
  358. package/dist/index.mjs.map +1 -1
  359. package/dist/react.browser.js +1 -1
  360. package/dist/react.browser.js.map +1 -1
  361. package/dist/react.browser.mjs +1 -1
  362. package/dist/react.browser.mjs.map +1 -1
  363. package/dist/react.d.mts +2 -2
  364. package/dist/react.d.ts +2 -2
  365. package/dist/react.js +1 -1
  366. package/dist/react.js.map +1 -1
  367. package/dist/react.mjs +1 -1
  368. package/dist/react.mjs.map +1 -1
  369. package/dist/types/blocks/brand.d.ts +11 -2
  370. package/dist/types/blocks/create.d.ts +7 -3
  371. package/dist/types/blocks/doctor.d.ts +1 -0
  372. package/dist/types/blocks/flows.d.ts +7 -1
  373. package/dist/types/blocks/orgBrand.d.ts +34 -0
  374. package/dist/types/cli.d.ts +3 -0
  375. package/dist/types/version.d.ts +2 -2
  376. package/docs/api/desk.d.ts +2 -2
  377. package/docs/api/desk.md +3 -3
  378. package/package.json +1 -1
  379. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  380. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  381. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  382. package/stacks/6sense.json +17 -4
  383. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  384. package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
  385. package/stacks/apollo/pages/contacts.tsx.tmpl +29 -95
  386. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  387. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  388. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  389. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  390. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  391. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  392. package/stacks/apollo.json +36 -8
  393. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  394. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  395. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  396. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  397. package/stacks/calendly.json +8 -4
  398. package/stacks/clay/pages/companies.tsx.tmpl +17 -1
  399. package/stacks/clay/pages/contacts.tsx.tmpl +12 -0
  400. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  401. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  402. package/stacks/clay.json +16 -3
  403. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  404. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  405. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  406. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  407. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  408. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  409. package/stacks/gong.json +23 -6
  410. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  411. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  412. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  413. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  414. package/stacks/hootsuite.json +9 -4
  415. package/stacks/hubspot/pages/companies.tsx.tmpl +14 -35
  416. package/stacks/hubspot/pages/contacts.tsx.tmpl +29 -95
  417. package/stacks/hubspot/pages/deals.tsx.tmpl +10 -1
  418. package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
  419. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  420. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  421. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  422. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  423. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  424. package/stacks/hubspot.json +21 -5
  425. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  426. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  427. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  428. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  429. package/stacks/intercom.json +8 -4
  430. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  431. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  432. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  433. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  434. package/stacks/mailchimp.json +20 -5
  435. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  436. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  437. package/stacks/outreach/pages/contacts.tsx.tmpl +27 -66
  438. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  439. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  440. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  441. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  442. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  443. package/stacks/outreach.json +25 -7
  444. package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
  445. package/stacks/pipedrive/pages/contacts.tsx.tmpl +15 -35
  446. package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
  447. package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
  448. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  449. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  450. package/stacks/pipedrive.json +4 -2
  451. package/stacks/salesforce/pages/companies.tsx.tmpl +14 -35
  452. package/stacks/salesforce/pages/contacts.tsx.tmpl +20 -35
  453. package/stacks/salesforce/pages/deals.tsx.tmpl +10 -1
  454. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  455. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  456. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  457. package/stacks/salesforce.json +7 -3
  458. package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
  459. package/stacks/zoominfo/pages/contacts.tsx.tmpl +12 -0
  460. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  461. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  462. package/stacks/zoominfo.json +16 -3
  463. package/styles/styles.css +1 -1
  464. package/templates/next-app/app/globals.css.tmpl +349 -64
  465. package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
  466. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
@@ -0,0 +1,161 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ResearchRun: start the website research, follow it step by step, and reset it. This is the workspace's OWN company
5
+ * research (the API locks it to one primary website): it collects the company profile, products, keywords, competitors,
6
+ * news, audience questions and brand assets that the rest of the block shows.
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. getIntelligencePrimaryWebsite / getIntelligenceOrgDomain / getLatestIntelligenceTask (read, through the hooks)
10
+ * 2. listIntelligenceDocuments GET /intelligence/documents (read) how many records a reset would remove
11
+ * 3. analyzeWebsite POST /intelligence/analyze (BILLABLE) scrapes and runs models; ConfirmSpendDialog first
12
+ * 4. getIntelligenceStatus GET /intelligence/status/{task_id} (read) polled until the run stops
13
+ * 5. resetIntelligence DELETE /intelligence (DESTRUCTIVE) removes every record and run, unlocks the website; ConfirmSpendDialog first
14
+ *
15
+ * Cost: the API quotes no price in advance and the run is metered by usage, so the confirm states the quantity it can
16
+ * state (how many sources are collected) and whether the call would only return a run from the last hour (which does
17
+ * nothing new and costs nothing). A reset states the real number of records it deletes.
18
+ */
19
+ import { useEffect, useRef, useState } from "react";
20
+ import { RotateCcw, Sparkles } from "lucide-react";
21
+ import { Button, Input, Label } from "@graph8/sdk/ui";
22
+ import { ApiError } from "@/g8/components/api-error";
23
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
24
+ import { useConfirmAction } from "@/g8/components/confirmed-action";
25
+ import { SectionCard } from "@/g8/components/screen/section-card";
26
+ import { StatusPill, toneForStatus } from "@/g8/components/screen/status-pill";
27
+ import { useBrand } from "@/g8/hooks/use-brand";
28
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
29
+ import { useJobPoll } from "@/g8/hooks/use-job-poll";
30
+ import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
31
+ import { plural } from "@/g8/lib/plural";
32
+ import { INTEL_KEY, refreshIntelligence, useLatestRun, useResearchTarget } from "../hooks/use-intelligence";
33
+ import { finishedWithinHour, humanize, intelBody, isRunFinal, rowsOf, runSteps, stepTone, when } from "../lib/intelligence-types";
34
+ import type { AnalyzeAccepted, IntelDocument, RunStatus } from "../lib/intelligence-types";
35
+
36
+ export interface ResearchRunProps {
37
+ /** Called after a run was started, finished or reset, so a parent can refresh its own numbers. */
38
+ onChanged?: () => void;
39
+ className?: string;
40
+ }
41
+
42
+ export function ResearchRun({ onChanged, className }: ResearchRunProps) {
43
+ const brand = useBrand();
44
+ const { ask, dialog, failure } = useConfirmAction();
45
+ const target = useResearchTarget();
46
+ const latest = useLatestRun();
47
+ const [typed, setTyped] = useState("");
48
+ const [force, setForce] = useState(false);
49
+ const [taskId, setTaskId] = useState<string | null>(null);
50
+ // The poll reads the id from a ref: start() runs its first tick before React re-renders with the new state.
51
+ const polled = useRef<string | null>(null);
52
+
53
+ const docs = useG8Query(async (signal) => rowsOf<IntelDocument>(intelBody(await g8Get("/intelligence/documents", undefined, { signal })), "documents"), [], { key: [INTEL_KEY, "documents"] });
54
+ const activeId = taskId ?? (latest.run && !isRunFinal(latest.run.status) ? latest.run.task_id ?? null : null);
55
+ const shownId = activeId ?? latest.run?.task_id ?? null;
56
+ const status = useJobPoll<RunStatus>(
57
+ async (signal) => intelBody<RunStatus>(await g8Get(`/intelligence/status/${encodeURIComponent(polled.current ?? "")}`, undefined, { signal })),
58
+ { enabled: false, intervalMs: 3000, maxAttempts: 200, isTerminal: (s) => isRunFinal(s.status), onDone: () => { void refreshIntelligence(); onChanged?.(); } },
59
+ );
60
+ useEffect(() => {
61
+ if (!activeId) return undefined;
62
+ polled.current = activeId;
63
+ status.start();
64
+ return () => status.stop();
65
+ // start/stop are stable; the poll restarts only when the run being followed changes.
66
+ // eslint-disable-next-line react-hooks/exhaustive-deps
67
+ }, [activeId]);
68
+
69
+ // The status of the run on screen: the polled one, else a one-time read of the last finished run.
70
+ const lastRun = useG8Query(async (signal) => (shownId && !activeId ? intelBody<RunStatus>(await g8Get(`/intelligence/status/${encodeURIComponent(shownId)}`, undefined, { signal })) : null), [shownId, activeId], { key: [INTEL_KEY, "run", shownId ?? "none", activeId ? "live" : "done"] });
71
+ const run: RunStatus | null = (activeId ? status.status : lastRun.data) ?? null;
72
+
73
+ const website = target.primary ?? (typed.trim() || null);
74
+ const recent = finishedWithinHour(latest.run) && !force;
75
+ const sources = runSteps(run ?? undefined).length;
76
+ const records = docs.data?.length;
77
+
78
+ const askResearch = () => {
79
+ if (!website) return;
80
+ ask({
81
+ tier: "billable",
82
+ title: `Research ${website}`,
83
+ confirmLabel: "Start research",
84
+ description: (
85
+ <>
86
+ {recent ? <p>A run finished less than an hour ago, so this returns that run and does no new work. Tick "Run again" to collect fresh data.</p>
87
+ : <p>{sources > 0 ? `Collects ${plural(sources, "source")} from the website` : "Collects the company profile, products, keywords, competitors, news and brand assets from the website"}, with scraping and models. The API gives no price in advance: it is metered by usage.</p>}
88
+ {!target.primary ? <p>This website becomes your workspace's research website and is locked: you can only change it by resetting the research.</p> : null}
89
+ </>
90
+ ),
91
+ run: async () => {
92
+ const env = await g8Post("/intelligence/analyze", force ? { website_url: website, force: true } : { website_url: website });
93
+ const accepted = intelBody<AnalyzeAccepted>(env);
94
+ if (accepted.task_id) setTaskId(accepted.task_id);
95
+ },
96
+ onDone: () => { void refreshIntelligence(); void target.refetch(); onChanged?.(); },
97
+ });
98
+ };
99
+
100
+ const askReset = () => ask({
101
+ tier: "destructive",
102
+ title: "Reset company research",
103
+ confirmLabel: "Reset",
104
+ affected: records,
105
+ description: <p>Deletes every collected source and every research run, and unlocks the research website so you can pick another. Wiki documents and research reports are not touched. Collecting the data again uses credits.</p>,
106
+ run: async () => { await g8Delete("/intelligence"); setTaskId(null); polled.current = null; status.stop(); },
107
+ onDone: () => { void refreshIntelligence(); void target.refetch(); onChanged?.(); },
108
+ });
109
+
110
+ const steps = runSteps(run ?? undefined);
111
+ const running = run ? !isRunFinal(run.status) : false;
112
+ return (
113
+ <SectionCard
114
+ className={className}
115
+ title="Website research"
116
+ description={`Collects what ${brand.linkLabel} learns about your company from its website.`}
117
+ icon={<Sparkles className="size-4" aria-hidden />}
118
+ actions={<Button type="button" variant="outline" size="sm" onClick={askReset} disabled={!target.primary && !records}><RotateCcw className="size-3.5" aria-hidden />Reset research</Button>}
119
+ >
120
+ <div className="space-y-4">
121
+ {target.error ? <ApiError error={target.error} title="Could not load the research website" onRetry={() => void target.refetch()} compact /> : null}
122
+ <div className="flex flex-wrap items-end gap-2">
123
+ {target.primary ? (
124
+ <div className="space-y-1">
125
+ <Label htmlFor="intel-website">Research website</Label>
126
+ <Input id="intel-website" readOnly value={target.primary} className="w-72" />
127
+ </div>
128
+ ) : (
129
+ <div className="space-y-1">
130
+ <Label htmlFor="intel-website">Website to research</Label>
131
+ <Input id="intel-website" placeholder="acme.com" value={typed} onChange={(e) => setTyped(e.target.value)} className="w-72" />
132
+ {target.suggested && !typed ? <button type="button" className="text-xs underline underline-offset-2" onClick={() => setTyped(target.suggested ?? "")}>Use {target.suggested} from your workspace settings</button> : null}
133
+ </div>
134
+ )}
135
+ <Button type="button" onClick={askResearch} disabled={!website || running}>{running ? "Researching..." : latest.run ? "Research again" : "Research website"}</Button>
136
+ {finishedWithinHour(latest.run) ? (
137
+ <label className="flex items-center gap-1.5 text-sm"><input type="checkbox" checked={force} onChange={(e) => setForce(e.target.checked)} />Run again even though one finished in the last hour</label>
138
+ ) : null}
139
+ </div>
140
+ {target.primary ? <p className="text-xs text-muted-foreground">The research is locked to this website. Reset the research to use another.</p> : null}
141
+ {failure ? <ApiError error={failure} title="That did not work" /> : null}
142
+ {status.error && activeId ? <ApiError error={status.error} title="Could not read the run's progress" compact /> : null}
143
+ {run ? (
144
+ <div className="space-y-2" aria-live="polite">
145
+ <div className="flex flex-wrap items-center gap-2 text-sm">
146
+ <StatusPill tone={toneForStatus(run.status ?? "")}>{humanize(run.status ?? "unknown")}</StatusPill>
147
+ {run.current_step && running ? <span className="text-muted-foreground">{run.current_step}</span> : null}
148
+ {typeof run.progress === "number" ? <span className="tabular-nums text-muted-foreground">{run.progress}%</span> : null}
149
+ {when(run.completed_at) ? <span className="text-xs text-muted-foreground">Finished {when(run.completed_at)}</span> : null}
150
+ </div>
151
+ {run.error_message ? <p role="alert" className="text-sm text-error-foreground">{run.error_message}</p> : null}
152
+ <ul className="grid gap-1 sm:grid-cols-2">
153
+ {steps.map((s) => <li key={s.key} className="flex items-center justify-between gap-2 text-sm"><span>{s.label}</span><StatusPill tone={stepTone(s.state)} quiet>{humanize(s.state)}</StatusPill></li>)}
154
+ </ul>
155
+ </div>
156
+ ) : latest.loading ? null : <p className="text-sm text-muted-foreground">No research has run yet.</p>}
157
+ </div>
158
+ <ConfirmSpendDialog {...dialog} />
159
+ </SectionCard>
160
+ );
161
+ }
@@ -0,0 +1,117 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SourceDetail: the complete screen for one intelligence source. Shows what it collected (SourceView), when, whether a
5
+ * person edited it, and can re-collect it. A source that was not collected says so and offers the same button.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. getIntelligencePrimaryWebsite GET /intelligence/primary-website (read, through useResearchTarget)
9
+ * 2. getIntelligenceData GET /intelligence/data/{data_type}?website_url (read) 404 means "not collected"
10
+ * 3. retryIntelligenceSource POST /intelligence/retry (BILLABLE) re-collects ONE source; ConfirmSpendDialog first
11
+ * Re-collecting keeps the last good data if it fails. The API gives no price; the confirm states the one source it touches.
12
+ */
13
+ import { useRef, useState, type ReactNode } from "react";
14
+ import { RefreshCw } from "lucide-react";
15
+ import { Button, Skeleton } 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 { EmptyState } from "@/g8/components/empty-state";
20
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
21
+ import { KeyValueList, EmptyValue } from "@/g8/components/screen/key-value-list";
22
+ import { StatusPill, toneForStatus } from "@/g8/components/screen/status-pill";
23
+ import { useJobPoll } from "@/g8/hooks/use-job-poll";
24
+ import { useBrand } from "@/g8/hooks/use-brand";
25
+ import { g8Post } from "@/g8/lib/g8-client";
26
+ import { refreshIntelligence, useResearchTarget, useSourceEntry } from "../hooks/use-intelligence";
27
+ import { humanize, isRec, text, when } from "../lib/intelligence-types";
28
+ import type { Rec } from "../lib/intelligence-types";
29
+ import { SourceView } from "./source-view";
30
+
31
+ export interface SourceDetailProps {
32
+ dataType: string;
33
+ onClose?: () => void;
34
+ onExpand?: () => void;
35
+ onCollapse?: () => void;
36
+ onChanged?: () => void;
37
+ /** Extra header actions from the screen's `renderDetailActions` slot. */
38
+ extraActions?: ReactNode;
39
+ /** Extra tabs from the screen's `extraDetailTabs` slot, after the built-in ones. */
40
+ extraTabs?: { id: string; label: string; content: ReactNode }[];
41
+ }
42
+
43
+ const is404 = (e: unknown): boolean => (e as { status?: number } | null)?.status === 404;
44
+
45
+ export function SourceDetail({ dataType, onClose, onExpand, onCollapse, onChanged, extraActions, extraTabs }: SourceDetailProps) {
46
+ const brand = useBrand();
47
+ const { ask, dialog, failure } = useConfirmAction();
48
+ const target = useResearchTarget();
49
+ const host = target.primary;
50
+ const [waiting, setWaiting] = useState(false);
51
+ const before = useRef<string | null>(null);
52
+ const query = useSourceEntry(dataType, host, !target.loading);
53
+ const entry = query.data && isRec(query.data) ? query.data : null;
54
+ const collectedAt = text(entry?.collected_at);
55
+
56
+ // After a re-collect the work runs in the background: look again until the collection time moves.
57
+ useJobPoll<Rec | undefined>(async () => (await query.refetch()) ?? undefined, {
58
+ enabled: waiting, intervalMs: 4000, maxAttempts: 30,
59
+ isTerminal: (e) => text(e?.collected_at) !== before.current,
60
+ onDone: () => { setWaiting(false); void refreshIntelligence(); onChanged?.(); },
61
+ });
62
+
63
+ const title = text(entry?.display_name) ?? humanize(dataType);
64
+ const askRetry = () => ask({
65
+ tier: "billable",
66
+ title: `Re-collect ${title}`,
67
+ confirmLabel: "Re-collect",
68
+ description: <p>Collects this one source again from {host} with scraping and models. If it fails, the last good data stays. The API gives no price in advance: it is metered by usage.</p>,
69
+ run: async () => { before.current = collectedAt; await g8Post("/intelligence/retry", { website_url: host, data_type: dataType }); setWaiting(true); },
70
+ onDone: () => { void refreshIntelligence(); },
71
+ });
72
+ const retry = (
73
+ <Button type="button" size="sm" variant="outline" disabled={!host || waiting} onClick={askRetry} title={host ? undefined : "Start the website research first"}>
74
+ <RefreshCw className="size-3.5" aria-hidden />{waiting ? "Collecting..." : "Re-collect"}
75
+ </Button>
76
+ );
77
+ const actions = <div className="flex flex-wrap items-center gap-2">{retry}{extraActions}</div>;
78
+
79
+ let body: ReactNode;
80
+ if (query.error && !is404(query.error)) body = <ApiError error={query.error} title="Could not load the source" onRetry={() => void query.refetch()} />;
81
+ else if (!query.data && !query.error) body = <Skeleton className="h-40 w-full" />;
82
+ else if (!entry) body = <EmptyState title="Not collected" description="This source has no data. Re-collect it, or run the website research again." compact />;
83
+ else body = <SourceView dataType={dataType} data={entry.data} />;
84
+
85
+ const edits = isRec(entry?.overrides) ? Object.keys(entry.overrides as Rec).length : 0;
86
+ const tabs = [
87
+ { id: "data", label: "Data", content: <div className="space-y-3">{edits ? <p className="text-xs text-muted-foreground">{edits === 1 ? "1 field was" : `${edits} fields were`} edited by a person in {brand.linkLabel}; the values below include the edits.</p> : null}{body}</div> },
88
+ {
89
+ id: "details", label: "Details",
90
+ content: (
91
+ <KeyValueList columns={2} items={[
92
+ { label: "Source", value: dataType, mono: true },
93
+ { label: "Website", value: text(entry?.website_url) ?? host ?? <EmptyValue /> },
94
+ { label: "Collected", value: when(collectedAt) ?? <EmptyValue /> },
95
+ { label: "Last edited", value: when(entry?.edited_at) ? `${when(entry?.edited_at)}${text(entry?.edited_by_name) ? ` by ${text(entry?.edited_by_name)}` : ""}` : <EmptyValue /> },
96
+ { label: "Note", value: text(entry?.note) ?? <EmptyValue />, wide: true },
97
+ ]} />
98
+ ),
99
+ },
100
+ ];
101
+ return (
102
+ <>
103
+ <DetailPanel
104
+ title={title}
105
+ subtitle={host ?? undefined}
106
+ badges={entry ? <StatusPill tone={toneForStatus(text(entry.status) ?? "completed")}>{humanize(text(entry.status) ?? "completed")}</StatusPill> : undefined}
107
+ actions={actions}
108
+ tabs={extraTabs?.length ? [...tabs, ...extraTabs] : tabs}
109
+ onClose={onClose}
110
+ onExpand={onExpand}
111
+ onCollapse={onCollapse}
112
+ />
113
+ {failure ? <ApiError error={failure} title="That did not work" /> : null}
114
+ <ConfirmSpendDialog {...dialog} />
115
+ </>
116
+ );
117
+ }
@@ -0,0 +1,274 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SourceView: shows what one intelligence source collected, in a form a person can read. No SDK operations: pass it the
5
+ * `data` of a source (`GET /intelligence/data/{data_type}` or one entry of `GET /intelligence/all/{website}`).
6
+ *
7
+ * The payloads are open-shaped, so every field is read defensively: a missing value shows a dash (never 0 and never a
8
+ * guess), web content is drawn as text (never HTML), and links are drawn only for http(s) URLs. A source with a shape
9
+ * this component does not know falls back to a readable tree of its values.
10
+ *
11
+ * Known shapes (from the writers in the app): company_enrichment (flat profile), competitor_discovery ({ competitors }),
12
+ * brand_news ({ articles }), product_inventory ({ products, services, capabilities }), company_keywords / organic_keywords /
13
+ * paid_keywords ({ keywords }), audience_questions ({ questions }), search_trends ({ trends }), top_content ({ pages }),
14
+ * website_scrape ({ pages }), visual_assets ({ logo_url, branding }), brand_sentiment (aggregates).
15
+ */
16
+ import { useState } from "react";
17
+ import { ExternalLink } from "lucide-react";
18
+ import { Button } from "@graph8/sdk/ui";
19
+ import { KeyValueList, EmptyValue } from "@/g8/components/screen/key-value-list";
20
+ import { StatusPill } from "@/g8/components/screen/status-pill";
21
+ import { SectionCard } from "@/g8/components/screen/section-card";
22
+ import { plural } from "@/g8/lib/plural";
23
+ import { asArray, humanize, isRec, num, safeHttpUrl, text, whenDate } from "../lib/intelligence-types";
24
+ import type { Rec } from "../lib/intelligence-types";
25
+
26
+ const SHOW = 12;
27
+ const HEX = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
28
+
29
+ function Link({ url, children }: { url: unknown; children?: React.ReactNode }) {
30
+ const href = safeHttpUrl(url);
31
+ if (!href) return <>{children ?? text(url) ?? <EmptyValue />}</>;
32
+ return (
33
+ <a href={href} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 underline underline-offset-2">
34
+ {children ?? href.replace(/^https?:\/\/(www\.)?/, "")}
35
+ <ExternalLink className="size-3" aria-hidden />
36
+ </a>
37
+ );
38
+ }
39
+
40
+ /** A list that shows its first rows and says how many more there are, with a button to see them. */
41
+ function More<T>({ items, noun, render }: { items: T[]; noun: string; render: (item: T, index: number) => React.ReactNode }) {
42
+ const [all, setAll] = useState(false);
43
+ const shown = all ? items : items.slice(0, SHOW);
44
+ return (
45
+ <div className="space-y-2">
46
+ <ul className="divide-y divide-border">{shown.map((item, i) => <li key={i} className="py-2 text-sm">{render(item, i)}</li>)}</ul>
47
+ {items.length > SHOW ? (
48
+ <div className="flex items-center justify-between text-xs text-muted-foreground">
49
+ <span role="status">Showing {shown.length.toLocaleString()} of {plural(items.length, noun)}</span>
50
+ <Button type="button" variant="outline" size="sm" onClick={() => setAll((v) => !v)}>{all ? "Show fewer" : "Show all"}</Button>
51
+ </div>
52
+ ) : null}
53
+ </div>
54
+ );
55
+ }
56
+
57
+ const PROFILE_FIELDS: Array<[string, string]> = [
58
+ ["company_name", "Company name"], ["industry", "Industry"], ["employee_count", "Employees"], ["revenue", "Revenue"],
59
+ ["founded_year", "Founded"], ["funding", "Funding"], ["address", "Headquarters"], ["phone", "Phone"], ["primary_email", "Email"],
60
+ ];
61
+ const PROFILE_LINKS: Array<[string, string]> = [
62
+ ["linkedin", "LinkedIn"], ["twitter", "X / Twitter"], ["facebook", "Facebook"], ["instagram", "Instagram"], ["youtube", "YouTube"], ["github", "GitHub"],
63
+ ];
64
+
65
+ export function CompanyProfileFields({ data }: { data: Rec }) {
66
+ const description = text(data.description);
67
+ return (
68
+ <div className="space-y-3">
69
+ {description ? <p className="text-sm leading-6">{description}</p> : null}
70
+ <KeyValueList columns={2} items={[
71
+ ...PROFILE_FIELDS.map(([k, label]) => ({ label, value: text(data[k]) ?? <EmptyValue /> })),
72
+ ...PROFILE_LINKS.filter(([k]) => text(data[k])).map(([k, label]) => ({ label, value: <Link url={data[k]} /> })),
73
+ ]} />
74
+ </div>
75
+ );
76
+ }
77
+
78
+ function Competitors({ data }: { data: Rec }) {
79
+ const rows = asArray(data.competitors).filter(isRec);
80
+ const summary = text(data.market_summary);
81
+ return (
82
+ <div className="space-y-3">
83
+ {summary ? <p className="text-sm text-muted-foreground">{summary}</p> : null}
84
+ {rows.length === 0 ? <p className="text-sm text-muted-foreground">No competitors in this source.</p> : (
85
+ <More items={rows} noun="competitor" render={(c) => (
86
+ <div className="space-y-1">
87
+ <div className="flex flex-wrap items-center gap-2">
88
+ <span className="font-medium">{text(c.name) ?? text(c.domain) ?? "Unnamed"}</span>
89
+ {text(c.domain) ? <span className="text-xs text-muted-foreground">{text(c.domain)}</span> : null}
90
+ {text(c.competitive_type) ? <StatusPill tone="info">{humanize(text(c.competitive_type) as string)}</StatusPill> : null}
91
+ {text(c.similarity) ? <StatusPill tone="neutral">{`${humanize(text(c.similarity) as string)} similarity`}</StatusPill> : null}
92
+ {text(c.confidence) ? <StatusPill tone="neutral" quiet>{`${humanize(text(c.confidence) as string)} confidence`}</StatusPill> : null}
93
+ </div>
94
+ {text(c.description) ? <p className="text-muted-foreground">{text(c.description)}</p> : null}
95
+ {text(c.reasoning) ? <p className="text-xs text-muted-foreground">{text(c.reasoning)}</p> : null}
96
+ </div>
97
+ )} />
98
+ )}
99
+ </div>
100
+ );
101
+ }
102
+
103
+ const sentimentTone = (s: number | null): "positive" | "negative" | "neutral" => (s == null ? "neutral" : s > 0.2 ? "positive" : s < -0.2 ? "negative" : "neutral");
104
+
105
+ function News({ data }: { data: Rec }) {
106
+ const rows = asArray(data.articles).filter(isRec);
107
+ if (rows.length === 0) return <p className="text-sm text-muted-foreground">No articles in this source.</p>;
108
+ return (
109
+ <More items={rows} noun="article" render={(a) => {
110
+ const source = isRec(a.source) ? text(a.source.name) : text(a.source);
111
+ const sentiment = num(a.sentiment);
112
+ return (
113
+ <div className="space-y-0.5">
114
+ <div className="font-medium"><Link url={a.url}>{text(a.title) ?? "Untitled article"}</Link></div>
115
+ <div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
116
+ {source ? <span>{source}</span> : null}
117
+ {whenDate(a.published_at) ? <span>{whenDate(a.published_at)}</span> : null}
118
+ {sentiment != null ? <StatusPill tone={sentimentTone(sentiment)} quiet>{`Sentiment ${sentiment.toFixed(2)}`}</StatusPill> : null}
119
+ </div>
120
+ {text(a.description) ? <p className="text-muted-foreground">{text(a.description)}</p> : null}
121
+ </div>
122
+ );
123
+ }} />
124
+ );
125
+ }
126
+
127
+ function Products({ data }: { data: Rec }) {
128
+ const lists: Array<[string, unknown[]]> = [["Products", asArray(data.products)], ["Services", asArray(data.services)], ["Capabilities", asArray(data.capabilities)]];
129
+ const description = text(data.company_description);
130
+ return (
131
+ <div className="space-y-3">
132
+ {description ? <p className="text-sm leading-6">{description}</p> : null}
133
+ {lists.filter(([, rows]) => rows.length).map(([label, rows]) => (
134
+ <SectionCard key={label} title={label} description={plural(rows.length, label.toLowerCase().replace(/s$/, ""))}>
135
+ <More items={rows} noun={label.toLowerCase().replace(/s$/, "")} render={(r) => isRec(r)
136
+ ? <div><span className="font-medium">{text(r.name) ?? "Unnamed"}</span>{text(r.description) ? <p className="text-muted-foreground">{text(r.description)}</p> : null}</div>
137
+ : <span>{text(r) ?? <EmptyValue />}</span>} />
138
+ </SectionCard>
139
+ ))}
140
+ </div>
141
+ );
142
+ }
143
+
144
+ /** Keyword rows use legacy SEMrush codes: Ph = phrase, Nq = monthly searches, Cp = cost per click, Po = position, Ur = url. */
145
+ const KEYWORD_COLUMNS: Array<[string, string]> = [["Nq", "Searches"], ["Po", "Position"], ["Cp", "CPC"], ["Ur", "URL"]];
146
+
147
+ function Keywords({ data }: { data: Rec }) {
148
+ const rows = asArray(data.keywords);
149
+ if (rows.length === 0) return <p className="text-sm text-muted-foreground">No keywords in this source.</p>;
150
+ return (
151
+ <More items={rows} noun="keyword" render={(k) => {
152
+ if (!isRec(k)) return <span>{text(k) ?? <EmptyValue />}</span>;
153
+ const extras = KEYWORD_COLUMNS.filter(([key]) => k[key] != null && k[key] !== "");
154
+ return (
155
+ <div className="flex flex-wrap items-baseline gap-x-3">
156
+ <span className="font-medium">{text(k.Ph) ?? text(k.keyword) ?? text(k.name) ?? "Keyword"}</span>
157
+ {extras.map(([key, label]) => <span key={key} className="text-xs text-muted-foreground">{label} {key === "Ur" ? <Link url={k[key]} /> : text(k[key])}</span>)}
158
+ </div>
159
+ );
160
+ }} />
161
+ );
162
+ }
163
+
164
+ function Questions({ data }: { data: Rec }) {
165
+ const rows = asArray(data.questions);
166
+ if (rows.length === 0) return <p className="text-sm text-muted-foreground">No questions in this source.</p>;
167
+ return <More items={rows} noun="question" render={(q) => <span>{isRec(q) ? text(q.question) ?? text(q.text) ?? <EmptyValue /> : text(q) ?? <EmptyValue />}</span>} />;
168
+ }
169
+
170
+ function Trends({ data }: { data: Rec }) {
171
+ const rows = asArray(data.trends);
172
+ if (rows.length === 0) return <p className="text-sm text-muted-foreground">No trends in this source.</p>;
173
+ return <More items={rows} noun="trend" render={(t) => (isRec(t) ? <GenericRow row={t} lead="keyword" /> : <span>{text(t) ?? <EmptyValue />}</span>)} />;
174
+ }
175
+
176
+ function Pages({ data, withText }: { data: Rec; withText?: boolean }) {
177
+ const rows = asArray(data.pages).filter(isRec);
178
+ const [open, setOpen] = useState<number | null>(null);
179
+ if (rows.length === 0) return <p className="text-sm text-muted-foreground">No pages in this source.</p>;
180
+ return (
181
+ <More items={rows} noun="page" render={(p, i) => {
182
+ const body = text(p.markdown) ?? text(p.content);
183
+ return (
184
+ <div className="space-y-1">
185
+ <div className="flex items-center justify-between gap-2">
186
+ <Link url={p.url ?? p.Ur}>{text(p.title) ?? undefined}</Link>
187
+ {withText && body ? <Button type="button" variant="ghost" size="sm" onClick={() => setOpen(open === i ? null : i)}>{open === i ? "Hide text" : `Read text (${plural(body.length, "character")})`}</Button> : null}
188
+ </div>
189
+ {withText && open === i && body ? <pre className="max-h-64 overflow-auto whitespace-pre-wrap rounded-md bg-muted p-2 text-xs">{body.slice(0, 20_000)}</pre> : null}
190
+ </div>
191
+ );
192
+ }} />
193
+ );
194
+ }
195
+
196
+ function Visual({ data }: { data: Rec }) {
197
+ const logo = safeHttpUrl(data.logo_url);
198
+ const branding = isRec(data.branding) ? data.branding : {};
199
+ const colors = isRec(branding.colors) ? Object.entries(branding.colors).filter(([, v]) => typeof v === "string" && HEX.test(v)) : [];
200
+ const fonts = asArray(branding.fonts).map((f) => (isRec(f) ? text(f.family) ?? text(f.name) : text(f))).filter(Boolean) as string[];
201
+ return (
202
+ <div className="space-y-3">
203
+ {logo ? <img src={logo} alt="Company logo" className="h-12 max-w-48 object-contain" /> : null}
204
+ {colors.length ? (
205
+ <ul className="flex flex-wrap gap-3">
206
+ {colors.map(([name, value]) => (
207
+ <li key={name} className="flex items-center gap-2 text-xs">
208
+ <span aria-hidden className="size-6 rounded border border-border" style={{ backgroundColor: value as string }} />
209
+ <span>{humanize(name)} <span className="text-muted-foreground">{value as string}</span></span>
210
+ </li>
211
+ ))}
212
+ </ul>
213
+ ) : null}
214
+ {fonts.length ? <p className="text-sm">Fonts: {fonts.join(", ")}</p> : null}
215
+ {!logo && !colors.length && !fonts.length ? <p className="text-sm text-muted-foreground">No brand assets in this source.</p> : null}
216
+ </div>
217
+ );
218
+ }
219
+
220
+ function GenericRow({ row, lead }: { row: Rec; lead?: string }) {
221
+ const entries = Object.entries(row).filter(([, v]) => typeof v === "string" || typeof v === "number" || typeof v === "boolean");
222
+ const first = lead ? entries.find(([k]) => k === lead) : undefined;
223
+ return (
224
+ <div className="flex flex-wrap items-baseline gap-x-3">
225
+ {first ? <span className="font-medium">{String(first[1])}</span> : null}
226
+ {entries.filter(([k]) => k !== lead).slice(0, 8).map(([k, v]) => <span key={k} className="text-xs text-muted-foreground">{humanize(k)} {String(v)}</span>)}
227
+ </div>
228
+ );
229
+ }
230
+
231
+ /** Fallback: scalars as key/value pairs, nested objects and lists one level down. Depth-limited so a huge blob stays readable. */
232
+ export function GenericData({ value, depth = 0 }: { value: unknown; depth?: number }) {
233
+ if (Array.isArray(value)) {
234
+ if (value.length === 0) return <p className="text-sm text-muted-foreground">Nothing here.</p>;
235
+ return <More items={value} noun="item" render={(v) => (isRec(v) && depth < 2 ? <GenericRow row={v} /> : <span>{typeof v === "object" ? "" : text(v) ?? ""}</span>)} />;
236
+ }
237
+ if (!isRec(value)) return <span className="text-sm">{text(value) ?? <EmptyValue />}</span>;
238
+ const scalars = Object.entries(value).filter(([, v]) => ["string", "number", "boolean"].includes(typeof v));
239
+ const nested = Object.entries(value).filter(([, v]) => v != null && typeof v === "object");
240
+ return (
241
+ <div className="space-y-3">
242
+ {scalars.length ? <KeyValueList columns={2} items={scalars.map(([k, v]) => ({ label: humanize(k), value: String(v) }))} /> : null}
243
+ {depth < 2 ? nested.map(([k, v]) => <SectionCard key={k} title={humanize(k)}><GenericData value={v} depth={depth + 1} /></SectionCard>) : null}
244
+ </div>
245
+ );
246
+ }
247
+
248
+ export interface SourceViewProps {
249
+ /** The source's type key, for example `company_enrichment`. */
250
+ dataType: string;
251
+ /** The `data` blob of the source. */
252
+ data: unknown;
253
+ }
254
+
255
+ export function SourceView({ dataType, data }: SourceViewProps) {
256
+ if (data == null) return <p className="text-sm text-muted-foreground">This source has no data yet.</p>;
257
+ const rec = isRec(data) ? data : null;
258
+ if (!rec) return <GenericData value={data} />;
259
+ switch (dataType) {
260
+ case "company_enrichment": return <CompanyProfileFields data={rec} />;
261
+ case "competitor_discovery": return <Competitors data={rec} />;
262
+ case "brand_news": return <News data={rec} />;
263
+ case "product_inventory": return <Products data={rec} />;
264
+ case "company_keywords":
265
+ case "organic_keywords":
266
+ case "paid_keywords": return <Keywords data={rec} />;
267
+ case "audience_questions": return <Questions data={rec} />;
268
+ case "search_trends": return <Trends data={rec} />;
269
+ case "top_content": return <Pages data={rec} />;
270
+ case "website_scrape": return <Pages data={rec} withText />;
271
+ case "visual_assets": return <Visual data={rec} />;
272
+ default: return <GenericData value={rec} />;
273
+ }
274
+ }