@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
@@ -0,0 +1,222 @@
1
+ "use client";
2
+
3
+ /**
4
+ * CoachScreen: the finished sales coach page. Tabs: Practice calls (graded calls newest first with a score and the first
5
+ * tip; open one for the full scorecard, recording and transcript, deep link `?id=`), Leaderboard (reps ranked by score) and
6
+ * Real calls (the rep's dialer calls, only when a rep email is known). Stats summarise the loaded calls: how many, the
7
+ * average and best score, and the stage reps miss most.
8
+ *
9
+ * Flows: browse and open calls; load a signed recording (CallScorecard); delete a call (destructive, ConfirmSpendDialog;
10
+ * hide with allowDelete={false}); create a practice call row (write; the live practice conversation itself runs in the
11
+ * graph8 app, so the form links out); managers type a rep's email to read that rep's calls (needs a manager key, a 403 is shown).
12
+ *
13
+ * SDK operations (through the composed parts): GET /sales-coach/calls, GET /sales-coach/leaderboard, POST /sales-coach/calls,
14
+ * and CallScorecard's getCall, getCallTranscript, getCallAudioUrl and deleteCall; GET /voice/dialer/calls via the dialer block.
15
+ */
16
+ import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
17
+ import { GraduationCap } from "lucide-react";
18
+ import { Button, Input, Label, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { EmptyState } from "@/g8/components/empty-state";
21
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
22
+ import { EntityAvatar } from "@/g8/components/screen/entity-avatar";
23
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
24
+ import { ScreenLayout } from "@/g8/components/screen/screen-layout";
25
+ import { SplitView } from "@/g8/components/screen/split-view";
26
+ import { StatusPill } from "@/g8/components/screen/status-pill";
27
+ import { Toolbar } from "@/g8/components/screen/toolbar";
28
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
29
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
30
+ import { useUrlState } from "@/g8/hooks/use-url-state";
31
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
32
+ import { CallHistory } from "@/g8/dialer/components/call-log-screen";
33
+ import { avgOf, listOf, scorecardOf } from "../lib/coach-types";
34
+ import type { CoachCall, LeaderboardRow } from "../lib/coach-types";
35
+ import { CallScorecard } from "./call-scorecard";
36
+ import { useBrand } from "@/g8/hooks/use-brand";
37
+
38
+ export interface CoachScreenProps {
39
+ /** Whose practice calls. Omit for the key owner's own; managers may type another rep's email. */
40
+ userEmail?: string;
41
+ /** Show the "New practice call" button (creates the row only). Default true when no rep email is set. */
42
+ allowCreate?: boolean;
43
+ allowDelete?: boolean;
44
+ toolbarActions?: ReactNode;
45
+ renderRowActions?: (call: CoachCall) => ReactNode;
46
+ renderDetailActions?: (call: CoachCall) => ReactNode;
47
+ extraDetailTabs?: Array<{ id: string; label: string; render: (call: CoachCall) => ReactNode }>;
48
+ className?: string;
49
+ }
50
+
51
+ function useDebounced<T>(value: T, ms = 400): T {
52
+ const [v, setV] = useState(value);
53
+ useEffect(() => {
54
+ const t = setTimeout(() => setV(value), ms);
55
+ return () => clearTimeout(t);
56
+ }, [value, ms]);
57
+ return v;
58
+ }
59
+
60
+ const scoreTone = (pct: number | null | undefined) => (pct == null ? "neutral" : pct >= 75 ? "positive" : pct >= 50 ? "warning" : "negative");
61
+ const EMPTY_FORM = { call_mode: "assisted" as "assisted" | "compete", prospect_name: "", prospect_company: "", persona_type: "" };
62
+
63
+ function weakestStage(rows: CoachCall[]): string | null {
64
+ const sums = new Map<string, { total: number; n: number }>();
65
+ for (const c of rows) for (const s of scorecardOf(c)?.stages ?? []) if (s.pct != null) sums.set(s.label, { total: (sums.get(s.label)?.total ?? 0) + s.pct, n: (sums.get(s.label)?.n ?? 0) + 1 });
66
+ const ranked = [...sums].map(([k, v]) => [k, v.total / v.n] as const).sort((a, b) => a[1] - b[1]);
67
+ return ranked[0]?.[0] ?? null;
68
+ }
69
+
70
+ function NewCallSheet({ open, onOpenChange, onCreated }: { open: boolean; onOpenChange: (o: boolean) => void; onCreated: () => void }) {
71
+ const brand = useBrand();
72
+ const [form, setForm] = useState(EMPTY_FORM);
73
+ const create = useG8Mutation(
74
+ () => g8Post("/sales-coach/calls", { call_mode: form.call_mode, prospect_name: form.prospect_name.trim() || undefined, prospect_company: form.prospect_company.trim() || undefined, persona_type: form.persona_type.trim() || undefined }),
75
+ { onSuccess: () => { setForm(EMPTY_FORM); onOpenChange(false); onCreated(); } },
76
+ );
77
+ const field = (id: string, text: string, key: "prospect_name" | "prospect_company" | "persona_type") => (
78
+ <div className="flex flex-col gap-1"><Label htmlFor={id}>{text}</Label><Input id={id} value={form[key]} onChange={(e) => setForm({ ...form, [key]: e.target.value })} /></div>
79
+ );
80
+ return (
81
+ <FormSheet
82
+ open={open}
83
+ onOpenChange={onOpenChange}
84
+ title="New practice call"
85
+ description={`This adds the call to your list. The live practice conversation runs in ${brand.linkLabel}.`}
86
+ footer={<><Button variant="ghost" onClick={() => onOpenChange(false)}>Cancel</Button><Button loading={create.loading} onClick={() => void create.mutate(undefined)}>Create call</Button></>}
87
+ >
88
+ <div className="grid gap-3">
89
+ {field("pc-name", "Prospect", "prospect_name")}
90
+ {field("pc-company", "Company", "prospect_company")}
91
+ {field("pc-persona", "Persona", "persona_type")}
92
+ <div className="flex flex-col gap-1"><Label htmlFor="pc-mode">Mode</Label>
93
+ <select id="pc-mode" className="h-9 rounded-md border border-border bg-background px-2 text-sm" value={form.call_mode} onChange={(e) => setForm({ ...form, call_mode: e.target.value as "assisted" | "compete" })}><option value="assisted">Training</option><option value="compete">Compete</option></select>
94
+ </div>
95
+ {create.error ? <ApiError error={create.error} title="Could not create the call" /> : null}
96
+ <a className="text-sm underline" href={brand.appUrl} target="_blank" rel="noreferrer">Practice in {brand.linkLabel}</a>
97
+ </div>
98
+ </FormSheet>
99
+ );
100
+ }
101
+
102
+ function Leaderboard({ enabled }: { enabled: boolean }) {
103
+ const board = useG8Query((signal) => g8Get("/sales-coach/leaderboard", undefined, { signal }), [], { enabled });
104
+ const ranked = [...listOf<LeaderboardRow>(board.data, "items")].sort((a, b) => b.score - a.score);
105
+ if (board.error) return <ApiError error={board.error} title="Could not load the leaderboard" onRetry={() => void board.refetch()} />;
106
+ if (board.loading && ranked.length === 0) return <Skeleton className="h-32 w-full" aria-label="Loading leaderboard" />;
107
+ if (ranked.length === 0) return <EmptyState title="Nobody on the board yet" description="Completed practice calls rank here." />;
108
+ return (
109
+ <Table>
110
+ <TableHeader><TableRow><TableHead className="w-12">#</TableHead><TableHead>Rep</TableHead><TableHead className="text-right">Calls</TableHead><TableHead className="text-right">Avg</TableHead><TableHead className="text-right">Total</TableHead></TableRow></TableHeader>
111
+ <TableBody>
112
+ {ranked.map((r, i) => (
113
+ <TableRow key={r.email} data-testid={`rank-${r.email}`}>
114
+ <TableCell>{i + 1}</TableCell>
115
+ <TableCell><span className="flex items-center gap-2"><EntityAvatar name={r.name || r.email} kind="person" size="sm" />{r.name}<span className="text-xs text-muted-foreground">{r.email}</span></span></TableCell>
116
+ <TableCell className="text-right">{r.calls}</TableCell><TableCell className="text-right">{avgOf(r)}</TableCell><TableCell className="text-right">{r.score}</TableCell>
117
+ </TableRow>
118
+ ))}
119
+ </TableBody>
120
+ </Table>
121
+ );
122
+ }
123
+
124
+ export function CoachScreen(props: CoachScreenProps) {
125
+ const brand = useBrand();
126
+ const { allowDelete, toolbarActions, renderRowActions, renderDetailActions, extraDetailTabs, className } = props;
127
+ const [tab, setTab] = useUrlState("tab", "calls");
128
+ const [openId, setOpenId] = useUrlState("id");
129
+ const [view, setView] = useUrlState("view");
130
+ const [typed, setTyped] = useState("");
131
+ const [shown, setShown] = useState(15);
132
+ const [creating, setCreating] = useState(false);
133
+ const typedEmail = useDebounced(typed.trim());
134
+ const email = props.userEmail || typedEmail || undefined;
135
+ const allowCreate = props.allowCreate ?? !email;
136
+
137
+ const calls = useG8Query((signal) => g8Get("/sales-coach/calls", { email }, { signal }), [email]);
138
+ // Another rep's calls are a different list: forget the selection and paging.
139
+ const lastEmail = useRef(email);
140
+ useEffect(() => {
141
+ if (lastEmail.current === email) return; // keep a deep-linked ?id= on first render
142
+ lastEmail.current = email;
143
+ setShown(15);
144
+ setOpenId(null);
145
+ }, [email]); // eslint-disable-line react-hooks/exhaustive-deps
146
+ const rows = listOf<CoachCall>(calls.data, "items");
147
+ const selected = openId ? rows.find((c) => c.id === openId) : undefined;
148
+ const cards = useMemo(() => rows.map((c) => scorecardOf(c)?.scorePct).filter((n): n is number => typeof n === "number"), [rows]);
149
+ const avg = cards.length ? Math.round(cards.reduce((a, b) => a + b, 0) / cards.length) : null;
150
+ const tabs = [{ id: "calls", label: "Practice calls", count: rows.length || undefined }, { id: "board", label: "Leaderboard" }, ...(email ? [{ id: "real", label: "Real calls" }] : [])];
151
+ const active = tabs.some((t) => t.id === tab) ? tab : "calls";
152
+
153
+ const list = (
154
+ <div className="space-y-2">
155
+ {calls.error ? <ApiError error={calls.error} title="Could not load practice calls" onRetry={() => void calls.refetch()} /> : null}
156
+ {calls.loading && rows.length === 0 ? Array.from({ length: 3 }, (_, i) => <Skeleton key={i} className="h-16 w-full" />) : null}
157
+ <ul className="flex flex-col gap-2" data-testid="feed">
158
+ {rows.slice(0, shown).map((c) => {
159
+ const card = scorecardOf(c);
160
+ const tip = card?.improvements[0];
161
+ return (
162
+ <li key={c.id} className="flex items-start gap-1">
163
+ <button type="button" data-testid={`call-${c.id}`} onClick={() => setOpenId(c.id)} className={`min-w-0 flex-1 rounded-md border border-border p-3 text-left ${openId === c.id ? "bg-muted" : "hover:bg-muted/50"}`}>
164
+ <span className="flex items-center justify-between gap-2">
165
+ <span className="truncate font-medium">{c.prospect_name || "Practice call"}{c.prospect_company ? ` at ${c.prospect_company}` : ""}</span>
166
+ {card?.scorePct != null ? <StatusPill tone={scoreTone(card.scorePct)}>{`${Math.round(card.scorePct)}%`}</StatusPill> : <StatusPill tone="neutral">{c.call_status ?? "not graded"}</StatusPill>}
167
+ </span>
168
+ <span className="block text-xs text-muted-foreground">{c.date_of_call ? new Date(c.date_of_call).toLocaleDateString() : ""} {c.call_mode ?? ""}</span>
169
+ {tip ? <span className="mt-1 block text-sm">Work on: {tip}</span> : null}
170
+ </button>
171
+ {renderRowActions ? <div className="shrink-0 p-1">{renderRowActions(c)}</div> : null}
172
+ </li>
173
+ );
174
+ })}
175
+ </ul>
176
+ {!calls.loading && !calls.error && rows.length === 0 ? (
177
+ <EmptyState icon={<GraduationCap className="size-6" />} title="No practice calls yet" description={`Graded practice calls and what to work on next show up here. Practice calls run in ${brand.linkLabel}.`} action={allowCreate ? <Button size="sm" onClick={() => setCreating(true)}>New practice call</Button> : undefined} />
178
+ ) : null}
179
+ {rows.length > shown ? <Button variant="outline" size="sm" onClick={() => setShown(shown + 15)}>Show more</Button> : null}
180
+ </div>
181
+ );
182
+ const expanded = view === "full" && Boolean(openId);
183
+ const detail = openId ? (
184
+ <DetailPanel
185
+ title={selected?.prospect_name || "Practice call"}
186
+ subtitle={selected?.prospect_company ?? undefined}
187
+ actions={selected ? renderDetailActions?.(selected) : undefined}
188
+ onClose={() => { setOpenId(null); setView(null); }}
189
+ onExpand={expanded ? undefined : () => setView("full")}
190
+ onCollapse={expanded ? () => setView(null) : undefined}
191
+ tabs={[
192
+ { id: "scorecard", label: "Scorecard", content: <CallScorecard key={openId} callId={openId} allowDelete={allowDelete} onDeleted={() => { setOpenId(null); setView(null); void calls.refetch(); }} /> },
193
+ ...(selected ? (extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(selected) })) : []),
194
+ ]}
195
+ />
196
+ ) : null;
197
+
198
+ return (
199
+ <ScreenLayout
200
+ className={className}
201
+ title="Sales coach"
202
+ description="Scored practice calls, what to work on next, and how the team ranks."
203
+ icon={<GraduationCap className="size-5" />}
204
+ actions={<>{allowCreate ? <Button size="sm" onClick={() => setCreating(true)}>New practice call</Button> : null}{toolbarActions}</>}
205
+ tabs={tabs}
206
+ activeTab={active}
207
+ onTabChange={(id) => { setTab(id); setOpenId(null); setView(null); }}
208
+ stats={rows.length > 0 ? [
209
+ { label: "Calls loaded", value: rows.length },
210
+ { label: "Average score", value: avg != null ? `${avg}%` : "", tone: scoreTone(avg) },
211
+ { label: "Best score", value: cards.length ? `${Math.round(Math.max(...cards))}%` : "" },
212
+ { label: "Weakest stage", value: weakestStage(rows) ?? "", hint: "lowest average" },
213
+ ] : undefined}
214
+ toolbar={props.userEmail ? undefined : <Toolbar search={{ value: typed, onChange: setTyped, placeholder: "Rep email (managers)" }} />}
215
+ >
216
+ {active === "calls" ? <SplitView list={list} detail={detail} detailOpen={Boolean(openId)} onCloseDetail={() => { setOpenId(null); setView(null); }} expanded={expanded} onExpandedChange={(next) => setView(next ? "full" : null)} /> : null}
217
+ {active === "board" ? <Leaderboard enabled /> : null}
218
+ {active === "real" ? <CallHistory userEmail={email} idParam="call" /> : null}
219
+ <NewCallSheet open={creating} onOpenChange={setCreating} onCreated={() => void calls.refetch()} />
220
+ </ScreenLayout>
221
+ );
222
+ }
@@ -4,6 +4,19 @@ Drop an "Ask graph8" side panel into your app. It chats with the copilot (which
4
4
  copy and propose changes), chats in a Work channel where agents answer, and manages what the copilot
