@graph8/sdk 0.370.0 → 0.374.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 (646) hide show
  1. package/README.md +22 -3
  2. package/blocks/_foundation/README.md +61 -0
  3. package/blocks/_foundation/block.json +57 -0
  4. package/blocks/_foundation/components/auth-gate.tsx +363 -0
  5. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  6. package/blocks/_foundation/components/empty-state.tsx +5 -2
  7. package/blocks/_foundation/components/list-select.tsx +112 -0
  8. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  9. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  10. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  11. package/blocks/_foundation/components/screen/index.ts +1 -0
  12. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  13. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  14. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  15. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  16. package/blocks/_foundation/lib/paging.ts +57 -0
  17. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  18. package/blocks/_foundation/server/auth.ts +815 -0
  19. package/blocks/agents/README.md +81 -4
  20. package/blocks/agents/block.json +311 -2
  21. package/blocks/agents/components/agent-activity.tsx +112 -0
  22. package/blocks/agents/components/agent-approvals.tsx +135 -0
  23. package/blocks/agents/components/agent-builder.tsx +17 -3
  24. package/blocks/agents/components/agent-conversations.tsx +143 -0
  25. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
  26. package/blocks/agents/components/agent-operators.tsx +164 -0
  27. package/blocks/agents/components/agent-run-detail.tsx +20 -3
  28. package/blocks/agents/components/agent-runs.tsx +3 -3
  29. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  30. package/blocks/agents/components/agent-test-history.tsx +121 -0
  31. package/blocks/agents/components/agents-screen.tsx +53 -11
  32. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  33. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  34. package/blocks/agents/components/from-template-button.tsx +65 -0
  35. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  36. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  37. package/blocks/agents/components/knowledge-form.tsx +69 -0
  38. package/blocks/agents/components/name-dialog.tsx +32 -0
  39. package/blocks/agents/components/receptionist-button.tsx +46 -0
  40. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  41. package/blocks/agents/components/skill-editor.tsx +147 -0
  42. package/blocks/agents/components/skill-library.tsx +111 -0
  43. package/blocks/agents/lib/agent-types.ts +7 -1
  44. package/blocks/agents/lib/facts.tsx +13 -0
  45. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  46. package/blocks/agents/lib/load-more.tsx +141 -0
  47. package/blocks/agents/lib/skill-utils.ts +91 -0
  48. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +16 -3
  50. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  51. package/blocks/analytics/components/report-builder.tsx +15 -2
  52. package/blocks/appointments/README.md +6 -0
  53. package/blocks/appointments/block.json +27 -0
  54. package/blocks/appointments/components/booking-page.tsx +2 -2
  55. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  56. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  57. package/blocks/appointments/components/meeting-list.tsx +29 -13
  58. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  59. package/blocks/campaigns/README.md +1 -1
  60. package/blocks/campaigns/block.json +34 -1
  61. package/blocks/campaigns/components/campaign-assets.tsx +1 -1
  62. package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
  63. package/blocks/campaigns/components/campaign-links.tsx +180 -0
  64. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  65. package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
  66. package/blocks/campaigns/components/icp-detail.tsx +11 -2
  67. package/blocks/campaigns/components/icp-editor.tsx +8 -3
  68. package/blocks/campaigns/components/persona-detail.tsx +11 -2
  69. package/blocks/campaigns/components/persona-editor.tsx +6 -1
  70. package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
  71. package/blocks/coach/components/call-scorecard.tsx +7 -7
  72. package/blocks/company-intelligence/README.md +132 -0
  73. package/blocks/company-intelligence/block.json +318 -0
  74. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  75. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  76. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  77. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  78. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  79. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  80. package/blocks/company-intelligence/components/research-run.tsx +165 -0
  81. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  82. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  83. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  84. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  85. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  86. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  87. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  88. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  89. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  90. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  91. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  92. package/blocks/copilot/components/copilot-panel.tsx +1 -1
  93. package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
  94. package/blocks/crm/README.md +25 -0
  95. package/blocks/crm/block.json +257 -9
  96. package/blocks/crm/components/bulk-update.tsx +119 -0
  97. package/blocks/crm/components/companies-screen.tsx +23 -9
  98. package/blocks/crm/components/company-detail.tsx +2 -2
  99. package/blocks/crm/components/contact-detail.tsx +4 -4
  100. package/blocks/crm/components/contact-documents-tab.tsx +144 -0
  101. package/blocks/crm/components/contact-insights-tab.tsx +118 -0
  102. package/blocks/crm/components/contact-lookup-link.tsx +85 -0
  103. package/blocks/crm/components/contact-row-actions.tsx +43 -0
  104. package/blocks/crm/components/contact-tag-chips.tsx +113 -0
  105. package/blocks/crm/components/contacts-screen.tsx +77 -18
  106. package/blocks/crm/components/dedupe-review.tsx +6 -1
  107. package/blocks/crm/components/import-wizard.tsx +3 -7
  108. package/blocks/crm/components/list-select.tsx +20 -0
  109. package/blocks/crm/components/owner-assign.tsx +132 -0
  110. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  111. package/blocks/crm/components/record-form.tsx +1 -1
  112. package/blocks/crm/components/record-open-links.tsx +79 -0
  113. package/blocks/crm/components/tag-assign.tsx +112 -0
  114. package/blocks/crm/components/tag-manager.tsx +178 -0
  115. package/blocks/crm/components/tasks-screen.tsx +35 -15
  116. package/blocks/crm/components/unlock-info-button.tsx +82 -0
  117. package/blocks/crm/components/verify-email-button.tsx +93 -0
  118. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  119. package/blocks/crm/hooks/use-contact-tags.ts +29 -0
  120. package/blocks/crm/hooks/use-page-query.ts +3 -1
  121. package/blocks/crm/lib/crm-types.ts +14 -0
  122. package/blocks/crm/lib/read-lists.ts +16 -0
  123. package/blocks/crm/lib/record-actions.ts +102 -0
  124. package/blocks/deals/README.md +25 -6
  125. package/blocks/deals/block.json +168 -2
  126. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  127. package/blocks/deals/components/create-deal-button.tsx +109 -0
  128. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  129. package/blocks/deals/components/deal-detail.tsx +7 -5
  130. package/blocks/deals/components/deal-filters.tsx +65 -0
  131. package/blocks/deals/components/deal-line-items.tsx +120 -0
  132. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  133. package/blocks/deals/components/deal-related.tsx +30 -9
  134. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  135. package/blocks/deals/components/deals-screen.tsx +57 -22
  136. package/blocks/deals/components/forecast-view.tsx +4 -4
  137. package/blocks/deals/components/pipeline-board.tsx +8 -6
  138. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  139. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  140. package/blocks/deals/components/stage-editor.tsx +1 -1
  141. package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
  142. package/blocks/deals/lib/deal-extras.ts +156 -0
  143. package/blocks/dialer/README.md +14 -5
  144. package/blocks/dialer/block.json +17 -2
  145. package/blocks/dialer/components/call-contact-button.tsx +26 -9
  146. package/blocks/dialer/components/call-log-screen.tsx +10 -2
  147. package/blocks/dialer/components/call-log.tsx +16 -4
  148. package/blocks/dialer/components/call-queue.tsx +3 -3
  149. package/blocks/dialer/components/dialer-screen.tsx +69 -27
  150. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  151. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  152. package/blocks/dialer/components/session-start-form.tsx +58 -11
  153. package/blocks/dialer/components/softphone.tsx +1 -1
  154. package/blocks/documents/README.md +86 -0
  155. package/blocks/documents/block.json +378 -0
  156. package/blocks/documents/components/account-bar.tsx +48 -0
  157. package/blocks/documents/components/correspondence-form.tsx +95 -0
  158. package/blocks/documents/components/correspondence-import.tsx +106 -0
  159. package/blocks/documents/components/correspondence-panel.tsx +191 -0
  160. package/blocks/documents/components/documents-screen.tsx +76 -0
  161. package/blocks/documents/components/library-document-detail.tsx +175 -0
  162. package/blocks/documents/components/library-document-form.tsx +105 -0
  163. package/blocks/documents/components/library-folder-form.tsx +74 -0
  164. package/blocks/documents/components/library-panel.tsx +235 -0
  165. package/blocks/documents/components/library-upload.tsx +160 -0
  166. package/blocks/documents/components/question-row.tsx +130 -0
  167. package/blocks/documents/components/questionnaire-detail.tsx +197 -0
  168. package/blocks/documents/components/questionnaires-panel.tsx +173 -0
  169. package/blocks/documents/components/record-documents-tab.tsx +109 -0
  170. package/blocks/documents/lib/documents-types.ts +252 -0
  171. package/blocks/documents/lib/download.ts +64 -0
  172. package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
  173. package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
  174. package/blocks/documents/recipes/organize-the-library.ts +25 -0
  175. package/blocks/email-signatures/README.md +123 -0
  176. package/blocks/email-signatures/block.json +341 -0
  177. package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
  178. package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
  179. package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
  180. package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
  181. package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
  182. package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
  183. package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
  184. package/blocks/email-signatures/components/signature-policy.tsx +130 -0
  185. package/blocks/email-signatures/components/signature-preview.tsx +119 -0
  186. package/blocks/email-signatures/components/signature-report.tsx +77 -0
  187. package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
  188. package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
  189. package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
  190. package/blocks/email-signatures/lib/signatures-types.ts +392 -0
  191. package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
  192. package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
  193. package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
  194. package/blocks/enrichment/README.md +38 -10
  195. package/blocks/enrichment/block.json +164 -12
  196. package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
  197. package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
  198. package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
  199. package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
  200. package/blocks/enrichment/components/enrich-button.tsx +1 -1
  201. package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
  202. package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
  203. package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
  204. package/blocks/enrichment/components/list-pipelines.tsx +97 -0
  205. package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
  206. package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
  207. package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
  208. package/blocks/enrichment/components/waterfall-config.tsx +27 -7
  209. package/blocks/enrichment/lib/enrichment-types.ts +51 -0
  210. package/blocks/enrichment/lib/pipeline-types.ts +234 -0
  211. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  212. package/blocks/forms/README.md +10 -0
  213. package/blocks/forms/block.json +107 -0
  214. package/blocks/forms/components/form-activity.tsx +71 -0
  215. package/blocks/forms/components/form-backfill.tsx +108 -0
  216. package/blocks/forms/components/form-builder.tsx +1 -1
  217. package/blocks/forms/components/form-embed.tsx +1 -1
  218. package/blocks/forms/components/form-groups.tsx +151 -0
  219. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  220. package/blocks/forms/components/form-route-history.tsx +72 -0
  221. package/blocks/forms/components/form-saved-views.tsx +81 -0
  222. package/blocks/forms/components/forms-screen.tsx +57 -20
  223. package/blocks/forms/components/more-rows.tsx +30 -0
  224. package/blocks/forms/components/submissions-export.tsx +67 -0
  225. package/blocks/forms/components/submissions-table.tsx +15 -6
  226. package/blocks/forms/lib/forms-extra.ts +67 -0
  227. package/blocks/inbox/README.md +45 -4
  228. package/blocks/inbox/block.json +145 -8
  229. package/blocks/inbox/components/blocked-senders.tsx +103 -0
  230. package/blocks/inbox/components/compose-message.tsx +193 -0
  231. package/blocks/inbox/components/inbox-screen.tsx +54 -6
  232. package/blocks/inbox/components/inbox-search.tsx +106 -0
  233. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  234. package/blocks/inbox/components/tag-manager.tsx +136 -0
  235. package/blocks/inbox/components/thread-actions.tsx +133 -0
  236. package/blocks/inbox/components/thread-badge.tsx +2 -2
  237. package/blocks/inbox/components/thread-view.tsx +3 -1
  238. package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
  239. package/blocks/inbox/lib/inbox-manage.ts +227 -0
  240. package/blocks/integrations/README.md +54 -1
  241. package/blocks/integrations/block.json +189 -15
  242. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  243. package/blocks/integrations/components/connection-health.tsx +59 -0
  244. package/blocks/integrations/components/crm-detail.tsx +20 -1
  245. package/blocks/integrations/components/crm-records.tsx +103 -0
  246. package/blocks/integrations/components/field-mappings.tsx +61 -0
  247. package/blocks/integrations/components/integration-list.tsx +4 -4
  248. package/blocks/integrations/components/integrations-screen.tsx +20 -3
  249. package/blocks/integrations/components/migration-panel.tsx +133 -0
  250. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  251. package/blocks/integrations/components/sync-status.tsx +6 -6
  252. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  253. package/blocks/integrations/components/webhook-detail.tsx +26 -7
  254. package/blocks/integrations/components/webhook-manager.tsx +30 -17
  255. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  256. package/blocks/integrations/lib/crm-utils.ts +39 -0
  257. package/blocks/integrations/lib/facts.tsx +13 -0
  258. package/blocks/integrations/lib/integrations-utils.ts +5 -0
  259. package/blocks/integrations/lib/load-more.tsx +141 -0
  260. package/blocks/intent/components/account-intent-score.tsx +5 -5
  261. package/blocks/intent/components/intent-feed.tsx +8 -8
  262. package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
  263. package/blocks/intent/lib/intent-utils.ts +4 -4
  264. package/blocks/lifecycle/components/account-related.tsx +8 -1
  265. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  266. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  267. package/blocks/lists/README.md +7 -1
  268. package/blocks/lists/block.json +31 -3
  269. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  270. package/blocks/lists/components/add-to-list-button.tsx +17 -7
  271. package/blocks/lists/components/list-picker.tsx +13 -3
  272. package/blocks/lists/components/list-table.tsx +1 -1
  273. package/blocks/lists/components/lists-screen.tsx +4 -2
  274. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  275. package/blocks/lists/components/suppression-manager.tsx +27 -6
  276. package/blocks/lists/components/suppressions-screen.tsx +21 -12
  277. package/blocks/mailboxes/block.json +4 -0
  278. package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
  279. package/blocks/mailboxes/components/domain-detail.tsx +10 -4
  280. package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
  281. package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
  282. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  283. package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
  284. package/blocks/mailboxes/components/warmup-health.tsx +20 -20
  285. package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
  286. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  287. package/blocks/newsletter/README.md +12 -0
  288. package/blocks/newsletter/block.json +117 -0
  289. package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
  290. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  291. package/blocks/newsletter/components/more-rows.tsx +30 -0
  292. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  293. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  294. package/blocks/newsletter/components/newsletter-screen.tsx +38 -13
  295. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  296. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  297. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  298. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  299. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  300. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  301. package/blocks/nurture/components/nurture-builder.tsx +10 -2
  302. package/blocks/nurture/components/nurture-enroll.tsx +6 -7
  303. package/blocks/nurture/components/nurture-form.tsx +5 -8
  304. package/blocks/nurture/components/nurture-screen.tsx +16 -5
  305. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  306. package/blocks/objects/README.md +1 -1
  307. package/blocks/objects/block.json +13 -5
  308. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  309. package/blocks/objects/components/object-record-detail.tsx +8 -1
  310. package/blocks/objects/components/object-schema-editor.tsx +1 -1
  311. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  312. package/blocks/pages/README.md +14 -0
  313. package/blocks/pages/block.json +123 -2
  314. package/blocks/pages/components/more-rows.tsx +30 -0
  315. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  316. package/blocks/pages/components/page-analytics.tsx +176 -0
  317. package/blocks/pages/components/page-clone.tsx +99 -0
  318. package/blocks/pages/components/page-domains.tsx +167 -0
  319. package/blocks/pages/components/page-editor.tsx +1 -1
  320. package/blocks/pages/components/page-generation-status.tsx +48 -0
  321. package/blocks/pages/components/page-tracking.tsx +137 -0
  322. package/blocks/pages/components/pages-screen.tsx +57 -16
  323. package/blocks/pages/components/seo-report.tsx +6 -1
  324. package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
  325. package/blocks/pages/lib/studio.ts +7 -0
  326. package/blocks/phone-numbers/README.md +74 -0
  327. package/blocks/phone-numbers/block.json +262 -0
  328. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  329. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  330. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  331. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  332. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  333. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  334. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  335. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  336. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  337. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  338. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  339. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  340. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  341. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  342. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  343. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  344. package/blocks/pickers/README.md +23 -0
  345. package/blocks/pickers/block.json +103 -0
  346. package/blocks/pickers/components/company-picker.tsx +90 -0
  347. package/blocks/pickers/components/contact-picker.tsx +131 -0
  348. package/blocks/pickers/components/member-picker.tsx +99 -0
  349. package/blocks/pickers/components/search-picker.tsx +98 -0
  350. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  351. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  352. package/blocks/pickers/lib/use-debounced.ts +13 -0
  353. package/blocks/programmatic-pages/README.md +82 -0
  354. package/blocks/programmatic-pages/block.json +292 -0
  355. package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
  356. package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
  357. package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
  358. package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
  359. package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
  360. package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
  361. package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
  362. package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
  363. package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
  364. package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
  365. package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
  366. package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
  367. package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
  368. package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
  369. package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
  370. package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
  371. package/blocks/quotes/README.md +38 -1
  372. package/blocks/quotes/block.json +170 -6
  373. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  374. package/blocks/quotes/components/manual-products.tsx +144 -0
  375. package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
  376. package/blocks/quotes/components/quote-builder.tsx +1 -1
  377. package/blocks/quotes/components/quote-documents.tsx +94 -0
  378. package/blocks/quotes/components/quote-evidence.tsx +95 -0
  379. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  380. package/blocks/quotes/components/quote-list.tsx +28 -8
  381. package/blocks/quotes/components/quote-related.tsx +3 -2
  382. package/blocks/quotes/components/quote-settings.tsx +184 -0
  383. package/blocks/quotes/components/quote-templates.tsx +189 -0
  384. package/blocks/quotes/components/quote-viewer.tsx +23 -5
  385. package/blocks/quotes/components/quotes-screen.tsx +15 -5
  386. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  387. package/blocks/quotes/lib/quote-admin.ts +264 -0
  388. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  389. package/blocks/radar/components/radar-overview.tsx +4 -4
  390. package/blocks/radar/components/radar-screen.tsx +2 -2
  391. package/blocks/sequencer/README.md +61 -0
  392. package/blocks/sequencer/block.json +200 -6
  393. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  394. package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
  395. package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
  396. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  397. package/blocks/sequencer/components/sequence-builder.tsx +32 -23
  398. package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
  399. package/blocks/sequencer/components/sequence-detail.tsx +10 -2
  400. package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
  401. package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
  402. package/blocks/sequencer/components/sequence-list.tsx +1 -1
  403. package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
  404. package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
  405. package/blocks/sequencer/components/sequence-stats.tsx +8 -8
  406. package/blocks/sequencer/components/sequence-templates.tsx +160 -0
  407. package/blocks/sequencer/components/sequences-screen.tsx +39 -6
  408. package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
  409. package/blocks/sequencer/components/step-editor.tsx +21 -12
  410. package/blocks/sequencer/components/step-templates.tsx +224 -0
  411. package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
  412. package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
  413. package/blocks/signals/README.md +39 -0
  414. package/blocks/signals/block.json +287 -0
  415. package/blocks/signals/components/auto-seed-panel.tsx +111 -0
  416. package/blocks/signals/components/listener-form.tsx +184 -0
  417. package/blocks/signals/components/matched-posts.tsx +195 -0
  418. package/blocks/signals/components/post-engagements.tsx +114 -0
  419. package/blocks/signals/components/record-social-tab.tsx +49 -0
  420. package/blocks/signals/components/social-listeners-screen.tsx +250 -0
  421. package/blocks/signals/components/social-settings.tsx +128 -0
  422. package/blocks/signals/components/source-lists.tsx +83 -0
  423. package/blocks/signals/components/top-engagers.tsx +68 -0
  424. package/blocks/signals/components/x-integration.tsx +129 -0
  425. package/blocks/signals/lib/signals-types.ts +228 -0
  426. package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
  427. package/blocks/signals/recipes/spend-against-caps.ts +14 -0
  428. package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
  429. package/blocks/social-ads/README.md +8 -0
  430. package/blocks/social-ads/block.json +137 -4
  431. package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
  432. package/blocks/social-ads/components/audience-sync.tsx +6 -2
  433. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  434. package/blocks/social-ads/components/campaign-detail.tsx +6 -1
  435. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  436. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  437. package/blocks/social-ads/components/creative-detail.tsx +6 -1
  438. package/blocks/social-ads/components/creative-gallery.tsx +20 -2
  439. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  440. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  441. package/blocks/social-ads/components/post-composer.tsx +4 -2
  442. package/blocks/social-ads/components/post-detail.tsx +3 -0
  443. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  444. package/blocks/social-ads/components/social-performance.tsx +80 -0
  445. package/blocks/social-ads/components/social-screen.tsx +33 -6
  446. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  447. package/blocks/surveys/README.md +91 -0
  448. package/blocks/surveys/block.json +299 -0
  449. package/blocks/surveys/components/account-nps.tsx +69 -0
  450. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  451. package/blocks/surveys/components/org-summary.tsx +82 -0
  452. package/blocks/surveys/components/sending-domains.tsx +113 -0
  453. package/blocks/surveys/components/survey-form.tsx +126 -0
  454. package/blocks/surveys/components/survey-responses.tsx +145 -0
  455. package/blocks/surveys/components/survey-results.tsx +72 -0
  456. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  457. package/blocks/surveys/components/survey-send.tsx +162 -0
  458. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  459. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  460. package/blocks/surveys/lib/surveys-types.ts +228 -0
  461. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  462. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  463. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  464. package/blocks/visitors/components/snippet-install.tsx +18 -9
  465. package/blocks/visitors/components/traffic-chart.tsx +7 -6
  466. package/blocks/visitors/components/visitor-company-card.tsx +24 -14
  467. package/blocks/visitors/components/visitor-feed.tsx +5 -5
  468. package/blocks/web-analytics/README.md +102 -0
  469. package/blocks/web-analytics/block.json +292 -0
  470. package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
  471. package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
  472. package/blocks/web-analytics/components/audience-tab.tsx +135 -0
  473. package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
  474. package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
  475. package/blocks/web-analytics/components/overview-tab.tsx +102 -0
  476. package/blocks/web-analytics/components/pages-tab.tsx +148 -0
  477. package/blocks/web-analytics/components/ranked-table.tsx +103 -0
  478. package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
  479. package/blocks/web-analytics/components/report-card.tsx +71 -0
  480. package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
  481. package/blocks/web-analytics/hooks/use-report.ts +48 -0
  482. package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
  483. package/blocks/web-analytics/lib/page-path.ts +14 -0
  484. package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
  485. package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
  486. package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
  487. package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
  488. package/blocks/webchat/components/chat-widget-config.tsx +1 -1
  489. package/blocks/webchat/components/conversation-list.tsx +1 -1
  490. package/blocks/workbench/README.md +54 -2
  491. package/blocks/workbench/block.json +234 -5
  492. package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
  493. package/blocks/workbench/components/run-progress.tsx +1 -1
  494. package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
  495. package/blocks/workbench/components/workbench-automation.tsx +36 -0
  496. package/blocks/workbench/components/workbench-connections.tsx +136 -0
  497. package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
  498. package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
  499. package/blocks/workbench/components/workbench-grid.tsx +71 -8
  500. package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
  501. package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
  502. package/blocks/workbench/components/workbench-schedule.tsx +191 -0
  503. package/blocks/workbench/components/workbench-screen.tsx +65 -22
  504. package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
  505. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  506. package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
  507. package/blocks/workbench/components/workbench-triggers.tsx +178 -0
  508. package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
  509. package/blocks/workbench/lib/api-status.ts +8 -0
  510. package/blocks/workbench/lib/automation-types.ts +257 -0
  511. package/blocks/workflows/components/workflow-editor.tsx +1 -1
  512. package/dist/cli.js +744 -394
  513. package/dist/index.browser.js +1 -1
  514. package/dist/index.browser.js.map +1 -1
  515. package/dist/index.browser.mjs +1 -1
  516. package/dist/index.browser.mjs.map +1 -1
  517. package/dist/index.d.mts +2 -2
  518. package/dist/index.d.ts +2 -2
  519. package/dist/index.js +1 -1
  520. package/dist/index.js.map +1 -1
  521. package/dist/index.mjs +1 -1
  522. package/dist/index.mjs.map +1 -1
  523. package/dist/react.browser.js +1 -1
  524. package/dist/react.browser.js.map +1 -1
  525. package/dist/react.browser.mjs +1 -1
  526. package/dist/react.browser.mjs.map +1 -1
  527. package/dist/react.d.mts +2 -2
  528. package/dist/react.d.ts +2 -2
  529. package/dist/react.js +1 -1
  530. package/dist/react.js.map +1 -1
  531. package/dist/react.mjs +1 -1
  532. package/dist/react.mjs.map +1 -1
  533. package/dist/types/blocks/auth.d.ts +45 -0
  534. package/dist/types/blocks/brand.d.ts +11 -2
  535. package/dist/types/blocks/create.d.ts +11 -4
  536. package/dist/types/blocks/doctor.d.ts +1 -0
  537. package/dist/types/blocks/flows.d.ts +7 -1
  538. package/dist/types/blocks/orgBrand.d.ts +34 -0
  539. package/dist/types/cli.d.ts +3 -0
  540. package/dist/types/version.d.ts +2 -2
  541. package/docs/api/desk.d.ts +2 -2
  542. package/docs/api/desk.md +3 -3
  543. package/docs/api/forms.md +1 -1
  544. package/package.json +1 -1
  545. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  546. package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
  547. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  548. package/stacks/6sense.json +37 -4
  549. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  550. package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
  551. package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
  552. package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
  553. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  554. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  555. package/stacks/apollo/pages/lists.tsx.tmpl +24 -0
  556. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  557. package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
  558. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  559. package/stacks/apollo.json +68 -9
  560. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  561. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  562. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  563. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  564. package/stacks/calendly.json +8 -4
  565. package/stacks/clay/pages/companies.tsx.tmpl +17 -1
  566. package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
  567. package/stacks/clay/pages/lists.tsx.tmpl +24 -0
  568. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  569. package/stacks/clay.json +16 -3
  570. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  571. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  572. package/stacks/gong/pages/contacts.tsx.tmpl +34 -0
  573. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  574. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  575. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  576. package/stacks/gong.json +23 -6
  577. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  578. package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
  579. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  580. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  581. package/stacks/hootsuite.json +19 -4
  582. package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
  583. package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
  584. package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
  585. package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
  586. package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
  587. package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
  588. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  589. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  590. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  591. package/stacks/hubspot/pages/sequences.tsx.tmpl +18 -0
  592. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  593. package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
  594. package/stacks/hubspot.json +66 -7
  595. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  596. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  597. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  598. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  599. package/stacks/intercom.json +18 -4
  600. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  601. package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
  602. package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
  603. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  604. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  605. package/stacks/mailchimp.json +22 -6
  606. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  607. package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
  608. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  609. package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
  610. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  611. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  612. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  613. package/stacks/outreach/pages/sequences.tsx.tmpl +18 -0
  614. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  615. package/stacks/outreach.json +38 -9
  616. package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
  617. package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
  618. package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
  619. package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
  620. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  621. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  622. package/stacks/pipedrive.json +14 -2
  623. package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
  624. package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
  625. package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
  626. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  627. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  628. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  629. package/stacks/salesforce.json +17 -3
  630. package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
  631. package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
  632. package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
  633. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  634. package/stacks/zoominfo.json +36 -3
  635. package/styles/styles.css +1 -1
  636. package/templates/auth/auth-route.ts.tmpl +16 -0
  637. package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
  638. package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
  639. package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
  640. package/templates/auth/g8.auth.password.ts.tmpl +18 -0
  641. package/templates/auth/g8.auth.session.ts.tmpl +23 -0
  642. package/templates/auth/middleware.ts.tmpl +14 -0
  643. package/templates/next-app/README.md.tmpl +5 -0
  644. package/templates/next-app/app/globals.css.tmpl +349 -64
  645. package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
  646. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
