@graph8/sdk 0.362.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 (349) hide show
  1. package/README.md +14 -10
  2. package/blocks/_foundation/README.md +64 -1
  3. package/blocks/_foundation/block.json +79 -6
  4. package/blocks/_foundation/components/api-error.tsx +9 -7
  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/screen/index.ts +1 -0
  9. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  10. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  11. package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
  12. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  13. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  14. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  15. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  16. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  17. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  18. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  19. package/blocks/_foundation/lib/g8-client.ts +1 -1
  20. package/blocks/_foundation/lib/plural.ts +23 -0
  21. package/blocks/_foundation/server/connect-route.ts +15 -0
  22. package/blocks/_foundation/server/connect.ts +127 -0
  23. package/blocks/_foundation/server/local-key.ts +70 -0
  24. package/blocks/_foundation/server/proxy.ts +103 -3
  25. package/blocks/_foundation/server/route.ts +16 -4
  26. package/blocks/agents/README.md +14 -0
  27. package/blocks/agents/block.json +33 -1
  28. package/blocks/agents/components/agent-builder.tsx +13 -8
  29. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
  30. package/blocks/agents/components/agent-run-detail.tsx +102 -0
  31. package/blocks/agents/components/agent-runs.tsx +16 -7
  32. package/blocks/agents/components/agents-screen.tsx +184 -0
  33. package/blocks/analytics/README.md +13 -0
  34. package/blocks/analytics/block.json +45 -1
  35. package/blocks/analytics/components/analytics-screen.tsx +209 -0
  36. package/blocks/analytics/components/leaderboard.tsx +14 -7
  37. package/blocks/analytics/components/report-builder.tsx +23 -9
  38. package/blocks/analytics/components/report-detail.tsx +84 -0
  39. package/blocks/analytics/components/trend-chart.tsx +62 -0
  40. package/blocks/appointments/block.json +6 -2
  41. package/blocks/appointments/components/availability-editor.tsx +17 -5
  42. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  43. package/blocks/appointments/components/event-type-related.tsx +4 -2
  44. package/blocks/appointments/components/meeting-list.tsx +9 -5
  45. package/blocks/appointments/components/meetings-screen.tsx +7 -4
  46. package/blocks/appointments/components/scheduling-screen.tsx +2 -1
  47. package/blocks/block.schema.json +18 -0
  48. package/blocks/block.types.ts +17 -0
  49. package/blocks/campaigns/README.md +41 -11
  50. package/blocks/campaigns/block.json +105 -42
  51. package/blocks/campaigns/components/campaign-assets.tsx +16 -5
  52. package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
  53. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  54. package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
  55. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  56. package/blocks/campaigns/components/icp-detail.tsx +154 -0
  57. package/blocks/campaigns/components/icp-editor.tsx +15 -15
  58. package/blocks/campaigns/components/persona-detail.tsx +172 -0
  59. package/blocks/campaigns/components/persona-editor.tsx +14 -11
  60. package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
  61. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  62. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  63. package/blocks/coach/README.md +10 -0
  64. package/blocks/coach/block.json +16 -1
  65. package/blocks/coach/components/coach-screen.tsx +222 -0
  66. package/blocks/copilot/README.md +13 -0
  67. package/blocks/copilot/block.json +14 -1
  68. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  69. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  70. package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
  71. package/blocks/crm/README.md +28 -0
  72. package/blocks/crm/block.json +58 -2
  73. package/blocks/crm/components/companies-screen.tsx +345 -0
  74. package/blocks/crm/components/company-detail.tsx +8 -6
  75. package/blocks/crm/components/company-table.tsx +11 -4
  76. package/blocks/crm/components/contact-detail.tsx +6 -3
  77. package/blocks/crm/components/contact-table.tsx +10 -3
  78. package/blocks/crm/components/contacts-screen.tsx +359 -0
  79. package/blocks/crm/components/dedupe-review.tsx +7 -5
  80. package/blocks/crm/components/import-wizard.tsx +4 -2
  81. package/blocks/crm/components/notes-panel.tsx +4 -4
  82. package/blocks/crm/components/record-form.tsx +5 -3
  83. package/blocks/crm/components/task-list.tsx +4 -4
  84. package/blocks/crm/components/tasks-screen.tsx +379 -0
  85. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  86. package/blocks/crm/hooks/use-page-query.ts +7 -1
  87. package/blocks/crm/lib/crm-types.ts +12 -2
  88. package/blocks/crm/lib/screen-parts.tsx +105 -0
  89. package/blocks/deals/block.json +9 -3
  90. package/blocks/deals/components/deal-detail.tsx +23 -11
  91. package/blocks/deals/components/deal-related.tsx +1 -1
  92. package/blocks/deals/components/deals-screen.tsx +10 -5
  93. package/blocks/deals/components/stage-editor.tsx +4 -4
  94. package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
  95. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  96. package/blocks/dialer/README.md +34 -2
  97. package/blocks/dialer/block.json +67 -1
  98. package/blocks/dialer/components/call-contact-button.tsx +252 -0
  99. package/blocks/dialer/components/call-log-screen.tsx +205 -0
  100. package/blocks/dialer/components/call-log.tsx +7 -3
  101. package/blocks/dialer/components/call-queue.tsx +1 -1
  102. package/blocks/dialer/components/dialer-screen.tsx +319 -0
  103. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  104. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  105. package/blocks/dialer/components/session-start-form.tsx +4 -2
  106. package/blocks/dialer/components/softphone.tsx +3 -1
  107. package/blocks/enrichment/README.md +14 -2
  108. package/blocks/enrichment/block.json +32 -4
  109. package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
  110. package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
  111. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  112. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  113. package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
  114. package/blocks/enrichment/components/waterfall-config.tsx +39 -10
  115. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  116. package/blocks/forms/block.json +32 -18
  117. package/blocks/forms/components/form-builder.tsx +3 -1
  118. package/blocks/forms/components/form-embed.tsx +3 -1
  119. package/blocks/forms/components/forms-screen.tsx +5 -2
  120. package/blocks/forms/components/submissions-table.tsx +3 -1
  121. package/blocks/inbox/README.md +27 -3
  122. package/blocks/inbox/block.json +14 -2
  123. package/blocks/inbox/components/inbox-list.tsx +7 -3
  124. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  125. package/blocks/inbox/components/reply-composer.tsx +65 -9
  126. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  127. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  128. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  129. package/blocks/inbox/lib/inbox-types.ts +47 -1
  130. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  131. package/blocks/integrations/README.md +9 -0
  132. package/blocks/integrations/block.json +46 -1
  133. package/blocks/integrations/components/crm-detail.tsx +100 -0
  134. package/blocks/integrations/components/integration-list.tsx +9 -4
  135. package/blocks/integrations/components/integrations-screen.tsx +135 -0
  136. package/blocks/integrations/components/sync-status.tsx +5 -3
  137. package/blocks/integrations/components/webhook-detail.tsx +160 -0
  138. package/blocks/integrations/components/webhook-manager.tsx +13 -8
  139. package/blocks/integrations/lib/integrations-utils.ts +6 -1
  140. package/blocks/intent/README.md +14 -0
  141. package/blocks/intent/block.json +27 -1
  142. package/blocks/intent/components/intent-feed.tsx +5 -2
  143. package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
  144. package/blocks/intent/components/intent-screen.tsx +265 -0
  145. package/blocks/intent/components/keyword-detail.tsx +124 -0
  146. package/blocks/lifecycle/block.json +13 -8
  147. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  148. package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
  149. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  150. package/blocks/lists/README.md +13 -0
  151. package/blocks/lists/block.json +39 -1
  152. package/blocks/lists/components/add-to-list-button.tsx +3 -2
  153. package/blocks/lists/components/list-picker.tsx +5 -1
  154. package/blocks/lists/components/list-table.tsx +11 -5
  155. package/blocks/lists/components/lists-screen.tsx +344 -0
  156. package/blocks/lists/components/suppression-manager.tsx +6 -2
  157. package/blocks/lists/components/suppressions-screen.tsx +255 -0
  158. package/blocks/mailboxes/README.md +17 -0
  159. package/blocks/mailboxes/block.json +65 -17
  160. package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
  161. package/blocks/mailboxes/components/domain-detail.tsx +142 -0
  162. package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
  163. package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
  164. package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
  165. package/blocks/mailboxes/components/warmup-health.tsx +4 -2
  166. package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
  167. package/blocks/newsletter/block.json +40 -24
  168. package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
  169. package/blocks/newsletter/components/newsletter-screen.tsx +3 -1
  170. package/blocks/nurture/block.json +28 -14
  171. package/blocks/nurture/components/nurture-builder.tsx +3 -1
  172. package/blocks/nurture/components/nurture-enroll.tsx +6 -3
  173. package/blocks/nurture/components/nurture-form.tsx +23 -3
  174. package/blocks/nurture/components/nurture-screen.tsx +10 -6
  175. package/blocks/nurture/lib/nurture-types.ts +1 -1
  176. package/blocks/objects/README.md +17 -0
  177. package/blocks/objects/block.json +29 -16
  178. package/blocks/objects/components/object-not-found.tsx +3 -1
  179. package/blocks/objects/components/object-record-detail.tsx +2 -1
  180. package/blocks/objects/components/object-schema-editor.tsx +3 -1
  181. package/blocks/objects/components/object-table.tsx +13 -3
  182. package/blocks/objects/components/objects-screen.tsx +147 -0
  183. package/blocks/objects/lib/object-types.ts +1 -1
  184. package/blocks/pages/block.json +28 -23
  185. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  186. package/blocks/pages/components/content-library.tsx +9 -4
  187. package/blocks/pages/components/page-list.tsx +2 -1
  188. package/blocks/quotes/block.json +17 -12
  189. package/blocks/quotes/components/product-picker.tsx +3 -1
  190. package/blocks/quotes/components/quote-related.tsx +5 -2
  191. package/blocks/radar/README.md +10 -0
  192. package/blocks/radar/block.json +41 -1
  193. package/blocks/radar/components/competitor-detail.tsx +117 -0
  194. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  195. package/blocks/radar/components/radar-report.tsx +4 -2
  196. package/blocks/radar/components/radar-screen.tsx +214 -0
  197. package/blocks/radar/components/report-detail.tsx +104 -0
  198. package/blocks/search/README.md +10 -0
  199. package/blocks/search/block.json +16 -1
  200. package/blocks/search/components/filter-builder.tsx +3 -3
  201. package/blocks/search/components/prospect-results.tsx +5 -3
  202. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  203. package/blocks/search/components/saved-searches.tsx +4 -1
  204. package/blocks/search/hooks/use-filter-options.ts +20 -6
  205. package/blocks/search/lib/search-types.ts +2 -0
  206. package/blocks/sequencer/README.md +49 -9
  207. package/blocks/sequencer/block.json +99 -19
  208. package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
  209. package/blocks/sequencer/components/sequence-builder.tsx +211 -46
  210. package/blocks/sequencer/components/sequence-detail.tsx +259 -0
  211. package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
  212. package/blocks/sequencer/components/sequence-list.tsx +33 -8
  213. package/blocks/sequencer/components/sequence-stats.tsx +3 -2
  214. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  215. package/blocks/sequencer/components/sequences-screen.tsx +159 -0
  216. package/blocks/sequencer/components/step-editor.tsx +3 -1
  217. package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
  218. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  219. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  220. package/blocks/social-ads/README.md +11 -0
  221. package/blocks/social-ads/block.json +101 -2
  222. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  223. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  224. package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
  225. package/blocks/social-ads/components/audience-sync.tsx +11 -7
  226. package/blocks/social-ads/components/campaign-detail.tsx +133 -0
  227. package/blocks/social-ads/components/creative-detail.tsx +150 -0
  228. package/blocks/social-ads/components/creative-gallery.tsx +15 -5
  229. package/blocks/social-ads/components/post-composer.tsx +3 -1
  230. package/blocks/social-ads/components/post-detail.tsx +160 -0
  231. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  232. package/blocks/social-ads/components/social-screen.tsx +160 -0
  233. package/blocks/visitors/README.md +10 -0
  234. package/blocks/visitors/block.json +25 -1
  235. package/blocks/visitors/components/snippet-install.tsx +3 -1
  236. package/blocks/visitors/components/visitor-company-card.tsx +2 -2
  237. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  238. package/blocks/visitors/components/visitor-feed.tsx +11 -2
  239. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  240. package/blocks/webchat/README.md +10 -0
  241. package/blocks/webchat/block.json +21 -7
  242. package/blocks/webchat/components/chat-widget-config.tsx +7 -4
  243. package/blocks/webchat/components/conversation-list.tsx +7 -3
  244. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  245. package/blocks/workbench/README.md +11 -0
  246. package/blocks/workbench/block.json +48 -1
  247. package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
  248. package/blocks/workbench/components/workbench-grid.tsx +9 -3
  249. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  250. package/blocks/workbench/components/workbench-screen.tsx +192 -0
  251. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  252. package/blocks/workflows/README.md +13 -0
  253. package/blocks/workflows/block.json +28 -9
  254. package/blocks/workflows/components/run-history.tsx +31 -9
  255. package/blocks/workflows/components/trigger-config.tsx +3 -1
  256. package/blocks/workflows/components/workflow-editor.tsx +3 -1
  257. package/blocks/workflows/components/workflow-list.tsx +13 -6
  258. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  259. package/dist/cli.js +2709 -1721
  260. package/dist/errors.d.mts +34 -0
  261. package/dist/errors.d.ts +34 -0
  262. package/dist/errors.js +46 -0
  263. package/dist/errors.js.map +1 -0
  264. package/dist/errors.mjs +21 -0
  265. package/dist/errors.mjs.map +1 -0
  266. package/dist/index.browser.js +11 -5
  267. package/dist/index.browser.js.map +1 -1
  268. package/dist/index.browser.mjs +11 -5
  269. package/dist/index.browser.mjs.map +1 -1
  270. package/dist/index.d.mts +164 -25
  271. package/dist/index.d.ts +164 -25
  272. package/dist/index.js +11 -5
  273. package/dist/index.js.map +1 -1
  274. package/dist/index.mjs +11 -5
  275. package/dist/index.mjs.map +1 -1
  276. package/dist/react.browser.js +10 -4
  277. package/dist/react.browser.js.map +1 -1
  278. package/dist/react.browser.mjs +10 -4
  279. package/dist/react.browser.mjs.map +1 -1
  280. package/dist/react.d.mts +142 -5
  281. package/dist/react.d.ts +142 -5
  282. package/dist/react.js +10 -4
  283. package/dist/react.js.map +1 -1
  284. package/dist/react.mjs +10 -4
  285. package/dist/react.mjs.map +1 -1
  286. package/dist/types/blocks/add.d.ts +11 -4
  287. package/dist/types/blocks/brand.d.ts +113 -0
  288. package/dist/types/blocks/compose.d.ts +53 -0
  289. package/dist/types/blocks/create.d.ts +22 -19
  290. package/dist/types/blocks/doctor.d.ts +5 -1
  291. package/dist/types/blocks/navfile.d.ts +35 -0
  292. package/dist/types/blocks/paths.d.ts +4 -0
  293. package/dist/types/blocks/prefixes.d.ts +37 -0
  294. package/dist/types/blocks/registry.d.ts +17 -0
  295. package/dist/types/blocks/screens.d.ts +53 -0
  296. package/dist/types/cli.d.ts +2 -0
  297. package/dist/types/errors.d.ts +33 -0
  298. package/dist/types/http.d.ts +2 -18
  299. package/dist/types/version.d.ts +2 -2
  300. package/docs/agents.md +15 -4
  301. package/docs/api/README.md +2 -2
  302. package/docs/api/by-path.md +1 -0
  303. package/docs/api/desk.d.ts +98 -2
  304. package/docs/api/desk.md +123 -1
  305. package/docs/api/inbox.d.ts +10 -2
  306. package/docs/api/inbox.md +10 -2
  307. package/docs/api/seo.d.ts +19 -1
  308. package/docs/api/seo.md +24 -1
  309. package/package.json +16 -2
  310. package/stacks/6sense.json +41 -16
  311. package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
  312. package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
  313. package/stacks/apollo.json +118 -36
  314. package/stacks/calendly.json +60 -0
  315. package/stacks/clay/pages/companies.tsx.tmpl +15 -0
  316. package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
  317. package/stacks/clay.json +59 -26
  318. package/stacks/gong.json +81 -35
  319. package/stacks/hootsuite.json +99 -0
  320. package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
  321. package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
  322. package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
  323. package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
  324. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  325. package/stacks/hubspot.json +167 -66
  326. package/stacks/intercom.json +48 -20
  327. package/stacks/mailchimp.json +52 -33
  328. package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
  329. package/stacks/outreach.json +88 -25
  330. package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
  331. package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
  332. package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
  333. package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
  334. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  335. package/stacks/pipedrive.json +115 -0
  336. package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
  337. package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
  338. package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
  339. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  340. package/stacks/salesforce.json +81 -31
  341. package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
  342. package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
  343. package/stacks/zoominfo.json +102 -0
  344. package/styles/styles.css +1 -1
  345. package/templates/next-app/README.md.tmpl +40 -9
  346. package/templates/next-app/app/layout.tsx.tmpl +7 -2
  347. package/templates/next-app/components/app-shell.tsx.tmpl +99 -89
  348. package/templates/next-app/dot-gitignore.tmpl +2 -0
  349. package/templates/next-app/package.json.tmpl +1 -1
