@graph8/sdk 0.361.0 → 0.370.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 (388) hide show
  1. package/README.md +14 -10
  2. package/blocks/_foundation/README.md +172 -1
  3. package/blocks/_foundation/block.json +234 -13
  4. package/blocks/_foundation/components/api-error.tsx +71 -25
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  7. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  8. package/blocks/_foundation/components/empty-state.tsx +39 -7
  9. package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
  10. package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
  11. package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
  12. package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
  13. package/blocks/_foundation/components/screen/index.ts +22 -0
  14. package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
  17. package/blocks/_foundation/components/screen/section-card.tsx +47 -0
  18. package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
  19. package/blocks/_foundation/components/screen/split-view.tsx +125 -0
  20. package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
  21. package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
  22. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  23. package/blocks/_foundation/components/screen/time-series-chart.tsx +32 -0
  24. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  25. package/blocks/_foundation/components/screen/toolbar.tsx +144 -0
  26. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  27. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  28. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  29. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  30. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  31. package/blocks/_foundation/hooks/use-url-state.ts +74 -0
  32. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  33. package/blocks/_foundation/lib/g8-client.ts +1 -1
  34. package/blocks/_foundation/lib/plural.ts +23 -0
  35. package/blocks/_foundation/server/connect-route.ts +15 -0
  36. package/blocks/_foundation/server/connect.ts +127 -0
  37. package/blocks/_foundation/server/local-key.ts +70 -0
  38. package/blocks/_foundation/server/proxy.ts +103 -3
  39. package/blocks/_foundation/server/route.ts +16 -4
  40. package/blocks/agents/README.md +14 -0
  41. package/blocks/agents/block.json +33 -1
  42. package/blocks/agents/components/agent-builder.tsx +13 -8
  43. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
  44. package/blocks/agents/components/agent-run-detail.tsx +102 -0
  45. package/blocks/agents/components/agent-runs.tsx +16 -7
  46. package/blocks/agents/components/agents-screen.tsx +184 -0
  47. package/blocks/analytics/README.md +13 -0
  48. package/blocks/analytics/block.json +45 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +209 -0
  50. package/blocks/analytics/components/leaderboard.tsx +14 -7
  51. package/blocks/analytics/components/report-builder.tsx +23 -9
  52. package/blocks/analytics/components/report-detail.tsx +84 -0
  53. package/blocks/analytics/components/trend-chart.tsx +62 -0
  54. package/blocks/appointments/README.md +21 -1
  55. package/blocks/appointments/block.json +43 -2
  56. package/blocks/appointments/components/availability-editor.tsx +17 -5
  57. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  58. package/blocks/appointments/components/event-type-related.tsx +77 -0
  59. package/blocks/appointments/components/meeting-list.tsx +14 -6
  60. package/blocks/appointments/components/meetings-screen.tsx +157 -0
  61. package/blocks/appointments/components/scheduling-screen.tsx +122 -0
  62. package/blocks/appointments/hooks/use-event-types.ts +17 -0
  63. package/blocks/block.schema.json +18 -0
  64. package/blocks/block.types.ts +26 -0
  65. package/blocks/campaigns/README.md +41 -11
  66. package/blocks/campaigns/block.json +105 -42
  67. package/blocks/campaigns/components/campaign-assets.tsx +16 -5
  68. package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
  69. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  70. package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
  71. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  72. package/blocks/campaigns/components/icp-detail.tsx +154 -0
  73. package/blocks/campaigns/components/icp-editor.tsx +15 -15
  74. package/blocks/campaigns/components/persona-detail.tsx +172 -0
  75. package/blocks/campaigns/components/persona-editor.tsx +14 -11
  76. package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
  77. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  78. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  79. package/blocks/coach/README.md +10 -0
  80. package/blocks/coach/block.json +16 -1
  81. package/blocks/coach/components/coach-screen.tsx +222 -0
  82. package/blocks/copilot/README.md +13 -0
  83. package/blocks/copilot/block.json +14 -1
  84. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  85. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  86. package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
  87. package/blocks/crm/README.md +28 -0
  88. package/blocks/crm/block.json +58 -2
  89. package/blocks/crm/components/companies-screen.tsx +345 -0
  90. package/blocks/crm/components/company-detail.tsx +8 -6
  91. package/blocks/crm/components/company-table.tsx +11 -4
  92. package/blocks/crm/components/contact-detail.tsx +6 -3
  93. package/blocks/crm/components/contact-table.tsx +10 -3
  94. package/blocks/crm/components/contacts-screen.tsx +359 -0
  95. package/blocks/crm/components/dedupe-review.tsx +7 -5
  96. package/blocks/crm/components/import-wizard.tsx +4 -2
  97. package/blocks/crm/components/notes-panel.tsx +4 -4
  98. package/blocks/crm/components/record-form.tsx +5 -3
  99. package/blocks/crm/components/task-list.tsx +4 -4
  100. package/blocks/crm/components/tasks-screen.tsx +379 -0
  101. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  102. package/blocks/crm/hooks/use-page-query.ts +7 -1
  103. package/blocks/crm/lib/crm-types.ts +12 -2
  104. package/blocks/crm/lib/screen-parts.tsx +105 -0
  105. package/blocks/deals/README.md +34 -3
  106. package/blocks/deals/block.json +45 -4
  107. package/blocks/deals/components/deal-detail.tsx +27 -13
  108. package/blocks/deals/components/deal-related.tsx +87 -0
  109. package/blocks/deals/components/deals-screen.tsx +298 -0
  110. package/blocks/deals/components/pipeline-board.tsx +4 -2
  111. package/blocks/deals/components/stage-editor.tsx +4 -4
  112. package/blocks/deals/hooks/use-default-owner.ts +60 -0
  113. package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
  114. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  115. package/blocks/deals/lib/deals-types.ts +14 -0
  116. package/blocks/dialer/README.md +34 -2
  117. package/blocks/dialer/block.json +67 -1
  118. package/blocks/dialer/components/call-contact-button.tsx +252 -0
  119. package/blocks/dialer/components/call-log-screen.tsx +205 -0
  120. package/blocks/dialer/components/call-log.tsx +7 -3
  121. package/blocks/dialer/components/call-queue.tsx +1 -1
  122. package/blocks/dialer/components/dialer-screen.tsx +319 -0
  123. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  124. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  125. package/blocks/dialer/components/session-start-form.tsx +4 -2
  126. package/blocks/dialer/components/softphone.tsx +3 -1
  127. package/blocks/enrichment/README.md +14 -2
  128. package/blocks/enrichment/block.json +32 -4
  129. package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
  130. package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
  131. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  132. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  133. package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
  134. package/blocks/enrichment/components/waterfall-config.tsx +39 -10
  135. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  136. package/blocks/forms/README.md +12 -0
  137. package/blocks/forms/block.json +39 -17
  138. package/blocks/forms/components/form-builder.tsx +3 -1
  139. package/blocks/forms/components/form-embed.tsx +3 -1
  140. package/blocks/forms/components/forms-screen.tsx +353 -0
  141. package/blocks/forms/components/submissions-table.tsx +16 -6
  142. package/blocks/inbox/README.md +27 -3
  143. package/blocks/inbox/block.json +14 -2
  144. package/blocks/inbox/components/inbox-list.tsx +7 -3
  145. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  146. package/blocks/inbox/components/reply-composer.tsx +65 -9
  147. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  148. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  149. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  150. package/blocks/inbox/lib/inbox-types.ts +47 -1
  151. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  152. package/blocks/integrations/README.md +9 -0
  153. package/blocks/integrations/block.json +46 -1
  154. package/blocks/integrations/components/crm-detail.tsx +100 -0
  155. package/blocks/integrations/components/integration-list.tsx +9 -4
  156. package/blocks/integrations/components/integrations-screen.tsx +135 -0
  157. package/blocks/integrations/components/sync-status.tsx +5 -3
  158. package/blocks/integrations/components/webhook-detail.tsx +160 -0
  159. package/blocks/integrations/components/webhook-manager.tsx +13 -8
  160. package/blocks/integrations/lib/integrations-utils.ts +6 -1
  161. package/blocks/intent/README.md +14 -0
  162. package/blocks/intent/block.json +27 -1
  163. package/blocks/intent/components/intent-feed.tsx +5 -2
  164. package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
  165. package/blocks/intent/components/intent-screen.tsx +265 -0
  166. package/blocks/intent/components/keyword-detail.tsx +124 -0
  167. package/blocks/lifecycle/README.md +16 -0
  168. package/blocks/lifecycle/block.json +34 -7
  169. package/blocks/lifecycle/components/account-related.tsx +66 -0
  170. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  171. package/blocks/lifecycle/components/lifecycle-screen.tsx +149 -0
  172. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  173. package/blocks/lists/README.md +13 -0
  174. package/blocks/lists/block.json +39 -1
  175. package/blocks/lists/components/add-to-list-button.tsx +3 -2
  176. package/blocks/lists/components/list-picker.tsx +5 -1
  177. package/blocks/lists/components/list-table.tsx +11 -5
  178. package/blocks/lists/components/lists-screen.tsx +344 -0
  179. package/blocks/lists/components/suppression-manager.tsx +6 -2
  180. package/blocks/lists/components/suppressions-screen.tsx +255 -0
  181. package/blocks/mailboxes/README.md +17 -0
  182. package/blocks/mailboxes/block.json +65 -17
  183. package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
  184. package/blocks/mailboxes/components/domain-detail.tsx +142 -0
  185. package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
  186. package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
  187. package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
  188. package/blocks/mailboxes/components/warmup-health.tsx +4 -2
  189. package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
  190. package/blocks/newsletter/README.md +13 -0
  191. package/blocks/newsletter/block.json +47 -23
  192. package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
  193. package/blocks/newsletter/components/newsletter-screen.tsx +404 -0
  194. package/blocks/newsletter/components/subscriber-table.tsx +12 -6
  195. package/blocks/nurture/README.md +12 -0
  196. package/blocks/nurture/block.json +35 -13
  197. package/blocks/nurture/components/nurture-builder.tsx +3 -1
  198. package/blocks/nurture/components/nurture-enroll.tsx +6 -3
  199. package/blocks/nurture/components/nurture-form.tsx +23 -3
  200. package/blocks/nurture/components/nurture-screen.tsx +283 -0
  201. package/blocks/nurture/lib/nurture-types.ts +1 -1
  202. package/blocks/objects/README.md +17 -0
  203. package/blocks/objects/block.json +29 -16
  204. package/blocks/objects/components/object-not-found.tsx +3 -1
  205. package/blocks/objects/components/object-record-detail.tsx +2 -1
  206. package/blocks/objects/components/object-schema-editor.tsx +3 -1
  207. package/blocks/objects/components/object-table.tsx +13 -3
  208. package/blocks/objects/components/objects-screen.tsx +147 -0
  209. package/blocks/objects/lib/object-types.ts +1 -1
  210. package/blocks/pages/README.md +12 -0
  211. package/blocks/pages/block.json +35 -22
  212. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  213. package/blocks/pages/components/content-library.tsx +46 -11
  214. package/blocks/pages/components/page-list.tsx +6 -3
  215. package/blocks/pages/components/pages-screen.tsx +154 -0
  216. package/blocks/quotes/README.md +17 -0
  217. package/blocks/quotes/block.json +38 -9
  218. package/blocks/quotes/components/product-picker.tsx +3 -1
  219. package/blocks/quotes/components/quote-list.tsx +7 -3
  220. package/blocks/quotes/components/quote-related.tsx +123 -0
  221. package/blocks/quotes/components/quotes-screen.tsx +169 -0
  222. package/blocks/radar/README.md +10 -0
  223. package/blocks/radar/block.json +41 -1
  224. package/blocks/radar/components/competitor-detail.tsx +117 -0
  225. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  226. package/blocks/radar/components/radar-report.tsx +4 -2
  227. package/blocks/radar/components/radar-screen.tsx +214 -0
  228. package/blocks/radar/components/report-detail.tsx +104 -0
  229. package/blocks/search/README.md +10 -0
  230. package/blocks/search/block.json +16 -1
  231. package/blocks/search/components/filter-builder.tsx +3 -3
  232. package/blocks/search/components/prospect-results.tsx +5 -3
  233. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  234. package/blocks/search/components/saved-searches.tsx +4 -1
  235. package/blocks/search/hooks/use-filter-options.ts +20 -6
  236. package/blocks/search/lib/search-types.ts +2 -0
  237. package/blocks/sequencer/README.md +49 -9
  238. package/blocks/sequencer/block.json +99 -19
  239. package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
  240. package/blocks/sequencer/components/sequence-builder.tsx +211 -46
  241. package/blocks/sequencer/components/sequence-detail.tsx +259 -0
  242. package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
  243. package/blocks/sequencer/components/sequence-list.tsx +33 -8
  244. package/blocks/sequencer/components/sequence-stats.tsx +3 -2
  245. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  246. package/blocks/sequencer/components/sequences-screen.tsx +159 -0
  247. package/blocks/sequencer/components/step-editor.tsx +3 -1
  248. package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
  249. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  250. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  251. package/blocks/social-ads/README.md +11 -0
  252. package/blocks/social-ads/block.json +101 -2
  253. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  254. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  255. package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
  256. package/blocks/social-ads/components/audience-sync.tsx +11 -7
  257. package/blocks/social-ads/components/campaign-detail.tsx +133 -0
  258. package/blocks/social-ads/components/creative-detail.tsx +150 -0
  259. package/blocks/social-ads/components/creative-gallery.tsx +15 -5
  260. package/blocks/social-ads/components/post-composer.tsx +3 -1
  261. package/blocks/social-ads/components/post-detail.tsx +160 -0
  262. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  263. package/blocks/social-ads/components/social-screen.tsx +160 -0
  264. package/blocks/visitors/README.md +10 -0
  265. package/blocks/visitors/block.json +25 -1
  266. package/blocks/visitors/components/snippet-install.tsx +3 -1
  267. package/blocks/visitors/components/visitor-company-card.tsx +2 -2
  268. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  269. package/blocks/visitors/components/visitor-feed.tsx +11 -2
  270. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  271. package/blocks/webchat/README.md +10 -0
  272. package/blocks/webchat/block.json +21 -7
  273. package/blocks/webchat/components/chat-widget-config.tsx +7 -4
  274. package/blocks/webchat/components/conversation-list.tsx +7 -3
  275. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  276. package/blocks/workbench/README.md +11 -0
  277. package/blocks/workbench/block.json +48 -1
  278. package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
  279. package/blocks/workbench/components/workbench-grid.tsx +9 -3
  280. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  281. package/blocks/workbench/components/workbench-screen.tsx +192 -0
  282. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  283. package/blocks/workflows/README.md +13 -0
  284. package/blocks/workflows/block.json +28 -9
  285. package/blocks/workflows/components/run-history.tsx +31 -9
  286. package/blocks/workflows/components/trigger-config.tsx +3 -1
  287. package/blocks/workflows/components/workflow-editor.tsx +3 -1
  288. package/blocks/workflows/components/workflow-list.tsx +13 -6
  289. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  290. package/dist/cli.js +2785 -1637
  291. package/dist/errors.d.mts +34 -0
  292. package/dist/errors.d.ts +34 -0
  293. package/dist/errors.js +46 -0
  294. package/dist/errors.js.map +1 -0
  295. package/dist/errors.mjs +21 -0
  296. package/dist/errors.mjs.map +1 -0
  297. package/dist/index.browser.js +11 -5
  298. package/dist/index.browser.js.map +1 -1
  299. package/dist/index.browser.mjs +11 -5
  300. package/dist/index.browser.mjs.map +1 -1
  301. package/dist/index.d.mts +164 -25
  302. package/dist/index.d.ts +164 -25
  303. package/dist/index.js +11 -5
  304. package/dist/index.js.map +1 -1
  305. package/dist/index.mjs +11 -5
  306. package/dist/index.mjs.map +1 -1
  307. package/dist/react.browser.js +10 -4
  308. package/dist/react.browser.js.map +1 -1
  309. package/dist/react.browser.mjs +10 -4
  310. package/dist/react.browser.mjs.map +1 -1
  311. package/dist/react.d.mts +142 -5
  312. package/dist/react.d.ts +142 -5
  313. package/dist/react.js +10 -4
  314. package/dist/react.js.map +1 -1
  315. package/dist/react.mjs +10 -4
  316. package/dist/react.mjs.map +1 -1
  317. package/dist/types/blocks/add.d.ts +11 -4
  318. package/dist/types/blocks/brand.d.ts +113 -0
  319. package/dist/types/blocks/compose.d.ts +53 -0
  320. package/dist/types/blocks/create.d.ts +25 -8
  321. package/dist/types/blocks/doctor.d.ts +6 -1
  322. package/dist/types/blocks/flows.d.ts +20 -0
  323. package/dist/types/blocks/navfile.d.ts +35 -0
  324. package/dist/types/blocks/paths.d.ts +4 -0
  325. package/dist/types/blocks/prefixes.d.ts +37 -0
  326. package/dist/types/blocks/registry.d.ts +32 -0
  327. package/dist/types/blocks/screens.d.ts +53 -0
  328. package/dist/types/cli.d.ts +2 -0
  329. package/dist/types/errors.d.ts +33 -0
  330. package/dist/types/http.d.ts +2 -18
  331. package/dist/types/version.d.ts +2 -2
  332. package/docs/agents.md +15 -4
  333. package/docs/api/README.md +2 -2
  334. package/docs/api/by-path.md +1 -0
  335. package/docs/api/desk.d.ts +98 -2
  336. package/docs/api/desk.md +123 -1
  337. package/docs/api/inbox.d.ts +10 -2
  338. package/docs/api/inbox.md +10 -2
  339. package/docs/api/seo.d.ts +19 -1
  340. package/docs/api/seo.md +24 -1
  341. package/package.json +16 -2
  342. package/stacks/6sense.json +41 -16
  343. package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
  344. package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
  345. package/stacks/apollo.json +118 -36
  346. package/stacks/calendly.json +60 -0
  347. package/stacks/clay/pages/companies.tsx.tmpl +15 -0
  348. package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
  349. package/stacks/clay.json +59 -26
  350. package/stacks/gong.json +81 -35
  351. package/stacks/hootsuite.json +99 -0
  352. package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
  353. package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
  354. package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
  355. package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
  356. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  357. package/stacks/hubspot.json +167 -66
  358. package/stacks/intercom.json +48 -20
  359. package/stacks/mailchimp.json +52 -33
  360. package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
  361. package/stacks/outreach.json +88 -25
  362. package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
  363. package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
  364. package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
  365. package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
  366. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  367. package/stacks/pipedrive.json +115 -0
  368. package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
  369. package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
  370. package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
  371. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  372. package/stacks/salesforce.json +81 -31
  373. package/stacks/stack.schema.json +6 -1
  374. package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
  375. package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
  376. package/stacks/zoominfo.json +102 -0
  377. package/styles/styles.css +1 -1
  378. package/templates/next-app/README.md.tmpl +55 -8
  379. package/templates/next-app/app/globals.css.tmpl +105 -0
  380. package/templates/next-app/app/layout.tsx.tmpl +18 -4
  381. package/templates/next-app/components/app-shell.tsx.tmpl +248 -23
  382. package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
  383. package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
  384. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
  385. package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
  386. package/templates/next-app/dot-gitignore.tmpl +2 -0
  387. package/templates/next-app/lib/nav.ts.tmpl +1 -1
  388. package/templates/next-app/package.json.tmpl +1 -1
