@graph8/sdk 0.361.0 → 0.370.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/README.md +14 -10
  2. package/blocks/_foundation/README.md +172 -1
  3. package/blocks/_foundation/block.json +234 -13
  4. package/blocks/_foundation/components/api-error.tsx +71 -25
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  7. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  8. package/blocks/_foundation/components/empty-state.tsx +39 -7
  9. package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
  10. package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
  11. package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
  12. package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
  13. package/blocks/_foundation/components/screen/index.ts +22 -0
  14. package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
  17. package/blocks/_foundation/components/screen/section-card.tsx +47 -0
  18. package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
  19. package/blocks/_foundation/components/screen/split-view.tsx +125 -0
  20. package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
  21. package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
  22. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  23. package/blocks/_foundation/components/screen/time-series-chart.tsx +32 -0
  24. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  25. package/blocks/_foundation/components/screen/toolbar.tsx +144 -0
  26. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  27. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  28. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  29. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  30. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  31. package/blocks/_foundation/hooks/use-url-state.ts +74 -0
  32. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  33. package/blocks/_foundation/lib/g8-client.ts +1 -1
  34. package/blocks/_foundation/lib/plural.ts +23 -0
  35. package/blocks/_foundation/server/connect-route.ts +15 -0
  36. package/blocks/_foundation/server/connect.ts +127 -0
  37. package/blocks/_foundation/server/local-key.ts +70 -0
  38. package/blocks/_foundation/server/proxy.ts +103 -3
  39. package/blocks/_foundation/server/route.ts +16 -4
  40. package/blocks/agents/README.md +14 -0
  41. package/blocks/agents/block.json +33 -1
  42. package/blocks/agents/components/agent-builder.tsx +13 -8
  43. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
  44. package/blocks/agents/components/agent-run-detail.tsx +102 -0
  45. package/blocks/agents/components/agent-runs.tsx +16 -7
  46. package/blocks/agents/components/agents-screen.tsx +184 -0
  47. package/blocks/analytics/README.md +13 -0
  48. package/blocks/analytics/block.json +45 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +209 -0
  50. package/blocks/analytics/components/leaderboard.tsx +14 -7
  51. package/blocks/analytics/components/report-builder.tsx +23 -9
  52. package/blocks/analytics/components/report-detail.tsx +84 -0
  53. package/blocks/analytics/components/trend-chart.tsx +62 -0
  54. package/blocks/appointments/README.md +21 -1
  55. package/blocks/appointments/block.json +43 -2
  56. package/blocks/appointments/components/availability-editor.tsx +17 -5
  57. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  58. package/blocks/appointments/components/event-type-related.tsx +77 -0
  59. package/blocks/appointments/components/meeting-list.tsx +14 -6
  60. package/blocks/appointments/components/meetings-screen.tsx +157 -0
  61. package/blocks/appointments/components/scheduling-screen.tsx +122 -0
  62. package/blocks/appointments/hooks/use-event-types.ts +17 -0
  63. package/blocks/block.schema.json +18 -0
  64. package/blocks/block.types.ts +26 -0
  65. package/blocks/campaigns/README.md +41 -11
  66. package/blocks/campaigns/block.json +105 -42
  67. package/blocks/campaigns/components/campaign-assets.tsx +16 -5
  68. package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
  69. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  70. package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
  71. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  72. package/blocks/campaigns/components/icp-detail.tsx +154 -0
  73. package/blocks/campaigns/components/icp-editor.tsx +15 -15
  74. package/blocks/campaigns/components/persona-detail.tsx +172 -0
  75. package/blocks/campaigns/components/persona-editor.tsx +14 -11
  76. package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
  77. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  78. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  79. package/blocks/coach/README.md +10 -0
  80. package/blocks/coach/block.json +16 -1
  81. package/blocks/coach/components/coach-screen.tsx +222 -0
  82. package/blocks/copilot/README.md +13 -0
  83. package/blocks/copilot/block.json +14 -1
  84. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  85. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  86. package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
  87. package/blocks/crm/README.md +28 -0
  88. package/blocks/crm/block.json +58 -2
  89. package/blocks/crm/components/companies-screen.tsx +345 -0
  90. package/blocks/crm/components/company-detail.tsx +8 -6
  91. package/blocks/crm/components/company-table.tsx +11 -4
  92. package/blocks/crm/components/contact-detail.tsx +6 -3
  93. package/blocks/crm/components/contact-table.tsx +10 -3
  94. package/blocks/crm/components/contacts-screen.tsx +359 -0
  95. package/blocks/crm/components/dedupe-review.tsx +7 -5
  96. package/blocks/crm/components/import-wizard.tsx +4 -2
  97. package/blocks/crm/components/notes-panel.tsx +4 -4
  98. package/blocks/crm/components/record-form.tsx +5 -3
  99. package/blocks/crm/components/task-list.tsx +4 -4
  100. package/blocks/crm/components/tasks-screen.tsx +379 -0
  101. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  102. package/blocks/crm/hooks/use-page-query.ts +7 -1
  103. package/blocks/crm/lib/crm-types.ts +12 -2
  104. package/blocks/crm/lib/screen-parts.tsx +105 -0
  105. package/blocks/deals/README.md +34 -3
  106. package/blocks/deals/block.json +45 -4
  107. package/blocks/deals/components/deal-detail.tsx +27 -13
  108. package/blocks/deals/components/deal-related.tsx +87 -0
  109. package/blocks/deals/components/deals-screen.tsx +298 -0
  110. package/blocks/deals/components/pipeline-board.tsx +4 -2
  111. package/blocks/deals/components/stage-editor.tsx +4 -4
  112. package/blocks/deals/hooks/use-default-owner.ts +60 -0
  113. package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
  114. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  115. package/blocks/deals/lib/deals-types.ts +14 -0
  116. package/blocks/dialer/README.md +34 -2
  117. package/blocks/dialer/block.json +67 -1
  118. package/blocks/dialer/components/call-contact-button.tsx +252 -0
  119. package/blocks/dialer/components/call-log-screen.tsx +205 -0
  120. package/blocks/dialer/components/call-log.tsx +7 -3
  121. package/blocks/dialer/components/call-queue.tsx +1 -1
  122. package/blocks/dialer/components/dialer-screen.tsx +319 -0
  123. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  124. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  125. package/blocks/dialer/components/session-start-form.tsx +4 -2
  126. package/blocks/dialer/components/softphone.tsx +3 -1
  127. package/blocks/enrichment/README.md +14 -2
  128. package/blocks/enrichment/block.json +32 -4
  129. package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
  130. package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
  131. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  132. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  133. package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
  134. package/blocks/enrichment/components/waterfall-config.tsx +39 -10
  135. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  136. package/blocks/forms/README.md +12 -0
  137. package/blocks/forms/block.json +39 -17
  138. package/blocks/forms/components/form-builder.tsx +3 -1
  139. package/blocks/forms/components/form-embed.tsx +3 -1
  140. package/blocks/forms/components/forms-screen.tsx +353 -0
  141. package/blocks/forms/components/submissions-table.tsx +16 -6
  142. package/blocks/inbox/README.md +27 -3
  143. package/blocks/inbox/block.json +14 -2
  144. package/blocks/inbox/components/inbox-list.tsx +7 -3
  145. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  146. package/blocks/inbox/components/reply-composer.tsx +65 -9
  147. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  148. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  149. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  150. package/blocks/inbox/lib/inbox-types.ts +47 -1
  151. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  152. package/blocks/integrations/README.md +9 -0
  153. package/blocks/integrations/block.json +46 -1
  154. package/blocks/integrations/components/crm-detail.tsx +100 -0
  155. package/blocks/integrations/components/integration-list.tsx +9 -4
  156. package/blocks/integrations/components/integrations-screen.tsx +135 -0
  157. package/blocks/integrations/components/sync-status.tsx +5 -3
  158. package/blocks/integrations/components/webhook-detail.tsx +160 -0
  159. package/blocks/integrations/components/webhook-manager.tsx +13 -8
  160. package/blocks/integrations/lib/integrations-utils.ts +6 -1
  161. package/blocks/intent/README.md +14 -0
  162. package/blocks/intent/block.json +27 -1
  163. package/blocks/intent/components/intent-feed.tsx +5 -2
  164. package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
  165. package/blocks/intent/components/intent-screen.tsx +265 -0
  166. package/blocks/intent/components/keyword-detail.tsx +124 -0
  167. package/blocks/lifecycle/README.md +16 -0
  168. package/blocks/lifecycle/block.json +34 -7
  169. package/blocks/lifecycle/components/account-related.tsx +66 -0
  170. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  171. package/blocks/lifecycle/components/lifecycle-screen.tsx +149 -0
  172. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  173. package/blocks/lists/README.md +13 -0
  174. package/blocks/lists/block.json +39 -1
  175. package/blocks/lists/components/add-to-list-button.tsx +3 -2
  176. package/blocks/lists/components/list-picker.tsx +5 -1
  177. package/blocks/lists/components/list-table.tsx +11 -5
  178. package/blocks/lists/components/lists-screen.tsx +344 -0
  179. package/blocks/lists/components/suppression-manager.tsx +6 -2
  180. package/blocks/lists/components/suppressions-screen.tsx +255 -0
  181. package/blocks/mailboxes/README.md +17 -0
  182. package/blocks/mailboxes/block.json +65 -17
  183. package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
  184. package/blocks/mailboxes/components/domain-detail.tsx +142 -0
  185. package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
  186. package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
  187. package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
  188. package/blocks/mailboxes/components/warmup-health.tsx +4 -2
  189. package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
  190. package/blocks/newsletter/README.md +13 -0
  191. package/blocks/newsletter/block.json +47 -23
  192. package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
  193. package/blocks/newsletter/components/newsletter-screen.tsx +404 -0
  194. package/blocks/newsletter/components/subscriber-table.tsx +12 -6
  195. package/blocks/nurture/README.md +12 -0
  196. package/blocks/nurture/block.json +35 -13
  197. package/blocks/nurture/components/nurture-builder.tsx +3 -1
  198. package/blocks/nurture/components/nurture-enroll.tsx +6 -3
  199. package/blocks/nurture/components/nurture-form.tsx +23 -3
  200. package/blocks/nurture/components/nurture-screen.tsx +283 -0
  201. package/blocks/nurture/lib/nurture-types.ts +1 -1
  202. package/blocks/objects/README.md +17 -0
  203. package/blocks/objects/block.json +29 -16
  204. package/blocks/objects/components/object-not-found.tsx +3 -1
  205. package/blocks/objects/components/object-record-detail.tsx +2 -1
  206. package/blocks/objects/components/object-schema-editor.tsx +3 -1
  207. package/blocks/objects/components/object-table.tsx +13 -3
  208. package/blocks/objects/components/objects-screen.tsx +147 -0
  209. package/blocks/objects/lib/object-types.ts +1 -1
  210. package/blocks/pages/README.md +12 -0
  211. package/blocks/pages/block.json +35 -22
  212. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  213. package/blocks/pages/components/content-library.tsx +46 -11
  214. package/blocks/pages/components/page-list.tsx +6 -3
  215. package/blocks/pages/components/pages-screen.tsx +154 -0
  216. package/blocks/quotes/README.md +17 -0
  217. package/blocks/quotes/block.json +38 -9
  218. package/blocks/quotes/components/product-picker.tsx +3 -1
  219. package/blocks/quotes/components/quote-list.tsx +7 -3
  220. package/blocks/quotes/components/quote-related.tsx +123 -0
  221. package/blocks/quotes/components/quotes-screen.tsx +169 -0
  222. package/blocks/radar/README.md +10 -0
  223. package/blocks/radar/block.json +41 -1
  224. package/blocks/radar/components/competitor-detail.tsx +117 -0
  225. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  226. package/blocks/radar/components/radar-report.tsx +4 -2
  227. package/blocks/radar/components/radar-screen.tsx +214 -0
  228. package/blocks/radar/components/report-detail.tsx +104 -0
  229. package/blocks/search/README.md +10 -0
  230. package/blocks/search/block.json +16 -1
  231. package/blocks/search/components/filter-builder.tsx +3 -3
  232. package/blocks/search/components/prospect-results.tsx +5 -3
  233. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  234. package/blocks/search/components/saved-searches.tsx +4 -1
  235. package/blocks/search/hooks/use-filter-options.ts +20 -6
  236. package/blocks/search/lib/search-types.ts +2 -0
  237. package/blocks/sequencer/README.md +49 -9
  238. package/blocks/sequencer/block.json +99 -19
  239. package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
  240. package/blocks/sequencer/components/sequence-builder.tsx +211 -46
  241. package/blocks/sequencer/components/sequence-detail.tsx +259 -0
  242. package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
  243. package/blocks/sequencer/components/sequence-list.tsx +33 -8
  244. package/blocks/sequencer/components/sequence-stats.tsx +3 -2
  245. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  246. package/blocks/sequencer/components/sequences-screen.tsx +159 -0
  247. package/blocks/sequencer/components/step-editor.tsx +3 -1
  248. package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
  249. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  250. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  251. package/blocks/social-ads/README.md +11 -0
  252. package/blocks/social-ads/block.json +101 -2
  253. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  254. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  255. package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
  256. package/blocks/social-ads/components/audience-sync.tsx +11 -7
  257. package/blocks/social-ads/components/campaign-detail.tsx +133 -0
  258. package/blocks/social-ads/components/creative-detail.tsx +150 -0
  259. package/blocks/social-ads/components/creative-gallery.tsx +15 -5
  260. package/blocks/social-ads/components/post-composer.tsx +3 -1
  261. package/blocks/social-ads/components/post-detail.tsx +160 -0
  262. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  263. package/blocks/social-ads/components/social-screen.tsx +160 -0
  264. package/blocks/visitors/README.md +10 -0
  265. package/blocks/visitors/block.json +25 -1
  266. package/blocks/visitors/components/snippet-install.tsx +3 -1
  267. package/blocks/visitors/components/visitor-company-card.tsx +2 -2
  268. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  269. package/blocks/visitors/components/visitor-feed.tsx +11 -2
  270. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  271. package/blocks/webchat/README.md +10 -0
  272. package/blocks/webchat/block.json +21 -7
  273. package/blocks/webchat/components/chat-widget-config.tsx +7 -4
  274. package/blocks/webchat/components/conversation-list.tsx +7 -3
  275. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  276. package/blocks/workbench/README.md +11 -0
  277. package/blocks/workbench/block.json +48 -1
  278. package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
  279. package/blocks/workbench/components/workbench-grid.tsx +9 -3
  280. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  281. package/blocks/workbench/components/workbench-screen.tsx +192 -0
  282. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  283. package/blocks/workflows/README.md +13 -0
  284. package/blocks/workflows/block.json +28 -9
  285. package/blocks/workflows/components/run-history.tsx +31 -9
  286. package/blocks/workflows/components/trigger-config.tsx +3 -1
  287. package/blocks/workflows/components/workflow-editor.tsx +3 -1
  288. package/blocks/workflows/components/workflow-list.tsx +13 -6
  289. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  290. package/dist/cli.js +2785 -1637
  291. package/dist/errors.d.mts +34 -0
  292. package/dist/errors.d.ts +34 -0
  293. package/dist/errors.js +46 -0
  294. package/dist/errors.js.map +1 -0
  295. package/dist/errors.mjs +21 -0
  296. package/dist/errors.mjs.map +1 -0
  297. package/dist/index.browser.js +11 -5
  298. package/dist/index.browser.js.map +1 -1
  299. package/dist/index.browser.mjs +11 -5
  300. package/dist/index.browser.mjs.map +1 -1
  301. package/dist/index.d.mts +164 -25
  302. package/dist/index.d.ts +164 -25
  303. package/dist/index.js +11 -5
  304. package/dist/index.js.map +1 -1
  305. package/dist/index.mjs +11 -5
  306. package/dist/index.mjs.map +1 -1
  307. package/dist/react.browser.js +10 -4
  308. package/dist/react.browser.js.map +1 -1
  309. package/dist/react.browser.mjs +10 -4
  310. package/dist/react.browser.mjs.map +1 -1
  311. package/dist/react.d.mts +142 -5
  312. package/dist/react.d.ts +142 -5
  313. package/dist/react.js +10 -4
  314. package/dist/react.js.map +1 -1
  315. package/dist/react.mjs +10 -4
  316. package/dist/react.mjs.map +1 -1
  317. package/dist/types/blocks/add.d.ts +11 -4
  318. package/dist/types/blocks/brand.d.ts +113 -0
  319. package/dist/types/blocks/compose.d.ts +53 -0
  320. package/dist/types/blocks/create.d.ts +25 -8
  321. package/dist/types/blocks/doctor.d.ts +6 -1
  322. package/dist/types/blocks/flows.d.ts +20 -0
  323. package/dist/types/blocks/navfile.d.ts +35 -0
  324. package/dist/types/blocks/paths.d.ts +4 -0
  325. package/dist/types/blocks/prefixes.d.ts +37 -0
  326. package/dist/types/blocks/registry.d.ts +32 -0
  327. package/dist/types/blocks/screens.d.ts +53 -0
  328. package/dist/types/cli.d.ts +2 -0
  329. package/dist/types/errors.d.ts +33 -0
  330. package/dist/types/http.d.ts +2 -18
  331. package/dist/types/version.d.ts +2 -2
  332. package/docs/agents.md +15 -4
  333. package/docs/api/README.md +2 -2
  334. package/docs/api/by-path.md +1 -0
  335. package/docs/api/desk.d.ts +98 -2
  336. package/docs/api/desk.md +123 -1
  337. package/docs/api/inbox.d.ts +10 -2
  338. package/docs/api/inbox.md +10 -2
  339. package/docs/api/seo.d.ts +19 -1
  340. package/docs/api/seo.md +24 -1
  341. package/package.json +16 -2
  342. package/stacks/6sense.json +41 -16
  343. package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
  344. package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
  345. package/stacks/apollo.json +118 -36
  346. package/stacks/calendly.json +60 -0
  347. package/stacks/clay/pages/companies.tsx.tmpl +15 -0
  348. package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
  349. package/stacks/clay.json +59 -26
  350. package/stacks/gong.json +81 -35
  351. package/stacks/hootsuite.json +99 -0
  352. package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
  353. package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
  354. package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
  355. package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
  356. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  357. package/stacks/hubspot.json +167 -66
  358. package/stacks/intercom.json +48 -20
  359. package/stacks/mailchimp.json +52 -33
  360. package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
  361. package/stacks/outreach.json +88 -25
  362. package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
  363. package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
  364. package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
  365. package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
  366. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  367. package/stacks/pipedrive.json +115 -0
  368. package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
  369. package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
  370. package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
  371. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  372. package/stacks/salesforce.json +81 -31
  373. package/stacks/stack.schema.json +6 -1
  374. package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
  375. package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
  376. package/stacks/zoominfo.json +102 -0
  377. package/styles/styles.css +1 -1
  378. package/templates/next-app/README.md.tmpl +55 -8
  379. package/templates/next-app/app/globals.css.tmpl +105 -0
  380. package/templates/next-app/app/layout.tsx.tmpl +18 -4
  381. package/templates/next-app/components/app-shell.tsx.tmpl +248 -23
  382. package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
  383. package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
  384. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
  385. package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
  386. package/templates/next-app/dot-gitignore.tmpl +2 -0
  387. package/templates/next-app/lib/nav.ts.tmpl +1 -1
  388. package/templates/next-app/package.json.tmpl +1 -1