5
5
  remembers. Everything is request/response or polled: the API has no streams.
6
6
 
7
+ ## Screens
8
+
9
+ `CopilotScreen` (`components/copilot-screen.tsx`) is the copilot as a full page; mount it as the page.
10
+
11
+ - **Ask**: the conversation with starter questions (`starterPrompts`), an **Ask about** card (whole workspace, one
12
+ campaign or one account; changing it (on Enter or leaving the field) starts a fresh conversation) and a **What it can do** card listing the tools
13
+ and which need approval in graph8. The first message of a session confirms credits. The conversation survives
14
+ switching tabs.
15
+ - **Team**: every Work channel as a list; opening one (`?id=`, full page with `&view=full`) shows the whole thread: details and the polled conversation with a message box and "ask an agent" (billable, confirmed).
16
+ - **Memory**: add and forget (confirmed) what the copilot remembers.
17
+ - Needs `knowledge:run|read|write|delete` and `work:read|write`. `CopilotPanel` gained `hideTabs` and
18
+ `starterPrompts`; `CopilotTeamChat` gained `hideChannelPicker`.
19
+
7
20
  Install: `npx @graph8/sdk add copilot` (installs `foundation` first).
8
21
 
9
22
  ## What you get
@@ -26,6 +26,18 @@
26
26
  "search_copilot_mentions_copilot_mention_search_post"
