@graph8/sdk 0.308.1 → 0.357.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 (654) hide show
  1. package/README.md +81 -0
  2. package/blocks/README.md +32 -0
  3. package/blocks/_foundation/README.md +78 -0
  4. package/blocks/_foundation/block.json +79 -0
  5. package/blocks/_foundation/components/api-error.tsx +62 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +142 -0
  7. package/blocks/_foundation/components/empty-state.tsx +24 -0
  8. package/blocks/_foundation/hooks/use-g8-mutation.ts +88 -0
  9. package/blocks/_foundation/hooks/use-g8-query.ts +87 -0
  10. package/blocks/_foundation/hooks/use-job-poll.ts +130 -0
  11. package/blocks/_foundation/hooks/use-paginated.ts +105 -0
  12. package/blocks/_foundation/lib/g8-client.ts +128 -0
  13. package/blocks/_foundation/recipes/verify-proxy-key.ts +18 -0
  14. package/blocks/_foundation/server/proxy.ts +233 -0
  15. package/blocks/_foundation/server/route.ts +30 -0
  16. package/blocks/agents/README.md +133 -0
  17. package/blocks/agents/block.json +223 -0
  18. package/blocks/agents/components/agent-builder.tsx +199 -0
  19. package/blocks/agents/components/agent-knowledge-panel.tsx +144 -0
  20. package/blocks/agents/components/agent-runs.tsx +175 -0
  21. package/blocks/agents/components/agent-skills-panel.tsx +126 -0
  22. package/blocks/agents/components/agent-test-console.tsx +175 -0
  23. package/blocks/agents/lib/agent-types.ts +114 -0
  24. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +50 -0
  25. package/blocks/agents/recipes/run-and-follow-agent-run.ts +46 -0
  26. package/blocks/agents/recipes/test-agent-before-activating.ts +36 -0
  27. package/blocks/analytics/README.md +73 -0
  28. package/blocks/analytics/block.json +178 -0
  29. package/blocks/analytics/components/funnel-chart.tsx +79 -0
  30. package/blocks/analytics/components/kpi-tiles.tsx +74 -0
  31. package/blocks/analytics/components/leaderboard.tsx +86 -0
  32. package/blocks/analytics/components/report-builder.tsx +200 -0
  33. package/blocks/analytics/hooks/use-report.ts +26 -0
  34. package/blocks/analytics/lib/analytics-types.ts +108 -0
  35. package/blocks/analytics/lib/use-debounced.ts +16 -0
  36. package/blocks/analytics/recipes/01-weekly-outbound-report.ts +32 -0
  37. package/blocks/analytics/recipes/02-revenue-snapshot.ts +22 -0
  38. package/blocks/analytics/recipes/03-saved-report.ts +30 -0
  39. package/blocks/appointments/README.md +80 -0
  40. package/blocks/appointments/block.json +218 -0
  41. package/blocks/appointments/components/availability-editor.tsx +192 -0
  42. package/blocks/appointments/components/booking-page.tsx +154 -0
  43. package/blocks/appointments/components/event-type-editor.tsx +214 -0
  44. package/blocks/appointments/components/meeting-detail.tsx +147 -0
  45. package/blocks/appointments/components/meeting-list.tsx +148 -0
  46. package/blocks/appointments/hooks/use-available-slots.ts +29 -0
  47. package/blocks/appointments/hooks/use-schedules.ts +18 -0
  48. package/blocks/appointments/lib/appointments-types.ts +159 -0
  49. package/blocks/appointments/lib/idempotency.ts +30 -0
  50. package/blocks/appointments/recipes/01-create-event-type-and-hours.ts +27 -0
  51. package/blocks/appointments/recipes/02-find-slots-and-book.ts +34 -0
  52. package/blocks/appointments/recipes/03-bookings-digest.ts +22 -0
  53. package/blocks/appointments/recipes/04-calendar-invite.ts +21 -0
  54. package/blocks/block.schema.json +130 -0
  55. package/blocks/block.types.ts +109 -0
  56. package/blocks/campaigns/README.md +118 -0
  57. package/blocks/campaigns/block.json +361 -0
  58. package/blocks/campaigns/components/campaign-assets.tsx +180 -0
  59. package/blocks/campaigns/components/campaign-audience-step.tsx +91 -0
  60. package/blocks/campaigns/components/campaign-launch-step.tsx +147 -0
  61. package/blocks/campaigns/components/campaign-list.tsx +120 -0
  62. package/blocks/campaigns/components/campaign-metrics.tsx +93 -0
  63. package/blocks/campaigns/components/campaign-select.tsx +52 -0
  64. package/blocks/campaigns/components/campaign-sequence-step.tsx +158 -0
  65. package/blocks/campaigns/components/campaign-wizard.tsx +238 -0
  66. package/blocks/campaigns/components/form-field.tsx +29 -0
  67. package/blocks/campaigns/components/icp-editor.tsx +230 -0
  68. package/blocks/campaigns/components/persona-editor.tsx +210 -0
  69. package/blocks/campaigns/hooks/use-company-profile.ts +34 -0
  70. package/blocks/campaigns/lib/campaigns-utils.ts +155 -0
  71. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +33 -0
  72. package/blocks/campaigns/recipes/campaign-report.ts +22 -0
  73. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +51 -0
  74. package/blocks/campaigns/recipes/set-up-icp-and-personas.ts +37 -0
  75. package/blocks/coach/README.md +93 -0
  76. package/blocks/coach/block.json +125 -0
  77. package/blocks/coach/components/call-scorecard.tsx +126 -0
  78. package/blocks/coach/components/coaching-feed.tsx +140 -0
  79. package/blocks/coach/lib/coach-types.ts +116 -0
  80. package/blocks/coach/recipes/read-a-scorecard.ts +27 -0
  81. package/blocks/coach/recipes/weekly-coaching-digest.ts +53 -0
  82. package/blocks/copilot/README.md +99 -0
  83. package/blocks/copilot/block.json +151 -0
  84. package/blocks/copilot/components/copilot-memory.tsx +87 -0
  85. package/blocks/copilot/components/copilot-panel.tsx +189 -0
  86. package/blocks/copilot/components/copilot-team-chat.tsx +111 -0
  87. package/blocks/copilot/lib/copilot-types.ts +111 -0
  88. package/blocks/copilot/recipes/ask-the-copilot.ts +35 -0
  89. package/blocks/copilot/recipes/post-in-a-work-channel.ts +33 -0
  90. package/blocks/copilot/recipes/remember-and-recall.ts +19 -0
  91. package/blocks/crm/README.md +122 -0
  92. package/blocks/crm/block.json +378 -0
  93. package/blocks/crm/components/activity-timeline.tsx +99 -0
  94. package/blocks/crm/components/company-detail.tsx +118 -0
  95. package/blocks/crm/components/company-table.tsx +173 -0
  96. package/blocks/crm/components/contact-detail.tsx +135 -0
  97. package/blocks/crm/components/contact-table.tsx +217 -0
  98. package/blocks/crm/components/dedupe-review.tsx +164 -0
  99. package/blocks/crm/components/import-wizard.tsx +193 -0
  100. package/blocks/crm/components/notes-panel.tsx +112 -0
  101. package/blocks/crm/components/record-form.tsx +191 -0
  102. package/blocks/crm/components/task-list.tsx +107 -0
  103. package/blocks/crm/hooks/use-custom-fields.ts +18 -0
  104. package/blocks/crm/hooks/use-debounced.ts +13 -0
  105. package/blocks/crm/hooks/use-page-query.ts +52 -0
  106. package/blocks/crm/lib/crm-types.ts +136 -0
  107. package/blocks/crm/recipes/custom-fields-and-values.ts +28 -0
  108. package/blocks/crm/recipes/dedupe-and-merge.ts +30 -0
  109. package/blocks/crm/recipes/notes-tasks-and-activity.ts +23 -0
  110. package/blocks/crm/recipes/upsert-contact-with-company.ts +33 -0
  111. package/blocks/deals/README.md +78 -0
  112. package/blocks/deals/block.json +198 -0
  113. package/blocks/deals/components/deal-detail.tsx +134 -0
  114. package/blocks/deals/components/forecast-view.tsx +112 -0
  115. package/blocks/deals/components/pipeline-board.tsx +142 -0
  116. package/blocks/deals/components/stage-editor.tsx +96 -0
  117. package/blocks/deals/hooks/use-pipeline-deals.ts +67 -0
  118. package/blocks/deals/hooks/use-pipelines.ts +19 -0
  119. package/blocks/deals/lib/deals-types.ts +128 -0
  120. package/blocks/deals/recipes/01-create-deal-and-advance.ts +32 -0
  121. package/blocks/deals/recipes/02-pipeline-report.ts +35 -0
  122. package/blocks/deals/recipes/03-stale-deals-sweep.ts +20 -0
  123. package/blocks/dialer/README.md +114 -0
  124. package/blocks/dialer/block.json +260 -0
  125. package/blocks/dialer/components/call-log.tsx +78 -0
  126. package/blocks/dialer/components/call-queue.tsx +188 -0
  127. package/blocks/dialer/components/dialer-workspace.tsx +47 -0
  128. package/blocks/dialer/components/disposition-panel.tsx +75 -0
  129. package/blocks/dialer/components/live-calls-table.tsx +107 -0
  130. package/blocks/dialer/components/session-start-form.tsx +111 -0
  131. package/blocks/dialer/components/softphone.tsx +122 -0
  132. package/blocks/dialer/components/transcript-viewer.tsx +82 -0
  133. package/blocks/dialer/hooks/use-dialer-numbers.ts +19 -0
  134. package/blocks/dialer/hooks/use-dialer-session.ts +68 -0
  135. package/blocks/dialer/hooks/use-dispositions.ts +29 -0
  136. package/blocks/dialer/lib/dialer-types.ts +158 -0
  137. package/blocks/dialer/lib/softphone-transport.ts +67 -0
  138. package/blocks/dialer/lib/twilio-transport.ts +74 -0
  139. package/blocks/dialer/recipes/01-create-session-and-dial.ts +39 -0
  140. package/blocks/dialer/recipes/02-disposition-calls.ts +26 -0
  141. package/blocks/dialer/recipes/03-review-a-call.ts +23 -0
  142. package/blocks/enrichment/README.md +105 -0
  143. package/blocks/enrichment/block.json +204 -0
  144. package/blocks/enrichment/components/bulk-enrich-panel.tsx +205 -0
  145. package/blocks/enrichment/components/email-verify-badge.tsx +82 -0
  146. package/blocks/enrichment/components/enrich-button.tsx +136 -0
  147. package/blocks/enrichment/components/waterfall-config.tsx +127 -0
  148. package/blocks/enrichment/hooks/use-providers.ts +18 -0
  149. package/blocks/enrichment/lib/enrichment-types.ts +104 -0
  150. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +41 -0
  151. package/blocks/enrichment/recipes/lookup-and-verify.ts +25 -0
  152. package/blocks/enrichment/recipes/waterfall-pipeline.ts +26 -0
  153. package/blocks/forms/README.md +112 -0
  154. package/blocks/forms/block.json +200 -0
  155. package/blocks/forms/components/form-builder.tsx +150 -0
  156. package/blocks/forms/components/form-embed.tsx +103 -0
  157. package/blocks/forms/components/form-mapping-editor.tsx +125 -0
  158. package/blocks/forms/components/site-audit-panel.tsx +73 -0
  159. package/blocks/forms/components/submissions-table.tsx +158 -0
  160. package/blocks/forms/lib/form-types.ts +127 -0
  161. package/blocks/forms/recipes/install-snippet-and-form.ts +21 -0
  162. package/blocks/forms/recipes/retry-failed-deliveries.ts +26 -0
  163. package/blocks/forms/recipes/route-form-to-list.ts +44 -0
  164. package/blocks/inbox/README.md +72 -0
  165. package/blocks/inbox/block.json +194 -0
  166. package/blocks/inbox/components/inbox-list.tsx +156 -0
  167. package/blocks/inbox/components/inbox-screen.tsx +49 -0
  168. package/blocks/inbox/components/reply-composer.tsx +183 -0
  169. package/blocks/inbox/components/tag-and-assign.tsx +189 -0
  170. package/blocks/inbox/components/thread-badge.tsx +17 -0
  171. package/blocks/inbox/components/thread-view.tsx +108 -0
  172. package/blocks/inbox/hooks/use-inbox-thread.ts +24 -0
  173. package/blocks/inbox/hooks/use-inbox-threads.ts +40 -0
  174. package/blocks/inbox/lib/idempotency.ts +30 -0
  175. package/blocks/inbox/lib/inbox-types.ts +100 -0
  176. package/blocks/inbox/recipes/01-triage-and-reply.ts +43 -0
  177. package/blocks/inbox/recipes/02-tag-and-assign.ts +31 -0
  178. package/blocks/inbox/recipes/03-hot-contacts-digest.ts +16 -0
  179. package/blocks/integrations/README.md +75 -0
  180. package/blocks/integrations/block.json +172 -0
  181. package/blocks/integrations/components/integration-list.tsx +165 -0
  182. package/blocks/integrations/components/sync-status.tsx +223 -0
  183. package/blocks/integrations/components/webhook-manager.tsx +300 -0
  184. package/blocks/integrations/lib/integrations-utils.ts +62 -0
  185. package/blocks/integrations/recipes/crm-sync-health.ts +19 -0
  186. package/blocks/integrations/recipes/push-contacts-to-crm.ts +20 -0
  187. package/blocks/integrations/recipes/register-webhook.ts +18 -0
  188. package/blocks/integrations/recipes/verify-webhook-route.ts +33 -0
  189. package/blocks/intent/README.md +57 -0
  190. package/blocks/intent/block.json +138 -0
  191. package/blocks/intent/components/account-intent-score.tsx +91 -0
  192. package/blocks/intent/components/intent-feed.tsx +215 -0
  193. package/blocks/intent/components/intent-keyword-manager.tsx +331 -0
  194. package/blocks/intent/hooks/use-account-intent.ts +46 -0
  195. package/blocks/intent/lib/intent-utils.ts +95 -0
  196. package/blocks/intent/recipes/rank-intent-accounts.ts +36 -0
  197. package/blocks/intent/recipes/track-competitor-domain.ts +23 -0
  198. package/blocks/intent/recipes/who-visited-url.ts +18 -0
  199. package/blocks/lifecycle/README.md +79 -0
  200. package/blocks/lifecycle/block.json +198 -0
  201. package/blocks/lifecycle/components/churn-list.tsx +92 -0
  202. package/blocks/lifecycle/components/customer-table.tsx +119 -0
  203. package/blocks/lifecycle/components/lead-queue.tsx +127 -0
  204. package/blocks/lifecycle/components/lifecycle-board.tsx +102 -0
  205. package/blocks/lifecycle/components/stage-move-dialog.tsx +75 -0
  206. package/blocks/lifecycle/hooks/use-debounced.ts +13 -0
  207. package/blocks/lifecycle/hooks/use-lifecycle-stages.ts +27 -0
  208. package/blocks/lifecycle/lib/lifecycle.ts +99 -0
  209. package/blocks/lifecycle/recipes/customer-brief.ts +23 -0
  210. package/blocks/lifecycle/recipes/move-company-stage.ts +17 -0
  211. package/blocks/lifecycle/recipes/read-lifecycle-board.ts +21 -0
  212. package/blocks/lifecycle/recipes/unlock-stage-contacts.ts +25 -0
  213. package/blocks/lists/README.md +96 -0
  214. package/blocks/lists/block.json +166 -0
  215. package/blocks/lists/components/add-to-list-button.tsx +111 -0
  216. package/blocks/lists/components/list-picker.tsx +94 -0
  217. package/blocks/lists/components/list-table.tsx +160 -0
  218. package/blocks/lists/components/suppression-manager.tsx +153 -0
  219. package/blocks/lists/recipes/create-list-and-add-contacts.ts +35 -0
  220. package/blocks/lists/recipes/refresh-and-check-lists.ts +21 -0
  221. package/blocks/lists/recipes/suppress-and-reinstate.ts +30 -0
  222. package/blocks/mailboxes/README.md +71 -0
  223. package/blocks/mailboxes/block.json +187 -0
  224. package/blocks/mailboxes/components/connect-mailbox.tsx +284 -0
  225. package/blocks/mailboxes/components/domain-purchase.tsx +271 -0
  226. package/blocks/mailboxes/components/mailbox-list.tsx +185 -0
  227. package/blocks/mailboxes/components/warmup-health.tsx +232 -0
  228. package/blocks/mailboxes/lib/mailboxes-utils.ts +69 -0
  229. package/blocks/mailboxes/recipes/buy-domain-and-mailboxes.ts +64 -0
  230. package/blocks/mailboxes/recipes/connect-imap-mailbox.ts +38 -0
  231. package/blocks/mailboxes/recipes/is-it-safe-to-send.ts +17 -0
  232. package/blocks/mailboxes/recipes/warmup-report.ts +31 -0
  233. package/blocks/newsletter/README.md +122 -0
  234. package/blocks/newsletter/block.json +224 -0
  235. package/blocks/newsletter/components/issue-form.tsx +124 -0
  236. package/blocks/newsletter/components/issue-scheduler.tsx +152 -0
  237. package/blocks/newsletter/components/newsletter-editor.tsx +131 -0
  238. package/blocks/newsletter/components/newsletter-stats.tsx +93 -0
  239. package/blocks/newsletter/components/subscriber-table.tsx +173 -0
  240. package/blocks/newsletter/lib/newsletter-types.ts +97 -0
  241. package/blocks/newsletter/recipes/create-newsletter-and-draft-issue.ts +27 -0
  242. package/blocks/newsletter/recipes/import-audience-and-send.ts +43 -0
  243. package/blocks/newsletter/recipes/issue-performance-report.ts +26 -0
  244. package/blocks/nurture/README.md +110 -0
  245. package/blocks/nurture/block.json +191 -0
  246. package/blocks/nurture/components/nurture-builder.tsx +114 -0
  247. package/blocks/nurture/components/nurture-enroll.tsx +107 -0
  248. package/blocks/nurture/components/nurture-form.tsx +120 -0
  249. package/blocks/nurture/components/nurture-stats.tsx +81 -0
  250. package/blocks/nurture/lib/nurture-types.ts +109 -0
  251. package/blocks/nurture/recipes/create-a-nurture.ts +34 -0
  252. package/blocks/nurture/recipes/enroll-contacts-and-launch.ts +24 -0
  253. package/blocks/nurture/recipes/nurture-health-check.ts +28 -0
  254. package/blocks/objects/README.md +120 -0
  255. package/blocks/objects/block.json +243 -0
  256. package/blocks/objects/components/attribute-field-input.tsx +82 -0
  257. package/blocks/objects/components/attribute-form-dialog.tsx +159 -0
  258. package/blocks/objects/components/object-not-found.tsx +34 -0
  259. package/blocks/objects/components/object-record-detail.tsx +143 -0
  260. package/blocks/objects/components/object-schema-editor.tsx +197 -0
  261. package/blocks/objects/components/object-table.tsx +130 -0
  262. package/blocks/objects/components/record-form-dialog.tsx +112 -0
  263. package/blocks/objects/components/record-roles-panel.tsx +106 -0
  264. package/blocks/objects/hooks/use-object-schema.ts +36 -0
  265. package/blocks/objects/lib/object-types.ts +214 -0
  266. package/blocks/objects/recipes/define-object-and-add-records.ts +24 -0
  267. package/blocks/objects/recipes/update-audit-and-archive-records.ts +39 -0
  268. package/blocks/objects/recipes/upsert-records-from-your-system.ts +30 -0
  269. package/blocks/pages/README.md +104 -0
  270. package/blocks/pages/block.json +223 -0
  271. package/blocks/pages/components/brand-kit-editor.tsx +152 -0
  272. package/blocks/pages/components/content-library.tsx +163 -0
  273. package/blocks/pages/components/page-editor.tsx +253 -0
  274. package/blocks/pages/components/page-list.tsx +159 -0
  275. package/blocks/pages/components/seo-report.tsx +159 -0
  276. package/blocks/pages/lib/studio.ts +77 -0
  277. package/blocks/pages/recipes/audit-site-seo.ts +28 -0
  278. package/blocks/pages/recipes/create-and-publish-page.ts +26 -0
  279. package/blocks/pages/recipes/edit-page-section.ts +25 -0
  280. package/blocks/pages/recipes/refresh-brand-kit.ts +22 -0
  281. package/blocks/quotes/README.md +79 -0
  282. package/blocks/quotes/block.json +183 -0
  283. package/blocks/quotes/components/product-picker.tsx +80 -0
  284. package/blocks/quotes/components/quote-builder.tsx +195 -0
  285. package/blocks/quotes/components/quote-list.tsx +79 -0
  286. package/blocks/quotes/components/quote-viewer.tsx +135 -0
  287. package/blocks/quotes/components/signature-status.tsx +77 -0
  288. package/blocks/quotes/hooks/use-debounced.ts +13 -0
  289. package/blocks/quotes/lib/quotes.ts +142 -0
  290. package/blocks/quotes/recipes/create-and-send-quote.ts +44 -0
  291. package/blocks/quotes/recipes/revise-sent-quote.ts +18 -0
  292. package/blocks/quotes/recipes/track-quote-signature.ts +22 -0
  293. package/blocks/radar/README.md +106 -0
  294. package/blocks/radar/block.json +189 -0
  295. package/blocks/radar/components/competitor-battle-card.tsx +53 -0
  296. package/blocks/radar/components/competitor-tracker.tsx +158 -0
  297. package/blocks/radar/components/radar-overview.tsx +67 -0
  298. package/blocks/radar/components/radar-report.tsx +86 -0
  299. package/blocks/radar/hooks/use-competitors.ts +23 -0
  300. package/blocks/radar/lib/radar-types.ts +124 -0
  301. package/blocks/radar/recipes/battle-card.ts +28 -0
  302. package/blocks/radar/recipes/deep-dive-report.ts +32 -0
  303. package/blocks/radar/recipes/marketing-snapshot.ts +18 -0
  304. package/blocks/radar/recipes/track-and-scan.ts +39 -0
  305. package/blocks/search/README.md +129 -0
  306. package/blocks/search/block.json +185 -0
  307. package/blocks/search/components/filter-builder.tsx +149 -0
  308. package/blocks/search/components/prospect-results.tsx +175 -0
  309. package/blocks/search/components/save-to-list-dialog.tsx +135 -0
  310. package/blocks/search/components/saved-searches.tsx +95 -0
  311. package/blocks/search/hooks/use-filter-options.ts +41 -0
  312. package/blocks/search/lib/search-types.ts +149 -0
  313. package/blocks/search/recipes/explore-filter-values.ts +27 -0
  314. package/blocks/search/recipes/saved-searches.ts +34 -0
  315. package/blocks/search/recipes/search-and-save-to-list.ts +37 -0
  316. package/blocks/sequencer/README.md +87 -0
  317. package/blocks/sequencer/block.json +214 -0
  318. package/blocks/sequencer/components/enroll-dialog.tsx +93 -0
  319. package/blocks/sequencer/components/sequence-builder.tsx +306 -0
  320. package/blocks/sequencer/components/sequence-list.tsx +200 -0
  321. package/blocks/sequencer/components/sequence-stats.tsx +256 -0
  322. package/blocks/sequencer/components/step-editor.tsx +213 -0
  323. package/blocks/sequencer/lib/sequencer-utils.ts +168 -0
  324. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +51 -0
  325. package/blocks/sequencer/recipes/launch-and-enroll.ts +44 -0
  326. package/blocks/sequencer/recipes/pause-when-unsafe.ts +30 -0
  327. package/blocks/sequencer/recipes/sequence-report.ts +22 -0
  328. package/blocks/social-ads/README.md +128 -0
  329. package/blocks/social-ads/block.json +256 -0
  330. package/blocks/social-ads/components/ad-campaign-list.tsx +136 -0
  331. package/blocks/social-ads/components/audience-sync.tsx +150 -0
  332. package/blocks/social-ads/components/creative-gallery.tsx +121 -0
  333. package/blocks/social-ads/components/post-composer.tsx +122 -0
  334. package/blocks/social-ads/components/social-post-list.tsx +116 -0
  335. package/blocks/social-ads/hooks/use-social-accounts.ts +16 -0
  336. package/blocks/social-ads/lib/social-ads-types.ts +167 -0
  337. package/blocks/social-ads/recipes/creative-to-gallery.ts +41 -0
  338. package/blocks/social-ads/recipes/schedule-post.ts +51 -0
  339. package/blocks/social-ads/recipes/sync-audience-and-launch.ts +41 -0
  340. package/blocks/visitors/README.md +56 -0
  341. package/blocks/visitors/block.json +139 -0
  342. package/blocks/visitors/components/snippet-install.tsx +169 -0
  343. package/blocks/visitors/components/traffic-chart.tsx +156 -0
  344. package/blocks/visitors/components/visitor-company-card.tsx +92 -0
  345. package/blocks/visitors/components/visitor-feed.tsx +157 -0
  346. package/blocks/visitors/lib/visitors-utils.ts +78 -0
  347. package/blocks/visitors/recipes/check-snippet-installed.ts +24 -0
  348. package/blocks/visitors/recipes/recent-company-visits.ts +27 -0
  349. package/blocks/visitors/recipes/traffic-summary.ts +24 -0
  350. package/blocks/webchat/README.md +87 -0
  351. package/blocks/webchat/block.json +134 -0
  352. package/blocks/webchat/components/chat-widget-config.tsx +160 -0
  353. package/blocks/webchat/components/conversation-list.tsx +74 -0
  354. package/blocks/webchat/lib/webchat-types.ts +83 -0
  355. package/blocks/webchat/recipes/create-bot-and-style-widget.ts +24 -0
  356. package/blocks/webchat/recipes/list-chat-visitors.ts +24 -0
  357. package/blocks/workbench/README.md +135 -0
  358. package/blocks/workbench/block.json +259 -0
  359. package/blocks/workbench/components/column-recipe-editor.tsx +183 -0
  360. package/blocks/workbench/components/run-progress.tsx +132 -0
  361. package/blocks/workbench/components/workbench-add-rows.tsx +100 -0
  362. package/blocks/workbench/components/workbench-grid.tsx +159 -0
  363. package/blocks/workbench/components/workbench-row-actions.tsx +102 -0
  364. package/blocks/workbench/components/workbench-table-list.tsx +105 -0
  365. package/blocks/workbench/hooks/use-workbench-table.ts +30 -0
  366. package/blocks/workbench/lib/waterfall-presets.ts +119 -0
  367. package/blocks/workbench/lib/workbench-types.ts +191 -0
  368. package/blocks/workbench/recipes/build-enriched-table.ts +65 -0
  369. package/blocks/workbench/recipes/import-from-search.ts +42 -0
  370. package/blocks/workbench/recipes/scrape-into-table.ts +36 -0
  371. package/blocks/workbench/recipes/waterfall-column.ts +55 -0
  372. package/blocks/workflows/README.md +100 -0
  373. package/blocks/workflows/block.json +188 -0
  374. package/blocks/workflows/components/run-history.tsx +136 -0
  375. package/blocks/workflows/components/trigger-config.tsx +134 -0
  376. package/blocks/workflows/components/workflow-editor.tsx +142 -0
  377. package/blocks/workflows/components/workflow-list.tsx +153 -0
  378. package/blocks/workflows/hooks/use-workflow.ts +51 -0
  379. package/blocks/workflows/lib/graph.ts +187 -0
  380. package/blocks/workflows/recipes/create-workflow.ts +35 -0
  381. package/blocks/workflows/recipes/find-failed-runs.ts +15 -0
  382. package/blocks/workflows/recipes/run-workflow-and-wait.ts +32 -0
  383. package/blocks/workflows/recipes/set-trigger.ts +22 -0
  384. package/dist/cli.js +5330 -29
  385. package/dist/index.browser.js +7740 -0
  386. package/dist/index.browser.js.map +1 -0
  387. package/dist/index.browser.mjs +7717 -0
  388. package/dist/index.browser.mjs.map +1 -0
  389. package/dist/index.d.mts +1440 -21
  390. package/dist/index.d.ts +1440 -21
  391. package/dist/index.js +42 -3
  392. package/dist/index.js.map +1 -1
  393. package/dist/index.mjs +42 -3
  394. package/dist/index.mjs.map +1 -1
  395. package/dist/react.browser.js +7598 -0
  396. package/dist/react.browser.js.map +1 -0
  397. package/dist/react.browser.mjs +7568 -0
  398. package/dist/react.browser.mjs.map +1 -0
  399. package/dist/react.d.mts +1340 -18
  400. package/dist/react.d.ts +1340 -18
  401. package/dist/react.js +41 -2
  402. package/dist/react.js.map +1 -1
  403. package/dist/react.mjs +41 -2
  404. package/dist/react.mjs.map +1 -1
  405. package/dist/types/agentSetup.d.ts +6 -6
  406. package/dist/types/blocks/add.d.ts +53 -0
  407. package/dist/types/blocks/create.d.ts +46 -0
  408. package/dist/types/blocks/doctor.d.ts +26 -0
  409. package/dist/types/blocks/registry.d.ts +134 -0
  410. package/dist/types/cli.d.ts +16 -0
  411. package/dist/types/core.d.ts +1 -1
  412. package/dist/types/crypto-browser.d.ts +2 -0
  413. package/dist/types/crypto-node.d.ts +6 -0
  414. package/dist/types/react.d.ts +1 -1
  415. package/dist/types/version.d.ts +2 -2
  416. package/dist/types/webhooks.d.ts +2 -2
  417. package/dist/ui/index.d.mts +54 -0
  418. package/dist/ui/index.d.ts +54 -0
  419. package/dist/ui/index.js +105 -0
  420. package/dist/ui/index.js.map +1 -0
  421. package/dist/ui/index.mjs +44 -0
  422. package/dist/ui/index.mjs.map +1 -0
  423. package/dist/ui/lib/compose-refs.d.mts +19 -0
  424. package/dist/ui/lib/compose-refs.d.ts +19 -0
  425. package/dist/ui/lib/compose-refs.js +76 -0
  426. package/dist/ui/lib/compose-refs.js.map +1 -0
  427. package/dist/ui/lib/compose-refs.mjs +41 -0
  428. package/dist/ui/lib/compose-refs.mjs.map +1 -0
  429. package/dist/ui/lib/use-mobile.d.mts +4 -0
  430. package/dist/ui/lib/use-mobile.d.ts +4 -0
  431. package/dist/ui/lib/use-mobile.js +57 -0
  432. package/dist/ui/lib/use-mobile.js.map +1 -0
  433. package/dist/ui/lib/use-mobile.mjs +22 -0
  434. package/dist/ui/lib/use-mobile.mjs.map +1 -0
  435. package/dist/ui/lib/utils.d.mts +10 -0
  436. package/dist/ui/lib/utils.d.ts +10 -0
  437. package/dist/ui/lib/utils.js +33 -0
  438. package/dist/ui/lib/utils.js.map +1 -0
  439. package/dist/ui/lib/utils.mjs +9 -0
  440. package/dist/ui/lib/utils.mjs.map +1 -0
  441. package/dist/ui/primitives/alert-dialog.d.mts +22 -0
  442. package/dist/ui/primitives/alert-dialog.d.ts +22 -0
  443. package/dist/ui/primitives/alert-dialog.js +163 -0
  444. package/dist/ui/primitives/alert-dialog.js.map +1 -0
  445. package/dist/ui/primitives/alert-dialog.mjs +119 -0
  446. package/dist/ui/primitives/alert-dialog.mjs.map +1 -0
  447. package/dist/ui/primitives/avatar.d.mts +8 -0
  448. package/dist/ui/primitives/avatar.d.ts +8 -0
  449. package/dist/ui/primitives/avatar.js +91 -0
  450. package/dist/ui/primitives/avatar.js.map +1 -0
  451. package/dist/ui/primitives/avatar.mjs +55 -0
  452. package/dist/ui/primitives/avatar.mjs.map +1 -0
  453. package/dist/ui/primitives/breadcrumb.d.mts +13 -0
  454. package/dist/ui/primitives/breadcrumb.d.ts +13 -0
  455. package/dist/ui/primitives/breadcrumb.js +127 -0
  456. package/dist/ui/primitives/breadcrumb.js.map +1 -0
  457. package/dist/ui/primitives/breadcrumb.mjs +97 -0
  458. package/dist/ui/primitives/breadcrumb.mjs.map +1 -0
  459. package/dist/ui/primitives/button.d.mts +15 -0
  460. package/dist/ui/primitives/button.d.ts +15 -0
  461. package/dist/ui/primitives/button.js +91 -0
  462. package/dist/ui/primitives/button.js.map +1 -0
  463. package/dist/ui/primitives/button.mjs +56 -0
  464. package/dist/ui/primitives/button.mjs.map +1 -0
  465. package/dist/ui/primitives/card.d.mts +10 -0
  466. package/dist/ui/primitives/card.d.ts +10 -0
  467. package/dist/ui/primitives/card.js +101 -0
  468. package/dist/ui/primitives/card.js.map +1 -0
  469. package/dist/ui/primitives/card.mjs +62 -0
  470. package/dist/ui/primitives/card.mjs.map +1 -0
  471. package/dist/ui/primitives/carousel.d.mts +45 -0
  472. package/dist/ui/primitives/carousel.d.ts +45 -0
  473. package/dist/ui/primitives/carousel.js +300 -0
  474. package/dist/ui/primitives/carousel.js.map +1 -0
  475. package/dist/ui/primitives/carousel.mjs +261 -0
  476. package/dist/ui/primitives/carousel.mjs.map +1 -0
  477. package/dist/ui/primitives/chart.d.mts +68 -0
  478. package/dist/ui/primitives/chart.d.ts +68 -0
  479. package/dist/ui/primitives/chart.js +280 -0
  480. package/dist/ui/primitives/chart.js.map +1 -0
  481. package/dist/ui/primitives/chart.mjs +241 -0
  482. package/dist/ui/primitives/chart.mjs.map +1 -0
  483. package/dist/ui/primitives/collapsible.d.mts +8 -0
  484. package/dist/ui/primitives/collapsible.d.ts +8 -0
  485. package/dist/ui/primitives/collapsible.js +72 -0
  486. package/dist/ui/primitives/collapsible.js.map +1 -0
  487. package/dist/ui/primitives/collapsible.mjs +36 -0
  488. package/dist/ui/primitives/collapsible.mjs.map +1 -0
  489. package/dist/ui/primitives/context-menu.d.mts +29 -0
  490. package/dist/ui/primitives/context-menu.d.ts +29 -0
  491. package/dist/ui/primitives/context-menu.js +203 -0
  492. package/dist/ui/primitives/context-menu.js.map +1 -0
  493. package/dist/ui/primitives/context-menu.mjs +155 -0
  494. package/dist/ui/primitives/context-menu.mjs.map +1 -0
  495. package/dist/ui/primitives/dialog.d.mts +26 -0
  496. package/dist/ui/primitives/dialog.d.ts +26 -0
  497. package/dist/ui/primitives/dialog.js +192 -0
  498. package/dist/ui/primitives/dialog.js.map +1 -0
  499. package/dist/ui/primitives/dialog.mjs +148 -0
  500. package/dist/ui/primitives/dialog.mjs.map +1 -0
  501. package/dist/ui/primitives/drawer.d.mts +26 -0
  502. package/dist/ui/primitives/drawer.d.ts +26 -0
  503. package/dist/ui/primitives/drawer.js +146 -0
  504. package/dist/ui/primitives/drawer.js.map +1 -0
  505. package/dist/ui/primitives/drawer.mjs +103 -0
  506. package/dist/ui/primitives/drawer.mjs.map +1 -0
  507. package/dist/ui/primitives/input.d.mts +5 -0
  508. package/dist/ui/primitives/input.d.ts +5 -0
  509. package/dist/ui/primitives/input.js +46 -0
  510. package/dist/ui/primitives/input.js.map +1 -0
  511. package/dist/ui/primitives/input.mjs +22 -0
  512. package/dist/ui/primitives/input.mjs.map +1 -0
  513. package/dist/ui/primitives/item.d.mts +27 -0
  514. package/dist/ui/primitives/item.d.ts +27 -0
  515. package/dist/ui/primitives/item.js +221 -0
  516. package/dist/ui/primitives/item.js.map +1 -0
  517. package/dist/ui/primitives/item.mjs +188 -0
  518. package/dist/ui/primitives/item.mjs.map +1 -0
  519. package/dist/ui/primitives/kbd.d.mts +15 -0
  520. package/dist/ui/primitives/kbd.d.ts +15 -0
  521. package/dist/ui/primitives/kbd.js +45 -0
  522. package/dist/ui/primitives/kbd.js.map +1 -0
  523. package/dist/ui/primitives/kbd.mjs +21 -0
  524. package/dist/ui/primitives/kbd.mjs.map +1 -0
  525. package/dist/ui/primitives/label.d.mts +6 -0
  526. package/dist/ui/primitives/label.d.ts +6 -0
  527. package/dist/ui/primitives/label.js +55 -0
  528. package/dist/ui/primitives/label.js.map +1 -0
  529. package/dist/ui/primitives/label.mjs +21 -0
  530. package/dist/ui/primitives/label.mjs.map +1 -0
  531. package/dist/ui/primitives/resizable.d.mts +10 -0
  532. package/dist/ui/primitives/resizable.d.ts +10 -0
  533. package/dist/ui/primitives/resizable.js +68 -0
  534. package/dist/ui/primitives/resizable.js.map +1 -0
  535. package/dist/ui/primitives/resizable.mjs +42 -0
  536. package/dist/ui/primitives/resizable.mjs.map +1 -0
  537. package/dist/ui/primitives/save-status.d.mts +31 -0
  538. package/dist/ui/primitives/save-status.d.ts +31 -0
  539. package/dist/ui/primitives/save-status.js +150 -0
  540. package/dist/ui/primitives/save-status.js.map +1 -0
  541. package/dist/ui/primitives/save-status.mjs +126 -0
  542. package/dist/ui/primitives/save-status.mjs.map +1 -0
  543. package/dist/ui/primitives/separator.d.mts +6 -0
  544. package/dist/ui/primitives/separator.d.ts +6 -0
  545. package/dist/ui/primitives/separator.js +62 -0
  546. package/dist/ui/primitives/separator.js.map +1 -0
  547. package/dist/ui/primitives/separator.mjs +28 -0
  548. package/dist/ui/primitives/separator.mjs.map +1 -0
  549. package/dist/ui/primitives/sheet.d.mts +16 -0
  550. package/dist/ui/primitives/sheet.d.ts +16 -0
  551. package/dist/ui/primitives/sheet.js +170 -0
  552. package/dist/ui/primitives/sheet.js.map +1 -0
  553. package/dist/ui/primitives/sheet.mjs +129 -0
  554. package/dist/ui/primitives/sheet.mjs.map +1 -0
  555. package/dist/ui/primitives/sidebar.d.mts +74 -0
  556. package/dist/ui/primitives/sidebar.d.ts +74 -0
  557. package/dist/ui/primitives/sidebar.js +697 -0
  558. package/dist/ui/primitives/sidebar.js.map +1 -0
  559. package/dist/ui/primitives/sidebar.mjs +651 -0
  560. package/dist/ui/primitives/sidebar.mjs.map +1 -0
  561. package/dist/ui/primitives/skeleton.d.mts +5 -0
  562. package/dist/ui/primitives/skeleton.d.ts +5 -0
  563. package/dist/ui/primitives/skeleton.js +40 -0
  564. package/dist/ui/primitives/skeleton.js.map +1 -0
  565. package/dist/ui/primitives/skeleton.mjs +16 -0
  566. package/dist/ui/primitives/skeleton.mjs.map +1 -0
  567. package/dist/ui/primitives/slider.d.mts +20 -0
  568. package/dist/ui/primitives/slider.d.ts +20 -0
  569. package/dist/ui/primitives/slider.js +112 -0
  570. package/dist/ui/primitives/slider.js.map +1 -0
  571. package/dist/ui/primitives/slider.mjs +78 -0
  572. package/dist/ui/primitives/slider.mjs.map +1 -0
  573. package/dist/ui/primitives/sortable.d.mts +50 -0
  574. package/dist/ui/primitives/sortable.d.ts +50 -0
  575. package/dist/ui/primitives/sortable.js +456 -0
  576. package/dist/ui/primitives/sortable.js.map +1 -0
  577. package/dist/ui/primitives/sortable.mjs +435 -0
  578. package/dist/ui/primitives/sortable.mjs.map +1 -0
  579. package/dist/ui/primitives/switch.d.mts +6 -0
  580. package/dist/ui/primitives/switch.d.ts +6 -0
  581. package/dist/ui/primitives/switch.js +67 -0
  582. package/dist/ui/primitives/switch.js.map +1 -0
  583. package/dist/ui/primitives/switch.mjs +33 -0
  584. package/dist/ui/primitives/switch.mjs.map +1 -0
  585. package/dist/ui/primitives/table.d.mts +12 -0
  586. package/dist/ui/primitives/table.d.ts +12 -0
  587. package/dist/ui/primitives/table.js +130 -0
  588. package/dist/ui/primitives/table.js.map +1 -0
  589. package/dist/ui/primitives/table.mjs +89 -0
  590. package/dist/ui/primitives/table.mjs.map +1 -0
  591. package/dist/ui/primitives/tabs.d.mts +9 -0
  592. package/dist/ui/primitives/tabs.d.ts +9 -0
  593. package/dist/ui/primitives/tabs.js +86 -0
  594. package/dist/ui/primitives/tabs.js.map +1 -0
  595. package/dist/ui/primitives/tabs.mjs +49 -0
  596. package/dist/ui/primitives/tabs.mjs.map +1 -0
  597. package/dist/ui/primitives/textarea.d.mts +5 -0
  598. package/dist/ui/primitives/textarea.d.ts +5 -0
  599. package/dist/ui/primitives/textarea.js +43 -0
  600. package/dist/ui/primitives/textarea.js.map +1 -0
  601. package/dist/ui/primitives/textarea.mjs +19 -0
  602. package/dist/ui/primitives/textarea.mjs.map +1 -0
  603. package/dist/ui/primitives/tooltip.d.mts +9 -0
  604. package/dist/ui/primitives/tooltip.d.ts +9 -0
  605. package/dist/ui/primitives/tooltip.js +94 -0
  606. package/dist/ui/primitives/tooltip.js.map +1 -0
  607. package/dist/ui/primitives/tooltip.mjs +57 -0
  608. package/dist/ui/primitives/tooltip.mjs.map +1 -0
  609. package/docs/agents.md +72 -29
  610. package/docs/api/README.md +6 -3
  611. package/docs/api/appObjects.d.ts +1 -1
  612. package/docs/api/appObjects.md +1 -1
  613. package/docs/api/by-path.md +29 -0
  614. package/docs/api/objects.d.ts +72 -5
  615. package/docs/api/objects.md +84 -7
  616. package/docs/api/processLists.d.ts +247 -0
  617. package/docs/api/processLists.md +409 -0
  618. package/docs/api/reports.d.ts +476 -0
  619. package/docs/api/reports.md +539 -0
  620. package/docs/api/scheduleBuy.d.ts +12 -8
  621. package/docs/api/scheduleBuy.md +13 -10
  622. package/docs/api/seo.d.ts +406 -3
  623. package/docs/api/seo.md +505 -1
  624. package/docs/api/workspaceBackup.d.ts +324 -0
  625. package/docs/api/workspaceBackup.md +427 -0
  626. package/package.json +174 -5
  627. package/stacks/6sense.json +46 -0
  628. package/stacks/README.md +20 -0
  629. package/stacks/apollo.json +128 -0
  630. package/stacks/clay.json +59 -0
  631. package/stacks/gong.json +72 -0
  632. package/stacks/hubspot.json +162 -0
  633. package/stacks/intercom.json +53 -0
  634. package/stacks/mailchimp.json +71 -0
  635. package/stacks/outreach.json +86 -0
  636. package/stacks/salesforce.json +94 -0
  637. package/stacks/stack.schema.json +32 -0
  638. package/stacks/starter.json +22 -0
  639. package/styles/styles.css +2 -0
  640. package/styles/theme.css +181 -0
  641. package/styles/tokens.css +284 -0
  642. package/templates/next-app/README.md.tmpl +35 -0
  643. package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
  644. package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
  645. package/templates/next-app/app/globals.css.tmpl +8 -0
  646. package/templates/next-app/app/layout.tsx.tmpl +19 -0
  647. package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
  648. package/templates/next-app/dot-env.local.example.tmpl +6 -0
  649. package/templates/next-app/dot-gitignore.tmpl +7 -0
  650. package/templates/next-app/lib/nav.ts.tmpl +2 -0
  651. package/templates/next-app/next.config.mjs.tmpl +4 -0
  652. package/templates/next-app/package.json.tmpl +25 -0
  653. package/templates/next-app/postcss.config.mjs.tmpl +4 -0
  654. package/templates/next-app/tsconfig.json.tmpl +21 -0
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ /**
4
+ * DialerWorkspace: the whole rep screen. CallQueue on the left; on the right the Softphone, the outcome panel for
5
+ * the selected call, and the transcript of whatever was just worked. It owns the wiring between them: the
6
+ * conference from each confirmed batch goes to the Softphone, and a finished call opens the DispositionPanel.
7
+ *
8
+ * SDK operations: none of its own; see CallQueue, Softphone, DispositionPanel and TranscriptViewer for theirs.
9
+ */
10
+ import { useMemo, useState } from "react";
11
+ import { EmptyState } from "@/g8/components/empty-state";
12
+ import { isLiveCall, needsDisposition, type LiveCall, type NextBatchResult } from "../lib/dialer-types";
13
+ import type { SoftphoneTransport } from "../lib/softphone-transport";
14
+ import { CallQueue } from "./call-queue";
15
+ import { DispositionPanel } from "./disposition-panel";
16
+ import { Softphone } from "./softphone";
17
+ import { TranscriptViewer } from "./transcript-viewer";
18
+
19
+ export interface DialerWorkspaceProps {
20
+ userId: string;
21
+ userEmail?: string;
22
+ listId?: string;
23
+ listTitle?: string;
24
+ sessionId?: string;
25
+ /** Your audio client, for example `createTwilioTransport()` created once. */
26
+ transport: SoftphoneTransport;
27
+ className?: string;
28
+ }
29
+
30
+ export function DialerWorkspace({ userId, userEmail, listId, listTitle, sessionId, transport, className }: DialerWorkspaceProps) {
31
+ const [calls, setCalls] = useState<LiveCall[]>([]);
32
+ const [selected, setSelected] = useState<LiveCall | null>(null);
33
+ const [batch, setBatch] = useState<NextBatchResult | null>(null);
34
+ const waiting = useMemo(() => calls.find(needsDisposition) ?? null, [calls]);
35
+ const focus = selected ?? waiting;
36
+
37
+ return (
38
+ <div className={`grid gap-4 lg:grid-cols-[minmax(0,3fr)_minmax(0,2fr)] ${className ?? ""}`}>
39
+ <CallQueue sessionId={sessionId} userId={userId} userEmail={userEmail} listId={listId} listTitle={listTitle} onBatch={setBatch} onCalls={setCalls} onSelectCall={setSelected} />
40
+ <div className="space-y-4">
41
+ <Softphone userId={userId} transport={transport} conferenceName={batch?.conference_name} joinMuted={(batch?.batch?.length ?? 1) > 1} />
42
+ <DispositionPanel key={`outcome-${focus?.call_id}`} call={focus} onSaved={() => setSelected(null)} />
43
+ {focus ? <TranscriptViewer key={`transcript-${focus.call_id}`} roomName={focus.room_name ?? focus.call_id} live={isLiveCall(focus)} /> : <EmptyState title="Nothing on screen" description="The transcript of the call you select shows here." />}
44
+ </div>
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,75 @@
1
+ "use client";
2
+
3
+ /**
4
+ * DispositionPanel: record how a finished call went (connected, voicemail, callback, wrong number, ...).
5
+ *
6
+ * SDK operations, in order:
7
+ * 1. GET /voice/dispositions (voice.listVoiceDispositions) read - the outcomes this org accepts
8
+ * 2. PATCH /dialer/parallel-calls/calls/{room_name}/disposition (dialer.setCallDisposition) write
9
+ * `room_name` is the call's `call_id` (they are the same value). Setting `callback_at` schedules a follow-up; it
10
+ * does not place the call. The session list refreshes by polling (CallQueue), so call `onSaved` to nudge it.
11
+ */
12
+ import { useEffect, useState } from "react";
13
+ import { Button, Input, Skeleton, Textarea } from "@graph8/sdk/ui";
14
+ import { ApiError } from "@/g8/components/api-error";
15
+ import { EmptyState } from "@/g8/components/empty-state";
16
+ import { g8Patch } from "@/g8/lib/g8-client";
17
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
18
+ import { useDispositions } from "../hooks/use-dispositions";
19
+ import { contactName, type LiveCall } from "../lib/dialer-types";
20
+
21
+ export interface DispositionPanelProps {
22
+ /** The finished call to classify (a row from CallQueue). */
23
+ call: Pick<LiveCall, "call_id" | "first_name" | "last_name" | "email" | "to_phone" | "disposition"> | null;
24
+ onSaved?: (disposition: string) => void;
25
+ className?: string;
26
+ }
27
+
28
+ export function DispositionPanel({ call, onSaved, className }: DispositionPanelProps) {
29
+ const { options, loading, error, refetch } = useDispositions();
30
+ const [picked, setPicked] = useState<string | null>(null);
31
+ const [notes, setNotes] = useState("");
32
+ const [callbackAt, setCallbackAt] = useState("");
33
+
34
+ // Stale context: a different call starts with a clean form, never the previous call's outcome and notes.
35
+ useEffect(() => { setPicked(null); setNotes(""); setCallbackAt(""); }, [call?.call_id]);
36
+
37
+ const save = useG8Mutation(
38
+ async (disposition: string) => {
39
+ await g8Patch(`/dialer/parallel-calls/calls/${encodeURIComponent(call?.call_id ?? "")}/disposition`, {
40
+ disposition,
41
+ notes: notes.trim() || undefined,
42
+ callback_at: callbackAt ? new Date(callbackAt).toISOString() : undefined,
43
+ });
44
+ return disposition;
45
+ },
46
+ { onSuccess: (d) => { setPicked(null); setNotes(""); setCallbackAt(""); onSaved?.(d); } },
47
+ );
48
+
49
+ if (!call) return <EmptyState className={className} title="No call selected" description="Pick a finished call from the queue to record its outcome." />;
50
+ if (error) return <ApiError error={error} title="Could not load outcomes" onRetry={() => void refetch()} />;
51
+ if (loading) return <Skeleton className="h-32 w-full" aria-label="Loading outcomes" />;
52
+ const wantsCallback = picked != null && /callback|call_back|follow/.test(picked);
53
+
54
+ return (
55
+ <section className={`space-y-3 rounded-lg border p-4 ${className ?? ""}`} aria-label="Call outcome">
56
+ <header>
57
+ <h3 className="text-sm font-semibold">How did the call with {contactName(call)} go?</h3>
58
+ {call.disposition ? <p className="text-xs text-muted-foreground">Currently: {call.disposition}</p> : null}
59
+ </header>
60
+ <div role="radiogroup" aria-label="Outcome" className="flex flex-wrap gap-2">
61
+ {options.map((o) => (
62
+ <Button key={o.key} role="radio" aria-checked={picked === o.key} size="sm" variant={picked === o.key ? "default" : "outline"} onClick={() => setPicked(o.key)}>
63
+ {o.label}
64
+ </Button>
65
+ ))}
66
+ </div>
67
+ {wantsCallback ? <Input aria-label="Call back at" type="datetime-local" value={callbackAt} onChange={(e) => setCallbackAt(e.target.value)} /> : null}
68
+ <Textarea aria-label="Notes" rows={3} placeholder="Notes (optional)" value={notes} onChange={(e) => setNotes(e.target.value)} />
69
+ {save.error ? <ApiError error={save.error} title="Could not save the outcome" /> : null}
70
+ <Button disabled={!picked || save.loading} loading={save.loading} onClick={() => picked && void save.mutate(picked)}>
71
+ Save outcome
72
+ </Button>
73
+ </section>
74
+ );
75
+ }
@@ -0,0 +1,107 @@
1
+ "use client";
2
+
3
+ /**
4
+ * LiveCallsTable: the calls in the current session with hang-up, mute and voicemail-drop controls.
5
+ *
6
+ * SDK operations:
7
+ * POST /dialer/sessions/calls/hangup (dialer.hangupSessionCall) write - ends a call that is up
8
+ * POST /dialer/sessions/calls/mute?call_id= (dialer.muteCall) write - explicit muted true/false
9
+ * POST /dialer/sessions/calls/drop-voicemail (dialer.dropSessionVoicemail) EXTERNAL -> ConfirmSpendDialog first
10
+ * plays the rep's cloned-voice recording to a real person's voicemail; there is no undo.
11
+ * The rows themselves come from useDialerSession (poll), not from this component.
12
+ */
13
+ import { useState } from "react";
14
+ import { Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
15
+ import { ApiError } from "@/g8/components/api-error";
16
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
17
+ import { EmptyState } from "@/g8/components/empty-state";
18
+ import { g8Post } from "@/g8/lib/g8-client";
19
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
20
+ import { contactName, isLiveCall, normalizeStatus, type LiveCall } from "../lib/dialer-types";
21
+
22
+ export interface LiveCallsTableProps {
23
+ sessionId: string;
24
+ calls: LiveCall[];
25
+ /** Called after a control succeeded; refresh the session here. */
26
+ onChanged?: () => void;
27
+ /** Open a finished call's disposition panel. */
28
+ onSelect?: (call: LiveCall) => void;
29
+ className?: string;
30
+ }
31
+
32
+ export function LiveCallsTable({ sessionId, calls, onChanged, onSelect, className }: LiveCallsTableProps) {
33
+ const [voicemailFor, setVoicemailFor] = useState<LiveCall | null>(null);
34
+ // Per-row busy state: only the row being acted on is disabled, not every call in the table.
35
+ const [busy, setBusy] = useState<ReadonlySet<string>>(new Set());
36
+ const track = async <T,>(id: string, run: () => Promise<T>): Promise<T> => {
37
+ setBusy((b) => new Set(b).add(id));
38
+ try { return await run(); } finally { setBusy((b) => { const n = new Set(b); n.delete(id); return n; }); }
39
+ };
40
+ const hangup = useG8Mutation((call: LiveCall) => track(call.call_id, () => g8Post("/dialer/sessions/calls/hangup", { call_id: call.call_id, session_id: sessionId })), { onSuccess: () => onChanged?.() });
41
+ const [muted, setMuted] = useState<ReadonlySet<string>>(new Set());
42
+ const mute = useG8Mutation(
43
+ (v: { call: LiveCall; muted: boolean }) => track(v.call.call_id, async () => {
44
+ await g8Post("/dialer/sessions/calls/mute", undefined, { query: { call_id: v.call.call_id, muted: v.muted } });
45
+ setMuted((prev) => { const next = new Set(prev); if (v.muted) next.add(v.call.call_id); else next.delete(v.call.call_id); return next; });
46
+ }),
47
+ );
48
+ const voicemail = useG8Mutation((call: LiveCall) => track(call.call_id, () => g8Post("/dialer/sessions/calls/drop-voicemail", { call_id: call.call_id, session_id: sessionId })), {
49
+ onSuccess: () => { setVoicemailFor(null); onChanged?.(); },
50
+ onError: () => setVoicemailFor(null),
51
+ });
52
+ const failure = hangup.error ?? mute.error ?? voicemail.error;
53
+
54
+ if (calls.length === 0) {
55
+ return <EmptyState title="No calls yet" description="Calls appear here as soon as you dial the next batch." className={className} />;
56
+ }
57
+ return (
58
+ <div className={`space-y-2 ${className ?? ""}`}>
59
+ {failure ? <ApiError error={failure} title="That call control failed" /> : null}
60
+ <Table aria-label="Calls in this session">
61
+ <TableHeader>
62
+ <TableRow>
63
+ <TableHead>Contact</TableHead>
64
+ <TableHead>Number</TableHead>
65
+ <TableHead>Status</TableHead>
66
+ <TableHead className="text-right">Controls</TableHead>
67
+ </TableRow>
68
+ </TableHeader>
69
+ <TableBody>
70
+ {calls.map((call) => {
71
+ const live = isLiveCall(call);
72
+ const rowBusy = busy.has(call.call_id);
73
+ return (
74
+ <TableRow key={call.call_id} className={onSelect ? "cursor-pointer" : undefined} onClick={() => onSelect?.(call)}>
75
+ <TableCell className="font-medium">{contactName(call)}</TableCell>
76
+ <TableCell>{call.to_phone}</TableCell>
77
+ <TableCell className="capitalize">{call.is_voicemail ? "voicemail" : normalizeStatus(call.status)}{call.disposition ? ` · ${call.disposition}` : ""}</TableCell>
78
+ <TableCell className="space-x-1 text-right" onClick={(e) => e.stopPropagation()}>
79
+ {live ? (
80
+ <>
81
+ <Button size="sm" variant="ghost" disabled={rowBusy} aria-label={`${muted.has(call.call_id) ? "Unmute" : "Mute"} ${contactName(call)}`} onClick={() => void mute.mutate({ call, muted: !muted.has(call.call_id) })}>
82
+ {muted.has(call.call_id) ? "Unmute" : "Mute"}
83
+ </Button>
84
+ <Button size="sm" variant="ghost" disabled={rowBusy} aria-label={`Voicemail for ${contactName(call)}`} onClick={() => setVoicemailFor(call)}>Voicemail</Button>
85
+ <Button size="sm" variant="outline" disabled={rowBusy} loading={rowBusy} aria-label={`Hang up ${contactName(call)}`} onClick={() => void hangup.mutate(call)}>Hang up</Button>
86
+ </>
87
+ ) : null}
88
+ </TableCell>
89
+ </TableRow>
90
+ );
91
+ })}
92
+ </TableBody>
93
+ </Table>
94
+ <ConfirmSpendDialog
95
+ open={voicemailFor != null}
96
+ tier="external"
97
+ title={`Leave a voicemail for ${voicemailFor ? contactName(voicemailFor) : "this contact"}?`}
98
+ description="Your recorded voicemail plays to their phone and the call ends. This cannot be undone, so check it is the right person."
99
+ confirmLabel="Drop voicemail"
100
+ recipients={1}
101
+ confirming={voicemail.loading}
102
+ onConfirm={() => voicemailFor && void voicemail.mutate(voicemailFor)}
103
+ onCancel={() => setVoicemailFor(null)}
104
+ />
105
+ </div>
106
+ );
107
+ }
@@ -0,0 +1,111 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SessionStartForm: create a DRAFT dialer session for a list. A draft dials NOBODY; dialing starts only when
5
+ * CallQueue's "Dial next batch" is confirmed.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. GET /voice/dialer/numbers (voice.listDialerNumbers) read - caller IDs and today's usage
9
+ * 2. POST /dialer/sessions/draft (dialer.createSessionDraft) write - safe: resolves the list, dials no one
10
+ * Calling the draft twice for the same list returns the existing live session instead of making a second one.
11
+ */
12
+ import { useState } from "react";
13
+ import { Button, Input, Label } from "@graph8/sdk/ui";
14
+ import { ApiError } from "@/g8/components/api-error";
15
+ import { g8Post } from "@/g8/lib/g8-client";
16
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
17
+ import { useDialerNumbers } from "../hooks/use-dialer-numbers";
18
+ import { dataOf, type DialerMode } from "../lib/dialer-types";
19
+
20
+ export interface SessionStartFormProps {
21
+ /** The rep these calls are attributed to. An organisation API key has no user of its own, so you name one. */
22
+ /** The rep these calls belong to. Optional so the form works as a nav page: when omitted the rep types their graph8 user id. */
23
+ userId?: string;
24
+ userEmail?: string;
25
+ /** Prefill from the lists block (ListPicker). */
26
+ listId?: string;
27
+ listTitle?: string;
28
+ onCreated: (session: { sessionId: string; mode: DialerMode; parallelCount: number; fromPhone: string }) => void;
29
+ className?: string;
30
+ }
31
+
32
+ export function SessionStartForm({ userId, userEmail, listId: initialListId = "", listTitle, onCreated, className }: SessionStartFormProps) {
33
+ const { numbers, loading, error, refetch } = useDialerNumbers();
34
+ const [listId, setListId] = useState(initialListId);
35
+ const [typedUserId, setTypedUserId] = useState("");
36
+ const repId = userId || typedUserId.trim();
37
+ const [fromPhone, setFromPhone] = useState("");
38
+ const [mode, setMode] = useState<DialerMode>("power");
39
+ const [parallelCount, setParallelCount] = useState(3);
40
+ const [name, setName] = useState(listTitle ? `Dialing ${listTitle}` : "");
41
+
42
+ const chosenPhone = fromPhone || numbers[0]?.phone_number || "";
43
+ const create = useG8Mutation(
44
+ async () => {
45
+ const count = mode === "parallel" ? Math.min(10, Math.max(2, Math.round(parallelCount) || 2)) : 1;
46
+ const env = await g8Post("/dialer/sessions/draft", {
47
+ user_id: repId,
48
+ user_email: userEmail,
49
+ list_id: listId,
50
+ list_title: listTitle,
51
+ name: name || undefined,
52
+ from_phone: chosenPhone || undefined,
53
+ dialing_mode: mode,
54
+ parallel_count: count,
55
+ user_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
56
+ });
57
+ const sessionId = dataOf<{ session_id?: string }>(env).session_id;
58
+ if (!sessionId) throw new Error("graph8 did not return a session id");
59
+ return { sessionId, mode, parallelCount: count, fromPhone: chosenPhone };
60
+ },
61
+ { onSuccess: onCreated },
62
+ );
63
+
64
+ return (
65
+ <form className={`space-y-3 rounded-lg border p-4 ${className ?? ""}`} onSubmit={(e) => { e.preventDefault(); if (listId && repId) void create.mutate(undefined); }}>
66
+ <h3 className="text-sm font-semibold">Start a dialing session</h3>
67
+ <div className="grid gap-3 sm:grid-cols-2">
68
+ <div className="space-y-1">
69
+ <Label htmlFor="ds-list">List id</Label>
70
+ <Input id="ds-list" value={listId} onChange={(e) => setListId(e.target.value)} placeholder="From your lists" />
71
+ </div>
72
+ {userId ? null : (
73
+ <div className="space-y-1">
74
+ <Label htmlFor="ds-user">Rep user id</Label>
75
+ <Input id="ds-user" value={typedUserId} onChange={(e) => setTypedUserId(e.target.value)} placeholder="graph8 user id" />
76
+ </div>
77
+ )}
78
+ <div className="space-y-1">
79
+ <Label htmlFor="ds-name">Session name</Label>
80
+ <Input id="ds-name" value={name} onChange={(e) => setName(e.target.value)} />
81
+ </div>
82
+ <div className="space-y-1">
83
+ <Label htmlFor="ds-from">Caller ID</Label>
84
+ <select id="ds-from" className="h-9 w-full rounded-md border bg-background px-2 text-sm" value={chosenPhone} disabled={loading} onChange={(e) => setFromPhone(e.target.value)}>
85
+ {numbers.length === 0 ? <option value="">{loading ? "Loading numbers..." : "No numbers available"}</option> : null}
86
+ {numbers.map((n) => (
87
+ <option key={n.phone_number} value={n.phone_number}>
88
+ {n.phone_number} ({n.calls_today ?? 0} calls today)
89
+ </option>
90
+ ))}
91
+ </select>
92
+ </div>
93
+ <div className="space-y-1">
94
+ <Label htmlFor="ds-mode">Mode</Label>
95
+ <div className="flex items-center gap-2">
96
+ <select id="ds-mode" className="h-9 rounded-md border bg-background px-2 text-sm" value={mode} onChange={(e) => setMode(e.target.value as DialerMode)}>
97
+ <option value="power">Power (one call at a time)</option>
98
+ <option value="parallel">Parallel (several at once)</option>
99
+ </select>
100
+ {mode === "parallel" ? <Input aria-label="Calls at once" type="number" min={2} max={10} className="w-20" value={parallelCount} onChange={(e) => setParallelCount(Number(e.target.value))} onBlur={() => setParallelCount((n) => Math.min(10, Math.max(2, Math.round(n) || 2)))} /> : null}
101
+ </div>
102
+ </div>
103
+ </div>
104
+ {error ? <ApiError error={error} title="Could not load caller IDs" onRetry={() => void refetch()} /> : null}
105
+ {create.error ? <ApiError error={create.error} title="Could not create the session" /> : null}
106
+ <Button type="submit" disabled={!listId || !repId || create.loading} loading={create.loading}>
107
+ Create session (dials no one yet)
108
+ </Button>
109
+ </form>
110
+ );
111
+ }
@@ -0,0 +1,122 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Softphone: the rep's browser audio endpoint. It registers a voice client and joins the conference each dialed
5
+ * batch opens, with mute and hang-up. It does NOT dial: calls are placed by the server when CallQueue's
6
+ * "Dial next batch" is confirmed; the Softphone only brings the rep's audio into the conference.
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. GET /dialer/sessions/token?user_id= (dialer.mintSoftphoneToken) EXTERNAL -> ConfirmSpendDialog first
10
+ * Returns a 1-hour bearer credential ({ token, identity }) that lets the browser place and receive calls as
11
+ * the rep. It is a SECRET: it is only passed to `transport.connect`, never rendered or logged.
12
+ * Everything after that is the audio vendor, behind the `transport` prop (see lib/softphone-transport.ts). The API
13
+ * has no event stream; `transport` events are the only live signal about the audio leg.
14
+ *
15
+ * Use `createTwilioTransport()` (lib/twilio-transport.ts, needs the optional peer @twilio/voice-sdk) or write your own.
16
+ */
17
+ import { useCallback, useEffect, useRef, useState } from "react";
18
+ import { Button } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
21
+ import { g8Get } from "@/g8/lib/g8-client";
22
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
23
+ import { dataOf } from "../lib/dialer-types";
24
+ import type { SoftphoneTransport } from "../lib/softphone-transport";
25
+
26
+ export interface SoftphoneProps {
27
+ /** The rep whose identity the credential is minted for. */
28
+ userId: string;
29
+ transport: SoftphoneTransport;
30
+ /** Join this conference when it changes (the `conference_name` from next-batch). */
31
+ conferenceName?: string | null;
32
+ /** Join muted (parallel mode: the server unmutes when a person answers). */
33
+ joinMuted?: boolean;
34
+ onStateChange?: (state: SoftphoneState) => void;
35
+ className?: string;
36
+ }
37
+
38
+ export type SoftphoneState = "off" | "connecting" | "ready" | "in-call" | "error";
39
+
40
+ export function Softphone({ userId, transport, conferenceName, joinMuted, onStateChange, className }: SoftphoneProps) {
41
+ const [state, setState] = useState<SoftphoneState>("off");
42
+ const [muted, setMuted] = useState(false);
43
+ const [expiring, setExpiring] = useState(false);
44
+ const [asking, setAsking] = useState<"enable" | "renew" | null>(null);
45
+ const [joinedConference, setJoinedConference] = useState<string | null>(null);
46
+ const joining = useRef<string | null>(null);
47
+ const [transportError, setTransportError] = useState<string | null>(null);
48
+
49
+ const update = useCallback((next: SoftphoneState) => { setState(next); onStateChange?.(next); }, [onStateChange]);
50
+
51
+ useEffect(() => transport.subscribe((e) => {
52
+ if (e.type === "registered") update("ready");
53
+ else if (e.type === "connected") update("in-call");
54
+ else if (e.type === "disconnected") { setMuted(false); update("ready"); }
55
+ else if (e.type === "unregistered") update("off");
56
+ else if (e.type === "muted") setMuted(e.muted);
57
+ else if (e.type === "token-expiring") setExpiring(true);
58
+ else if (e.type === "error") { setTransportError(e.message); update("error"); }
59
+ }), [transport, update]);
60
+
61
+ const mint = useG8Mutation(async () => {
62
+ const env = await g8Get("/dialer/sessions/token", { user_id: userId });
63
+ const { token, identity } = dataOf<{ token?: string; identity?: string }>(env);
64
+ if (!token) throw new Error("graph8 did not return a softphone credential");
65
+ return { token, identity };
66
+ });
67
+
68
+ const confirm = async () => {
69
+ const intent = asking;
70
+ try {
71
+ const creds = await mint.mutateAsync(undefined);
72
+ if (intent === "renew") { transport.updateToken?.(creds.token); setExpiring(false); }
73
+ else { update("connecting"); setTransportError(null); await transport.connect(creds); }
74
+ } catch (err) {
75
+ setTransportError(err instanceof Error ? err.message : "Could not start the softphone");
76
+ if (intent === "enable") update("error");
77
+ } finally {
78
+ setAsking(null);
79
+ }
80
+ };
81
+
82
+ useEffect(() => {
83
+ if (state !== "ready" && state !== "in-call") return;
84
+ if (!conferenceName || conferenceName === joinedConference || conferenceName === joining.current || !transport.joinConference) return;
85
+ joining.current = conferenceName;
86
+ // Only a SUCCESSFUL join counts: a failed one leaves joinedConference unset so the next state change retries.
87
+ transport.joinConference(conferenceName, { muted: joinMuted }).then(() => setJoinedConference(conferenceName)).catch((e: unknown) => { joining.current = null; setTransportError(e instanceof Error ? e.message : "Could not join the call"); });
88
+ }, [conferenceName, joinedConference, joinMuted, state, transport]);
89
+
90
+ const label = { off: "Calling is off", connecting: "Connecting...", ready: "Ready for calls", "in-call": "On a call", error: "Softphone error" }[state];
91
+ return (
92
+ <section className={`space-y-3 rounded-lg border p-4 ${className ?? ""}`} aria-label="Softphone">
93
+ <div className="flex items-center justify-between">
94
+ <h3 className="text-sm font-semibold">Softphone</h3>
95
+ <span role="status" className="text-sm text-muted-foreground">{label}</span>
96
+ </div>
97
+ {transportError ? <p role="alert" className="text-sm text-destructive">{transportError}</p> : null}
98
+ {mint.error ? <ApiError error={mint.error} title="Could not get a softphone credential" /> : null}
99
+ {expiring ? <p className="text-sm">Your softphone credential expires soon. <Button size="sm" variant="link" onClick={() => setAsking("renew")}>Renew</Button></p> : null}
100
+ <div className="flex flex-wrap gap-2">
101
+ {state === "off" || state === "error" ? <Button onClick={() => setAsking("enable")}>Enable calling</Button> : null}
102
+ {state === "in-call" ? (
103
+ <>
104
+ <Button variant="outline" onClick={() => transport.mute(!muted)}>{muted ? "Unmute" : "Mute"}</Button>
105
+ <Button variant="destructive" onClick={() => transport.hangup()}>Leave call</Button>
106
+ </>
107
+ ) : null}
108
+ {state === "ready" || state === "in-call" ? <Button variant="ghost" onClick={() => { void transport.disconnect(); setJoinedConference(null); joining.current = null; }}>Turn off</Button> : null}
109
+ </div>
110
+ <ConfirmSpendDialog
111
+ open={asking != null}
112
+ tier="external"
113
+ title={asking === "renew" ? "Renew the softphone credential?" : "Let this browser place and receive calls?"}
114
+ description="This mints a one-hour credential that lets this browser make and take calls as you. It is kept in memory only."
115
+ confirmLabel={asking === "renew" ? "Renew" : "Enable calling"}
116
+ confirming={mint.loading}
117
+ onConfirm={() => void confirm()}
118
+ onCancel={() => setAsking(null)}
119
+ />
120
+ </section>
121
+ );
122
+ }
@@ -0,0 +1,82 @@
1
+ "use client";
2
+
3
+ /**
4
+ * TranscriptViewer: the speaker-by-speaker transcript, AI summary, AI grading and the recording for one call.
5
+ *
6
+ * SDK operations, in order:
7
+ * 1. GET /voice/dialer/calls/{room_name}/transcript (voice.getCallTranscript) read - status "empty" until flushed
8
+ * 2. GET /voice/dialer/calls/{room_name}/grading (voice.getCallGrading) read - "pending" while being computed
9
+ * 3. GET /voice/calls/{call_id}/recording (voice.downloadCallRecording) read - audio bytes through your /api/g8 proxy
10
+ * (425 while the recording is still being finalised; the audio element simply fails until then)
11
+ * There is no live transcript stream: with `live` set the component re-reads every 4 seconds until the call ends.
12
+ */
13
+ import { Skeleton } from "@graph8/sdk/ui";
14
+ import { ApiError } from "@/g8/components/api-error";
15
+ import { EmptyState } from "@/g8/components/empty-state";
16
+ import { buildG8Url, g8Get } from "@/g8/lib/g8-client";
17
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
18
+ import { useJobPoll } from "@/g8/hooks/use-job-poll";
19
+ import { dataOf, formatDuration, type TranscriptSegment } from "../lib/dialer-types";
20
+
21
+ export interface TranscriptViewerProps {
22
+ /** The call's room_name (equals its call_id). */
23
+ roomName: string;
24
+ /** Keep re-reading while the call is in progress. */
25
+ live?: boolean;
26
+ className?: string;
27
+ }
28
+
29
+ interface TranscriptData {
30
+ status?: string;
31
+ summary?: string | null;
32
+ disposition?: string | null;
33
+ call_duration?: number | null;
34
+ transcript?: TranscriptSegment[];
35
+ }
36
+
37
+ /** Grading is a free-form payload; show its flat fields rather than guess a schema. */
38
+ function GradingSummary({ grading }: { grading: Record<string, unknown> }) {
39
+ const rows = Object.entries(grading).filter(([, v]) => ["string", "number", "boolean"].includes(typeof v) && String(v).length < 400);
40
+ if (rows.length === 0) return null;
41
+ return (
42
+ <dl className="grid gap-2 text-sm sm:grid-cols-2" aria-label="AI grading">
43
+ {rows.map(([k, v]) => (
44
+ <div key={k}><dt className="text-xs uppercase text-muted-foreground">{k.replace(/_/g, " ")}</dt><dd>{String(v)}</dd></div>
45
+ ))}
46
+ </dl>
47
+ );
48
+ }
49
+
50
+ export function TranscriptViewer({ roomName, live = false, className }: TranscriptViewerProps) {
51
+ const path = `/voice/dialer/calls/${encodeURIComponent(roomName)}`;
52
+ const transcript = useG8Query(async (signal) => dataOf<TranscriptData>(await g8Get(`${path}/transcript`, undefined, { signal })), [roomName]);
53
+ const grading = useG8Query(async (signal) => dataOf<Record<string, unknown> | null>(await g8Get(`${path}/grading`, undefined, { signal })), [roomName]);
54
+ useJobPoll(async () => (await transcript.refetch()) ?? null, { enabled: live, isTerminal: () => !live, intervalMs: 4000, backoff: 1, maxIntervalMs: 4000 });
55
+
56
+ if (transcript.error) return <ApiError error={transcript.error} title="Could not load the transcript" onRetry={() => void transcript.refetch()} />;
57
+ if (transcript.loading && !transcript.data) return <Skeleton className="h-40 w-full" aria-label="Loading transcript" />;
58
+ const t = transcript.data;
59
+ const segments = t?.transcript ?? [];
60
+ const pendingGrade = !grading.data || (grading.data as { status?: string }).status === "pending";
61
+
62
+ return (
63
+ <section className={`space-y-3 ${className ?? ""}`}>
64
+ <header className="flex flex-wrap items-center gap-3 text-sm text-muted-foreground">
65
+ <span>Length {formatDuration(t?.call_duration)}</span>
66
+ {t?.disposition ? <span className="capitalize">Outcome: {t.disposition.replace(/_/g, " ")}</span> : null}
67
+ </header>
68
+ <audio controls preload="none" src={buildG8Url(`/voice/calls/${encodeURIComponent(roomName)}/recording`)} aria-label="Call recording" className="w-full" />
69
+ {t?.summary ? <p className="rounded-lg bg-muted p-3 text-sm">{t.summary}</p> : null}
70
+ {!pendingGrade && grading.data ? <GradingSummary grading={grading.data} /> : <p className="text-xs text-muted-foreground">AI grading is not ready yet.</p>}
71
+ {segments.length === 0 ? (
72
+ <EmptyState title={t?.status === "empty" ? "Transcript not ready" : "No transcript"} description="Transcripts are saved a moment after a call ends." />
73
+ ) : (
74
+ <ol className="space-y-2" aria-label="Transcript">
75
+ {segments.map((s, i) => (
76
+ <li key={i} className="text-sm"><span className="mr-2 font-medium capitalize">{s.speaker ?? "speaker"}</span>{s.content}</li>
77
+ ))}
78
+ </ol>
79
+ )}
80
+ </section>
81
+ );
82
+ }
@@ -0,0 +1,19 @@
1
+ "use client";
2
+
3
+ /**
4
+ * useDialerNumbers: caller IDs the dialer may use, with today's usage.
5
+ *
6
+ * SDK operations: GET /voice/dialer/numbers (voice.listDialerNumbers), read tier.
7
+ */
8
+ import { g8Get } from "@/g8/lib/g8-client";
9
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
10
+ import { dataOf, type DialerNumber } from "../lib/dialer-types";
11
+
12
+ export function useDialerNumbers() {
13
+ const q = useG8Query(async (signal) => {
14
+ const data = dataOf<{ numbers?: DialerNumber[]; items?: DialerNumber[] } | DialerNumber[]>(await g8Get("/voice/dialer/numbers", undefined, { signal }));
15
+ const list = Array.isArray(data) ? data : (data.numbers ?? data.items ?? []);
16
+ return list.map((n) => ({ ...n, phone_number: n.phone_number ?? n.number ?? "" })).filter((n) => n.phone_number);
17
+ }, []);
18
+ return { ...q, numbers: q.data ?? [] };
19
+ }
@@ -0,0 +1,68 @@
1
+ "use client";
2
+
3
+ /**
4
+ * useDialerSession: poll one dialer session until it is over.
5
+ *
6
+ * SDK operations, each poll (in parallel):
7
+ * GET /dialer/sessions/{id}/queue-stats (dialer.readQueueStats) read - status, batch_status, queue_remaining
8
+ * GET /dialer/sessions/{id} (voice.getDialerSession) read - name, list, live call list (live_state.calls)
9
+ *
10
+ * The API has NO event stream (no WebSocket/SSE for sessions), so this polls with useJobPoll, every 5 seconds by
11
+ * default (the same cadence the graph8 app uses as its fallback). It stops by itself when the session is
12
+ * COMPLETED, after repeated errors, or on a 4xx (a missing or foreign session). Call `refresh()` right after
13
+ * you change something (dial, hang up, disposition) for an immediate read instead of waiting for the next tick.
14
+ */
15
+ import { useCallback, useEffect } from "react";
16
+ import { g8Get } from "@/g8/lib/g8-client";
17
+ import { useJobPoll } from "@/g8/hooks/use-job-poll";
18
+ import { dataOf, isSessionOver, type LiveCall, type QueueStats, type SessionDetail } from "../lib/dialer-types";
19
+
20
+ const NO_CALLS: LiveCall[] = [];
21
+
22
+ export interface DialerSessionSnapshot {
23
+ stats: QueueStats;
24
+ detail: SessionDetail;
25
+ calls: LiveCall[];
26
+ }
27
+
28
+ export interface UseDialerSessionOptions {
29
+ intervalMs?: number;
30
+ enabled?: boolean;
31
+ }
32
+
33
+ export function useDialerSession(sessionId: string | null | undefined, options: UseDialerSessionOptions = {}) {
34
+ const id = encodeURIComponent(sessionId ?? "");
35
+ const poll = useJobPoll<DialerSessionSnapshot>(
36
+ async (signal) => {
37
+ const [stats, detail] = await Promise.all([
38
+ g8Get(`/dialer/sessions/${id}/queue-stats`, undefined, { signal }),
39
+ g8Get(`/dialer/sessions/${id}`, undefined, { signal }),
40
+ ]);
41
+ const d = dataOf<SessionDetail>(detail);
42
+ return { stats: dataOf<QueueStats>(stats), detail: d, calls: d.live_state?.calls ?? [] };
43
+ },
44
+ {
45
+ isTerminal: (s) => isSessionOver(s.stats.status),
46
+ enabled: Boolean(sessionId) && (options.enabled ?? true),
47
+ intervalMs: options.intervalMs ?? 5000,
48
+ backoff: 1,
49
+ maxIntervalMs: options.intervalMs ?? 5000,
50
+ maxAttempts: 100_000,
51
+ maxConsecutiveErrors: 5,
52
+ },
53
+ );
54
+ const { start } = poll;
55
+ const refresh = useCallback(() => start(), [start]);
56
+ // A different session starts a fresh poll immediately and never shows the previous session's numbers.
57
+ useEffect(() => { if (sessionId) start(); }, [sessionId, start]);
58
+ const snapshot = poll.status && poll.status.stats.session_id === sessionId ? poll.status : undefined;
59
+ return {
60
+ stats: snapshot?.stats,
61
+ detail: snapshot?.detail,
62
+ calls: snapshot?.calls ?? NO_CALLS,
63
+ error: poll.error,
64
+ polling: poll.polling,
65
+ over: poll.done,
66
+ refresh,
67
+ };
68
+ }