@graph8/sdk 0.362.0 → 0.373.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (615) hide show
  1. package/README.md +33 -10
  2. package/blocks/_foundation/README.md +78 -1
  3. package/blocks/_foundation/block.json +123 -6
  4. package/blocks/_foundation/components/api-error.tsx +9 -7
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  7. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  8. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  9. package/blocks/_foundation/components/empty-state.tsx +5 -2
  10. package/blocks/_foundation/components/list-select.tsx +112 -0
  11. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  12. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  13. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  14. package/blocks/_foundation/components/screen/index.ts +2 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  17. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  18. package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
  19. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  20. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  21. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  22. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  23. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  24. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  25. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  26. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  27. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  28. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  29. package/blocks/_foundation/lib/g8-client.ts +1 -1
  30. package/blocks/_foundation/lib/paging.ts +57 -0
  31. package/blocks/_foundation/lib/plural.ts +23 -0
  32. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  33. package/blocks/_foundation/server/connect-route.ts +15 -0
  34. package/blocks/_foundation/server/connect.ts +127 -0
  35. package/blocks/_foundation/server/local-key.ts +70 -0
  36. package/blocks/_foundation/server/proxy.ts +103 -3
  37. package/blocks/_foundation/server/route.ts +16 -4
  38. package/blocks/agents/README.md +93 -2
  39. package/blocks/agents/block.json +343 -2
  40. package/blocks/agents/components/agent-activity.tsx +112 -0
  41. package/blocks/agents/components/agent-approvals.tsx +135 -0
  42. package/blocks/agents/components/agent-builder.tsx +29 -10
  43. package/blocks/agents/components/agent-conversations.tsx +143 -0
  44. package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
  45. package/blocks/agents/components/agent-operators.tsx +164 -0
  46. package/blocks/agents/components/agent-run-detail.tsx +119 -0
  47. package/blocks/agents/components/agent-runs.tsx +19 -10
  48. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  49. package/blocks/agents/components/agent-test-history.tsx +121 -0
  50. package/blocks/agents/components/agents-screen.tsx +226 -0
  51. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  52. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  53. package/blocks/agents/components/from-template-button.tsx +65 -0
  54. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  55. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  56. package/blocks/agents/components/knowledge-form.tsx +69 -0
  57. package/blocks/agents/components/name-dialog.tsx +32 -0
  58. package/blocks/agents/components/receptionist-button.tsx +46 -0
  59. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  60. package/blocks/agents/components/skill-editor.tsx +147 -0
  61. package/blocks/agents/components/skill-library.tsx +111 -0
  62. package/blocks/agents/lib/agent-types.ts +7 -1
  63. package/blocks/agents/lib/facts.tsx +13 -0
  64. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  65. package/blocks/agents/lib/load-more.tsx +141 -0
  66. package/blocks/agents/lib/skill-utils.ts +91 -0
  67. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  68. package/blocks/analytics/README.md +13 -0
  69. package/blocks/analytics/block.json +45 -1
  70. package/blocks/analytics/components/analytics-screen.tsx +222 -0
  71. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  72. package/blocks/analytics/components/leaderboard.tsx +14 -7
  73. package/blocks/analytics/components/report-builder.tsx +38 -11
  74. package/blocks/analytics/components/report-detail.tsx +84 -0
  75. package/blocks/analytics/components/trend-chart.tsx +62 -0
  76. package/blocks/appointments/README.md +6 -0
  77. package/blocks/appointments/block.json +33 -2
  78. package/blocks/appointments/components/availability-editor.tsx +17 -5
  79. package/blocks/appointments/components/booking-page.tsx +2 -2
  80. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  81. package/blocks/appointments/components/event-type-related.tsx +4 -2
  82. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  83. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  84. package/blocks/appointments/components/meeting-list.tsx +38 -18
  85. package/blocks/appointments/components/meetings-screen.tsx +7 -4
  86. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  87. package/blocks/appointments/components/scheduling-screen.tsx +2 -1
  88. package/blocks/block.schema.json +18 -0
  89. package/blocks/block.types.ts +17 -0
  90. package/blocks/campaigns/README.md +41 -11
  91. package/blocks/campaigns/block.json +105 -42
  92. package/blocks/campaigns/components/campaign-assets.tsx +17 -6
  93. package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
  94. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  95. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  96. package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
  97. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  98. package/blocks/campaigns/components/icp-detail.tsx +163 -0
  99. package/blocks/campaigns/components/icp-editor.tsx +22 -17
  100. package/blocks/campaigns/components/persona-detail.tsx +181 -0
  101. package/blocks/campaigns/components/persona-editor.tsx +19 -11
  102. package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
  103. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  104. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  105. package/blocks/coach/README.md +10 -0
  106. package/blocks/coach/block.json +16 -1
  107. package/blocks/coach/components/call-scorecard.tsx +7 -7
  108. package/blocks/coach/components/coach-screen.tsx +222 -0
  109. package/blocks/company-intelligence/README.md +132 -0
  110. package/blocks/company-intelligence/block.json +318 -0
  111. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  112. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  113. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  114. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  115. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  116. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  117. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  118. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  119. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  120. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  121. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  122. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  123. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  124. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  125. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  126. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  127. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  128. package/blocks/copilot/README.md +13 -0
  129. package/blocks/copilot/block.json +14 -1
  130. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  131. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  132. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  133. package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
  134. package/blocks/crm/README.md +34 -0
  135. package/blocks/crm/block.json +107 -2
  136. package/blocks/crm/components/companies-screen.tsx +359 -0
  137. package/blocks/crm/components/company-detail.tsx +10 -8
  138. package/blocks/crm/components/company-table.tsx +11 -4
  139. package/blocks/crm/components/contact-detail.tsx +10 -7
  140. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  141. package/blocks/crm/components/contact-table.tsx +10 -3
  142. package/blocks/crm/components/contacts-screen.tsx +377 -0
  143. package/blocks/crm/components/dedupe-review.tsx +13 -6
  144. package/blocks/crm/components/import-wizard.tsx +7 -9
  145. package/blocks/crm/components/list-select.tsx +20 -0
  146. package/blocks/crm/components/notes-panel.tsx +4 -4
  147. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  148. package/blocks/crm/components/record-form.tsx +6 -4
  149. package/blocks/crm/components/task-list.tsx +4 -4
  150. package/blocks/crm/components/tasks-screen.tsx +399 -0
  151. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  152. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  153. package/blocks/crm/hooks/use-page-query.ts +9 -1
  154. package/blocks/crm/lib/crm-types.ts +26 -2
  155. package/blocks/crm/lib/read-lists.ts +16 -0
  156. package/blocks/crm/lib/screen-parts.tsx +105 -0
  157. package/blocks/deals/README.md +24 -6
  158. package/blocks/deals/block.json +160 -5
  159. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  160. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  161. package/blocks/deals/components/deal-detail.tsx +25 -13
  162. package/blocks/deals/components/deal-filters.tsx +65 -0
  163. package/blocks/deals/components/deal-line-items.tsx +120 -0
  164. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  165. package/blocks/deals/components/deal-related.tsx +31 -10
  166. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  167. package/blocks/deals/components/deals-screen.tsx +66 -26
  168. package/blocks/deals/components/forecast-view.tsx +4 -4
  169. package/blocks/deals/components/pipeline-board.tsx +8 -6
  170. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  171. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  172. package/blocks/deals/components/stage-editor.tsx +5 -5
  173. package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
  174. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  175. package/blocks/deals/lib/deal-extras.ts +156 -0
  176. package/blocks/dialer/README.md +45 -4
  177. package/blocks/dialer/block.json +84 -3
  178. package/blocks/dialer/components/call-contact-button.tsx +269 -0
  179. package/blocks/dialer/components/call-log-screen.tsx +208 -0
  180. package/blocks/dialer/components/call-log.tsx +23 -7
  181. package/blocks/dialer/components/call-queue.tsx +4 -4
  182. package/blocks/dialer/components/dialer-screen.tsx +361 -0
  183. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  184. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  185. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  186. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  187. package/blocks/dialer/components/session-start-form.tsx +61 -12
  188. package/blocks/dialer/components/softphone.tsx +4 -2
  189. package/blocks/enrichment/README.md +29 -7
  190. package/blocks/enrichment/block.json +49 -9
  191. package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
  192. package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
  193. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  194. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  195. package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
  196. package/blocks/enrichment/components/waterfall-config.tsx +66 -17
  197. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  198. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  199. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  200. package/blocks/forms/README.md +8 -0
  201. package/blocks/forms/block.json +139 -18
  202. package/blocks/forms/components/form-activity.tsx +71 -0
  203. package/blocks/forms/components/form-backfill.tsx +108 -0
  204. package/blocks/forms/components/form-builder.tsx +4 -2
  205. package/blocks/forms/components/form-embed.tsx +4 -2
  206. package/blocks/forms/components/form-groups.tsx +151 -0
  207. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  208. package/blocks/forms/components/form-route-history.tsx +72 -0
  209. package/blocks/forms/components/form-saved-views.tsx +81 -0
  210. package/blocks/forms/components/forms-screen.tsx +56 -19
  211. package/blocks/forms/components/more-rows.tsx +30 -0
  212. package/blocks/forms/components/submissions-export.tsx +67 -0
  213. package/blocks/forms/components/submissions-table.tsx +17 -6
  214. package/blocks/forms/lib/forms-extra.ts +67 -0
  215. package/blocks/inbox/README.md +32 -3
  216. package/blocks/inbox/block.json +29 -2
  217. package/blocks/inbox/components/inbox-list.tsx +7 -3
  218. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  219. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  220. package/blocks/inbox/components/reply-composer.tsx +65 -9
  221. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  222. package/blocks/inbox/components/thread-badge.tsx +2 -2
  223. package/blocks/inbox/components/thread-view.tsx +1 -1
  224. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  225. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  226. package/blocks/inbox/lib/inbox-types.ts +47 -1
  227. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  228. package/blocks/integrations/README.md +63 -1
  229. package/blocks/integrations/block.json +234 -15
  230. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  231. package/blocks/integrations/components/connection-health.tsx +59 -0
  232. package/blocks/integrations/components/crm-detail.tsx +119 -0
  233. package/blocks/integrations/components/crm-records.tsx +103 -0
  234. package/blocks/integrations/components/field-mappings.tsx +61 -0
  235. package/blocks/integrations/components/integration-list.tsx +13 -8
  236. package/blocks/integrations/components/integrations-screen.tsx +152 -0
  237. package/blocks/integrations/components/migration-panel.tsx +133 -0
  238. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  239. package/blocks/integrations/components/sync-status.tsx +11 -9
  240. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  241. package/blocks/integrations/components/webhook-detail.tsx +179 -0
  242. package/blocks/integrations/components/webhook-manager.tsx +43 -25
  243. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  244. package/blocks/integrations/lib/crm-utils.ts +39 -0
  245. package/blocks/integrations/lib/facts.tsx +13 -0
  246. package/blocks/integrations/lib/integrations-utils.ts +11 -1
  247. package/blocks/integrations/lib/load-more.tsx +141 -0
  248. package/blocks/intent/README.md +14 -0
  249. package/blocks/intent/block.json +27 -1
  250. package/blocks/intent/components/account-intent-score.tsx +5 -5
  251. package/blocks/intent/components/intent-feed.tsx +13 -10
  252. package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
  253. package/blocks/intent/components/intent-screen.tsx +265 -0
  254. package/blocks/intent/components/keyword-detail.tsx +124 -0
  255. package/blocks/intent/lib/intent-utils.ts +4 -4
  256. package/blocks/lifecycle/block.json +13 -8
  257. package/blocks/lifecycle/components/account-related.tsx +8 -1
  258. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  259. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  260. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  261. package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
  262. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  263. package/blocks/lists/README.md +20 -1
  264. package/blocks/lists/block.json +69 -3
  265. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  266. package/blocks/lists/components/add-to-list-button.tsx +20 -9
  267. package/blocks/lists/components/list-picker.tsx +17 -3
  268. package/blocks/lists/components/list-table.tsx +12 -6
  269. package/blocks/lists/components/lists-screen.tsx +346 -0
  270. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  271. package/blocks/lists/components/suppression-manager.tsx +33 -8
  272. package/blocks/lists/components/suppressions-screen.tsx +264 -0
  273. package/blocks/mailboxes/README.md +17 -0
  274. package/blocks/mailboxes/block.json +69 -17
  275. package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
  276. package/blocks/mailboxes/components/domain-detail.tsx +148 -0
  277. package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
  278. package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
  279. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  280. package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
  281. package/blocks/mailboxes/components/warmup-health.tsx +24 -22
  282. package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
  283. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  284. package/blocks/newsletter/README.md +12 -0
  285. package/blocks/newsletter/block.json +157 -24
  286. package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
  287. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  288. package/blocks/newsletter/components/more-rows.tsx +30 -0
  289. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  290. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  291. package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
  292. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  293. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  294. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  295. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  296. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  297. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  298. package/blocks/nurture/block.json +28 -14
  299. package/blocks/nurture/components/nurture-builder.tsx +13 -3
  300. package/blocks/nurture/components/nurture-enroll.tsx +12 -10
  301. package/blocks/nurture/components/nurture-form.tsx +27 -10
  302. package/blocks/nurture/components/nurture-screen.tsx +26 -11
  303. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  304. package/blocks/nurture/lib/nurture-types.ts +1 -1
  305. package/blocks/objects/README.md +18 -1
  306. package/blocks/objects/block.json +42 -21
  307. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  308. package/blocks/objects/components/object-not-found.tsx +3 -1
  309. package/blocks/objects/components/object-record-detail.tsx +10 -2
  310. package/blocks/objects/components/object-schema-editor.tsx +4 -2
  311. package/blocks/objects/components/object-table.tsx +13 -3
  312. package/blocks/objects/components/objects-screen.tsx +147 -0
  313. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  314. package/blocks/objects/lib/object-types.ts +1 -1
  315. package/blocks/pages/README.md +14 -0
  316. package/blocks/pages/block.json +140 -23
  317. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  318. package/blocks/pages/components/content-library.tsx +9 -4
  319. package/blocks/pages/components/more-rows.tsx +30 -0
  320. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  321. package/blocks/pages/components/page-analytics.tsx +176 -0
  322. package/blocks/pages/components/page-clone.tsx +99 -0
  323. package/blocks/pages/components/page-domains.tsx +167 -0
  324. package/blocks/pages/components/page-editor.tsx +1 -1
  325. package/blocks/pages/components/page-generation-status.tsx +48 -0
  326. package/blocks/pages/components/page-list.tsx +2 -1
  327. package/blocks/pages/components/page-tracking.tsx +137 -0
  328. package/blocks/pages/components/pages-screen.tsx +57 -16
  329. package/blocks/pages/components/seo-report.tsx +6 -1
  330. package/blocks/pages/lib/studio.ts +7 -0
  331. package/blocks/phone-numbers/README.md +74 -0
  332. package/blocks/phone-numbers/block.json +262 -0
  333. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  334. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  335. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  336. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  337. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  338. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  339. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  340. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  341. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  342. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  343. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  344. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  345. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  346. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  347. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  348. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  349. package/blocks/pickers/README.md +23 -0
  350. package/blocks/pickers/block.json +103 -0
  351. package/blocks/pickers/components/company-picker.tsx +90 -0
  352. package/blocks/pickers/components/contact-picker.tsx +131 -0
  353. package/blocks/pickers/components/member-picker.tsx +99 -0
  354. package/blocks/pickers/components/search-picker.tsx +98 -0
  355. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  356. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  357. package/blocks/pickers/lib/use-debounced.ts +13 -0
  358. package/blocks/quotes/README.md +13 -0
  359. package/blocks/quotes/block.json +65 -13
  360. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  361. package/blocks/quotes/components/product-picker.tsx +3 -1
  362. package/blocks/quotes/components/quote-builder.tsx +1 -1
  363. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  364. package/blocks/quotes/components/quote-list.tsx +5 -3
  365. package/blocks/quotes/components/quote-related.tsx +8 -4
  366. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  367. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  368. package/blocks/radar/README.md +10 -0
  369. package/blocks/radar/block.json +41 -1
  370. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  371. package/blocks/radar/components/competitor-detail.tsx +117 -0
  372. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  373. package/blocks/radar/components/radar-overview.tsx +4 -4
  374. package/blocks/radar/components/radar-report.tsx +4 -2
  375. package/blocks/radar/components/radar-screen.tsx +214 -0
  376. package/blocks/radar/components/report-detail.tsx +104 -0
  377. package/blocks/search/README.md +10 -0
  378. package/blocks/search/block.json +16 -1
  379. package/blocks/search/components/filter-builder.tsx +3 -3
  380. package/blocks/search/components/prospect-results.tsx +5 -3
  381. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  382. package/blocks/search/components/saved-searches.tsx +4 -1
  383. package/blocks/search/hooks/use-filter-options.ts +20 -6
  384. package/blocks/search/lib/search-types.ts +2 -0
  385. package/blocks/sequencer/README.md +55 -9
  386. package/blocks/sequencer/block.json +126 -19
  387. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  388. package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
  389. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  390. package/blocks/sequencer/components/sequence-builder.tsx +224 -60
  391. package/blocks/sequencer/components/sequence-detail.tsx +261 -0
  392. package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
  393. package/blocks/sequencer/components/sequence-list.tsx +34 -9
  394. package/blocks/sequencer/components/sequence-stats.tsx +11 -10
  395. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  396. package/blocks/sequencer/components/sequences-screen.tsx +162 -0
  397. package/blocks/sequencer/components/step-editor.tsx +14 -12
  398. package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
  399. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  400. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  401. package/blocks/social-ads/README.md +19 -0
  402. package/blocks/social-ads/block.json +233 -1
  403. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  404. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  405. package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
  406. package/blocks/social-ads/components/audience-sync.tsx +17 -9
  407. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  408. package/blocks/social-ads/components/campaign-detail.tsx +138 -0
  409. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  410. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  411. package/blocks/social-ads/components/creative-detail.tsx +155 -0
  412. package/blocks/social-ads/components/creative-gallery.tsx +35 -7
  413. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  414. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  415. package/blocks/social-ads/components/post-composer.tsx +7 -3
  416. package/blocks/social-ads/components/post-detail.tsx +163 -0
  417. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  418. package/blocks/social-ads/components/social-performance.tsx +80 -0
  419. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  420. package/blocks/social-ads/components/social-screen.tsx +187 -0
  421. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  422. package/blocks/surveys/README.md +91 -0
  423. package/blocks/surveys/block.json +299 -0
  424. package/blocks/surveys/components/account-nps.tsx +69 -0
  425. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  426. package/blocks/surveys/components/org-summary.tsx +82 -0
  427. package/blocks/surveys/components/sending-domains.tsx +113 -0
  428. package/blocks/surveys/components/survey-form.tsx +126 -0
  429. package/blocks/surveys/components/survey-responses.tsx +145 -0
  430. package/blocks/surveys/components/survey-results.tsx +72 -0
  431. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  432. package/blocks/surveys/components/survey-send.tsx +162 -0
  433. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  434. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  435. package/blocks/surveys/lib/surveys-types.ts +228 -0
  436. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  437. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  438. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  439. package/blocks/visitors/README.md +10 -0
  440. package/blocks/visitors/block.json +25 -1
  441. package/blocks/visitors/components/snippet-install.tsx +21 -10
  442. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  443. package/blocks/visitors/components/visitor-company-card.tsx +26 -16
  444. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  445. package/blocks/visitors/components/visitor-feed.tsx +16 -7
  446. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  447. package/blocks/webchat/README.md +10 -0
  448. package/blocks/webchat/block.json +21 -7
  449. package/blocks/webchat/components/chat-widget-config.tsx +8 -5
  450. package/blocks/webchat/components/conversation-list.tsx +8 -4
  451. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  452. package/blocks/workbench/README.md +11 -0
  453. package/blocks/workbench/block.json +48 -1
  454. package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
  455. package/blocks/workbench/components/run-progress.tsx +1 -1
  456. package/blocks/workbench/components/workbench-grid.tsx +10 -4
  457. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  458. package/blocks/workbench/components/workbench-screen.tsx +202 -0
  459. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  460. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  461. package/blocks/workflows/README.md +13 -0
  462. package/blocks/workflows/block.json +28 -9
  463. package/blocks/workflows/components/run-history.tsx +31 -9
  464. package/blocks/workflows/components/trigger-config.tsx +3 -1
  465. package/blocks/workflows/components/workflow-editor.tsx +4 -2
  466. package/blocks/workflows/components/workflow-list.tsx +13 -6
  467. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  468. package/dist/cli.js +3109 -1977
  469. package/dist/errors.d.mts +34 -0
  470. package/dist/errors.d.ts +34 -0
  471. package/dist/errors.js +46 -0
  472. package/dist/errors.js.map +1 -0
  473. package/dist/errors.mjs +21 -0
  474. package/dist/errors.mjs.map +1 -0
  475. package/dist/index.browser.js +11 -5
  476. package/dist/index.browser.js.map +1 -1
  477. package/dist/index.browser.mjs +11 -5
  478. package/dist/index.browser.mjs.map +1 -1
  479. package/dist/index.d.mts +164 -25
  480. package/dist/index.d.ts +164 -25
  481. package/dist/index.js +11 -5
  482. package/dist/index.js.map +1 -1
  483. package/dist/index.mjs +11 -5
  484. package/dist/index.mjs.map +1 -1
  485. package/dist/react.browser.js +10 -4
  486. package/dist/react.browser.js.map +1 -1
  487. package/dist/react.browser.mjs +10 -4
  488. package/dist/react.browser.mjs.map +1 -1
  489. package/dist/react.d.mts +142 -5
  490. package/dist/react.d.ts +142 -5
  491. package/dist/react.js +10 -4
  492. package/dist/react.js.map +1 -1
  493. package/dist/react.mjs +10 -4
  494. package/dist/react.mjs.map +1 -1
  495. package/dist/types/blocks/add.d.ts +11 -4
  496. package/dist/types/blocks/brand.d.ts +122 -0
  497. package/dist/types/blocks/compose.d.ts +53 -0
  498. package/dist/types/blocks/create.d.ts +27 -20
  499. package/dist/types/blocks/doctor.d.ts +6 -1
  500. package/dist/types/blocks/flows.d.ts +7 -1
  501. package/dist/types/blocks/navfile.d.ts +35 -0
  502. package/dist/types/blocks/orgBrand.d.ts +34 -0
  503. package/dist/types/blocks/paths.d.ts +4 -0
  504. package/dist/types/blocks/prefixes.d.ts +37 -0
  505. package/dist/types/blocks/registry.d.ts +17 -0
  506. package/dist/types/blocks/screens.d.ts +53 -0
  507. package/dist/types/cli.d.ts +5 -0
  508. package/dist/types/errors.d.ts +33 -0
  509. package/dist/types/http.d.ts +2 -18
  510. package/dist/types/version.d.ts +2 -2
  511. package/docs/agents.md +15 -4
  512. package/docs/api/README.md +2 -2
  513. package/docs/api/by-path.md +1 -0
  514. package/docs/api/desk.d.ts +98 -2
  515. package/docs/api/desk.md +123 -1
  516. package/docs/api/inbox.d.ts +10 -2
  517. package/docs/api/inbox.md +10 -2
  518. package/docs/api/seo.d.ts +19 -1
  519. package/docs/api/seo.md +24 -1
  520. package/package.json +16 -2
  521. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  522. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  523. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  524. package/stacks/6sense.json +55 -17
  525. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  526. package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
  527. package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
  528. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  529. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  530. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  531. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  532. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  533. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  534. package/stacks/apollo.json +147 -37
  535. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  536. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  537. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  538. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  539. package/stacks/calendly.json +64 -0
  540. package/stacks/clay/pages/companies.tsx.tmpl +31 -0
  541. package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
  542. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  543. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  544. package/stacks/clay.json +73 -27
  545. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  546. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  547. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  548. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  549. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  550. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  551. package/stacks/gong.json +98 -35
  552. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  553. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  554. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  555. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  556. package/stacks/hootsuite.json +104 -0
  557. package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
  558. package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
  559. package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
  560. package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
  561. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  562. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  563. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  564. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  565. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  566. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  567. package/stacks/hubspot.json +181 -64
  568. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  569. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  570. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  571. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  572. package/stacks/intercom.json +52 -20
  573. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  574. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  575. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  576. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  577. package/stacks/mailchimp.json +68 -34
  578. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  579. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  580. package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
  581. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  582. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  583. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  584. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  585. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  586. package/stacks/outreach.json +106 -25
  587. package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
  588. package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
  589. package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
  590. package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
  591. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  592. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  593. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  594. package/stacks/pipedrive.json +117 -0
  595. package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
  596. package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
  597. package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
  598. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  599. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  600. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  601. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  602. package/stacks/salesforce.json +85 -31
  603. package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
  604. package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
  605. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  606. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  607. package/stacks/zoominfo.json +115 -0
  608. package/styles/styles.css +1 -1
  609. package/templates/next-app/README.md.tmpl +40 -9
  610. package/templates/next-app/app/globals.css.tmpl +349 -64
  611. package/templates/next-app/app/layout.tsx.tmpl +7 -2
  612. package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
  613. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
  614. package/templates/next-app/dot-gitignore.tmpl +2 -0
  615. package/templates/next-app/package.json.tmpl +1 -1