@@ -87,6 +87,45 @@
87
87
  "read",
88
88
  "billable"
89
89
  ]
90
+ },
91
+ {
92
+ "name": "RadarScreen",
93
+ "file": "components/radar-screen.tsx",
94
+ "purpose": "Finished competitive-radar page: overview with threat board, competitor roster with battle-card panel, and reports.",
95
+ "operations": [
96
+ "get_marketing_overview_analytics_marketing_overview_get",
97
+ "overview_radar_overview_get"
98
+ ],
99
+ "tiers": [
100
+ "read"
101
+ ]
102
+ },
103
+ {
104
+ "name": "CompetitorDetail",
105
+ "file": "components/competitor-detail.tsx",
106
+ "purpose": "Complete screen for one competitor: every field, battle card, scan, pause or resume, remove.",
107
+ "operations": [
108
+ "scan_competitor_radar_competitors__competitor_id__scan_post",
109
+ "update_competitor_radar_competitors__competitor_id__patch",
110
+ "delete_competitor_radar_competitors__competitor_id__delete"
111
+ ],
112
+ "tiers": [
113
+ "billable",
114
+ "destructive"
115
+ ]
116
+ },
117
+ {
118
+ "name": "ReportDetail",
119
+ "file": "components/report-detail.tsx",
120
+ "purpose": "Complete screen for one radar report: status, preview, HTML and PDF links, regenerate.",
121
+ "operations": [
122
+ "list_radar_reports_radar_reports_get",
123
+ "create_radar_report_radar_reports_post"
124
+ ],
125
+ "tiers": [
126
+ "read",
127
+ "billable"
128
+ ]
90
129
  }
