@athenaintel/react 0.23.3 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/README.md +6 -0
  2. package/dist/assets/AssetPanel.cjs +1 -1
  3. package/dist/assets/AssetPanel.cjs.map +1 -1
  4. package/dist/assets/AssetPanel.js +1 -1
  5. package/dist/assets/AssetPanel.js.map +1 -1
  6. package/dist/assets/AthenaAssetEmbed.cjs +1 -1
  7. package/dist/assets/AthenaAssetEmbed.cjs.map +1 -1
  8. package/dist/assets/AthenaAssetEmbed.js +1 -1
  9. package/dist/assets/AthenaAssetEmbed.js.map +1 -1
  10. package/dist/chat/AthenaChat.cjs +15 -23
  11. package/dist/chat/AthenaChat.cjs.map +1 -1
  12. package/dist/chat/AthenaChat.d.ts +9 -4
  13. package/dist/chat/AthenaChat.js +15 -23
  14. package/dist/chat/AthenaChat.js.map +1 -1
  15. package/dist/chat/StatewireApprovalCard.cjs +3 -18
  16. package/dist/chat/StatewireApprovalCard.cjs.map +1 -1
  17. package/dist/chat/StatewireApprovalCard.js +3 -18
  18. package/dist/chat/StatewireApprovalCard.js.map +1 -1
  19. package/dist/chat/StatewireClientToolBridge.cjs +2 -16
  20. package/dist/chat/StatewireClientToolBridge.cjs.map +1 -1
  21. package/dist/chat/StatewireClientToolBridge.js +2 -16
  22. package/dist/chat/StatewireClientToolBridge.js.map +1 -1
  23. package/dist/chat/StatewireQueuedMessages.cjs +5 -16
  24. package/dist/chat/StatewireQueuedMessages.cjs.map +1 -1
  25. package/dist/chat/StatewireQueuedMessages.js +5 -16
  26. package/dist/chat/StatewireQueuedMessages.js.map +1 -1
  27. package/dist/chat/WaitingOnYouBanner.cjs +3 -18
  28. package/dist/chat/WaitingOnYouBanner.cjs.map +1 -1
  29. package/dist/chat/WaitingOnYouBanner.js +3 -18
  30. package/dist/chat/WaitingOnYouBanner.js.map +1 -1
  31. package/dist/chat/run-activity.cjs +0 -16
  32. package/dist/chat/run-activity.cjs.map +1 -1
  33. package/dist/chat/run-activity.js +0 -16
  34. package/dist/chat/run-activity.js.map +1 -1
  35. package/dist/chat/statewire-client-tools.cjs +1 -17
  36. package/dist/chat/statewire-client-tools.cjs.map +1 -1
  37. package/dist/chat/statewire-client-tools.js +1 -17
  38. package/dist/chat/statewire-client-tools.js.map +1 -1
  39. package/dist/chat/super-grouping/activity-timeline.cjs +0 -16
  40. package/dist/chat/super-grouping/activity-timeline.cjs.map +1 -1
  41. package/dist/chat/super-grouping/activity-timeline.js +0 -16
  42. package/dist/chat/super-grouping/activity-timeline.js.map +1 -1
  43. package/dist/chat/tiptap-text.cjs +1 -17
  44. package/dist/chat/tiptap-text.cjs.map +1 -1
  45. package/dist/chat/tiptap-text.js +1 -17
  46. package/dist/chat/tiptap-text.js.map +1 -1
  47. package/dist/chat/use-athena-run-live.cjs +0 -15
  48. package/dist/chat/use-athena-run-live.cjs.map +1 -1
  49. package/dist/chat/use-athena-run-live.js +0 -15
  50. package/dist/chat/use-athena-run-live.js.map +1 -1
  51. package/dist/chat/use-statewire-approval-request.cjs +4 -16
  52. package/dist/chat/use-statewire-approval-request.cjs.map +1 -1
  53. package/dist/chat/use-statewire-approval-request.d.ts +2 -0
  54. package/dist/chat/use-statewire-approval-request.js +4 -16
  55. package/dist/chat/use-statewire-approval-request.js.map +1 -1
  56. package/dist/chat-ui/composer/tiptap-composer.cjs +1 -16
  57. package/dist/chat-ui/composer/tiptap-composer.cjs.map +1 -1
  58. package/dist/chat-ui/composer/tiptap-composer.js +1 -16
  59. package/dist/chat-ui/composer/tiptap-composer.js.map +1 -1
  60. package/dist/chat-ui/index.cjs +0 -16
  61. package/dist/chat-ui/index.cjs.map +1 -1
  62. package/dist/chat-ui/index.js +0 -16
  63. package/dist/chat-ui/index.js.map +1 -1
  64. package/dist/chat-ui/mentions/mention-chip.cjs +1 -16
  65. package/dist/chat-ui/mentions/mention-chip.cjs.map +1 -1
  66. package/dist/chat-ui/mentions/mention-chip.js +1 -16
  67. package/dist/chat-ui/mentions/mention-chip.js.map +1 -1
  68. package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.cjs +4 -6
  69. package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.cjs.map +1 -1
  70. package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.js +4 -6
  71. package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.js.map +1 -1
  72. package/dist/chat-ui/mentions/suggestions/data/sources/index.cjs +2 -9
  73. package/dist/chat-ui/mentions/suggestions/data/sources/index.cjs.map +1 -1
  74. package/dist/chat-ui/mentions/suggestions/data/sources/index.js +2 -9
  75. package/dist/chat-ui/mentions/suggestions/data/sources/index.js.map +1 -1
  76. package/dist/chat-ui/messages/message-components.cjs +5 -19
  77. package/dist/chat-ui/messages/message-components.cjs.map +1 -1
  78. package/dist/chat-ui/messages/message-components.js +5 -19
  79. package/dist/chat-ui/messages/message-components.js.map +1 -1
  80. package/dist/chat-ui/messages/runtime-message.cjs +1 -16
  81. package/dist/chat-ui/messages/runtime-message.cjs.map +1 -1
  82. package/dist/chat-ui/messages/runtime-message.js +1 -16
  83. package/dist/chat-ui/messages/runtime-message.js.map +1 -1
  84. package/dist/chat-ui/messages/statewire-continue-run.cjs +3 -15
  85. package/dist/chat-ui/messages/statewire-continue-run.cjs.map +1 -1
  86. package/dist/chat-ui/messages/statewire-continue-run.js +3 -15
  87. package/dist/chat-ui/messages/statewire-continue-run.js.map +1 -1
  88. package/dist/chat-ui/messages/user-edit-composer.cjs +0 -16
  89. package/dist/chat-ui/messages/user-edit-composer.cjs.map +1 -1
  90. package/dist/chat-ui/messages/user-edit-composer.js +0 -16
  91. package/dist/chat-ui/messages/user-edit-composer.js.map +1 -1
  92. package/dist/chat-ui/messages/wake-marker.cjs +1 -16
  93. package/dist/chat-ui/messages/wake-marker.cjs.map +1 -1
  94. package/dist/chat-ui/messages/wake-marker.js +1 -16
  95. package/dist/chat-ui/messages/wake-marker.js.map +1 -1
  96. package/dist/chat-ui/statewire.cjs +0 -16
  97. package/dist/chat-ui/statewire.cjs.map +1 -1
  98. package/dist/chat-ui/statewire.js +0 -16
  99. package/dist/chat-ui/statewire.js.map +1 -1
  100. package/dist/chat-ui/thread/statewire-history-edge.cjs +2 -17
  101. package/dist/chat-ui/thread/statewire-history-edge.cjs.map +1 -1
  102. package/dist/chat-ui/thread/statewire-history-edge.js +2 -17
  103. package/dist/chat-ui/thread/statewire-history-edge.js.map +1 -1
  104. package/dist/dictation/DictationStrip.cjs +2 -1
  105. package/dist/dictation/DictationStrip.cjs.map +1 -1
  106. package/dist/dictation/DictationStrip.d.ts +2 -0
  107. package/dist/dictation/DictationStrip.js +2 -1
  108. package/dist/dictation/DictationStrip.js.map +1 -1
  109. package/dist/dictation/dictation-engine.cjs.map +1 -1
  110. package/dist/dictation/dictation-engine.d.ts +8 -0
  111. package/dist/dictation/dictation-engine.js.map +1 -1
  112. package/dist/dictation/recorded-dictation-engine.cjs +153 -0
  113. package/dist/dictation/recorded-dictation-engine.cjs.map +1 -0
  114. package/dist/dictation/recorded-dictation-engine.d.ts +66 -0
  115. package/dist/dictation/recorded-dictation-engine.js +153 -0
  116. package/dist/dictation/recorded-dictation-engine.js.map +1 -0
  117. package/dist/dictation/use-athena-dictation-engine.cjs +64 -0
  118. package/dist/dictation/use-athena-dictation-engine.cjs.map +1 -0
  119. package/dist/dictation/use-athena-dictation-engine.d.ts +25 -0
  120. package/dist/dictation/use-athena-dictation-engine.js +64 -0
  121. package/dist/dictation/use-athena-dictation-engine.js.map +1 -0
  122. package/dist/dictation/use-dictation.cjs +17 -6
  123. package/dist/dictation/use-dictation.cjs.map +1 -1
  124. package/dist/dictation/use-dictation.d.ts +20 -4
  125. package/dist/dictation/use-dictation.js +17 -6
  126. package/dist/dictation/use-dictation.js.map +1 -1
  127. package/dist/index.cjs +28 -39
  128. package/dist/index.cjs.map +1 -1
  129. package/dist/index.d.ts +4 -1
  130. package/dist/index.js +10 -21
  131. package/dist/index.js.map +1 -1
  132. package/dist/packages/agent-runtime/src/edit-command.cjs.map +1 -1
  133. package/dist/packages/agent-runtime/src/edit-command.js.map +1 -1
  134. package/dist/packages/agent-runtime/src/errors.cjs +18 -0
  135. package/dist/packages/agent-runtime/src/errors.cjs.map +1 -1
  136. package/dist/packages/agent-runtime/src/errors.js +19 -1
  137. package/dist/packages/agent-runtime/src/errors.js.map +1 -1
  138. package/dist/packages/agent-runtime/src/generated/runconfig-contract.cjs +1 -0
  139. package/dist/packages/agent-runtime/src/generated/runconfig-contract.cjs.map +1 -1
  140. package/dist/packages/agent-runtime/src/generated/runconfig-contract.js +1 -0
  141. package/dist/packages/agent-runtime/src/generated/runconfig-contract.js.map +1 -1
  142. package/dist/packages/agent-runtime/src/interrupts.cjs +61 -12
  143. package/dist/packages/agent-runtime/src/interrupts.cjs.map +1 -1
  144. package/dist/packages/agent-runtime/src/interrupts.js +61 -12
  145. package/dist/packages/agent-runtime/src/interrupts.js.map +1 -1
  146. package/dist/packages/agent-runtime/src/quota-safe-local-storage-session.cjs +49 -0
  147. package/dist/packages/agent-runtime/src/quota-safe-local-storage-session.cjs.map +1 -1
  148. package/dist/packages/agent-runtime/src/quota-safe-local-storage-session.js +49 -0
  149. package/dist/packages/agent-runtime/src/quota-safe-local-storage-session.js.map +1 -1
  150. package/dist/packages/agent-runtime/src/thread-status.js +1 -1
  151. package/dist/packages/agent-runtime/src/tool-metadata.cjs +1 -37
  152. package/dist/packages/agent-runtime/src/tool-metadata.cjs.map +1 -1
  153. package/dist/packages/agent-runtime/src/tool-metadata.js +1 -37
  154. package/dist/packages/agent-runtime/src/tool-metadata.js.map +1 -1
  155. package/dist/packages/agent-runtime/src/use-deep-agent-config.cjs +0 -1
  156. package/dist/packages/agent-runtime/src/use-deep-agent-config.cjs.map +1 -1
  157. package/dist/packages/agent-runtime/src/use-deep-agent-config.js +0 -1
  158. package/dist/packages/agent-runtime/src/use-deep-agent-config.js.map +1 -1
  159. package/dist/packages/agent-runtime/src/use-deep-agent-thread.cjs.map +1 -1
  160. package/dist/packages/agent-runtime/src/use-deep-agent-thread.js.map +1 -1
  161. package/dist/packages/react/package.json.cjs +1 -1
  162. package/dist/packages/react/package.json.js +1 -1
  163. package/dist/provider/AthenaProvider.cjs +3 -17
  164. package/dist/provider/AthenaProvider.cjs.map +1 -1
  165. package/dist/provider/AthenaProvider.js +3 -17
  166. package/dist/provider/AthenaProvider.js.map +1 -1
  167. package/dist/runtime/statewire-lifecycle.cjs +0 -16
  168. package/dist/runtime/statewire-lifecycle.cjs.map +1 -1
  169. package/dist/runtime/statewire-lifecycle.js +0 -16
  170. package/dist/runtime/statewire-lifecycle.js.map +1 -1
  171. package/dist/runtime/useAthenaStatewireRuntime.cjs +3 -17
  172. package/dist/runtime/useAthenaStatewireRuntime.cjs.map +1 -1
  173. package/dist/runtime/useAthenaStatewireRuntime.js +3 -17
  174. package/dist/runtime/useAthenaStatewireRuntime.js.map +1 -1
  175. package/dist/threads/statewire-thread-list.cjs +4 -17
  176. package/dist/threads/statewire-thread-list.cjs.map +1 -1
  177. package/dist/threads/statewire-thread-list.js +4 -17
  178. package/dist/threads/statewire-thread-list.js.map +1 -1
  179. package/dist/threads/use-rest-conversation-history.cjs +2 -16
  180. package/dist/threads/use-rest-conversation-history.cjs.map +1 -1
  181. package/dist/threads/use-rest-conversation-history.js +2 -16
  182. package/dist/threads/use-rest-conversation-history.js.map +1 -1
  183. package/dist/threads/use-statewire-conversation-history.cjs +2 -16
  184. package/dist/threads/use-statewire-conversation-history.cjs.map +1 -1
  185. package/dist/threads/use-statewire-conversation-history.js +2 -16
  186. package/dist/threads/use-statewire-conversation-history.js.map +1 -1
  187. package/dist/toolkits/index.cjs +0 -7
  188. package/dist/toolkits/index.cjs.map +1 -1
  189. package/dist/toolkits/index.d.ts +0 -2
  190. package/dist/toolkits/index.js +0 -7
  191. package/dist/toolkits/index.js.map +1 -1
  192. package/dist/tools/index.cjs +4 -20
  193. package/dist/tools/index.cjs.map +1 -1
  194. package/dist/tools/index.js +1 -17
  195. package/dist/tools/index.js.map +1 -1
  196. package/dist/tools/misc-tool-uis.cjs +2 -17
  197. package/dist/tools/misc-tool-uis.cjs.map +1 -1
  198. package/dist/tools/misc-tool-uis.js +2 -17
  199. package/dist/tools/misc-tool-uis.js.map +1 -1
  200. package/dist/tools/primitives.cjs +1 -16
  201. package/dist/tools/primitives.cjs.map +1 -1
  202. package/dist/tools/primitives.js +1 -16
  203. package/dist/tools/primitives.js.map +1 -1
  204. package/dist/tools/tool-fallback.cjs +2 -17
  205. package/dist/tools/tool-fallback.cjs.map +1 -1
  206. package/dist/tools/tool-fallback.js +2 -17
  207. package/dist/tools/tool-fallback.js.map +1 -1
  208. package/dist/tools/voice-update-tool-ui.cjs +1 -17
  209. package/dist/tools/voice-update-tool-ui.cjs.map +1 -1
  210. package/dist/tools/voice-update-tool-ui.js +1 -17
  211. package/dist/tools/voice-update-tool-ui.js.map +1 -1
  212. package/package.json +1 -1
  213. package/dist/node_modules/qrcode-generator/dist/qrcode.cjs +0 -32
  214. package/dist/node_modules/qrcode-generator/dist/qrcode.cjs.map +0 -1
  215. package/dist/node_modules/qrcode-generator/dist/qrcode.js +0 -31
  216. package/dist/node_modules/qrcode-generator/dist/qrcode.js.map +0 -1
  217. package/dist/node_modules/zod/v4/classic/errors.cjs +0 -44
  218. package/dist/node_modules/zod/v4/classic/errors.cjs.map +0 -1
  219. package/dist/node_modules/zod/v4/classic/errors.js +0 -44
  220. package/dist/node_modules/zod/v4/classic/errors.js.map +0 -1
  221. package/dist/node_modules/zod/v4/classic/iso.cjs +0 -43
  222. package/dist/node_modules/zod/v4/classic/iso.cjs.map +0 -1
  223. package/dist/node_modules/zod/v4/classic/iso.js +0 -43
  224. package/dist/node_modules/zod/v4/classic/iso.js.map +0 -1
  225. package/dist/node_modules/zod/v4/classic/parse.cjs +0 -29
  226. package/dist/node_modules/zod/v4/classic/parse.cjs.map +0 -1
  227. package/dist/node_modules/zod/v4/classic/parse.js +0 -29
  228. package/dist/node_modules/zod/v4/classic/parse.js.map +0 -1
  229. package/dist/node_modules/zod/v4/classic/schemas.cjs +0 -871
  230. package/dist/node_modules/zod/v4/classic/schemas.cjs.map +0 -1
  231. package/dist/node_modules/zod/v4/classic/schemas.js +0 -871
  232. package/dist/node_modules/zod/v4/classic/schemas.js.map +0 -1
  233. package/dist/node_modules/zod/v4/core/api.cjs +0 -570
  234. package/dist/node_modules/zod/v4/core/api.cjs.map +0 -1
  235. package/dist/node_modules/zod/v4/core/api.js +0 -570
  236. package/dist/node_modules/zod/v4/core/api.js.map +0 -1
  237. package/dist/node_modules/zod/v4/core/checks.cjs +0 -411
  238. package/dist/node_modules/zod/v4/core/checks.cjs.map +0 -1
  239. package/dist/node_modules/zod/v4/core/checks.js +0 -411
  240. package/dist/node_modules/zod/v4/core/checks.js.map +0 -1
  241. package/dist/node_modules/zod/v4/core/core.cjs +0 -77
  242. package/dist/node_modules/zod/v4/core/core.cjs.map +0 -1
  243. package/dist/node_modules/zod/v4/core/core.js +0 -77
  244. package/dist/node_modules/zod/v4/core/core.js.map +0 -1
  245. package/dist/node_modules/zod/v4/core/doc.cjs +0 -38
  246. package/dist/node_modules/zod/v4/core/doc.cjs.map +0 -1
  247. package/dist/node_modules/zod/v4/core/doc.js +0 -38
  248. package/dist/node_modules/zod/v4/core/doc.js.map +0 -1
  249. package/dist/node_modules/zod/v4/core/errors.cjs +0 -76
  250. package/dist/node_modules/zod/v4/core/errors.cjs.map +0 -1
  251. package/dist/node_modules/zod/v4/core/errors.js +0 -76
  252. package/dist/node_modules/zod/v4/core/errors.js.map +0 -1
  253. package/dist/node_modules/zod/v4/core/json-schema-processors.cjs +0 -312
  254. package/dist/node_modules/zod/v4/core/json-schema-processors.cjs.map +0 -1
  255. package/dist/node_modules/zod/v4/core/json-schema-processors.js +0 -312
  256. package/dist/node_modules/zod/v4/core/json-schema-processors.js.map +0 -1
  257. package/dist/node_modules/zod/v4/core/parse.cjs +0 -96
  258. package/dist/node_modules/zod/v4/core/parse.cjs.map +0 -1
  259. package/dist/node_modules/zod/v4/core/parse.js +0 -96
  260. package/dist/node_modules/zod/v4/core/parse.js.map +0 -1
  261. package/dist/node_modules/zod/v4/core/regexes.cjs +0 -86
  262. package/dist/node_modules/zod/v4/core/regexes.cjs.map +0 -1
  263. package/dist/node_modules/zod/v4/core/regexes.js +0 -86
  264. package/dist/node_modules/zod/v4/core/regexes.js.map +0 -1
  265. package/dist/node_modules/zod/v4/core/registries.cjs +0 -52
  266. package/dist/node_modules/zod/v4/core/registries.cjs.map +0 -1
  267. package/dist/node_modules/zod/v4/core/registries.js +0 -52
  268. package/dist/node_modules/zod/v4/core/registries.js.map +0 -1
  269. package/dist/node_modules/zod/v4/core/schemas.cjs +0 -1412
  270. package/dist/node_modules/zod/v4/core/schemas.cjs.map +0 -1
  271. package/dist/node_modules/zod/v4/core/schemas.js +0 -1413
  272. package/dist/node_modules/zod/v4/core/schemas.js.map +0 -1
  273. package/dist/node_modules/zod/v4/core/to-json-schema.cjs +0 -370
  274. package/dist/node_modules/zod/v4/core/to-json-schema.cjs.map +0 -1
  275. package/dist/node_modules/zod/v4/core/to-json-schema.js +0 -370
  276. package/dist/node_modules/zod/v4/core/to-json-schema.js.map +0 -1
  277. package/dist/node_modules/zod/v4/core/util.cjs +0 -444
  278. package/dist/node_modules/zod/v4/core/util.cjs.map +0 -1
  279. package/dist/node_modules/zod/v4/core/util.js +0 -444
  280. package/dist/node_modules/zod/v4/core/util.js.map +0 -1
  281. package/dist/node_modules/zod/v4/core/versions.cjs +0 -9
  282. package/dist/node_modules/zod/v4/core/versions.cjs.map +0 -1
  283. package/dist/node_modules/zod/v4/core/versions.js +0 -9
  284. package/dist/node_modules/zod/v4/core/versions.js.map +0 -1
  285. package/dist/packages/agent-runtime/src/thread-handoff.cjs +0 -85
  286. package/dist/packages/agent-runtime/src/thread-handoff.cjs.map +0 -1
  287. package/dist/packages/agent-runtime/src/thread-handoff.js +0 -85
  288. package/dist/packages/agent-runtime/src/thread-handoff.js.map +0 -1
