@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
@@ -23,6 +23,7 @@ import { SplitView } from "@/g8/components/screen/split-view";
23
23
  import { StatusPill } from "@/g8/components/screen/status-pill";
24
24
  import { Toolbar } from "@/g8/components/screen/toolbar";
25
25
  import { useG8Query } from "@/g8/hooks/use-g8-query";
26
+ import { useCurrentMember } from "@/g8/pickers/hooks/use-current-member";
26
27
  import { useUrlState } from "@/g8/hooks/use-url-state";
27
28
  import { g8Get } from "@/g8/lib/g8-client";
28
29
  import { dataOf, formatDuration, type CallSummary } from "../lib/dialer-types";
@@ -76,7 +77,9 @@ export function CallHistory({ contactId, userEmail, sessionId, limit = 25, idPar
76
77
  const [outcome, setOutcome] = useState("");
77
78
  const [openId, setOpenId] = useUrlState(idParam);
78
79
  const [view, setView] = useUrlState("view");
79
- const rep = useDebounced(userEmail || typedEmail.trim());
80
+ // Unscoped history defaults to YOUR calls (the signed-in member); typing another rep's email still overrides.
81
+ const me = useCurrentMember({ enabled: !userEmail && !contactId && !sessionId });
82
+ const rep = useDebounced(userEmail || typedEmail.trim() || me.email || "");
80
83
  const scoped = Boolean(contactId || rep || sessionId);
81
84
  const q = useG8Query(
82
85
  async (signal) => {
@@ -89,12 +92,17 @@ export function CallHistory({ contactId, userEmail, sessionId, limit = 25, idPar
89
92
  // A different rep, session or contact is a different list.
90
93
  const scope = `${contactId}|${rep}|${sessionId}|${limit}`;
91
94
  const lastScope = useRef(scope);
95
+ const wasScoped = useRef(scoped);
92
96
  useEffect(() => {
97
+ const was = wasScoped.current;
98
+ wasScoped.current = scoped;
93
99
  if (lastScope.current === scope) return; // keep a deep-linked ?id= on first render
94
100
  lastScope.current = scope;
95
101
  setRows(limit);
96
102
  setOutcome("");
97
- setOpenId(null);
103
+ // The default rep resolves AFTER the first render (GET /team-members/me). Going from "no list yet" to the rep's list
104
+ // is not a different list, so a deep-linked call (`/calls?id=<room>`, from a coach scorecard or a contact) stays open.
105
+ if (was) setOpenId(null);
98
106
  }, [scope]); // eslint-disable-line react-hooks/exhaustive-deps
99
107
 
100
108
  const all = q.data ?? [];
@@ -9,7 +9,7 @@
9
9
  * Org-wide history across all reps: use POST /voice/call-results/search (voice.searchCallResults) from a server recipe.
10
10
  */
11
11
  import { useState } from "react";
12
- import { Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
12
+ import { Button, Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
13
13
  import { ApiError } from "@/g8/components/api-error";
14
14
  import { EmptyState } from "@/g8/components/empty-state";
15
15
  import { g8Get } from "@/g8/lib/g8-client";
@@ -39,12 +39,16 @@ export function CallLog({ linkParam, contactId, userEmail, sessionId, limit = 25
39
39
  const [typedEmail, setTypedEmail] = useState("");
40
40
  const repEmail = userEmail || typedEmail.trim();
41
41
  const scoped = Boolean(contactId || repEmail || sessionId);
42
+ // The calls API pages by a growing `limit`: "Load more" asks for another page-worth and keeps the earlier calls.
43
+ const scope = `${contactId}|${repEmail}|${sessionId}|${limit}`;
44
+ const [size, setSize] = useState({ scope, rows: limit });
45
+ const rowsWanted = size.scope === scope ? size.rows : limit;
42
46
  const q = useG8Query(
43
47
  async (signal) => {
44
- const env = await g8Get("/voice/dialer/calls", { contact_id: contactId, user_email: repEmail || undefined, parallel_session_id: sessionId, limit }, { signal });
48
+ const env = await g8Get("/voice/dialer/calls", { contact_id: contactId, user_email: repEmail || undefined, parallel_session_id: sessionId, limit: rowsWanted }, { signal });
45
49
  return dataOf<{ calls?: CallSummary[] }>(env).calls ?? [];
46
50
  },
47
- [contactId, repEmail, sessionId, limit],
51
+ [contactId, repEmail, sessionId, rowsWanted],
48
52
  { enabled: scoped },
49
53
  );
50
54
 
@@ -62,7 +66,8 @@ export function CallLog({ linkParam, contactId, userEmail, sessionId, limit = 25
62
66
  if (calls.length === 0) return <EmptyState className={className} title="No calls yet" description="Calls appear here after the dialer places them." />;
63
67
 
64
68
  return (
65
- <Table className={className} aria-label="Call log">
69
+ <div className={className}>
70
+ <Table aria-label="Call log">
66
71
  <TableHeader>
67
72
  <TableRow><TableHead>When</TableHead><TableHead>Number</TableHead><TableHead>Outcome</TableHead><TableHead>Length</TableHead><TableHead>Summary</TableHead></TableRow>
68
73
  </TableHeader>
@@ -78,5 +83,12 @@ export function CallLog({ linkParam, contactId, userEmail, sessionId, limit = 25
78
83
  ))}
79
84
  </TableBody>
80
85
  </Table>
86
+ {calls.length >= rowsWanted ? (
87
+ <div className="flex items-center justify-between gap-3 py-2 text-sm text-muted-foreground">
88
+ <span role="status">Showing {calls.length.toLocaleString()} calls</span>
89
+ <Button size="sm" variant="outline" loading={q.loading} onClick={() => setSize({ scope, rows: rowsWanted + limit })}>Load more</Button>
90
+ </div>
91
+ ) : null}
92
+ </div>
81
93
  );
82
94
  }
@@ -50,7 +50,7 @@ function Stat({ label, value }: { label: string; value: string | number }) {
50
50
  return (
51
51
  <div className="rounded-lg border p-3">
52
52
  <div className="text-xs uppercase text-muted-foreground">{label}</div>
53
- <div className="text-lg font-semibold capitalize">{value}</div>
53
+ <div className="text-lg font-medium capitalize">{value}</div>
54
54
  </div>
55
55
  );
56
56
  }
@@ -71,7 +71,7 @@ export function WaitingContacts({ sessionId, version, onSkipped }: { sessionId:
71
71
  const rows = (grid.data ?? []).filter((r) => !r.callresult?.disposition);
72
72
  return (
73
73
  <div className="space-y-2">
74
- <h3 className="text-sm font-semibold">Up next</h3>
74
+ <h3 className="text-sm font-medium">Up next</h3>
75
75
  {skip.error ? <ApiError error={skip.error} title="Could not skip that contact" /> : null}
76
76
  <Table aria-label="Contacts waiting">
77
77
  <TableHeader>
@@ -141,7 +141,7 @@ export function CallQueue({ sessionId: initialId, userId, userEmail, listId, lis
141
141
  return (
142
142
  <div className={`space-y-4 ${className ?? ""}`}>
143
143
  <header className="flex flex-wrap items-center justify-between gap-2">
144
- <h2 className="text-base font-semibold">{session.detail?.name ?? "Dialing session"}</h2>
144
+ <h2 className="text-base font-medium">{session.detail?.name ?? "Dialing session"}</h2>
145
145
  <div className="flex gap-2">
146
146
  <Button disabled={!stats || over || nextBatch.loading} onClick={() => setConfirming(true)}>Dial next batch</Button>
147
147
  <Button variant="outline" disabled={!stats || over} onClick={() => void pause.mutate(undefined)}>Pause</Button>
@@ -22,11 +22,17 @@ import { Button, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader,
22
22
  import { ApiError } from "@/g8/components/api-error";
23
23
  import { KeyValueList } from "@/g8/components/screen/key-value-list";
24
24
  import { useG8Query } from "@/g8/hooks/use-g8-query";
25
+ import { useCurrentMember } from "@/g8/pickers/hooks/use-current-member";
26
+ import { MemberPicker } from "@/g8/pickers/components/member-picker";
27
+ import type { MemberOption } from "@/g8/pickers/components/member-picker";
25
28
  import { g8Get } from "@/g8/lib/g8-client";
26
29
  import { EmptyState } from "@/g8/components/empty-state";
27
30
  import { ScreenLayout } from "@/g8/components/screen/screen-layout";
28
31
  import { SectionCard } from "@/g8/components/screen/section-card";
29
32
  import { useUrlState } from "@/g8/hooks/use-url-state";
33
+ import { useUrlFilters } from "@/g8/hooks/use-url-filters";
34
+ import { ListSelect } from "@/g8/crm/components/list-select";
35
+ import { MoreFilters, RangeFilter, SelectFilter, TextFilter } from "@/g8/components/screen/filter-controls";
30
36
  import { ListPicker, type ListSummary } from "@/g8/lists/components/list-picker";
31
37
  import { SplitView } from "@/g8/components/screen/split-view";
32
38
  import { StatusPill } from "@/g8/components/screen/status-pill";
@@ -43,7 +49,10 @@ import { rowActivationProps } from "@/g8/hooks/use-row-activation";
43
49
  import { RecordLink } from "@/g8/components/screen/record-link";
44
50
 
45
51
  export interface DialerScreenProps {
46
- /** The rep these calls belong to. When omitted the start form asks for the graph8 user id. */
52
+ /**
53
+ * The rep these calls belong to. Optional: when omitted the rep is the signed-in team member (GET /team-members/me,
54
+ * a personal API key) and a MemberPicker switches rep. A passed `userId` always wins and hides the switcher.
55
+ */
47
56
  userId?: string;
48
57
  userEmail?: string;
49
58
  /** Skip the list picker. */
@@ -79,15 +88,6 @@ interface SessionRow {
79
88
 
80
89
  const STATUSES = ["ACTIVE", "PAUSED", "COMPLETED"];
81
90
 
82
- function useDebounced<T>(value: T, ms = 300): T {
83
- const [v, setV] = useState(value);
84
- useEffect(() => {
85
- const t = setTimeout(() => setV(value), ms);
86
- return () => clearTimeout(t);
87
- }, [value, ms]);
88
- return v;
89
- }
90
-
91
91
  function sessionTone(status?: string | null) {
92
92
  const s = normalizeStatus(status);
93
93
  return s === "active" ? "positive" : s === "paused" ? "warning" : "neutral";
@@ -99,33 +99,56 @@ function created(v: unknown): string {
99
99
  }
100
100
 
101
101
  /** All dialing sessions (GET /voice/dialer/sessions). A row opens the full session record: ?tab=sessions&open=<id>[&view=full]. */
102
+ /** Every filter the sessions API takes (user_email comes from the screen's `userEmail`). */
103
+ const SESSION_FILTERS = ["name", "status", "campaign_id", "list_id", "list_name", "date_from", "date_to", "sort_by", "sort_order"] as const;
104
+ const SESSION_MORE = ["campaign_id", "list_id", "list_name", "date_from", "date_to", "sort_by", "sort_order"] as const;
105
+
102
106
  function SessionsTab({ userEmail, onWork }: { userEmail?: string; onWork: (id: string) => void }) {
103
107
  // Its own param: `session` belongs to the Session tab (the running session); closing a detail here must never clear it.
104
108
  const [openId, setOpenId] = useUrlState("open");
105
109
  const [view, setView] = useUrlState("view");
106
110
  const [page, setPage] = useState(1);
107
- const [status, setStatus] = useState("");
108
- const [name, setName] = useState("");
109
- const search = useDebounced(name.trim());
111
+ // Every filter the sessions API takes lives in the URL (`s_campaign_id=...`): status, name, campaign, list, dates, sort.
112
+ const uf = useUrlFilters(SESSION_FILTERS, { prefix: "s_" });
113
+ const { status, name } = uf.draft;
114
+ const f = uf.filters;
115
+ const search = f.name.trim();
110
116
  const q = useG8Query(
111
117
  async (signal) => {
112
- const env = await g8Get("/voice/dialer/sessions", { page, page_size: 25, status: status || undefined, name: search || undefined, user_email: userEmail }, { signal });
118
+ const env = await g8Get("/voice/dialer/sessions", {
119
+ page, page_size: 25, status: f.status || undefined, name: search || undefined, user_email: userEmail,
120
+ campaign_id: f.campaign_id || undefined, list_id: f.list_id || undefined, list_name: f.list_name || undefined,
121
+ date_from: f.date_from ? `${f.date_from}T00:00:00Z` : undefined, date_to: f.date_to ? `${f.date_to}T23:59:59Z` : undefined,
122
+ sort_by: f.sort_by || undefined, sort_order: f.sort_by ? f.sort_order || undefined : undefined,
123
+ }, { signal });
113
124
  const d = dataOf<{ sessions?: SessionRow[]; pagination?: { total_items?: number; total_pages?: number } }>(env);
114
125
  return { rows: d.sessions ?? [], total: d.pagination?.total_items, pages: d.pagination?.total_pages };
115
126
  },
116
- [page, status, search, userEmail],
127
+ [page, f, search, userEmail],
128
+ { enabled: uf.ready },
117
129
  );
118
130
  const rows = q.data?.rows ?? [];
119
- const reset = () => setPage(1);
131
+ useEffect(() => { setPage(1); }, [f]);
120
132
  const list = (
121
133
  <div className="space-y-3">
122
134
  <Toolbar
123
- search={{ value: name, onChange: (v) => { setName(v); reset(); }, placeholder: "Search by session name" }}
135
+ search={{ value: name, onChange: (v) => uf.set("name", v), debounceMs: 0, placeholder: "Search by session name" }}
124
136
  filters={
125
- <select aria-label="Session status" className="h-8 rounded-md border border-border bg-background px-2 text-sm" value={status} onChange={(e) => { setStatus(e.target.value); reset(); }}>
126
- <option value="">Any status</option>
127
- {STATUSES.map((s) => <option key={s} value={s}>{s.toLowerCase()}</option>)}
128
- </select>
137
+ <>
138
+ <select aria-label="Session status" className="h-8 rounded-md border border-border bg-background px-2 text-sm" value={status} onChange={(e) => uf.set("status", e.target.value)}>
139
+ <option value="">Any status</option>
140
+ {STATUSES.map((s) => <option key={s} value={s}>{s.toLowerCase()}</option>)}
141
+ </select>
142
+ <MoreFilters activeCount={SESSION_MORE.filter((k) => uf.draft[k]).length}>
143
+ <TextFilter label="campaign" placeholder="Campaign ID" value={uf.draft.campaign_id} onChange={(v) => uf.set("campaign_id", v)} className="w-40" />
144
+ <ListSelect label="Filter by list" emptyOption="Any list" value={uf.draft.list_id} valueLabel={`List ${uf.draft.list_id}`} pageSize={100} onChange={(l) => uf.set("list_id", l ? String(l.id) : "")} />
145
+ <TextFilter label="list name" placeholder="List name" value={uf.draft.list_name} onChange={(v) => uf.set("list_name", v)} className="w-40" />
146
+ <RangeFilter label="Created" type="date" min={uf.draft.date_from} max={uf.draft.date_to} onMinChange={(v) => uf.set("date_from", v)} onMaxChange={(v) => uf.set("date_to", v)} />
147
+ <SelectFilter label="sort" any="Newest first" value={uf.draft.sort_by} onChange={(v) => uf.set("sort_by", v)} options={[{ value: "created_at", label: "Created" }, { value: "updated_at", label: "Last updated" }, { value: "total_calls", label: "Calls" }]} />
148
+ <SelectFilter label="sort order" any="Descending" value={uf.draft.sort_order} onChange={(v) => uf.set("sort_order", v)} options={[{ value: "asc", label: "Ascending" }]} />
149
+ </MoreFilters>
150
+ {uf.active ? <Button variant="ghost" size="sm" onClick={uf.clear}>Clear filters</Button> : null}
151
+ </>
129
152
  }
130
153
  />
131
154
  {q.error ? (
@@ -133,7 +156,7 @@ function SessionsTab({ userEmail, onWork }: { userEmail?: string; onWork: (id: s
133
156
  ) : q.loading && !q.data ? (
134
157
  <Skeleton className="h-32 w-full" aria-label="Loading sessions" />
135
158
  ) : rows.length === 0 ? (
136
- <EmptyState icon={<Phone className="size-6" />} title={name || status ? "No sessions match" : "No sessions yet"} description={name || status ? "Try a different search or status." : "Start one in the Session tab. Each session keeps its queue, calls and outcomes."} />
159
+ <EmptyState icon={<Phone className="size-6" />} title={uf.active ? "No sessions match" : "No sessions yet"} description={uf.active ? "Try a different search or fewer filters." : "Start one in the Session tab. Each session keeps its queue, calls and outcomes."} />
137
160
  ) : (
138
161
  <Table aria-label="Dialing sessions">
139
162
  <TableHeader><TableRow><TableHead>Session</TableHead><TableHead>Status</TableHead><TableHead>Calls</TableHead><TableHead>Rep</TableHead><TableHead>Started</TableHead></TableRow></TableHeader>
@@ -256,11 +279,11 @@ function SessionTab(props: DialerScreenProps & { onCalls: (calls: LiveCall[]) =>
256
279
  />
257
280
  <div className="space-y-4">
258
281
  {props.userId && transport ? (
259
- <Softphone userId={props.userId} transport={transport} conferenceName={batch?.conference_name} joinMuted={(batch?.batch?.length ?? 1) > 1} />
282
+ <Softphone key={props.userId} userId={props.userId} transport={transport} conferenceName={batch?.conference_name} joinMuted={(batch?.batch?.length ?? 1) > 1} />
260
283
  ) : props.userId ? (
261
284
  <Skeleton className="h-24 w-full" aria-label="Loading softphone" />
262
285
  ) : (
263
- <SectionCard title="Softphone"><p className="text-sm text-muted-foreground">Pass the rep's user id to take calls in this browser.</p></SectionCard>
286
+ <SectionCard title="Softphone"><p className="text-sm text-muted-foreground">Choose the rep above to take calls in this browser. A personal API key is the rep automatically; an organisation key is not a person.</p></SectionCard>
264
287
  )}
265
288
  {sessionId ? (
266
289
  <>
@@ -282,11 +305,30 @@ function SessionTab(props: DialerScreenProps & { onCalls: (calls: LiveCall[]) =>
282
305
  );
283
306
  }
284
307
 
308
+ /** Who is calling, shown with a switcher when the rep was not passed in. Managers can pick a teammate; a personal key can only be itself. */
309
+ function RepSwitcher({ me, picked, onPick }: { me: ReturnType<typeof useCurrentMember>; picked: MemberOption | null; onPick: (m: MemberOption | null) => void }) {
310
+ const current = picked ?? me.member;
311
+ return (
312
+ <div className="flex flex-wrap items-center gap-2 text-sm" aria-label="Rep">
313
+ {me.loading ? <span className="text-muted-foreground">Finding your user...</span> : null}
314
+ {current ? <span>Calling as {current.name}{current.id === me.userId ? " (you)" : ""}</span> : null}
315
+ <MemberPicker label={current ? "Switch rep" : "Rep"} value={current?.id ?? null} onChange={onPick} placeholder="Choose who is calling" />
316
+ {picked && me.member && picked.id !== me.userId ? <Button size="sm" variant="ghost" onClick={() => onPick(null)}>Use me</Button> : null}
317
+ </div>
318
+ );
319
+ }
320
+
285
321
  export function DialerScreen(props: DialerScreenProps) {
286
322
  const [tab, setTab] = useUrlState("tab", "session");
287
323
  const [, setView] = useUrlState("view");
288
324
  const [, setSession] = useUrlState("session");
289
325
  const [calls, setCalls] = useState<LiveCall[]>([]);
326
+ // The rep: a passed userId wins (BC). Otherwise the signed-in team member, or a teammate picked in the switcher.
327
+ const me = useCurrentMember({ enabled: !props.userId });
328
+ const [picked, setPicked] = useState<MemberOption | null>(null);
329
+ const rep = props.userId ? null : (picked ?? me.member);
330
+ const userId = props.userId ?? rep?.id;
331
+ const userEmail = props.userId ? props.userEmail : (rep ? (rep.email ?? undefined) : props.userEmail);
290
332
  const active = TABS.some((t) => t.id === tab) ? tab : "session";
291
333
  // The header counters describe the Session tab's calls; drop them when it unmounts so they are never stale.
292
334
  useEffect(() => { if (active !== "session") setCalls([]); }, [active]);
@@ -305,15 +347,15 @@ export function DialerScreen(props: DialerScreenProps) {
305
347
  title="Dialer"
306
348
  description="Call a list batch by batch, take calls in the browser and record every outcome."
307
349
  icon={<Phone className="size-5" />}
308
- actions={props.toolbarActions}
350
+ actions={props.userId ? props.toolbarActions : <><RepSwitcher me={me} picked={picked} onPick={setPicked} />{props.toolbarActions}</>}
309
351
  tabs={TABS}
310
352
  activeTab={active}
311
353
  onTabChange={(id) => setTab(id)}
312
354
  stats={stats}
313
355
  >
314
- {active === "session" ? <SessionTab {...props} onCalls={setCalls} /> : null}
356
+ {active === "session" ? <SessionTab {...props} userId={userId} userEmail={userEmail} onCalls={setCalls} /> : null}
315
357
  {active === "sessions" ? <SessionsTab userEmail={props.userEmail} onWork={(id) => { setSession(id); setView(null); setTab("session"); }} /> : null}
316
- {active === "history" ? <CallHistory userEmail={props.userEmail} /> : null}
358
+ {active === "history" ? <CallHistory userEmail={props.userEmail ?? userEmail} /> : null}
317
359
  </ScreenLayout>
318
360
  );
319
361
  }
@@ -54,7 +54,7 @@ export function DispositionPanel({ call, onSaved, className }: DispositionPanelP
54
54
  return (
55
55
  <section className={`space-y-3 rounded-lg border p-4 ${className ?? ""}`} aria-label="Call outcome">
56
56
  <header>
57
- <h3 className="text-sm font-semibold">How did the call with {contactName(call)} go?</h3>
57
+ <h3 className="text-sm font-medium">How did the call with {contactName(call)} go?</h3>
58
58
  {call.disposition ? <p className="text-xs text-muted-foreground">Currently: {call.disposition}</p> : null}
59
59
  </header>
60
60
  <div role="radiogroup" aria-label="Outcome" className="flex flex-wrap gap-2">
@@ -0,0 +1,101 @@
1
+ "use client";
2
+
3
+ /**
4
+ * RecordCallsTab: one contact's call history, newest first, with a "Call" button that places one confirmed call through the
5
+ * dialer. Use it as an `extraDetailTabs` entry of ContactsScreen:
6
+ * { id: "calls", label: "Calls", render: (c) => <RecordCallsTab contact={{ id: c.id, first_name: c.first_name, last_name: c.last_name, email: c.work_email, mobile_phone: c.mobile_phone, direct_phone: c.direct_phone }} onOpenCall={(room) => router.push(`/calls?id=${room}`)} /> }
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. listCallsForContact GET /voice/dialer/calls?contact_id=&limit= (read; 25 at first, "Show more" asks for up to 100, the API maximum)
10
+ * 2. CallContactButton: getMyTeamMember, listDialerSessions, listDialerNumbers, manualDial (external: confirmed in its dialog)
11
+ * The API has no offset, so past 100 calls the tab says so instead of cutting the list silently.
12
+ */
13
+
14
+ import { useState } from "react";
15
+ import { Button, Skeleton } from "@graph8/sdk/ui";
16
+ import { PhoneCall } from "lucide-react";
17
+ import { ApiError } from "@/g8/components/api-error";
18
+ import { EmptyState } from "@/g8/components/empty-state";
19
+ import { StatusPill } from "@/g8/components/screen/status-pill";
20
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
21
+ import { g8Get } from "@/g8/lib/g8-client";
22
+ import { CallContactButton } from "./call-contact-button";
23
+ import type { CallableContact } from "./call-contact-button";
24
+ import { dataOf, formatDuration, type CallSummary } from "../lib/dialer-types";
25
+ import { outcomeTone } from "./call-log-screen";
26
+
27
+ /** The most calls one read can return. */
28
+ export const CALLS_TAB_API_MAX = 100;
29
+
30
+ function when(iso?: string | null): string {
31
+ const d = iso ? new Date(iso) : null;
32
+ return d && !Number.isNaN(d.getTime()) ? d.toLocaleString(undefined, { month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit" }) : "";
33
+ }
34
+
35
+ export interface RecordCallsTabProps {
36
+ /** The contact: `id` scopes the history; the name and numbers feed the call button. */
37
+ contact: CallableContact & { id: string | number };
38
+ /** Route to the call in your calls screen; receives the call's room name. Without it the rows link to `${callsPath}?id=<room name>`. */
39
+ onOpenCall?: (roomName: string) => void;
40
+ /** Path of your calls screen for the plain links. Default "/calls". */
41
+ callsPath?: string;
42
+ /** Calls to read first. Default 25. */
43
+ pageSize?: number;
44
+ /** Pass the rep's softphone identity through to the call button (see CallContactButton). */
45
+ userId?: string;
46
+ userEmail?: string;
47
+ }
48
+
49
+ export function RecordCallsTab({ contact, onOpenCall, callsPath = "/calls", pageSize = 25, userId, userEmail }: RecordCallsTabProps) {
50
+ const [limit, setLimit] = useState(Math.min(pageSize, CALLS_TAB_API_MAX));
51
+ const contactId = String(contact.id);
52
+ const q = useG8Query(
53
+ async (signal) => dataOf<{ calls?: CallSummary[] }>(await g8Get("/voice/dialer/calls", { contact_id: contactId, limit }, { signal })).calls ?? [],
54
+ [contactId, limit],
55
+ );
56
+ const call = <CallContactButton contact={contact} userId={userId} userEmail={userEmail} onCalled={() => void q.refetch()} />;
57
+ if (q.error) return <ApiError error={q.error} title="Could not load calls" onRetry={() => void q.refetch()} />;
58
+ if (!q.data) return <Skeleton className="h-24 w-full" aria-label="Loading calls" />;
59
+ const calls = q.data;
60
+ const full = calls.length >= limit;
61
+ return (
62
+ <div className="space-y-3">
63
+ <div className="flex items-center justify-between gap-2">
64
+ <p className="text-sm text-muted-foreground">{calls.length === 1 ? "1 call" : `${calls.length.toLocaleString()}${full ? "+" : ""} calls`}</p>
65
+ {call}
66
+ </div>
67
+ {calls.length === 0 ? (
68
+ <EmptyState compact icon={<PhoneCall className="size-8" />} title="No calls yet" description="Calls the dialer places to this contact show up here." />
69
+ ) : (
70
+ <ul className="divide-y rounded-md border" aria-label="Calls">
71
+ {calls.map((c) => (
72
+ <li key={c.room_name}>
73
+ <a
74
+ href={`${callsPath}?id=${encodeURIComponent(c.room_name)}`}
75
+ className="flex items-center justify-between gap-3 p-3 text-sm hover:bg-muted"
76
+ onClick={(e) => {
77
+ if (!onOpenCall || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
78
+ e.preventDefault();
79
+ onOpenCall(c.room_name);
80
+ }}
81
+ >
82
+ <span className="min-w-0">
83
+ <span className="block font-medium">{when(c.created_at) || "Call"}</span>
84
+ <span className="block truncate text-xs text-muted-foreground">{[c.to, formatDuration(c.call_duration), c.summary ?? (c.has_transcript ? "Transcript available" : null)].filter(Boolean).join(" · ")}</span>
85
+ </span>
86
+ <StatusPill tone={outcomeTone(c.disposition)}>{c.disposition ? c.disposition.replace(/_/g, " ") : "no outcome"}</StatusPill>
87
+ </a>
88
+ </li>
89
+ ))}
90
+ </ul>
91
+ )}
92
+ {full && limit < CALLS_TAB_API_MAX ? (
93
+ <div className="flex items-center justify-between text-sm text-muted-foreground">
94
+ <span role="status">Showing {calls.length.toLocaleString()} calls</span>
95
+ <Button size="sm" variant="outline" onClick={() => setLimit(CALLS_TAB_API_MAX)}>Show more</Button>
96
+ </div>
97
+ ) : null}
98
+ {full && limit >= CALLS_TAB_API_MAX ? <p role="note" className="text-xs text-muted-foreground">Showing the {CALLS_TAB_API_MAX} most recent calls. Older calls are in the call log by rep or session.</p> : null}
99
+ </div>
100
+ );
101
+ }
@@ -4,9 +4,15 @@
4
4
  * SessionStartForm: create a DRAFT dialer session for a list. A draft dials NOBODY; dialing starts only when
5
5
  * CallQueue's "Dial next batch" is confirmed.
6
6
  *
7
+ * No ids to type: the rep defaults to the signed-in team member (`useCurrentMember`, GET /team-members/me) and can be
8
+ * switched with a MemberPicker; the list is picked with a ListPicker. A `userId` / `listId` prop still wins, and the
9
+ * raw id fields stay under "Use a ... id instead" for a key that is not a person (an organisation key).
10
+ *
7
11
  * SDK operations, in order:
12
+ * 0. GET /team-members/me (teamMembers.getMyTeamMember) read - only without `userId`: the rep is you
8
13
  * 1. GET /voice/dialer/numbers (voice.listDialerNumbers) read - caller IDs and today's usage
9
14
  * 2. POST /dialer/sessions/draft (dialer.createSessionDraft) write - safe: resolves the list, dials no one
15
+ * A personal key may only name itself as the rep: naming someone else answers 403, which the form explains.
10
16
  * Calling the draft twice for the same list returns the existing live session instead of making a second one.
11
17
  */
12
18
  import { useState } from "react";
@@ -17,6 +23,10 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
17
23
  import { useDialerNumbers } from "../hooks/use-dialer-numbers";
18
24
  import { dataOf, type DialerMode } from "../lib/dialer-types";
19
25
  import { useBrand } from "@/g8/hooks/use-brand";
26
+ import { useCurrentMember } from "@/g8/pickers/hooks/use-current-member";
27
+ import { MemberPicker } from "@/g8/pickers/components/member-picker";
28
+ import type { MemberOption } from "@/g8/pickers/components/member-picker";
29
+ import { ListPicker } from "@/g8/lists/components/list-picker";
20
30
 
21
31
  export interface SessionStartFormProps {
22
32
  /** The rep these calls are attributed to. An organisation API key has no user of its own, so you name one. */
@@ -34,12 +44,22 @@ export function SessionStartForm({ userId, userEmail, listId: initialListId = ""
34
44
  const brand = useBrand();
35
45
  const { numbers, loading, error, refetch } = useDialerNumbers();
36
46
  const [listId, setListId] = useState(initialListId);
47
+ const [pickedTitle, setPickedTitle] = useState("");
37
48
  const [typedUserId, setTypedUserId] = useState("");
38
- const repId = userId || typedUserId.trim();
49
+ const [refused, setRefused] = useState(false);
50
+ // With a passed userId nobody is looked up, until a 403 needs to know whether the rep is someone else.
51
+ const me = useCurrentMember({ enabled: !userId || refused });
52
+ const [picked, setPicked] = useState<MemberOption | null>(null);
53
+ // Who the session is for: a passed userId, else a typed id, else the chosen teammate, else you. user_id and user_email
54
+ // always describe the same person.
55
+ const resolved = userId || typedUserId.trim() ? null : (picked ?? me.member);
56
+ const repId = userId || typedUserId.trim() || resolved?.id || "";
57
+ const repEmail = resolved ? (resolved.email ?? undefined) : userEmail;
39
58
  const [fromPhone, setFromPhone] = useState("");
40
59
  const [mode, setMode] = useState<DialerMode>("power");
41
60
  const [parallelCount, setParallelCount] = useState(3);
42
61
  const [name, setName] = useState(listTitle ? `Dialing ${listTitle}` : "");
62
+ const title = listTitle ?? pickedTitle;
43
63
 
44
64
  const chosenPhone = fromPhone || numbers[0]?.phone_number || "";
45
65
  const create = useG8Mutation(
@@ -47,9 +67,9 @@ export function SessionStartForm({ userId, userEmail, listId: initialListId = ""
47
67
  const count = mode === "parallel" ? Math.min(10, Math.max(2, Math.round(parallelCount) || 2)) : 1;
48
68
  const env = await g8Post("/dialer/sessions/draft", {
49
69
  user_id: repId,
50
- user_email: userEmail,
70
+ user_email: repEmail,
51
71
  list_id: listId,
52
- list_title: listTitle,
72
+ list_title: title || undefined,
53
73
  name: name || undefined,
54
74
  from_phone: chosenPhone || undefined,
55
75
  dialing_mode: mode,
@@ -60,21 +80,41 @@ export function SessionStartForm({ userId, userEmail, listId: initialListId = ""
60
80
  if (!sessionId) throw new Error(`${brand.linkLabel} did not return a session id`);
61
81
  return { sessionId, mode, parallelCount: count, fromPhone: chosenPhone };
62
82
  },
63
- { onSuccess: onCreated },
83
+ { onSuccess: onCreated, onError: (e) => setRefused((e as { status?: number } | undefined)?.status === 403) },
64
84
  );
65
85
 
66
86
  return (
67
87
  <form className={`space-y-3 rounded-lg border p-4 ${className ?? ""}`} onSubmit={(e) => { e.preventDefault(); if (listId && repId) void create.mutate(undefined); }}>
68
- <h3 className="text-sm font-semibold">Start a dialing session</h3>
88
+ <h3 className="text-sm font-medium">Start a dialing session</h3>
69
89
  <div className="grid gap-3 sm:grid-cols-2">
70
- <div className="space-y-1">
71
- <Label htmlFor="ds-list">List id</Label>
72
- <Input id="ds-list" value={listId} onChange={(e) => setListId(e.target.value)} placeholder="From your lists" />
90
+ <div className="space-y-1 sm:col-span-2">
91
+ <span className="text-sm font-medium leading-none">List</span>
92
+ {initialListId ? (
93
+ <p className="text-sm">{title || `List ${initialListId}`}</p>
94
+ ) : (
95
+ <ListPicker type="contacts" allowCreate={false} value={Number(listId) || null} onChange={(l) => { setListId(String(l.id)); setPickedTitle(l.title); setName((n) => n || `Dialing ${l.title}`); }} />
96
+ )}
97
+ <details className="text-sm">
98
+ <summary className="cursor-pointer text-muted-foreground">Use a list id instead</summary>
99
+ <div className="mt-1 space-y-1">
100
+ <Label htmlFor="ds-list">List id</Label>
101
+ <Input id="ds-list" value={listId} onChange={(e) => setListId(e.target.value)} placeholder="From your lists" />
102
+ </div>
103
+ </details>
73
104
  </div>
74
105
  {userId ? null : (
75
- <div className="space-y-1">
76
- <Label htmlFor="ds-user">Rep user id</Label>
77
- <Input id="ds-user" value={typedUserId} onChange={(e) => setTypedUserId(e.target.value)} placeholder={`${brand.linkLabel} user id`} />
106
+ <div className="space-y-1 sm:col-span-2">
107
+ <span className="text-sm font-medium leading-none">Rep</span>
108
+ {me.loading ? <p className="text-sm text-muted-foreground">Finding your user...</p> : null}
109
+ {resolved ? <p className="text-sm">Calling as {resolved.name}{resolved.id === me.userId ? " (you)" : ""}</p> : null}
110
+ <MemberPicker label={resolved ? "Switch rep" : "Rep"} value={resolved?.id ?? null} onChange={(m) => { setPicked(m); setTypedUserId(""); }} placeholder="Choose who is calling" />
111
+ <details className="text-sm">
112
+ <summary className="cursor-pointer text-muted-foreground">Use a user id instead</summary>
113
+ <div className="mt-1 space-y-1">
114
+ <Label htmlFor="ds-user">Rep user id</Label>
115
+ <Input id="ds-user" value={typedUserId} onChange={(e) => setTypedUserId(e.target.value)} placeholder={`${brand.linkLabel} user id`} />
116
+ </div>
117
+ </details>
78
118
  </div>
79
119
  )}
80
120
  <div className="space-y-1">
@@ -105,6 +145,13 @@ export function SessionStartForm({ userId, userEmail, listId: initialListId = ""
105
145
  </div>
106
146
  {error ? <ApiError error={error} title="Could not load caller IDs" onRetry={() => void refetch()} /> : null}
107
147
  {create.error ? <ApiError error={create.error} title="Could not create the session" /> : null}
148
+ {(create.error as { status?: number } | undefined)?.status === 403 ? (
149
+ <p role="alert" className="text-sm text-muted-foreground">
150
+ {me.userId && repId && repId !== me.userId
151
+ ? "A personal API key can only act as its own user, so this session cannot be started for someone else. Switch the rep back to yourself."
152
+ : "This API key is not allowed to start dialer sessions. Check that it has the voice scopes, or ask an admin."}
153
+ </p>
154
+ ) : null}
108
155
  <Button type="submit" disabled={!listId || !repId || create.loading} loading={create.loading}>
109
156
  Create session (dials no one yet)
110
157
  </Button>
@@ -93,7 +93,7 @@ export function Softphone({ userId, transport, conferenceName, joinMuted, onStat
93
93
  return (
94
94
  <section className={`space-y-3 rounded-lg border p-4 ${className ?? ""}`} aria-label="Softphone">
95
95
  <div className="flex items-center justify-between">
96
- <h3 className="text-sm font-semibold">Softphone</h3>
96
+ <h3 className="text-sm font-medium">Softphone</h3>
97
97
  <span role="status" className="text-sm text-muted-foreground">{label}</span>
98
98
  </div>
99
99
  {transportError ? <p role="alert" className="text-sm text-destructive">{transportError}</p> : null}