91
130
  ],
92
131
  "hooks": [
@@ -184,6 +223,7 @@
184
223
  }
185
224
  ],
186
225
  "peerDependencies": {
187
- "react": ">=18"
226
+ "react": ">=18",
227
+ "lucide-react": ">=0.400"
188
228
  }
189
229
  }
@@ -0,0 +1,117 @@
1
+ "use client";
2
+
3
+ /**
4
+ * CompetitorDetail: the complete screen for one tracked competitor. Finds it by id (so `?id=` deep links work), shows every
5
+ * field (tier, threat score, traffic and trend, posting cadence, ads live, scan state and budget, capture errors), the battle
6
+ * card, and every action: scan now, pause or resume, remove. A running scan is followed until it finishes.
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. listCompetitors GET /radar/competitors (read; re-read every 5 s while a scan runs)
10
+ * 2. competitorBattleCard GET /radar/competitors/{id}/battle-card (read, free; inside CompetitorBattleCard)
11
+ * 3. scanCompetitor POST /radar/competitors/{id}/scan (billable, confirmed: at most budget + 1 credits)
12
+ * 4. updateCompetitor PATCH /radar/competitors/{id} (billable tier, free; confirmed; pause or resume)
13
+ * 5. deleteCompetitor DELETE /radar/competitors/{id} (destructive, confirmed; removes its history too)
14
+ */
15
+ import { useEffect, type ReactNode } from "react";
16
+ import { Skeleton } from "@graph8/sdk/ui";
17
+ import { ApiError } from "@/g8/components/api-error";
18
+ import { EmptyState } from "@/g8/components/empty-state";
19
+ import { g8Delete, g8Patch, g8Post } from "@/g8/lib/g8-client";
20
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
21
+ import { EntityAvatar } from "@/g8/components/screen/entity-avatar";
22
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
23
+ import { SectionCard } from "@/g8/components/screen/section-card";
24
+ import { StatusPill } from "@/g8/components/screen/status-pill";
25
+ import { useCompetitors } from "../hooks/use-competitors";
26
+ import { formatCount, isScanning, scanCostEstimate } from "../lib/radar-types";
27
+ import { CompetitorBattleCard } from "./competitor-battle-card";
28
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
29
+ import { AskButton, useConfirmAction } from "@/g8/components/confirmed-action";
30
+
31
+ export interface CompetitorDetailProps {
32
+ competitorId: 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
+ export function CompetitorDetail({ competitorId, onClose, onExpand, onCollapse, onChanged, extraActions, extraTabs }: CompetitorDetailProps) {
44
+ const { ask, dialog, failure } = useConfirmAction();
45
+ const { competitors, capacity, loading, error, refetch } = useCompetitors();
46
+ const c = competitors.find((x) => x.id === competitorId);
47
+ const scanning = c ? isScanning(c) : false;
48
+
49
+ useEffect(() => {
50
+ if (!scanning) return;
51
+ const timer = setInterval(() => void refetch(), 5000);
52
+ return () => clearInterval(timer);
53
+ }, [scanning, refetch]);
54
+
55
+ if (error) return <ApiError error={error} title="Could not load the competitor" onRetry={() => void refetch()} />;
56
+ if (loading && competitors.length === 0) return <Skeleton className="h-40 w-full" />;
57
+ if (!c) return <EmptyState title="Competitor not found" description="It may have been removed." />;
58
+ const label = c.name || c.domain;
59
+ const changed = () => {
60
+ void refetch();
61
+ onChanged?.();
62
+ };
63
+ const errors = Object.entries(c.enrichment_errors ?? {});
64
+ const trend = typeof c.traffic_trend_pct === "number" ? `${c.traffic_trend_pct > 0 ? "+" : ""}${Math.round(c.traffic_trend_pct)}%` : "-";
65
+
66
+ const actions = (
67
+ <div className="flex flex-wrap gap-1">
68
+ <AskButton ask={ask} label="Scan now" tier="billable" title={`Scan ${label}`} confirmLabel="Scan" estimatedCredits={scanCostEstimate(c)} disabled={scanning || !c.is_active}
69
+ description={`1 credit per page captured (up to ${c.page_budget}) plus 1 for the traffic lookup. This is the most it can charge; your balance is not checked here.`}
70
+ run={() => g8Post(`/radar/competitors/${c.id}/scan`, {})} onDone={changed} />
71
+ <AskButton ask={ask} label={c.is_active ? "Pause" : "Resume"} variant="ghost" tier="billable" title={`${c.is_active ? "Pause" : "Resume"} ${label}`} confirmLabel={c.is_active ? "Pause" : "Resume"} estimatedCredits={0}
72
+ description={c.is_active ? "A paused competitor is no longer scanned. Its history is kept." : "Scans resume on the normal schedule."}
73
+ run={() => g8Patch(`/radar/competitors/${c.id}`, { is_active: !c.is_active })} onDone={changed} />
74
+ <AskButton ask={ask} label="Remove" variant="ghost" tier="destructive" title={`Remove ${label}`} confirmLabel="Remove" tolerateStatus={[404]}
75
+ description="The competitor and its captured pages, ads and history are deleted." run={() => g8Delete(`/radar/competitors/${c.id}`)} onDone={() => { changed(); onClose?.(); }} />
76
+ </div>
77
+ );
78
+
79
+ const tabs = [
80
+ {
81
+ id: "overview",
82
+ label: "Overview",
83
+ content: (
84
+ <div className="space-y-4">
85
+ <KeyValueList columns={2} items={[
86
+ { label: "Website", value: <a className="underline" href={`https://${c.domain}`} target="_blank" rel="noreferrer">{c.domain}</a> },
87
+ { label: "Tier", value: c.tier === "tier1" ? "Tier 1 (25 pages per scan)" : "Watchlist (8 pages per scan)" },
88
+ { label: "Threat score", value: c.threat_score ?? "-" },
89
+ { label: "Monthly traffic", value: formatCount(c.traffic_estimate) },
90
+ { label: "Traffic trend", value: trend },
91
+ { label: "Posts per week", value: c.posts_per_week ?? "-" },
92
+ { label: "Ads live", value: formatCount(c.ads_live_count) },
93
+ { label: "Page budget", value: c.page_budget },
94
+ { label: "Last scanned", value: c.last_scanned_at ? c.last_scanned_at.slice(0, 10) : "not scanned yet" },
95
+ { label: "Scan state", value: scanning ? "scanning..." : c.scan_status },
96
+ ]} />
97
+ {errors.length > 0 ? (
98
+ <SectionCard title="Capture issues" description="Sources that could not be read on the last scan.">
99
+ <ul className="space-y-1 text-sm">{errors.map(([source, message]) => <li key={source}><span className="font-medium">{source}</span>: {message}</li>)}</ul>
100
+ </SectionCard>
101
+ ) : null}
102
+ {capacity ? <p className="text-xs text-muted-foreground">{capacity.used} of {capacity.limit} competitors tracked on your plan.</p> : null}
103
+ </div>
104
+ ),
105
+ },
106
+ { id: "card", label: "Battle card", content: <CompetitorBattleCard competitorId={c.id} /> },
107
+ ];
108
+ return (
109
+ <>
110
+ <DetailPanel title={label} subtitle={c.domain} avatar={<EntityAvatar name={label} kind="company" size="lg" />}
111
+ badges={<StatusPill tone={c.is_active ? (scanning ? "info" : "positive") : "neutral"}>{!c.is_active ? "paused" : scanning ? "scanning" : "tracking"}</StatusPill>}
112
+ actions={extraActions ? <>{actions}{extraActions}</> : actions} tabs={extraTabs?.length ? [...tabs, ...extraTabs] : tabs} onClose={onClose} onExpand={onExpand} onCollapse={onCollapse} />
113
+ {failure ? <ApiError error={failure} title="That did not work" /> : null}
114
+ <ConfirmSpendDialog {...dialog} />
115
+ </>
116
+ );
117
+ }
@@ -30,6 +30,7 @@ import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
30
30
  import { useCompetitors } from "../hooks/use-competitors";
31
31
  import { formatCount, isScanning, normalizeDomain, radarBody, scanCostEstimate } from "../lib/radar-types";
32
32
  import type { Competitor, CompetitorList, CompetitorSuggestion } from "../lib/radar-types";
33
+ import { useBrand } from "@/g8/hooks/use-brand";
33
34
 
34
35
  export interface CompetitorTrackerProps {
35
36
  /** Called when a person opens a competitor (for example to show its CompetitorBattleCard). */
@@ -46,6 +47,7 @@ type Pending =
46
47
  | { type: "delete"; competitor: Competitor };
47
48
 
48
49
  export function CompetitorTracker({ onSelect, className }: CompetitorTrackerProps) {
50
+ const brand = useBrand();
49
51
  const { competitors, capacity, scanning, loading, error, refetch } = useCompetitors();
50
52
  const [domain, setDomain] = useState("");
51
53
  const [name, setName] = useState("");
@@ -112,7 +114,7 @@ export function CompetitorTracker({ onSelect, className }: CompetitorTrackerProp
112
114
  ) : null}
113
115
 
114
116
  {loading && competitors.length === 0 ? <Skeleton className="h-24 w-full" /> : null}
115
- {!loading && competitors.length === 0 ? <EmptyState title="No competitors tracked yet" description="Add a website above, or pick one graph8 detected from your market research." /> : null}
117
+ {!loading && competitors.length === 0 ? <EmptyState title="No competitors tracked yet" description={`Add a website above, or pick one ${brand.linkLabel} detected from your market research.`} /> : null}
116
118
  <ul className="flex flex-col gap-2">
117
119
  {competitors.map((c) => (
118
120
  <li key={c.id} className="flex flex-wrap items-center justify-between gap-2 rounded-md border p-3" data-testid="competitor">
@@ -28,10 +28,12 @@ import type { Capability, RadarReportRow } from "../lib/radar-types";
28
28
  export interface RadarReportProps {
29
29
  /** Preselect a capability (a feature-matrix row id). */
30
30
  capabilityId?: string;
31
+ /** Called when a person opens a report (show a ReportDetail). */
32
+ onSelectReport?: (report: RadarReportRow) => void;
31
33
  className?: string;
32
34
  }
33
35
 
34
- export function RadarReport({ capabilityId, className }: RadarReportProps) {
36
+ export function RadarReport({ capabilityId, onSelectReport, className }: RadarReportProps) {
35
37
  const [capability, setCapability] = useState(capabilityId ?? "");
36
38
  const [audience, setAudience] = useState<"executive" | "developer">("executive");
37
39
  const [edge, setEdge] = useState<"us" | "them">("us");
@@ -70,7 +72,7 @@ export function RadarReport({ capabilityId, className }: RadarReportProps) {
70
72
  <ul className="flex flex-col gap-2">
71
73
  {rows.map((r) => (
72
74
  <li key={r.id} className="flex items-center justify-between gap-2 rounded-md border p-3" data-testid="report">
73
- <div className="flex flex-col"><span className="text-sm font-medium">{r.title || r.capability_name}</span>
75
+ <div className="flex flex-col">{onSelectReport ? <button type="button" className="text-left text-sm font-medium hover:underline" onClick={() => onSelectReport(r)}>{r.title || r.capability_name}</button> : <span className="text-sm font-medium">{r.title || r.capability_name}</span>}
74
76
  <span className="text-xs text-muted-foreground">{r.audience} · {r.status}{r.error_message ? ` · ${r.error_message}` : ""}</span></div>
75
77
  {r.status === "completed" ? (
76
78
  <div className="flex gap-2"><a className="text-sm underline" href={`${G8_PROXY_BASE}/radar/reports/${r.id}/html`} target="_blank" rel="noreferrer">Open</a><a className="text-sm underline" href={`${G8_PROXY_BASE}/radar/reports/${r.id}/pdf`} target="_blank" rel="noreferrer">PDF</a></div>
@@ -0,0 +1,214 @@
1
+ "use client";
2
+
3
+ /**
4
+ * RadarScreen: the finished competitive-intelligence page. Three tabs:
5
+ * Overview stat strip (tracked, moves, open gaps, act now), threat board, alerts and recent moves. A threat-board row
6
+ * opens that competitor's battle card.
7
+ * Competitors the roster (CompetitorTracker: add, bulk-add detected, scan one or all, pause, resume, remove) with the
8
+ * selected competitor's complete detail screen (every field, battle card, scan, pause, remove) (`?id=`).
9
+ * Reports generate deep-dive reports on a capability (RadarReport); each report opens its detail screen (`?id=`).
10
+ *
11
+ * Every detail also opens as a full page with `?view=full`.
12
+ *
13
+ * SDK operations: reads of overview (GET /radar/overview) and getMarketingOverview (GET /analytics/marketing/overview, 7 days) for the
14
+ * Overview tab; the marketing card shows only the scalar values the payload carries, since the contract leaves its shape open. Everything else is composed:
15
+ * CompetitorTracker (billable + destructive, confirmed), CompetitorBattleCard (free read), RadarReport (billable, confirmed).
16
+ * Prerequisites: `radar` in the proxy allowlist, credits for scans/reports, market research done in graph8.
17
+ */
18
+ import { useState } from "react";
19
+ import type { ReactNode } from "react";
20
+ import { Button } from "@graph8/sdk/ui";
21
+ import { Crosshair } from "lucide-react";
22
+ import { ApiError } from "@/g8/components/api-error";
23
+ import { EmptyState } from "@/g8/components/empty-state";
24
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
25
+ import { useUrlState } from "@/g8/hooks/use-url-state";
26
+ import { g8Get } from "@/g8/lib/g8-client";
27
+ import { ScreenLayout } from "@/g8/components/screen/screen-layout";
28
+ import type { StatItem } from "@/g8/components/screen/stat-strip";
29
+ import { SplitView } from "@/g8/components/screen/split-view";
30
+ import { SectionCard } from "@/g8/components/screen/section-card";
31
+ import { StatusPill } from "@/g8/components/screen/status-pill";
32
+ import { EntityAvatar } from "@/g8/components/screen/entity-avatar";
33
+ import { radarBody } from "../lib/radar-types";
34
+ import type { RadarOverviewBody } from "../lib/radar-types";
35
+ import { CompetitorDetail } from "./competitor-detail";
36
+ import { ReportDetail } from "./report-detail";
37
+ import { CompetitorTracker } from "./competitor-tracker";
38
+ import { RadarReport } from "./radar-report";
39
+
40
+ /** The record open in the detail panel, passed to the slot props. */
41
+ export type RadarRecord = { kind: "competitor" | "report"; id: string };
42
+
43
+ export interface RadarScreenProps {
44
+ toolbarActions?: ReactNode;
45
+ /** Buttons added to the open competitor's or report's header. */
46
+ renderDetailActions?: (record: RadarRecord) => ReactNode;
47
+ /** Tabs added after the built-in ones on the open competitor or report. */
48
+ extraDetailTabs?: { id: string; label: string; render: (record: RadarRecord) => ReactNode }[];
49
+ className?: string;
50
+ }
51
+
52
+ type Tab = "overview" | "competitors" | "reports";
53
+ const TABS = ["overview", "competitors", "reports"] as const;
54
+
55
+ function statsOf(o: RadarOverviewBody | undefined): StatItem[] | undefined {
56
+ if (!o?.summary || !o.summary.tracked) return undefined;
57
+ const s = o.summary;
58
+ return [
59
+ { label: "Tracked", value: s.tracked, hint: `${s.tier1} tier 1, ${s.watchlist} watchlist` },
60
+ { label: "Moves (7 days)", value: s.moves_7d },
61
+ { label: "Open gaps", value: s.open_gaps, tone: s.open_gaps ? "warning" : "neutral" },
62
+ { label: "Act now", value: s.act_now, tone: s.act_now ? "negative" : "neutral" },
63
+ ];
64
+ }
65
+
66
+ /** Your own marketing numbers next to rival moves. The payload is open-shaped: show scalar values only, never guess field names. */
67
+ function MarketingCard() {
68
+ const q = useG8Query(async (signal) => (await g8Get("/analytics/marketing/overview", { days: 7 }, { signal })) as { data?: unknown }, []);
69
+ const data = q.data?.data;
70
+ const scalars = data && typeof data === "object" && !Array.isArray(data)
71
+ ? Object.entries(data as Record<string, unknown>).filter(([, v]) => typeof v === "number" || (typeof v === "string" && v.length < 60)).slice(0, 8)
72
+ : [];
73
+ if (q.error || scalars.length === 0) return null;
74
+ return (
75
+ <SectionCard title="Your marketing, last 7 days" description="Your own numbers next to what rivals did.">
76
+ <dl className="grid grid-cols-2 gap-x-4 gap-y-1 text-sm sm:grid-cols-4">
77
+ {scalars.map(([k, v]) => (
78
+ <div key={k}>
79
+ <dt className="text-xs text-muted-foreground">{k.replace(/_/g, " ")}</dt>
80
+ <dd className="font-medium tabular-nums">{typeof v === "number" ? v.toLocaleString() : String(v)}</dd>
81
+ </div>
82
+ ))}
83
+ </dl>
84
+ </SectionCard>
85
+ );
86
+ }
87
+
88
+ function Overview({ data, onOpen, onTrack }: { data: RadarOverviewBody; onOpen: (id: string, name: string) => void; onTrack: () => void }) {
89
+ if (!data.summary?.tracked) {
90
+ return (
91
+ <EmptyState
92
+ title="Nothing is tracked yet"
93
+ description="Add a competitor to start seeing moves, gaps and a threat board."
94
+ action={<Button onClick={onTrack}>Add a competitor</Button>}
95
+ />
96
+ );
97
+ }
98
+ return (
99
+ <div className="space-y-4">
100
+ <MarketingCard />
101
+ {data.summary.scan_stale ? <p role="status" className="text-sm text-amber-700">The last scan{data.summary.last_scan_at ? ` (${data.summary.last_scan_at.slice(0, 10)})` : ""} is older than your cadence. Scan from the Competitors tab.</p> : null}
102
+ {data.summary.credit_cap_reached ? <p role="status" className="text-sm text-amber-700">The monthly radar credit cap is reached: automatic scans are paused.</p> : null}
103
+ <div className="grid gap-4 lg:grid-cols-2">
104
+ <SectionCard title="Threat board" description="Competitors ranked by threat score. Open one for its battle card.">
105
+ <ul className="divide-y">
106
+ {(data.threat_board ?? []).map((r) => (
107
+ <li key={r.id}>
108
+ <button type="button" className="flex w-full items-center gap-3 py-2 text-left text-sm hover:bg-muted/50" onClick={() => onOpen(r.id, r.name || r.domain)}>
109
+ <EntityAvatar name={r.name || r.domain} kind="company" size="sm" />
110
+ <span className="min-w-0 flex-1 truncate font-medium">{r.name || r.domain}</span>
111
+ <StatusPill tone={r.tier === "tier1" ? "info" : "neutral"}>{r.tier === "tier1" ? "Tier 1" : "Watchlist"}</StatusPill>
112
+ <span className="shrink-0 text-xs text-muted-foreground tabular-nums">threat {r.threat_score ?? "-"} · {r.moves_7d} moves</span>
113
+ </button>
114
+ </li>
115
+ ))}
116
+ </ul>
117
+ </SectionCard>
118
+ <div className="space-y-4">
119
+ <SectionCard title="Alerts">
120
+ {(data.alerts ?? []).length === 0 ? <p className="text-sm text-muted-foreground">No alerts.</p> : (
121
+ <ul className="space-y-1.5 text-sm">
122
+ {(data.alerts ?? []).map((a) => (
123
+ <li key={a.id}><span className="font-medium">{a.title}</span>{a.detail ? <span className="text-muted-foreground"> {a.detail}</span> : null}</li>
124
+ ))}
125
+ </ul>
126
+ )}
127
+ </SectionCard>
128
+ <SectionCard title="Recent moves">
129
+ {(data.moves ?? []).length === 0 ? <p className="text-sm text-muted-foreground">No moves yet.</p> : (
130
+ <ul className="space-y-1.5 text-sm">
131
+ {(data.moves ?? []).map((m) => (
132
+ <li key={m.id}>
133
+ {m.competitor_domain ? <span className="font-medium">{m.competitor_domain}: </span> : null}
134
+ {m.text || m.title}
135
+ {m.became_gap ? <span className="ml-1"><StatusPill tone="warning">became a gap</StatusPill></span> : null}
136
+ </li>
137
+ ))}
138
+ </ul>
139
+ )}
140
+ </SectionCard>
141
+ </div>
142
+ </div>
143
+ </div>
144
+ );
145
+ }
146
+
147
+ export function RadarScreen(props: RadarScreenProps) {
148
+ const [tabValue, setTab] = useUrlState("tab", "overview");
149
+ const tab: Tab = (TABS as readonly string[]).includes(tabValue) ? (tabValue as Tab) : "overview";
150
+ const [id, setId] = useUrlState("id", "");
151
+ const [view, setView] = useUrlState("view", "");
152
+ const expanded = view === "full" && Boolean(id);
153
+ const [tick, setTick] = useState(0);
154
+ const overview = useG8Query(async (signal) => radarBody<RadarOverviewBody>(await g8Get("/radar/overview", undefined, { signal })), [tick]);
155
+
156
+ const close = () => {
157
+ setId("");
158
+ setView("");
159
+ };
160
+ const panel = {
161
+ onClose: close,
162
+ onExpand: expanded ? undefined : () => setView("full"),
163
+ onCollapse: expanded ? () => setView("") : undefined,
164
+ onChanged: () => setTick((t) => t + 1),
165
+ };
166
+ const slots = (record: RadarRecord) => ({
167
+ extraActions: props.renderDetailActions?.(record),
168
+ extraTabs: (props.extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(record) })),
169
+ });
170
+ const split = (list: ReactNode, detail: ReactNode) => (
171
+ <SplitView detailOpen={Boolean(id)} onCloseDetail={close} expanded={expanded} onExpandedChange={(next) => setView(next ? "full" : "")} detailWidth="lg" list={list} detail={id ? detail : null} />
172
+ );
173
+
174
+ return (
175
+ <ScreenLayout
176
+ title="Competitive radar"
177
+ description="Track rivals, read what changed, and walk into every deal with a battle card."
178
+ icon={<Crosshair className="size-5" />}
179
+ actions={
180
+ <>
181
+ {props.toolbarActions}
182
+ <Button variant="outline" onClick={() => { setTab("reports"); close(); }}>New report</Button>
183
+ <Button onClick={() => { setTab("competitors"); close(); }}>Add competitor</Button>
184
+ </>
185
+ }
186
+ tabs={[
187
+ { id: "overview", label: "Overview" },
188
+ { id: "competitors", label: "Competitors" },
189
+ { id: "reports", label: "Reports" },
190
+ ]}
191
+ activeTab={tab}
192
+ onTabChange={(next) => {
193
+ setTab(next);
194
+ close();
195
+ }}
196
+ stats={statsOf(overview.data)}
197
+ className={props.className}
198
+ >
199
+ {tab === "overview" ? (
200
+ overview.error ? (
201
+ <ApiError error={overview.error} title="Could not load the overview" onRetry={() => void overview.refetch()} />
202
+ ) : overview.data ? (
203
+ <Overview data={overview.data} onOpen={(competitorId) => { setTab("competitors"); setId(competitorId); }} onTrack={() => setTab("competitors")} />
204
+ ) : (
205
+ <p className="text-sm text-muted-foreground" role="status">Loading overview...</p>
206
+ )
207
+ ) : tab === "competitors" ? (
208
+ split(<CompetitorTracker key={tick} onSelect={(c) => setId(c.id)} />, <CompetitorDetail key={id} competitorId={id} {...panel} {...slots({ kind: "competitor", id })} />)
209
+ ) : (
210
+ split(<RadarReport key={tick} onSelectReport={(r) => setId(r.id)} />, <ReportDetail key={id} reportId={id} {...panel} {...slots({ kind: "report", id })} />)
211
+ )}
212
+ </ScreenLayout>
213
+ );
214
+ }
@@ -0,0 +1,104 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ReportDetail: the complete screen for one radar report. Finds it by id (so `?id=` deep links work), shows its capability,
5
+ * audience, angle, status and times, follows it while it generates, links to the finished HTML and PDF (through your proxy so the
6
+ * signed-in session authorizes them), and can regenerate it. Regenerating overwrites the earlier report for the same
7
+ * capability, audience and angle.
8
+ *
9
+ * SDK operations, in order:
10
+ * 1. listRadarReports GET /radar/reports (read; re-read every 4 s while generating)
11
+ * 2. getRadarReportHtml GET /radar/reports/{id}/html (read; opened as a link)
12
+ * 3. getRadarReportPdf GET /radar/reports/{id}/pdf (read; opened as a link)
13
+ * 4. createRadarReport POST /radar/reports (billable, confirmed; no advance price)
14
+ */
15
+ import { useEffect, type ReactNode } from "react";
16
+ import { Skeleton } from "@graph8/sdk/ui";
17
+ import { ApiError } from "@/g8/components/api-error";
18
+ import { EmptyState } from "@/g8/components/empty-state";
19
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
20
+ import { G8_PROXY_BASE, g8Get, g8Post } from "@/g8/lib/g8-client";
21
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
22
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
23
+ import { StatusPill } from "@/g8/components/screen/status-pill";
24
+ import { isReportFinal, radarBody } from "../lib/radar-types";
25
+ import type { RadarReportRow } from "../lib/radar-types";
26
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
27
+ import { AskButton, useConfirmAction } from "@/g8/components/confirmed-action";
28
+
29
+ export interface ReportDetailProps {
30
+ reportId: string;
31
+ onClose?: () => void;
32
+ onExpand?: () => void;
33
+ onCollapse?: () => void;
34
+ onChanged?: () => void;
35
+ /** Extra header actions from the screen's `renderDetailActions` slot. */
36
+ extraActions?: ReactNode;
37
+ /** Extra tabs from the screen's `extraDetailTabs` slot, after the built-in ones. */
38
+ extraTabs?: { id: string; label: string; content: ReactNode }[];
39
+ }
40
+
41
+ const when = (v: string | null | undefined) => (v ? new Date(v).toLocaleString() : "-");
42
+
43
+ export function ReportDetail({ reportId, onClose, onExpand, onCollapse, onChanged, extraActions, extraTabs }: ReportDetailProps) {
44
+ const { ask, dialog, failure } = useConfirmAction();
45
+ const query = useG8Query(async (signal) => radarBody<{ items?: RadarReportRow[]; reports?: RadarReportRow[] }>(await g8Get("/radar/reports", undefined, { signal })), []);
46
+ const rows = query.data?.items ?? query.data?.reports ?? [];
47
+ const report = rows.find((r) => r.id === reportId);
48
+ const generating = report ? !isReportFinal(report.status) : false;
49
+
50
+ useEffect(() => {
51
+ if (!generating) return;
52
+ const timer = setInterval(() => void query.refetch(), 4000);
53
+ return () => clearInterval(timer);
54
+ }, [generating, query.refetch]); // eslint-disable-line react-hooks/exhaustive-deps
55
+
56
+ if (query.error) return <ApiError error={query.error} title="Could not load the report" onRetry={() => void query.refetch()} />;
57
+ if (!query.data) return <Skeleton className="h-40 w-full" />;
58
+ if (!report) return <EmptyState title="Report not found" description="It may have been replaced by a newer one." />;
59
+ const title = report.title || report.capability_name || "Report";
60
+ const done = report.status === "completed";
61
+
62
+ const actions = (
63
+ <div className="flex flex-wrap items-center gap-2">
64
+ {done ? (
65
+ <>
66
+ <a className="text-sm underline" href={`${G8_PROXY_BASE}/radar/reports/${report.id}/html`} target="_blank" rel="noreferrer">Open</a>
67
+ <a className="text-sm underline" href={`${G8_PROXY_BASE}/radar/reports/${report.id}/pdf`} target="_blank" rel="noreferrer">PDF</a>
68
+ </>
69
+ ) : null}
70
+ <AskButton ask={ask} label="Regenerate" tier="billable" title={`Regenerate the ${report.audience} report on ${report.capability_name ?? "this capability"}`} confirmLabel="Generate"
71
+ disabled={generating || !report.capability_id}
72
+ description="A model writes the report and it uses credits. The API gives no price in advance and your balance is not checked here. This overwrites the earlier report for this capability and audience."
73
+ run={() => g8Post("/radar/reports", { audience: report.audience, capability_id: report.capability_id, edge: report.edge ?? "us" })}
74
+ onDone={() => { void query.refetch(); onChanged?.(); }} />
75
+ </div>
76
+ );
77
+ const tabs = [
78
+ {
79
+ id: "report",
80
+ label: "Report",
81
+ content: (
82
+ <div className="space-y-3">
83
+ <KeyValueList columns={2} items={[
84
+ { label: "Capability", value: report.capability_name ?? "-" },
85
+ { label: "Audience", value: report.audience },
86
+ { label: "Angle", value: report.edge === "them" ? "Where they win" : report.edge === "us" ? "Where we win" : "-" },
87
+ { label: "Status", value: generating ? "generating..." : report.status },
88
+ { label: "Created", value: when(report.created_at) },
89
+ { label: "Completed", value: when(report.completed_at) },
90
+ ]} />
91
+ {report.error_message ? <p role="alert" className="text-sm text-error-foreground">{report.error_message}</p> : null}
92
+ {done ? <iframe title={`${title} preview`} className="h-[28rem] w-full rounded border" src={`${G8_PROXY_BASE}/radar/reports/${report.id}/html`} /> : null}
93
+ </div>
94
+ ),
95
+ },
96
+ ];
97
+ return (
98
+ <>
99
+ <DetailPanel title={title} subtitle={report.audience} badges={<StatusPill tone={done ? "positive" : report.status === "failed" ? "negative" : "info"}>{report.status}</StatusPill>} actions={extraActions ? <>{actions}{extraActions}</> : actions} tabs={extraTabs?.length ? [...tabs, ...extraTabs] : tabs} onClose={onClose} onExpand={onExpand} onCollapse={onCollapse} />
100
+ {failure ? <ApiError error={failure} title="That did not work" /> : null}
101
+ <ConfirmSpendDialog {...dialog} />
102
+ </>
103
+ );
104
+ }
@@ -6,6 +6,16 @@ searches. Builds on `lists` (which builds on `crm`): once results are saved, you
6
6
 
7
7
  Install: `npx @graph8/sdk add search` (installs `foundation`, `crm` and `lists` first).
8
8
 
9
+ ## Screens
10
+
11
+ `ProspectSearchScreen` is the finished prospecting page; mount it with no props. People and Companies tabs (`?tab=`),
12
+ the filter builder with real value suggestions and Clear filters, saved searches (save, load, delete), results with
13
+ paging and the locked-field banner, **Save results to a list** (billable, confirmed, polled until built) and, after
14
+ "Open list", a panel with the saved list's real contacts (emails and phones unlocked) that can be added to another list.
15
+ A **Saved searches** tab (`?tab=saved&id=`) opens each saved search with its filters, Run and a confirmed Delete; a result's name opens a prospect detail with every field (locked ones stay hidden; results have no id, so no deep link). Slots: `toolbarActions`, `renderSavedListActions(list)` (for example an enrich or enroll button), `onSaved`.
16
+
17
+ Prerequisites: `lists` and `crm` installed (the CLI does it), proxy allowlist `search`, `lists`, `contacts`, an active subscription.
18
+
9
19
  ## Read this first: search results are masked
10
20
 
11
21
  `POST /search/contacts` returns **previews**, not CRM records:
@@ -8,7 +8,8 @@
8
8
  "paged contact and company results with total and paging-window handling",
9
9
  "locked emails and phones shown as Locked, with a one-step path to reveal them",
10
10
  "save-to-list into a new list with a billable confirmation and polled progress",
11
- "saved searches: save, load, delete"
11
+ "saved searches: save, load, delete",
12
+ "finished ProspectSearchScreen page"
12
13
  ],
13
14
  "dependsOn": [
14
15
  "lists"
@@ -90,6 +91,19 @@
90
91
  "write",
91
92
  "destructive"
92
93
  ]
94
+ },
95
+ {
96
+ "name": "ProspectSearchScreen",
97
+ "file": "components/prospect-search-screen.tsx",
98
+ "purpose": "Finished prospecting page: People/Companies tabs, filters, saved searches, results, save to list and the saved list's unlocked members.",
99
+ "operations": [
100
+ "delete_saved_search_search_saved_searches__query_id__delete",
101
+ "list_saved_searches_search_saved_searches_get"
102
+ ],
103
+ "tiers": [
104
+ "read",
105
+ "destructive"
106
+ ]
93
107
  }
94
108
  ],
95
109
  "hooks": [
@@ -180,6 +194,7 @@
180
194
  }
181
195
  ],
182
196
  "peerDependencies": {
197
+ "lucide-react": ">=0.400.0",
183
198
  "react": ">=18"
184
199
  }
185
200
  }
@@ -54,10 +54,10 @@ function useDebounced<T>(value: T, ms = 250): T {
54
54
  return v;
55
55
  }
56
56
 
57
- function ValuePicker({ resource, meta, value, onChange, disabled }: { resource: SearchResource; meta: FieldMeta; value: unknown[]; onChange: (v: unknown[]) => void; disabled?: boolean }) {
57
+ function ValuePicker({ resource, meta, value, onChange, disabled, within }: { resource: SearchResource; meta: FieldMeta; within?: SearchCondition[]; value: unknown[]; onChange: (v: unknown[]) => void; disabled?: boolean }) {
58
58
  const [draft, setDraft] = useState("");
59
59
  const typed = useDebounced(draft.trim());
60
- const options = useFilterOptions(resource, meta.key, meta.suggest === "options");
60
+ const options = useFilterOptions(resource, meta.key, meta.suggest === "options", within);
61
61
  const auto = useG8Query(
62
62
  async (signal) => extractSuggestions((await g8Get("/search/autocomplete", { resource, field: meta.key, value: typed, size: 8 }, { signal })).data),
63
63
  [resource, meta.key, typed],
@@ -134,7 +134,7 @@ export function FilterBuilder({ resource = "contacts", value, onChange, onSearch
134
134
  </select>
135
135
  {NO_VALUE_OPERATORS.includes(c.operator) ? null : c.operator === "between"
136
136
  ? <BetweenPicker meta={m} value={c.value ?? []} disabled={disabled} onChange={(v) => update(i, { value: v })} />
137
- : <ValuePicker resource={resource} meta={m} value={c.value ?? []} disabled={disabled} onChange={(v) => update(i, { value: v })} />}
137
+ : <ValuePicker resource={resource} meta={m} within={value.filter((_, j) => j !== i)} value={c.value ?? []} disabled={disabled} onChange={(v) => update(i, { value: v })} />}
138
138
  <Button type="button" variant="ghost" size="sm" disabled={disabled} aria-label="Remove filter" onClick={() => onChange(value.filter((_, j) => j !== i))}>Remove</Button>
139
139
  </div>
140
140
  );