27
27
  ],
28
28
  "components": [
29
+ {
30
+ "name": "CopilotScreen",
31
+ "file": "components/copilot-screen.tsx",
32
+ "purpose": "Full-page copilot: conversation with starters, ask-about context, tools card, team channel and memory tabs.",
33
+ "operations": [
34
+ "list_copilot_tools_copilot_tools_get",
35
+ "list_work_channels_work_channels_get"
36
+ ],
37
+ "tiers": [
38
+ "read"
39
+ ]
40
+ },
29
41
  {
30
42
  "name": "CopilotPanel",
31
43
  "file": "components/copilot-panel.tsx",
@@ -146,6 +158,7 @@
146
158
  }
147
159
  ],
148
160
  "peerDependencies": {
149
- "react": ">=18"
161
+ "react": ">=18",
162
+ "lucide-react": ">=0.400.0"
150
163
  }
151
164
  }
@@ -27,6 +27,7 @@ import { listOf, mentionLabel, summariseReply } from "../lib/copilot-types";
27
27
  import type { ChatTurn, CopilotReply, MentionHit } from "../lib/copilot-types";
28
28
  import { CopilotMemory } from "./copilot-memory";
29
29
  import { CopilotTeamChat } from "./copilot-team-chat";
30
+ import { useBrand } from "@/g8/hooks/use-brand";
30
31
 