@@ -12,7 +12,7 @@
12
12
  * 5. DELETE /deals/{id} (deals.deleteDeal) DESTRUCTIVE -> ConfirmSpendDialog first
13
13
  * Compose notes, tasks and the activity timeline from the crm block under `children`.
14
14
  */
15
- import { useEffect, useState, type ReactNode } from "react";
15
+ import { useEffect, useRef, useState, type ReactNode } from "react";
16
16
  import { Button, Input, Label, Skeleton, Textarea } from "@graph8/sdk/ui";
17
17
  import { ApiError } from "@/g8/components/api-error";
18
18
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
@@ -21,6 +21,7 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
21
21
  import { useG8Query } from "@/g8/hooks/use-g8-query";
22
22
  import { usePipelines } from "../hooks/use-pipelines";
23
23
  import { dataOf, formatMoney, type Deal } from "../lib/deals-types";
24
+ import { useBrand } from "@/g8/hooks/use-brand";
24
25
 
25
26
  export interface DealDetailProps {
26
27
  /** Edit this deal; omit to create one. */
@@ -53,6 +54,7 @@ function fromDeal(d: Deal): Form {
53
54
  }
54
55
 
55
56
  export function DealDetail({ dealId, ownerId, contactIds = [], pipelineId, allowDuplicate, onSaved, onDeleted, children, className }: DealDetailProps) {
57
+ const brand = useBrand();
56
58
  const { pipelines, defaultPipeline } = usePipelines();
57
59
  const [form, setForm] = useState<Form>(EMPTY);
58
60
  const [confirmDelete, setConfirmDelete] = useState(false);
@@ -60,14 +62,23 @@ export function DealDetail({ dealId, ownerId, contactIds = [], pipelineId, allow
60
62
  const [createdId, setCreatedId] = useState<string | null>(null);
61
63
  const [createdRevision, setCreatedRevision] = useState<number | null>(null);
62
64
  const effectiveId = dealId ?? createdId ?? undefined;
63
- const deal = useG8Query(async (signal) => dataOf<Deal>(await g8Get(`/deals/${encodeURIComponent(dealId as string)}`, undefined, { signal })), [dealId], { enabled: Boolean(dealId) });
64
- const history = useG8Query(async (signal) => dataOf<{ items?: HistoryItem[] }>(await g8Get(`/deals/${encodeURIComponent(dealId as string)}/history`, { limit: 20 }, { signal })).items ?? [], [dealId], { enabled: Boolean(dealId) });
65
- useEffect(() => { if (deal.data) setForm(fromDeal(deal.data)); }, [deal.data]);
66
- // A different deal (or create mode again) must not show the previous one's fields.
67
- useEffect(() => { setForm(EMPTY); setCreatedId(null); setCreatedRevision(null); setConfirmDelete(false); }, [dealId]);
65
+ const deal = useG8Query(async (signal) => dataOf<Deal>(await g8Get(`/deals/${encodeURIComponent(dealId as string)}`, undefined, { signal })), [dealId], {
66
+ enabled: Boolean(dealId),
67
+ key: ["deals", "detail", dealId ?? null],
68
+ // The form is seeded from this record: never swap it under someone typing on a window refocus.
69
+ refetchOnFocus: false,
70
+ });
71
+ const history = useG8Query(async (signal) => dataOf<{ items?: HistoryItem[] }>(await g8Get(`/deals/${encodeURIComponent(dealId as string)}/history`, { limit: 20 }, { signal })).items ?? [], [dealId], { enabled: Boolean(dealId), key: ["deals", "history", dealId ?? null] });
72
+ // Unsaved edits win over a refreshed record (focus, invalidation from a board move, refetch): only an
73
+ // untouched form follows the server. Saving or switching deals makes the form untouched again.
74
+ const dirty = useRef(false);
75
+ // A different deal (or create mode again) must not show the previous one's fields. Declared BEFORE the
76
+ // seeding effect so that, when the record is already cached, the reset runs first and the seed wins.
77
+ useEffect(() => { dirty.current = false; setForm(EMPTY); setCreatedId(null); setCreatedRevision(null); setConfirmDelete(false); }, [dealId]);
78
+ useEffect(() => { if (deal.data && !dirty.current) setForm(fromDeal(deal.data)); }, [deal.data]);
68
79
 
69
80
  const pipeline = pipelines.find((p) => p.id === (deal.data?.pipeline_id ?? pipelineId)) ?? defaultPipeline;
70
- const set = <K extends keyof Form>(k: K, v: Form[K]) => setForm((f) => ({ ...f, [k]: v }));
81
+ const set = <K extends keyof Form>(k: K, v: Form[K]) => { dirty.current = true; setForm((f) => ({ ...f, [k]: v })); };
71
82
 
72
83
  const save = useG8Mutation(async () => {
73
84
  const body = {
@@ -81,11 +92,12 @@ export function DealDetail({ dealId, ownerId, contactIds = [], pipelineId, allow
81
92
  ? await g8Patch(`/deals/${encodeURIComponent(effectiveId)}`, { ...body, expected_revision: (dealId ? deal.data?.revision : createdRevision) ?? undefined })
82
93
  : await g8Post("/deals", { ...body, owner_id: ownerId, contact_ids: contactIds, allow_duplicate: allowDuplicate || undefined, pipeline_id: pipeline?.id, amount: body.amount ?? undefined, close_date: body.close_date ?? undefined });
83
94
  return dataOf<Deal>(env);
84
- }, { onSuccess: (saved) => { if (!dealId && saved.id) { setCreatedId(saved.id); setCreatedRevision(saved.revision ?? null); } if (dealId) { void deal.refetch(); void history.refetch(); } onSaved?.(saved); } });
85
- const remove = useG8Mutation(async () => { await g8Delete(`/deals/${encodeURIComponent(dealId as string)}`); }, { onSuccess: () => { setConfirmDelete(false); onDeleted?.(dealId as string); }, onError: () => setConfirmDelete(false) });
95
+ }, { invalidates: [["deals", "list"], ["deals", "history"]], onSuccess: (saved) => { dirty.current = false; if (!dealId && saved.id) { setCreatedId(saved.id); setCreatedRevision(saved.revision ?? null); } if (dealId) { void deal.refetch(); void history.refetch(); } onSaved?.(saved); } });
96
+ const remove = useG8Mutation(async () => { await g8Delete(`/deals/${encodeURIComponent(dealId as string)}`); }, { invalidates: [["deals", "list"]], onSuccess: () => { setConfirmDelete(false); onDeleted?.(dealId as string); }, onError: () => setConfirmDelete(false) });
86
97
 
87
98
  if (deal.error) return <ApiError error={deal.error} title="Could not load the deal" onRetry={() => void deal.refetch()} />;
88
- if (dealId && deal.loading && !deal.data) return <Skeleton className="h-56 w-full" aria-label="Loading deal" />;
99
+ // The hook keeps the previous deal's data while the next loads: wait for THIS deal, so a blank form is never shown (or saved) over it.
100
+ if (dealId && (!deal.data || String(deal.data.id) !== String(dealId))) return <Skeleton className="h-56 w-full" aria-label="Loading deal" />;
89
101
  const d = deal.data;
90
102
  const canCreate = Boolean(ownerId);
91
103
 
@@ -105,7 +117,7 @@ export function DealDetail({ dealId, ownerId, contactIds = [], pipelineId, allow
105
117
  </div>
106
118
  </div>
107
119
  <div className="space-y-1"><Label htmlFor="deal-desc">Description</Label><Textarea id="deal-desc" rows={3} value={form.description} onChange={(e) => set("description", e.target.value)} /></div>
108
- {!dealId && !canCreate ? <p className="text-sm text-muted-foreground">Pass ownerId (a graph8 user id) to create deals: the API requires an owner.</p> : null}
120
+ {!dealId && !canCreate ? <p className="text-sm text-muted-foreground">Pass ownerId (a {brand.linkLabel} user id) to create deals: the API requires an owner.</p> : null}
109
121
  {save.error ? <ApiError error={save.error} title="Could not save the deal" /> : null}
110
122
  {remove.error ? <ApiError error={remove.error} title="Could not delete the deal" /> : null}
111
123
  <div className="flex justify-between">
@@ -24,7 +24,7 @@ import { dataOf, type Deal } from "../lib/deals-types";
24
24
 
25
25
  export function DealContactsTab({ dealId }: { dealId: string }) {
26
26
  const [open, setOpen] = useState<number | null>(null);
27
- const q = useG8Query(async (signal) => dataOf<Deal>(await g8Get(`/deals/${encodeURIComponent(dealId)}`, undefined, { signal })), [dealId]);
27
+ const q = useG8Query(async (signal) => dataOf<Deal>(await g8Get(`/deals/${encodeURIComponent(dealId)}`, undefined, { signal })), [dealId], { key: ["deals", "detail", dealId] });
28
28
  if (q.error) return <ApiError error={q.error} title="Could not load the deal's contacts" onRetry={() => void q.refetch()} />;
29
29
  if (!q.data) return <Skeleton className="h-24 w-full" aria-label="Loading contacts" />;
30
30
  const contacts = q.data.contacts ?? [];
@@ -43,6 +43,8 @@ import { useG8Query } from "@/g8/hooks/use-g8-query";
43
43
  import { ActivityTimeline } from "@/g8/crm/components/activity-timeline";
44
44
  import { useDebouncedValue } from "@/g8/crm/hooks/use-debounced";
45
45
  import { contactName, type ContactRecord } from "@/g8/crm/lib/crm-types";
46
+ import { rowActivationProps } from "@/g8/hooks/use-row-activation";
47
+ import { RecordLink } from "@/g8/components/screen/record-link";
46
48
  import { useDefaultOwner } from "../hooks/use-default-owner";
47
49
  import { usePipelineDeals } from "../hooks/use-pipeline-deals";
48
50
  import { usePipelines } from "../hooks/use-pipelines";
@@ -52,6 +54,7 @@ import { DealContactsTab, DealQuotesTab } from "./deal-related";
52
54
  import { ForecastView } from "./forecast-view";
53
55
  import { PipelineBoard } from "./pipeline-board";
54
56
  import { StageEditor } from "./stage-editor";
57
+ import { useBrand } from "@/g8/hooks/use-brand";
55
58
 
56
59
  export interface DealsScreenProps {
57
60
  /** Start on this pipeline; defaults to the org's default one. */
@@ -84,6 +87,7 @@ function stageTone(stage: Stage | undefined): "neutral" | "positive" | "negative
84
87
 
85
88
  /** Contact search + chips for the new-deal sheet. The API requires at least one contact. */
86
89
  function ContactPicker({ value, onChange }: { value: ContactRecord[]; onChange: (next: ContactRecord[]) => void }) {
90
+ const brand = useBrand();
87
91
  const [text, setText] = useState("");
88
92
  const term = useDebouncedValue(text.trim());
89
93
  const found = useG8Query(async (signal) => {
@@ -119,7 +123,7 @@ function ContactPicker({ value, onChange }: { value: ContactRecord[]; onChange:
119
123
  </li>
120
124
  ))}
121
125
  </ul>
122
- <p className="text-xs text-muted-foreground">graph8 needs at least one contact on a deal. Every contact must work at the same company.</p>
126
+ <p className="text-xs text-muted-foreground">{brand.linkLabel} needs at least one contact on a deal. Every contact must work at the same company.</p>
123
127
  </div>
124
128
  );
125
129
  }
@@ -182,8 +186,8 @@ function DealRows({ deals, stages, selectedId, onOpen, renderRowActions }: { dea
182
186
  {deals.map((d) => {
183
187
  const stage = d.stage_id ? byId.get(d.stage_id) : undefined;
184
188
  return (
185
- <TableRow key={d.id} data-state={selectedId === d.id ? "selected" : undefined} className="cursor-pointer" onClick={() => onOpen(d)}>
186
- <TableCell><button type="button" className="font-medium hover:underline" onClick={(e) => { e.stopPropagation(); onOpen(d); }}>{d.name || "Untitled deal"}</button></TableCell>
189
+ <TableRow key={d.id} data-state={selectedId === d.id ? "selected" : undefined} className="cursor-pointer" {...rowActivationProps(() => onOpen(d))}>
190
+ <TableCell><RecordLink param="id" id={d.id} onOpen={() => onOpen(d)}>{d.name || "Untitled deal"}</RecordLink></TableCell>
187
191
  <TableCell><StatusPill tone={stageTone(stage)} dot>{stage?.name ?? d.stage_name ?? "Unknown"}</StatusPill></TableCell>
188
192
  <TableCell className="text-right tabular-nums">{formatMoney(d.amount, d.currency)}</TableCell>
189
193
  <TableCell><span className="inline-flex items-center gap-2"><EntityAvatar name={d.owner_name ?? d.owner_email ?? "?"} size="sm" />{d.owner_name ?? d.owner_email ?? "No owner"}</span></TableCell>
@@ -198,6 +202,7 @@ function DealRows({ deals, stages, selectedId, onOpen, renderRowActions }: { dea
198
202
  }
199
203
 
200
204
  export function DealsScreen({ pipelineId: initialPipelineId, renderRowActions, renderDetailActions, extraDetailTabs, toolbarActions, onOpenQuote }: DealsScreenProps) {
205
+ const brand = useBrand();
201
206
  const [viewParam, setViewParam] = useUrlState("tab", "board");
202
207
  const [selectedId, setSelectedId] = useUrlState("id");
203
208
  const [mode, setMode] = useUrlState("view");
@@ -228,7 +233,7 @@ export function DealsScreen({ pipelineId: initialPipelineId, renderRowActions, r
228
233
 
229
234
  const listed = data.deals.find((d) => d.id === selectedId);
230
235
  // A deep link can point at a deal in another pipeline or hidden by the search: resolve it directly.
231
- const fetched = useG8Query(async (signal) => dataOf<Deal>(await g8Get(`/deals/${encodeURIComponent(selectedId ?? "")}`, undefined, { signal })), [selectedId, version], { enabled: Boolean(selectedId) && !listed && !data.loading });
236
+ const fetched = useG8Query(async (signal) => dataOf<Deal>(await g8Get(`/deals/${encodeURIComponent(selectedId ?? "")}`, undefined, { signal })), [selectedId, version], { enabled: Boolean(selectedId) && !listed && !data.loading, key: ["deals", "detail", selectedId ?? null] });
232
237
  const selected = listed ?? (fetched.data?.id === selectedId ? fetched.data : undefined);
233
238
  const open = (id: string) => setSelectedId(id);
234
239
  const close = () => { setSelectedId(null); setMode(null); };
@@ -259,7 +264,7 @@ export function DealsScreen({ pipelineId: initialPipelineId, renderRowActions, r
259
264
 
260
265
  let body: ReactNode;
261
266
  if (view === "forecast") body = <ForecastView pipelineId={pipeline?.id} />;
262
- else if (view === "stages") body = pipeline ? <div className="space-y-6"><StageSummary stages={stages} deals={data.deals} /><StageEditor pipelineId={pipeline.id} onChanged={refresh} /></div> : <EmptyState title="No pipeline yet" description="Create a pipeline in graph8 first." />;
267
+ else if (view === "stages") body = pipeline ? <div className="space-y-6"><StageSummary stages={stages} deals={data.deals} /><StageEditor pipelineId={pipeline.id} onChanged={refresh} /></div> : <EmptyState title="No pipeline yet" description={`Create a pipeline in ${brand.linkLabel} first.`} />;
263
268
  else body = <SplitView detailOpen={Boolean(selectedId)} onCloseDetail={close} expanded={mode === "full" && Boolean(selectedId)} onExpandedChange={(full) => setMode(full ? "full" : null)} detailWidth="lg" detail={detail} list={view === "board" ? <PipelineBoard key={`${pipeline?.id}-${version}`} pipelineId={pipeline?.id} search={search} onSelectDeal={(d) => open(d.id)} onChanged={() => void data.reload()} /> : list} />;
264
269
 
265
270
  return (
@@ -45,21 +45,21 @@ export function StageEditor({ pipelineId, onChanged, className }: StageEditorPro
45
45
  const saved = dataOf<Stage>(await g8Put(`${base}/stages/${encodeURIComponent(s.id)}`, { name: s.name, probability: s.probability ?? 0, stage_type: s.stage_type ?? "open", color: s.color ?? undefined }));
46
46
  done(stages.map((x) => (x.id === saved.id ? { ...x, ...saved } : x)));
47
47
  } finally { setSavingId(null); }
48
- });
48
+ }, { invalidates: [["deals", "pipelines"]] });
49
49
  const add = useG8Mutation(async (name: string) => {
50
50
  const created = dataOf<Stage>(await g8Post(`${base}/stages`, { name, probability: 0, stage_type: "open" }));
51
51
  done([...stages, created]);
52
52
  setNewName("");
53
- });
53
+ }, { invalidates: [["deals", "pipelines"]] });
54
54
  const reorder = useG8Mutation(async (next: Stage[]) => {
55
55
  await g8Put(`${base}/stages/reorder`, { stage_ids: next.map((s) => s.id) });
56
56
  done(next);
57
- });
57
+ }, { invalidates: [["deals", "pipelines"]] });
58
58
  const remove = useG8Mutation(async (s: Stage) => {
59
59
  await g8Delete(`${base}/stages/${encodeURIComponent(s.id)}`);
60
60
  done(stages.filter((x) => x.id !== s.id));
61
61
  setRemoving(null);
62
- }, { onError: () => setRemoving(null) });
62
+ }, { invalidates: [["deals", "pipelines"]], onError: () => setRemoving(null) });
63
63
 
64
64
  const patch = (id: string, change: Partial<Stage>) => setStages((all) => all.map((s) => (s.id === id ? { ...s, ...change } : s)));
65
65
  const swap = (i: number, j: number) => { const next = [...stages]; [next[i], next[j]] = [next[j], next[i]]; void reorder.mutate(next); };
@@ -15,6 +15,7 @@
15
15
  import { useCallback, useEffect, useState } from "react";
16
16
  import { g8Get, g8Patch } from "@/g8/lib/g8-client";
17
17
  import { useG8Query } from "@/g8/hooks/use-g8-query";
18
+ import { invalidate } from "@/g8/lib/g8-cache";
18
19
  import type { Deal } from "../lib/deals-types";
19
20
  import { dataOf } from "../lib/deals-types";
20
21
 
@@ -42,7 +43,7 @@ export function usePipelineDeals(pipelineId: string | undefined, filters: Pipeli
42
43
  return all.slice(0, maxDeals);
43
44
  },
44
45
  [pipelineId, filters.search, filters.ownerId, maxDeals],
45
- { enabled: Boolean(pipelineId) },
46
+ { enabled: Boolean(pipelineId), key: ["deals", "list", "board", { pipelineId, search: filters.search || undefined, ownerId: filters.ownerId || undefined, maxDeals }] },
46
47
  );
47
48
  useEffect(() => { if (query.data) setDeals(query.data); }, [query.data]);
48
49
 
@@ -53,6 +54,9 @@ export function usePipelineDeals(pipelineId: string | undefined, filters: Pipeli
53
54
  try {
54
55
  const saved = dataOf<Deal>(await g8Patch(`/deals/${encodeURIComponent(deal.id)}`, { stage_id: stageId, expected_revision: deal.revision ?? undefined, ...extra }));
55
56
  setDeals((all) => all.map((d) => (d.id === deal.id ? { ...d, ...saved } : d)));
57
+ // The deal's revision changed: a cached detail or history of it is out of date.
58
+ void invalidate(["deals", "detail"]);
59
+ void invalidate(["deals", "history"]);
56
60
  return saved;
57
61
  } catch (err) {
58
62
  // Roll back by re-reading the server's truth: a snapshot taken before the move could itself be stale.
@@ -13,7 +13,7 @@ export function usePipelines() {
13
13
  const q = useG8Query(async (signal) => {
14
14
  const env = await g8Get<Pipeline[]>("/deals/pipelines", undefined, { signal });
15
15
  return (env.data ?? []).map((p) => ({ ...p, stages: sortedStages(p) }));
16
- }, []);
16
+ }, [], { key: ["deals", "pipelines"] });
17
17
  const pipelines = q.data ?? [];
18
18
  return { ...q, pipelines, defaultPipeline: pipelines.find((p) => p.is_default) ?? pipelines[0] };
19
19
  }
@@ -9,6 +9,26 @@ browser, record outcomes, and review transcripts. Copy `DialerWorkspace` for the
9
9
  > voicemail, or mints a softphone credential without a confirm dialog first, and the block never auto-advances to
10
10
  > the next batch.
11
11
 
12
+ ## Screens
13
+
14
+ - `DialerScreen` (`components/dialer-screen.tsx`): **Session** tab picks a list (ListPicker), creates a session (dials
15
+ nobody), then runs the queue: every batch, voicemail drop, softphone credential and stop is confirmed; finished calls
16
+ open the outcome panel and transcript. **Sessions** tab lists every session; a row opens `DialerSessionDetail` (stats, details, calls, queue; pause, confirmed resume, confirmed stop; `?tab=sessions&session=<id>`). **History** tab lists the rep's calls. Props are optional: `userId` (needed to
17
+ enable the browser softphone), `userEmail`, `listId`, `sessionId`, `transport` (default: the Twilio adapter, loaded
18
+ on demand, needs `@twilio/voice-sdk`). Resume a session with `?session=<id>`. Add `&view=full` to any detail (session, call, and the same in the other screens) to open it as a full page.
19
+ - `CallLogScreen` (`components/call-log-screen.tsx`): history of one rep, contact or session with outcome filter and search;
20
+ open a call for recording, transcript and AI grade (`?id=<room_name>`), and **Call back** the number (`CallContactButton`). Needs a rep email (it asks) or a prop.
21
+ - Slots on both: `toolbarActions`, `renderRowActions`, `renderDetailActions`, `extraDetailTabs`.
22
+ - `CallContactButton` (`components/call-contact-button.tsx`): **call one contact from any screen**. Drop it into a
23
+ contact page's `renderDetailActions` (the apollo and outreach stacks do) or a row slot: `<CallContactButton contact={c} />`.
24
+ Closed, it loads nothing. Open: pick the number (masked `***` numbers are never offered), a PAUSED or COMPLETED
25
+ session to log the call under, and the caller ID; turn on the softphone; confirm (external, 1 recipient). It calls
26
+ `POST /dialer/parallel-calls/manual-dial` and the softphone joins the returned `conference_name`. The rep is
27
+ `userId` if passed, else the personal key's user (`GET /team-members/me`); an organisation key is not a rep, so it
28
+ then asks for `userId`. Why not `next-batch` with `force_next_contact`: that only picks a contact already in the
29
+ session's list and dials a whole batch.
30
+ Scopes: `voice:read`, `voice:write`, `voice:run` (and `team:read` for `CallContactButton` without `userId`). A phone number must exist (buy or verify it in the graph8 app).
31
+
12
32
  ## How it works (read this before building)
13
33
 
14
34
  - graph8 places the calls **from its servers** into a conference. The rep's browser only **joins that
@@ -81,13 +101,22 @@ and the call log for a rep email. Pass `userId` / `userEmail` from your session
81
101
  - Voicemail drop plays the rep's cloned voice to a stranger's phone. The confirm dialog names the contact.
82
102
  - Another tenant's session id is a 404, the same as a missing one.
83
103
  - Manual dial (`POST /dialer/parallel-calls/manual-dial`, external) needs a PAUSED or COMPLETED session and can
84
- create a contact; it is a server recipe here, not a component.
104
+ create a contact (an unknown number is matched to a contact or one is created). `CallContactButton` uses it.
105
+ - **Late join.** The conference name of a batch is only known from the `next-batch` (or `manual-dial`) response; the
106
+ app learns the NEXT batch's name earlier over a WebSocket the API does not have, and the session read does not
107
+ expose it. Voice waits up to 2 s for the rep only when the name was announced, so an SDK softphone joins after the
108
+ dials go out and the first second of an instant answer can be missed (voice's late-join reconciliation still
109
+ connects the audio). Keep the softphone registered BEFORE dialing (`CallContactButton` only enables the call once
110
+ it is ready) so the join is a single `joinConference` the moment the response lands.
111
+ - `next-batch` `user_email`: with a personal key the dialer's rep is the key's own user (naming someone else is a
112
+ 403); with an organisation key the name is passed through but not vouched for, so voice keeps dialing without
113
+ moving the session to that rep.
85
114
 
86
115
  ## Operations
87
116
 
88
117
  | Operation | Method and path | Tier | Used by |
89
118
  |---|---|---|---|
90
- | `voice.listDialerNumbers` | GET /voice/dialer/numbers | read | SessionStartForm |
119
+ | `voice.listDialerNumbers` | GET /voice/dialer/numbers | read | SessionStartForm, CallContactButton |
91
120
  | `dialer.createSessionDraft` | POST /dialer/sessions/draft | write | SessionStartForm |
92
121
  | `dialer.readQueueStats` | GET /dialer/sessions/{id}/queue-stats | read | useDialerSession |
93
122
  | `voice.getDialerSession` | GET /dialer/sessions/{id} | read | useDialerSession |
@@ -106,6 +135,9 @@ and the call log for a rep email. Pass `userId` / `userEmail` from your session
106
135
  | `voice.getCallTranscript` | GET /voice/dialer/calls/{room}/transcript | read | TranscriptViewer |
107
136
  | `voice.getCallGrading` | GET /voice/dialer/calls/{room}/grading | read | TranscriptViewer |
108
137
  | `voice.downloadCallRecording` | GET /voice/calls/{id}/recording | read | TranscriptViewer |
138
+ | `teamMembers.getMyTeamMember` | GET /team-members/me | read | CallContactButton (only without `userId`) |
139
+ | `voice.listDialerSessions` | GET /voice/dialer/sessions | read | CallContactButton |
140
+ | `dialer.manualDial` | POST /dialer/parallel-calls/manual-dial | external | CallContactButton |
109
141
 
110
142
  ## Recipes (server side)
111
143
 
@@ -21,15 +21,19 @@
21
21
  "get_call_grading_voice_dialer_calls__room_name__grading_get",
22
22
  "get_call_transcript_voice_dialer_calls__room_name__transcript_get",
23
23
  "get_dialer_session_dialer_sessions__session_id__get",
24
+ "get_my_team_member_team_members_me_get",
24
25
  "hangup_session_call_dialer_sessions_calls_hangup_post",
25
26
  "list_calls_for_contact_voice_dialer_calls_get",
26
27
  "list_dialer_numbers_voice_dialer_numbers_get",
28
+ "list_dialer_sessions_voice_dialer_sessions_get",
27
29
  "list_voice_dispositions_voice_dispositions_get",
30
+ "manual_dial_dialer_parallel_calls_manual_dial_post",
28
31
  "mint_softphone_token_dialer_sessions_token_get",
29
32
  "mute_call_dialer_sessions_calls_mute_post",
30
33
  "pause_dialer_session_dialer_sessions__session_id__pause_post",
31
34
  "read_queue_stats_dialer_sessions__session_id__queue_stats_get",
32
35
  "read_session_grid_dialer_sessions__session_id__grid_post",
36
+ "resume_dialer_session_voice_dialer_sessions__session_id__resume_post",
33
37
  "set_call_disposition_dialer_parallel_calls_calls__livekit_room_name__disposition_patch",
34
38
  "skip_contact_dialer_sessions__session_id__calls_skip_contact_post",
35
39
  "stop_dialer_session_dialer_sessions__session_id__stop_post"
@@ -135,6 +139,61 @@
135
139
  "purpose": "The whole rep screen wiring queue, softphone, outcome panel and transcript together.",
136
140
  "operations": [],
137
141
  "tiers": []
142
+ },
143
+ {
144
+ "name": "DialerScreen",
145
+ "file": "components/dialer-screen.tsx",
146
+ "purpose": "Finished calling page: pick a list, run a session, softphone, outcomes, transcript and history.",
147
+ "operations": [
148
+ "get_dialer_session_dialer_sessions__session_id__get",
149
+ "list_dialer_sessions_voice_dialer_sessions_get"
150
+ ],
151
+ "tiers": [
152
+ "read"
153
+ ]
154
+ },
155
+ {
156
+ "name": "CallLogScreen",
157
+ "file": "components/call-log-screen.tsx",
158
+ "purpose": "Finished call history page with outcome filter, recording, transcript and AI grade, and Call back on a call (CallContactButton, confirmed); also exports CallHistory.",
159
+ "operations": [
160
+ "list_calls_for_contact_voice_dialer_calls_get"
161
+ ],
162
+ "tiers": [
163
+ "read"
164
+ ]
165
+ },
166
+ {
167
+ "name": "DialerSessionDetail",
168
+ "file": "components/dialer-session-detail.tsx",
169
+ "purpose": "One dialing session as a full record: stats, details, calls and queue, with pause, confirmed resume and confirmed stop.",
170
+ "operations": [
171
+ "get_dialer_session_dialer_sessions__session_id__get",
172
+ "pause_dialer_session_dialer_sessions__session_id__pause_post",
173
+ "read_queue_stats_dialer_sessions__session_id__queue_stats_get",
174
+ "resume_dialer_session_voice_dialer_sessions__session_id__resume_post",
175
+ "stop_dialer_session_dialer_sessions__session_id__stop_post"
176
+ ],
177
+ "tiers": [
178
+ "read",
179
+ "write",
180
+ "external"
181
+ ]
182
+ },
183
+ {
184
+ "name": "CallContactButton",
185
+ "file": "components/call-contact-button.tsx",
186
+ "purpose": "Call one contact from any screen (contact page, table row): pick the number, a paused or completed session and the caller ID, turn on the softphone, confirm (external), and the softphone joins the call.",
187
+ "operations": [
188
+ "get_my_team_member_team_members_me_get",
189
+ "list_dialer_sessions_voice_dialer_sessions_get",
190
+ "list_dialer_numbers_voice_dialer_numbers_get",
191
+ "manual_dial_dialer_parallel_calls_manual_dial_post"
192
+ ],
193
+ "tiers": [
194
+ "read",
195
+ "external"
196
+ ]
138
197
  }
139
198
  ],
140
199
  "hooks": [
@@ -222,6 +281,7 @@
222
281
  }
223
282
  ],
224
283
  "scopes": [
284
+ "team:read",
225
285
  "voice:read",
226
286
  "voice:run",
227
287
  "voice:write"
@@ -251,10 +311,16 @@
251
311
  "decision": "Browser audio vendor for the Softphone",
252
312
  "default": "Twilio via lib/twilio-transport.ts; graph8's dialer runs on Twilio conferences. For another vendor write a SoftphoneTransport and delete twilio-transport.ts",
253
313
  "askHuman": true
314
+ },
315
+ {
316
+ "decision": "How a single contact is called outside a dialing session",
317
+ "default": "manual-dial under a PAUSED or COMPLETED session the rep picks (same call log and analytics), only once the softphone is ready; the rep is the personal key's user unless userId is passed",
318
+ "askHuman": false
254
319
  }
255
320
  ],
256
321
  "peerDependencies": {
257
322
  "react": ">=18",
258
- "@twilio/voice-sdk": "^2.18.5"
323
+ "@twilio/voice-sdk": "^2.18.5",
324
+ "lucide-react": ">=0.400.0"
259
325
  }
260
326
  }