@@ -0,0 +1,205 @@
1
+ "use client";
2
+
3
+ /**
4
+ * CallLogScreen: the dialer's call history as a finished page. Pick whose calls to show (a rep's email, a session or a
5
+ * contact; the API never lists the whole organisation at once), then filter by outcome, search numbers and summaries,
6
+ * and open a call to hear the recording, read the transcript and AI grade (deep link `?id=<room_name>`).
7
+ *
8
+ * SDK operations (through the composed parts): GET /voice/dialer/calls (read, CallLog's query), transcript, grading and
9
+ * recording (read, via TranscriptViewer). The page itself dials, spends and deletes nothing. A call's detail offers
10
+ * "Call back" (CallContactButton), which dials only through its own external confirm dialog and loads nothing until opened.
11
+ * `CallHistory` is exported so DialerScreen can embed the same body under its own header.
12
+ */
13
+ import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
14
+ import { Phone } from "lucide-react";
15
+ import { Button, Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
16
+ import { ApiError } from "@/g8/components/api-error";
17
+ import { EmptyState } from "@/g8/components/empty-state";
18
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
19
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
20
+ import { ScreenLayout } from "@/g8/components/screen/screen-layout";
21
+ import { StatStrip } from "@/g8/components/screen/stat-strip";
22
+ import { SplitView } from "@/g8/components/screen/split-view";
23
+ import { StatusPill } from "@/g8/components/screen/status-pill";
24
+ import { Toolbar } from "@/g8/components/screen/toolbar";
25
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
26
+ import { useUrlState } from "@/g8/hooks/use-url-state";
27
+ import { g8Get } from "@/g8/lib/g8-client";
28
+ import { dataOf, formatDuration, type CallSummary } from "../lib/dialer-types";
29
+ import { TranscriptViewer } from "./transcript-viewer";
30
+ import { CallContactButton } from "./call-contact-button";
31
+ import { rowActivationProps } from "@/g8/hooks/use-row-activation";
32
+ import { RecordLink } from "@/g8/components/screen/record-link";
33
+
34
+ export interface CallLogScreenProps {
35
+ contactId?: string;
36
+ userEmail?: string;
37
+ sessionId?: string;
38
+ /** Rows loaded at first; "Load more" adds this many again. Default 25. */
39
+ limit?: number;
40
+ toolbarActions?: ReactNode;
41
+ renderRowActions?: (call: CallSummary) => ReactNode;
42
+ renderDetailActions?: (call: CallSummary) => ReactNode;
43
+ extraDetailTabs?: Array<{ id: string; label: string; render: (call: CallSummary) => ReactNode }>;
44
+ className?: string;
45
+ /** URL query key that holds the open call (a room name). Default "id"; give a screen-specific key when another list on the page shares `id`. */
46
+ idParam?: string;
47
+ }
48
+
49
+ function useDebounced<T>(value: T, ms = 300): T {
50
+ const [v, setV] = useState(value);
51
+ useEffect(() => {
52
+ const t = setTimeout(() => setV(value), ms);
53
+ return () => clearTimeout(t);
54
+ }, [value, ms]);
55
+ return v;
56
+ }
57
+
58
+ const label = (d?: string | null) => (d ? d.replace(/_/g, " ") : "none");
59
+ export function outcomeTone(d?: string | null): "neutral" | "info" | "positive" | "warning" | "negative" {
60
+ if (!d) return "neutral";
61
+ if (/wrong|do_not|dnc|bad|invalid|fail/.test(d)) return "negative";
62
+ if (/call_?back|follow|meeting|interested|connected|booked/.test(d)) return /connected|interested|meeting|booked/.test(d) ? "positive" : "info";
63
+ if (/voicemail|no_answer|busy/.test(d)) return "warning";
64
+ return "neutral";
65
+ }
66
+
67
+ function when(iso?: string | null): string {
68
+ const d = iso ? new Date(iso) : null;
69
+ return d && !Number.isNaN(d.getTime()) ? d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : "";
70
+ }
71
+
72
+ export function CallHistory({ contactId, userEmail, sessionId, limit = 25, idParam = "id", renderRowActions, renderDetailActions, extraDetailTabs }: CallLogScreenProps) {
73
+ const [typedEmail, setTypedEmail] = useState("");
74
+ const [rows, setRows] = useState(limit);
75
+ const [search, setSearch] = useState("");
76
+ const [outcome, setOutcome] = useState("");
77
+ const [openId, setOpenId] = useUrlState(idParam);
78
+ const [view, setView] = useUrlState("view");
79
+ const rep = useDebounced(userEmail || typedEmail.trim());
80
+ const scoped = Boolean(contactId || rep || sessionId);
81
+ const q = useG8Query(
82
+ async (signal) => {
83
+ const env = await g8Get("/voice/dialer/calls", { contact_id: contactId, user_email: rep || undefined, parallel_session_id: sessionId, limit: rows }, { signal });
84
+ return dataOf<{ calls?: CallSummary[] }>(env).calls ?? [];
85
+ },
86
+ [contactId, rep, sessionId, rows],
87
+ { enabled: scoped },
88
+ );
89
+ // A different rep, session or contact is a different list.
90
+ const scope = `${contactId}|${rep}|${sessionId}|${limit}`;
91
+ const lastScope = useRef(scope);
92
+ useEffect(() => {
93
+ if (lastScope.current === scope) return; // keep a deep-linked ?id= on first render
94
+ lastScope.current = scope;
95
+ setRows(limit);
96
+ setOutcome("");
97
+ setOpenId(null);
98
+ }, [scope]); // eslint-disable-line react-hooks/exhaustive-deps
99
+
100
+ const all = q.data ?? [];
101
+ const needle = search.trim().toLowerCase();
102
+ const calls = useMemo(
103
+ () => all.filter((c) => (!outcome || (c.disposition ?? "none") === outcome) && (!needle || `${c.to ?? ""} ${c.from_ ?? ""} ${c.summary ?? ""}`.toLowerCase().includes(needle))),
104
+ [all, outcome, needle],
105
+ );
106
+ const outcomes = [...new Set(all.map((c) => c.disposition ?? "none"))];
107
+ const selected = openId ? all.find((c) => c.room_name === openId) : undefined;
108
+ const expanded = view === "full" && Boolean(selected);
109
+ const totalSecs = all.reduce((n, c) => n + (c.call_duration ?? 0), 0);
110
+
111
+ if (!scoped) {
112
+ return (
113
+ <div className="max-w-md space-y-3">
114
+ <EmptyState icon={<Phone className="size-6" />} title="Choose whose calls to show" description="Type a rep's email. The API will not list the whole organisation's calls at once." />
115
+ <Input aria-label="Rep email" type="email" placeholder="rep@company.com" value={typedEmail} onChange={(e) => setTypedEmail(e.target.value)} />
116
+ </div>
117
+ );
118
+ }
119
+ const list = (
120
+ <div className="space-y-3">
121
+ <Toolbar
122
+ search={{ value: search, onChange: setSearch, placeholder: "Search number or summary" }}
123
+ filters={
124
+ <select aria-label="Outcome" className="h-8 rounded-md border border-border bg-background px-2 text-sm" value={outcome} onChange={(e) => setOutcome(e.target.value)}>
125
+ <option value="">Any outcome</option>
126
+ {outcomes.map((o) => <option key={o} value={o}>{label(o)}</option>)}
127
+ </select>
128
+ }
129
+ />
130
+ {q.error ? (
131
+ <ApiError error={q.error} title="Could not load calls" onRetry={() => void q.refetch()} />
132
+ ) : q.loading && !q.data ? (
133
+ <Skeleton className="h-32 w-full" aria-label="Loading calls" />
134
+ ) : calls.length === 0 ? (
135
+ all.length === 0 ? (
136
+ <EmptyState icon={<Phone className="size-6" />} title="No calls yet" description="Calls appear here after the dialer places them." />
137
+ ) : (
138
+ <EmptyState title="No calls match" description="Nothing fits this search or outcome." action={<Button size="sm" variant="outline" onClick={() => { setSearch(""); setOutcome(""); }}>Clear filters</Button>} />
139
+ )
140
+ ) : (
141
+ <Table aria-label="Call history">
142
+ <TableHeader><TableRow><TableHead>When</TableHead><TableHead>Number</TableHead><TableHead>Outcome</TableHead><TableHead>Length</TableHead><TableHead>Summary</TableHead>{renderRowActions ? <TableHead /> : null}</TableRow></TableHeader>
143
+ <TableBody>
144
+ {calls.map((c) => (
145
+ <TableRow key={c.room_name} data-state={openId === c.room_name ? "selected" : undefined} className="cursor-pointer" {...rowActivationProps(() => setOpenId(c.room_name))}>
146
+ <TableCell className="whitespace-nowrap"><RecordLink param={idParam} id={c.room_name} onOpen={() => setOpenId(c.room_name)}>{when(c.created_at)}</RecordLink></TableCell>
147
+ <TableCell>{c.to}</TableCell>
148
+ <TableCell><StatusPill tone={outcomeTone(c.disposition)}>{label(c.disposition)}</StatusPill></TableCell>
149
+ <TableCell>{formatDuration(c.call_duration)}</TableCell>
150
+ <TableCell className="max-w-64 truncate text-muted-foreground">{c.summary ?? (c.has_transcript ? "Transcript available" : "")}</TableCell>
151
+ {renderRowActions ? <TableCell onClick={(e) => e.stopPropagation()}>{renderRowActions(c)}</TableCell> : null}
152
+ </TableRow>
153
+ ))}
154
+ </TableBody>
155
+ </Table>
156
+ )}
157
+ {all.length >= rows ? <Button size="sm" variant="outline" disabled={q.loading} onClick={() => setRows(rows + limit)}>Load more</Button> : null}
158
+ </div>
159
+ );
160
+ const detail = selected ? (
161
+ <DetailPanel
162
+ title={selected.to ?? "Call"}
163
+ subtitle={when(selected.created_at)}
164
+ badges={<StatusPill tone={outcomeTone(selected.disposition)}>{label(selected.disposition)}</StatusPill>}
165
+ actions={
166
+ <>
167
+ {renderDetailActions?.(selected)}
168
+ {/* Call the same number again: CallContactButton (manual-dial, confirmed; loads nothing until opened). */}
169
+ {selected.to ? <CallContactButton contact={{ id: selected.contact_id, phone: selected.to }} userEmail={userEmail} label="Call back" /> : null}
170
+ </>
171
+ }
172
+ onClose={() => { setOpenId(null); setView(null); }}
173
+ onExpand={expanded ? undefined : () => setView("full")}
174
+ onCollapse={expanded ? () => setView(null) : undefined}
175
+ tabs={[
176
+ { id: "recording", label: "Recording and transcript", content: <TranscriptViewer key={selected.room_name} roomName={selected.room_name} /> },
177
+ { id: "details", label: "Details", content: <KeyValueList columns={1} items={[{ label: "From", value: selected.from_ ?? "" }, { label: "To", value: selected.to ?? "" }, { label: "Length", value: formatDuration(selected.call_duration) }, { label: "Contact id", value: selected.contact_id ?? "" }, { label: "Room", value: selected.room_name }]} /> },
178
+ ...(extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(selected) })),
179
+ ]}
180
+ />
181
+ ) : null;
182
+ return (
183
+ <div className="space-y-4">
184
+ {all.length > 0 ? (
185
+ <StatStrip
186
+ items={[
187
+ { label: "Calls loaded", value: all.length },
188
+ { label: "Talk time", value: formatDuration(totalSecs) },
189
+ { label: "With transcript", value: all.filter((c) => c.has_transcript).length },
190
+ { label: "No outcome yet", value: all.filter((c) => !c.disposition).length, tone: all.some((c) => !c.disposition) ? "warning" : "neutral" },
191
+ ]}
192
+ />
193
+ ) : null}
194
+ <SplitView list={list} detail={detail} detailOpen={Boolean(selected)} onCloseDetail={() => { setOpenId(null); setView(null); }} expanded={expanded} onExpandedChange={(next) => setView(next ? "full" : null)} />
195
+ </div>
196
+ );
197
+ }
198
+
199
+ export function CallLogScreen(props: CallLogScreenProps) {
200
+ return (
201
+ <ScreenLayout className={props.className} title="Call log" description="Every dialer call with its outcome, recording, transcript and AI grade." icon={<Phone className="size-5" />} actions={props.toolbarActions}>
202
+ <CallHistory {...props} />
203
+ </ScreenLayout>
204
+ );
205
+ }
@@ -15,8 +15,12 @@ import { EmptyState } from "@/g8/components/empty-state";
15
15
  import { g8Get } from "@/g8/lib/g8-client";