@@ -0,0 +1,150 @@
1
+ "use client";
2
+
3
+ /**
4
+ * AssistantSessions: conversations with the graph8 assistant (the workspace agent that answers questions and starts jobs). The list of
5
+ * your sessions (pinned first, "Load more" pages through all of them), a session's messages and the steps the agent took, and the
6
+ * actions: new session, rename, pin, delete (destructive, confirmed), add a note (no model call; the agent reads it on its next turn)
7
+ * and "Ask" (one live turn: the agent answers, or reads the message as a job and starts a run).
8
+ * A live turn runs a model and may start autonomous work that spends credits, so Ask goes through ConfirmSpendDialog (billable).
9
+ * Live streaming is not available through the API; an answer arrives whole.
10
+ *
11
+ * SDK operations, in order:
12
+ * 1. listAgentSessions GET /agent/sessions?limit&offset (read)
13
+ * 2. createAgentSession POST /agent/sessions { title? } (write)
14
+ * 3. getAgentSessionState GET /agent/sessions/{session_id}/state (read) messages + steps
15
+ * 4. markAgentSessionRead PUT /agent/sessions/{session_id}/read { message_count } (write)
16
+ * 5. renameAgentSession POST /agent/sessions/{session_id}/rename { title } (write)
17
+ * 6. pinAgentSession POST /agent/sessions/{session_id}/pin { is_pinned } (write)
18
+ * 7. deleteAgentSession DELETE /agent/sessions/{session_id} (destructive, ConfirmSpendDialog)
19
+ * 8. postAgentSessionMessage POST /agent/sessions/{session_id}/messages { content } (write, no model call)
20
+ * 9. postAgentChat POST /agent/chat { message, session_id? } (billable, ConfirmSpendDialog)
21
+ */
22
+ import { useEffect, useRef, useState } from "react";
23
+ import { MessagesSquare, Pin } from "lucide-react";
24
+ import { Button, Input, Skeleton, Textarea } from "@graph8/sdk/ui";
25
+ import { ApiError } from "@/g8/components/api-error";
26
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
27
+ import { EmptyState } from "@/g8/components/empty-state";
28
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
29
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
30
+ import { g8Delete, g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
31
+ import { bodyOf, listOf } from "../lib/agent-types";
32
+ import { LoadMore, useLoadMoreList } from "../lib/load-more";
33
+
34
+ export interface AssistantSessionsProps {
35
+ /** Called when a live turn started a run, so the host can open it. */
36
+ onOpenRun?: (runId: string) => void;
37
+ className?: string;
38
+ }
39
+
40
+ interface Session { id?: string | null; title?: string | null; is_pinned?: boolean | null; updated_at?: string | null; created_at?: string | null }
41
+ interface Message { role?: string; content?: unknown }
42
+ interface Step { title?: string; summary?: string; tool?: string; status?: string }
43
+ const enc = encodeURIComponent;
44
+ const text = (v: unknown): string => (typeof v === "string" ? v : v == null ? "" : JSON.stringify(v));
45
+
46
+ function Transcript({ sessionId, refreshKey }: { sessionId: string; refreshKey: number }) {
47
+ const state = useG8Query((signal) => g8Get(`/agent/sessions/${enc(sessionId)}/state`, undefined, { signal }), [sessionId, refreshKey]);
48
+ const body = bodyOf<{ messages?: Message[]; steps?: Step[] }>(state.data);
49
+ const messages = body.messages ?? [];
50
+ const lastRead = useRef<string>("");
51
+ const markRead = useG8Mutation((n: number) => g8Put(`/agent/sessions/${enc(sessionId)}/read`, { message_count: n }));
52
+ useEffect(() => {
53
+ const key = `${sessionId}:${messages.length}`;
54
+ if (state.data && messages.length > 0 && lastRead.current !== key) { lastRead.current = key; void markRead.mutate(messages.length); }
55
+ }, [state.data, messages.length, sessionId]); // eslint-disable-line react-hooks/exhaustive-deps
56
+ if (state.error) return <ApiError error={state.error} title="Could not load the session" onRetry={() => void state.refetch()} />;
57
+ if (state.loading && !state.data) return <Skeleton className="h-24 w-full" aria-label="Loading session" />;
58
+ return (
59
+ <div className="flex flex-col gap-3">
60
+ {messages.length === 0 ? <p className="text-sm text-muted-foreground">No messages in this session yet.</p> : null}
61
+ <ol className="flex flex-col gap-2" aria-label="Messages">
62
+ {messages.map((m, i) => <li key={i} className={`max-w-[85%] rounded-md border px-3 py-2 text-sm ${m.role === "user" ? "self-end bg-accent" : "self-start"}`}><span className="block text-xs text-muted-foreground">{m.role === "user" ? "You" : "Assistant"}</span><span className="whitespace-pre-wrap">{text(m.content)}</span></li>)}
63
+ </ol>
64
+ {(body.steps ?? []).length ? <details className="text-sm"><summary className="cursor-pointer text-muted-foreground">What the agent did ({body.steps?.length})</summary><ul className="mt-2 list-disc pl-5">{(body.steps ?? []).map((s, i) => <li key={i}>{s.title ?? s.summary ?? s.tool}{s.status ? ` (${s.status})` : ""}</li>)}</ul></details> : null}
65
+ </div>
66
+ );
67
+ }
68
+
69
+ export function AssistantSessions({ onOpenRun, className }: AssistantSessionsProps) {
70
+ const [openId, setOpenId] = useState<string | null>(null);
71
+ const [draft, setDraft] = useState("");
72
+ const [asking, setAsking] = useState<string | null>(null);
73
+ const [renaming, setRenaming] = useState<string | null>(null);
74
+ const [title, setTitle] = useState("");
75
+ const [deleting, setDeleting] = useState<Session | null>(null);
76
+ const [reply, setReply] = useState<{ message?: string; kind?: string; work_item_id?: string } | null>(null);
77
+ const [turnKey, setTurn] = useState(0);
78
+ const list = useLoadMoreList<Session>(async (offset, limit, signal) => listOf<Session>(await g8Get("/agent/sessions", { limit, offset }, { signal }), "sessions", "items"), [], { pageSize: 50 });
79
+ const rows = [...list.items].sort((a, b) => Number(Boolean(b.is_pinned)) - Number(Boolean(a.is_pinned)));
80
+ const open = rows.find((s) => s.id === openId) ?? null;
81
+
82
+ const create = useG8Mutation(async () => bodyOf<Session>(await g8Post("/agent/sessions", {})).id ?? null, { onSuccess: (id) => { list.reload(); if (id) { setOpenId(id); setReply(null); } } });
83
+ const rename = useG8Mutation((v: { id: string; title: string }) => g8Post(`/agent/sessions/${enc(v.id)}/rename`, { title: v.title }), { onSuccess: () => { setRenaming(null); list.reload(); } });
84
+ const pin = useG8Mutation((v: { id: string; pinned: boolean }) => g8Post(`/agent/sessions/${enc(v.id)}/pin`, { is_pinned: v.pinned }), { onSuccess: () => list.reload() });
85
+ const remove = useG8Mutation((id: string) => g8Delete(`/agent/sessions/${enc(id)}`), { onSuccess: (_r, id) => { setDeleting(null); if (openId === id) setOpenId(null); list.reload(); }, onError: () => setDeleting(null) });
86
+ const note = useG8Mutation((v: { id: string; content: string }) => g8Post(`/agent/sessions/${enc(v.id)}/messages`, { content: v.content }), { onSuccess: () => { setDraft(""); setTurn((t) => t + 1); } });
87
+ // One id per turn, reused when the same turn is retried, so a retry after a timeout cannot be charged twice (the API dedupes on it within a session).
88
+ const turn = useRef<{ id: string; text: string } | null>(null);
89
+ const ask = useG8Mutation(async (message: string) => {
90
+ if (turn.current?.text !== message) turn.current = { id: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`, text: message };
91
+ return bodyOf<{ message?: string; kind?: string; work_item_id?: string; session_id?: string }>(await g8Post("/agent/chat", { message, ...(openId ? { session_id: openId, client_turn_id: turn.current.id } : {}) }));
92
+ },
93
+ { onSuccess: (r) => { turn.current = null; setAsking(null); setDraft(""); setReply(r); setTurn((t) => t + 1); list.reload(); if (!openId && (r as { session_id?: string }).session_id) setOpenId((r as { session_id?: string }).session_id ?? null); }, onError: () => setAsking(null) });
94
+
95
+ return (
96
+ <div className={`grid gap-4 lg:grid-cols-[18rem_1fr] ${className ?? ""}`}>
97
+ <div className="flex flex-col gap-2">
98
+ <div className="flex items-center justify-between"><h3 className="text-sm font-medium">Sessions</h3><Button size="sm" variant="outline" loading={create.loading} onClick={() => void create.mutate(undefined)}>New session</Button></div>
99
+ {list.error ? <ApiError error={list.error} title="Could not load sessions" onRetry={list.reload} /> : null}
100
+ {list.loading ? <Skeleton className="h-16 w-full" aria-label="Loading sessions" /> : null}
101
+ <ul className="divide-y rounded-md border">
102
+ {rows.map((s) => (
103
+ <li key={s.id} className={s.id === openId ? "bg-accent" : ""}>
104
+ <button type="button" data-testid={`session-${s.id}`} className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm" onClick={() => { setOpenId(s.id ?? null); setReply(null); setRenaming(null); }}>
105
+ {s.is_pinned ? <Pin className="size-3.5 shrink-0" aria-label="Pinned" /> : null}
106
+ <span className="min-w-0"><span className="block truncate font-medium">{s.title || "Untitled session"}</span><span className="block text-xs text-muted-foreground">{s.updated_at ? new Date(s.updated_at).toLocaleString() : ""}</span></span>
107
+ </button>
108
+ </li>
109
+ ))}
110
+ </ul>
111
+ {!list.loading && !list.error && rows.length === 0 ? <EmptyState compact icon={<MessagesSquare strokeWidth={1.5} />} title="No sessions yet" description="Ask the assistant something to start one." /> : null}
112
+ <LoadMore list={list} noun="sessions" />
113
+ {create.error ? <ApiError error={create.error} title="Could not start a session" /> : null}
114
+ </div>
115
+ <div className="flex min-w-0 flex-col gap-3">
116
+ {open?.id ? (
117
+ <>
118
+ <div className="flex flex-wrap items-center gap-2">
119
+ {renaming === open.id ? (
120
+ <><Input aria-label="Session title" className="max-w-xs" value={title} onChange={(e) => setTitle(e.target.value)} /><Button size="sm" loading={rename.loading} disabled={!title.trim()} onClick={() => void rename.mutate({ id: open.id as string, title: title.trim() })}>Save title</Button><Button size="sm" variant="ghost" onClick={() => setRenaming(null)}>Cancel</Button></>
121
+ ) : (
122
+ <><h3 className="text-sm font-medium">{open.title || "Untitled session"}</h3><Button size="sm" variant="outline" onClick={() => { setTitle(open.title ?? ""); setRenaming(open.id ?? null); }}>Rename</Button>
123
+ <Button size="sm" variant="outline" loading={pin.loading} onClick={() => void pin.mutate({ id: open.id as string, pinned: !open.is_pinned })}>{open.is_pinned ? "Unpin" : "Pin"}</Button><Button size="sm" variant="ghost" onClick={() => setDeleting(open)}>Delete</Button></>
124
+ )}
125
+ </div>
126
+ {rename.error ? <ApiError error={rename.error} title="Could not rename the session" /> : null}
127
+ {pin.error ? <ApiError error={pin.error} title="Could not pin the session" /> : null}
128
+ <Transcript sessionId={open.id} refreshKey={turnKey} />
129
+ </>
130
+ ) : <p className="text-sm text-muted-foreground">Pick a session, or ask below to start a new one.</p>}
131
+ {reply?.message ? <p role="status" className="rounded-md border bg-accent px-3 py-2 text-sm" data-testid="assistant-reply">{reply.message}</p> : null}
132
+ {reply?.kind === "work_item" && reply.work_item_id ? <p role="status" className="text-sm">The assistant started a run. {onOpenRun ? <button type="button" className="underline" onClick={() => onOpenRun(reply.work_item_id as string)}>Open the run</button> : <span className="text-muted-foreground">Follow it on the Runs tab ({reply.work_item_id}).</span>}</p> : null}
133
+ <div className="flex flex-col gap-2 border-t pt-3">
134
+ <Textarea aria-label="Message to the assistant" rows={3} value={draft} onChange={(e) => setDraft(e.target.value)} placeholder="Ask the assistant, or leave it a note" />
135
+ {ask.error ? <ApiError error={ask.error} title="The assistant could not answer" /> : null}
136
+ {note.error ? <ApiError error={note.error} title="Could not add the note" /> : null}
137
+ <div className="flex justify-end gap-2">
138
+ <Button variant="outline" loading={note.loading} disabled={!draft.trim() || !open?.id} onClick={() => open?.id && void note.mutate({ id: open.id, content: draft.trim() })}>Add a note</Button>
139
+ <Button disabled={!draft.trim()} onClick={() => setAsking(draft.trim())}>Ask</Button>
140
+ </div>
141
+ </div>
142
+ </div>
143
+ {remove.error ? <ApiError error={remove.error} title="Could not delete the session" /> : null}
144
+ <ConfirmSpendDialog open={asking != null} tier="billable" title="Ask the assistant" description="The assistant runs a model for this turn and charges for it. If it reads your message as a job, it starts a run that works on its own and spends credits as it goes."
145
+ confirmLabel="Ask" confirming={ask.loading} onConfirm={() => asking && void ask.mutate(asking)} onCancel={() => setAsking(null)} />
146
+ <ConfirmSpendDialog open={deleting != null} tier="destructive" title={`Delete "${deleting?.title || "this session"}"`} description="The conversation and its history are removed." affected={1}
147
+ confirmLabel="Delete" confirming={remove.loading} onConfirm={() => deleting?.id && void remove.mutate(deleting.id)} onCancel={() => setDeleting(null)} />
148
+ </div>
149
+ );
150
+ }
@@ -0,0 +1,42 @@
1
+ "use client";
2
+
3
+ /**
4
+ * DuplicateAgentButton: copy an agent under a new name. The copy keeps the persona, voice, knowledge and skills and is a separate agent
5
+ * from then on. The copy is made once: if the dialog is retried after a failure, the id of the copy already made is reused.
6
+ *
7
+ * SDK operations:
8
+ * cloneAgent POST /voice/agents/clone { source_agent_id, new_agent_name } (write)
9
+ */
10
+ import { useRef, useState } from "react";
11
+ import { Button } from "@graph8/sdk/ui";
12
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
13
+ import { g8Post } from "@/g8/lib/g8-client";
14
+ import { agentIdOf } from "../lib/agent-types";
15
+ import type { AgentSummary } from "../lib/agent-types";
16
+ import { NameDialog } from "./name-dialog";
17
+
18
+ export interface DuplicateAgentButtonProps {
19
+ agent: Pick<AgentSummary, "agent_id" | "agent_name">;
20
+ /** Called with the id of the new agent. */
21
+ onDuplicated?: (agentId: string) => void;
22
+ variant?: "outline" | "ghost";
23
+ }
24
+
25
+ export function DuplicateAgentButton({ agent, onDuplicated, variant = "outline" }: DuplicateAgentButtonProps) {
26
+ const [open, setOpen] = useState(false);
27
+ const [name, setName] = useState("");
28
+ const made = useRef<string | null>(null);
29
+ const clone = useG8Mutation(async (n: string) => {
30
+ if (made.current) return made.current;
31
+ const id = agentIdOf(await g8Post("/voice/agents/clone", { source_agent_id: agent.agent_id, new_agent_name: n }));
32
+ made.current = id || null;
33
+ return id;
34
+ }, { onSuccess: (id) => { made.current = null; setOpen(false); if (id) onDuplicated?.(id); } });
35
+ return (
36
+ <>
37
+ <Button size="sm" variant={variant} onClick={() => { made.current = null; clone.reset(); setName(`Copy of ${agent.agent_name}`); setOpen(true); }}>Duplicate</Button>
38
+ <NameDialog open={open} title={`Duplicate ${agent.agent_name}`} description="The copy keeps the persona, voice, knowledge and skills. It is a separate agent from now on." label="Name of the copy"
39
+ value={name} onChange={setName} confirm="Duplicate" busy={clone.loading} error={clone.error} onCancel={() => setOpen(false)} onConfirm={() => void clone.mutate(name.trim())} />
40
+ </>
41
+ );
42
+ }
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ /**
4
+ * FromTemplateButton: start a new agent from a graph8 or workspace template. Pick a template, name the agent, and it is created as a copy
5
+ * (adjust it afterwards). The copy is made once: a retry after a failure reuses the id of the copy already made.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. listAgentTemplates GET /voice/agent-templates?entity_type=agent&include_system_templates=true (read)
9
+ * 2. cloneAgent POST /voice/agents/clone { source_agent_id, new_agent_name } (write)
10
+ */
11
+ import { useRef, useState } from "react";
12
+ import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, Skeleton } from "@graph8/sdk/ui";
13
+ import { ApiError } from "@/g8/components/api-error";
14
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
15
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
16
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
17
+ import { agentIdOf, listOf } from "../lib/agent-types";
18
+
19
+ export interface FromTemplateButtonProps {
20
+ /** Called with the id of the new agent. */
21
+ onCreated?: (agentId: string) => void;
22
+ }
23
+
24
+ interface Template { agent_id: string; agent_name: string; role?: string; description?: string | null }
25
+
26
+ export function FromTemplateButton({ onCreated }: FromTemplateButtonProps) {
27
+ const [open, setOpen] = useState(false);
28
+ const [picked, setPicked] = useState<Template | null>(null);
29
+ const [name, setName] = useState("");
30
+ const made = useRef<string | null>(null);
31
+ const templates = useG8Query((signal) => g8Get("/voice/agent-templates", { entity_type: "agent", include_system_templates: true }, { signal }), [], { enabled: open });
32
+ const create = useG8Mutation(async (v: { source: string; name: string }) => {
33
+ if (made.current) return made.current;
34
+ const id = agentIdOf(await g8Post("/voice/agents/clone", { source_agent_id: v.source, new_agent_name: v.name }));
35
+ made.current = id || null;
36
+ return id;
37
+ }, { onSuccess: (id) => { made.current = null; setOpen(false); setPicked(null); if (id) onCreated?.(id); } });
38
+ const rows = listOf<Template>(templates.data, "templates", "items");
39
+ return (
40
+ <>
41
+ <Button variant="outline" onClick={() => { made.current = null; create.reset(); setOpen(true); }}>From a template</Button>
42
+ <Dialog open={open} onOpenChange={(o) => !o && !create.loading && setOpen(false)}>
43
+ <DialogContent>
44
+ <DialogHeader><DialogTitle>Start from a template</DialogTitle><DialogDescription>Pick a template, name your agent, and adjust it afterwards.</DialogDescription></DialogHeader>
45
+ {templates.error ? <ApiError error={templates.error} title="Could not load templates" onRetry={() => void templates.refetch()} /> : null}
46
+ {templates.loading && !templates.data ? <Skeleton className="h-16 w-full" /> : null}
47
+ <ul className="max-h-64 divide-y overflow-y-auto rounded-md border">
48
+ {rows.map((t) => (
49
+ <li key={t.agent_id}>
50
+ <button type="button" aria-pressed={picked?.agent_id === t.agent_id} className={`block w-full px-3 py-2 text-left text-sm ${picked?.agent_id === t.agent_id ? "bg-accent" : ""}`} onClick={() => { setPicked(t); setName(t.agent_name); }}>
51
+ <span className="font-medium">{t.agent_name}</span>{t.role ? <span className="ml-2 text-xs text-muted-foreground">{t.role}</span> : null}
52
+ {t.description ? <span className="block text-xs text-muted-foreground">{t.description}</span> : null}
53
+ </button>
54
+ </li>
55
+ ))}
56
+ </ul>
57
+ {!templates.loading && !templates.error && rows.length === 0 ? <p className="text-sm text-muted-foreground">No templates yet. Save an agent as a template first.</p> : null}
58
+ {picked ? <div className="flex flex-col gap-1"><Label htmlFor="template-agent-name">Name of the new agent</Label><Input id="template-agent-name" value={name} onChange={(e) => setName(e.target.value)} /></div> : null}
59
+ {create.error ? <ApiError error={create.error} title="Could not create the agent" /> : null}
60
+ <DialogFooter><Button variant="ghost" disabled={create.loading} onClick={() => setOpen(false)}>Cancel</Button><Button loading={create.loading} disabled={!picked || !name.trim()} onClick={() => picked && void create.mutate({ source: picked.agent_id, name: name.trim() })}>Create agent</Button></DialogFooter>
61
+ </DialogContent>
62
+ </Dialog>
63
+ </>
64
+ );
65
+ }
@@ -0,0 +1,107 @@
1
+ "use client";
2
+
3
+ /**
4
+ * KnowledgeBrowser: the company knowledge agents draw on. A status strip (entries, how many are draft, stable or deprecated, sync state),
5
+ * search (meaning-based, with an excerpt) or browse (filter by type and lifecycle), the list, and the open entry in a side panel
6
+ * (KnowledgeDetail, deep link ?concept=). "New entry" writes one (KnowledgeForm). "Export" downloads everything as a single archive.
7
+ * The list loads 25 at a time and "Load more" asks the API for more, up to its maximum of 100; at the maximum it says so.
8
+ *
9
+ * SDK operations, in order:
10
+ * 1. getKnowledgeStatus GET /knowledge/status (read)
11
+ * 2. listKnowledge GET /knowledge?concept_type&lifecycle&limit (read)
12
+ * 3. searchKnowledge GET /knowledge/search?query&limit=50 (read) when the search box has text
13
+ * 4. exportKnowledge GET /knowledge/export (read) a link to the archive (opened in a new tab)
14
+ * 5. KnowledgeDetail / KnowledgeForm see their files (get, history, sources, related, author, verify, deprecate)
15
+ */
16
+ import { useMemo, useState } from "react";
17
+ import { BookOpen, Plus } from "lucide-react";
18
+ import { Button, Input } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { EmptyState } from "@/g8/components/empty-state";
21
+ import { SplitView } from "@/g8/components/screen/split-view";
22
+ import { StatStrip } from "@/g8/components/screen/stat-strip";
23
+ import { StatusPill, toneForStatus } from "@/g8/components/screen/status-pill";
24
+ import { Toolbar } from "@/g8/components/screen/toolbar";
25
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
26
+ import { useUrlState } from "@/g8/hooks/use-url-state";
27
+ import { g8Get } from "@/g8/lib/g8-client";
28
+ import { bodyOf, listOf } from "../lib/agent-types";
29
+ import { LIFECYCLES } from "../lib/knowledge-utils";
30
+ import type { KnowledgeSummary } from "../lib/knowledge-utils";
31
+ import { LoadMore, useGrowingList } from "../lib/load-more";
32
+ import { KnowledgeDetail } from "./knowledge-detail";
33
+ import { KnowledgeForm } from "./knowledge-form";
34
+
35
+ export interface KnowledgeBrowserProps {
36
+ className?: string;
37
+ }
38
+
39
+ interface Hit { summary?: KnowledgeSummary; excerpt?: string }
40
+ interface Status { concept_count?: number; counts_by_lifecycle?: Record<string, number>; sync_state?: string; capabilities?: Record<string, boolean> }
41
+
42
+ // A constant: the form resets its draft when `mode` changes identity, so it must not be rebuilt on every render.
43
+ const NEW_MODE = { kind: "new" } as const;
44
+
45
+ export function KnowledgeBrowser({ className }: KnowledgeBrowserProps) {
46
+ const [open, setOpen] = useUrlState("concept", "");
47
+ const [view, setView] = useUrlState("view", "");
48
+ const [search, setSearch] = useState("");
49
+ const [type, setType] = useState("");
50
+ const [lifecycle, setLifecycle] = useState("");
51
+ const [creating, setCreating] = useState(false);
52
+ const q = search.trim();
53
+ const status = useG8Query((signal) => g8Get("/knowledge/status", undefined, { signal }), []);
54
+ const list = useGrowingList<KnowledgeSummary>(async (limit, signal) => listOf<KnowledgeSummary>(await g8Get("/knowledge", { concept_type: type.trim() || undefined, lifecycle: lifecycle || undefined, limit }, { signal }), "items", "concepts"), [type, lifecycle], { pageSize: 25, max: 100, enabled: !q });
55
+ const hits = useG8Query((signal) => g8Get("/knowledge/search", { query: q, limit: 50 }, { signal }), [q], { enabled: Boolean(q) });
56
+ const st = bodyOf<Status>(status.data);
57
+ const lc = st.counts_by_lifecycle ?? {};
58
+ const rows: Array<{ s: KnowledgeSummary; excerpt?: string }> = useMemo(() => q
59
+ ? listOf<Hit>(hits.data).filter((h) => h.summary).map((h) => ({ s: h.summary as KnowledgeSummary, excerpt: h.excerpt }))
60
+ : list.items.map((s) => ({ s })), [q, hits.data, list.items]);
61
+ const types = useMemo(() => [...new Set(list.items.map((s) => s.concept_type).filter(Boolean))], [list.items]);
62
+ const loading = q ? hits.loading && !hits.data : list.loading;
63
+ const error = q ? hits.error : list.error;
64
+ const expanded = view === "full" && Boolean(open);
65
+ const refresh = () => { void status.refetch(); list.reload(); };
66
+
67
+ const listing = (
68
+ <div className="flex flex-col gap-3">
69
+ <Toolbar search={{ value: search, onChange: setSearch, placeholder: "Search company knowledge" }}
70
+ filters={q ? undefined : <><Input aria-label="Filter by type" placeholder="Any type" list="kn-filter-types" className="h-8 w-36" value={type} onChange={(e) => setType(e.target.value)} />
71
+ <datalist id="kn-filter-types">{types.map((t) => <option key={t} value={t} />)}</datalist>
72
+ <select aria-label="Lifecycle" className="h-8 rounded-md border bg-background px-2 text-sm" value={lifecycle} onChange={(e) => setLifecycle(e.target.value)}><option value="">Any state</option>{LIFECYCLES.map((l) => <option key={l} value={l}>{l}</option>)}</select></>}
73
+ actions={<><Button size="sm" variant="outline" asChild><a href="/api/g8/knowledge/export" target="_blank" rel="noreferrer">Export</a></Button><Button size="sm" onClick={() => setCreating(true)}><Plus className="mr-1 size-4" />New entry</Button></>} />
74
+ {error ? <ApiError error={error} title="Could not load company knowledge" onRetry={() => (q ? void hits.refetch() : list.reload())} /> : null}
75
+ {loading ? <p className="text-sm text-muted-foreground" aria-busy="true">Loading knowledge...</p> : null}
76
+ <ul className="divide-y rounded-md border">
77
+ {rows.map(({ s, excerpt }) => (
78
+ <li key={s.concept_id} className={open === s.concept_id ? "bg-accent" : ""}>
79
+ <button type="button" data-testid={`concept-${s.concept_id}`} className="flex w-full items-start justify-between gap-3 px-3 py-2 text-left text-sm" onClick={() => setOpen(s.concept_id)}>
80
+ <span className="min-w-0"><span className="block truncate font-medium">{s.title}</span><span className="block truncate text-xs text-muted-foreground">{excerpt || s.logical_path || s.concept_type}</span></span>
81
+ <span className="flex shrink-0 items-center gap-1.5"><span className="text-xs text-muted-foreground">{s.concept_type}</span><StatusPill tone={toneForStatus(s.knowledge_status)} dot>{s.knowledge_status ?? "unknown"}</StatusPill></span>
82
+ </button>
83
+ </li>
84
+ ))}
85
+ </ul>
86
+ {!loading && !error && rows.length === 0 ? <EmptyState icon={<BookOpen strokeWidth={1.5} />} title={q ? "Nothing matches" : "No knowledge yet"} description={q ? "Try other words." : "Write down how your company sells, prices and supports, so agents can use it."} action={q ? undefined : <Button onClick={() => setCreating(true)}>New entry</Button>} /> : null}
87
+ {q && rows.length >= 50 ? <p className="text-center text-xs text-muted-foreground">Showing the 50 best matches. Narrow the search to see others.</p> : null}
88
+ {!q ? <LoadMore list={list} noun="entries" /> : null}
89
+ </div>
90
+ );
91
+
92
+ return (
93
+ <div className={`flex flex-col gap-4 ${className ?? ""}`}>
94
+ <StatStrip loading={status.loading && !status.data} items={status.error ? [] : [
95
+ { label: "Entries", value: String(st.concept_count ?? 0) },
96
+ { label: "Stable", value: String(lc.stable ?? 0), tone: "positive" },
97
+ { label: "Draft", value: String(lc.draft ?? 0), tone: "warning" },
98
+ { label: "Deprecated", value: String(lc.deprecated ?? 0) },
99
+ { label: "Sync", value: st.sync_state ?? "unknown" },
100
+ ]} />
101
+ {status.error ? <ApiError compact error={status.error} title="Could not load the knowledge status" onRetry={() => void status.refetch()} /> : null}
102
+ <SplitView list={listing} detailOpen={Boolean(open)} onCloseDetail={() => { setOpen(""); setView(""); }} expanded={expanded} onExpandedChange={(e) => setView(e ? "full" : "")} detailWidth="xl"
103
+ detail={open ? <KnowledgeDetail key={open} conceptId={open} onClose={() => { setOpen(""); setView(""); }} onExpand={expanded ? undefined : () => setView("full")} onCollapse={expanded ? () => setView("") : undefined} onOpenConcept={setOpen} onChanged={refresh} /> : null} />
104
+ <KnowledgeForm mode={creating ? NEW_MODE : null} typeSuggestions={types} onClose={() => setCreating(false)} onSaved={(id) => { refresh(); if (id) setOpen(id); }} />
105
+ </div>
106
+ );
107
+ }
@@ -0,0 +1,156 @@
1
+ "use client";
2
+
3
+ /**
4
+ * KnowledgeDetail: one company knowledge entry in full. Tabs: Content (the Markdown, warnings), History (every version and amendment, newest
5
+ * first, "Load more" follows the cursor), Sources (what it was written from) and Related (linked entries, which open in place).
6
+ * Actions for authored entries (generated ones come from your CRM and campaigns and are read only):
7
+ * Edit draft / Propose new version (KnowledgeForm), Verify (approve or reject, with the authority that signs off), Deprecate (a reason, optionally
8
+ * the entry that replaces it; destructive, confirmed).
9
+ *
10
+ * SDK operations, in order:
11
+ * 1. getKnowledge GET /knowledge/{concept_id} (read)
12
+ * 2. getKnowledgeHistory GET /knowledge/{concept_id}/history?cursor&limit (read)
13
+ * 3. knowledgeSources GET /knowledge/{concept_id}/sources (read)
14
+ * 4. relatedKnowledge GET /knowledge/{concept_id}/related?depth=1&limit=20 (read)
15
+ * 5. verifyAuthoredKnowledge POST /knowledge/authored/{concept_id}/verify { approve, authority } (write)
16
+ * 6. deprecateAuthoredKnowledge POST /knowledge/authored/{concept_id}/deprecate { reason, superseded_by? } (destructive, ConfirmSpendDialog)
17
+ * 7. KnowledgeForm: create/update draft/amend (see knowledge-form.tsx)
18
+ */
19
+ import { useEffect, useState } from "react";
20
+ import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, Skeleton } from "@graph8/sdk/ui";
21
+ import { ApiError } from "@/g8/components/api-error";
22
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
23
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
24
+ import { StatusPill, toneForStatus } from "@/g8/components/screen/status-pill";
25
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
26
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
27
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
28
+ import { bodyOf, listOf } from "../lib/agent-types";
29
+ import { bodyFromMarkdown, conceptOf, isAuthored } from "../lib/knowledge-utils";
30
+ import { KnowledgeForm } from "./knowledge-form";
31
+ import type { KnowledgeFormMode } from "./knowledge-form";
32
+
33
+ export interface KnowledgeDetailProps {
34
+ conceptId: string;
35
+ onClose?: () => void;
36
+ onExpand?: () => void;
37
+ onCollapse?: () => void;
38
+ /** Open another entry (from Related). */
39
+ onOpenConcept?: (conceptId: string) => void;
40
+ /** After any change, so the host can refresh its list. */
41
+ onChanged?: () => void;
42
+ className?: string;
43
+ }
44
+
45
+ type Row = Record<string, unknown>;
46
+ const str = (v: unknown): string => (typeof v === "string" ? v : typeof v === "number" ? String(v) : "");
47
+
48
+ function History({ conceptId }: { conceptId: string }) {
49
+ const [rows, setRows] = useState<Row[]>([]);
50
+ const [cursor, setCursor] = useState<string | undefined>();
51
+ const [next, setNext] = useState<string | undefined>();
52
+ const q = useG8Query((signal) => g8Get(`/knowledge/${encodeURIComponent(conceptId)}/history`, { limit: 25, cursor }, { signal }), [conceptId, cursor]);
53
+ useEffect(() => { setRows([]); setCursor(undefined); setNext(undefined); }, [conceptId]);
54
+ useEffect(() => {
55
+ if (!q.data) return;
56
+ const page = listOf<Row>(q.data, "items", "versions", "revisions", "history");
57
+ setRows((prev) => (cursor ? [...prev, ...page] : page));
58
+ const body = bodyOf<{ next_cursor?: string | null }>(q.data);
59
+ setNext(body.next_cursor ?? (q.data as { pagination?: { next_cursor?: string | null } }).pagination?.next_cursor ?? undefined);
60
+ }, [q.data]); // eslint-disable-line react-hooks/exhaustive-deps
61
+ if (q.error) return <ApiError error={q.error} title="Could not load the history" onRetry={() => void q.refetch()} />;
62
+ if (q.loading && rows.length === 0) return <Skeleton className="h-16 w-full" />;
63
+ if (rows.length === 0) return <p className="text-sm text-muted-foreground">No revisions recorded.</p>;
64
+ return (
65
+ <div className="flex flex-col gap-2">
66
+ <ul className="divide-y rounded-md border" aria-label="Revisions">
67
+ {rows.map((r, i) => (
68
+ <li key={i} className="flex flex-wrap items-center justify-between gap-2 px-3 py-2 text-sm">
69
+ <span>{str(r.summary ?? r.title ?? r.change ?? r.action ?? r.version ?? r.revision) || `Revision ${rows.length - i}`}<span className="block text-xs text-muted-foreground">{str(r.actor ?? r.author ?? r.actor_id)}</span></span>
70
+ <span className="flex items-center gap-2">{str(r.knowledge_status ?? r.status) ? <StatusPill tone={toneForStatus(str(r.knowledge_status ?? r.status))}>{str(r.knowledge_status ?? r.status)}</StatusPill> : null}<span className="text-xs text-muted-foreground">{str(r.created_at ?? r.at ?? r.recorded_at) ? new Date(str(r.created_at ?? r.at ?? r.recorded_at)).toLocaleString() : ""}</span></span>
71
+ </li>
72
+ ))}
73
+ </ul>
74
+ {next ? <Button size="sm" variant="outline" className="self-center" loading={q.loading} onClick={() => setCursor(next)}>Load more revisions</Button> : null}
75
+ </div>
76
+ );
77
+ }
78
+
79
+ export function KnowledgeDetail({ conceptId, onClose, onExpand, onCollapse, onOpenConcept, onChanged, className }: KnowledgeDetailProps) {
80
+ const [form, setForm] = useState<KnowledgeFormMode | null>(null);
81
+ const [verifying, setVerifying] = useState<{ approve: boolean; authority: string } | null>(null);
82
+ const [deprecating, setDeprecating] = useState<{ reason: string; superseded_by: string } | null>(null);
83
+ const [confirmDeprecate, setConfirmDeprecate] = useState(false);
84
+ useEffect(() => { setForm(null); setVerifying(null); setDeprecating(null); setConfirmDeprecate(false); }, [conceptId]);
85
+ const enc = encodeURIComponent(conceptId);
86
+ const concept = useG8Query((signal) => g8Get(`/knowledge/${enc}`, undefined, { signal }), [conceptId]);
87
+ const sources = useG8Query((signal) => g8Get(`/knowledge/${enc}/sources`, undefined, { signal }), [conceptId]);
88
+ const related = useG8Query((signal) => g8Get(`/knowledge/${enc}/related`, { depth: 1, limit: 20 }, { signal }), [conceptId]);
89
+ const c = conceptOf(concept.data);
90
+ const done = () => { void concept.refetch(); onChanged?.(); };
91
+ const verify = useG8Mutation((v: { approve: boolean; authority: string }) => g8Post(`/knowledge/authored/${enc}/verify`, v), { onSuccess: () => { setVerifying(null); done(); } });
92
+ const deprecate = useG8Mutation((v: { reason: string; superseded_by: string }) => g8Post(`/knowledge/authored/${enc}/deprecate`, { reason: v.reason, ...(v.superseded_by ? { superseded_by: v.superseded_by } : {}) }), { onSuccess: () => { setConfirmDeprecate(false); setDeprecating(null); done(); }, onError: () => setConfirmDeprecate(false) });
93
+
94
+ if (concept.error) return <ApiError error={concept.error} title="Could not load the entry" onRetry={() => void concept.refetch()} />;
95
+ if (!c) return <Skeleton className="h-40 w-full" aria-label="Loading entry" />;
96
+ const s = c.summary;
97
+ const authored = isAuthored(s);
98
+ const status = s.knowledge_status ?? "";
99
+ const loaded = { title: s.title, concept_type: s.concept_type, body: bodyFromMarkdown(c.markdown) };
100
+ const srcRows = listOf<{ resource?: string; title?: string | null; locator?: string | null }>(sources.data);
101
+ const relRows = listOf<{ concept_id?: string; title?: string; concept_type?: string; summary?: { concept_id?: string; title?: string } }>(related.data);
102
+
103
+ const actions = authored ? (
104
+ <>
105
+ {status === "draft" ? <Button size="sm" variant="outline" onClick={() => setForm({ kind: "edit", conceptId, loaded })}>Edit draft</Button> : null}
106
+ {status === "stable" ? <Button size="sm" variant="outline" onClick={() => setForm({ kind: "amend", conceptId, loaded })}>Propose new version</Button> : null}
107
+ {status !== "deprecated" ? <Button size="sm" variant="outline" onClick={() => setVerifying({ approve: true, authority: "" })}>Verify</Button> : null}
108
+ {status !== "deprecated" ? <Button size="sm" variant="ghost" onClick={() => setDeprecating({ reason: "", superseded_by: "" })}>Deprecate</Button> : null}
109
+ </>
110
+ ) : undefined;
111
+
112
+ return (
113
+ <>
114
+ <DetailPanel className={className} title={s.title} subtitle={s.logical_path ?? s.concept_type} onClose={onClose} onExpand={onExpand} onCollapse={onCollapse} actions={actions}
115
+ badges={<><StatusPill tone={toneForStatus(status)} dot>{status || "unknown"}</StatusPill>{s.trust_tier ? <StatusPill tone="neutral">{s.trust_tier.replace(/_/g, " ")}</StatusPill> : null}{s.stale ? <StatusPill tone="warning">stale</StatusPill> : null}{!authored ? <StatusPill tone="info" title="Built from your records; change the source, not the entry">generated</StatusPill> : null}</>}
116
+ tabs={[
117
+ { id: "content", label: "Content", content: (
118
+ <div className="flex flex-col gap-3">
119
+ {(c.warnings ?? []).map((w) => <p key={w} role="status" className="rounded-md border border-warning-foreground/25 bg-warning p-2 text-sm text-warning-foreground">{w}</p>)}
120
+ <pre className="whitespace-pre-wrap text-sm">{bodyFromMarkdown(c.markdown) || "This entry has no content."}</pre>
121
+ </div>) },
122
+ { id: "history", label: "History", content: <History conceptId={conceptId} /> },
123
+ { id: "sources", label: "Sources", count: srcRows.length, content: sources.error ? <ApiError error={sources.error} title="Could not load the sources" onRetry={() => void sources.refetch()} /> : srcRows.length === 0 ? <p className="text-sm text-muted-foreground">No sources recorded.</p> : (
124
+ <ul className="divide-y rounded-md border">{srcRows.map((r, i) => <li key={i} className="px-3 py-2 text-sm">{r.title || r.resource}{r.locator ? <span className="block text-xs text-muted-foreground">{r.locator}</span> : null}</li>)}</ul>) },
125
+ { id: "related", label: "Related", count: relRows.length, content: related.error ? <ApiError error={related.error} title="Could not load related entries" onRetry={() => void related.refetch()} /> : relRows.length === 0 ? <p className="text-sm text-muted-foreground">Nothing linked yet.</p> : (
126
+ <ul className="divide-y rounded-md border">{relRows.map((r, i) => { const id = r.concept_id ?? r.summary?.concept_id ?? ""; const title = r.title ?? r.summary?.title ?? id; return <li key={id || i}><button type="button" className="w-full px-3 py-2 text-left text-sm hover:bg-accent" disabled={!id || !onOpenConcept} onClick={() => onOpenConcept?.(id)}>{title}{r.concept_type ? <span className="ml-2 text-xs text-muted-foreground">{r.concept_type}</span> : null}</button></li>; })}</ul>) },
127
+ ]}>
128
+ {null}
129
+ </DetailPanel>
130
+ <KnowledgeForm mode={form} onClose={() => setForm(null)} onSaved={() => done()} />
131
+ <Dialog open={verifying != null} onOpenChange={(o) => !o && setVerifying(null)}>
132
+ <DialogContent>
133
+ <DialogHeader><DialogTitle>Verify this entry</DialogTitle><DialogDescription>Say who signs it off. Approved entries are trusted more by agents.</DialogDescription></DialogHeader>
134
+ <div className="flex flex-col gap-1"><Label htmlFor="kn-authority">Authority</Label><Input id="kn-authority" placeholder="Head of Sales" value={verifying?.authority ?? ""} onChange={(e) => setVerifying((v) => v && { ...v, authority: e.target.value })} /></div>
135
+ {verify.error ? <ApiError error={verify.error} title="Could not record the decision" /> : null}
136
+ <DialogFooter>
137
+ <Button variant="ghost" onClick={() => setVerifying(null)}>Cancel</Button>
138
+ <Button variant="outline" loading={verify.loading} disabled={!verifying?.authority.trim()} onClick={() => verifying && void verify.mutate({ approve: false, authority: verifying.authority.trim() })}>Reject</Button>
139
+ <Button loading={verify.loading} disabled={!verifying?.authority.trim()} onClick={() => verifying && void verify.mutate({ approve: true, authority: verifying.authority.trim() })}>Approve</Button>
140
+ </DialogFooter>
141
+ </DialogContent>
142
+ </Dialog>
143
+ <Dialog open={deprecating != null && !confirmDeprecate} onOpenChange={(o) => !o && setDeprecating(null)}>
144
+ <DialogContent>
145
+ <DialogHeader><DialogTitle>Deprecate this entry</DialogTitle><DialogDescription>Agents stop relying on it. The history is kept.</DialogDescription></DialogHeader>
146
+ <div className="flex flex-col gap-1"><Label htmlFor="kn-reason">Reason</Label><Input id="kn-reason" value={deprecating?.reason ?? ""} onChange={(e) => setDeprecating((v) => v && { ...v, reason: e.target.value })} /></div>
147
+ <div className="flex flex-col gap-1"><Label htmlFor="kn-superseded">Replaced by (entry id, optional)</Label><Input id="kn-superseded" value={deprecating?.superseded_by ?? ""} onChange={(e) => setDeprecating((v) => v && { ...v, superseded_by: e.target.value })} /></div>
148
+ {deprecate.error ? <ApiError error={deprecate.error} title="Could not deprecate the entry" /> : null}
149
+ <DialogFooter><Button variant="ghost" onClick={() => setDeprecating(null)}>Cancel</Button><Button disabled={!deprecating?.reason.trim()} onClick={() => setConfirmDeprecate(true)}>Continue</Button></DialogFooter>
150
+ </DialogContent>
151
+ </Dialog>
152
+ <ConfirmSpendDialog open={confirmDeprecate} tier="destructive" title={`Deprecate "${s.title}"`} description="Agents stop relying on this entry. It stays in the history, but cannot be made current again." affected={1}
153
+ confirmLabel="Deprecate" confirming={deprecate.loading} onConfirm={() => deprecating && void deprecate.mutate(deprecating)} onCancel={() => setConfirmDeprecate(false)} />
154
+ </>
155
+ );
156
+ }
@@ -0,0 +1,69 @@
1
+ "use client";
2
+
3
+ /**
4
+ * KnowledgeForm: write a company knowledge entry in a side sheet.
5
+ * mode "new" a new draft entry (createAuthoredKnowledge).
6
+ * mode "edit" change a DRAFT entry in place (updateAuthoredKnowledgeDraft).
7
+ * mode "amend" propose a new version of a published entry (createAuthoredKnowledgeAmendment); it is reviewed before it replaces the old one.
8
+ * The API takes the whole entry, so edit and amend start from the LOADED entry and stay disabled until it has loaded; Save is only
9
+ * enabled when something changed. Nothing here spends credits or reaches outside the workspace.
10
+ *
11
+ * SDK operations:
12
+ * createAuthoredKnowledge POST /knowledge/authored { title, concept_type, body } (write)
13
+ * updateAuthoredKnowledgeDraft PATCH /knowledge/authored/{concept_id} { title, concept_type, body } (write)
14
+ * createAuthoredKnowledgeAmendment POST /knowledge/authored/{concept_id}/amendments { title, concept_type, body } (write)
15
+ */
16
+ import { useEffect, useState } from "react";
17
+ import { Button, Input, Label, Textarea } from "@graph8/sdk/ui";
18
+ import { ApiError } from "@/g8/components/api-error";
19
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
20
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
21
+ import { g8Patch, g8Post } from "@/g8/lib/g8-client";
22
+ import { bodyOf } from "../lib/agent-types";
23
+ import type { KnowledgeInput } from "../lib/knowledge-utils";
24
+
25
+ export type KnowledgeFormMode = { kind: "new" } | { kind: "edit"; conceptId: string; loaded: KnowledgeInput } | { kind: "amend"; conceptId: string; loaded: KnowledgeInput };
26
+
27
+ export interface KnowledgeFormProps {
28
+ mode: KnowledgeFormMode | null;
29
+ /** Types already in use, offered as suggestions. */
30
+ typeSuggestions?: string[];
31
+ onClose: () => void;
32
+ onSaved?: (conceptId: string | undefined) => void;
33
+ }
34
+
35
+ const BLANK: KnowledgeInput = { title: "", concept_type: "", body: "" };
36
+ const COPY = { new: ["New knowledge entry", "Create entry"], edit: ["Edit draft", "Save draft"], amend: ["Propose a new version", "Propose version"] } as const;
37
+
38
+ export function KnowledgeForm({ mode, typeSuggestions = [], onClose, onSaved }: KnowledgeFormProps) {
39
+ const [d, setD] = useState<KnowledgeInput>(BLANK);
40
+ const seed = mode && mode.kind !== "new" ? mode.loaded : BLANK;
41
+ useEffect(() => { setD(mode && mode.kind !== "new" ? mode.loaded : BLANK); }, [mode]);
42
+ const save = useG8Mutation(async () => {
43
+ if (!mode) return undefined;
44
+ const res = mode.kind === "new" ? await g8Post("/knowledge/authored", d)
45
+ : mode.kind === "edit" ? await g8Patch(`/knowledge/authored/${encodeURIComponent(mode.conceptId)}`, d)
46
+ : await g8Post(`/knowledge/authored/${encodeURIComponent(mode.conceptId)}/amendments`, d);
47
+ return bodyOf<{ concept_id?: string }>(res).concept_id;
48
+ }, { onSuccess: (id) => { onSaved?.(id); onClose(); } });
49
+ const set = (patch: Partial<KnowledgeInput>) => setD((x) => ({ ...x, ...patch }));
50
+ const changed = d.title !== seed.title || d.concept_type !== seed.concept_type || d.body !== seed.body;
51
+ const valid = d.title.trim() && d.concept_type.trim() && d.body.trim();
52
+ const [title, cta] = COPY[mode?.kind ?? "new"];
53
+ return (
54
+ <FormSheet open={mode != null} onOpenChange={(o) => !o && onClose()} title={title} width="xl"
55
+ description={mode?.kind === "amend" ? "The new version is reviewed before it replaces the published one." : "Written in Markdown. Agents can draw on it once it is published."}
56
+ footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button loading={save.loading} disabled={!valid || !changed} onClick={() => void save.mutate(undefined)}>{cta}</Button></>}>
57
+ <div className="flex flex-col gap-3">
58
+ <div className="flex flex-col gap-1"><Label htmlFor="kn-title">Title</Label><Input id="kn-title" value={d.title} onChange={(e) => set({ title: e.target.value })} /></div>
59
+ <div className="flex flex-col gap-1">
60
+ <Label htmlFor="kn-type">Type</Label>
61
+ <Input id="kn-type" list="kn-types" placeholder="playbook, policy, faq" value={d.concept_type} onChange={(e) => set({ concept_type: e.target.value })} />
62
+ <datalist id="kn-types">{typeSuggestions.map((t) => <option key={t} value={t} />)}</datalist>
63
+ </div>
64
+ <div className="flex flex-col gap-1"><Label htmlFor="kn-body">Content (Markdown)</Label><Textarea id="kn-body" rows={14} className="font-mono text-xs" value={d.body} onChange={(e) => set({ body: e.target.value })} /></div>
65
+ {save.error ? <ApiError error={save.error} title="Could not save the entry" /> : null}
66
+ </div>
67
+ </FormSheet>
68
+ );
69
+ }