31
32
  export interface CopilotPanelProps {
32
33
  /** Where the copilot is being asked from; passed as `canvas_context`. */
@@ -35,6 +36,10 @@ export interface CopilotPanelProps {
35
36
  initialTab?: "ask" | "team" | "memory";
36
37
  /** Ask for confirmation before every message instead of once per session. */
37
38
  confirmEveryMessage?: boolean;
39
+ /** Hide the Ask / Team / Memory tab bar when the host (for example CopilotScreen) provides its own navigation. */
40
+ hideTabs?: boolean;
41
+ /** Example questions shown as buttons in the empty conversation; clicking one fills the message box. */
42
+ starterPrompts?: string[];
38
43
  className?: string;
39
44
  }
40
45
 
@@ -47,7 +52,8 @@ const mentionsIn = (message: string, list: MentionHit[]): MentionHit[] | undefin
47
52
  };
48
53
  const MENTION_AT_END = /(^|\s)@([\w.-]{1,40})$/;
49
54
 
50
- export function CopilotPanel({ context, initialTab = "ask", confirmEveryMessage = false, className }: CopilotPanelProps) {
55
+ export function CopilotPanel({ context, initialTab = "ask", confirmEveryMessage = false, hideTabs = false, starterPrompts, className }: CopilotPanelProps) {
56
+ const brand = useBrand();
51
57
  const [tab, setTab] = useState<Tab>(initialTab);
52
58
  const [turns, setTurns] = useState<ChatTurn[]>([]);
53
59
  const [draft, setDraft] = useState("");
@@ -129,22 +135,25 @@ export function CopilotPanel({ context, initialTab = "ask", confirmEveryMessage
129
135
 
130
136
  return (
131
137
  <div className={`flex h-full min-h-[24rem] flex-col gap-3 ${className ?? ""}`}>
132
- <div role="tablist" className="flex gap-1 border-b">
138
+ {hideTabs ? null : <div role="tablist" className="flex gap-1 border-b">
133
139
  {TABS.map((t) => (
134
140
  <button key={t.id} role="tab" type="button" aria-selected={tab === t.id} onClick={() => setTab(t.id)}
135
141
  className={`px-3 py-2 text-sm ${tab === t.id ? "border-b-2 border-primary font-medium" : "text-muted-foreground"}`}>{t.label}</button>
136
142
  ))}
137
- </div>
143
+ </div>}
138
144
 
139
145
  {tab === "ask" ? (
140
146
  <>
141
147
  <div className="flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto" aria-live="polite" data-testid="transcript">
142
- {turns.length === 0 ? <EmptyState title="Ask the copilot" description="It can look things up, draft copy and propose changes. Type @ to mention a campaign, document or teammate." /> : null}
148
+ {turns.length === 0 ? (
149
+ <EmptyState title="Ask the copilot" description="It can look things up, draft copy and propose changes. Type @ to mention a campaign, document or teammate."
150
+ action={starterPrompts?.length ? <div className="flex flex-wrap justify-center gap-2">{starterPrompts.map((p) => <Button key={p} size="sm" variant="outline" onClick={() => setDraft(p)}>{p}</Button>)}</div> : undefined} />
151
+ ) : null}
143
152
  {turns.map((t) => (
144
153
  <div key={t.id} data-testid={`turn-${t.role}`} className={`max-w-[85%] rounded-lg px-3 py-2 text-sm ${t.role === "user" ? "self-end bg-primary text-primary-foreground" : t.error ? "self-start border border-destructive" : "self-start bg-muted"}`}>
145
154
  <p className="whitespace-pre-wrap">{t.text}</p>
146
155
  {t.tools?.length ? <p className="mt-1 text-xs opacity-70">Used: {t.tools.join(", ")}</p> : null}
147
- {t.needsApproval?.length ? <p className="mt-1 text-xs text-amber-700">Needs your approval in graph8 before it runs: {t.needsApproval.join(", ")}</p> : null}
156
+ {t.needsApproval?.length ? <p className="mt-1 text-xs text-amber-700">Needs your approval in {brand.linkLabel} before it runs: {t.needsApproval.join(", ")}</p> : null}
148
157
  {t.sources ? <p className="mt-1 text-xs opacity-70">{t.sources} sources</p> : null}
149
158
  </div>
150
159
  ))}
@@ -0,0 +1,148 @@
1
+ "use client";
2
+
3
+ /**
4
+ * CopilotScreen: the graph8 copilot as a full page. Three tabs.
5
+ * Ask: the conversation (CopilotPanel without its own tabs), starter questions, an "Ask about" card that points the
6
+ * copilot at the whole workspace, one campaign or one account, and a "What it can do" card listing its tools and
7
+ * which need a person's approval in graph8.
8
+ * Team: every Work channel the key can read as a list; opening one (deep link ?id=) shows the whole thread: its details
9
+ * and the polled conversation with "ask an agent" and a message box (CopilotTeamChat).
10
+ * Memory: what the copilot remembers, add / forget (CopilotMemory).
11
+ * The Ask conversation stays alive while you visit other tabs.
12
+ * The first message of a session asks to confirm credits (billable) inside CopilotPanel; forgetting a memory and
13
+ * posting to an agent are confirmed in their parts.
14
+ *
15
+ * SDK operations of its own:
16
+ * 1. GET /copilot/tools (copilot.listCopilotTools) read the "What it can do" card
17
+ * Everything else is CopilotPanel / CopilotTeamChat / CopilotMemory (chat, mention search, work channels and
18
+ * messages, memories). Slot props: toolbarActions, sidebarExtra.
19
+ */
20
+ import { useState, type ReactNode } from "react";
21
+ import { Sparkles } from "lucide-react";
22
+ import { Input, Label, Skeleton } from "@graph8/sdk/ui";
23
+ import { EmptyState } from "@/g8/components/empty-state";
24
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
25
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
26
+ import { SplitView } from "@/g8/components/screen/split-view";
27
+ import { ApiError } from "@/g8/components/api-error";
28
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
29
+ import { g8Get } from "@/g8/lib/g8-client";
30
+ import { ScreenLayout } from "@/g8/components/screen/screen-layout";
31
+ import { SectionCard } from "@/g8/components/screen/section-card";
32
+ import { StatusPill } from "@/g8/components/screen/status-pill";
33
+ import { useUrlState } from "@/g8/hooks/use-url-state";
34
+ import { listOf } from "../lib/copilot-types";
35
+ import type { ToolRow } from "../lib/copilot-types";
36
+ import { CopilotMemory } from "./copilot-memory";
37
+ import { CopilotPanel } from "./copilot-panel";
38
+ import { CopilotTeamChat } from "./copilot-team-chat";
39
+ import type { WorkChannel } from "../lib/copilot-types";
40
+ import { useBrand } from "@/g8/hooks/use-brand";
41
+
42
+ export interface CopilotScreenProps {
43
+ /** Starter questions shown in the empty conversation. */
44
+ starterPrompts?: string[];
45
+ /** Start with the copilot pointed at this campaign or account. */
46
+ initialContext?: { mode: "global" | "campaign" | "account"; id?: string };
47
+ toolbarActions?: ReactNode;
48
+ sidebarExtra?: ReactNode;
49
+ className?: string;
50
+ }
51
+
52
+ const TABS = [{ id: "ask", label: "Ask" }, { id: "team", label: "Team" }, { id: "memory", label: "Memory" }];
53
+ const MODES = [{ id: "global", label: "Everything" }, { id: "campaign", label: "One campaign" }, { id: "account", label: "One account" }] as const;
54
+ const STARTERS = ["Summarise how outbound is doing this month", "Draft a follow-up for a prospect who went quiet", "Which of my campaigns needs attention?"];
55
+
56
+ function ToolsCard() {
57
+ const tools = useG8Query((signal) => g8Get("/copilot/tools", undefined, { signal }), []);
58
+ const rows = listOf<ToolRow>(tools.data, "items");
59
+ const gated = rows.filter((t) => t.requires_approval || t.needs_approval);
60
+ return (
61
+ <SectionCard title="What it can do" description={rows.length ? `${rows.length} tools, ${gated.length} need your approval` : undefined}>
62
+ {tools.error ? <ApiError error={tools.error} title="Could not load tools" onRetry={() => void tools.refetch()} /> : null}
63
+ {tools.loading && rows.length === 0 ? <Skeleton className="h-16 w-full" /> : null}
64
+ <ul className="max-h-64 space-y-2 overflow-y-auto" data-testid="tools">
65
+ {rows.map((t) => (
66
+ <li key={t.name} className="flex items-start justify-between gap-2 text-sm">
67
+ <span><span className="font-medium">{t.name}</span>{t.description ? <span className="block text-xs text-muted-foreground">{t.description}</span> : null}</span>
68
+ {t.requires_approval || t.needs_approval ? <StatusPill tone="warning">needs approval</StatusPill> : null}
69
+ </li>
70
+ ))}
71
+ </ul>
72
+ </SectionCard>
73
+ );
74
+ }
75
+
76
+ function TeamPane() {
77
+ const [selected, setSelected] = useUrlState("id", "");
78
+ const [view, setView] = useUrlState("view", "");
79
+ const close = () => { setSelected(""); setView(""); };
80
+ const channels = useG8Query((signal) => g8Get("/work/channels", undefined, { signal }), []);
81
+ const rows = listOf<WorkChannel>(channels.data, "channels");
82
+ const open = rows.find((c) => c.id === selected);
83
+ const label = (c: WorkChannel) => c.displayName ?? c.name ?? c.id ?? "Channel";
84
+ const list = (
85
+ <div className="flex flex-col gap-2">
86
+ {channels.error ? <ApiError error={channels.error} title="Could not load channels" onRetry={() => void channels.refetch()} /> : null}
87
+ {channels.loading && rows.length === 0 ? <Skeleton className="h-14 w-full" /> : null}
88
+ <ul className="rounded-md border" data-testid="channels">
89
+ {rows.map((c) => (
90
+ <li key={c.id} className={c.id === selected ? "bg-accent" : ""}>
91
+ <button type="button" data-testid={`channel-${c.id}`} onClick={() => setSelected(c.id ?? "")} className="block w-full border-b px-3 py-2 text-left">
92
+ <span className="block truncate text-sm font-medium">{label(c)}</span>
93
+ {c.description ? <span className="block truncate text-xs text-muted-foreground">{c.description}</span> : null}
94
+ </button>
95
+ </li>
96
+ ))}
97
+ </ul>
98
+ {!channels.loading && !channels.error && rows.length === 0 ? <EmptyState title="No channels" description="Work channels this key may read show up here." /> : null}
99
+ </div>
100
+ );
101
+ const detail = open ? (
102
+ <DetailPanel title={label(open)} subtitle={open.kind} onClose={close} onExpand={() => setView("full")}
103
+ tabs={[
104
+ { id: "conversation", label: "Conversation", content: <CopilotTeamChat key={open.id} channelId={open.id} hideChannelPicker /> },
105
+ { id: "details", label: "Details", content: <KeyValueList columns={2} items={[{ label: "Name", value: label(open) }, { label: "Kind", value: open.kind }, { label: "Purpose", value: open.description, wide: true }, { label: "Channel id", value: open.id, mono: true }]} /> },
106
+ ]} />
107
+ ) : selected && !channels.loading ? <EmptyState title="Channel not found" description="This key may not have a seat in it." /> : null;
108
+ return <SplitView list={list} detail={detail} detailOpen={Boolean(selected)} onCloseDetail={close} expanded={view === "full"} onExpandedChange={(e) => setView(e ? "full" : "")} detailWidth="xl" />;
109
+ }
110
+
111
+ export function CopilotScreen({ starterPrompts = STARTERS, initialContext, toolbarActions, sidebarExtra, className }: CopilotScreenProps) {
112
+ const brand = useBrand();
113
+ const [tab, setTab] = useUrlState("tab", "ask");
114
+ const [mode, setMode] = useState<"global" | "campaign" | "account">(initialContext?.mode ?? "global");
115
+ const [refId, setRefId] = useState(initialContext?.id ?? "");
116
+ // The id applies on blur or Enter, so typing does not restart the conversation on every keystroke.
117
+ const [refDraft, setRefDraft] = useState(initialContext?.id ?? "");
118
+ const current = TABS.some((t) => t.id === tab) ? tab : "ask";
119
+ const id = refId.trim() || null;
120
+ const context = { mode, campaignId: mode === "campaign" ? id : null, accountId: mode === "account" ? id : null };
121
+
122
+ return (
123
+ <ScreenLayout className={className} title="Copilot" description={`Ask ${brand.linkLabel} anything about your pipeline, draft with it, and decide what it remembers.`}
124
+ icon={<Sparkles className="size-5" />} tabs={TABS} activeTab={current}
125
+ onTabChange={setTab} actions={toolbarActions}>
126
+ <div className={current === "ask" ? "grid gap-4 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]" : "hidden"}>
127
+ {/* Keyed on the context so a changed target starts a fresh conversation instead of mixing subjects. */}
128
+ <CopilotPanel key={`${mode}:${id ?? ""}`} hideTabs initialTab="ask" context={context} starterPrompts={starterPrompts} className="min-h-[32rem] rounded-lg border p-3" />
129
+ <div className="space-y-4">
130
+ <SectionCard title="Ask about" description="Changing this starts a new conversation.">
131
+ <div role="group" aria-label="Ask about" className="flex flex-col gap-2">
132
+ <select aria-label="Subject" className="h-9 rounded-md border bg-background px-2 text-sm" value={mode} onChange={(e) => { setMode(e.target.value as typeof mode); setRefDraft(""); setRefId(""); }}>
133
+ {MODES.map((m) => <option key={m.id} value={m.id}>{m.label}</option>)}
134
+ </select>
135
+ {mode !== "global" ? (
136
+ <div className="flex flex-col gap-1"><Label htmlFor="cp-ref">{mode === "campaign" ? "Campaign id" : "Account id"}</Label><Input id="cp-ref" value={refDraft} onChange={(e) => setRefDraft(e.target.value)} onBlur={() => setRefId(refDraft)} onKeyDown={(e) => { if (e.key === "Enter") setRefId(refDraft); }} /></div>
137
+ ) : null}
138
+ </div>
139
+ </SectionCard>
140
+ <ToolsCard />
141
+ {sidebarExtra}
142
+ </div>
143
+ </div>
144
+ {current === "team" ? <TeamPane /> : null}
145
+ {current === "memory" ? <CopilotMemory /> : null}
146
+ </ScreenLayout>
147
+ );
148
+ }
@@ -33,6 +33,8 @@ export interface CopilotTeamChatProps {
33
33
  channelId?: string;
34
34
  /** Poll interval in ms. Default 4000. */
35
35
  pollMs?: number;
36
+ /** Hide the channel picker when the host (for example CopilotScreen) already chose the channel. */
37
+ hideChannelPicker?: boolean;
36
38
  className?: string;
37
39
  }
38
40
 
@@ -41,14 +43,14 @@ interface Page { conversation_id?: string; messages?: WorkMessageRow[] }
41
43
 
42
44
  const newKey = (): string => `team-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
43
45
 
44
- export function CopilotTeamChat({ channelId, pollMs = 4000, className }: CopilotTeamChatProps) {
46
+ export function CopilotTeamChat({ channelId, pollMs = 4000, hideChannelPicker, className }: CopilotTeamChatProps) {
45
47
  const [channel, setChannel] = useState<string | undefined>(channelId);
46
48
  const [text, setText] = useState("");
47
49
  const [agentId, setAgentId] = useState("");
48
50
  const [confirming, setConfirming] = useState(false);
49
51
  const draftKey = useRef(newKey());
50
52
 
51
- const channels = useG8Query((signal) => g8Get("/work/channels", undefined, { signal }), []);
53
+ const channels = useG8Query((signal) => g8Get("/work/channels", undefined, { signal }), [], { enabled: !(hideChannelPicker && channelId) });
52
54
  const agents = useG8Query((signal) => g8Get("/work/agents", { limit: 50 }, { signal }), []);
53
55
  const rows = listOf<WorkChannel>(channels.data, "channels");
54
56
  const roster = listOf<AgentRow>(agents.data, "items");
@@ -73,10 +75,10 @@ export function CopilotTeamChat({ channelId, pollMs = 4000, className }: Copilot
73
75
  return (
74
76
  <div className={`flex min-h-0 flex-1 flex-col gap-3 ${className ?? ""}`}>
75
77
  {channels.error ? <ApiError error={channels.error} title="Could not load channels" onRetry={() => void channels.refetch()} /> : null}
76
- <select aria-label="Channel" className="h-9 rounded-md border bg-background px-2 text-sm" value={active ?? ""} onChange={(e) => setChannel(e.target.value)}>
78
+ {hideChannelPicker ? null : <select aria-label="Channel" className="h-9 rounded-md border bg-background px-2 text-sm" value={active ?? ""} onChange={(e) => setChannel(e.target.value)}>
77
79
  {rows.map((c) => <option key={c.id} value={c.id}>{c.displayName ?? c.name ?? c.id}</option>)}
78
- </select>
79
- {!channels.loading && !channels.error && rows.length === 0 ? <EmptyState title="No channels" description="Work channels this key may read show up here." /> : null}
80
+ </select>}
81
+ {!hideChannelPicker && !channels.loading && !channels.error && rows.length === 0 ? <EmptyState title="No channels" description="Work channels this key may read show up here." /> : null}
80
82
 
81
83
  {active ? (
82
84
  <>
@@ -7,6 +7,28 @@ list, the dialer opens a `ContactDetail`), so their props are part of the contra
7
7
 
8
8
  Install: `npx @graph8/sdk add crm` (installs `foundation` first).
9
9
 
10
+ ## Screens
11
+
12
+ Three finished screens compose the parts below into complete workspaces. Each needs no props, deep-links the open record
13
+ (`?id=`), and takes optional slot props so other blocks can add buttons and tabs.
14
+
15
+ | Screen | What a person can do | Slots |
16
+ |---|---|---|
17
+ | `ContactsScreen` | Search and filter (name, title, seniority, country), sort the loaded page, paginate, open a contact (timeline, notes, tasks, fields), create, edit, delete one or many, import a CSV/Excel file, review and merge duplicates. Locked emails show as Locked. | `renderRowActions(row, { reload })`, `renderDetailActions(contact, { reload })` (`reload` refetches the list and the open record, keeping filters, page, sort and selection), `extraDetailTabs`, `toolbarActions`, `renderBulkActions`, `onOpenCompany`, `listId` |
18
+ | `CompaniesScreen` | Same flow for accounts: filters (name, domain, industry, country), detail with People, Activity, Notes, Tasks, create (domain required), edit, delete, duplicate review. | `renderRowActions(row, { reload })`, `renderDetailActions(company, { reload })`, `extraDetailTabs`, `toolbarActions`, `renderBulkActions`, `onOpenContact` |
19
+ | `TasksScreen` | The workspace-wide task queue: Open / Completed / All, search, priority / type / linked-record filters, tick to complete or reopen, create, edit, delete, bulk complete and delete, overdue marker. | `renderRowActions`, `renderDetailActions`, `toolbarActions`, `defaultRecord`, `onOpenRecord` |
20
+
21
+ ```tsx
22
+ "use client";
23
+ import { ContactsScreen } from "@/components/g8/crm/contacts-screen";
24
+ export default function Page() { return <ContactsScreen />; }
25
+ ```
26
+
27
+ Prerequisites are the block's own (proxy allow-list including `tasks`, scopes in `block.json`). Sorting works on the loaded page only
28
+ (the API has no sort). Deleting (rows, panel, bulk) and merging always ask first. `TasksScreen` links new tasks to a record only when you
29
+ pass `defaultRecord`; otherwise they are standalone. Company import does not exist in the API: importing contacts creates their
30
+ companies from the domain.
31
+
10
32
  ## What you get
11
33
 
12
34
  | Component | Use it for | Embed props |
@@ -73,6 +95,11 @@ message to a person.
73
95
  - `api-key-scopes` (api): the server key has the scopes in `block.json` (`contacts:delete` is needed for bulk merge).
74
96
  - `import-size-limit` (app): the proxy caps bodies at 5 MiB; larger files need a raised cap or a server-side import.
75
97
 
98
+ ## Known limits
99
+
100
+ - The tasks "Low" priority filter sends `priority=3`. `GET /tasks` takes a single priority value, so historical
101
+ tasks stored as `4` (also Low) are not matched by that filter; they still show as Low in the list.
102
+
76
103
  ## Defaults the agent can use without asking
77
104
 
78
105
  | Decision | Default |
@@ -119,4 +146,5 @@ Ask the person: who may delete or merge records (decide it in the proxy's `autho
119
146
  | duplicateCounts / listDuplicates / listRecentMerges / getBulkMergeStatus | GET /duplicates... | read | DedupeReview |
120
147
  | rejectSuggestion / startBulkMerge / undoMerge | POST /duplicates... | write | DedupeReview |
121
148
  | mergeContact / mergeCompany | POST /contacts/{id}/merge, /companies/{id}/merge | destructive | DedupeReview |
149
+ | listAllTasks / getTask / createGlobalTask | GET /tasks, GET /tasks/{task_id}, POST /tasks | read / read / write | TasksScreen |
122
150
  | assertContact, assertCompany (+ batch) | PUT /contacts/assert, /companies/assert | write | recipes (`g8.contacts.upsert`) |