16
16
  import { useG8Query } from "@/g8/hooks/use-g8-query";
17
17
  import { dataOf, formatDuration, type CallSummary } from "../lib/dialer-types";
18
+ import { rowActivationProps } from "@/g8/hooks/use-row-activation";
19
+ import { RecordLink } from "@/g8/components/screen/record-link";
18
20
 
19
21
  export interface CallLogProps {
22
+ /** URL param the host screen reads for this record type. With it the name is a real link (Cmd-click opens it in a new tab); without it a plain button. */
23
+ linkParam?: string;
20
24
  contactId?: string;
21
25
  userEmail?: string;
22
26
  sessionId?: string;
@@ -31,7 +35,7 @@ function when(iso: string | null | undefined): string {
31
35
  return Number.isNaN(d.getTime()) ? "" : d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
32
36
  }
33
37
 
34
- export function CallLog({ contactId, userEmail, sessionId, limit = 25, onSelect, className }: CallLogProps) {
38
+ export function CallLog({ linkParam, contactId, userEmail, sessionId, limit = 25, onSelect, className }: CallLogProps) {
35
39
  const [typedEmail, setTypedEmail] = useState("");
36
40
  const repEmail = userEmail || typedEmail.trim();
37
41
  const scoped = Boolean(contactId || repEmail || sessionId);
@@ -64,8 +68,8 @@ export function CallLog({ contactId, userEmail, sessionId, limit = 25, onSelect,
64
68
  </TableHeader>
65
69
  <TableBody>
66
70
  {calls.map((c) => (
67
- <TableRow key={c.room_name} className={onSelect ? "cursor-pointer" : undefined} onClick={() => onSelect?.(c)}>
68
- <TableCell className="whitespace-nowrap">{when(c.created_at)}</TableCell>
71
+ <TableRow key={c.room_name} className={onSelect ? "cursor-pointer" : undefined} {...(onSelect ? rowActivationProps(() => onSelect(c)) : {})}>
72
+ <TableCell className="whitespace-nowrap">{onSelect ? <RecordLink param={linkParam} id={c.room_name} onOpen={() => onSelect(c)}>{when(c.created_at)}</RecordLink> : when(c.created_at)}</TableCell>
69
73
  <TableCell>{c.to}</TableCell>
70
74
  <TableCell className="capitalize">{c.disposition?.replace(/_/g, " ") ?? "none"}</TableCell>
71
75
  <TableCell>{formatDuration(c.call_duration)}</TableCell>
@@ -55,7 +55,7 @@ function Stat({ label, value }: { label: string; value: string | number }) {
55
55
  );
56
56
  }
57
57
 
58
- function WaitingContacts({ sessionId, version, onSkipped }: { sessionId: string; version: number; onSkipped: () => void }) {
58
+ export function WaitingContacts({ sessionId, version, onSkipped }: { sessionId: string; version: number; onSkipped: () => void }) {
59
59
  const grid = useG8Query(
60
60
  async (signal) => {
61
61
  const env = await g8Post(`/dialer/sessions/${encodeURIComponent(sessionId)}/grid`, { limit: 10, offset: 0, include_contact_data: true }, { signal });
@@ -0,0 +1,319 @@
1
+ "use client";
2
+
3
+ /**
4
+ * DialerScreen: the whole calling page for a rep. Tabs: Session (pick a list, create a session, dial batch by batch,
5
+ * watch live calls, take them in the browser, record outcomes, read the transcript), Sessions (every session as a list;
6
+ * a row opens the full session record with stats, calls, queue, pause / resume / stop; ?tab=sessions&open=<id>&view=full
7
+ * is the full page) and History (the rep's past calls).
8
+ *
9
+ * Flow: choose a list (ListPicker) -> SessionStartForm (caller ID, power or parallel mode; creating it dials nobody)
10
+ * -> CallQueue ("Dial next batch" confirms each batch; pause, stop with confirm, skip contacts, hang up, mute, voicemail
11
+ * drop with confirm) -> Softphone ("Enable calling" confirms the one-hour credential) -> DispositionPanel for the call
12
+ * that just finished -> TranscriptViewer. Resume a session with `?session=<id>`.
13
+ *
14
+ * SDK operations (all through the composed parts): see CallQueue, SessionStartForm, Softphone, DispositionPanel, CallLog.
15
+ * External and destructive steps always open ConfirmSpendDialog; nothing dials by itself and there is no auto-advance.
16
+ * The browser audio vendor is `transport`; by default the Twilio adapter loads on demand (needs @twilio/voice-sdk).
17
+ * Buying or verifying a caller ID number happens in the graph8 app.
18
+ */
19
+ import { useEffect, useMemo, useState, type ReactNode } from "react";
20
+ import { Phone } from "lucide-react";
21
+ import { Button, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
22
+ import { ApiError } from "@/g8/components/api-error";
23
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
24
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
25
+ import { g8Get } from "@/g8/lib/g8-client";
26
+ import { EmptyState } from "@/g8/components/empty-state";
27
+ import { ScreenLayout } from "@/g8/components/screen/screen-layout";
28
+ import { SectionCard } from "@/g8/components/screen/section-card";
29
+ import { useUrlState } from "@/g8/hooks/use-url-state";
30
+ import { ListPicker, type ListSummary } from "@/g8/lists/components/list-picker";
31
+ import { SplitView } from "@/g8/components/screen/split-view";
32
+ import { StatusPill } from "@/g8/components/screen/status-pill";
33
+ import { Toolbar } from "@/g8/components/screen/toolbar";
34
+ import { DialerSessionDetail } from "./dialer-session-detail";
35
+ import { dataOf, isLiveCall, normalizeStatus, needsDisposition, type LiveCall, type NextBatchResult, type SessionDetail } from "../lib/dialer-types";
36
+ import type { SoftphoneTransport } from "../lib/softphone-transport";
37
+ import { CallHistory } from "./call-log-screen";
38
+ import { CallQueue } from "./call-queue";
39
+ import { DispositionPanel } from "./disposition-panel";
40
+ import { Softphone } from "./softphone";
41
+ import { TranscriptViewer } from "./transcript-viewer";
42
+ import { rowActivationProps } from "@/g8/hooks/use-row-activation";
43
+ import { RecordLink } from "@/g8/components/screen/record-link";
44
+
45
+ export interface DialerScreenProps {
46
+ /** The rep these calls belong to. When omitted the start form asks for the graph8 user id. */
47
+ userId?: string;
48
+ userEmail?: string;
49
+ /** Skip the list picker. */
50
+ listId?: string;
51
+ listTitle?: string;
52
+ /** Resume this session. */
53
+ sessionId?: string;
54
+ /** Your audio client. Default: the Twilio adapter, loaded on demand. */
55
+ transport?: SoftphoneTransport;
56
+ toolbarActions?: ReactNode;
57
+ renderRowActions?: (call: LiveCall) => ReactNode;
58
+ renderDetailActions?: (call: LiveCall) => ReactNode;
59
+ extraDetailTabs?: Array<{ id: string; label: string; render: (call: LiveCall) => ReactNode }>;
60
+ className?: string;
61
+ }
62
+
63
+ const TABS = [
64
+ { id: "session", label: "Session" },
65
+ { id: "sessions", label: "Sessions" },
66
+ { id: "history", label: "History" },
67
+ ];
68
+
69
+ interface SessionRow {
70
+ session_id: string;
71
+ name?: string | null;
72
+ status?: string | null;
73
+ total_calls?: number | null;
74
+ user_email?: string | null;
75
+ from_phone?: string | null;
76
+ created_at?: unknown;
77
+ session_metadata?: Record<string, unknown> | null;
78
+ }
79
+
80
+ const STATUSES = ["ACTIVE", "PAUSED", "COMPLETED"];
81
+
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
+ function sessionTone(status?: string | null) {
92
+ const s = normalizeStatus(status);
93
+ return s === "active" ? "positive" : s === "paused" ? "warning" : "neutral";
94
+ }
95
+
96
+ function created(v: unknown): string {
97
+ const d = typeof v === "string" ? new Date(v) : null;
98
+ return d && !Number.isNaN(d.getTime()) ? d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : "";
99
+ }
100
+
101
+ /** All dialing sessions (GET /voice/dialer/sessions). A row opens the full session record: ?tab=sessions&open=<id>[&view=full]. */
102
+ function SessionsTab({ userEmail, onWork }: { userEmail?: string; onWork: (id: string) => void }) {
103
+ // Its own param: `session` belongs to the Session tab (the running session); closing a detail here must never clear it.
104
+ const [openId, setOpenId] = useUrlState("open");
105
+ const [view, setView] = useUrlState("view");
106
+ const [page, setPage] = useState(1);
107
+ const [status, setStatus] = useState("");
108
+ const [name, setName] = useState("");
109
+ const search = useDebounced(name.trim());
110
+ const q = useG8Query(
111
+ async (signal) => {
112
+ const env = await g8Get("/voice/dialer/sessions", { page, page_size: 25, status: status || undefined, name: search || undefined, user_email: userEmail }, { signal });
113
+ const d = dataOf<{ sessions?: SessionRow[]; pagination?: { total_items?: number; total_pages?: number } }>(env);
114
+ return { rows: d.sessions ?? [], total: d.pagination?.total_items, pages: d.pagination?.total_pages };
115
+ },
116
+ [page, status, search, userEmail],
117
+ );
118
+ const rows = q.data?.rows ?? [];
119
+ const reset = () => setPage(1);
120
+ const list = (
121
+ <div className="space-y-3">
122
+ <Toolbar
123
+ search={{ value: name, onChange: (v) => { setName(v); reset(); }, placeholder: "Search by session name" }}
124
+ 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>
129
+ }
130
+ />
131
+ {q.error ? (
132
+ <ApiError error={q.error} title="Could not load sessions" onRetry={() => void q.refetch()} />
133
+ ) : q.loading && !q.data ? (
134
+ <Skeleton className="h-32 w-full" aria-label="Loading sessions" />
135
+ ) : 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."} />
137
+ ) : (
138
+ <Table aria-label="Dialing sessions">
139
+ <TableHeader><TableRow><TableHead>Session</TableHead><TableHead>Status</TableHead><TableHead>Calls</TableHead><TableHead>Rep</TableHead><TableHead>Started</TableHead></TableRow></TableHeader>
140
+ <TableBody>
141
+ {rows.map((r) => (
142
+ <TableRow key={r.session_id} data-state={openId === r.session_id ? "selected" : undefined} className="cursor-pointer" {...rowActivationProps(() => setOpenId(r.session_id))}>
143
+ <TableCell className="font-medium"><RecordLink param="open" id={r.session_id} onOpen={() => setOpenId(r.session_id)}>{r.name ?? "Dialing session"}</RecordLink></TableCell>
144
+ <TableCell><StatusPill tone={sessionTone(r.status)}>{normalizeStatus(r.status) || "unknown"}</StatusPill></TableCell>
145
+ <TableCell className="tabular-nums">{r.total_calls ?? ""}</TableCell>
146
+ <TableCell>{r.user_email ?? ""}</TableCell>
147
+ <TableCell>{created(r.created_at)}</TableCell>
148
+ </TableRow>
149
+ ))}
150
+ </TableBody>
151
+ </Table>
152
+ )}
153
+ <div className="flex items-center justify-between text-sm text-muted-foreground">
154
+ <span>{q.data?.total != null ? `${q.data.total.toLocaleString()} sessions` : `Page ${page}`}</span>
155
+ <div className="flex gap-2">
156
+ <Button size="sm" variant="outline" disabled={page <= 1} onClick={() => setPage(page - 1)}>Previous</Button>
157
+ <Button size="sm" variant="outline" disabled={q.data?.pages != null ? page >= q.data.pages : rows.length < 25} onClick={() => setPage(page + 1)}>Next</Button>
158
+ </div>
159
+ </div>
160
+ </div>
161
+ );
162
+ const expanded = view === "full" && Boolean(openId);
163
+ return (
164
+ <SplitView
165
+ list={list}
166
+ detailOpen={Boolean(openId)}
167
+ onCloseDetail={() => { setOpenId(null); setView(null); }}
168
+ expanded={expanded}
169
+ onExpandedChange={(next) => setView(next ? "full" : null)}
170
+ detail={openId ? (
171
+ <DialerSessionDetail
172
+ key={openId}
173
+ sessionId={openId}
174
+ userEmail={userEmail}
175
+ onWork={onWork}
176
+ onClose={() => { setOpenId(null); setView(null); }}
177
+ onExpand={expanded ? undefined : () => setView("full")}
178
+ onCollapse={expanded ? () => setView(null) : undefined}
179
+ onChanged={() => void q.refetch()}
180
+ />
181
+ ) : null}
182
+ />
183
+ );
184
+ }
185
+
186
+ /** The Twilio adapter, created once in the browser; null until loaded or when the optional peer is missing. */
187
+ function useDefaultTransport(provided?: SoftphoneTransport): SoftphoneTransport | null {
188
+ const [loaded, setLoaded] = useState<SoftphoneTransport | null>(null);
189
+ useEffect(() => {
190
+ if (provided) return;
191
+ let live = true;
192
+ import("../lib/twilio-transport").then((m) => { if (live) setLoaded(m.createTwilioTransport()); }).catch(() => undefined);
193
+ return () => { live = false; };
194
+ }, [provided]);
195
+ return provided ?? loaded;
196
+ }
197
+
198
+ /** Every field of the session in one card; re-read when the call list changes. */
199
+ function SessionDetails({ sessionId, version }: { sessionId: string; version: number }) {
200
+ const q = useG8Query(async (signal) => dataOf<SessionDetail>(await g8Get(`/dialer/sessions/${encodeURIComponent(sessionId)}`, undefined, { signal })), [sessionId, version]);
201
+ const d = q.data;
202
+ if (q.error) return <ApiError error={q.error} title="Could not load the session" onRetry={() => void q.refetch()} />;
203
+ if (!d) return <Skeleton className="h-24 w-full" aria-label="Loading session details" />;
204
+ const dash = "";
205
+ return (
206
+ <SectionCard title="Session details">
207
+ <KeyValueList
208
+ columns={1}
209
+ items={[
210
+ { label: "Name", value: d.name ?? dash },
211
+ { label: "Status", value: String(d.status ?? dash).toLowerCase() },
212
+ { label: "List", value: d.list_title ?? d.list_id ?? dash },
213
+ { label: "Mode", value: d.dialing_mode ? `${d.dialing_mode}${d.dialing_mode === "parallel" ? ` (${Math.min(4, Math.max(1, d.parallel_count ?? 2))} at once)` : ""}` : dash },
214
+ { label: "Caller ID", value: d.from_phone ?? dash },
215
+ { label: "Contacts", value: d.counts?.total ?? dash },
216
+ { label: "Dialed", value: d.counts?.total_dialed ?? dash },
217
+ { label: "Left in queue", value: d.counts?.queue_remaining ?? dash },
218
+ { label: "Voicemails", value: d.live_state?.voicemail_calls ?? dash },
219
+ ]}
220
+ />
221
+ </SectionCard>
222
+ );
223
+ }
224
+
225
+ function SessionTab(props: DialerScreenProps & { onCalls: (calls: LiveCall[]) => void }) {
226
+ const [sessionId, setSessionId] = useUrlState("session", props.sessionId ?? "");
227
+ const [list, setList] = useState<{ id?: string; title?: string } | null>(props.listId ? { id: props.listId, title: props.listTitle } : null);
228
+ const [calls, setCalls] = useState<LiveCall[]>([]);
229
+ const [selected, setSelected] = useState<LiveCall | null>(null);
230
+ const [batch, setBatch] = useState<NextBatchResult | null>(null);
231
+ const transport = useDefaultTransport(props.transport);
232
+ const waiting = useMemo(() => calls.find(needsDisposition) ?? null, [calls]);
233
+ const focus = selected ?? waiting;
234
+ const { onCalls } = props;
235
+ useEffect(() => { onCalls(calls); }, [calls, onCalls]);
236
+
237
+ if (!sessionId && !list) {
238
+ return (
239
+ <SectionCard title="Who do you want to call?" description="Pick a contact list. Creating a session dials nobody; each batch is confirmed before it goes out.">
240
+ <ListPicker type="contacts" allowCreate={false} onChange={(l: ListSummary) => setList({ id: String(l.id), title: l.title })} />
241
+ </SectionCard>
242
+ );
243
+ }
244
+ return (
245
+ <div className="grid gap-4 lg:grid-cols-[minmax(0,3fr)_minmax(0,2fr)]">
246
+ <CallQueue
247
+ sessionId={sessionId || undefined}
248
+ userId={props.userId}
249
+ userEmail={props.userEmail}
250
+ listId={list?.id}
251
+ listTitle={list?.title}
252
+ onSessionCreated={setSessionId}
253
+ onBatch={setBatch}
254
+ onCalls={setCalls}
255
+ onSelectCall={setSelected}
256
+ />
257
+ <div className="space-y-4">
258
+ {props.userId && transport ? (
259
+ <Softphone userId={props.userId} transport={transport} conferenceName={batch?.conference_name} joinMuted={(batch?.batch?.length ?? 1) > 1} />
260
+ ) : props.userId ? (
261
+ <Skeleton className="h-24 w-full" aria-label="Loading softphone" />
262
+ ) : (
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>
264
+ )}
265
+ {sessionId ? (
266
+ <>
267
+ <SessionDetails sessionId={sessionId} version={calls.length} />
268
+ <DispositionPanel key={`outcome-${focus?.call_id}`} call={focus} onSaved={() => setSelected(null)} />
269
+ {focus ? (
270
+ <>
271
+ <TranscriptViewer key={`transcript-${focus.call_id}`} roomName={focus.room_name ?? focus.call_id} live={isLiveCall(focus)} />
272
+ {props.renderDetailActions ? <div className="flex flex-wrap gap-2">{props.renderDetailActions(focus)}</div> : null}
273
+ {(props.extraDetailTabs ?? []).map((t) => <SectionCard key={t.id} title={t.label}>{t.render(focus)}</SectionCard>)}
274
+ </>
275
+ ) : (
276
+ <EmptyState title="Nothing on screen" description="The transcript of the call you select shows here." />
277
+ )}
278
+ </>
279
+ ) : null}
280
+ </div>
281
+ </div>
282
+ );
283
+ }
284
+
285
+ export function DialerScreen(props: DialerScreenProps) {
286
+ const [tab, setTab] = useUrlState("tab", "session");
287
+ const [, setView] = useUrlState("view");
288
+ const [, setSession] = useUrlState("session");
289
+ const [calls, setCalls] = useState<LiveCall[]>([]);
290
+ const active = TABS.some((t) => t.id === tab) ? tab : "session";
291
+ // The header counters describe the Session tab's calls; drop them when it unmounts so they are never stale.
292
+ useEffect(() => { if (active !== "session") setCalls([]); }, [active]);
293
+ const live = calls.filter(isLiveCall).length;
294
+ const stats = calls.length > 0
295
+ ? [
296
+ { label: "On calls now", value: live, tone: live > 0 ? ("positive" as const) : ("neutral" as const) },
297
+ { label: "Need an outcome", value: calls.filter(needsDisposition).length, tone: calls.some(needsDisposition) ? ("warning" as const) : ("neutral" as const) },
298
+ { label: "Voicemails", value: calls.filter((c) => c.is_voicemail).length },
299
+ { label: "Calls this session", value: calls.length },
300
+ ]
301
+ : undefined;
302
+ return (
303
+ <ScreenLayout
304
+ className={props.className}
305
+ title="Dialer"
306
+ description="Call a list batch by batch, take calls in the browser and record every outcome."
307
+ icon={<Phone className="size-5" />}
308
+ actions={props.toolbarActions}
309
+ tabs={TABS}
310
+ activeTab={active}
311
+ onTabChange={(id) => setTab(id)}
312
+ stats={stats}
313
+ >
314
+ {active === "session" ? <SessionTab {...props} onCalls={setCalls} /> : null}
315
+ {active === "sessions" ? <SessionsTab userEmail={props.userEmail} onWork={(id) => { setSession(id); setView(null); setTab("session"); }} /> : null}
316
+ {active === "history" ? <CallHistory userEmail={props.userEmail} /> : null}
317
+ </ScreenLayout>
318
+ );
319
+ }