@signalcore/ui 0.1.0 → 1.0.0-beta.1

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 (611) hide show
  1. package/README.md +896 -513
  2. package/dist/AgentAbout.d.ts +186 -0
  3. package/dist/AgentAbout.js +1020 -0
  4. package/dist/AgentAbout.js.map +1 -0
  5. package/dist/AgentCard.d.ts +49 -0
  6. package/dist/AgentCard.js +77 -0
  7. package/dist/AgentCard.js.map +1 -0
  8. package/dist/AgentChat.d.ts +233 -0
  9. package/dist/AgentChat.js +1127 -0
  10. package/dist/AgentChat.js.map +1 -0
  11. package/dist/AgentSpace.d.ts +112 -0
  12. package/dist/AgentSpace.js +339 -0
  13. package/dist/AgentSpace.js.map +1 -0
  14. package/dist/AgentTasks.d.ts +48 -0
  15. package/dist/AgentTasks.js +120 -0
  16. package/dist/AgentTasks.js.map +1 -0
  17. package/dist/BrowserView.d.ts +52 -0
  18. package/dist/BrowserView.js +328 -0
  19. package/dist/BrowserView.js.map +1 -0
  20. package/dist/FileView.d.ts +63 -0
  21. package/dist/FileView.js +449 -0
  22. package/dist/FileView.js.map +1 -0
  23. package/dist/Modes.d.ts +13 -0
  24. package/dist/Modes.js +75 -0
  25. package/dist/Modes.js.map +1 -0
  26. package/dist/VoiceCall.d.ts +91 -0
  27. package/dist/VoiceCall.js +302 -0
  28. package/dist/VoiceCall.js.map +1 -0
  29. package/dist/about.d.ts +212 -0
  30. package/dist/about.js +136 -0
  31. package/dist/about.js.map +1 -0
  32. package/dist/agentIcons.d.ts +33 -0
  33. package/dist/agentIcons.js +36 -0
  34. package/dist/agentIcons.js.map +1 -0
  35. package/dist/automation/Automation.d.ts +35 -0
  36. package/dist/automation/Automation.js +268 -0
  37. package/dist/automation/Automation.js.map +1 -0
  38. package/dist/automation/RunView.d.ts +25 -0
  39. package/dist/automation/RunView.js +187 -0
  40. package/dist/automation/RunView.js.map +1 -0
  41. package/dist/automation/cron.d.ts +41 -0
  42. package/dist/automation/cron.js +169 -0
  43. package/dist/automation/cron.js.map +1 -0
  44. package/dist/automation/marks.js +121 -0
  45. package/dist/automation/marks.js.map +1 -0
  46. package/dist/automation/messages.d.ts +113 -0
  47. package/dist/automation/messages.js +113 -0
  48. package/dist/automation/messages.js.map +1 -0
  49. package/dist/automation/record.d.ts +47 -0
  50. package/dist/automation/record.js +177 -0
  51. package/dist/automation/record.js.map +1 -0
  52. package/dist/automation/types.d.ts +143 -0
  53. package/dist/automation/when.d.ts +36 -0
  54. package/dist/automation/when.js +168 -0
  55. package/dist/automation/when.js.map +1 -0
  56. package/dist/blocks.d.ts +185 -0
  57. package/dist/browser.js +292 -0
  58. package/dist/browser.js.map +1 -0
  59. package/dist/call.d.ts +89 -0
  60. package/dist/call.js +218 -0
  61. package/dist/call.js.map +1 -0
  62. package/dist/channelParts.js +26 -0
  63. package/dist/channelParts.js.map +1 -0
  64. package/dist/classes.js +8 -3
  65. package/dist/classes.js.map +1 -0
  66. package/dist/client.d.ts +146 -54
  67. package/dist/client.js +204 -1
  68. package/dist/client.js.map +1 -0
  69. package/dist/conversationFiles.js +46 -0
  70. package/dist/conversationFiles.js.map +1 -0
  71. package/dist/credentials.d.ts +45 -0
  72. package/dist/credentials.js +98 -0
  73. package/dist/credentials.js.map +1 -0
  74. package/dist/dev.js +17 -0
  75. package/dist/dev.js.map +1 -0
  76. package/dist/es.d.ts +17 -0
  77. package/dist/es.js +382 -0
  78. package/dist/es.js.map +1 -0
  79. package/dist/files.d.ts +21 -0
  80. package/dist/files.js +214 -0
  81. package/dist/files.js.map +1 -0
  82. package/dist/frames.d.ts +115 -0
  83. package/dist/frames.js +219 -0
  84. package/dist/frames.js.map +1 -0
  85. package/dist/icons.js +29 -0
  86. package/dist/icons.js.map +1 -0
  87. package/dist/index.d.ts +63 -60
  88. package/dist/index.js +59 -45
  89. package/dist/kit/AudioPlayer.d.ts +20 -0
  90. package/dist/kit/AudioPlayer.js +116 -0
  91. package/dist/kit/AudioPlayer.js.map +1 -0
  92. package/dist/kit/Button.d.ts +29 -0
  93. package/dist/kit/Button.js +59 -0
  94. package/dist/kit/Button.js.map +1 -0
  95. package/dist/kit/Card.d.ts +16 -0
  96. package/dist/kit/Card.js +36 -0
  97. package/dist/kit/Card.js.map +1 -0
  98. package/dist/kit/Chart.js +152 -0
  99. package/dist/kit/Chart.js.map +1 -0
  100. package/dist/kit/Choices.d.ts +21 -0
  101. package/dist/kit/Choices.js +99 -0
  102. package/dist/kit/Choices.js.map +1 -0
  103. package/dist/kit/Combobox.d.ts +26 -0
  104. package/dist/kit/Combobox.js +190 -0
  105. package/dist/kit/Combobox.js.map +1 -0
  106. package/dist/kit/DateField.d.ts +17 -0
  107. package/dist/kit/DateField.js +133 -0
  108. package/dist/kit/DateField.js.map +1 -0
  109. package/dist/kit/Field.d.ts +40 -0
  110. package/dist/kit/Field.js +98 -0
  111. package/dist/kit/Field.js.map +1 -0
  112. package/dist/kit/FileField.d.ts +113 -0
  113. package/dist/kit/FileField.js +341 -0
  114. package/dist/kit/FileField.js.map +1 -0
  115. package/dist/kit/FileMark.d.ts +15 -0
  116. package/dist/kit/FileMark.js +39 -0
  117. package/dist/kit/FileMark.js.map +1 -0
  118. package/dist/kit/Inputs.d.ts +59 -0
  119. package/dist/kit/Inputs.js +205 -0
  120. package/dist/kit/Inputs.js.map +1 -0
  121. package/dist/kit/List.d.ts +47 -0
  122. package/dist/kit/List.js +85 -0
  123. package/dist/kit/List.js.map +1 -0
  124. package/dist/kit/Mark.d.ts +30 -0
  125. package/dist/kit/Mark.js +27 -0
  126. package/dist/kit/Mark.js.map +1 -0
  127. package/dist/kit/MarkStack.d.ts +15 -0
  128. package/dist/kit/MarkStack.js +26 -0
  129. package/dist/kit/MarkStack.js.map +1 -0
  130. package/dist/kit/Note.d.ts +11 -0
  131. package/dist/kit/Note.js +31 -0
  132. package/dist/kit/Note.js.map +1 -0
  133. package/dist/kit/Popover.js +90 -0
  134. package/dist/kit/Popover.js.map +1 -0
  135. package/dist/kit/RangeField.d.ts +14 -0
  136. package/dist/kit/RangeField.js +47 -0
  137. package/dist/kit/RangeField.js.map +1 -0
  138. package/dist/kit/RatingField.d.ts +14 -0
  139. package/dist/kit/RatingField.js +50 -0
  140. package/dist/kit/RatingField.js.map +1 -0
  141. package/dist/kit/Stat.d.ts +26 -0
  142. package/dist/kit/Stat.js +46 -0
  143. package/dist/kit/Stat.js.map +1 -0
  144. package/dist/kit/Switch.d.ts +14 -0
  145. package/dist/kit/Switch.js +21 -0
  146. package/dist/kit/Switch.js.map +1 -0
  147. package/dist/kit/Tag.d.ts +16 -0
  148. package/dist/kit/Tag.js +20 -0
  149. package/dist/kit/Tag.js.map +1 -0
  150. package/dist/kit/Tile.d.ts +34 -0
  151. package/dist/kit/Tile.js +52 -0
  152. package/dist/kit/Tile.js.map +1 -0
  153. package/dist/kit/TimeField.d.ts +31 -0
  154. package/dist/kit/TimeField.js +109 -0
  155. package/dist/kit/TimeField.js.map +1 -0
  156. package/dist/kit/context.d.ts +22 -0
  157. package/dist/kit/context.js +46 -0
  158. package/dist/kit/context.js.map +1 -0
  159. package/dist/kit/dates.d.ts +14 -0
  160. package/dist/kit/dates.js +129 -0
  161. package/dist/kit/dates.js.map +1 -0
  162. package/dist/kit/messages.d.ts +99 -0
  163. package/dist/kit/messages.js +66 -0
  164. package/dist/kit/messages.js.map +1 -0
  165. package/dist/kit/numbers.d.ts +12 -0
  166. package/dist/kit/numbers.js +40 -0
  167. package/dist/kit/numbers.js.map +1 -0
  168. package/dist/kit/options.d.ts +10 -0
  169. package/dist/kit/options.js +23 -0
  170. package/dist/kit/options.js.map +1 -0
  171. package/dist/kit/uploads.d.ts +42 -0
  172. package/dist/kit/uploads.js +165 -0
  173. package/dist/kit/uploads.js.map +1 -0
  174. package/dist/kit/waveform.js +76 -0
  175. package/dist/kit/waveform.js.map +1 -0
  176. package/dist/locale.d.ts +1 -0
  177. package/dist/locale.js +1 -0
  178. package/dist/markdown.js +84 -0
  179. package/dist/markdown.js.map +1 -0
  180. package/dist/marks.js +217 -0
  181. package/dist/marks.js.map +1 -0
  182. package/dist/reveal.js +48 -0
  183. package/dist/reveal.js.map +1 -0
  184. package/dist/stepGlyphs.js +62 -0
  185. package/dist/stepGlyphs.js.map +1 -0
  186. package/dist/surface/Form.js +264 -0
  187. package/dist/surface/Form.js.map +1 -0
  188. package/dist/surface/Markdown.js +42 -0
  189. package/dist/surface/Markdown.js.map +1 -0
  190. package/dist/surface/Surface.d.ts +31 -17
  191. package/dist/surface/Surface.js +480 -57
  192. package/dist/surface/Surface.js.map +1 -0
  193. package/dist/surface/scope.d.ts +32 -0
  194. package/dist/surface/scope.js +23 -0
  195. package/dist/surface/scope.js.map +1 -0
  196. package/dist/surface/values.d.ts +44 -0
  197. package/dist/surface/values.js +237 -0
  198. package/dist/surface/values.js.map +1 -0
  199. package/dist/tint.d.ts +10 -0
  200. package/dist/tint.js +19 -0
  201. package/dist/tint.js.map +1 -0
  202. package/dist/types.d.ts +541 -0
  203. package/dist/types.js +16 -0
  204. package/dist/types.js.map +1 -0
  205. package/dist/useAgentFace.d.ts +18 -0
  206. package/dist/useAgentFace.js +45 -0
  207. package/dist/useAgentFace.js.map +1 -0
  208. package/dist/useCall.d.ts +39 -0
  209. package/dist/useCall.js +273 -0
  210. package/dist/useCall.js.map +1 -0
  211. package/dist/useConversation.d.ts +77 -0
  212. package/dist/useConversation.js +523 -0
  213. package/dist/useConversation.js.map +1 -0
  214. package/dist/useDictation.d.ts +17 -0
  215. package/dist/useDictation.js +68 -0
  216. package/dist/useDictation.js.map +1 -0
  217. package/dist/useLive.d.ts +26 -0
  218. package/dist/useLive.js +88 -0
  219. package/dist/useLive.js.map +1 -0
  220. package/dist/usePolled.js +68 -0
  221. package/dist/usePolled.js.map +1 -0
  222. package/dist/voice/Orb.d.ts +10 -9
  223. package/dist/voice/Orb.js +43 -39
  224. package/dist/voice/Orb.js.map +1 -0
  225. package/dist/voice/StillOrb.js +33 -0
  226. package/dist/voice/StillOrb.js.map +1 -0
  227. package/dist/voice/WebGLOrb.js +98 -0
  228. package/dist/voice/WebGLOrb.js.map +1 -0
  229. package/dist/voice/fluidDots.js +161 -0
  230. package/dist/voice/fluidDots.js.map +1 -0
  231. package/dist/voice/motion.js +84 -0
  232. package/dist/voice/motion.js.map +1 -0
  233. package/dist/voice/orbs.d.ts +32 -0
  234. package/dist/voice/orbs.js +16 -0
  235. package/dist/voice/orbs.js.map +1 -0
  236. package/dist/voice/useReducedMotion.js +16 -16
  237. package/dist/voice/useReducedMotion.js.map +1 -0
  238. package/dist/voice/wave.js +116 -0
  239. package/dist/voice/wave.js.map +1 -0
  240. package/dist/voice/webgl.js +177 -0
  241. package/dist/voice/webgl.js.map +1 -0
  242. package/dist/widget.js +284 -8
  243. package/package.json +49 -39
  244. package/src/AgentAbout.tsx +1621 -0
  245. package/src/AgentCard.tsx +123 -0
  246. package/src/AgentChat.tsx +1812 -0
  247. package/src/AgentSpace.tsx +540 -0
  248. package/src/AgentTasks.tsx +163 -0
  249. package/src/BrowserView.tsx +442 -0
  250. package/src/FileView.tsx +620 -0
  251. package/src/Modes.tsx +112 -0
  252. package/src/VoiceCall.tsx +411 -0
  253. package/src/about.ts +355 -0
  254. package/src/agentIcons.ts +56 -0
  255. package/src/automation/Automation.tsx +308 -0
  256. package/src/automation/RunView.tsx +213 -0
  257. package/src/automation/cron.ts +196 -0
  258. package/src/automation/marks.tsx +144 -0
  259. package/src/automation/messages.ts +231 -0
  260. package/src/automation/record.ts +206 -0
  261. package/src/automation/types.ts +146 -0
  262. package/src/automation/when.ts +195 -0
  263. package/src/blocks.ts +249 -0
  264. package/src/browser.ts +334 -0
  265. package/src/call.ts +300 -0
  266. package/src/channelParts.ts +17 -0
  267. package/src/classes.ts +10 -0
  268. package/src/client.ts +397 -0
  269. package/src/conversationFiles.ts +54 -0
  270. package/src/credentials.ts +158 -0
  271. package/src/dev.ts +20 -0
  272. package/src/es.ts +423 -0
  273. package/src/files.ts +238 -0
  274. package/src/frames.ts +291 -0
  275. package/src/icons.tsx +49 -0
  276. package/src/index.ts +271 -0
  277. package/src/kit/AudioPlayer.tsx +129 -0
  278. package/src/kit/Button.tsx +97 -0
  279. package/src/kit/Card.tsx +47 -0
  280. package/src/kit/Chart.tsx +129 -0
  281. package/src/kit/Choices.tsx +131 -0
  282. package/src/kit/Combobox.tsx +247 -0
  283. package/src/kit/DateField.tsx +156 -0
  284. package/src/kit/Field.tsx +137 -0
  285. package/src/kit/FileField.tsx +492 -0
  286. package/src/kit/FileMark.tsx +56 -0
  287. package/src/kit/Inputs.tsx +250 -0
  288. package/src/kit/List.tsx +135 -0
  289. package/src/kit/Mark.tsx +64 -0
  290. package/src/kit/MarkStack.tsx +29 -0
  291. package/src/kit/Note.tsx +42 -0
  292. package/src/kit/Popover.tsx +121 -0
  293. package/src/kit/RangeField.tsx +53 -0
  294. package/src/kit/RatingField.tsx +62 -0
  295. package/src/kit/Stat.tsx +60 -0
  296. package/src/kit/Switch.tsx +28 -0
  297. package/src/kit/Tag.tsx +32 -0
  298. package/src/kit/Tile.tsx +74 -0
  299. package/src/kit/TimeField.tsx +157 -0
  300. package/src/kit/context.tsx +246 -0
  301. package/src/kit/dates.ts +150 -0
  302. package/src/kit/messages.ts +160 -0
  303. package/src/kit/numbers.ts +44 -0
  304. package/src/kit/options.ts +30 -0
  305. package/src/kit/uploads.ts +244 -0
  306. package/src/kit/waveform.ts +84 -0
  307. package/src/locale.ts +6 -0
  308. package/src/markdown.ts +69 -0
  309. package/src/marks.tsx +328 -0
  310. package/src/reveal.ts +52 -0
  311. package/src/stepGlyphs.ts +83 -0
  312. package/src/styles.css +6970 -0
  313. package/src/surface/Form.tsx +299 -0
  314. package/src/surface/Markdown.tsx +55 -0
  315. package/src/surface/Surface.tsx +605 -0
  316. package/src/surface/scope.ts +82 -0
  317. package/src/surface/values.ts +361 -0
  318. package/src/tint.ts +14 -0
  319. package/src/types.ts +604 -0
  320. package/src/useAgentFace.ts +47 -0
  321. package/src/useCall.ts +292 -0
  322. package/src/useConversation.ts +627 -0
  323. package/src/useDictation.ts +83 -0
  324. package/src/useLive.ts +115 -0
  325. package/src/usePolled.ts +60 -0
  326. package/src/voice/Orb.tsx +45 -0
  327. package/src/voice/StillOrb.tsx +29 -0
  328. package/src/voice/WebGLOrb.tsx +98 -0
  329. package/src/voice/fluidDots.tsx +171 -0
  330. package/src/voice/motion.ts +112 -0
  331. package/src/voice/orbs.ts +39 -0
  332. package/src/voice/useReducedMotion.ts +24 -0
  333. package/src/voice/wave.tsx +129 -0
  334. package/src/voice/webgl.ts +209 -0
  335. package/src/widget/css.d.ts +5 -0
  336. package/src/widget/element.tsx +535 -0
  337. package/src/widget/index.ts +10 -0
  338. package/src/widget/mount.ts +27 -0
  339. package/src/widget/widget.css +253 -0
  340. package/dist/SignalcoreProvider.d.ts +0 -113
  341. package/dist/SignalcoreProvider.js +0 -84
  342. package/dist/automations/AppMarks.d.ts +0 -11
  343. package/dist/automations/AppMarks.js +0 -18
  344. package/dist/automations/AutomationCard.d.ts +0 -27
  345. package/dist/automations/AutomationCard.js +0 -53
  346. package/dist/automations/AutomationsWindow.d.ts +0 -38
  347. package/dist/automations/AutomationsWindow.js +0 -57
  348. package/dist/automations/Card.d.ts +0 -46
  349. package/dist/automations/Card.js +0 -45
  350. package/dist/automations/ScheduleMarks.d.ts +0 -13
  351. package/dist/automations/ScheduleMarks.js +0 -24
  352. package/dist/automations/presentationIcons.d.ts +0 -7
  353. package/dist/automations/presentationIcons.js +0 -67
  354. package/dist/automations/schedule.d.ts +0 -25
  355. package/dist/automations/schedule.js +0 -59
  356. package/dist/automations/time.d.ts +0 -3
  357. package/dist/automations/time.js +0 -25
  358. package/dist/browser/BrowserView.d.ts +0 -43
  359. package/dist/browser/BrowserView.js +0 -94
  360. package/dist/browser/room.d.ts +0 -21
  361. package/dist/browser/room.js +0 -33
  362. package/dist/browser/useBrowserSession.d.ts +0 -17
  363. package/dist/browser/useBrowserSession.js +0 -67
  364. package/dist/builder/BuilderChat.d.ts +0 -32
  365. package/dist/builder/BuilderChat.js +0 -41
  366. package/dist/builder/BuilderTranscript.d.ts +0 -49
  367. package/dist/builder/BuilderTranscript.js +0 -62
  368. package/dist/builder/StatusLine.d.ts +0 -8
  369. package/dist/builder/StatusLine.js +0 -17
  370. package/dist/builder/answer.d.ts +0 -7
  371. package/dist/builder/answer.js +0 -8
  372. package/dist/builder/blocks.d.ts +0 -31
  373. package/dist/builder/blocks.js +0 -74
  374. package/dist/builder/items.d.ts +0 -15
  375. package/dist/builder/items.js +0 -21
  376. package/dist/builder/steps.d.ts +0 -20
  377. package/dist/builder/steps.js +0 -120
  378. package/dist/builder/time.d.ts +0 -4
  379. package/dist/builder/time.js +0 -24
  380. package/dist/builder/useBuildSession.d.ts +0 -36
  381. package/dist/builder/useBuildSession.js +0 -130
  382. package/dist/builder/workingOn.d.ts +0 -7
  383. package/dist/builder/workingOn.js +0 -15
  384. package/dist/chat/AgentChat.d.ts +0 -37
  385. package/dist/chat/AgentChat.js +0 -21
  386. package/dist/chat/ChatView.d.ts +0 -25
  387. package/dist/chat/ChatView.js +0 -34
  388. package/dist/chat/Composer.d.ts +0 -20
  389. package/dist/chat/Composer.js +0 -22
  390. package/dist/chat/MessageRow.d.ts +0 -22
  391. package/dist/chat/MessageRow.js +0 -32
  392. package/dist/chat/arrival.d.ts +0 -15
  393. package/dist/chat/arrival.js +0 -25
  394. package/dist/chat/history.d.ts +0 -24
  395. package/dist/chat/history.js +0 -45
  396. package/dist/chat/moments.d.ts +0 -25
  397. package/dist/chat/moments.js +0 -72
  398. package/dist/chat/types.d.ts +0 -12
  399. package/dist/chat/types.js +0 -1
  400. package/dist/chat/useComposer.d.ts +0 -46
  401. package/dist/chat/useComposer.js +0 -62
  402. package/dist/chat/useConversation.d.ts +0 -35
  403. package/dist/chat/useConversation.js +0 -71
  404. package/dist/chat/useStickToBottom.d.ts +0 -9
  405. package/dist/chat/useStickToBottom.js +0 -33
  406. package/dist/chat/useTurns.d.ts +0 -49
  407. package/dist/chat/useTurns.js +0 -111
  408. package/dist/classes.d.ts +0 -2
  409. package/dist/development.d.ts +0 -5
  410. package/dist/development.js +0 -8
  411. package/dist/flow/FlowWindow.d.ts +0 -42
  412. package/dist/flow/FlowWindow.js +0 -126
  413. package/dist/flow/index.d.ts +0 -16
  414. package/dist/flow/index.js +0 -16
  415. package/dist/flow/kinds.d.ts +0 -23
  416. package/dist/flow/kinds.js +0 -48
  417. package/dist/flow/map.d.ts +0 -44
  418. package/dist/flow/map.js +0 -225
  419. package/dist/flow/nodePaths.d.ts +0 -7
  420. package/dist/flow/nodePaths.js +0 -17
  421. package/dist/flow/relations.d.ts +0 -42
  422. package/dist/flow/relations.js +0 -174
  423. package/dist/host.d.ts +0 -124
  424. package/dist/host.js +0 -1
  425. package/dist/live/LiveView.d.ts +0 -28
  426. package/dist/live/LiveView.js +0 -55
  427. package/dist/live/capture.d.ts +0 -20
  428. package/dist/live/capture.js +0 -100
  429. package/dist/live/follow.d.ts +0 -26
  430. package/dist/live/follow.js +0 -72
  431. package/dist/live/paint.d.ts +0 -13
  432. package/dist/live/paint.js +0 -77
  433. package/dist/live/useLiveStream.d.ts +0 -16
  434. package/dist/live/useLiveStream.js +0 -57
  435. package/dist/look.d.ts +0 -33
  436. package/dist/look.js +0 -241
  437. package/dist/messages/en.d.ts +0 -3
  438. package/dist/messages/en.js +0 -251
  439. package/dist/messages/es.d.ts +0 -3
  440. package/dist/messages/es.js +0 -251
  441. package/dist/messages/index.d.ts +0 -16
  442. package/dist/messages/index.js +0 -33
  443. package/dist/messages/types.d.ts +0 -246
  444. package/dist/messages/types.js +0 -1
  445. package/dist/parts/Button.d.ts +0 -18
  446. package/dist/parts/Button.js +0 -17
  447. package/dist/parts/Icon.d.ts +0 -39
  448. package/dist/parts/Icon.js +0 -37
  449. package/dist/parts/Input.d.ts +0 -6
  450. package/dist/parts/Input.js +0 -11
  451. package/dist/parts/Spinner.d.ts +0 -2
  452. package/dist/parts/Spinner.js +0 -7
  453. package/dist/patterns.d.ts +0 -15
  454. package/dist/patterns.js +0 -5
  455. package/dist/presets.d.ts +0 -82
  456. package/dist/presets.js +0 -81
  457. package/dist/primitives.d.ts +0 -22
  458. package/dist/primitives.js +0 -58
  459. package/dist/request/RequestCard.d.ts +0 -34
  460. package/dist/request/RequestCard.js +0 -101
  461. package/dist/request/approval/ApprovalPreview.d.ts +0 -5
  462. package/dist/request/approval/ApprovalPreview.js +0 -27
  463. package/dist/request/approval/shapes.d.ts +0 -32
  464. package/dist/request/approval/shapes.js +0 -68
  465. package/dist/request/apps.d.ts +0 -11
  466. package/dist/request/apps.js +0 -27
  467. package/dist/request/cards.d.ts +0 -12
  468. package/dist/request/cards.js +0 -24
  469. package/dist/request/deliver.d.ts +0 -12
  470. package/dist/request/deliver.js +0 -21
  471. package/dist/request/handOver.d.ts +0 -18
  472. package/dist/request/handOver.js +0 -33
  473. package/dist/request/kinds/Approval.d.ts +0 -3
  474. package/dist/request/kinds/Approval.js +0 -9
  475. package/dist/request/kinds/Choice.d.ts +0 -3
  476. package/dist/request/kinds/Choice.js +0 -8
  477. package/dist/request/kinds/Connect.d.ts +0 -9
  478. package/dist/request/kinds/Connect.js +0 -119
  479. package/dist/request/kinds/FreeText.d.ts +0 -3
  480. package/dist/request/kinds/FreeText.js +0 -20
  481. package/dist/request/kinds/HandOver.d.ts +0 -13
  482. package/dist/request/kinds/HandOver.js +0 -20
  483. package/dist/request/kinds/Secret.d.ts +0 -7
  484. package/dist/request/kinds/Secret.js +0 -49
  485. package/dist/request/kinds/Upload.d.ts +0 -6
  486. package/dist/request/kinds/Upload.js +0 -47
  487. package/dist/request/types.d.ts +0 -83
  488. package/dist/request/types.js +0 -1
  489. package/dist/safeUrl.d.ts +0 -6
  490. package/dist/safeUrl.js +0 -8
  491. package/dist/stream/builder.d.ts +0 -176
  492. package/dist/stream/builder.js +0 -202
  493. package/dist/stream/reduce.d.ts +0 -51
  494. package/dist/stream/reduce.js +0 -58
  495. package/dist/stream/untilAborted.d.ts +0 -7
  496. package/dist/stream/untilAborted.js +0 -32
  497. package/dist/stream/useRunStream.d.ts +0 -26
  498. package/dist/stream/useRunStream.js +0 -137
  499. package/dist/styles.css +0 -2318
  500. package/dist/surface/HostNode.d.ts +0 -14
  501. package/dist/surface/HostNode.js +0 -30
  502. package/dist/surface/Node.d.ts +0 -7
  503. package/dist/surface/Node.js +0 -49
  504. package/dist/surface/SurfaceBoundary.d.ts +0 -26
  505. package/dist/surface/SurfaceBoundary.js +0 -20
  506. package/dist/surface/bindings.d.ts +0 -14
  507. package/dist/surface/bindings.js +0 -37
  508. package/dist/surface/check.d.ts +0 -27
  509. package/dist/surface/check.js +0 -102
  510. package/dist/surface/format.d.ts +0 -10
  511. package/dist/surface/format.js +0 -32
  512. package/dist/surface/hosts.d.ts +0 -6
  513. package/dist/surface/hosts.js +0 -11
  514. package/dist/surface/scene.d.ts +0 -21
  515. package/dist/surface/scene.js +0 -8
  516. package/dist/surface/templates/Browser.d.ts +0 -6
  517. package/dist/surface/templates/Browser.js +0 -11
  518. package/dist/surface/templates/Chart.d.ts +0 -6
  519. package/dist/surface/templates/Chart.js +0 -13
  520. package/dist/surface/templates/Choices.d.ts +0 -3
  521. package/dist/surface/templates/Choices.js +0 -12
  522. package/dist/surface/templates/Form.d.ts +0 -3
  523. package/dist/surface/templates/Form.js +0 -44
  524. package/dist/surface/templates/KeyValue.d.ts +0 -3
  525. package/dist/surface/templates/KeyValue.js +0 -7
  526. package/dist/surface/templates/Layout.d.ts +0 -5
  527. package/dist/surface/templates/Layout.js +0 -16
  528. package/dist/surface/templates/Live.d.ts +0 -6
  529. package/dist/surface/templates/Live.js +0 -9
  530. package/dist/surface/templates/Media.d.ts +0 -5
  531. package/dist/surface/templates/Media.js +0 -13
  532. package/dist/surface/templates/Slots.d.ts +0 -3
  533. package/dist/surface/templates/Slots.js +0 -19
  534. package/dist/surface/templates/Table.d.ts +0 -3
  535. package/dist/surface/templates/Table.js +0 -12
  536. package/dist/surface/templates/Text.d.ts +0 -4
  537. package/dist/surface/templates/Text.js +0 -15
  538. package/dist/surface/templates/index.d.ts +0 -7
  539. package/dist/surface/templates/index.js +0 -32
  540. package/dist/surface/types.d.ts +0 -68
  541. package/dist/surface/types.js +0 -6
  542. package/dist/theme.d.ts +0 -62
  543. package/dist/theme.js +0 -9
  544. package/dist/useTokens.d.ts +0 -7
  545. package/dist/useTokens.js +0 -41
  546. package/dist/voice/VoiceWindow.d.ts +0 -18
  547. package/dist/voice/VoiceWindow.js +0 -35
  548. package/dist/voice/index.d.ts +0 -13
  549. package/dist/voice/index.js +0 -12
  550. package/dist/voice/meter.d.ts +0 -16
  551. package/dist/voice/meter.js +0 -52
  552. package/dist/voice/orbColors.d.ts +0 -16
  553. package/dist/voice/orbColors.js +0 -75
  554. package/dist/voice/pushable.d.ts +0 -16
  555. package/dist/voice/pushable.js +0 -56
  556. package/dist/voice/useDictation.d.ts +0 -24
  557. package/dist/voice/useDictation.js +0 -60
  558. package/dist/voice/useReducedMotion.d.ts +0 -5
  559. package/dist/voice/useVoice.d.ts +0 -70
  560. package/dist/voice/useVoice.js +0 -197
  561. package/dist/voice/useVoicePrimitives.d.ts +0 -7
  562. package/dist/voice/useVoicePrimitives.js +0 -11
  563. package/dist/voice/voice.d.ts +0 -55
  564. package/dist/voice/voice.js +0 -9
  565. package/dist/voice/webSpeech.d.ts +0 -33
  566. package/dist/voice/webSpeech.js +0 -258
  567. package/dist/widget.js.map +0 -1
  568. package/dist/windows/AgentSpace.d.ts +0 -78
  569. package/dist/windows/AgentSpace.js +0 -135
  570. package/dist/windows/Captions.d.ts +0 -13
  571. package/dist/windows/Captions.js +0 -22
  572. package/dist/windows/Dock.d.ts +0 -37
  573. package/dist/windows/Dock.js +0 -22
  574. package/dist/windows/Journal.d.ts +0 -15
  575. package/dist/windows/Journal.js +0 -19
  576. package/dist/windows/KindMark.d.ts +0 -7
  577. package/dist/windows/KindMark.js +0 -31
  578. package/dist/windows/Pending.d.ts +0 -8
  579. package/dist/windows/Pending.js +0 -17
  580. package/dist/windows/Stage.d.ts +0 -18
  581. package/dist/windows/Stage.js +0 -21
  582. package/dist/windows/StatusLabel.d.ts +0 -9
  583. package/dist/windows/StatusLabel.js +0 -7
  584. package/dist/windows/Strip.d.ts +0 -17
  585. package/dist/windows/Strip.js +0 -42
  586. package/dist/windows/SurfaceWindow.d.ts +0 -20
  587. package/dist/windows/SurfaceWindow.js +0 -12
  588. package/dist/windows/WindowFrame.d.ts +0 -23
  589. package/dist/windows/WindowFrame.js +0 -22
  590. package/dist/windows/hearing.d.ts +0 -11
  591. package/dist/windows/hearing.js +0 -26
  592. package/dist/windows/kinds.d.ts +0 -47
  593. package/dist/windows/kinds.js +0 -28
  594. package/dist/windows/reduce.d.ts +0 -98
  595. package/dist/windows/reduce.js +0 -151
  596. package/dist/windows/registry.d.ts +0 -18
  597. package/dist/windows/registry.js +0 -61
  598. package/dist/windows/reveal.d.ts +0 -9
  599. package/dist/windows/reveal.js +0 -6
  600. package/dist/windows/space.d.ts +0 -21
  601. package/dist/windows/space.js +0 -6
  602. package/dist/windows/status.d.ts +0 -11
  603. package/dist/windows/status.js +0 -30
  604. package/dist/windows/types.d.ts +0 -15
  605. package/dist/windows/types.js +0 -1
  606. package/dist/windows/useLoaded.d.ts +0 -44
  607. package/dist/windows/useLoaded.js +0 -58
  608. package/dist/windows/useUnread.d.ts +0 -7
  609. package/dist/windows/useUnread.js +0 -21
  610. package/dist/windows/useWindows.d.ts +0 -70
  611. package/dist/windows/useWindows.js +0 -60