@@ -0,0 +1,153 @@
1
+ import { DICTATION_ENGINE_ERROR_KINDS } from "./dictation-engine.js";
2
+ const DEFAULT_MIME_TYPES = ["audio/webm;codecs=opus", "audio/webm", "audio/mp4", "audio/ogg;codecs=opus"];
3
+ const DEFAULT_FLUSH_TIMEOUT_MS = 9e4;
4
+ const DEFAULT_MAX_RECORDING_MS = 5 * 6e4;
5
+ function resolveMediaRecorderCtor() {
6
+ if (typeof window === "undefined") return null;
7
+ return window.MediaRecorder ?? null;
8
+ }
9
+ function pickRecorderMimeType(Ctor, candidates) {
10
+ if (!Ctor.isTypeSupported) return void 0;
11
+ return candidates.find((type) => {
12
+ var _a;
13
+ return (_a = Ctor.isTypeSupported) == null ? void 0 : _a.call(Ctor, type);
14
+ });
15
+ }
16
+ function transcriptionError(error) {
17
+ return {
18
+ kind: DICTATION_ENGINE_ERROR_KINDS.connection,
19
+ message: error instanceof Error && error.message ? error.message : "Transcription failed. Try again."
20
+ };
21
+ }
22
+ function createRecordedDictationEngine(options) {
23
+ const {
24
+ transcribe,
25
+ mimeTypes = DEFAULT_MIME_TYPES,
26
+ flushTimeoutMs = DEFAULT_FLUSH_TIMEOUT_MS,
27
+ maxDurationMs = DEFAULT_MAX_RECORDING_MS
28
+ } = options;
29
+ return {
30
+ id: "recorded-upload",
31
+ flushTimeoutMs,
32
+ stoppingLabel: "Transcribing…",
33
+ isSupported: () => {
34
+ var _a;
35
+ return resolveMediaRecorderCtor() !== null && typeof navigator !== "undefined" && typeof ((_a = navigator.mediaDevices) == null ? void 0 : _a.getUserMedia) === "function";
36
+ },
37
+ start: ({ stream, lang, callbacks }) => {
38
+ const Ctor = resolveMediaRecorderCtor();
39
+ const noop = { stop: () => void 0, abort: () => void 0 };
40
+ if (!Ctor || !stream) {
41
+ callbacks.onError({
42
+ kind: !Ctor ? DICTATION_ENGINE_ERROR_KINDS.unsupported : DICTATION_ENGINE_ERROR_KINDS.permission,
43
+ message: !Ctor ? "This browser cannot record audio." : "No microphone stream was available."
44
+ });
45
+ callbacks.onEnd();
46
+ return noop;
47
+ }
48
+ const mimeType = pickRecorderMimeType(Ctor, mimeTypes);
49
+ let recorder;
50
+ try {
51
+ recorder = new Ctor(stream, mimeType ? { mimeType } : void 0);
52
+ } catch (error) {
53
+ callbacks.onError({
54
+ kind: DICTATION_ENGINE_ERROR_KINDS.unknown,
55
+ message: error instanceof Error ? error.message : "Recording could not start."
56
+ });
57
+ callbacks.onEnd();
58
+ return noop;
59
+ }
60
+ const chunks = [];
61
+ const upload = new AbortController();
62
+ let ended = false;
63
+ let aborted = false;
64
+ let limitTimer;
65
+ const clearLimit = () => {
66
+ if (limitTimer === void 0) return;
67
+ clearTimeout(limitTimer);
68
+ limitTimer = void 0;
69
+ };
70
+ const end = () => {
71
+ if (ended) return;
72
+ ended = true;
73
+ clearLimit();
74
+ recorder.ondataavailable = null;
75
+ recorder.onstop = null;
76
+ recorder.onerror = null;
77
+ callbacks.onEnd();
78
+ };
79
+ const stopRecorder = () => {
80
+ if (recorder.state === "inactive") return false;
81
+ try {
82
+ recorder.stop();
83
+ return true;
84
+ } catch {
85
+ return false;
86
+ }
87
+ };
88
+ recorder.ondataavailable = (event) => {
89
+ if (event.data && event.data.size > 0) chunks.push(event.data);
90
+ };
91
+ recorder.onerror = () => {
92
+ callbacks.onError({
93
+ kind: DICTATION_ENGINE_ERROR_KINDS.unknown,
94
+ message: "Recording stopped unexpectedly."
95
+ });
96
+ end();
97
+ };
98
+ recorder.onstop = () => {
99
+ if (aborted || ended) {
100
+ end();
101
+ return;
102
+ }
103
+ const clip = new Blob(chunks, { type: recorder.mimeType || mimeType || "audio/webm" });
104
+ if (clip.size === 0) {
105
+ end();
106
+ return;
107
+ }
108
+ void transcribe(clip, { lang, signal: upload.signal }).then((text) => {
109
+ if (ended) return;
110
+ const trimmed = text.trim();
111
+ if (trimmed) callbacks.onTranscript({ final: trimmed, interim: "" });
112
+ }).catch((error) => {
113
+ if (ended) return;
114
+ callbacks.onError(transcriptionError(error));
115
+ }).finally(end);
116
+ };
117
+ try {
118
+ recorder.start();
119
+ } catch (error) {
120
+ callbacks.onError({
121
+ kind: DICTATION_ENGINE_ERROR_KINDS.unknown,
122
+ message: error instanceof Error ? error.message : "Recording could not start."
123
+ });
124
+ end();
125
+ return noop;
126
+ }
127
+ if (Number.isFinite(maxDurationMs) && maxDurationMs > 0) {
128
+ limitTimer = setTimeout(() => {
129
+ limitTimer = void 0;
130
+ if (!ended) stopRecorder();
131
+ }, maxDurationMs);
132
+ }
133
+ return {
134
+ stop: () => {
135
+ clearLimit();
136
+ if (!stopRecorder()) end();
137
+ },
138
+ abort: () => {
139
+ aborted = true;
140
+ upload.abort();
141
+ stopRecorder();
142
+ end();
143
+ }
144
+ };
145
+ }
146
+ };
147
+ }
148
+ export {
149
+ DEFAULT_MAX_RECORDING_MS,
150
+ createRecordedDictationEngine,
151
+ pickRecorderMimeType
152
+ };
153
+ //# sourceMappingURL=recorded-dictation-engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recorded-dictation-engine.js","sources":["../../src/dictation/recorded-dictation-engine.ts"],"sourcesContent":["import {\n DICTATION_ENGINE_ERROR_KINDS,\n type DictationEngine,\n type DictationEngineError,\n} from './dictation-engine';\n\nexport interface DictationTranscribeContext {\n /** BCP-47 tag the session was started with (host `lang` or the document's). */\n lang: string;\n /** Fires when the user cancels or the composer gives up waiting. */\n signal: AbortSignal;\n}\n\n/**\n * Turn a finished recording into text. Receives the whole clip once the user\n * stops; resolves with the transcript (empty when nothing was said). Should\n * honour `signal` so a cancelled dictation does not keep uploading.\n */\nexport type DictationTranscriber = (clip: Blob, context: DictationTranscribeContext) => Promise<string>;\n\nexport interface RecordedDictationEngineOptions {\n transcribe: DictationTranscriber;\n /**\n * Preferred `MediaRecorder` container, tried in order until the browser\n * accepts one. Defaults to Opus in WebM (Chrome, Firefox, Edge) then MP4\n * (Safari); the browser's own default is the last resort.\n */\n mimeTypes?: readonly string[];\n /**\n * Upper bound on the transcription round trip; the in-flight request is\n * aborted when it elapses. Defaults to 90s, above agora's 60s Deepgram\n * budget plus upload time.\n */\n flushTimeoutMs?: number;\n /**\n * Longest recording the engine accepts before it stops on its own and\n * transcribes what it has, exactly as if Stop had been pressed. Bounds a\n * mic left open by mistake: every recorded minute is a minute billed by the\n * recognizer, and agora refuses clips over its size cap. Defaults to\n * 5 minutes; pass `Infinity` to disable.\n */\n maxDurationMs?: number;\n}\n\nconst DEFAULT_MIME_TYPES = ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4', 'audio/ogg;codecs=opus'];\nconst DEFAULT_FLUSH_TIMEOUT_MS = 90_000;\nexport const DEFAULT_MAX_RECORDING_MS = 5 * 60_000;\n\n// lib.dom typings for MediaRecorder vary across the TypeScript versions the\n// SDK supports; describe only what the engine touches.\ninterface MediaRecorderLike extends EventTarget {\n readonly state: 'inactive' | 'recording' | 'paused';\n readonly mimeType: string;\n start: (timeslice?: number) => void;\n stop: () => void;\n ondataavailable: ((event: { data: Blob }) => void) | null;\n onstop: (() => void) | null;\n onerror: ((event: Event) => void) | null;\n}\ninterface MediaRecorderCtor {\n new (stream: MediaStream, options?: { mimeType?: string }): MediaRecorderLike;\n isTypeSupported?: (type: string) => boolean;\n}\n\nfunction resolveMediaRecorderCtor(): MediaRecorderCtor | null {\n if (typeof window === 'undefined') return null;\n return (window as unknown as { MediaRecorder?: MediaRecorderCtor }).MediaRecorder ?? null;\n}\n\n/** First container the recorder accepts, or undefined for the browser default. */\nexport function pickRecorderMimeType(\n Ctor: Pick<MediaRecorderCtor, 'isTypeSupported'>,\n candidates: readonly string[],\n): string | undefined {\n if (!Ctor.isTypeSupported) return undefined;\n return candidates.find((type) => Ctor.isTypeSupported?.(type));\n}\n\nfunction transcriptionError(error: unknown): DictationEngineError {\n return {\n kind: DICTATION_ENGINE_ERROR_KINDS.connection,\n message: error instanceof Error && error.message ? error.message : 'Transcription failed. Try again.',\n };\n}\n\n/**\n * Record-then-transcribe: capture the microphone with `MediaRecorder` while\n * the user talks, then hand the finished clip to `transcribe` on stop. No\n * interim words are shown; the composer shows a \"transcribing\" state until\n * the text lands. Works in every browser with `MediaRecorder`, so the mic is\n * available where Web Speech is not (Firefox) and the quality is whatever the\n * server-side recognizer provides.\n */\nexport function createRecordedDictationEngine(options: RecordedDictationEngineOptions): DictationEngine {\n const {\n transcribe,\n mimeTypes = DEFAULT_MIME_TYPES,\n flushTimeoutMs = DEFAULT_FLUSH_TIMEOUT_MS,\n maxDurationMs = DEFAULT_MAX_RECORDING_MS,\n } = options;\n return {\n id: 'recorded-upload',\n flushTimeoutMs,\n stoppingLabel: 'Transcribing…',\n isSupported: () =>\n resolveMediaRecorderCtor() !== null &&\n typeof navigator !== 'undefined' &&\n typeof navigator.mediaDevices?.getUserMedia === 'function',\n start: ({ stream, lang, callbacks }) => {\n const Ctor = resolveMediaRecorderCtor();\n const noop = { stop: () => undefined, abort: () => undefined };\n if (!Ctor || !stream) {\n callbacks.onError({\n kind: !Ctor ? DICTATION_ENGINE_ERROR_KINDS.unsupported : DICTATION_ENGINE_ERROR_KINDS.permission,\n message: !Ctor ? 'This browser cannot record audio.' : 'No microphone stream was available.',\n });\n callbacks.onEnd();\n return noop;\n }\n\n const mimeType = pickRecorderMimeType(Ctor, mimeTypes);\n let recorder: MediaRecorderLike;\n try {\n recorder = new Ctor(stream, mimeType ? { mimeType } : undefined);\n } catch (error) {\n callbacks.onError({\n kind: DICTATION_ENGINE_ERROR_KINDS.unknown,\n message: error instanceof Error ? error.message : 'Recording could not start.',\n });\n callbacks.onEnd();\n return noop;\n }\n\n const chunks: Blob[] = [];\n const upload = new AbortController();\n let ended = false;\n let aborted = false;\n let limitTimer: ReturnType<typeof setTimeout> | undefined;\n const clearLimit = () => {\n if (limitTimer === undefined) return;\n clearTimeout(limitTimer);\n limitTimer = undefined;\n };\n const end = () => {\n if (ended) return;\n ended = true;\n clearLimit();\n recorder.ondataavailable = null;\n recorder.onstop = null;\n recorder.onerror = null;\n callbacks.onEnd();\n };\n const stopRecorder = () => {\n if (recorder.state === 'inactive') return false;\n try {\n recorder.stop();\n return true;\n } catch {\n return false;\n }\n };\n\n recorder.ondataavailable = (event) => {\n if (event.data && event.data.size > 0) chunks.push(event.data);\n };\n recorder.onerror = () => {\n callbacks.onError({\n kind: DICTATION_ENGINE_ERROR_KINDS.unknown,\n message: 'Recording stopped unexpectedly.',\n });\n end();\n };\n recorder.onstop = () => {\n if (aborted || ended) {\n end();\n return;\n }\n const clip = new Blob(chunks, { type: recorder.mimeType || mimeType || 'audio/webm' });\n if (clip.size === 0) {\n end();\n return;\n }\n void transcribe(clip, { lang, signal: upload.signal })\n .then((text) => {\n if (ended) return;\n const trimmed = text.trim();\n if (trimmed) callbacks.onTranscript({ final: trimmed, interim: '' });\n })\n .catch((error: unknown) => {\n if (ended) return;\n callbacks.onError(transcriptionError(error));\n })\n .finally(end);\n };\n\n try {\n recorder.start();\n } catch (error) {\n callbacks.onError({\n kind: DICTATION_ENGINE_ERROR_KINDS.unknown,\n message: error instanceof Error ? error.message : 'Recording could not start.',\n });\n end();\n return noop;\n }\n\n if (Number.isFinite(maxDurationMs) && maxDurationMs > 0) {\n limitTimer = setTimeout(() => {\n limitTimer = undefined;\n // Same path as the user's Stop: onstop transcribes the clip and the\n // words land in the draft. A recorder that is already inactive is a\n // stop in flight, which must run to completion untouched.\n if (!ended) stopRecorder();\n }, maxDurationMs);\n }\n\n return {\n stop: () => {\n clearLimit();\n if (!stopRecorder()) end();\n },\n abort: () => {\n aborted = true;\n upload.abort();\n stopRecorder();\n end();\n },\n };\n },\n };\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const react = require("react");
4
+ const AthenaContext = require("../provider/AthenaContext.cjs");
5
+ const statewireAuthHeaders = require("../runtime/statewire-auth-headers.cjs");
6
+ const dictationEngine = require("./dictation-engine.cjs");
7
+ const recordedDictationEngine = require("./recorded-dictation-engine.cjs");
8
+ function transcriptionUploadUrl(backendUrl) {
9
+ return `${backendUrl.replace(/\/api\/assistant-ui\/?$/, "").replace(/\/$/, "")}/api/transcription/upload`;
10
+ }
11
+ function transcriptionUploadErrorMessage(status) {
12
+ switch (status) {
13
+ case 401:
14
+ case 403:
15
+ return "Dictation is not available for this sign-in. Reload and try again.";
16
+ case 413:
17
+ return "That recording is too long to transcribe.";
18
+ case 422:
19
+ return "The dictation language was rejected. Use a BCP-47 tag such as en-US.";
20
+ case 429:
21
+ return "Too many dictations in a row. Wait a moment and try again.";
22
+ case 503:
23
+ return "Dictation is not set up on this server.";
24
+ default:
25
+ return `Transcription failed (${status}).`;
26
+ }
27
+ }
28
+ function createAthenaUploadTranscriber(getConfig) {
29
+ return async (clip, { lang, signal }) => {
30
+ const config = getConfig();
31
+ const headers = await statewireAuthHeaders.resolveStatewireAuthHeaders(config, { staleAuth: false });
32
+ const form = new FormData();
33
+ const extension = clip.type.includes("mp4") ? "mp4" : clip.type.includes("ogg") ? "ogg" : "webm";
34
+ form.append("file", clip, `dictation.${extension}`);
35
+ if (lang) form.append("language", lang);
36
+ const response = await fetch(transcriptionUploadUrl(config.backendUrl), {
37
+ method: "POST",
38
+ headers,
39
+ body: form,
40
+ credentials: "include",
41
+ signal
42
+ });
43
+ if (!response.ok) throw new Error(transcriptionUploadErrorMessage(response.status));
44
+ const payload = await response.json();
45
+ return typeof payload.transcript === "string" ? payload.transcript : "";
46
+ };
47
+ }
48
+ function useAthenaDictationEngine(override) {
49
+ const config = AthenaContext.useAthenaConfig();
50
+ const configRef = react.useRef(config);
51
+ configRef.current = config;
52
+ return react.useMemo(() => {
53
+ if (override) return override;
54
+ const recorded = recordedDictationEngine.createRecordedDictationEngine({
55
+ transcribe: createAthenaUploadTranscriber(() => configRef.current)
56
+ });
57
+ return recorded.isSupported() ? recorded : dictationEngine.createWebSpeechDictationEngine();
58
+ }, [override]);
59
+ }
60
+ exports.createAthenaUploadTranscriber = createAthenaUploadTranscriber;
61
+ exports.transcriptionUploadErrorMessage = transcriptionUploadErrorMessage;
62
+ exports.transcriptionUploadUrl = transcriptionUploadUrl;
63
+ exports.useAthenaDictationEngine = useAthenaDictationEngine;
64
+ //# sourceMappingURL=use-athena-dictation-engine.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-athena-dictation-engine.cjs","sources":["../../src/dictation/use-athena-dictation-engine.ts"],"sourcesContent":["import { useMemo, useRef } from 'react';\nimport { type AthenaConfig, useAthenaConfig } from '../provider/AthenaContext';\nimport { resolveStatewireAuthHeaders } from '../runtime/statewire-auth-headers';\nimport { createWebSpeechDictationEngine, type DictationEngine } from './dictation-engine';\nimport { createRecordedDictationEngine, type DictationTranscriber } from './recorded-dictation-engine';\n\ninterface UploadTranscriptionResponse {\n transcript: string;\n}\n\n/** Agora's REST base from the configured backend URL (which may point at a nested endpoint). */\nexport function transcriptionUploadUrl(backendUrl: string): string {\n return `${backendUrl.replace(/\\/api\\/assistant-ui\\/?$/, '').replace(/\\/$/, '')}/api/transcription/upload`;\n}\n\n/**\n * One line for the composer per way `/transcription/upload` refuses a clip.\n * The copy lives here, apart from the fetch, so it can be pinned by a test.\n */\nexport function transcriptionUploadErrorMessage(status: number): string {\n switch (status) {\n case 401:\n case 403:\n return 'Dictation is not available for this sign-in. Reload and try again.';\n case 413:\n return 'That recording is too long to transcribe.';\n case 422:\n return 'The dictation language was rejected. Use a BCP-47 tag such as en-US.';\n case 429:\n return 'Too many dictations in a row. Wait a moment and try again.';\n case 503:\n return 'Dictation is not set up on this server.';\n default:\n return `Transcription failed (${status}).`;\n }\n}\n\n/**\n * Post a finished clip to agora's `/transcription/upload` route, which runs\n * it through Deepgram and returns the text. Credentials are read at call\n * time so a rotated token is picked up. The session language (host `lang`\n * or the document's) is forwarded so non-English speech is recognised as\n * such; the request is dropped when the engine aborts.\n */\nexport function createAthenaUploadTranscriber(\n getConfig: () => Pick<AthenaConfig, 'backendUrl' | 'apiKey' | 'token' | 'getToken'>,\n): DictationTranscriber {\n return async (clip, { lang, signal }) => {\n const config = getConfig();\n const headers = await resolveStatewireAuthHeaders(config, { staleAuth: false });\n const form = new FormData();\n const extension = clip.type.includes('mp4') ? 'mp4' : clip.type.includes('ogg') ? 'ogg' : 'webm';\n form.append('file', clip, `dictation.${extension}`);\n if (lang) form.append('language', lang);\n const response = await fetch(transcriptionUploadUrl(config.backendUrl), {\n method: 'POST',\n headers,\n body: form,\n credentials: 'include',\n signal,\n });\n if (!response.ok) throw new Error(transcriptionUploadErrorMessage(response.status));\n const payload = (await response.json()) as UploadTranscriptionResponse;\n return typeof payload.transcript === 'string' ? payload.transcript : '';\n };\n}\n\n/**\n * The composer's default speech engine: record with `MediaRecorder`, then\n * transcribe the clip through agora (Deepgram). Falls back to the browser's\n * Web Speech API only where `MediaRecorder` is missing. A host-provided\n * `engine` always wins.\n */\nexport function useAthenaDictationEngine(override?: DictationEngine): DictationEngine {\n const config = useAthenaConfig();\n const configRef = useRef(config);\n configRef.current = config;\n return useMemo(() => {\n if (override) return override;\n const recorded = createRecordedDictationEngine({\n transcribe: createAthenaUploadTranscriber(() => configRef.current),\n });\n return recorded.isSupported() ? recorded : createWebSpeechDictationEngine();\n }, [override]);\n}\n"],"names":["resolveStatewireAuthHeaders","useAthenaConfig","useRef","useMemo","createRecordedDictationEngine","createWebSpeechDictationEngine"],"mappings":";;;;;;;AAWO,SAAS,uBAAuB,YAA4B;AACjE,SAAO,GAAG,WAAW,QAAQ,2BAA2B,EAAE,EAAE,QAAQ,OAAO,EAAE,CAAC;AAChF;AAMO,SAAS,gCAAgC,QAAwB;AACtE,UAAQ,QAAA;AAAA,IACN,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO,yBAAyB,MAAM;AAAA,EAAA;AAE5C;AASO,SAAS,8BACd,WACsB;AACtB,SAAO,OAAO,MAAM,EAAE,MAAM,aAAa;AACvC,UAAM,SAAS,UAAA;AACf,UAAM,UAAU,MAAMA,qBAAAA,4BAA4B,QAAQ,EAAE,WAAW,OAAO;AAC9E,UAAM,OAAO,IAAI,SAAA;AACjB,UAAM,YAAY,KAAK,KAAK,SAAS,KAAK,IAAI,QAAQ,KAAK,KAAK,SAAS,KAAK,IAAI,QAAQ;AAC1F,SAAK,OAAO,QAAQ,MAAM,aAAa,SAAS,EAAE;AAClD,QAAI,KAAM,MAAK,OAAO,YAAY,IAAI;AACtC,UAAM,WAAW,MAAM,MAAM,uBAAuB,OAAO,UAAU,GAAG;AAAA,MACtE,QAAQ;AAAA,MACR;AAAA,MACA,MAAM;AAAA,MACN,aAAa;AAAA,MACb;AAAA,IAAA,CACD;AACD,QAAI,CAAC,SAAS,GAAI,OAAM,IAAI,MAAM,gCAAgC,SAAS,MAAM,CAAC;AAClF,UAAM,UAAW,MAAM,SAAS,KAAA;AAChC,WAAO,OAAO,QAAQ,eAAe,WAAW,QAAQ,aAAa;AAAA,EACvE;AACF;AAQO,SAAS,yBAAyB,UAA6C;AACpF,QAAM,SAASC,cAAAA,gBAAA;AACf,QAAM,YAAYC,MAAAA,OAAO,MAAM;AAC/B,YAAU,UAAU;AACpB,SAAOC,MAAAA,QAAQ,MAAM;AACnB,QAAI,SAAU,QAAO;AACrB,UAAM,WAAWC,wBAAAA,8BAA8B;AAAA,MAC7C,YAAY,8BAA8B,MAAM,UAAU,OAAO;AAAA,IAAA,CAClE;AACD,WAAO,SAAS,gBAAgB,WAAWC,gBAAAA,+BAAA;AAAA,EAC7C,GAAG,CAAC,QAAQ,CAAC;AACf;;;;;"}
@@ -0,0 +1,25 @@
1
+ import { type AthenaConfig } from '../provider/AthenaContext';
2
+ import { type DictationEngine } from './dictation-engine';
3
+ import { type DictationTranscriber } from './recorded-dictation-engine';
4
+ /** Agora's REST base from the configured backend URL (which may point at a nested endpoint). */
5
+ export declare function transcriptionUploadUrl(backendUrl: string): string;
6
+ /**
7
+ * One line for the composer per way `/transcription/upload` refuses a clip.
8
+ * The copy lives here, apart from the fetch, so it can be pinned by a test.
9
+ */
10
+ export declare function transcriptionUploadErrorMessage(status: number): string;
11
+ /**
12
+ * Post a finished clip to agora's `/transcription/upload` route, which runs
13
+ * it through Deepgram and returns the text. Credentials are read at call
14
+ * time so a rotated token is picked up. The session language (host `lang`
15
+ * or the document's) is forwarded so non-English speech is recognised as
16
+ * such; the request is dropped when the engine aborts.
17
+ */
18
+ export declare function createAthenaUploadTranscriber(getConfig: () => Pick<AthenaConfig, 'backendUrl' | 'apiKey' | 'token' | 'getToken'>): DictationTranscriber;
19
+ /**
20
+ * The composer's default speech engine: record with `MediaRecorder`, then
21
+ * transcribe the clip through agora (Deepgram). Falls back to the browser's
22
+ * Web Speech API only where `MediaRecorder` is missing. A host-provided
23
+ * `engine` always wins.
24
+ */
25
+ export declare function useAthenaDictationEngine(override?: DictationEngine): DictationEngine;
@@ -0,0 +1,64 @@
1
+ import { useRef, useMemo } from "react";
2
+ import { useAthenaConfig } from "../provider/AthenaContext.js";
3
+ import { resolveStatewireAuthHeaders } from "../runtime/statewire-auth-headers.js";
4
+ import { createWebSpeechDictationEngine } from "./dictation-engine.js";
5
+ import { createRecordedDictationEngine } from "./recorded-dictation-engine.js";
6
+ function transcriptionUploadUrl(backendUrl) {
7
+ return `${backendUrl.replace(/\/api\/assistant-ui\/?$/, "").replace(/\/$/, "")}/api/transcription/upload`;
8
+ }
9
+ function transcriptionUploadErrorMessage(status) {
10
+ switch (status) {
11
+ case 401:
12
+ case 403:
13
+ return "Dictation is not available for this sign-in. Reload and try again.";
14
+ case 413:
15
+ return "That recording is too long to transcribe.";
16
+ case 422:
17
+ return "The dictation language was rejected. Use a BCP-47 tag such as en-US.";
18
+ case 429:
19
+ return "Too many dictations in a row. Wait a moment and try again.";
20
+ case 503:
21
+ return "Dictation is not set up on this server.";
22
+ default:
23
+ return `Transcription failed (${status}).`;
24
+ }
25
+ }
26
+ function createAthenaUploadTranscriber(getConfig) {
27
+ return async (clip, { lang, signal }) => {
28
+ const config = getConfig();
29
+ const headers = await resolveStatewireAuthHeaders(config, { staleAuth: false });
30
+ const form = new FormData();
31
+ const extension = clip.type.includes("mp4") ? "mp4" : clip.type.includes("ogg") ? "ogg" : "webm";
32
+ form.append("file", clip, `dictation.${extension}`);
33
+ if (lang) form.append("language", lang);
34
+ const response = await fetch(transcriptionUploadUrl(config.backendUrl), {
35
+ method: "POST",
36
+ headers,
37
+ body: form,
38
+ credentials: "include",
39
+ signal
40
+ });
41
+ if (!response.ok) throw new Error(transcriptionUploadErrorMessage(response.status));
42
+ const payload = await response.json();
43
+ return typeof payload.transcript === "string" ? payload.transcript : "";
44
+ };
45
+ }
46
+ function useAthenaDictationEngine(override) {
47
+ const config = useAthenaConfig();
48
+ const configRef = useRef(config);
49
+ configRef.current = config;
50
+ return useMemo(() => {
51
+ if (override) return override;
52
+ const recorded = createRecordedDictationEngine({
53
+ transcribe: createAthenaUploadTranscriber(() => configRef.current)
54
+ });
55
+ return recorded.isSupported() ? recorded : createWebSpeechDictationEngine();
56
+ }, [override]);
57
+ }
58
+ export {
59
+ createAthenaUploadTranscriber,
60
+ transcriptionUploadErrorMessage,
61
+ transcriptionUploadUrl,
62
+ useAthenaDictationEngine
63
+ };
64
+ //# sourceMappingURL=use-athena-dictation-engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-athena-dictation-engine.js","sources":["../../src/dictation/use-athena-dictation-engine.ts"],"sourcesContent":["import { useMemo, useRef } from 'react';\nimport { type AthenaConfig, useAthenaConfig } from '../provider/AthenaContext';\nimport { resolveStatewireAuthHeaders } from '../runtime/statewire-auth-headers';\nimport { createWebSpeechDictationEngine, type DictationEngine } from './dictation-engine';\nimport { createRecordedDictationEngine, type DictationTranscriber } from './recorded-dictation-engine';\n\ninterface UploadTranscriptionResponse {\n transcript: string;\n}\n\n/** Agora's REST base from the configured backend URL (which may point at a nested endpoint). */\nexport function transcriptionUploadUrl(backendUrl: string): string {\n return `${backendUrl.replace(/\\/api\\/assistant-ui\\/?$/, '').replace(/\\/$/, '')}/api/transcription/upload`;\n}\n\n/**\n * One line for the composer per way `/transcription/upload` refuses a clip.\n * The copy lives here, apart from the fetch, so it can be pinned by a test.\n */\nexport function transcriptionUploadErrorMessage(status: number): string {\n switch (status) {\n case 401:\n case 403:\n return 'Dictation is not available for this sign-in. Reload and try again.';\n case 413:\n return 'That recording is too long to transcribe.';\n case 422:\n return 'The dictation language was rejected. Use a BCP-47 tag such as en-US.';\n case 429:\n return 'Too many dictations in a row. Wait a moment and try again.';\n case 503:\n return 'Dictation is not set up on this server.';\n default:\n return `Transcription failed (${status}).`;\n }\n}\n\n/**\n * Post a finished clip to agora's `/transcription/upload` route, which runs\n * it through Deepgram and returns the text. Credentials are read at call\n * time so a rotated token is picked up. The session language (host `lang`\n * or the document's) is forwarded so non-English speech is recognised as\n * such; the request is dropped when the engine aborts.\n */\nexport function createAthenaUploadTranscriber(\n getConfig: () => Pick<AthenaConfig, 'backendUrl' | 'apiKey' | 'token' | 'getToken'>,\n): DictationTranscriber {\n return async (clip, { lang, signal }) => {\n const config = getConfig();\n const headers = await resolveStatewireAuthHeaders(config, { staleAuth: false });\n const form = new FormData();\n const extension = clip.type.includes('mp4') ? 'mp4' : clip.type.includes('ogg') ? 'ogg' : 'webm';\n form.append('file', clip, `dictation.${extension}`);\n if (lang) form.append('language', lang);\n const response = await fetch(transcriptionUploadUrl(config.backendUrl), {\n method: 'POST',\n headers,\n body: form,\n credentials: 'include',\n signal,\n });\n if (!response.ok) throw new Error(transcriptionUploadErrorMessage(response.status));\n const payload = (await response.json()) as UploadTranscriptionResponse;\n return typeof payload.transcript === 'string' ? payload.transcript : '';\n };\n}\n\n/**\n * The composer's default speech engine: record with `MediaRecorder`, then\n * transcribe the clip through agora (Deepgram). Falls back to the browser's\n * Web Speech API only where `MediaRecorder` is missing. A host-provided\n * `engine` always wins.\n */\nexport function useAthenaDictationEngine(override?: DictationEngine): DictationEngine {\n const config = useAthenaConfig();\n const configRef = useRef(config);\n configRef.current = config;\n return useMemo(() => {\n if (override) return override;\n const recorded = createRecordedDictationEngine({\n transcribe: createAthenaUploadTranscriber(() => configRef.current),\n });\n return recorded.isSupported() ? recorded : createWebSpeechDictationEngine();\n }, [override]);\n}\n"],"names":[],"mappings":";;;;;AAWO,SAAS,uBAAuB,YAA4B;AACjE,SAAO,GAAG,WAAW,QAAQ,2BAA2B,EAAE,EAAE,QAAQ,OAAO,EAAE,CAAC;AAChF;AAMO,SAAS,gCAAgC,QAAwB;AACtE,UAAQ,QAAA;AAAA,IACN,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO,yBAAyB,MAAM;AAAA,EAAA;AAE5C;AASO,SAAS,8BACd,WACsB;AACtB,SAAO,OAAO,MAAM,EAAE,MAAM,aAAa;AACvC,UAAM,SAAS,UAAA;AACf,UAAM,UAAU,MAAM,4BAA4B,QAAQ,EAAE,WAAW,OAAO;AAC9E,UAAM,OAAO,IAAI,SAAA;AACjB,UAAM,YAAY,KAAK,KAAK,SAAS,KAAK,IAAI,QAAQ,KAAK,KAAK,SAAS,KAAK,IAAI,QAAQ;AAC1F,SAAK,OAAO,QAAQ,MAAM,aAAa,SAAS,EAAE;AAClD,QAAI,KAAM,MAAK,OAAO,YAAY,IAAI;AACtC,UAAM,WAAW,MAAM,MAAM,uBAAuB,OAAO,UAAU,GAAG;AAAA,MACtE,QAAQ;AAAA,MACR;AAAA,MACA,MAAM;AAAA,MACN,aAAa;AAAA,MACb;AAAA,IAAA,CACD;AACD,QAAI,CAAC,SAAS,GAAI,OAAM,IAAI,MAAM,gCAAgC,SAAS,MAAM,CAAC;AAClF,UAAM,UAAW,MAAM,SAAS,KAAA;AAChC,WAAO,OAAO,QAAQ,eAAe,WAAW,QAAQ,aAAa;AAAA,EACvE;AACF;AAQO,SAAS,yBAAyB,UAA6C;AACpF,QAAM,SAAS,gBAAA;AACf,QAAM,YAAY,OAAO,MAAM;AAC/B,YAAU,UAAU;AACpB,SAAO,QAAQ,MAAM;AACnB,QAAI,SAAU,QAAO;AACrB,UAAM,WAAW,8BAA8B;AAAA,MAC7C,YAAY,8BAA8B,MAAM,UAAU,OAAO;AAAA,IAAA,CAClE;AACD,WAAO,SAAS,gBAAgB,WAAW,+BAAA;AAAA,EAC7C,GAAG,CAAC,QAAQ,CAAC;AACf;"}
@@ -4,7 +4,7 @@ const react = require("react");
4
4
  const athenaDiagnostics = require("../diagnostics/athena-diagnostics.cjs");