@@ -0,0 +1,144 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ContactDocumentsTab: the AI documents graph8 writes about ONE contact (outreach personalization, conversation prep,
5
+ * stakeholder intel, career timeline, relationship map, persona value prop, engagement digest). Shows which exist, which
6
+ * are being written, which failed; generate one, regenerate one, generate all, and read a finished one.
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. getContactGenerationStatus GET /contacts/{id}/documents/generation-status (read; polled every 4 s while a document is generating)
10
+ * 2. getContactDocument GET /contacts/{id}/documents/types/{doc_type} (read, when "View" is opened)
11
+ * 3. generateContactDocument POST /contacts/{id}/documents/types/{doc_type}/generate (billable, ConfirmSpendDialog)
12
+ * 4. regenerateContactDocument POST /contacts/{id}/documents/types/{doc_type}/regenerate (billable, ConfirmSpendDialog)
13
+ * 5. generateAllContactDocuments POST /contacts/{id}/documents/generate-all (billable, ConfirmSpendDialog)
14
+ *
15
+ * Every document is a model call. graph8 publishes no price for them, so the confirm says that instead of showing an
16
+ * invented number. Regenerate replaces the stored text (and any inline edits to it), which the confirm states. The
17
+ * three writes only queue the work; the tab then follows the status until nothing is generating.
18
+ */
19
+
20
+ import { useEffect, useState } from "react";
21
+ import { Button, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@graph8/sdk/ui";
22
+ import { FileText, RefreshCw, Sparkles } from "lucide-react";
23
+ import { ApiError } from "@/g8/components/api-error";
24
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
25
+ import { StatusPill } from "@/g8/components/screen/status-pill";
26
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
27
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
28
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
29
+ import { formatDay } from "../lib/screen-parts";
30
+
31
+ /** The seven synthesis documents the API generates (campaign_builder/models/contact_documents.py CONTACT_SYNTHESIS_STEPS). */
32
+ export const CONTACT_DOC_TYPES = [
33
+ "outreach_personalization", "conversation_prep", "stakeholder_intel", "career_trigger_timeline", "relationship_map", "persona_value_prop", "engagement_digest",
34
+ ] as const;
35
+
36
+ interface DocStatus { id?: string; doc_type: string; status?: string; title?: string | null; folder?: string | null; updated_at?: string | null }
37
+ interface GenStatus { is_generating?: boolean; total?: number; ready?: number; generating?: number; failed?: number; documents?: DocStatus[] }
38
+
39
+ const humanize = (t: string) => t.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
40
+ type Step = { kind: "generate" | "regenerate"; docType: string } | { kind: "all" };
41
+
42
+ function pill(status: string | undefined) {
43
+ if (status === "completed") return <StatusPill tone="positive" dot>Ready</StatusPill>;
44
+ if (status === "failed") return <StatusPill tone="negative" dot>Failed</StatusPill>;
45
+ if (status === "pending" || status === "generating" || status === "queued" || status === "running") return <StatusPill tone="info" dot>Writing</StatusPill>;
46
+ return <StatusPill tone="neutral" dot>Not written</StatusPill>;
47
+ }
48
+
49
+ export function ContactDocumentsTab({ contactId, contactName }: { contactId: number; contactName: string }) {
50
+ const [step, setStep] = useState<Step | null>(null);
51
+ const [viewing, setViewing] = useState<string | null>(null);
52
+ const status = useG8Query((signal) => g8Get<GenStatus>(`/contacts/${contactId}/documents/generation-status`, undefined, { signal }), [contactId], { key: ["contacts", "documents", contactId] });
53
+ const gen = status.data?.data;
54
+ const generating = gen?.is_generating === true;
55
+
56
+ // Follow a running generation; stop when nothing is generating any more.
57
+ useEffect(() => {
58
+ if (!generating) return;
59
+ const t = setTimeout(() => void status.refetch(), 4000);
60
+ return () => clearTimeout(t);
61
+ }, [generating, status.updatedAt]); // eslint-disable-line react-hooks/exhaustive-deps
62
+
63
+ const run = useG8Mutation(async (s: Step) => {
64
+ const base = `/contacts/${contactId}/documents`;
65
+ if (s.kind === "all") await g8Post(`${base}/generate-all`);
66
+ else if (s.kind === "generate") await g8Post(`${base}/types/${s.docType}/generate`);
67
+ else await g8Post(`${base}/types/${s.docType}/regenerate`);
68
+ }, { invalidates: [["contacts", "documents", contactId]], onSuccess: () => { setStep(null); void status.refetch(); } });
69
+
70
+ const docs = new Map((gen?.documents ?? []).map((d) => [d.doc_type, d]));
71
+ const types = [...CONTACT_DOC_TYPES, ...[...docs.keys()].filter((t) => !(CONTACT_DOC_TYPES as readonly string[]).includes(t))];
72
+
73
+ if (status.loading && !status.data) return <p className="p-2 text-sm text-muted-foreground" role="status">Loading documents</p>;
74
+ if (status.error && !status.data) return <ApiError error={status.error} title="Could not load the documents" onRetry={() => void status.refetch()} />;
75
+
76
+ const title = step?.kind === "all" ? `Write all documents for ${contactName}` : step ? `${step.kind === "generate" ? "Write" : "Rewrite"} "${humanize(step.docType)}" for ${contactName}` : "";
77
+ return (
78
+ <div className="flex flex-col gap-3">
79
+ <div className="flex items-center justify-between gap-2">
80
+ <p className="text-sm text-muted-foreground">{gen?.ready ?? 0} of {gen?.total ?? CONTACT_DOC_TYPES.length} written{generating ? ", writing now" : ""}{gen?.failed ? `, ${gen.failed} failed` : ""}.</p>
81
+ <Button size="sm" variant="outline" disabled={generating || run.loading} onClick={() => { run.reset(); setStep({ kind: "all" }); }}><Sparkles className="size-4" aria-hidden />Write all</Button>
82
+ </div>
83
+ {status.error ? <ApiError compact error={status.error} title="Could not refresh the status" onRetry={() => void status.refetch()} /> : null}
84
+ <ul className="flex flex-col divide-y rounded-md border">
85
+ {types.map((t) => {
86
+ const d = docs.get(t);
87
+ const busy = d?.status === "pending" || d?.status === "generating" || d?.status === "queued" || d?.status === "running";
88
+ const ready = d?.status === "completed";
89
+ return (
90
+ <li key={t} className="flex flex-wrap items-center gap-2 px-3 py-2" data-testid={`contact-doc-${t}`}>
91
+ <FileText className="size-4 text-muted-foreground" aria-hidden />
92
+ <span className="text-sm font-medium">{d?.title || humanize(t)}</span>
93
+ {pill(d?.status)}
94
+ {d?.updated_at ? <span className="text-xs text-muted-foreground">{formatDay(d.updated_at)}</span> : null}
95
+ <span className="ml-auto flex gap-1">
96
+ {ready ? <Button size="sm" variant="ghost" onClick={() => setViewing(t)}>View</Button> : null}
97
+ <Button size="sm" variant="outline" disabled={busy || run.loading} aria-label={`${ready ? "Rewrite" : "Write"} ${humanize(t)}`} onClick={() => { run.reset(); setStep({ kind: ready ? "regenerate" : "generate", docType: t }); }}>
98
+ <RefreshCw className="size-4" aria-hidden />{ready ? "Rewrite" : d?.status === "failed" ? "Try again" : "Write"}
99
+ </Button>
100
+ </span>
101
+ </li>
102
+ );
103
+ })}
104
+ </ul>
105
+ <ContactDocumentSheet contactId={contactId} docType={viewing} onClose={() => setViewing(null)} />
106
+ <ConfirmSpendDialog
107
+ open={step != null}
108
+ tier="billable"
109
+ title={title}
110
+ description={
111
+ <>
112
+ Each document is written by an AI model and charged to your credits. No price is published for a document, so no estimate is shown.
113
+ {step?.kind === "regenerate" ? " The current text, including any edits you made to it, is replaced." : ""}
114
+ {step?.kind === "all" ? ` Writing all ${CONTACT_DOC_TYPES.length} documents again replaces the ones that exist, and each one is charged.` : ""}
115
+ {run.error ? <span role="alert" className="mt-2 block text-destructive">It could not be started. Nothing was queued; you can try again.</span> : null}
116
+ </>
117
+ }
118
+ affected={step?.kind === "all" ? CONTACT_DOC_TYPES.length : step ? 1 : undefined}
119
+ confirmLabel="Write"
120
+ confirming={run.loading}
121
+ onConfirm={() => step && void run.mutate(step)}
122
+ onCancel={() => setStep(null)}
123
+ />
124
+ </div>
125
+ );
126
+ }
127
+
128
+ function ContactDocumentSheet({ contactId, docType, onClose }: { contactId: number; docType: string | null; onClose: () => void }) {
129
+ const doc = useG8Query((signal) => g8Get<{ title?: string; content?: string | null }>(`/contacts/${contactId}/documents/types/${docType}`, undefined, { signal }), [contactId, docType], { enabled: docType != null, key: ["contacts", "documents", contactId, docType ?? ""] });
130
+ const d = docType ? doc.data?.data : undefined;
131
+ return (
132
+ <Sheet open={docType != null} onOpenChange={(o) => (o ? undefined : onClose())}>
133
+ <SheetContent className="sm:max-w-xl overflow-y-auto">
134
+ <SheetHeader>
135
+ <SheetTitle>{d?.title || (docType ? humanize(docType) : "")}</SheetTitle>
136
+ <SheetDescription>Written by an AI model from this contact's record and activity.</SheetDescription>
137
+ </SheetHeader>
138
+ {doc.loading && !d ? <p className="p-4 text-sm text-muted-foreground" role="status">Loading</p> : null}
139
+ {doc.error ? <div className="p-4"><ApiError error={doc.error} title="Could not load the document" onRetry={() => void doc.refetch()} /></div> : null}
140
+ {d ? <div className="whitespace-pre-wrap p-4 text-sm">{d.content || "This document has no text yet."}</div> : null}
141
+ </SheetContent>
142
+ </Sheet>
143
+ );
144
+ }
@@ -0,0 +1,118 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ContactInsightsTab: what graph8 knows about a contact's buying signals, and a suggested next step.
5
+ *
6
+ * SDK operations, in order:
7
+ * 1. getContactSignals GET /contacts/{id}/signals (read) signal score, trend and the last 30 days of intent,
8
+ * page visits, email engagement, forms and bookings.
9
+ * 2. getContactNextBestStep GET /contacts/{id}/next-best-step (BILLABLE although it is a GET, ConfirmSpendDialog)
10
+ * A model writes the suggestion on a cache miss, and the cache key includes the contact's engagement, so new
11
+ * activity makes the next ask a paid one again. It is therefore a button behind a confirm, never loaded on open.
12
+ * graph8 publishes no price for it, so the confirm says that instead of showing a number.
13
+ *
14
+ * The suggestion is shown as written (action, why, confidence); nothing is executed for the person. A null answer (no
15
+ * context to base it on) says so.
16
+ */
17
+
18
+ import { useState } from "react";
19
+ import { Button } from "@graph8/sdk/ui";
20
+ import { Lightbulb } from "lucide-react";
21
+ import { ApiError } from "@/g8/components/api-error";
22
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
23
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
24
+ import { StatusPill } 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 } from "@/g8/lib/g8-client";
28
+
29
+ interface Signals {
30
+ signal_score?: number | null;
31
+ signal_trend?: string | null;
32
+ keyword_intents?: Array<{ keyword?: string; page_url?: string }>;
33
+ topic_intents?: unknown[];
34
+ page_visits?: Array<{ page_url?: string; visit_count?: number }>;
35
+ email_engagement?: unknown[];
36
+ form_submissions?: unknown[];
37
+ meeting_bookings?: unknown[];
38
+ intent_pages?: unknown[];
39
+ }
40
+ interface NextStep { action?: string; action_type?: string; reasoning?: string[]; confidence?: number }
41
+
42
+ const count = (v: unknown) => (Array.isArray(v) ? v.length : 0);
43
+ const humanize = (t: string) => t.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
44
+
45
+ export function ContactInsightsTab({ contactId }: { contactId: number }) {
46
+ const [confirm, setConfirm] = useState(false);
47
+ const signals = useG8Query((signal) => g8Get<Signals>(`/contacts/${contactId}/signals`, undefined, { signal }), [contactId], { key: ["contacts", "signals", contactId] });
48
+ const next = useG8Mutation(async () => {
49
+ const res = await g8Get<NextStep | null>(`/contacts/${contactId}/next-best-step`);
50
+ return res.data ?? null;
51
+ }, { onSuccess: () => setConfirm(false) });
52
+ const s = signals.data?.data;
53
+ const card = next.data;
54
+
55
+ return (
56
+ <div className="flex flex-col gap-4">
57
+ <section aria-label="Next best step" className="flex flex-col gap-2 rounded-md border p-3">
58
+ <div className="flex items-center justify-between gap-2">
59
+ <h3 className="flex items-center gap-2 text-sm font-medium"><Lightbulb className="size-4" aria-hidden />Next best step</h3>
60
+ <Button size="sm" variant="outline" disabled={next.loading} onClick={() => { next.reset(); setConfirm(true); }}>{card !== undefined ? "Suggest again" : "Suggest a next step"}</Button>
61
+ </div>
62
+ {next.error && !confirm ? <ApiError error={next.error} title="Could not get a suggestion" /> : null}
63
+ {card === null ? <p className="text-sm text-muted-foreground" role="status">There is not enough on this contact to suggest a step yet.</p> : null}
64
+ {card ? (
65
+ <div className="flex flex-col gap-1 text-sm" role="status">
66
+ <p className="font-medium">{card.action}</p>
67
+ <div className="flex items-center gap-2">
68
+ {card.action_type ? <StatusPill tone="info">{humanize(card.action_type)}</StatusPill> : null}
69
+ {typeof card.confidence === "number" ? <span className="text-xs text-muted-foreground">Confidence {Math.round(card.confidence * 100)}%</span> : null}
70
+ </div>
71
+ {card.reasoning?.length ? <ul className="list-disc pl-5 text-muted-foreground">{card.reasoning.map((r, i) => <li key={i}>{r}</li>)}</ul> : null}
72
+ </div>
73
+ ) : null}
74
+ {card === undefined && !next.error ? <p className="text-xs text-muted-foreground">Written by an AI model from this contact's activity. It costs credits, so it only runs when you ask.</p> : null}
75
+ </section>
76
+
77
+ <section aria-label="Signals" className="flex flex-col gap-2">
78
+ <h3 className="text-sm font-medium">Signals, last 30 days</h3>
79
+ {signals.loading && !s ? <p className="text-sm text-muted-foreground" role="status">Loading signals</p> : null}
80
+ {signals.error ? <ApiError error={signals.error} title="Could not load signals" onRetry={() => void signals.refetch()} /> : null}
81
+ {s ? (
82
+ <>
83
+ <KeyValueList columns={2} items={[
84
+ { label: "Signal score", value: s.signal_score ?? <span className="text-muted-foreground">-</span> },
85
+ { label: "Trend", value: s.signal_trend ? humanize(s.signal_trend) : <span className="text-muted-foreground">-</span> },
86
+ { label: "Keyword intents", value: count(s.keyword_intents) },
87
+ { label: "Topic intents", value: count(s.topic_intents) },
88
+ { label: "Pages visited", value: count(s.page_visits) },
89
+ { label: "Email engagement", value: count(s.email_engagement) },
90
+ { label: "Form submissions", value: count(s.form_submissions) },
91
+ { label: "Meeting bookings", value: count(s.meeting_bookings) },
92
+ ]} />
93
+ {s.page_visits?.length ? (
94
+ <ul className="text-sm text-muted-foreground">{s.page_visits.slice(0, 5).map((p, i) => <li key={`${p.page_url}-${i}`} className="truncate">{p.page_url}{p.visit_count ? ` (${p.visit_count})` : ""}</li>)}</ul>
95
+ ) : null}
96
+ </>
97
+ ) : null}
98
+ </section>
99
+
100
+ <ConfirmSpendDialog
101
+ open={confirm}
102
+ tier="billable"
103
+ title="Suggest a next step"
104
+ description={
105
+ <>
106
+ An AI model writes the suggestion from this contact's activity and charges your credits. A recent suggestion for the same activity is reused. No price is published for it, so no estimate is shown.
107
+ {next.error ? <span role="alert" className="mt-2 block text-destructive">The suggestion could not be made. You can try again.</span> : null}
108
+ </>
109
+ }
110
+ affected={1}
111
+ confirmLabel="Suggest"
112
+ confirming={next.loading}
113
+ onConfirm={() => void next.mutate(undefined)}
114
+ onCancel={() => setConfirm(false)}
115
+ />
116
+ </div>
117
+ );
118
+ }
@@ -0,0 +1,85 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ContactLookupLink: "Open contact" for something that only knows a person's email (an inbox thread, a meeting
5
+ * attendee, a visitor). It looks the email up in your CRM and, when exactly one contact has it, shows a button that
6
+ * calls `onOpen(contactId)` (route to your contacts screen: `router.push("/contacts?id=" + id)`). When nobody has that
7
+ * email it renders nothing, so a thread from a stranger gets no dead button, unless `createIfMissing` is set: then a
8
+ * stranger gets an "Add to contacts" button that creates the contact from the email (and name, when you pass one) and
9
+ * opens it.
10
+ *
11
+ * SDK operations, in order:
12
+ * 1. listContacts GET /contacts?email=<email>&limit=2 (read)
13
+ * 2. createContacts POST /contacts (write; only after "Add to contacts" is clicked)
14
+ * Nothing here is billable, external or destructive.
15
+ */
16
+
17
+ import { Button } from "@graph8/sdk/ui";
18
+ import { UserPlus, UserRound } from "lucide-react";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
21
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
22
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
23
+
24
+ export interface ContactLookupLinkProps {
25
+ /** The person's email. Nothing is looked up (and nothing rendered) without one. */
26
+ email?: string | null;
27
+ /** Called with the CRM contact id when the button is clicked. */
28
+ onOpen: (contactId: number) => void;
29
+ label?: string;
30
+ variant?: "default" | "outline" | "secondary" | "ghost";
31
+ size?: "default" | "sm";
32
+ /** Offer to create the contact when nobody has this email. Pass the sender's name to prefill it. */
33
+ createIfMissing?: boolean | { firstName?: string | null; lastName?: string | null };
34
+ /** Called with the new contact's id after it was created (before `onOpen`). */
35
+ onCreated?: (contactId: number) => void;
36
+ }
37
+
38
+ /** Splits "Maya Chen" into first and last name; a single word is a first name. */
39
+ export function splitName(name: string | null | undefined): { firstName?: string; lastName?: string } {
40
+ const parts = (name ?? "").trim().split(/\s+/).filter(Boolean);
41
+ if (parts.length === 0) return {};
42
+ return parts.length === 1 ? { firstName: parts[0] } : { firstName: parts[0], lastName: parts.slice(1).join(" ") };
43
+ }
44
+
45
+ export function ContactLookupLink({ email, onOpen, label = "Open contact", variant = "outline", size = "sm", createIfMissing, onCreated }: ContactLookupLinkProps) {
46
+ const address = email?.trim().toLowerCase() ?? "";
47
+ const q = useG8Query(
48
+ async (signal) => ((await g8Get<Array<{ id: number; work_email?: string | null }>>("/contacts", { email: address, limit: 2, page: 1 }, { signal })).data ?? []),
49
+ [address],
50
+ { enabled: address.includes("@"), key: ["contacts", "by-email", address] },
51
+ );
52
+ const create = useG8Mutation(
53
+ async () => {
54
+ const name = typeof createIfMissing === "object" ? createIfMissing : {};
55
+ const env = await g8Post<{ contact_id?: number | null }>("/contacts", { work_email: address, first_name: name.firstName || undefined, last_name: name.lastName || undefined });
56
+ const id = (env.data as { contact_id?: number | null } | undefined)?.contact_id;
57
+ // Never open a guess: when the API sent no id, look the new contact up instead.
58
+ if (id != null) return id;
59
+ const again = (await g8Get<Array<{ id: number }>>("/contacts", { email: address, limit: 1, page: 1 })).data ?? [];
60
+ return again[0]?.id ?? null;
61
+ },
62
+ { invalidates: [["contacts"]], onError: (err) => { if ((err as { status?: number }).status === 409) void q.refetch(); }, onSuccess: (id) => { if (id != null) { onCreated?.(id); onOpen(id); } } },
63
+ );
64
+ const match = (q.data ?? []).find((c) => !c.work_email || c.work_email.toLowerCase() === address);
65
+ if (!address) return null;
66
+ if (!match) {
67
+ // Only offer to create once the lookup has really answered "nobody": never while loading or after an error.
68
+ if (!createIfMissing || q.loading || q.error || q.data === undefined) return null;
69
+ return (
70
+ <>
71
+ <Button size={size} variant={variant} loading={create.loading} onClick={() => void create.mutate(undefined)}>
72
+ <UserPlus className="mr-1 size-4" aria-hidden />
73
+ Add to contacts
74
+ </Button>
75
+ {create.error ? <ApiError error={create.error} title="Could not add the contact" onRetry={create.reset} /> : null}
76
+ </>
77
+ );
78
+ }
79
+ return (
80
+ <Button size={size} variant={variant} onClick={() => onOpen(match.id)}>
81
+ <UserRound className="mr-1 size-4" aria-hidden />
82
+ {label}
83
+ </Button>
84
+ );
85
+ }
@@ -0,0 +1,43 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ContactRowActions: the "..." button at the end of a contact row. It opens a small dialog for that ONE contact with
5
+ * the record actions (tags, verify email, unlock info). Each action is the same component the bulk bar
6
+ * and the detail panel use, so a row, a selection and the open contact behave identically.
7
+ *
8
+ * SDK operations: none of its own. The actions it hosts call assign/unassignContactTags
9
+ * (write), verifyContactEmails (billable) and unlockContactInfo (billable); the paid ones keep their own
10
+ * ConfirmSpendDialog. Also exported for apps that render their own table through ContactTable's row slot.
11
+ */
12
+
13
+ import { useState } from "react";
14
+ import { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@graph8/sdk/ui";
15
+ import { MoreHorizontal } from "lucide-react";
16
+ import { contactName } from "../lib/crm-types";
17
+ import type { ContactRecord } from "../lib/crm-types";
18
+ import { UnlockInfoButton } from "./unlock-info-button";
19
+ import { VerifyEmailButton } from "./verify-email-button";
20
+ import { TagAssignButton } from "./tag-assign";
21
+
22
+ export function ContactRowActions({ row, onDone }: { row: ContactRecord; onDone?: () => void }) {
23
+ const [open, setOpen] = useState(false);
24
+ const name = contactName(row);
25
+ return (
26
+ <>
27
+ <Button variant="ghost" size="sm" aria-label={`Actions for ${name}`} onClick={() => setOpen(true)}><MoreHorizontal className="size-4" aria-hidden /></Button>
28
+ <Dialog open={open} onOpenChange={setOpen}>
29
+ <DialogContent>
30
+ <DialogHeader>
31
+ <DialogTitle>{name}</DialogTitle>
32
+ <DialogDescription>Actions for this contact.</DialogDescription>
33
+ </DialogHeader>
34
+ <div className="flex flex-wrap items-center gap-2">
35
+ <TagAssignButton contactIds={[row.id]} onDone={() => onDone?.()} />
36
+ <VerifyEmailButton rows={[row]} onDone={onDone} />
37
+ <UnlockInfoButton rows={[row]} onDone={onDone} />
38
+ </div>
39
+ </DialogContent>
40
+ </Dialog>
41
+ </>
42
+ );
43
+ }
@@ -0,0 +1,113 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ContactTagChips: the tags on ONE contact as chips, with a remove button on each and an "Add tag" picker that can also
5
+ * create a tag on the spot. Used in the contact panel's Overview.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. getContactTagIds GET /contacts/{contact_id}/tags (read) -> { tag_ids }
9
+ * 2. listContactTags GET /contacts/tags (read) names and colours, via useContactTags
10
+ * 3. assignContactTags POST /contacts/tags/assign { tag_ids, contact_ids: [id] } (write, idempotent)
11
+ * 4. unassignContactTags POST /contacts/tags/unassign { tag_ids, contact_ids: [id] } (write; the tag itself survives)
12
+ * 5. createContactTag POST /contacts/tags { name, color } (write) only from "Create tag", then assigned
13
+ *
14
+ * Removing a chip is immediate and reversible (add the tag again), so it needs no confirmation. A tag id that the
15
+ * list does not know (archived since) is shown as "Archived tag" rather than a raw id.
16
+ */
17
+
18
+ import { useRef, useState } from "react";
19
+ import { Button, cn } from "@graph8/sdk/ui";
20
+ import { Plus, Tag, X } from "lucide-react";
21
+ import { ApiError } from "@/g8/components/api-error";
22
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
23
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
24
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
25
+ import { useContactTags } from "../hooks/use-contact-tags";
26
+ import { HEX_COLOR, TAG_COLORS } from "../lib/record-actions";
27
+ import type { ContactTag } from "../lib/record-actions";
28
+
29
+ /** One tag as a chip: a colour dot and the name. The colour is the tag's own (data), never a theme colour. */
30
+ export function TagChip({ tag, onRemove, disabled }: { tag: Pick<ContactTag, "name" | "color">; onRemove?: () => void; disabled?: boolean }) {
31
+ const color = tag.color && HEX_COLOR.test(tag.color) ? tag.color : undefined;
32
+ return (
33
+ <span className="inline-flex items-center gap-1.5 rounded-full border bg-background px-2 py-0.5 text-xs font-medium">
34
+ <span aria-hidden className={cn("size-2 rounded-full", color ? "" : "bg-muted-foreground")} style={color ? { backgroundColor: color } : undefined} />
35
+ {tag.name}
36
+ {onRemove ? (
37
+ <button type="button" aria-label={`Remove tag ${tag.name}`} disabled={disabled} onClick={onRemove} className="-mr-1 rounded-full p-0.5 text-muted-foreground hover:text-foreground disabled:opacity-50">
38
+ <X className="size-3" aria-hidden />
39
+ </button>
40
+ ) : null}
41
+ </span>
42
+ );
43
+ }
44
+
45
+ export interface ContactTagChipsProps {
46
+ contactId: number;
47
+ /** Called after a tag was added, removed or created (refresh the list that shows tags). */
48
+ onChanged?: () => void;
49
+ }
50
+
51
+ export function ContactTagChips({ contactId, onChanged }: ContactTagChipsProps) {
52
+ const [adding, setAdding] = useState(false);
53
+ const [newName, setNewName] = useState("");
54
+ const [color, setColor] = useState<string>(TAG_COLORS[0]);
55
+ const all = useContactTags();
56
+ const ids = useG8Query((signal) => g8Get<{ tag_ids?: string[] }>(`/contacts/${contactId}/tags`, undefined, { signal }), [contactId], { key: ["contacts", "tags", contactId] });
57
+ const assigned = ids.data?.data?.tag_ids ?? [];
58
+ const byId = new Map(all.tags.map((t) => [t.id, t]));
59
+ const free = all.tags.filter((t) => !assigned.includes(t.id));
60
+
61
+ const write = useG8Mutation(async (v: { op: "assign" | "unassign"; tagId: string }) => {
62
+ const body = { tag_ids: [v.tagId], contact_ids: [contactId] };
63
+ if (v.op === "assign") await g8Post("/contacts/tags/assign", body);
64
+ else await g8Post("/contacts/tags/unassign", body);
65
+ }, { invalidates: [["contacts", "tags", contactId], ["contact-tags"], ["contacts", "list"]], onSuccess: () => { void ids.refetch(); onChanged?.(); } });
66
+
67
+ /** The tag step 1 created, kept so a retry after a failed assign does not create it a second time. */
68
+ const made = useRef<{ name: string; id: string } | null>(null);
69
+ const create = useG8Mutation(async (v: { name: string; color: string }) => {
70
+ if (made.current?.name !== v.name) {
71
+ const res = await g8Post<ContactTag>("/contacts/tags", { name: v.name, color: v.color });
72
+ if (!res.data?.id) throw new Error("The tag was created but the server did not return its id. Pick it from the list.");
73
+ made.current = { name: v.name, id: res.data.id };
74
+ }
75
+ await g8Post("/contacts/tags/assign", { tag_ids: [made.current.id], contact_ids: [contactId] });
76
+ made.current = null;
77
+ }, { invalidates: [["contacts", "tags", contactId], ["contact-tags"], ["contacts", "list"]], onSuccess: () => { setNewName(""); setAdding(false); void ids.refetch(); onChanged?.(); } });
78
+
79
+ if (ids.loading && !ids.data) return <p className="text-sm text-muted-foreground" role="status">Loading tags</p>;
80
+ if (ids.error) return <ApiError compact error={ids.error} title="Could not load tags" onRetry={() => void ids.refetch()} />;
81
+
82
+ return (
83
+ <div className="flex flex-col gap-2" aria-label="Tags">
84
+ <div className="flex flex-wrap items-center gap-1.5">
85
+ <Tag className="size-3.5 text-muted-foreground" aria-hidden />
86
+ {assigned.length === 0 ? <span className="text-sm text-muted-foreground">No tags</span> : null}
87
+ {assigned.map((id) => <TagChip key={id} tag={byId.get(id) ?? { name: all.loaded ? "Archived tag" : "Tag" }} disabled={write.loading} onRemove={() => void write.mutate({ op: "unassign", tagId: id })} />)}
88
+ <Button variant="ghost" size="sm" onClick={() => setAdding((a) => !a)} aria-expanded={adding}><Plus className="size-3.5" aria-hidden />Add tag</Button>
89
+ </div>
90
+ {adding ? (
91
+ <div className="flex flex-col gap-2 rounded-md border p-2">
92
+ {all.error ? <ApiError compact error={all.error} title="Could not load the tag list" onRetry={() => void all.refetch()} /> : null}
93
+ {free.length > 0 ? (
94
+ <div className="flex flex-wrap gap-1.5" role="group" aria-label="Existing tags">
95
+ {free.map((t) => (
96
+ <button key={t.id} type="button" disabled={write.loading} onClick={() => void write.mutate({ op: "assign", tagId: t.id })} aria-label={`Add tag ${t.name}`} className="rounded-full hover:bg-muted disabled:opacity-50"><TagChip tag={t} /></button>
97
+ ))}
98
+ </div>
99
+ ) : <p className="text-xs text-muted-foreground">{all.loaded ? "Every tag is already on this contact." : "Loading tags"}</p>}
100
+ <form className="flex flex-wrap items-center gap-2" onSubmit={(e) => { e.preventDefault(); if (newName.trim()) void create.mutate({ name: newName.trim(), color }); }}>
101
+ <input aria-label="New tag name" maxLength={50} placeholder="New tag name" value={newName} onChange={(e) => setNewName(e.target.value)} className="h-8 w-40 rounded-md border bg-background px-2 text-sm" />
102
+ <span className="flex gap-1" role="radiogroup" aria-label="New tag colour">
103
+ {TAG_COLORS.map((c) => <button key={c} type="button" role="radio" aria-checked={color === c} aria-label={`Colour ${c}`} onClick={() => setColor(c)} className={cn("size-5 rounded-full border-2", color === c ? "border-foreground" : "border-transparent")} style={{ backgroundColor: c }} />)}
104
+ </span>
105
+ <Button type="submit" size="sm" variant="outline" loading={create.loading} disabled={!newName.trim()}>Create and add</Button>
106
+ </form>
107
+ </div>
108
+ ) : null}
109
+ {write.error ? <ApiError compact error={write.error} title="Could not change the tag" /> : null}
110
+ {create.error ? <ApiError compact error={create.error} title="Could not create the tag" /> : null}
111
+ </div>
112
+ );
113
+ }