@@ -0,0 +1,1812 @@
1
+ import {
2
+ ArrowUpIcon,
3
+ CaretRightIcon,
4
+ CheckIcon,
5
+ ChecksIcon,
6
+ ClockIcon,
7
+ MicrophoneIcon,
8
+ PaperclipIcon,
9
+ ShieldWarningIcon,
10
+ StopIcon,
11
+ UploadSimpleIcon,
12
+ WarningCircleIcon,
13
+ } from "@phosphor-icons/react";
14
+ import {
15
+ Fragment,
16
+ useEffect,
17
+ useLayoutEffect,
18
+ useRef,
19
+ useState,
20
+ type KeyboardEvent,
21
+ type ReactNode,
22
+ type RefObject,
23
+ } from "react";
24
+
25
+ import { actionWords } from "./about";
26
+ import type { Surface as Shown } from "./blocks";
27
+ import {
28
+ BROWSER_MESSAGES,
29
+ BrowserView,
30
+ type BrowserMessages,
31
+ type BrowserPart,
32
+ } from "./BrowserView";
33
+ import { channelParts } from "./channelParts";
34
+ import { classes } from "./classes";
35
+ import { useDictation, type Dictate } from "./useDictation";
36
+ import { useReveal } from "./reveal";
37
+ import type { Client } from "./client";
38
+ import type { Speaker } from "./credentials";
39
+ import { useConversationFiles } from "./conversationFiles";
40
+ import { FILES_PER_MESSAGE } from "./frames";
41
+ import { actionMark } from "./marks";
42
+ import { AudioPlayer } from "./kit/AudioPlayer";
43
+ import { KitProvider, type KitPart } from "./kit/context";
44
+ import { carriesFiles, FileChips, UploadChips, useUploads, type Uploads } from "./kit/FileField";
45
+ import { KIT_MESSAGES, type KitLocale, type KitMessages } from "./kit/messages";
46
+ import type { FileHost } from "./kit/uploads";
47
+ import { stepGlyph } from "./stepGlyphs";
48
+ import { Markdown } from "./surface/Markdown";
49
+ import type { SurfaceComponents, SurfacePart } from "./surface/scope";
50
+ import { framesItself, Surface, usePlayed } from "./surface/Surface";
51
+ import type { SurfaceAction } from "./surface/values";
52
+ import type {
53
+ Action,
54
+ Attachment,
55
+ Decision,
56
+ Delivery,
57
+ Handover,
58
+ Holder,
59
+ Message,
60
+ Request,
61
+ Step,
62
+ StepApp,
63
+ StepState,
64
+ } from "./types";
65
+ import {
66
+ NO_TOOL,
67
+ useConversation,
68
+ type LiveConversation,
69
+ type ToolHandler,
70
+ } from "./useConversation";
71
+
72
+ /** The words the chat says; a host gives its own, in its language. */
73
+ export interface ChatMessages {
74
+ /** The composer's invitation, and the field's name; `fresh` while nothing has been said. */
75
+ placeholder: (name: string, fresh: boolean) => string;
76
+ /** In the composer while the socket is not open. */
77
+ connecting: string;
78
+ /** In the composer while the turn waits for the person to act above. */
79
+ awaiting: string;
80
+ /** Over the composer while a reply is on its way. */
81
+ thinking: (name: string) => string;
82
+ /** Over the composer while a reply is on its way, where the agent's steps are hidden. */
83
+ typing: string;
84
+ /** The builder's line over the composer, while nothing runs and nothing is written yet. */
85
+ working: string;
86
+ /** The builder's line over the composer, while its words come. */
87
+ writing: string;
88
+ /** How long the reply has taken so far, beside it. */
89
+ elapsed: (seconds: number) => string;
90
+ /** At the end of that line: how to stop the reply. */
91
+ escape: string;
92
+ /** Under the log once a reply is in: how long it took. */
93
+ done: (seconds: number) => string;
94
+ /** A conversation with nothing said yet: its title. */
95
+ empty: (name: string) => string;
96
+ /** Under the title, a line on what to say; nothing when empty. */
97
+ intro: (name: string) => string;
98
+ /** The send button's name. */
99
+ send: string;
100
+ /** The button that attaches files, by its name. */
101
+ attach: string;
102
+ /** Over the chat while files are dragged over it. */
103
+ drop: string;
104
+ /** The microphone's name: to dictate, and to stop and have it written. */
105
+ dictate: string;
106
+ dictateStop: string;
107
+ /** Beside the microphone while it listens, writes, or could not. */
108
+ dictation: Readonly<Record<"listening" | "writing" | "failed", string>>;
109
+ /** The stop button's name, in the send button's place while a reply is on its way. */
110
+ stop: string;
111
+ /** The message list's name, for screen readers. */
112
+ log: string;
113
+ /** A step's name, by the action's name as the platform gives it; the name itself otherwise. */
114
+ steps: Readonly<Record<string, string>>;
115
+ /** Beside a step that failed, in the builder's look: the colour is never the whole message. */
116
+ failed: string;
117
+ /** What heads the card of an action that needs the person's leave. */
118
+ approval: string;
119
+ /** The agent wants to run an action that needs the person's leave. */
120
+ request: (name: string, action: string) => string;
121
+ /** An action's input fields, by their name, as the approval card labels them. */
122
+ inputs: Readonly<Record<string, string>>;
123
+ allow: string;
124
+ decline: string;
125
+ /**
126
+ * On a person's page, in place of the approval card: what the agent asked to do waits for the
127
+ * business's yes (`decides: "business"`), which its people give from their app.
128
+ */
129
+ confirming: string;
130
+ /** The line across the log where the agent handed the conversation to a person. */
131
+ handedOff: string;
132
+ /**
133
+ * The line across the log where who answers the conversation changed (`handovers`), before
134
+ * why: to whom (`holder`), and who changed it (`by`).
135
+ */
136
+ handover: (holder: Holder, by: HandoverBy, name: string) => string;
137
+ /** Under a person's message the agent let pass this time (a `skip`), before why. */
138
+ skipped: string;
139
+ /**
140
+ * On a call, under a reply the person cut short: the last of what they heard of it (`""`:
141
+ * nothing of it).
142
+ */
143
+ cut: (heard: string) => string;
144
+ /** The composer's invitation when the host takes what is written (`write`): the business's. */
145
+ write: string;
146
+ /** Under what a person of the business wrote on the channel: who it was. */
147
+ business: string;
148
+ /** A voice note's player, by its name. */
149
+ voiceNote: string;
150
+ /** Where what went out on a channel got to, as its mark's name. */
151
+ delivery: Readonly<Record<Delivery, string>>;
152
+ /** Beside a message that never reached the person, with the provider's reason when it gave one. */
153
+ undelivered: (error: string | null) => string;
154
+ /**
155
+ * What the page answers when the agent calls one of the host's tools and the chat was given no
156
+ * `onTool` to run it: read by the agent, never shown.
157
+ */
158
+ noTool: string;
159
+ /** What the kit's controls say on the agent's screens: a form's checks, a calendar, a list. */
160
+ kit: Partial<KitMessages>;
161
+ /** What the browser the agent drives says: who holds it, take it, give it back. */
162
+ browser: BrowserMessages;
163
+ }
164
+
165
+ export const CHAT_MESSAGES: ChatMessages = {
166
+ placeholder: (name) => `Write to ${name}…`,
167
+ connecting: "Connecting…",
168
+ awaiting: "Answer above to go on…",
169
+ thinking: (name) => `${name} is thinking`,
170
+ typing: "Typing",
171
+ working: "Working",
172
+ writing: "Writing",
173
+ elapsed: (seconds) => `${seconds} s`,
174
+ escape: "esc to stop",
175
+ done: (seconds) => `Done in ${seconds} s`,
176
+ empty: (name) => `Talk with ${name}`,
177
+ intro: () => "",
178
+ send: "Send",
179
+ attach: "Attach files",
180
+ drop: "Drop to attach",
181
+ dictate: "Dictate",
182
+ dictateStop: "Stop and write it",
183
+ dictation: {
184
+ listening: "Listening…",
185
+ writing: "Writing it down…",
186
+ failed: "Couldn't hear you: allow the microphone and try again",
187
+ },
188
+ stop: "Stop",
189
+ log: "Conversation",
190
+ steps: { show: "Shows", ask_person: "Asks" },
191
+ failed: "failed",
192
+ approval: "Needs your approval",
193
+ request: (name, action) => `${name} wants to ${action}`,
194
+ inputs: {},
195
+ allow: "Allow",
196
+ decline: "Decline",
197
+ confirming: "Waiting for the business to confirm",
198
+ handedOff: "Handed to a person",
199
+ handover: (holder, by, name) => {
200
+ const automation = by.kind === "automation" ? ` by “${by.title}”` : "";
201
+ switch (holder) {
202
+ case "agent":
203
+ if (by.kind === "owner") return `You gave it back to ${name}`;
204
+ return `Given back to ${name}${automation}`;
205
+ case "person":
206
+ if (by.kind === "owner") return `You paused ${name}`;
207
+ if (by.kind === "agent") return `${name} handed it to a person`;
208
+ return `Handed to a person${automation}`;
209
+ case "nobody":
210
+ return `Ignored${automation}`;
211
+ }
212
+ },
213
+ skipped: "Not answered",
214
+ cut: (heard) =>
215
+ heard === "" ? "Cut off before a word of it was heard" : `Cut off after «${heard}»`,
216
+ write: "Reply as the business…",
217
+ business: "You",
218
+ voiceNote: "Voice note",
219
+ delivery: {
220
+ queued: "Waiting to go",
221
+ sent: "Sent",
222
+ delivered: "Delivered",
223
+ read: "Read",
224
+ failed: "Not delivered",
225
+ },
226
+ undelivered: (error) => (error ? `Not delivered: ${error}` : "Not delivered"),
227
+ noTool: NO_TOOL,
228
+ kit: KIT_MESSAGES,
229
+ browser: BROWSER_MESSAGES,
230
+ };
231
+
232
+ /** The chat's parts, by the class each carries; `classNames` adds a host's own under the same keys. */
233
+ export type ChatPart =
234
+ | "sc-chat"
235
+ | "sc-chat--builder"
236
+ | "sc-chat--console"
237
+ | "sc-chat__log"
238
+ | "sc-chat__log--channel"
239
+ | "sc-chat__message"
240
+ | "sc-chat__message--person"
241
+ | "sc-chat__message--agent"
242
+ | "sc-chat__message--action"
243
+ | "sc-chat__message--business"
244
+ | "sc-chat__said"
245
+ | "sc-chat__business"
246
+ | "sc-chat__meta"
247
+ | "sc-chat__delivery"
248
+ | "sc-chat__text"
249
+ | "sc-chat__steps"
250
+ | "sc-chat__step"
251
+ | "sc-chat__mark"
252
+ | "sc-chat__step--running"
253
+ | "sc-chat__step--queued"
254
+ | "sc-chat__step--waiting"
255
+ | "sc-chat__step--done"
256
+ | "sc-chat__step--failed"
257
+ | "sc-chat__output"
258
+ | "sc-chat__request"
259
+ | "sc-chat__confirming"
260
+ | "sc-chat__screen"
261
+ | "sc-chat__screen--bare"
262
+ | "sc-chat__handoff"
263
+ | "sc-chat__handover"
264
+ | "sc-chat__decision"
265
+ | "sc-chat__message--heard"
266
+ | "sc-chat__message--voice"
267
+ | "sc-chat__transcript"
268
+ | "sc-chat__cut"
269
+ | "sc-chat__reply"
270
+ | "sc-chat__status"
271
+ | "sc-chat__done"
272
+ | "sc-chat__empty"
273
+ | "sc-chat__suggestion"
274
+ | "sc-chat__composer"
275
+ | "sc-chat__files"
276
+ | "sc-chat__attach"
277
+ | "sc-chat__dictate"
278
+ | "sc-chat__typing"
279
+ | "sc-chat__dictation"
280
+ | "sc-chat__drop"
281
+ | "sc-chat__input"
282
+ | "sc-chat__actions"
283
+ | "sc-chat__send"
284
+ | "sc-chat__stop";
285
+
286
+ type ClassNames =
287
+ Partial<Record<ChatPart | SurfacePart | KitPart | BrowserPart, string>> | undefined;
288
+
289
+ /**
290
+ * How the chat looks. `agent` is an agent's conversation. `builder` is the builder's, v2's: the
291
+ * working line says what it does under the brand's sweeping light, the steps open on what they
292
+ * returned, the send is the brand's; `console` is the same seen as a console beside the work, the
293
+ * person's words as prompt lines.
294
+ */
295
+ export type ChatLook = "agent" | "builder" | "console";
296
+
297
+ /**
298
+ * Whether the chat shows the steps the agent took (searching its files, remembering, calling a
299
+ * tool, with what each returned): `shown` for the business, who wants to see the work; `hidden`
300
+ * for the people it serves, who see only what the agent says and shows, and a quiet line while
301
+ * it works.
302
+ */
303
+ export type ChatSteps = "shown" | "hidden";
304
+
305
+ /**
306
+ * Who changed who answers a conversation: one of the business's people (`owner`), the agent itself
307
+ * (its `escalate`), the platform (a release after quiet hours, a contact the owner marked), or an
308
+ * automation, by its title.
309
+ */
310
+ export type HandoverBy =
311
+ { kind: "owner" | "agent" | "platform" } | { kind: "automation"; title: string };
312
+
313
+ /** Something the host hands the chat from outside it, to say or to leave in the composer. */
314
+ export interface ChatCompose {
315
+ text: string;
316
+ /** Said at once; otherwise left in the composer for the person to finish. */
317
+ send: boolean;
318
+ /** Tells one request from the last: each new key is taken once. */
319
+ key: number;
320
+ }
321
+
322
+ export interface AgentChatProps {
323
+ client: Client;
324
+ conversationId: string;
325
+ /** The agent's name, for the words that address it. */
326
+ name: string;
327
+ messages?: Partial<ChatMessages> | undefined;
328
+ classNames?: ClassNames;
329
+ /** The host's own drawing of a block type, for every surface the agent shows. */
330
+ components?: SurfaceComponents | undefined;
331
+ /** The language of the surfaces' calendars, days, times and numbers; English by default. */
332
+ locale?: KitLocale | undefined;
333
+ /** An error's code, from the socket or `load` for a history that could not be read; the host words it. */
334
+ onError?: ((code: string) => void) | undefined;
335
+ /** The host's own controls, at the composer's left. */
336
+ actions?: ReactNode;
337
+ /** A few things the person might say first, offered while nothing has been said; a click says it. */
338
+ suggestions?: readonly string[] | undefined;
339
+ /**
340
+ * A request from elsewhere on the page (a button beside the chat), taken once each time `key`
341
+ * changes; the one the chat mounts with is not. With `send` it is said, once the socket is open;
342
+ * while a reply is on its way or the turn waits, it goes into the composer instead. Without, it
343
+ * goes into the composer, focused with the caret at its end.
344
+ */
345
+ compose?: ChatCompose | undefined;
346
+ /** An agent's conversation by default; the builder's, or the builder's as a console. */
347
+ look?: ChatLook | undefined;
348
+ /**
349
+ * Whether the agent's steps show: by default as the page speaks (`client.who`): `shown` for the
350
+ * business, `hidden` for a person or a visitor, who see what the agent says and shows (its
351
+ * surfaces, files, a yes it waits for) and `typing` while it works.
352
+ */
353
+ steps?: ChatSteps | undefined;
354
+ /**
355
+ * Whether the person can attach files to what they say (the paperclip, dropping them on the
356
+ * chat, pasting a picture): each uploads to the conversation at once. On in every look, the
357
+ * builder's too; a host turns it off where nobody talks (a transcript read).
358
+ */
359
+ attachments?: boolean | undefined;
360
+ /**
361
+ * Opens a file of the conversation (one the person sent, one sent through a form), as the host
362
+ * shows files (`FileView` in a dialog, its bytes from the API); without it, files only read.
363
+ */
364
+ onOpenFile?: ((file: Attachment) => void) | undefined;
365
+ /**
366
+ * Turns what the person says out loud into words in the composer (the microphone beside the
367
+ * paperclip): the host's speech to text. Without it there is no microphone.
368
+ */
369
+ dictate?: Dictate | undefined;
370
+ /**
371
+ * Where what is written goes instead of the socket: on a conversation on a channel, a person of
372
+ * the business writing to the person there (`POST /conversations/{id}/operator`). The composer
373
+ * then says `write`, takes words and files whatever the agent is doing, and empties once the
374
+ * promise resolves (a rejection leaves them, for the host to say why); what was written comes
375
+ * back on the socket as a message.
376
+ */
377
+ write?: ((text: string, files: readonly string[]) => Promise<unknown>) | undefined;
378
+ /**
379
+ * Whether the log draws who answers the conversation, for a host of the business's (the inbox,
380
+ * the activity): each change (read with the history, then heard live) a quiet line where it
381
+ * fell, with why and when, and under a person's message the agent let pass, that it did. Off,
382
+ * the log draws only where the agent handed it to a person, from its `escalate` step.
383
+ */
384
+ handovers?: boolean | undefined;
385
+ /** Hears each change of who answers the conversation, live: the host's own views of it follow. */
386
+ onHolder?: ((holder: Holder, handover: Handover) => void) | undefined;
387
+ /** Hears each step the agent starts, live, by its action's name: the host's views follow. */
388
+ onStep?: ((action: string) => void) | undefined;
389
+ /**
390
+ * The conversation's last change of who answers it, as the host knows it (one it made itself,
391
+ * as its request answered): drawn at once, whether or not the socket tells it, and kept once.
392
+ */
393
+ handover?: Handover | null | undefined;
394
+ /**
395
+ * Runs one of the host's own tools the agent calls in the page (opening a record in the host's
396
+ * screen, say): what it resolves to goes back to the agent, what it rejects with as why it
397
+ * failed; its step shows as working meanwhile. Without it, each is refused with `noTool`.
398
+ */
399
+ onTool?: ToolHandler | undefined;
400
+ }
401
+
402
+ /** How close to the foot, in pixels, still counts as being at it (v2's). */
403
+ const NEAR_FOOT = 80;
404
+
405
+ /**
406
+ * The conversation with an agent: the person's messages at the right in a bubble, the agent's at
407
+ * the left as text with the steps it took and what they drew, its reply growing as it streams;
408
+ * the composer at the foot. On a channel, the business's side stays at the left: the agent's
409
+ * words with where they got to (a quiet mark), and what a person of the business wrote set apart
410
+ * in a bubble of its own; the surfaces went to the person there, so they only read here, and the
411
+ * reply, which runs on its own time, can't be stopped from here. With `handovers`, each change of
412
+ * who answers it is a quiet line where it fell, the moment it is made, and a person's message the
413
+ * agent let pass says so under it. The host picks the conversation; another id starts the chat
414
+ * afresh.
415
+ */
416
+ export function AgentChat(props: AgentChatProps) {
417
+ // The last request taken, kept across conversations so another one does not take it again. The
418
+ // one given at mount counts as taken: a chat mounted again never says it twice.
419
+ const taken = useRef<number | null>(props.compose?.key ?? null);
420
+ // The kit's pieces in it (a file's chip, a surface's controls) read the host's words and classes.
421
+ const kit = props.messages?.kit ?? CHAT_MESSAGES.kit;
422
+ return (
423
+ <KitProvider classNames={props.classNames} messages={kit} locale={props.locale}>
424
+ <Chat key={props.conversationId} {...props} taken={taken} />
425
+ </KitProvider>
426
+ );
427
+ }
428
+
429
+ function Chat({
430
+ client,
431
+ conversationId,
432
+ name,
433
+ messages,
434
+ classNames,
435
+ components,
436
+ locale,
437
+ onError,
438
+ actions,
439
+ suggestions,
440
+ compose,
441
+ look = "agent",
442
+ steps = client.who === "person" ? "hidden" : "shown",
443
+ attachments = true,
444
+ onOpenFile,
445
+ dictate,
446
+ write,
447
+ handovers = false,
448
+ onHolder,
449
+ onStep,
450
+ handover,
451
+ onTool,
452
+ taken,
453
+ }: AgentChatProps & { taken: RefObject<number | null> }) {
454
+ const words = { ...CHAT_MESSAGES, ...messages };
455
+ const live = useConversation(client, conversationId, {
456
+ onError,
457
+ onHolder,
458
+ onStep,
459
+ handover,
460
+ onTool: onTool ?? (() => Promise.reject(new Error(words.noTool))),
461
+ });
462
+ const files = useConversationFiles(client, conversationId, live.messages, onOpenFile);
463
+ // What the person attaches to their next message: picked, dropped on the chat, or pasted.
464
+ const uploads = useUploads(attachments ? files : undefined, { max: FILES_PER_MESSAGE });
465
+ // Files go with what is said, once the turn waits on nothing; to the host's `write`, at once.
466
+ const attaching = attachments && (write !== undefined || live.awaiting === null);
467
+ const [over, setOver] = useState(false);
468
+ // Whether the messages follow the conversation down: until the person scrolls up to read, and
469
+ // again as soon as they speak.
470
+ const following = useRef(true);
471
+ const fresh = !live.loading && live.messages.length === 0 && !live.pending;
472
+ const took = useTook(live.pending);
473
+ const builder = look !== "agent";
474
+ const { pending } = live;
475
+ // A conversation on a channel: its messages say who said them.
476
+ const onChannel = live.messages.some((message) => message.author != null);
477
+
478
+ // The builder stops on Esc from anywhere on the page, as v2's did; an agent's chat, from inside it.
479
+ const stop = useRef(live.stop);
480
+ useEffect(() => {
481
+ stop.current = live.stop;
482
+ });
483
+ useEffect(() => {
484
+ if (!builder || !pending) return undefined;
485
+ const onKey = (event: globalThis.KeyboardEvent) => {
486
+ if (event.key === "Escape" && !event.defaultPrevented) stop.current();
487
+ };
488
+ window.addEventListener("keydown", onKey);
489
+ return () => {
490
+ window.removeEventListener("keydown", onKey);
491
+ };
492
+ }, [builder, pending]);
493
+
494
+ const looks: ChatPart[] = builder ? ["sc-chat--builder"] : [];
495
+ if (look === "console") looks.push("sc-chat--console");
496
+ const stepsShown = steps === "shown";
497
+
498
+ return (
499
+ <div
500
+ part="chat"
501
+ className={`sc ${classes(classNames, "sc-chat", ...looks)}`}
502
+ onKeyDown={(event) => {
503
+ if (!builder && !onChannel && event.key === "Escape" && live.pending) live.stop();
504
+ }}
505
+ // Files dropped anywhere on the chat go with the next message; a form's own drop zone
506
+ // (`data-drop`) takes those dropped on it.
507
+ onDragOver={(event) => {
508
+ if (!attaching || !carriesFiles(event)) return;
509
+ const zone = event.target instanceof Element && event.target.closest("[data-drop]");
510
+ setOver(!zone);
511
+ if (zone) return;
512
+ event.preventDefault();
513
+ event.dataTransfer.dropEffect = "copy";
514
+ }}
515
+ onDragLeave={(event) => {
516
+ const to = event.relatedTarget;
517
+ if (!(to instanceof Node && event.currentTarget.contains(to))) setOver(false);
518
+ }}
519
+ onDrop={(event) => {
520
+ setOver(false);
521
+ if (event.defaultPrevented || !attaching || !carriesFiles(event)) return;
522
+ event.preventDefault();
523
+ uploads.add([...event.dataTransfer.files]);
524
+ }}
525
+ >
526
+ {fresh ? (
527
+ <div part="empty" className={classes(classNames, "sc-chat__empty")}>
528
+ <h2>{words.empty(name)}</h2>
529
+ {words.intro(name) !== "" && <p>{words.intro(name)}</p>}
530
+ {suggestions !== undefined && suggestions.length > 0 && (
531
+ <ul>
532
+ {suggestions.map((text) => (
533
+ <li key={text}>
534
+ <button
535
+ type="button"
536
+ disabled={!live.open}
537
+ onClick={() => {
538
+ if (live.say(text)) following.current = true;
539
+ }}
540
+ part="suggestion"
541
+ className={classes(classNames, "sc-chat__suggestion")}
542
+ >
543
+ «{text}»
544
+ </button>
545
+ </li>
546
+ ))}
547
+ </ul>
548
+ )}
549
+ </div>
550
+ ) : (
551
+ <Log
552
+ live={live}
553
+ name={name}
554
+ words={words}
555
+ classNames={classNames}
556
+ components={components}
557
+ locale={locale}
558
+ following={following}
559
+ took={took}
560
+ builder={builder}
561
+ onChannel={onChannel}
562
+ files={files}
563
+ handovers={handovers ? live.handovers : undefined}
564
+ who={client.who}
565
+ steps={stepsShown}
566
+ />
567
+ )}
568
+ {/* The browser the agent drives, over the composer while the conversation has one. */}
569
+ <BrowserView live={live} name={name} messages={words.browser} classNames={classNames} />
570
+ {live.pending && (
571
+ <Status
572
+ label={
573
+ builder ? workingOn(live, words) : stepsShown ? words.thinking(name) : words.typing
574
+ }
575
+ stoppable={!onChannel}
576
+ timed={stepsShown}
577
+ words={words}
578
+ classNames={classNames}
579
+ />
580
+ )}
581
+ <Composer
582
+ live={live}
583
+ name={name}
584
+ words={words}
585
+ classNames={classNames}
586
+ actions={actions}
587
+ compose={compose}
588
+ taken={taken}
589
+ fresh={fresh}
590
+ builder={builder}
591
+ uploads={attachments ? uploads : undefined}
592
+ files={files}
593
+ dictate={dictate}
594
+ write={write}
595
+ onSent={() => {
596
+ following.current = true;
597
+ }}
598
+ />
599
+ {over && (
600
+ <div aria-hidden className={classes(classNames, "sc-chat__drop")}>
601
+ <UploadSimpleIcon weight="bold" />
602
+ {words.drop}
603
+ </div>
604
+ )}
605
+ </div>
606
+ );
607
+ }
608
+
609
+ /**
610
+ * What the builder is doing right now, v2's way: the step running in this turn, by its name (the
611
+ * first one not settled: the actions of a reply run in its order); its words, while they come; or
612
+ * that it is working.
613
+ */
614
+ export function workingOn(
615
+ live: Pick<LiveConversation, "messages" | "draft">,
616
+ words: Pick<ChatMessages, "steps" | "working" | "writing">,
617
+ ): string {
618
+ const lastSaid = live.messages.findLastIndex((message) => message.role === "user");
619
+ const running = live.messages
620
+ .slice(lastSaid + 1)
621
+ .flatMap((message) => message.steps)
622
+ .find((step) => step.state === "running");
623
+ if (running) return words.steps[running.action] ?? running.action;
624
+ return live.draft === "" ? words.working : words.writing;
625
+ }
626
+
627
+ interface PartProps {
628
+ live: LiveConversation;
629
+ name: string;
630
+ words: ChatMessages;
631
+ classNames: ClassNames;
632
+ }
633
+
634
+ const NO_HANDOFFS: ReadonlySet<string> = new Set();
635
+
636
+ /** Whether a message is the agent's own words: a reply, not what a person of the business wrote. */
637
+ const agentSaid = (message: Message | undefined): boolean =>
638
+ message?.role === "assistant" && message.author !== "operator";
639
+
640
+ /**
641
+ * The replies a handoff line follows: in each turn where the agent handed the conversation to a
642
+ * person (an `escalate` step done), its last reply, so the line closes what the agent said.
643
+ */
644
+ export function handoffs(messages: readonly Message[]): ReadonlySet<string> {
645
+ const after = new Set<string>();
646
+ let handed = false;
647
+ for (const [index, message] of messages.entries()) {
648
+ handed ||= message.steps.some((step) => step.action === "escalate" && step.state === "done");
649
+ if (handed && !agentSaid(messages[index + 1])) {
650
+ after.add(message.id);
651
+ handed = false;
652
+ }
653
+ }
654
+ return after;
655
+ }
656
+
657
+ /** Who changed who answers, from a handover's `by`: anything but these three is an automation's name. */
658
+ export function handoverBy({ by, title }: Pick<Handover, "by" | "title">): HandoverBy {
659
+ if (by === "owner" || by === "agent" || by === "platform") return { kind: by };
660
+ return { kind: "automation", title: title ?? by };
661
+ }
662
+
663
+ /** A change of who answers, as its line says it: what changed and who changed it, then why. */
664
+ export function handoverLine(
665
+ handover: Handover,
666
+ name: string,
667
+ words: Pick<ChatMessages, "handover">,
668
+ ): string {
669
+ const line = words.handover(handover.holder, handoverBy(handover), name);
670
+ const reason = handover.reason.trim();
671
+ return reason === "" ? line : `${line}: ${reason}`;
672
+ }
673
+
674
+ /**
675
+ * The line under a person's message the agent let pass this time, with why (the owner's words,
676
+ * else the automation that decided); none for any other decision: an answer is the reply itself,
677
+ * and a conversation held or ignored has its line where who answers changed.
678
+ */
679
+ export function decisionLine(
680
+ decision: Decision | null | undefined,
681
+ words: Pick<ChatMessages, "skipped">,
682
+ ): string | null {
683
+ if (decision?.kind !== "skip") return null;
684
+ const why = decision.reason?.trim() || decision.title;
685
+ return why ? `${words.skipped}: ${why}` : words.skipped;
686
+ }
687
+
688
+ /**
689
+ * Where each change of who answers falls among the messages, by the index of the message it goes
690
+ * before (`messages.length`: after them all): before the first message said after it, and the
691
+ * agent's own after the last reply of its turn, so its line closes what the agent said.
692
+ */
693
+ export function handoversAt(
694
+ messages: readonly Message[],
695
+ handovers: readonly Handover[],
696
+ ): ReadonlyMap<number, readonly Handover[]> {
697
+ const at = new Map<number, Handover[]>();
698
+ let next = 0;
699
+ for (const [index, message] of messages.entries()) {
700
+ const said = Date.parse(message.created_at);
701
+ const before: Handover[] = [];
702
+ let handover = handovers[next];
703
+ while (
704
+ handover !== undefined &&
705
+ Date.parse(handover.at) < said &&
706
+ !(handover.by === "agent" && agentSaid(message))
707
+ ) {
708
+ before.push(handover);
709
+ next += 1;
710
+ handover = handovers[next];
711
+ }
712
+ if (before.length > 0) at.set(index, before);
713
+ }
714
+ if (next < handovers.length) at.set(messages.length, handovers.slice(next));
715
+ return at;
716
+ }
717
+
718
+ /** When a line across the log happened, as the locale writes it: the time today, else the day too. */
719
+ export function whenSaid(at: string, now: Date, code: string): string {
720
+ const moment = new Date(at);
721
+ const today = moment.toDateString() === now.toDateString();
722
+ return new Intl.DateTimeFormat(code, {
723
+ ...(today ? {} : { day: "numeric", month: "short" }),
724
+ hour: "numeric",
725
+ minute: "2-digit",
726
+ }).format(moment);
727
+ }
728
+
729
+ /**
730
+ * The messages, read down, and the reply on its way: who is thinking until its first words, then
731
+ * the words as they come with the agent's mark at their end. Where who answers the conversation
732
+ * changed (`handovers`, when the host draws them; else where the agent handed it to a person),
733
+ * one quiet line across the log says so, and under a person's message the agent let pass, that
734
+ * it did. Screen readers hear a reply once, when it is stored.
735
+ */
736
+ function Log({
737
+ live,
738
+ name,
739
+ words,
740
+ classNames,
741
+ components,
742
+ locale,
743
+ following,
744
+ took,
745
+ builder,
746
+ onChannel,
747
+ files,
748
+ handovers,
749
+ who,
750
+ steps,
751
+ }: PartProps & {
752
+ components: SurfaceComponents | undefined;
753
+ locale: KitLocale | undefined;
754
+ following: RefObject<boolean>;
755
+ took: number | null;
756
+ builder: boolean;
757
+ onChannel: boolean;
758
+ files: FileHost;
759
+ handovers: readonly Handover[] | undefined;
760
+ who: Speaker;
761
+ /** Whether the agent's steps show, and how long its last reply took. */
762
+ steps: boolean;
763
+ }) {
764
+ const scroller = useRef<HTMLDivElement>(null);
765
+ const said = useRef<HTMLDivElement>(null);
766
+ const { messages, pending, draft, awaiting } = live;
767
+ // The reply as it streams, at a reading pace (`reveal.ts`); when it is stored as a message, the
768
+ // message goes on from where the reveal was, so the words never jump to the end.
769
+ const shown = useRef(0);
770
+ // On a channel the reply being written shows as typing until it is stored; then it shows as
771
+ // the person gets it (its own typing while its parts go).
772
+ const last = messages.at(-1);
773
+ const outgoing =
774
+ last !== undefined &&
775
+ last.role === "assistant" &&
776
+ last.delivery != null &&
777
+ last.delivery !== "failed";
778
+ const drafted = useReveal(draft, 0, (count) => {
779
+ shown.current = count;
780
+ });
781
+ const lastDraft = useRef("");
782
+ const carried = useRef<{ id: string; from: number } | null>(null);
783
+ const latest = messages.at(-1);
784
+ if (
785
+ latest?.role === "assistant" &&
786
+ lastDraft.current !== "" &&
787
+ draft === "" &&
788
+ carried.current?.id !== latest.id
789
+ ) {
790
+ carried.current = { id: latest.id, from: shown.current };
791
+ }
792
+ lastDraft.current = draft;
793
+ useLayoutEffect(() => {
794
+ const box = scroller.current;
795
+ if (box && following.current) box.scrollTop = box.scrollHeight;
796
+ }, [messages, pending, drafted, following]);
797
+ // What grows by itself (a surface's picture landing, a step opened on its output) keeps the
798
+ // foot in view too, while the person is at it; and so does less room for it (the browser
799
+ // opening large under it).
800
+ useEffect(() => {
801
+ const box = scroller.current;
802
+ const log = said.current;
803
+ if (!box || !log || typeof ResizeObserver === "undefined") return undefined;
804
+ const observer = new ResizeObserver(() => {
805
+ if (following.current) box.scrollTop = box.scrollHeight;
806
+ });
807
+ observer.observe(log);
808
+ observer.observe(box);
809
+ return () => {
810
+ observer.disconnect();
811
+ };
812
+ }, [following]);
813
+
814
+ // A surface takes input while the turn waits on it or, once nothing runs or waits, when the
815
+ // agent showed it since the person last spoke; older ones are read, not used. On a channel the
816
+ // person answered it there: here it only reads.
817
+ const idle = !onChannel && !pending && awaiting === null;
818
+ const lastSaid = messages.findLastIndex((message) => message.role === "user");
819
+ // What the person did on each surface, by the surface's id: a surface draws its answer.
820
+ const answers = new Map<string, Action>();
821
+ for (const message of messages) {
822
+ if (message.action) answers.set(message.action.surface, message.action);
823
+ }
824
+ // Drawn, who answers says where the agent handed it over; otherwise its `escalate` step does.
825
+ const handed = handovers === undefined ? handoffs(messages) : NO_HANDOFFS;
826
+ const placed = handoversAt(messages, handovers ?? []);
827
+ const lines = (index: number) =>
828
+ placed
829
+ .get(index)
830
+ ?.map((handover) => (
831
+ <HandoverLine
832
+ key={`${handover.at} ${handover.holder}`}
833
+ handover={handover}
834
+ name={name}
835
+ words={words}
836
+ classNames={classNames}
837
+ code={locale?.code ?? "en-US"}
838
+ />
839
+ ));
840
+
841
+ return (
842
+ <div
843
+ ref={scroller}
844
+ onScroll={(event) => {
845
+ const box = event.currentTarget;
846
+ following.current = box.scrollHeight - box.scrollTop - box.clientHeight < NEAR_FOOT;
847
+ }}
848
+ part="transcript"
849
+ className={classes(
850
+ classNames,
851
+ "sc-chat__log",
852
+ ...(onChannel ? (["sc-chat__log--channel"] as const) : []),
853
+ )}
854
+ >
855
+ <div ref={said} role="log" aria-label={words.log}>
856
+ {messages.map((message, index) => (
857
+ <Fragment key={message.id}>
858
+ {lines(index)}
859
+ {message.role === "user" ? (
860
+ <>
861
+ <Heard
862
+ message={message}
863
+ after={messages[index - 1]}
864
+ words={words}
865
+ classNames={classNames}
866
+ />
867
+ <Said message={message} words={words} classNames={classNames} files={files} />
868
+ {handovers !== undefined && (
869
+ <Decided decision={message.decision} words={words} classNames={classNames} />
870
+ )}
871
+ </>
872
+ ) : message.author === "operator" ? (
873
+ <Business message={message} words={words} classNames={classNames} files={files} />
874
+ ) : (
875
+ <>
876
+ <Reply
877
+ message={message}
878
+ from={carried.current?.id === message.id ? carried.current.from : undefined}
879
+ awaiting={onChannel ? null : awaiting}
880
+ current={idle && index > lastSaid}
881
+ answers={answers}
882
+ name={name}
883
+ words={words}
884
+ classNames={classNames}
885
+ components={components}
886
+ locale={locale}
887
+ onAction={live.act}
888
+ builder={builder}
889
+ files={files}
890
+ who={who}
891
+ steps={steps}
892
+ onChannel={onChannel}
893
+ />
894
+ {handed.has(message.id) && (
895
+ <p className={classes(classNames, "sc-chat__handoff")}>
896
+ {actionMark("escalate", "library")}
897
+ <span>{words.handedOff}</span>
898
+ </p>
899
+ )}
900
+ </>
901
+ )}
902
+ </Fragment>
903
+ ))}
904
+ {lines(messages.length)}
905
+ </div>
906
+ {steps && !pending && took !== null && (
907
+ <p className={classes(classNames, "sc-chat__done")}>{words.done(took)}</p>
908
+ )}
909
+ {pending && onChannel && !outgoing && <Typing label={words.typing} classNames={classNames} />}
910
+ {pending && !onChannel && drafted !== "" && (
911
+ <div
912
+ aria-hidden
913
+ part="message message-agent"
914
+ className={classes(classNames, "sc-chat__text", "sc-chat__reply")}
915
+ >
916
+ <Markdown text={drafted} />
917
+ </div>
918
+ )}
919
+ </div>
920
+ );
921
+ }
922
+
923
+ /** The channel's "typing": three dots in the business's bubble, while a reply or its parts come. */
924
+ function Typing({ label, classNames }: { label: string; classNames: ClassNames }) {
925
+ return (
926
+ <p role="status" aria-label={label} className={classes(classNames, "sc-chat__typing")}>
927
+ <span />
928
+ <span />
929
+ <span />
930
+ </p>
931
+ );
932
+ }
933
+
934
+ /** How many of the last words heard a cut line quotes. */
935
+ const HEARD_WORDS = 8;
936
+
937
+ /** The last words of what was heard, with an ellipsis before them when there was more. */
938
+ export function heardTail(heard: string, count = HEARD_WORDS): string {
939
+ const words = heard.trim().split(/\s+/).filter(Boolean);
940
+ if (words.length <= count) return words.join(" ");
941
+ return `…${words.slice(-count).join(" ")}`;
942
+ }
943
+
944
+ /**
945
+ * On a call, what the person heard of the agent's last words before this message, when it wasn't
946
+ * what was stored. After a reply (the greeting is the first), they cut it short: a quiet line
947
+ * under it quotes the last of what they heard. With no reply before, it was a greeting from before
948
+ * the API kept it: drawn here as the agent's words, where it was said.
949
+ */
950
+ function Heard({
951
+ message,
952
+ after,
953
+ words,
954
+ classNames,
955
+ }: {
956
+ message: Message;
957
+ after: Message | undefined;
958
+ words: ChatMessages;
959
+ classNames: ClassNames;
960
+ }) {
961
+ const { heard } = message;
962
+ if (heard == null) return null;
963
+ if (after?.role === "assistant") {
964
+ return <p className={classes(classNames, "sc-chat__cut")}>{words.cut(heardTail(heard))}</p>;
965
+ }
966
+ if (heard.trim() === "") return null;
967
+ return (
968
+ <div
969
+ part="message message-agent"
970
+ className={classes(
971
+ classNames,
972
+ "sc-chat__message",
973
+ "sc-chat__message--agent",
974
+ "sc-chat__message--heard",
975
+ )}
976
+ >
977
+ <div className={classes(classNames, "sc-chat__text")}>
978
+ <p>{heard}</p>
979
+ </div>
980
+ </div>
981
+ );
982
+ }
983
+
984
+ /**
985
+ * The person's message: their words in a bubble (a voice note's player with its words under it);
986
+ * the files they sent with it as chips over it, each opening where the host shows files. An
987
+ * answer on a surface is its words alone: the surface shows its files.
988
+ */
989
+ function Said({
990
+ message,
991
+ words,
992
+ classNames,
993
+ files,
994
+ }: {
995
+ message: Message;
996
+ words: ChatMessages;
997
+ classNames: ClassNames;
998
+ files: FileHost;
999
+ }) {
1000
+ const sent = message.action ? [] : (message.files ?? []);
1001
+ const bubble = message.voice ? (
1002
+ <VoiceNote
1003
+ file={message.voice}
1004
+ text={message.text}
1005
+ parts="message message-person"
1006
+ kind="sc-chat__message--person"
1007
+ words={words}
1008
+ classNames={classNames}
1009
+ files={files}
1010
+ />
1011
+ ) : (
1012
+ message.text !== "" && (
1013
+ <p
1014
+ part="message message-person"
1015
+ className={classes(
1016
+ classNames,
1017
+ "sc-chat__message",
1018
+ "sc-chat__message--person",
1019
+ ...(message.action ? (["sc-chat__message--action"] as const) : []),
1020
+ )}
1021
+ >
1022
+ {message.text}
1023
+ </p>
1024
+ )
1025
+ );
1026
+ if (sent.length === 0) return bubble;
1027
+ return (
1028
+ <div part="files" className={classes(classNames, "sc-chat__said")}>
1029
+ <FileChips files={sent} onOpen={files.open} />
1030
+ {bubble}
1031
+ </div>
1032
+ );
1033
+ }
1034
+
1035
+ /**
1036
+ * A voice note on a channel, either way: a bubble with the page's own player, streaming from where
1037
+ * the file plays (asked again, once, when that ran out), and what was said under it.
1038
+ */
1039
+ function VoiceNote({
1040
+ file,
1041
+ text,
1042
+ parts,
1043
+ kind,
1044
+ words,
1045
+ classNames,
1046
+ files,
1047
+ }: {
1048
+ file: Attachment;
1049
+ text: string;
1050
+ /** The bubble's parts, a host's to style. */
1051
+ parts: string;
1052
+ kind: "sc-chat__message--person" | "sc-chat__text";
1053
+ words: ChatMessages;
1054
+ classNames: ClassNames;
1055
+ files: FileHost;
1056
+ }) {
1057
+ const played = usePlayed(file, files.play);
1058
+ const lost = useRef(false);
1059
+ return (
1060
+ <div
1061
+ part={parts}
1062
+ className={classes(
1063
+ classNames,
1064
+ ...(kind === "sc-chat__text" ? [] : (["sc-chat__message"] as const)),
1065
+ kind,
1066
+ "sc-chat__message--voice",
1067
+ )}
1068
+ >
1069
+ <AudioPlayer
1070
+ src={played?.url}
1071
+ label={words.voiceNote}
1072
+ onError={() => {
1073
+ if (lost.current || played === null) return;
1074
+ lost.current = true;
1075
+ played.again();
1076
+ }}
1077
+ />
1078
+ {text !== "" && (
1079
+ <div className={classes(classNames, "sc-chat__transcript")}>
1080
+ <Markdown text={text} />
1081
+ </div>
1082
+ )}
1083
+ </div>
1084
+ );
1085
+ }
1086
+
1087
+ /** Under a person's message the agent let pass this time: that it didn't answer it, and why. */
1088
+ function Decided({
1089
+ decision,
1090
+ words,
1091
+ classNames,
1092
+ }: {
1093
+ decision: Decision | null | undefined;
1094
+ words: ChatMessages;
1095
+ classNames: ClassNames;
1096
+ }) {
1097
+ const line = decisionLine(decision, words);
1098
+ if (line === null) return null;
1099
+ return <p className={classes(classNames, "sc-chat__decision")}>{line}</p>;
1100
+ }
1101
+
1102
+ /**
1103
+ * Where who answers the conversation changed: a quiet line across the log, what changed and who
1104
+ * changed it, why, and when.
1105
+ */
1106
+ function HandoverLine({
1107
+ handover,
1108
+ name,
1109
+ words,
1110
+ classNames,
1111
+ code,
1112
+ }: {
1113
+ handover: Handover;
1114
+ name: string;
1115
+ words: ChatMessages;
1116
+ classNames: ClassNames;
1117
+ /** The locale's, for when. */
1118
+ code: string;
1119
+ }) {
1120
+ return (
1121
+ <p className={classes(classNames, "sc-chat__handover")}>
1122
+ <span>
1123
+ {handoverLine(handover, name, words)}
1124
+ <time dateTime={handover.at}>{whenSaid(handover.at, new Date(), code)}</time>
1125
+ </span>
1126
+ </p>
1127
+ );
1128
+ }
1129
+
1130
+ /**
1131
+ * What a person of the business wrote on the channel, on the business's side but set apart from
1132
+ * the agent's words: a bubble of its own, the files it carried over it, and under it who wrote it
1133
+ * and where it got to.
1134
+ */
1135
+ function Business({
1136
+ message,
1137
+ words,
1138
+ classNames,
1139
+ files,
1140
+ }: {
1141
+ message: Message;
1142
+ words: ChatMessages;
1143
+ classNames: ClassNames;
1144
+ files: FileHost;
1145
+ }) {
1146
+ const sent = message.files ?? [];
1147
+ return (
1148
+ <div className={classes(classNames, "sc-chat__business")}>
1149
+ {sent.length > 0 && <FileChips files={sent} onOpen={files.open} />}
1150
+ {message.text !== "" && (
1151
+ <p
1152
+ part="message message-business"
1153
+ className={classes(classNames, "sc-chat__message", "sc-chat__message--business")}
1154
+ >
1155
+ {message.text}
1156
+ </p>
1157
+ )}
1158
+ <p className={classes(classNames, "sc-chat__meta")}>
1159
+ <span>{words.business}</span>
1160
+ <DeliveryMark message={message} words={words} classNames={classNames} />
1161
+ </p>
1162
+ </div>
1163
+ );
1164
+ }
1165
+
1166
+ const DELIVERY_MARKS: Readonly<Record<Delivery, ReactNode>> = {
1167
+ queued: <ClockIcon aria-hidden />,
1168
+ sent: <CheckIcon aria-hidden weight="bold" />,
1169
+ delivered: <ChecksIcon aria-hidden weight="bold" />,
1170
+ read: <ChecksIcon aria-hidden weight="bold" />,
1171
+ failed: <WarningCircleIcon aria-hidden weight="fill" />,
1172
+ };
1173
+
1174
+ /**
1175
+ * Where a message that went out on a channel got to, quietly: a clock while it waits to go, a
1176
+ * check once sent, two once delivered, two in the accent once read; one that never got there in
1177
+ * the danger tone, with why. Nothing for a message that didn't go out.
1178
+ */
1179
+ function DeliveryMark({
1180
+ message,
1181
+ words,
1182
+ classNames,
1183
+ }: {
1184
+ message: Message;
1185
+ words: ChatMessages;
1186
+ classNames: ClassNames;
1187
+ }) {
1188
+ const { delivery } = message;
1189
+ if (delivery == null) return null;
1190
+ const failed = delivery === "failed";
1191
+ return (
1192
+ <span
1193
+ role={failed ? undefined : "img"}
1194
+ aria-label={failed ? undefined : words.delivery[delivery]}
1195
+ title={failed ? undefined : words.delivery[delivery]}
1196
+ data-delivery={delivery}
1197
+ className={classes(classNames, "sc-chat__delivery")}
1198
+ >
1199
+ {DELIVERY_MARKS[delivery]}
1200
+ {failed && <span>{words.undelivered(message.delivery_error ?? null)}</span>}
1201
+ </span>
1202
+ );
1203
+ }
1204
+
1205
+ interface DrawProps {
1206
+ words: ChatMessages;
1207
+ classNames: ClassNames;
1208
+ components: SurfaceComponents | undefined;
1209
+ locale: KitLocale | undefined;
1210
+ onAction: (action: Action) => void;
1211
+ }
1212
+
1213
+ /**
1214
+ * The agent's message: its words, then the steps it took as a compact list (a mark for the
1215
+ * state, the action's name, its output), then what each step drew (with what the person did on
1216
+ * it, from `answers`), and the card for one that waits to be allowed by whoever this page speaks
1217
+ * for (on a person's page, one the business allows is a quiet line instead); on a channel, where
1218
+ * it got to.
1219
+ */
1220
+ function Reply({
1221
+ message,
1222
+ from,
1223
+ awaiting,
1224
+ current,
1225
+ answers,
1226
+ name,
1227
+ words,
1228
+ classNames,
1229
+ components,
1230
+ locale,
1231
+ onAction,
1232
+ builder,
1233
+ files,
1234
+ who,
1235
+ steps,
1236
+ onChannel,
1237
+ }: DrawProps & {
1238
+ message: Message;
1239
+ /** It was streaming: its words go on revealing from here. None: they show whole. */
1240
+ from: number | undefined;
1241
+ awaiting: string | null;
1242
+ current: boolean;
1243
+ answers: ReadonlyMap<string, Action>;
1244
+ name: string;
1245
+ builder: boolean;
1246
+ files: FileHost;
1247
+ who: Speaker;
1248
+ /** Whether the steps it took show, as a list under its words. */
1249
+ steps: boolean;
1250
+ /** On a channel: its words as the person got them, a bubble for each part, and who sent them. */
1251
+ onChannel: boolean;
1252
+ }) {
1253
+ const text = useReveal(message.text, from ?? message.text.length);
1254
+ // On a channel, what the person there has got so far; elsewhere, the words as they come.
1255
+ const { shown: said, typing } = onChannel
1256
+ ? channelParts(message, text)
1257
+ : { shown: [text], typing: false };
1258
+ return (
1259
+ <div
1260
+ part="message message-agent"
1261
+ className={classes(classNames, "sc-chat__message", "sc-chat__message--agent")}
1262
+ >
1263
+ {/* A voice note shows once it went, as each part does. */}
1264
+ {message.voice && said.length > 0 ? (
1265
+ <VoiceNote
1266
+ file={message.voice}
1267
+ text={text}
1268
+ parts="text"
1269
+ kind="sc-chat__text"
1270
+ words={words}
1271
+ classNames={classNames}
1272
+ files={files}
1273
+ />
1274
+ ) : (
1275
+ text !== "" &&
1276
+ said.map((words, index) => (
1277
+ <div key={index} part="text" className={classes(classNames, "sc-chat__text")}>
1278
+ <Markdown text={words} />
1279
+ </div>
1280
+ ))
1281
+ )}
1282
+ {typing && <Typing label={words.typing} classNames={classNames} />}
1283
+ {steps && message.steps.length > 0 && (
1284
+ <ol className={classes(classNames, "sc-chat__steps")}>
1285
+ {message.steps.map((step, index) => (
1286
+ <li
1287
+ key={step.id}
1288
+ className={classes(
1289
+ classNames,
1290
+ "sc-chat__step",
1291
+ `sc-chat__step--${shownState(step, message.steps.slice(0, index))}`,
1292
+ )}
1293
+ >
1294
+ {builder ? (
1295
+ <BuilderStep step={step} words={words} classNames={classNames} />
1296
+ ) : (
1297
+ <>
1298
+ <Mark
1299
+ state={step.state}
1300
+ action={step.action}
1301
+ app={step.app ?? null}
1302
+ classNames={classNames}
1303
+ />
1304
+ <span>{words.steps[step.action] ?? actionWords(step.action)}</span>
1305
+ {/* What a step answered is the model's, in its words: only why it failed. */}
1306
+ {step.state === "failed" && step.output !== null && <small>{step.output}</small>}
1307
+ </>
1308
+ )}
1309
+ </li>
1310
+ ))}
1311
+ </ol>
1312
+ )}
1313
+ {message.steps.map(
1314
+ (step) =>
1315
+ step.surface && (
1316
+ <div
1317
+ key={step.id}
1318
+ part="surface"
1319
+ className={classes(
1320
+ classNames,
1321
+ "sc-chat__screen",
1322
+ ...(framesItself(step.surface) ? (["sc-chat__screen--bare"] as const) : []),
1323
+ )}
1324
+ >
1325
+ <Surface
1326
+ surface={step.surface}
1327
+ active={step.id === awaiting || current}
1328
+ answered={answers.get(step.id)}
1329
+ components={components}
1330
+ classNames={classNames}
1331
+ messages={words.kit}
1332
+ locale={locale}
1333
+ files={files}
1334
+ onAction={(action) => onAction({ surface: step.id, ...action })}
1335
+ />
1336
+ </div>
1337
+ ),
1338
+ )}
1339
+ {message.steps.map((step) => {
1340
+ const { request } = step;
1341
+ if (request === null) return null;
1342
+ if (request.decides === "business" && who === "person") {
1343
+ return (
1344
+ <p
1345
+ key={step.id}
1346
+ part="confirming"
1347
+ className={classes(classNames, "sc-chat__confirming")}
1348
+ >
1349
+ <ClockIcon aria-hidden />
1350
+ <span>{words.confirming}</span>
1351
+ </p>
1352
+ );
1353
+ }
1354
+ return (
1355
+ <Approval
1356
+ key={step.id}
1357
+ request={request}
1358
+ name={name}
1359
+ words={words}
1360
+ classNames={classNames}
1361
+ components={components}
1362
+ locale={locale}
1363
+ onAction={(action) => onAction({ surface: step.id, ...action })}
1364
+ />
1365
+ );
1366
+ })}
1367
+ {message.delivery != null && (
1368
+ <p className={classes(classNames, "sc-chat__meta")}>
1369
+ {onChannel && <span>{name}</span>}
1370
+ <DeliveryMark message={message} words={words} classNames={classNames} />
1371
+ </p>
1372
+ )}
1373
+ </div>
1374
+ );
1375
+ }
1376
+
1377
+ /**
1378
+ * A step's state as its list shows it, after the steps `before` it in its reply: the actions of
1379
+ * one reply run one after another, in its order, so one still `running` after another that runs
1380
+ * is only to come (`queued`).
1381
+ */
1382
+ export function shownState(step: Step, before: readonly Step[]): StepState | "queued" {
1383
+ const behind = before.some((earlier) => earlier.state === "running");
1384
+ return step.state === "running" && behind ? "queued" : step.state;
1385
+ }
1386
+
1387
+ /**
1388
+ * A step in the builder's look, v2's: its mark and its name, "failed" beside it when it did, and
1389
+ * what it returned folded under a caret that shows on hover, opened on a click.
1390
+ */
1391
+ function BuilderStep({
1392
+ step,
1393
+ words,
1394
+ classNames,
1395
+ }: {
1396
+ step: Step;
1397
+ words: ChatMessages;
1398
+ classNames: ClassNames;
1399
+ }) {
1400
+ const row = (
1401
+ <>
1402
+ <Mark
1403
+ state={step.state}
1404
+ action={step.action}
1405
+ app={step.app ?? null}
1406
+ classNames={classNames}
1407
+ />
1408
+ <span>{words.steps[step.action] ?? actionWords(step.action)}</span>
1409
+ {step.state === "failed" && <em>{words.failed}</em>}
1410
+ </>
1411
+ );
1412
+ if (!step.output) return row;
1413
+ return (
1414
+ <details>
1415
+ <summary>
1416
+ {row}
1417
+ <CaretRightIcon aria-hidden />
1418
+ </summary>
1419
+ <pre className={classes(classNames, "sc-chat__output")}>{step.output}</pre>
1420
+ </details>
1421
+ );
1422
+ }
1423
+
1424
+ /**
1425
+ * A step's mark on the list's line: the kind of work it is (its glyph) once it is done, a turning
1426
+ * ring while it runs, a ring while it waits for the person, a warning when it failed.
1427
+ */
1428
+ function Mark({
1429
+ state,
1430
+ action,
1431
+ app,
1432
+ classNames,
1433
+ }: {
1434
+ state: StepState;
1435
+ action: string;
1436
+ app: StepApp | null;
1437
+ classNames: ClassNames;
1438
+ }) {
1439
+ const Glyph = stepGlyph(action);
1440
+ // A connected app's step wears the app's logo once done; only an https one is loaded.
1441
+ const logo = app?.icon && /^https:\/\//i.test(app.icon) ? app.icon : null;
1442
+ return (
1443
+ <span aria-hidden className={classes(classNames, "sc-chat__mark")}>
1444
+ {state === "done" && logo !== null ? (
1445
+ <img src={logo} alt="" loading="lazy" draggable={false} />
1446
+ ) : state === "done" ? (
1447
+ <Glyph />
1448
+ ) : state === "running" ? null : (
1449
+ <svg viewBox="0 0 14 14" fill="none">
1450
+ {MARKS[state]}
1451
+ </svg>
1452
+ )}
1453
+ </span>
1454
+ );
1455
+ }
1456
+
1457
+ const STROKE = { stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round" } as const;
1458
+ const MARKS: Record<"waiting" | "failed", ReactNode> = {
1459
+ waiting: <circle cx="7" cy="7" r="4.75" {...STROKE} />,
1460
+ failed: (
1461
+ <>
1462
+ <path d="M7 1.9l5.6 9.7H1.4L7 1.9z" {...STROKE} strokeLinejoin="round" />
1463
+ <path d="M7 5.6v2.6" {...STROKE} />
1464
+ <circle cx="7" cy="10" r="0.8" fill="currentColor" />
1465
+ </>
1466
+ ),
1467
+ };
1468
+
1469
+ /** How long the last reply took, in whole seconds, once it is in; none before the first. */
1470
+ function useTook(pending: boolean): number | null {
1471
+ const [took, setTook] = useState<number | null>(null);
1472
+ const started = useRef<number | null>(null);
1473
+ useEffect(() => {
1474
+ if (pending) {
1475
+ started.current = Date.now();
1476
+ setTook(null);
1477
+ } else if (started.current !== null) {
1478
+ setTook(Math.max(1, Math.round((Date.now() - started.current) / 1000)));
1479
+ started.current = null;
1480
+ }
1481
+ }, [pending]);
1482
+ return took;
1483
+ }
1484
+
1485
+ /**
1486
+ * What is happening while a reply is on its way, in one quiet line over the composer: a dot that
1487
+ * breathes, what goes on (`label`) under a sweeping light, the seconds it has taken, and how to
1488
+ * stop it. A new label rises in where the last was.
1489
+ */
1490
+ function Status({
1491
+ label,
1492
+ stoppable,
1493
+ timed,
1494
+ words,
1495
+ classNames,
1496
+ }: {
1497
+ label: string;
1498
+ /** Whether the reply can be stopped from here: not on a channel, where it runs on its own. */
1499
+ stoppable: boolean;
1500
+ /** Whether it says the seconds so far and the key that stops it: not where steps are hidden. */
1501
+ timed: boolean;
1502
+ words: ChatMessages;
1503
+ classNames: ClassNames;
1504
+ }) {
1505
+ const [seconds, setSeconds] = useState(0);
1506
+ useEffect(() => {
1507
+ const started = Date.now();
1508
+ const tick = setInterval(() => {
1509
+ setSeconds(Math.floor((Date.now() - started) / 1000));
1510
+ }, 1000);
1511
+ return () => {
1512
+ clearInterval(tick);
1513
+ };
1514
+ }, []);
1515
+ return (
1516
+ <p role="status" part="status" className={classes(classNames, "sc-chat__status")}>
1517
+ <i aria-hidden />
1518
+ <span key={label}>{label}…</span>
1519
+ {timed && <small>{words.elapsed(seconds)}</small>}
1520
+ {timed && stoppable && <kbd>{words.escape}</kbd>}
1521
+ </p>
1522
+ );
1523
+ }
1524
+
1525
+ /**
1526
+ * An action the platform holds until the person allows it, as v2's card: a head that says it
1527
+ * needs their approval, then the sentence, what the action would take, and the two answers,
1528
+ * drawn as a surface so the host's blocks and words apply. Drawn from the step, so it is there
1529
+ * live and read back alike.
1530
+ */
1531
+ function Approval({
1532
+ request,
1533
+ name,
1534
+ words,
1535
+ classNames,
1536
+ components,
1537
+ locale,
1538
+ onAction,
1539
+ }: Omit<DrawProps, "onAction"> & {
1540
+ request: Request;
1541
+ name: string;
1542
+ onAction: (action: SurfaceAction) => void;
1543
+ }) {
1544
+ const sentence = words.request(name, request.action);
1545
+ const surface: Shown = {
1546
+ blocks: [
1547
+ {
1548
+ type: "fields",
1549
+ items: Object.entries(request.input).map(([field, value]) => ({
1550
+ label: words.inputs[field] ?? field,
1551
+ value: typeof value === "string" ? value : JSON.stringify(value),
1552
+ })),
1553
+ },
1554
+ {
1555
+ type: "stack",
1556
+ direction: "row",
1557
+ children: [
1558
+ { type: "button", label: words.allow, action: "approve", tone: "primary" },
1559
+ { type: "button", label: words.decline, action: "decline", tone: "neutral" },
1560
+ ],
1561
+ },
1562
+ ],
1563
+ text: sentence,
1564
+ };
1565
+ return (
1566
+ <div part="approval" className={classes(classNames, "sc-chat__request")}>
1567
+ <header>
1568
+ <ShieldWarningIcon aria-hidden />
1569
+ {words.approval}
1570
+ </header>
1571
+ <div>
1572
+ <p>{sentence}</p>
1573
+ <Surface
1574
+ surface={surface}
1575
+ components={components}
1576
+ classNames={classNames}
1577
+ messages={words.kit}
1578
+ locale={locale}
1579
+ onAction={onAction}
1580
+ />
1581
+ </div>
1582
+ </div>
1583
+ );
1584
+ }
1585
+
1586
+ /**
1587
+ * Where the person writes: a box that grows a few lines with the text. Enter sends, Shift+Enter
1588
+ * breaks the line; nothing goes while a reply is on its way or the socket is closed, but the
1589
+ * person can keep writing. While the turn waits for them to act above, the box is closed and
1590
+ * says so; while a reply is on its way, the send button is a stop button (Esc stops too). The
1591
+ * host's `compose` is said from here, or put in the box. With `uploads`, the paperclip attaches
1592
+ * files (a picture pasted too): they upload at once, as chips over the words, and go with them
1593
+ * (or alone) once every one is in. With `write`, what is written goes to the host instead (a
1594
+ * person of the business, on a channel): whatever the turn is doing, emptied once it went.
1595
+ */
1596
+ function Composer({
1597
+ live,
1598
+ name,
1599
+ words,
1600
+ classNames,
1601
+ actions,
1602
+ compose,
1603
+ taken,
1604
+ fresh,
1605
+ builder,
1606
+ uploads,
1607
+ files,
1608
+ dictate,
1609
+ write,
1610
+ onSent,
1611
+ }: PartProps & {
1612
+ actions: ReactNode;
1613
+ compose: ChatCompose | undefined;
1614
+ taken: RefObject<number | null>;
1615
+ fresh: boolean;
1616
+ builder: boolean;
1617
+ uploads: Uploads | undefined;
1618
+ files: FileHost;
1619
+ dictate: Dictate | undefined;
1620
+ write: ((text: string, files: readonly string[]) => Promise<unknown>) | undefined;
1621
+ onSent: () => void;
1622
+ }) {
1623
+ const [text, setText] = useState("");
1624
+ // What is written on its way to the host's `write`: nothing more goes until it went.
1625
+ const [writing, setWriting] = useState(false);
1626
+ const input = useRef<HTMLTextAreaElement>(null);
1627
+ const picker = useRef<HTMLInputElement>(null);
1628
+ // Counts the host's texts put in the box: each one focuses it, the caret at the text's end.
1629
+ const [placed, setPlaced] = useState(0);
1630
+ // Waiting for the person to act above holds the composer, unless the words go to the host.
1631
+ const held = write === undefined && live.awaiting !== null;
1632
+ // What is dictated joins what is written, after it, and the caret goes to its end.
1633
+ const dictation = useDictation(dictate, (words) => {
1634
+ setText((now) => (now.trim() === "" ? words : `${now.trimEnd()} ${words}`));
1635
+ setPlaced((count) => count + 1);
1636
+ });
1637
+ const listening = dictation.state === "listening";
1638
+
1639
+ // The host's request, once: said when it asks so (waiting for the socket to open), put in the
1640
+ // box otherwise, or when it cannot go now (a reply on its way, a turn waiting).
1641
+ useEffect(() => {
1642
+ if (compose === undefined || taken.current === compose.key) return;
1643
+ if (compose.send && !live.open) return;
1644
+ taken.current = compose.key;
1645
+ if (compose.send && live.say(compose.text)) {
1646
+ onSent();
1647
+ return;
1648
+ }
1649
+ setText(compose.text);
1650
+ setPlaced((count) => count + 1);
1651
+ }, [compose, taken, live, onSent]);
1652
+
1653
+ useLayoutEffect(() => {
1654
+ const box = input.current;
1655
+ if (placed === 0 || box === null) return;
1656
+ box.focus();
1657
+ box.setSelectionRange(box.value.length, box.value.length);
1658
+ }, [placed]);
1659
+
1660
+ const attached = uploads?.ids ?? [];
1661
+ const settled = uploads === undefined || uploads.waiting === null;
1662
+ const said = text.trim() !== "" || attached.length > 0;
1663
+ const sent = () => {
1664
+ setText("");
1665
+ uploads?.clear();
1666
+ onSent();
1667
+ };
1668
+ const submit = () => {
1669
+ if (!settled) return;
1670
+ if (write === undefined) {
1671
+ if (live.say(text, attached)) sent();
1672
+ return;
1673
+ }
1674
+ if (writing || !said) return;
1675
+ setWriting(true);
1676
+ write(text.trim(), attached)
1677
+ .then(sent, () => undefined)
1678
+ .finally(() => {
1679
+ setWriting(false);
1680
+ });
1681
+ };
1682
+ const onKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
1683
+ if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return;
1684
+ event.preventDefault();
1685
+ submit();
1686
+ };
1687
+ const ready =
1688
+ write === undefined
1689
+ ? live.open && !live.pending && !held && settled && said
1690
+ : !writing && settled && said;
1691
+ const invitation = write === undefined ? words.placeholder(name, fresh) : words.write;
1692
+
1693
+ return (
1694
+ <form
1695
+ onSubmit={(event) => {
1696
+ event.preventDefault();
1697
+ submit();
1698
+ }}
1699
+ part="composer"
1700
+ className={classes(classNames, "sc-chat__composer")}
1701
+ >
1702
+ {uploads && uploads.items.length > 0 && (
1703
+ <div part="files" className={classes(classNames, "sc-chat__files")}>
1704
+ <UploadChips uploads={uploads} host={files} />
1705
+ </div>
1706
+ )}
1707
+ <textarea
1708
+ ref={input}
1709
+ rows={1}
1710
+ value={text}
1711
+ maxLength={20_000}
1712
+ disabled={held}
1713
+ // The builder takes the keyboard as it opens, as v2's did: it is there to be told things.
1714
+ autoFocus={builder}
1715
+ aria-label={invitation}
1716
+ placeholder={
1717
+ held ? words.awaiting : live.open || write !== undefined ? invitation : words.connecting
1718
+ }
1719
+ onChange={(event) => {
1720
+ setText(event.target.value);
1721
+ }}
1722
+ onKeyDown={onKeyDown}
1723
+ part="input"
1724
+ // A picture pasted (a screenshot, an image copied) is attached; words pasted stay words.
1725
+ onPaste={(event) => {
1726
+ const pasted = [...event.clipboardData.files];
1727
+ if (!uploads || pasted.length === 0) return;
1728
+ if (event.clipboardData.getData("text/plain") !== "") return;
1729
+ event.preventDefault();
1730
+ uploads.add(pasted);
1731
+ }}
1732
+ className={classes(classNames, "sc-chat__input")}
1733
+ />
1734
+ {uploads && (
1735
+ <>
1736
+ <button
1737
+ type="button"
1738
+ aria-label={words.attach}
1739
+ title={words.attach}
1740
+ disabled={held}
1741
+ onClick={() => picker.current?.click()}
1742
+ part="attach"
1743
+ className={classes(classNames, "sc-chat__attach")}
1744
+ >
1745
+ <PaperclipIcon aria-hidden />
1746
+ </button>
1747
+ <input
1748
+ ref={picker}
1749
+ type="file"
1750
+ multiple
1751
+ hidden
1752
+ tabIndex={-1}
1753
+ onChange={(event) => {
1754
+ uploads.add([...(event.target.files ?? [])]);
1755
+ event.target.value = "";
1756
+ }}
1757
+ />
1758
+ </>
1759
+ )}
1760
+ <div part="actions" className={classes(classNames, "sc-chat__actions")}>
1761
+ {dictation.available && (
1762
+ <button
1763
+ type="button"
1764
+ aria-label={listening ? words.dictateStop : words.dictate}
1765
+ title={listening ? words.dictateStop : words.dictate}
1766
+ aria-pressed={listening}
1767
+ data-state={dictation.state}
1768
+ disabled={held || dictation.state === "writing"}
1769
+ onClick={dictation.toggle}
1770
+ part="dictate"
1771
+ className={classes(classNames, "sc-chat__dictate")}
1772
+ >
1773
+ <MicrophoneIcon aria-hidden weight={listening ? "fill" : "regular"} />
1774
+ </button>
1775
+ )}
1776
+ {dictation.state !== "idle" && (
1777
+ <span
1778
+ role="status"
1779
+ data-state={dictation.state}
1780
+ className={classes(classNames, "sc-chat__dictation")}
1781
+ >
1782
+ {words.dictation[dictation.state]}
1783
+ </span>
1784
+ )}
1785
+ {actions}
1786
+ </div>
1787
+ {live.pending && write === undefined ? (
1788
+ <button
1789
+ type="button"
1790
+ aria-label={words.stop}
1791
+ title={`${words.stop} (Esc)`}
1792
+ onClick={() => live.stop()}
1793
+ part="stop"
1794
+ className={classes(classNames, "sc-chat__stop")}
1795
+ >
1796
+ <StopIcon weight="fill" aria-hidden />
1797
+ </button>
1798
+ ) : (
1799
+ <button
1800
+ type="submit"
1801
+ aria-label={words.send}
1802
+ title={words.send}
1803
+ disabled={!ready}
1804
+ part="send"
1805
+ className={classes(classNames, "sc-chat__send")}
1806
+ >
1807
+ <ArrowUpIcon weight="bold" aria-hidden />
1808
+ </button>
1809
+ )}
1810
+ </form>
1811
+ );
1812
+ }