5
5
  const dictationEngine = require("./dictation-engine.cjs");
6
6
  const dictationState = require("./dictation-state.cjs");
7
- const STOP_FLUSH_TIMEOUT_MS = 1500;
7
+ const DEFAULT_STOP_FLUSH_TIMEOUT_MS = 1500;
8
8
  const EMPTY_TRANSCRIPT = { final: "", interim: "" };
9
9
  function documentLanguage() {
10
10
  if (typeof document !== "undefined" && document.documentElement.lang) {
@@ -36,6 +36,7 @@ function useDictation(options = {}) {
36
36
  const onAutoEndRef = react.useRef(onAutoEnd);
37
37
  onAutoEndRef.current = onAutoEnd;
38
38
  const stopWaitersRef = react.useRef([]);
39
+ const sessionErrorRef = react.useRef(null);
39
40
  const generationRef = react.useRef(0);
40
41
  react.useEffect(() => {
41
42
  setIsSupported(engine.isSupported());
@@ -43,7 +44,8 @@ function useDictation(options = {}) {
43
44
  const settleWaiters = react.useCallback((spoken) => {
44
45
  const waiters = stopWaitersRef.current;
45
46
  stopWaitersRef.current = [];
46
- for (const resolve of waiters) resolve(spoken);
47
+ const result = { text: spoken, error: sessionErrorRef.current };
48
+ for (const resolve of waiters) resolve(result);
47
49
  }, []);
48
50
  const releaseSession = react.useCallback(() => {
49
51
  sessionRef.current = null;
@@ -65,6 +67,7 @@ function useDictation(options = {}) {
65
67
  level: "warn",
66
68
  data: { phase: "error", engine: engine.id, kind: engineError.kind }
67
69
  });
70
+ sessionErrorRef.current = engineError;
68
71
  dispatch({ type: "error", error: engineError });
69
72
  },
70
73
  [engine.id]
@@ -74,6 +77,7 @@ function useDictation(options = {}) {
74
77
  const generation = generationRef.current + 1;
75
78
  generationRef.current = generation;
76
79
  transcriptRef.current = EMPTY_TRANSCRIPT;
80
+ sessionErrorRef.current = null;
77
81
  dispatch({ type: "start" });
78
82
  athenaDiagnostics.athenaDiagnostics.emit("composer.dictation", {
79
83
  level: "debug",
@@ -162,25 +166,31 @@ function useDictation(options = {}) {
162
166
  if (stateRef.current.status === dictationState.DICTATION_STATUSES.starting) {
163
167
  teardown();
164
168
  dispatch({ type: "cancel" });
165
- return Promise.resolve("");
169
+ return Promise.resolve({ text: "", error: null });
166
170
  }
167
- return Promise.resolve(dictationState.spokenText(transcriptRef.current));
171
+ return Promise.resolve({ text: dictationState.spokenText(transcriptRef.current), error: sessionErrorRef.current });
168
172
  }
169
173
  dispatch({ type: "stop" });
170
174
  const flushed = new Promise((resolve) => {
171
175
  stopWaitersRef.current.push(resolve);
172
176
  window.setTimeout(() => {
173
177
  if (sessionRef.current === session) {
178
+ if (!dictationState.spokenText(transcriptRef.current)) {
179
+ reportEngineError({
180
+ kind: dictationEngine.DICTATION_ENGINE_ERROR_KINDS.connection,
181
+ message: "Transcription timed out. Try again."
182
+ });
183
+ }
174
184
  session.abort();
175
185
  dispatch({ type: "ended" });
176
186
  releaseSession();
177
187
  settleWaiters(dictationState.spokenText(transcriptRef.current));
178
188
  }
179
- }, STOP_FLUSH_TIMEOUT_MS);
189
+ }, engine.flushTimeoutMs ?? DEFAULT_STOP_FLUSH_TIMEOUT_MS);
180
190
  });
181
191
  session.stop();
182
192
  return flushed;
183
- }, [releaseSession, settleWaiters, teardown]);
193
+ }, [engine.flushTimeoutMs, releaseSession, reportEngineError, settleWaiters, teardown]);
184
194
  const cancel = react.useCallback(() => {
185
195
  teardown();
186
196
  dispatch({ type: "cancel" });
@@ -195,6 +205,7 @@ function useDictation(options = {}) {
195
205
  error: state.error,
196
206
  stream,
197
207
  isSupported,
208
+ engine,
198
209
  start,
199
210
  stop,
200
211
  cancel
@@ -1 +1 @@
1
- {"version":3,"file":"use-dictation.cjs","sources":["../../src/dictation/use-dictation.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';\nimport { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\nimport {\n createWebSpeechDictationEngine,\n DICTATION_ENGINE_ERROR_KINDS,\n type DictationEngine,\n type DictationEngineError,\n type DictationEngineSession,\n} from './dictation-engine';\nimport {\n DICTATION_STATUSES,\n dictationReducer,\n INITIAL_DICTATION_STATE,\n joinSpeech,\n spokenText,\n type DictationStatus,\n type DictationTranscript,\n} from './dictation-state';\n\nexport interface UseDictationOptions {\n /** Speech engine. Defaults to the browser's Web Speech API. */\n engine?: DictationEngine;\n /** BCP-47 language tag. Defaults to the document language, else `en-US`. */\n lang?: string;\n /**\n * Open the microphone through `getUserMedia` so callers get a `stream` for\n * a waveform. Web Speech opens its own capture; this second capture is\n * what the level meter reads. Defaults to true.\n */\n captureStream?: boolean;\n /**\n * The engine ended on its own (silence window, recognizer timeout) while\n * listening, without `stop()` or `cancel()`. Receives the spoken text so\n * the words are not lost; the composer puts them into the draft.\n */\n onAutoEnd?: (spoken: string) => void;\n}\n\nexport interface DictationController {\n status: DictationStatus;\n /** True while starting, listening or stopping. */\n isActive: boolean;\n transcript: DictationTranscript;\n /** `final` plus the interim tail, ready to hand to the composer. */\n text: string;\n error: DictationEngineError | null;\n /** The microphone stream while active (for a waveform), else null. */\n stream: MediaStream | null;\n /** Whether the engine can run here. Hide the control when false. */\n isSupported: boolean;\n start: () => void;\n /**\n * Finish recording. Resolves with the spoken text once the engine has\n * flushed (bounded wait), so trailing words are not lost.\n */\n stop: () => Promise<string>;\n /** Discard the session and its transcript. */\n cancel: () => void;\n}\n\n/** How long `stop()` waits for the engine's final flush before giving up. */\nconst STOP_FLUSH_TIMEOUT_MS = 1500;\n\ntype StopResolver = (spoken: string) => void;\n\nconst EMPTY_TRANSCRIPT: DictationTranscript = { final: '', interim: '' };\n\nfunction documentLanguage(): string {\n if (typeof document !== 'undefined' && document.documentElement.lang) {\n return document.documentElement.lang;\n }\n if (typeof navigator !== 'undefined' && navigator.language) return navigator.language;\n return 'en-US';\n}\n\nasync function openMicrophone(): Promise<MediaStream | null> {\n if (typeof navigator === 'undefined' || !navigator.mediaDevices?.getUserMedia) return null;\n return navigator.mediaDevices.getUserMedia({ audio: true });\n}\n\nfunction releaseStream(stream: MediaStream | null): void {\n if (!stream) return;\n for (const track of stream.getTracks()) track.stop();\n}\n\n/**\n * Drive a dictation session: microphone, speech engine, and the transcript\n * state machine. The composer renders the waveform from `stream` and the\n * live caption from `transcript`; on `stop()` it appends `text` to the draft.\n */\nexport function useDictation(options: UseDictationOptions = {}): DictationController {\n const engine = useMemo(() => options.engine ?? createWebSpeechDictationEngine(), [options.engine]);\n const { lang, captureStream = true, onAutoEnd } = options;\n const [state, dispatch] = useReducer(dictationReducer, INITIAL_DICTATION_STATE);\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [isSupported, setIsSupported] = useState(false);\n\n const sessionRef = useRef<DictationEngineSession | null>(null);\n const streamRef = useRef<MediaStream | null>(null);\n const stateRef = useRef(state);\n stateRef.current = state;\n // The authoritative transcript, updated synchronously as the engine reports\n // it. The reducer's copy renders the caption; this one is what `stop()` and\n // `onAutoEnd` hand over, so an engine that flushes its last words and ends\n // in the same tick — before React has rendered — loses nothing.\n const transcriptRef = useRef<DictationTranscript>(EMPTY_TRANSCRIPT);\n const onAutoEndRef = useRef(onAutoEnd);\n onAutoEndRef.current = onAutoEnd;\n // stop() callers waiting for the engine's final flush.\n const stopWaitersRef = useRef<StopResolver[]>([]);\n // Distinguishes a fresh session from the callbacks of a torn-down one.\n const generationRef = useRef(0);\n\n useEffect(() => {\n setIsSupported(engine.isSupported());\n }, [engine]);\n\n const settleWaiters = useCallback((spoken: string) => {\n const waiters = stopWaitersRef.current;\n stopWaitersRef.current = [];\n for (const resolve of waiters) resolve(spoken);\n }, []);\n\n const releaseSession = useCallback(() => {\n sessionRef.current = null;\n releaseStream(streamRef.current);\n streamRef.current = null;\n setStream(null);\n }, []);\n\n const teardown = useCallback(() => {\n generationRef.current += 1;\n sessionRef.current?.abort();\n releaseSession();\n transcriptRef.current = EMPTY_TRANSCRIPT;\n settleWaiters('');\n }, [releaseSession, settleWaiters]);\n\n const reportEngineError = useCallback(\n (engineError: DictationEngineError) => {\n athenaDiagnostics.emit('composer.dictation', {\n level: 'warn',\n data: { phase: 'error', engine: engine.id, kind: engineError.kind },\n });\n dispatch({ type: 'error', error: engineError });\n },\n [engine.id],\n );\n\n const start = useCallback(() => {\n if (sessionRef.current) return;\n const generation = generationRef.current + 1;\n generationRef.current = generation;\n transcriptRef.current = EMPTY_TRANSCRIPT;\n dispatch({ type: 'start' });\n athenaDiagnostics.emit('composer.dictation', {\n level: 'debug',\n data: { phase: 'start', engine: engine.id },\n });\n\n const run = async () => {\n let microphone: MediaStream | null = null;\n if (captureStream) {\n try {\n microphone = await openMicrophone();\n } catch (error) {\n if (generationRef.current !== generation) {\n releaseStream(microphone);\n return;\n }\n dispatch({\n type: 'error',\n error: {\n kind: DICTATION_ENGINE_ERROR_KINDS.permission,\n message:\n error instanceof DOMException && error.name === 'NotFoundError'\n ? 'No microphone was found.'\n : 'Microphone access was denied. Allow the microphone for this site and try again.',\n },\n });\n return;\n }\n }\n if (generationRef.current !== generation) {\n releaseStream(microphone);\n return;\n }\n streamRef.current = microphone;\n setStream(microphone);\n\n // An engine may fail synchronously inside start() (Web Speech throws\n // when recognition.start() is refused) and run onError/onEnd before we\n // hold the session. Those callbacks flip this flag so a dead session is\n // never stored, which would make every later mic click return early.\n let terminated = false;\n const endSession = () => {\n terminated = true;\n const current = stateRef.current;\n const spoken = spokenText(transcriptRef.current);\n // No stop() is waiting: the engine gave up on its own. Hand the\n // words over so they land in the draft instead of vanishing.\n const endedOnItsOwn =\n stopWaitersRef.current.length === 0 && current.status === DICTATION_STATUSES.listening;\n // The reducer keeps an error status through 'ended', so an error\n // reported just before the engine closed stays on screen.\n dispatch({ type: 'ended' });\n releaseSession();\n settleWaiters(spoken);\n if (endedOnItsOwn) onAutoEndRef.current?.(spoken);\n };\n let session: DictationEngineSession;\n try {\n session = engine.start({\n stream: microphone,\n lang: lang ?? documentLanguage(),\n callbacks: {\n onTranscript: (update) => {\n if (generationRef.current !== generation) return;\n transcriptRef.current = {\n final: joinSpeech(transcriptRef.current.final, update.final),\n interim: update.interim,\n };\n dispatch({ type: 'transcript', update });\n },\n onError: (engineError) => {\n if (generationRef.current !== generation) return;\n // A silence timeout is the engine giving up politely; keep the\n // words we have instead of showing an error.\n if (engineError.kind === DICTATION_ENGINE_ERROR_KINDS.noSpeech) return;\n reportEngineError(engineError);\n },\n onEnd: () => {\n if (generationRef.current !== generation) return;\n endSession();\n },\n },\n });\n } catch (error) {\n // A relay engine that cannot build its session must not strand the\n // microphone or leave the composer in `starting`.\n releaseSession();\n reportEngineError({\n kind: DICTATION_ENGINE_ERROR_KINDS.connection,\n message: error instanceof Error ? error.message : 'Dictation could not start.',\n });\n return;\n }\n if (terminated) return;\n sessionRef.current = session;\n dispatch({ type: 'listening' });\n };\n void run();\n }, [captureStream, engine, lang, releaseSession, reportEngineError, settleWaiters]);\n\n const stop = useCallback((): Promise<string> => {\n const session = sessionRef.current;\n if (!session) {\n // Stopped while the microphone prompt was still open: nothing was heard\n // yet, and the pending start must not bring the engine up afterwards.\n if (stateRef.current.status === DICTATION_STATUSES.starting) {\n teardown();\n dispatch({ type: 'cancel' });\n return Promise.resolve('');\n }\n return Promise.resolve(spokenText(transcriptRef.current));\n }\n dispatch({ type: 'stop' });\n const flushed = new Promise<string>((resolve) => {\n stopWaitersRef.current.push(resolve);\n window.setTimeout(() => {\n // Bounded wait: a recognizer that never fires onend must not hold the\n // composer hostage. Force the session closed and hand over what was\n // heard so far.\n if (sessionRef.current === session) {\n session.abort();\n dispatch({ type: 'ended' });\n releaseSession();\n settleWaiters(spokenText(transcriptRef.current));\n }\n }, STOP_FLUSH_TIMEOUT_MS);\n });\n session.stop();\n return flushed;\n }, [releaseSession, settleWaiters, teardown]);\n\n const cancel = useCallback(() => {\n teardown();\n dispatch({ type: 'cancel' });\n }, [teardown]);\n\n // Release the microphone with the component.\n useEffect(() => () => teardown(), [teardown]);\n\n const isActive =\n state.status === DICTATION_STATUSES.starting ||\n state.status === DICTATION_STATUSES.listening ||\n state.status === DICTATION_STATUSES.stopping;\n\n return {\n status: state.status,\n isActive,\n transcript: state.transcript,\n text: spokenText(state.transcript),\n error: state.error,\n stream,\n isSupported,\n start,\n stop,\n cancel,\n };\n}\n"],"names":["useMemo","createWebSpeechDictationEngine","useReducer","dictationReducer","INITIAL_DICTATION_STATE","useState","useRef","useEffect","useCallback","athenaDiagnostics","DICTATION_ENGINE_ERROR_KINDS","spokenText","DICTATION_STATUSES","joinSpeech"],"mappings":"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"}
1
+ {"version":3,"file":"use-dictation.cjs","sources":["../../src/dictation/use-dictation.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';\nimport { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\nimport {\n createWebSpeechDictationEngine,\n DICTATION_ENGINE_ERROR_KINDS,\n type DictationEngine,\n type DictationEngineError,\n type DictationEngineSession,\n} from './dictation-engine';\nimport {\n DICTATION_STATUSES,\n dictationReducer,\n INITIAL_DICTATION_STATE,\n joinSpeech,\n spokenText,\n type DictationStatus,\n type DictationTranscript,\n} from './dictation-state';\n\nexport interface UseDictationOptions {\n /**\n * Speech engine. Defaults to the browser's Web Speech API; `AthenaChat`\n * passes the record-then-transcribe engine from `useAthenaDictationEngine`.\n */\n engine?: DictationEngine;\n /** BCP-47 language tag. Defaults to the document language, else `en-US`. */\n lang?: string;\n /**\n * Open the microphone through `getUserMedia` so callers get a `stream` for\n * a waveform. Web Speech opens its own capture; this second capture is\n * what the level meter reads. Defaults to true.\n */\n captureStream?: boolean;\n /**\n * The engine ended on its own (silence window, recognizer timeout) while\n * listening, without `stop()` or `cancel()`. Receives the spoken text so\n * the words are not lost; the composer puts them into the draft.\n */\n onAutoEnd?: (spoken: string) => void;\n}\n\nexport interface DictationStopResult {\n /** Spoken text (final plus interim tail) gathered before the engine closed. */\n text: string;\n /**\n * The engine error reported during this session, if any. When set the\n * transcript may be incomplete: a recorded engine whose upload failed has no\n * words to hand over, so callers should not send the draft as if it were.\n */\n error: DictationEngineError | null;\n}\n\nexport interface DictationController {\n status: DictationStatus;\n /** True while starting, listening or stopping. */\n isActive: boolean;\n transcript: DictationTranscript;\n /** `final` plus the interim tail, ready to hand to the composer. */\n text: string;\n error: DictationEngineError | null;\n /** The microphone stream while active (for a waveform), else null. */\n stream: MediaStream | null;\n /** Whether the engine can run here. Hide the control when false. */\n isSupported: boolean;\n /** The engine in use (for its `stoppingLabel` and diagnostics). */\n engine: DictationEngine;\n start: () => void;\n /**\n * Finish recording. Resolves once the engine has flushed (bounded wait), so\n * trailing words are not lost, with the spoken text and any error raised\n * along the way.\n */\n stop: () => Promise<DictationStopResult>;\n /** Discard the session and its transcript. */\n cancel: () => void;\n}\n\n/** How long `stop()` waits for a live engine's final flush before giving up. */\nconst DEFAULT_STOP_FLUSH_TIMEOUT_MS = 1500;\n\ntype StopResolver = (result: DictationStopResult) => void;\n\nconst EMPTY_TRANSCRIPT: DictationTranscript = { final: '', interim: '' };\n\nfunction documentLanguage(): string {\n if (typeof document !== 'undefined' && document.documentElement.lang) {\n return document.documentElement.lang;\n }\n if (typeof navigator !== 'undefined' && navigator.language) return navigator.language;\n return 'en-US';\n}\n\nasync function openMicrophone(): Promise<MediaStream | null> {\n if (typeof navigator === 'undefined' || !navigator.mediaDevices?.getUserMedia) return null;\n return navigator.mediaDevices.getUserMedia({ audio: true });\n}\n\nfunction releaseStream(stream: MediaStream | null): void {\n if (!stream) return;\n for (const track of stream.getTracks()) track.stop();\n}\n\n/**\n * Drive a dictation session: microphone, speech engine, and the transcript\n * state machine. The composer renders the waveform from `stream` and the\n * live caption from `transcript`; on `stop()` it appends `text` to the draft.\n */\nexport function useDictation(options: UseDictationOptions = {}): DictationController {\n const engine = useMemo(() => options.engine ?? createWebSpeechDictationEngine(), [options.engine]);\n const { lang, captureStream = true, onAutoEnd } = options;\n const [state, dispatch] = useReducer(dictationReducer, INITIAL_DICTATION_STATE);\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [isSupported, setIsSupported] = useState(false);\n\n const sessionRef = useRef<DictationEngineSession | null>(null);\n const streamRef = useRef<MediaStream | null>(null);\n const stateRef = useRef(state);\n stateRef.current = state;\n // The authoritative transcript, updated synchronously as the engine reports\n // it. The reducer's copy renders the caption; this one is what `stop()` and\n // `onAutoEnd` hand over, so an engine that flushes its last words and ends\n // in the same tick — before React has rendered — loses nothing.\n const transcriptRef = useRef<DictationTranscript>(EMPTY_TRANSCRIPT);\n const onAutoEndRef = useRef(onAutoEnd);\n onAutoEndRef.current = onAutoEnd;\n // stop() callers waiting for the engine's final flush.\n const stopWaitersRef = useRef<StopResolver[]>([]);\n // Last engine error of the current session, handed to stop() callers\n // synchronously (the reducer's copy may not have rendered yet).\n const sessionErrorRef = useRef<DictationEngineError | null>(null);\n // Distinguishes a fresh session from the callbacks of a torn-down one.\n const generationRef = useRef(0);\n\n useEffect(() => {\n setIsSupported(engine.isSupported());\n }, [engine]);\n\n const settleWaiters = useCallback((spoken: string) => {\n const waiters = stopWaitersRef.current;\n stopWaitersRef.current = [];\n const result: DictationStopResult = { text: spoken, error: sessionErrorRef.current };\n for (const resolve of waiters) resolve(result);\n }, []);\n\n const releaseSession = useCallback(() => {\n sessionRef.current = null;\n releaseStream(streamRef.current);\n streamRef.current = null;\n setStream(null);\n }, []);\n\n const teardown = useCallback(() => {\n generationRef.current += 1;\n sessionRef.current?.abort();\n releaseSession();\n transcriptRef.current = EMPTY_TRANSCRIPT;\n settleWaiters('');\n }, [releaseSession, settleWaiters]);\n\n const reportEngineError = useCallback(\n (engineError: DictationEngineError) => {\n athenaDiagnostics.emit('composer.dictation', {\n level: 'warn',\n data: { phase: 'error', engine: engine.id, kind: engineError.kind },\n });\n sessionErrorRef.current = engineError;\n dispatch({ type: 'error', error: engineError });\n },\n [engine.id],\n );\n\n const start = useCallback(() => {\n if (sessionRef.current) return;\n const generation = generationRef.current + 1;\n generationRef.current = generation;\n transcriptRef.current = EMPTY_TRANSCRIPT;\n sessionErrorRef.current = null;\n dispatch({ type: 'start' });\n athenaDiagnostics.emit('composer.dictation', {\n level: 'debug',\n data: { phase: 'start', engine: engine.id },\n });\n\n const run = async () => {\n let microphone: MediaStream | null = null;\n if (captureStream) {\n try {\n microphone = await openMicrophone();\n } catch (error) {\n if (generationRef.current !== generation) {\n releaseStream(microphone);\n return;\n }\n dispatch({\n type: 'error',\n error: {\n kind: DICTATION_ENGINE_ERROR_KINDS.permission,\n message:\n error instanceof DOMException && error.name === 'NotFoundError'\n ? 'No microphone was found.'\n : 'Microphone access was denied. Allow the microphone for this site and try again.',\n },\n });\n return;\n }\n }\n if (generationRef.current !== generation) {\n releaseStream(microphone);\n return;\n }\n streamRef.current = microphone;\n setStream(microphone);\n\n // An engine may fail synchronously inside start() (Web Speech throws\n // when recognition.start() is refused) and run onError/onEnd before we\n // hold the session. Those callbacks flip this flag so a dead session is\n // never stored, which would make every later mic click return early.\n let terminated = false;\n const endSession = () => {\n terminated = true;\n const current = stateRef.current;\n const spoken = spokenText(transcriptRef.current);\n // No stop() is waiting: the engine gave up on its own. Hand the\n // words over so they land in the draft instead of vanishing.\n const endedOnItsOwn =\n stopWaitersRef.current.length === 0 && current.status === DICTATION_STATUSES.listening;\n // The reducer keeps an error status through 'ended', so an error\n // reported just before the engine closed stays on screen.\n dispatch({ type: 'ended' });\n releaseSession();\n settleWaiters(spoken);\n if (endedOnItsOwn) onAutoEndRef.current?.(spoken);\n };\n let session: DictationEngineSession;\n try {\n session = engine.start({\n stream: microphone,\n lang: lang ?? documentLanguage(),\n callbacks: {\n onTranscript: (update) => {\n if (generationRef.current !== generation) return;\n transcriptRef.current = {\n final: joinSpeech(transcriptRef.current.final, update.final),\n interim: update.interim,\n };\n dispatch({ type: 'transcript', update });\n },\n onError: (engineError) => {\n if (generationRef.current !== generation) return;\n // A silence timeout is the engine giving up politely; keep the\n // words we have instead of showing an error.\n if (engineError.kind === DICTATION_ENGINE_ERROR_KINDS.noSpeech) return;\n reportEngineError(engineError);\n },\n onEnd: () => {\n if (generationRef.current !== generation) return;\n endSession();\n },\n },\n });\n } catch (error) {\n // A relay engine that cannot build its session must not strand the\n // microphone or leave the composer in `starting`.\n releaseSession();\n reportEngineError({\n kind: DICTATION_ENGINE_ERROR_KINDS.connection,\n message: error instanceof Error ? error.message : 'Dictation could not start.',\n });\n return;\n }\n if (terminated) return;\n sessionRef.current = session;\n dispatch({ type: 'listening' });\n };\n void run();\n }, [captureStream, engine, lang, releaseSession, reportEngineError, settleWaiters]);\n\n const stop = useCallback((): Promise<DictationStopResult> => {\n const session = sessionRef.current;\n if (!session) {\n // Stopped while the microphone prompt was still open: nothing was heard\n // yet, and the pending start must not bring the engine up afterwards.\n if (stateRef.current.status === DICTATION_STATUSES.starting) {\n teardown();\n dispatch({ type: 'cancel' });\n return Promise.resolve({ text: '', error: null });\n }\n return Promise.resolve({ text: spokenText(transcriptRef.current), error: sessionErrorRef.current });\n }\n dispatch({ type: 'stop' });\n const flushed = new Promise<DictationStopResult>((resolve) => {\n stopWaitersRef.current.push(resolve);\n window.setTimeout(() => {\n // Bounded wait: a recognizer that never fires onend must not hold the\n // composer hostage. Force the session closed and hand over what was\n // heard so far. An engine that had nothing to show yet (a recorded\n // clip still transcribing) surfaces that as an error rather than\n // passing off silence as the result.\n if (sessionRef.current === session) {\n // Report before abort(): an engine that ends synchronously on\n // abort settles the waiters, and they must see this error.\n if (!spokenText(transcriptRef.current)) {\n reportEngineError({\n kind: DICTATION_ENGINE_ERROR_KINDS.connection,\n message: 'Transcription timed out. Try again.',\n });\n }\n session.abort();\n dispatch({ type: 'ended' });\n releaseSession();\n settleWaiters(spokenText(transcriptRef.current));\n }\n }, engine.flushTimeoutMs ?? DEFAULT_STOP_FLUSH_TIMEOUT_MS);\n });\n session.stop();\n return flushed;\n }, [engine.flushTimeoutMs, releaseSession, reportEngineError, settleWaiters, teardown]);\n\n const cancel = useCallback(() => {\n teardown();\n dispatch({ type: 'cancel' });\n }, [teardown]);\n\n // Release the microphone with the component.\n useEffect(() => () => teardown(), [teardown]);\n\n const isActive =\n state.status === DICTATION_STATUSES.starting ||\n state.status === DICTATION_STATUSES.listening ||\n state.status === DICTATION_STATUSES.stopping;\n\n return {\n status: state.status,\n isActive,\n transcript: state.transcript,\n text: spokenText(state.transcript),\n error: state.error,\n stream,\n isSupported,\n engine,\n start,\n stop,\n cancel,\n };\n}\n"],"names":["useMemo","createWebSpeechDictationEngine","useReducer","dictationReducer","INITIAL_DICTATION_STATE","useState","useRef","useEffect","useCallback","athenaDiagnostics","DICTATION_ENGINE_ERROR_KINDS","spokenText","DICTATION_STATUSES","joinSpeech"],"mappings":"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"}
@@ -1,7 +1,10 @@
1
1
  import { type DictationEngine, type DictationEngineError } from './dictation-engine';
2
2
  import { type DictationStatus, type DictationTranscript } from './dictation-state';
3
3
  export interface UseDictationOptions {
4
- /** Speech engine. Defaults to the browser's Web Speech API. */
4
+ /**
5
+ * Speech engine. Defaults to the browser's Web Speech API; `AthenaChat`
6
+ * passes the record-then-transcribe engine from `useAthenaDictationEngine`.
7
+ */
5
8
  engine?: DictationEngine;
6
9
  /** BCP-47 language tag. Defaults to the document language, else `en-US`. */
7
10
  lang?: string;
@@ -18,6 +21,16 @@ export interface UseDictationOptions {
18
21
  */
19
22
  onAutoEnd?: (spoken: string) => void;
20
23
  }
24
+ export interface DictationStopResult {
25
+ /** Spoken text (final plus interim tail) gathered before the engine closed. */
26
+ text: string;
27
+ /**
28
+ * The engine error reported during this session, if any. When set the
29
+ * transcript may be incomplete: a recorded engine whose upload failed has no
30
+ * words to hand over, so callers should not send the draft as if it were.
31
+ */
32
+ error: DictationEngineError | null;
33
+ }
21
34
  export interface DictationController {
22
35
  status: DictationStatus;
23
36
  /** True while starting, listening or stopping. */
@@ -30,12 +43,15 @@ export interface DictationController {
30
43
  stream: MediaStream | null;
31
44
  /** Whether the engine can run here. Hide the control when false. */
32
45
  isSupported: boolean;
46
+ /** The engine in use (for its `stoppingLabel` and diagnostics). */
47
+ engine: DictationEngine;
33
48
  start: () => void;
34
49
  /**
35
- * Finish recording. Resolves with the spoken text once the engine has
36
- * flushed (bounded wait), so trailing words are not lost.
50
+ * Finish recording. Resolves once the engine has flushed (bounded wait), so
51
+ * trailing words are not lost, with the spoken text and any error raised
52
+ * along the way.
37
53
  */
38
- stop: () => Promise<string>;
54
+ stop: () => Promise<DictationStopResult>;
39
55
  /** Discard the session and its transcript. */
40
56
  cancel: () => void;
41
57
  }