@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,130 @@
1
+ "use client";
2
+
3
+ /**
4
+ * QuestionRow: one question of a security questionnaire with the AI's answer and every review action.
5
+ *
6
+ * SDK operations, in order (each is one button; the row shows its own spinner and error):
7
+ * 1. updateQuestionnaireAnswer (PATCH .../questions/{question_id}) write replace the answer with a human one; the AI's original is kept beside it
8
+ * 2. approveQuestionnaireAnswer (POST .../questions/{question_id}/approve) write confirm the answer is right (no text changes)
9
+ * 3. escalateQuestionnaireQuestion (POST .../escalate) write hand it to the security team, with notes
10
+ * 4. skipQuestionnaireQuestion (POST .../mark-skip) write not applicable to this organization; the workbook says so
11
+ * 5. flagQuestionnaireAnswer (POST .../mark-wrong) write say the AI's answer is wrong so a person rewrites it
12
+ * 6. saveQuestionnaireAnswerToLibrary (POST .../save-to-library) write keep the answer as org knowledge: the NEXT questionnaire is answered from it
13
+ * Nothing here is billable, external or destructive. After any action the questionnaire is read again, so the row shows the
14
+ * state the API holds, never a guess.
15
+ */
16
+
17
+ import { useState } from "react";
18
+ import { BookmarkPlus, Check, Flag, MinusCircle, Pencil, UserRound } from "lucide-react";
19
+ import { Button, Textarea } from "@graph8/sdk/ui";
20
+ import { ApiError } from "@/g8/components/api-error";
21
+ import { StatusPill } from "@/g8/components/screen/status-pill";
22
+ import { g8Patch, g8Post } from "@/g8/lib/g8-client";
23
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
24
+ import { answerOf, reviewState } from "../lib/documents-types";
25
+ import type { Question, Rec } from "../lib/documents-types";
26
+
27
+ export interface QuestionRowProps {
28
+ accountId: number | string;
29
+ questionnaireId: string;
30
+ question: Question;
31
+ /** Called after any action succeeds, so the questionnaire is read again. */
32
+ onChanged: () => void;
33
+ }
34
+
35
+ type Mode = null | "edit" | "escalate" | "skip" | "wrong";
36
+ type Action = { kind: "edit"; text: string } | { kind: "approve" } | { kind: "escalate" | "skip" | "wrong"; notes: string } | { kind: "library" };
37
+
38
+ const NOTE_COPY: Record<"escalate" | "skip" | "wrong", { label: string; confirm: string; hint: string }> = {
39
+ escalate: { label: "Notes for the security team", confirm: "Escalate", hint: "What the person picking this up needs to know." },
40
+ skip: { label: "Why it does not apply (optional)", confirm: "Mark not applicable", hint: "The filled workbook says this question does not apply." },
41
+ wrong: { label: "What is wrong with the answer (optional)", confirm: "Mark wrong", hint: "A person rewrites it; this does not change the text." },
42
+ };
43
+
44
+ export function QuestionRow({ accountId, questionnaireId, question: q, onChanged }: QuestionRowProps) {
45
+ const [mode, setMode] = useState<Mode>(null);
46
+ const [text, setText] = useState("");
47
+ const [saved, setSaved] = useState<string | null>(null);
48
+ const base = `/accounts/${encodeURIComponent(String(accountId))}/security-questionnaires/${encodeURIComponent(questionnaireId)}/questions/${encodeURIComponent(q.id)}`;
49
+ const state = reviewState(q);
50
+ const current = answerOf(q);
51
+
52
+ const run = useG8Mutation(
53
+ async (a: Action): Promise<Rec> => {
54
+ if (a.kind === "edit") return (await g8Patch<Rec>(base, { final_answer: a.text })).data;
55
+ if (a.kind === "approve") return (await g8Post<Rec>(`${base}/approve`)).data;
56
+ if (a.kind === "library") return (await g8Post<Rec>(`${base}/save-to-library`)).data;
57
+ const notes = a.notes.trim() || null;
58
+ if (a.kind === "escalate") return (await g8Post<Rec>(`${base}/escalate`, { notes })).data;
59
+ if (a.kind === "skip") return (await g8Post<Rec>(`${base}/mark-skip`, { notes })).data;
60
+ return (await g8Post<Rec>(`${base}/mark-wrong`, { notes })).data;
61
+ },
62
+ {
63
+ onSuccess: (_r, a) => {
64
+ setMode(null);
65
+ setSaved(a.kind === "library" ? "Saved to the answer library." : null);
66
+ onChanged();
67
+ },
68
+ },
69
+ );
70
+ const open = (m: Exclude<Mode, null>): void => { setMode(m); setText(m === "edit" ? current : ""); setSaved(null); };
71
+ const busy = run.loading;
72
+ const [pending, setPending] = useState<Action["kind"] | null>(null);
73
+ const go = async (a: Action): Promise<void> => {
74
+ setPending(a.kind);
75
+ await run.mutate(a);
76
+ setPending(null);
77
+ };
78
+ const spin = (k: Action["kind"]): boolean => busy && pending === k;
79
+
80
+ return (
81
+ <li className="grid gap-2 rounded-md border p-3" aria-label={q.question_text ?? "Question"}>
82
+ <div className="flex flex-wrap items-start justify-between gap-2">
83
+ <div className="min-w-0">
84
+ <p className="font-medium">{q.question_text || "Untitled question"}</p>
85
+ <p className="text-muted-foreground text-xs">{[q.question_topic, q.sheet_name, q.cell_reference].filter(Boolean).join(" · ") || "-"}</p>
86
+ </div>
87
+ <div className="flex shrink-0 gap-1">
88
+ <StatusPill tone={state.tone}>{state.label}</StatusPill>
89
+ {q.confidence && q.confidence !== "confirmed" && q.confidence !== "pending" ? <StatusPill tone="neutral" quiet>{q.confidence} confidence</StatusPill> : null}
90
+ </div>
91
+ </div>
92
+ {mode === "edit" ? (
93
+ <div className="grid gap-2">
94
+ <Textarea aria-label="Your answer" rows={5} value={text} onChange={(e) => setText(e.target.value)} />
95
+ <div className="flex gap-2">
96
+ <Button size="sm" loading={spin("edit")} disabled={busy || text.trim() === "" || text === current} onClick={() => void go({ kind: "edit", text })}>Save answer</Button>
97
+ <Button size="sm" variant="outline" disabled={busy} onClick={() => setMode(null)}>Cancel</Button>
98
+ </div>
99
+ </div>
100
+ ) : (
101
+ <p className="whitespace-pre-wrap text-sm">{current || <span className="text-muted-foreground">No answer yet.</span>}</p>
102
+ )}
103
+ {q.final_answer && q.answer_text ? <details className="text-xs"><summary className="text-muted-foreground cursor-pointer">AI answer before your edit</summary><p className="mt-1 whitespace-pre-wrap">{q.answer_text}</p></details> : null}
104
+ {(q.guardrail_warnings ?? []).length > 0 ? <ul className="text-warning-foreground list-disc pl-5 text-xs">{(q.guardrail_warnings ?? []).map((w) => <li key={w}>{w}</li>)}</ul> : null}
105
+ {q.escalation_notes ? <p className="text-muted-foreground text-xs">Note: {q.escalation_notes}</p> : null}
106
+ {mode === "escalate" || mode === "skip" || mode === "wrong" ? (
107
+ <div className="grid gap-2">
108
+ <Textarea aria-label={NOTE_COPY[mode].label} rows={3} value={text} placeholder={NOTE_COPY[mode].hint} onChange={(e) => setText(e.target.value)} />
109
+ <p className="text-muted-foreground text-xs">{NOTE_COPY[mode].label}</p>
110
+ <div className="flex gap-2">
111
+ <Button size="sm" loading={spin(mode)} disabled={busy} onClick={() => void go({ kind: mode, notes: text })}>{NOTE_COPY[mode].confirm}</Button>
112
+ <Button size="sm" variant="outline" disabled={busy} onClick={() => setMode(null)}>Cancel</Button>
113
+ </div>
114
+ </div>
115
+ ) : null}
116
+ {mode === null ? (
117
+ <div className="flex flex-wrap gap-1">
118
+ <Button size="sm" variant="ghost" disabled={busy} onClick={() => open("edit")}><Pencil className="size-4" />Edit answer</Button>
119
+ <Button size="sm" variant="ghost" loading={spin("approve")} disabled={busy || state.label === "Approved" || !current} onClick={() => void go({ kind: "approve" })}><Check className="size-4" />Approve</Button>
120
+ <Button size="sm" variant="ghost" disabled={busy} onClick={() => open("escalate")}><UserRound className="size-4" />Escalate</Button>
121
+ <Button size="sm" variant="ghost" disabled={busy} onClick={() => open("skip")}><MinusCircle className="size-4" />Not applicable</Button>
122
+ <Button size="sm" variant="ghost" disabled={busy || !q.answer_text} onClick={() => open("wrong")}><Flag className="size-4" />Wrong answer</Button>
123
+ <Button size="sm" variant="ghost" loading={spin("library")} disabled={busy || !current} onClick={() => void go({ kind: "library" })}><BookmarkPlus className="size-4" />Save to answer library</Button>
124
+ </div>
125
+ ) : null}
126
+ {saved ? <p role="status" className="text-xs">{saved}</p> : null}
127
+ {run.error ? <ApiError error={run.error} title="Could not update this question" compact /> : null}
128
+ </li>
129
+ );
130
+ }
@@ -0,0 +1,197 @@
1
+ "use client";
2
+
3
+ /**
4
+ * QuestionnaireDetail: one security questionnaire, the review loop on its questions, and its file actions.
5
+ *
6
+ * SDK operations, in order:
7
+ * 1. getSecurityQuestionnaire (GET .../security-questionnaires/{questionnaire_id}) read the questionnaire with EVERY question and its answer
8
+ * 2. getSecurityQuestionnaireStatus (GET .../{questionnaire_id}/status) read the cheap progress read, polled while it is still being answered
9
+ * 3. QuestionRow (update, approve, escalate, skip, flag, save to library) write the review actions, one per button
10
+ * 4. downloadSecurityQuestionnaire (GET .../{questionnaire_id}/download) read the FILLED workbook (XLSX bytes, not the JSON envelope); the API refuses it until the questionnaire is completed or needs review
11
+ * 5. updateSecurityQuestionnaire (PATCH .../{questionnaire_id}) write rename only; never touches an answer
12
+ * 6. deleteSecurityQuestionnaire (DELETE .../{questionnaire_id}) DESTRUCTIVE removes it AND every question; behind ConfirmSpendDialog
13
+ * Uploading a questionnaire (the billable step) lives in QuestionnairesPanel.
14
+ */
15
+
16
+ import { useState } from "react";
17
+ import { Download, Pencil, Trash2 } from "lucide-react";
18
+ import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
19
+ import { useBrand } from "@/g8/hooks/use-brand";
20
+ import { ApiError } from "@/g8/components/api-error";
21
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
22
+ import { EmptyState } from "@/g8/components/empty-state";
23
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
24
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
25
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
26
+ import { StatusPill } from "@/g8/components/screen/status-pill";
27
+ import { g8Delete, g8Get, g8Patch } from "@/g8/lib/g8-client";
28
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
29
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
30
+ import { useJobPoll } from "@/g8/hooks/use-job-poll";
31
+ import { plural } from "@/g8/lib/plural";
32
+ import { downloadFile } from "../lib/download";
33
+ import { canDownload, isWorking, percentOf, reviewState, statusInfo, when } from "../lib/documents-types";
34
+ import type { Question, Questionnaire, QuestionnaireProgress, Rec } from "../lib/documents-types";
35
+ import { QuestionRow } from "./question-row";
36
+
37
+ export interface QuestionnaireDetailProps {
38
+ accountId: number;
39
+ questionnaire: Questionnaire;
40
+ /** The list should be read again (a rename, a delete, a status change). */
41
+ onChanged: () => void;
42
+ onClose: () => void;
43
+ onDeleted?: (q: Questionnaire) => void;
44
+ onExpand?: () => void;
45
+ onCollapse?: () => void;
46
+ }
47
+
48
+ const PAGE = 25;
49
+ const FILTERS = [
50
+ { id: "all", label: "All" },
51
+ { id: "attention", label: "Needs attention" },
52
+ { id: "approved", label: "Approved" },
53
+ { id: "skipped", label: "Not applicable" },
54
+ ] as const;
55
+
56
+ const nameOf = (q: Questionnaire): string => q.display_name || q.original_file_name || "Untitled questionnaire";
57
+
58
+ export function QuestionnaireDetail({ accountId, questionnaire, onChanged, onClose, onDeleted, onExpand, onCollapse }: QuestionnaireDetailProps) {
59
+ const brand = useBrand();
60
+ const [filter, setFilter] = useState<(typeof FILTERS)[number]["id"]>("all");
61
+ const [shown, setShown] = useState(PAGE);
62
+ const [renaming, setRenaming] = useState(false);
63
+ const [name, setName] = useState("");
64
+ const [deleting, setDeleting] = useState(false);
65
+ const [downloadError, setDownloadError] = useState<unknown>(undefined);
66
+ const [downloading, setDownloading] = useState(false);
67
+ const base = `/accounts/${encodeURIComponent(String(accountId))}/security-questionnaires/${encodeURIComponent(questionnaire.id)}`;
68
+
69
+ const full = useG8Query((signal) => g8Get<Questionnaire>(base, {}, { signal }).then((r) => r.data), [base], { key: ["documents", "questionnaire", accountId, questionnaire.id], placeholderData: questionnaire });
70
+ const q = full.data && full.data.id === questionnaire.id ? full.data : questionnaire;
71
+ const loaded = !full.isPlaceholderData && full.data?.id === questionnaire.id;
72
+ const poll = useJobPoll<QuestionnaireProgress>((signal) => g8Get<QuestionnaireProgress>(`${base}/status`, {}, { signal }).then((r) => r.data), {
73
+ enabled: isWorking(q.status),
74
+ intervalMs: 4000,
75
+ isTerminal: (s) => !isWorking(s.status),
76
+ onDone: () => { void full.refetch(); onChanged(); },
77
+ });
78
+ const live = poll.status ?? q;
79
+ const info = statusInfo(live.status ?? q.status);
80
+ const total = live.total_questions ?? q.total_questions ?? null;
81
+ const answered = live.answered_questions ?? q.answered_questions ?? null;
82
+ const pct = percentOf(answered, total);
83
+
84
+ const rename = useG8Mutation((next: string) => g8Patch<Rec>(base, { display_name: next.trim() || null }), {
85
+ onSuccess: () => { setRenaming(false); void full.refetch(); onChanged(); },
86
+ });
87
+ const remove = useG8Mutation(() => g8Delete(base), { onSuccess: () => { setDeleting(false); onChanged(); onDeleted?.(questionnaire); } });
88
+
89
+ const save = async (): Promise<void> => {
90
+ setDownloading(true);
91
+ setDownloadError(undefined);
92
+ try {
93
+ await downloadFile(`${base}/download`, `${(q.original_file_name || nameOf(q)).replace(/\.[a-z0-9]+$/i, "")}_filled.xlsx`);
94
+ } catch (err) {
95
+ setDownloadError(err);
96
+ } finally {
97
+ setDownloading(false);
98
+ }
99
+ };
100
+
101
+ const all = q.questions ?? [];
102
+ const keep = (x: Question): boolean => {
103
+ const s = reviewState(x).label;
104
+ if (filter === "approved") return s === "Approved";
105
+ if (filter === "skipped") return s === "Not applicable";
106
+ if (filter === "attention") return s !== "Approved" && s !== "Not applicable";
107
+ return true;
108
+ };
109
+ const visible = all.filter(keep);
110
+
111
+ const review = full.error && !loaded ? <ApiError error={full.error} title="Could not load this questionnaire" onRetry={() => void full.refetch()} /> : !loaded ? (
112
+ <Skeleton className="h-40 w-full" aria-label="Loading questions" />
113
+ ) : (
114
+ <div className="grid gap-3">
115
+ <div role="group" aria-label="Show questions" className="flex flex-wrap gap-1">
116
+ {FILTERS.map((f) => <Button key={f.id} size="sm" variant={filter === f.id ? "default" : "outline"} aria-pressed={filter === f.id} onClick={() => { setFilter(f.id); setShown(PAGE); }}>{f.label}</Button>)}
117
+ </div>
118
+ {all.length === 0 ? (
119
+ <EmptyState compact title={isWorking(live.status ?? q.status) ? "Questions are still being read" : "No questions"} description={q.status === "failed" ? "No questions could be read from this file." : `Questions appear here as ${brand.name} reads the file.`} />
120
+ ) : visible.length === 0 ? (
121
+ <EmptyState compact title="No questions here" description="Try another view." />
122
+ ) : (
123
+ <>
124
+ <p role="status" className="text-muted-foreground text-xs">{visible.length === all.length ? plural(all.length, "question") : `${visible.length} of ${plural(all.length, "question")}`}</p>
125
+ <ul className="grid gap-2" aria-label="Questions">
126
+ {visible.slice(0, shown).map((x) => <QuestionRow key={x.id} accountId={accountId} questionnaireId={questionnaire.id} question={x} onChanged={() => { void full.refetch(); onChanged(); }} />)}
127
+ </ul>
128
+ {visible.length > shown ? <Button variant="outline" onClick={() => setShown((n) => n + PAGE)}>Show {Math.min(PAGE, visible.length - shown)} more</Button> : null}
129
+ </>
130
+ )}
131
+ </div>
132
+ );
133
+
134
+ return (
135
+ <>
136
+ <DetailPanel
137
+ title={nameOf(q)}
138
+ subtitle={q.original_file_name && q.original_file_name !== nameOf(q) ? q.original_file_name : undefined}
139
+ badges={<StatusPill tone={info.tone}>{info.label}</StatusPill>}
140
+ actions={(
141
+ <>
142
+ <Button size="sm" variant="outline" disabled={!loaded} onClick={() => { setName(q.display_name ?? ""); setRenaming(true); }}><Pencil className="size-4" />Rename</Button>
143
+ <Button size="sm" variant="outline" loading={downloading} disabled={!canDownload(q.status)} title={canDownload(q.status) ? undefined : "The filled workbook is available once the questionnaire is completed or needs review."} onClick={() => void save()}><Download className="size-4" />Download filled workbook</Button>
144
+ <Button size="sm" variant="outline" onClick={() => setDeleting(true)}><Trash2 className="size-4" />Delete</Button>
145
+ </>
146
+ )}
147
+ onClose={onClose}
148
+ onExpand={onExpand}
149
+ onCollapse={onCollapse}
150
+ summary={(
151
+ <div className="grid gap-2 px-4 pt-3">
152
+ <p className="text-sm" role="status">{total != null ? `${answered ?? "-"} of ${plural(total, "question")} answered${pct != null ? ` (${pct}%)` : ""}.` : "Reading the file."}{live.escalated_questions ? ` ${live.escalated_questions} escalated.` : ""}</p>
153
+ {pct != null ? <div role="progressbar" aria-valuenow={pct} aria-valuemin={0} aria-valuemax={100} aria-label="Answered" className="bg-muted h-1.5 overflow-hidden rounded-full"><div className="bg-foreground h-full" style={{ width: `${pct}%` }} /></div> : null}
154
+ {q.status === "failed" && (live.error_message || q.error_message) ? <p role="alert" className="text-error text-sm">{live.error_message || q.error_message}</p> : null}
155
+ {poll.error ? <ApiError error={poll.error} title="Could not check progress" compact onRetry={poll.start} /> : null}
156
+ {downloadError ? <ApiError error={downloadError} title="Could not download the workbook" compact /> : null}
157
+ </div>
158
+ )}
159
+ tabs={[
160
+ { id: "review", label: "Review", content: review, count: loaded ? all.length : undefined },
161
+ { id: "details", label: "Details", content: (
162
+ <KeyValueList columns={2} items={[
163
+ { label: "Name", value: q.display_name },
164
+ { label: "File", value: q.original_file_name },
165
+ { label: "File type", value: q.original_file_type ? q.original_file_type.toUpperCase() : "" },
166
+ { label: "Status", value: info.label },
167
+ { label: "Questions", value: total != null ? String(total) : "" },
168
+ { label: "Answered", value: answered != null ? String(answered) : "" },
169
+ { label: "Escalated", value: q.escalated_questions != null ? String(q.escalated_questions) : "" },
170
+ { label: "Uploaded", value: when(q.created_at) },
171
+ { label: "Completed", value: when(q.completed_at) },
172
+ { label: "Questionnaire id", value: q.id, mono: true },
173
+ ]} />
174
+ ) },
175
+ ]}
176
+ />
177
+ <FormSheet open={renaming} onOpenChange={setRenaming} title="Rename questionnaire" description="The name is only a label. No answer changes.">
178
+ <form className="grid gap-3" onSubmit={(e) => { e.preventDefault(); void rename.mutate(name); }}>
179
+ <div><Label htmlFor="g8-qd-name" className="mb-1">Name</Label><Input id="g8-qd-name" value={name} onChange={(e) => setName(e.target.value)} placeholder={q.original_file_name ?? "Questionnaire"} /><p className="text-muted-foreground mt-1 text-xs">Left blank, the file name is shown.</p></div>
180
+ {rename.error ? <ApiError error={rename.error} title="Could not rename the questionnaire" /> : null}
181
+ <div className="flex gap-2"><Button type="submit" loading={rename.loading} disabled={name.trim() === (q.display_name ?? "")}>Save name</Button><Button type="button" variant="outline" onClick={() => setRenaming(false)}>Cancel</Button></div>
182
+ </form>
183
+ </FormSheet>
184
+ <ConfirmSpendDialog
185
+ open={deleting}
186
+ tier="destructive"
187
+ title={`Delete ${nameOf(q)}?`}
188
+ affected={1}
189
+ description={<><p>The questionnaire and {total != null ? plural(total, "question") : "every question"} with their answers are removed. This cannot be undone. Answers you saved to the answer library stay there.</p>{remove.error ? <ApiError error={remove.error} title="Could not delete the questionnaire" /> : null}</>}
190
+ confirmLabel="Delete"
191
+ confirming={remove.loading}
192
+ onCancel={() => setDeleting(false)}
193
+ onConfirm={() => void remove.mutate(undefined)}
194
+ />
195
+ </>
196
+ );
197
+ }
@@ -0,0 +1,173 @@
1
+ "use client";
2
+
3
+ /**
4
+ * QuestionnairesPanel: an account's security questionnaires as a finished page: the list, upload a new one, and the
5
+ * selected questionnaire's review (QuestionnaireDetail).
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. listSecurityQuestionnaires (GET /accounts/{account_id}/security-questionnaires) read the account's questionnaires, newest first, paged 50 at a time with Load more
9
+ * 2. uploadSecurityQuestionnaire (POST /accounts/{account_id}/security-questionnaires) BILLABLE uploads an XLSX or PDF and starts answering it in the background; behind ConfirmSpendDialog
10
+ * Everything on one questionnaire (read, progress, review actions, workbook download, rename, delete) is QuestionnaireDetail.
11
+ * Answering uses credits, but the API publishes no price for it, so the confirmation names the file and says so instead of
12
+ * showing a made-up estimate. Pass `accountId` (a company id); without one the panel asks for an account.
13
+ * URL: `?qid=` the open questionnaire, `&view=full` as a page.
14
+ */
15
+
16
+ import { useState } from "react";
17
+ import type { ReactNode } from "react";
18
+ import { FileSpreadsheet, Upload } from "lucide-react";
19
+ import { Button, Label, Skeleton } from "@graph8/sdk/ui";
20
+ import { useBrand } from "@/g8/hooks/use-brand";
21
+ import { ApiError } from "@/g8/components/api-error";
22
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
23
+ import { EmptyState } from "@/g8/components/empty-state";
24
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
25
+ import { SplitView } from "@/g8/components/screen/split-view";
26
+ import { StatStrip } from "@/g8/components/screen/stat-strip";
27
+ import { StatusPill } from "@/g8/components/screen/status-pill";
28
+ import { Toolbar } from "@/g8/components/screen/toolbar";
29
+ import { LoadMore } from "@/g8/components/paging-notices";
30
+ import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
31
+ import { useUrlState } from "@/g8/hooks/use-url-state";
32
+ import { byOffset } from "@/g8/lib/paging";
33
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
34
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
35
+ import { plural } from "@/g8/lib/plural";
36
+ import { DEFAULT_MAX_BYTES, QUESTIONNAIRE_EXTENSIONS, QUESTIONNAIRE_STATUS, checkFiles, fileLimit, fmtSize, rowsOf, statusInfo, totalOf, when } from "../lib/documents-types";
37
+ import type { Questionnaire, Rec } from "../lib/documents-types";
38
+ import { QuestionnaireDetail } from "./questionnaire-detail";
39
+
40
+ export interface QuestionnairesPanelProps {
41
+ /** The account (company id) whose questionnaires to show. */
42
+ accountId: number | null;
43
+ renderRowActions?: (q: Questionnaire) => ReactNode;
44
+ toolbarActions?: ReactNode;
45
+ /** Largest upload in bytes. Default 5 MiB, the /api/g8 proxy's body limit. */
46
+ maxBytes?: number;
47
+ }
48
+
49
+ const PAGE = 50;
50
+ const SELECT = "border-input bg-background h-9 rounded-md border px-2 text-sm";
51
+ const nameOf = (q: Questionnaire): string => q.display_name || q.original_file_name || "Untitled questionnaire";
52
+
53
+ export function QuestionnairesPanel({ accountId, renderRowActions, toolbarActions, maxBytes = DEFAULT_MAX_BYTES }: QuestionnairesPanelProps) {
54
+ const brand = useBrand();
55
+ const [qid, setQid] = useUrlState("qid", "");
56
+ const [view, setView] = useUrlState("view", "");
57
+ const [search, setSearch] = useState("");
58
+ const [status, setStatus] = useState("");
59
+ const [uploading, setUploading] = useState(false);
60
+ const [file, setFile] = useState<File | null>(null);
61
+ const [problems, setProblems] = useState<string[]>([]);
62
+ const [confirming, setConfirming] = useState(false);
63
+
64
+ const list = useLoadMoreList<Questionnaire>(
65
+ byOffset(PAGE, async (offset, signal) => {
66
+ if (accountId == null) return { rows: [], hasNext: false };
67
+ const env = await g8Get<Rec>(`/accounts/${encodeURIComponent(String(accountId))}/security-questionnaires`, { status: status || undefined, limit: PAGE, offset }, { signal });
68
+ const rows = rowsOf<Questionnaire>(env.data);
69
+ const total = totalOf(env.data);
70
+ return { rows, hasNext: total != null ? offset + rows.length < total : rows.length >= PAGE };
71
+ }),
72
+ [accountId, status],
73
+ );
74
+
75
+ const send = useG8Mutation(
76
+ async (f: File): Promise<Questionnaire> => {
77
+ const form = new FormData();
78
+ form.append("file", f);
79
+ return (await g8Post<Questionnaire>(`/accounts/${encodeURIComponent(String(accountId))}/security-questionnaires`, form)).data;
80
+ },
81
+ {
82
+ onSuccess: (created) => { setConfirming(false); setUploading(false); setFile(null); void list.refetch(); setQid(created.id); },
83
+ invalidates: [["documents", "questionnaire"]],
84
+ },
85
+ );
86
+
87
+ const rows = list.data ?? [];
88
+ const q = search.trim().toLowerCase();
89
+ const shown = rows.filter((r) => !q || `${nameOf(r)} ${r.original_file_name ?? ""}`.toLowerCase().includes(q));
90
+ const selected = rows.find((r) => r.id === qid) ?? null;
91
+ const closeDetail = (): void => { setQid(null); setView(null); };
92
+ const count = (s: string): string => (list.data ? `${rows.filter((r) => r.status === s).length.toLocaleString()}${list.hasMore ? "+" : ""}` : "...");
93
+
94
+ if (accountId == null) {
95
+ return <EmptyState icon={<FileSpreadsheet className="size-8" />} title="Choose an account" description="Pick a company above to see the security questionnaires it sent you, or to upload a new one." />;
96
+ }
97
+
98
+ const body = list.error ? <ApiError error={list.error} title="Could not load questionnaires" onRetry={() => void list.refetch()} /> : list.loading && !list.data ? (
99
+ <div aria-busy="true" className="grid gap-2"><Skeleton className="h-16 w-full" /><Skeleton className="h-16 w-full" /></div>
100
+ ) : shown.length === 0 ? (
101
+ <EmptyState icon={<FileSpreadsheet className="size-8" />} title={rows.length === 0 && !status ? "No questionnaires yet" : "No questionnaires match"} description={rows.length === 0 && !status ? `Upload the spreadsheet or PDF a buyer sent. ${brand.name} answers it from your answer library and policy documents, and you review the answers here.` : "Try another search or status."} action={rows.length === 0 && !status ? <Button onClick={() => setUploading(true)}>Upload a questionnaire</Button> : undefined} />
102
+ ) : (
103
+ <>
104
+ <ul className="grid gap-2">
105
+ {shown.map((r) => {
106
+ const info = statusInfo(r.status);
107
+ return (
108
+ <li key={r.id} className={`flex flex-wrap items-center justify-between gap-2 rounded-md border p-3 ${r.id === qid ? "bg-muted" : ""}`}>
109
+ <button type="button" className="min-w-0 flex-1 text-left" onClick={() => setQid(r.id)}>
110
+ <span className="block truncate font-medium">{nameOf(r)}</span>
111
+ <span className="text-muted-foreground block truncate text-xs">{r.total_questions != null ? `${r.answered_questions ?? "-"} of ${plural(r.total_questions, "question")} answered` : "Questions not counted yet"} · {when(r.created_at) || "-"}</span>
112
+ </button>
113
+ <StatusPill tone={info.tone}>{info.label}</StatusPill>
114
+ {renderRowActions?.(r)}
115
+ </li>
116
+ );
117
+ })}
118
+ </ul>
119
+ <LoadMore shown={rows.length} noun="questionnaires" hasMore={list.hasMore} loading={list.loadingMore} onLoadMore={list.loadMore} />
120
+ </>
121
+ );
122
+
123
+ const chosen = file ? checkFiles([file], QUESTIONNAIRE_EXTENSIONS, fileLimit(maxBytes)) : null;
124
+ return (
125
+ <div className="grid gap-4">
126
+ <StatStrip items={[
127
+ { label: "Questionnaires", value: list.data ? `${rows.length.toLocaleString()}${list.hasMore ? "+" : ""}` : "..." },
128
+ { label: "Needs review", value: count("needs_review"), tone: "warning" },
129
+ { label: "Answering", value: count("processing") },
130
+ { label: "Completed", value: count("completed"), tone: "positive" },
131
+ ]} />
132
+ <SplitView
133
+ list={(
134
+ <div className="grid gap-3">
135
+ <Toolbar
136
+ search={{ value: search, onChange: setSearch, placeholder: "Search questionnaires" }}
137
+ filters={<select aria-label="Filter by status" className={SELECT} value={status} onChange={(e) => { setStatus(e.target.value); closeDetail(); }}><option value="">All statuses</option>{Object.entries(QUESTIONNAIRE_STATUS).map(([v, s]) => <option key={v} value={v}>{s.label}</option>)}</select>}
138
+ actions={<>{toolbarActions}<Button size="sm" onClick={() => setUploading(true)}><Upload className="size-4" />Upload questionnaire</Button></>}
139
+ />
140
+ {body}
141
+ </div>
142
+ )}
143
+ detail={selected ? <QuestionnaireDetail key={selected.id} accountId={accountId} questionnaire={selected} onChanged={() => void list.refetch()} onClose={closeDetail} onDeleted={closeDetail} onExpand={() => setView("full")} onCollapse={view === "full" ? () => setView(null) : undefined} /> : null}
144
+ detailOpen={Boolean(selected)}
145
+ onCloseDetail={closeDetail}
146
+ expanded={view === "full"}
147
+ onExpandedChange={(e) => setView(e ? "full" : null)}
148
+ detailWidth="xl"
149
+ />
150
+ <FormSheet open={uploading} onOpenChange={(o) => { setUploading(o); if (!o) { setFile(null); setProblems([]); } }} title="Upload a questionnaire" description={`${brand.name} reads the questions and answers each one in the background. Nothing is sent to the buyer.`}>
151
+ <form className="grid gap-3" onSubmit={(e) => { e.preventDefault(); if (chosen && chosen.ok.length === 1) setConfirming(true); }}>
152
+ <div>
153
+ <Label htmlFor="g8-qp-file" className="mb-1">Questionnaire file</Label>
154
+ <input id="g8-qp-file" type="file" accept={QUESTIONNAIRE_EXTENSIONS.map((x) => `.${x}`).join(",")} className="border-input bg-background w-full rounded-md border p-2 text-sm" onChange={(e) => { const f = e.target.files?.[0] ?? null; setFile(f); setProblems(f ? checkFiles([f], QUESTIONNAIRE_EXTENSIONS, fileLimit(maxBytes)).problems : []); }} />
155
+ <p className="text-muted-foreground mt-1 text-xs">{QUESTIONNAIRE_EXTENSIONS.join(" or ")}, up to {fmtSize(fileLimit(maxBytes))}. A spreadsheet keeps its layout; a PDF is answered into a new spreadsheet.</p>
156
+ </div>
157
+ {problems.length > 0 ? <ul role="alert" className="text-error list-disc pl-5 text-xs">{problems.map((p) => <li key={p}>{p}</li>)}</ul> : null}
158
+ <div className="flex gap-2"><Button type="submit" disabled={!chosen || chosen.ok.length !== 1}>Upload and answer</Button><Button type="button" variant="outline" onClick={() => setUploading(false)}>Cancel</Button></div>
159
+ </form>
160
+ </FormSheet>
161
+ <ConfirmSpendDialog
162
+ open={confirming && file !== null}
163
+ tier="billable"
164
+ title={`Upload and answer ${file?.name ?? "this file"}?`}
165
+ description={<><p>{brand.name} reads every question in the file and writes an answer to each from your answer library and policy documents. Answering uses credits. The API does not publish a price for it, so no estimate is shown here.</p>{send.error ? <ApiError error={send.error} title="Could not upload the questionnaire" /> : null}</>}
166
+ confirmLabel="Upload and answer"
167
+ confirming={send.loading}
168
+ onCancel={() => setConfirming(false)}
169
+ onConfirm={() => { if (file) void send.mutate(file); }}
170
+ />
171
+ </div>
172
+ );
173
+ }
@@ -0,0 +1,109 @@
1
+ "use client";
2
+
3
+ /**
4
+ * RecordDocumentsTab: the paperwork behind one account, on a company, contact or deal page. Use it as an `extraDetailTabs`
5
+ * entry of CompaniesScreen / ContactsScreen / DealsScreen:
6
+ * { id: "documents", label: "Documents", render: (c) => <RecordDocumentsTab companyId={c.id} documentsPath="/documents" /> }
7
+ * { id: "documents", label: "Documents", render: (c) => <RecordDocumentsTab contactId={c.id} /> } (a contact: their company's paperwork, messages with them first)
8
+ * { id: "documents", label: "Documents", render: (d) => <RecordDocumentsTab contactId={d.primary_contact?.id} scope="account" /> } (a deal: the primary contact finds the company; all its messages show)
9
+ *
10
+ * SDK operations, in order:
11
+ * 1. getContact (GET /contacts/{contact_id}) read only when only a contactId is given: finds the contact's company
12
+ * 2. listAccountCorrespondence (GET /accounts/{account_id}/correspondence) read the latest messages (with that person only when a contactId is given)
13
+ * 3. listSecurityQuestionnaires (GET /accounts/{account_id}/security-questionnaires) read the latest questionnaires for the account
14
+ * Both lists are the newest five; "Open in documents" goes to the full screen with the account chosen. Read only: recording,
15
+ * uploading and reviewing happen in DocumentsScreen. A contact with no company has no account to read, and the tab says so.
16
+ */
17
+
18
+ import { Mail } from "lucide-react";
19
+ import { Skeleton } from "@graph8/sdk/ui";
20
+ import { ApiError } from "@/g8/components/api-error";
21
+ import { EmptyState } from "@/g8/components/empty-state";
22
+ import { SectionCard } from "@/g8/components/screen/section-card";
23
+ import { StatusPill } from "@/g8/components/screen/status-pill";
24
+ import { g8Get } from "@/g8/lib/g8-client";
25
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
26
+ import { plural } from "@/g8/lib/plural";
27
+ import { CORRESPONDENCE_CHANNELS, rowsOf, statusInfo, totalOf, when } from "../lib/documents-types";
28
+ import type { Correspondence, Questionnaire, Rec } from "../lib/documents-types";
29
+
30
+ export interface RecordDocumentsTabProps {
31
+ /** The company (account) id. */
32
+ companyId?: number | null;
33
+ /** A contact: narrows the messages to this person and, without a companyId, finds the company. */
34
+ contactId?: number | null;
35
+ /** "person" (default with a contactId) shows only the messages with that contact; "account" shows the whole account's, using the contact only to find the company (a deal page). */
36
+ scope?: "person" | "account";
37
+ /** Path of your documents screen for the links. Default "/documents". */
38
+ documentsPath?: string;
39
+ /** Route to the documents screen yourself (for example with the router) instead of a plain link. */
40
+ onOpenDocuments?: (href: string) => void;
41
+ }
42
+
43
+ const LIMIT = 5;
44
+
45
+ export function RecordDocumentsTab({ companyId, contactId, scope = "person", documentsPath = "/documents", onOpenDocuments }: RecordDocumentsTabProps) {
46
+ const personal = contactId != null && scope !== "account";
47
+ const needContact = companyId == null && contactId != null;
48
+ const contact = useG8Query((signal) => g8Get<{ company_id?: number | null }>(`/contacts/${contactId}`, {}, { signal }).then((r) => r.data), [contactId], { enabled: needContact, key: ["documents", "contact-company", contactId ?? ""] });
49
+ const account = companyId ?? contact.data?.company_id ?? null;
50
+ const base = `/accounts/${account}`;
51
+ const mail = useG8Query(
52
+ (signal) => g8Get<Rec>(`${base}/correspondence`, { stakeholder_id: personal ? String(contactId) : undefined, limit: LIMIT }, { signal }).then((r) => ({ rows: rowsOf<Correspondence>(r.data), total: totalOf(r.data) })),
53
+ [account, personal ? contactId : null],
54
+ { enabled: account != null, key: ["documents", "correspondence", "tab", account ?? "", personal ? contactId ?? "" : ""] },
55
+ );
56
+ const forms = useG8Query(
57
+ (signal) => g8Get<Rec>(`${base}/security-questionnaires`, { limit: LIMIT }, { signal }).then((r) => ({ rows: rowsOf<Questionnaire>(r.data), total: totalOf(r.data) })),
58
+ [account],
59
+ { enabled: account != null, key: ["documents", "questionnaire", "tab", account ?? ""] },
60
+ );
61
+
62
+ if (companyId == null && contactId == null) return <EmptyState compact icon={<Mail className="size-8" />} title="No account to show" description="Documents are kept per company. Add a contact or a company to this record to see them." />;
63
+ if (needContact && contact.error) return <ApiError error={contact.error} title="Could not find this person's company" onRetry={() => void contact.refetch()} />;
64
+ if (needContact && contact.loading && !contact.data) return <Skeleton className="h-24 w-full" aria-label="Loading documents" />;
65
+ if (account == null) return <EmptyState compact icon={<Mail className="size-8" />} title="No company on this contact" description="Messages and questionnaires are kept per company, so there is nothing to show until this person has one." />;
66
+
67
+ const href = (tab: string): string => `${documentsPath}?tab=${tab}&account=${account}`;
68
+ const link = (tab: string, text: string) => (
69
+ <a href={href(tab)} className="text-sm underline" onClick={(e) => { if (!onOpenDocuments || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; e.preventDefault(); onOpenDocuments(href(tab)); }}>{text}</a>
70
+ );
71
+
72
+ return (
73
+ <div className="grid gap-4">
74
+ <SectionCard title={personal ? "Messages with this person" : "Correspondence"} actions={link("correspondence", "Open in documents")}>
75
+ {mail.error ? <ApiError error={mail.error} title="Could not load messages" compact onRetry={() => void mail.refetch()} /> : !mail.data ? <Skeleton className="h-16 w-full" aria-label="Loading messages" /> : mail.data.rows.length === 0 ? (
76
+ <EmptyState compact title="No messages recorded" description="Emails, LinkedIn messages and call notes recorded for this account show up here." />
77
+ ) : (
78
+ <ul className="divide-y rounded-md border" aria-label="Messages">
79
+ {mail.data.rows.map((m) => (
80
+ <li key={m.id} className="flex items-center justify-between gap-3 p-3 text-sm">
81
+ <span className="min-w-0"><span className="block truncate font-medium">{m.subject || (m.content ? m.content.slice(0, 80) : "No subject")}</span><span className="text-muted-foreground block text-xs">{CORRESPONDENCE_CHANNELS.find((c) => c.value === m.channel)?.label ?? m.channel ?? "-"} · {when(m.correspondence_date) || "-"}</span></span>
82
+ {m.direction ? <StatusPill tone={m.direction === "inbound" ? "info" : "neutral"}>{m.direction === "inbound" ? "Inbound" : "Outbound"}</StatusPill> : null}
83
+ </li>
84
+ ))}
85
+ </ul>
86
+ )}
87
+ {mail.data && mail.data.total != null && mail.data.total > mail.data.rows.length ? <p className="text-muted-foreground mt-2 text-xs">Showing the newest {mail.data.rows.length} of {plural(mail.data.total, "message")}.</p> : null}
88
+ </SectionCard>
89
+ <SectionCard title="Security questionnaires" description="Kept for the company, not the person." actions={link("questionnaires", "Open in documents")}>
90
+ {forms.error ? <ApiError error={forms.error} title="Could not load questionnaires" compact onRetry={() => void forms.refetch()} /> : !forms.data ? <Skeleton className="h-16 w-full" aria-label="Loading questionnaires" /> : forms.data.rows.length === 0 ? (
91
+ <EmptyState compact title="No questionnaires" description="Questionnaires uploaded for this account show up here." />
92
+ ) : (
93
+ <ul className="divide-y rounded-md border" aria-label="Questionnaires">
94
+ {forms.data.rows.map((f) => {
95
+ const info = statusInfo(f.status);
96
+ return (
97
+ <li key={f.id} className="flex items-center justify-between gap-3 p-3 text-sm">
98
+ <span className="min-w-0"><span className="block truncate font-medium">{f.display_name || f.original_file_name || "Untitled questionnaire"}</span><span className="text-muted-foreground block text-xs">{f.total_questions != null ? `${f.answered_questions ?? "-"} of ${plural(f.total_questions, "question")} answered` : "-"}</span></span>
99
+ <StatusPill tone={info.tone}>{info.label}</StatusPill>
100
+ </li>
101
+ );
102
+ })}
103
+ </ul>
104
+ )}
105
+ {forms.data && forms.data.total != null && forms.data.total > forms.data.rows.length ? <p className="text-muted-foreground mt-2 text-xs">Showing the newest {forms.data.rows.length} of {plural(forms.data.total, "questionnaire")}.</p> : null}
106
+ </SectionCard>
107
+ </div>
108
+ );
109
+ }