@agent-native/toolkit 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/README.md +17 -1
  2. package/dist/chat-history/ChatHistoryList.d.ts +103 -0
  3. package/dist/chat-history/ChatHistoryList.d.ts.map +1 -0
  4. package/dist/chat-history/ChatHistoryList.js +109 -0
  5. package/dist/chat-history/ChatHistoryList.js.map +1 -0
  6. package/dist/chat-history/ChatHistoryList.spec.d.ts +2 -0
  7. package/dist/chat-history/ChatHistoryList.spec.d.ts.map +1 -0
  8. package/dist/chat-history/ChatHistoryList.spec.js +283 -0
  9. package/dist/chat-history/ChatHistoryList.spec.js.map +1 -0
  10. package/dist/chat-history/index.d.ts +2 -0
  11. package/dist/chat-history/index.d.ts.map +1 -0
  12. package/dist/chat-history/index.js +2 -0
  13. package/dist/chat-history/index.js.map +1 -0
  14. package/dist/chat-history.css +314 -0
  15. package/dist/composer/AgentComposerFrame.d.ts +19 -0
  16. package/dist/composer/AgentComposerFrame.d.ts.map +1 -0
  17. package/dist/composer/AgentComposerFrame.js +14 -0
  18. package/dist/composer/AgentComposerFrame.js.map +1 -0
  19. package/dist/composer/ComposerPlusMenu.d.ts +17 -0
  20. package/dist/composer/ComposerPlusMenu.d.ts.map +1 -0
  21. package/dist/composer/ComposerPlusMenu.js +501 -0
  22. package/dist/composer/ComposerPlusMenu.js.map +1 -0
  23. package/dist/composer/MentionPopover.d.ts +29 -0
  24. package/dist/composer/MentionPopover.d.ts.map +1 -0
  25. package/dist/composer/MentionPopover.js +180 -0
  26. package/dist/composer/MentionPopover.js.map +1 -0
  27. package/dist/composer/PastedTextChip.d.ts +9 -0
  28. package/dist/composer/PastedTextChip.d.ts.map +1 -0
  29. package/dist/composer/PastedTextChip.js +47 -0
  30. package/dist/composer/PastedTextChip.js.map +1 -0
  31. package/dist/composer/PromptComposer.d.ts +114 -0
  32. package/dist/composer/PromptComposer.d.ts.map +1 -0
  33. package/dist/composer/PromptComposer.js +302 -0
  34. package/dist/composer/PromptComposer.js.map +1 -0
  35. package/dist/composer/PromptComposer.spec.d.ts +2 -0
  36. package/dist/composer/PromptComposer.spec.d.ts.map +1 -0
  37. package/dist/composer/PromptComposer.spec.js +64 -0
  38. package/dist/composer/PromptComposer.spec.js.map +1 -0
  39. package/dist/composer/RealtimeVoiceMode.d.ts +104 -0
  40. package/dist/composer/RealtimeVoiceMode.d.ts.map +1 -0
  41. package/dist/composer/RealtimeVoiceMode.js +245 -0
  42. package/dist/composer/RealtimeVoiceMode.js.map +1 -0
  43. package/dist/composer/RealtimeVoiceMode.spec.d.ts +2 -0
  44. package/dist/composer/RealtimeVoiceMode.spec.d.ts.map +1 -0
  45. package/dist/composer/RealtimeVoiceMode.spec.js +439 -0
  46. package/dist/composer/RealtimeVoiceMode.spec.js.map +1 -0
  47. package/dist/composer/TiptapComposer.d.ts +221 -0
  48. package/dist/composer/TiptapComposer.d.ts.map +1 -0
  49. package/dist/composer/TiptapComposer.js +1838 -0
  50. package/dist/composer/TiptapComposer.js.map +1 -0
  51. package/dist/composer/TiptapComposer.spec.d.ts +2 -0
  52. package/dist/composer/TiptapComposer.spec.d.ts.map +1 -0
  53. package/dist/composer/TiptapComposer.spec.js +277 -0
  54. package/dist/composer/TiptapComposer.spec.js.map +1 -0
  55. package/dist/composer/VoiceButton.d.ts +25 -0
  56. package/dist/composer/VoiceButton.d.ts.map +1 -0
  57. package/dist/composer/VoiceButton.js +100 -0
  58. package/dist/composer/VoiceButton.js.map +1 -0
  59. package/dist/composer/VoiceButton.spec.d.ts +2 -0
  60. package/dist/composer/VoiceButton.spec.d.ts.map +1 -0
  61. package/dist/composer/VoiceButton.spec.js +19 -0
  62. package/dist/composer/VoiceButton.spec.js.map +1 -0
  63. package/dist/composer/asset-picker-url.d.ts +16 -0
  64. package/dist/composer/asset-picker-url.d.ts.map +1 -0
  65. package/dist/composer/asset-picker-url.js +64 -0
  66. package/dist/composer/asset-picker-url.js.map +1 -0
  67. package/dist/composer/asset-picker-url.spec.d.ts +2 -0
  68. package/dist/composer/asset-picker-url.spec.d.ts.map +1 -0
  69. package/dist/composer/asset-picker-url.spec.js +23 -0
  70. package/dist/composer/asset-picker-url.spec.js.map +1 -0
  71. package/dist/composer/attachment-accept.d.ts +9 -0
  72. package/dist/composer/attachment-accept.d.ts.map +1 -0
  73. package/dist/composer/attachment-accept.js +65 -0
  74. package/dist/composer/attachment-accept.js.map +1 -0
  75. package/dist/composer/attachment-accept.spec.d.ts +2 -0
  76. package/dist/composer/attachment-accept.spec.d.ts.map +1 -0
  77. package/dist/composer/attachment-accept.spec.js +15 -0
  78. package/dist/composer/attachment-accept.spec.js.map +1 -0
  79. package/dist/composer/draft-key.d.ts +2 -0
  80. package/dist/composer/draft-key.d.ts.map +1 -0
  81. package/dist/composer/draft-key.js +8 -0
  82. package/dist/composer/draft-key.js.map +1 -0
  83. package/dist/composer/draft-key.spec.d.ts +2 -0
  84. package/dist/composer/draft-key.spec.d.ts.map +1 -0
  85. package/dist/composer/draft-key.spec.js +13 -0
  86. package/dist/composer/draft-key.spec.js.map +1 -0
  87. package/dist/composer/extensions/FileReference.d.ts +3 -0
  88. package/dist/composer/extensions/FileReference.d.ts.map +1 -0
  89. package/dist/composer/extensions/FileReference.js +36 -0
  90. package/dist/composer/extensions/FileReference.js.map +1 -0
  91. package/dist/composer/extensions/MentionReference.d.ts +3 -0
  92. package/dist/composer/extensions/MentionReference.d.ts.map +1 -0
  93. package/dist/composer/extensions/MentionReference.js +66 -0
  94. package/dist/composer/extensions/MentionReference.js.map +1 -0
  95. package/dist/composer/extensions/SkillReference.d.ts +3 -0
  96. package/dist/composer/extensions/SkillReference.d.ts.map +1 -0
  97. package/dist/composer/extensions/SkillReference.js +40 -0
  98. package/dist/composer/extensions/SkillReference.js.map +1 -0
  99. package/dist/composer/index.d.ts +14 -0
  100. package/dist/composer/index.d.ts.map +1 -0
  101. package/dist/composer/index.js +13 -0
  102. package/dist/composer/index.js.map +1 -0
  103. package/dist/composer/pasted-text.d.ts +31 -0
  104. package/dist/composer/pasted-text.d.ts.map +1 -0
  105. package/dist/composer/pasted-text.js +130 -0
  106. package/dist/composer/pasted-text.js.map +1 -0
  107. package/dist/composer/pasted-text.spec.d.ts +2 -0
  108. package/dist/composer/pasted-text.spec.d.ts.map +1 -0
  109. package/dist/composer/pasted-text.spec.js +107 -0
  110. package/dist/composer/pasted-text.spec.js.map +1 -0
  111. package/dist/composer/prompt-attachments.d.ts +18 -0
  112. package/dist/composer/prompt-attachments.d.ts.map +1 -0
  113. package/dist/composer/prompt-attachments.js +65 -0
  114. package/dist/composer/prompt-attachments.js.map +1 -0
  115. package/dist/composer/prompt-attachments.spec.d.ts +2 -0
  116. package/dist/composer/prompt-attachments.spec.d.ts.map +1 -0
  117. package/dist/composer/prompt-attachments.spec.js +63 -0
  118. package/dist/composer/prompt-attachments.spec.js.map +1 -0
  119. package/dist/composer/realtime-voice-audio-level.d.ts +14 -0
  120. package/dist/composer/realtime-voice-audio-level.d.ts.map +1 -0
  121. package/dist/composer/realtime-voice-audio-level.js +47 -0
  122. package/dist/composer/realtime-voice-audio-level.js.map +1 -0
  123. package/dist/composer/realtime-voice-audio-level.spec.d.ts +2 -0
  124. package/dist/composer/realtime-voice-audio-level.spec.d.ts.map +1 -0
  125. package/dist/composer/realtime-voice-audio-level.spec.js +25 -0
  126. package/dist/composer/realtime-voice-audio-level.spec.js.map +1 -0
  127. package/dist/composer/realtime-voice-transcript.d.ts +26 -0
  128. package/dist/composer/realtime-voice-transcript.d.ts.map +1 -0
  129. package/dist/composer/realtime-voice-transcript.js +103 -0
  130. package/dist/composer/realtime-voice-transcript.js.map +1 -0
  131. package/dist/composer/realtime-voice-transcript.spec.d.ts +2 -0
  132. package/dist/composer/realtime-voice-transcript.spec.d.ts.map +1 -0
  133. package/dist/composer/realtime-voice-transcript.spec.js +76 -0
  134. package/dist/composer/realtime-voice-transcript.spec.js.map +1 -0
  135. package/dist/composer/runtime-adapters.d.ts +163 -0
  136. package/dist/composer/runtime-adapters.d.ts.map +1 -0
  137. package/dist/composer/runtime-adapters.js +130 -0
  138. package/dist/composer/runtime-adapters.js.map +1 -0
  139. package/dist/composer/types.d.ts +60 -0
  140. package/dist/composer/types.d.ts.map +1 -0
  141. package/dist/composer/types.js +2 -0
  142. package/dist/composer/types.js.map +1 -0
  143. package/dist/composer/use-file-search.d.ts +6 -0
  144. package/dist/composer/use-file-search.d.ts.map +1 -0
  145. package/dist/composer/use-file-search.js +53 -0
  146. package/dist/composer/use-file-search.js.map +1 -0
  147. package/dist/composer/use-mention-search.d.ts +6 -0
  148. package/dist/composer/use-mention-search.d.ts.map +1 -0
  149. package/dist/composer/use-mention-search.js +75 -0
  150. package/dist/composer/use-mention-search.js.map +1 -0
  151. package/dist/composer/use-skills.d.ts +7 -0
  152. package/dist/composer/use-skills.d.ts.map +1 -0
  153. package/dist/composer/use-skills.js +40 -0
  154. package/dist/composer/use-skills.js.map +1 -0
  155. package/dist/composer/useRealtimeVoiceMode.d.ts +179 -0
  156. package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -0
  157. package/dist/composer/useRealtimeVoiceMode.js +1657 -0
  158. package/dist/composer/useRealtimeVoiceMode.js.map +1 -0
  159. package/dist/composer/useRealtimeVoiceMode.spec.d.ts +2 -0
  160. package/dist/composer/useRealtimeVoiceMode.spec.d.ts.map +1 -0
  161. package/dist/composer/useRealtimeVoiceMode.spec.js +618 -0
  162. package/dist/composer/useRealtimeVoiceMode.spec.js.map +1 -0
  163. package/dist/composer/useVoiceDictation.d.ts +57 -0
  164. package/dist/composer/useVoiceDictation.d.ts.map +1 -0
  165. package/dist/composer/useVoiceDictation.js +1004 -0
  166. package/dist/composer/useVoiceDictation.js.map +1 -0
  167. package/dist/composer/useVoiceDictation.spec.d.ts +2 -0
  168. package/dist/composer/useVoiceDictation.spec.d.ts.map +1 -0
  169. package/dist/composer/useVoiceDictation.spec.js +32 -0
  170. package/dist/composer/useVoiceDictation.spec.js.map +1 -0
  171. package/dist/editor/RegistryBlockContext.d.ts +68 -0
  172. package/dist/editor/RegistryBlockContext.d.ts.map +1 -0
  173. package/dist/editor/RegistryBlockContext.js +10 -0
  174. package/dist/editor/RegistryBlockContext.js.map +1 -0
  175. package/dist/editor/RegistryBlockNode.d.ts +66 -0
  176. package/dist/editor/RegistryBlockNode.d.ts.map +1 -0
  177. package/dist/editor/RegistryBlockNode.js +406 -0
  178. package/dist/editor/RegistryBlockNode.js.map +1 -0
  179. package/dist/editor/RegistryBlockNode.spec.d.ts +2 -0
  180. package/dist/editor/RegistryBlockNode.spec.d.ts.map +1 -0
  181. package/dist/editor/RegistryBlockNode.spec.js +215 -0
  182. package/dist/editor/RegistryBlockNode.spec.js.map +1 -0
  183. package/dist/editor/index.d.ts +2 -0
  184. package/dist/editor/index.d.ts.map +1 -1
  185. package/dist/editor/index.js +2 -0
  186. package/dist/editor/index.js.map +1 -1
  187. package/dist/editor/registrySlashCommands.d.ts +68 -0
  188. package/dist/editor/registrySlashCommands.d.ts.map +1 -0
  189. package/dist/editor/registrySlashCommands.js +51 -0
  190. package/dist/editor/registrySlashCommands.js.map +1 -0
  191. package/package.json +32 -1
  192. package/src/app-shell/header-actions.tsx +84 -0
  193. package/src/app-shell/index.ts +1 -0
  194. package/src/chat-history/ChatHistoryList.spec.tsx +453 -0
  195. package/src/chat-history/ChatHistoryList.tsx +469 -0
  196. package/src/chat-history/index.ts +7 -0
  197. package/src/chat-history.css +314 -0
  198. package/src/collab-ui/AgentPresenceChip.tsx +69 -0
  199. package/src/collab-ui/LiveCursorOverlay.tsx +241 -0
  200. package/src/collab-ui/PresenceBar.tsx +346 -0
  201. package/src/collab-ui/RecentEditHighlights.tsx +225 -0
  202. package/src/collab-ui/RemoteSelectionRings.tsx +227 -0
  203. package/src/collab-ui/agent-identity.ts +16 -0
  204. package/src/collab-ui/index.ts +39 -0
  205. package/src/collab-ui/lead-client.ts +32 -0
  206. package/src/collab-ui/types.spec.ts +18 -0
  207. package/src/collab-ui/types.ts +96 -0
  208. package/src/composer/AgentComposerFrame.tsx +59 -0
  209. package/src/composer/ComposerPlusMenu.tsx +983 -0
  210. package/src/composer/MentionPopover.tsx +412 -0
  211. package/src/composer/PastedTextChip.tsx +150 -0
  212. package/src/composer/PromptComposer.spec.ts +73 -0
  213. package/src/composer/PromptComposer.tsx +718 -0
  214. package/src/composer/RealtimeVoiceMode.spec.tsx +771 -0
  215. package/src/composer/RealtimeVoiceMode.tsx +856 -0
  216. package/src/composer/TiptapComposer.spec.ts +390 -0
  217. package/src/composer/TiptapComposer.tsx +2936 -0
  218. package/src/composer/VoiceButton.spec.ts +33 -0
  219. package/src/composer/VoiceButton.tsx +255 -0
  220. package/src/composer/asset-picker-url.spec.ts +56 -0
  221. package/src/composer/asset-picker-url.ts +80 -0
  222. package/src/composer/attachment-accept.spec.ts +22 -0
  223. package/src/composer/attachment-accept.ts +71 -0
  224. package/src/composer/draft-key.spec.ts +19 -0
  225. package/src/composer/draft-key.ts +7 -0
  226. package/src/composer/extensions/FileReference.tsx +55 -0
  227. package/src/composer/extensions/MentionReference.tsx +93 -0
  228. package/src/composer/extensions/SkillReference.tsx +56 -0
  229. package/src/composer/index.ts +79 -0
  230. package/src/composer/pasted-text.spec.ts +162 -0
  231. package/src/composer/pasted-text.ts +160 -0
  232. package/src/composer/prompt-attachments.spec.ts +80 -0
  233. package/src/composer/prompt-attachments.ts +93 -0
  234. package/src/composer/realtime-voice-audio-level.spec.ts +32 -0
  235. package/src/composer/realtime-voice-audio-level.ts +64 -0
  236. package/src/composer/realtime-voice-transcript.spec.ts +95 -0
  237. package/src/composer/realtime-voice-transcript.ts +134 -0
  238. package/src/composer/runtime-adapters.tsx +343 -0
  239. package/src/composer/types.ts +66 -0
  240. package/src/composer/use-file-search.ts +64 -0
  241. package/src/composer/use-mention-search.ts +90 -0
  242. package/src/composer/use-skills.ts +45 -0
  243. package/src/composer/useRealtimeVoiceMode.spec.ts +818 -0
  244. package/src/composer/useRealtimeVoiceMode.tsx +2117 -0
  245. package/src/composer/useVoiceDictation.spec.ts +45 -0
  246. package/src/composer/useVoiceDictation.ts +1191 -0
  247. package/src/context-ui/ContextMeter.tsx +101 -0
  248. package/src/context-ui/ContextSegmentRow.tsx +184 -0
  249. package/src/context-ui/ContextTreemap.tsx +125 -0
  250. package/src/context-ui/ContextXRayPanel.tsx +377 -0
  251. package/src/context-ui/format.ts +42 -0
  252. package/src/context-ui/index.ts +15 -0
  253. package/src/context-ui/types.ts +43 -0
  254. package/src/design-tweaks/index.ts +1 -0
  255. package/src/design-tweaks/visual-style-controls.tsx +1084 -0
  256. package/src/editor/BubbleToolbar.tsx +253 -0
  257. package/src/editor/CodeBlockNode.tsx +254 -0
  258. package/src/editor/DragHandle.spec.ts +588 -0
  259. package/src/editor/DragHandle.ts +1437 -0
  260. package/src/editor/ImageExtension.roundtrip.spec.ts +117 -0
  261. package/src/editor/ImageExtension.ts +306 -0
  262. package/src/editor/RegistryBlockContext.tsx +107 -0
  263. package/src/editor/RegistryBlockNode.spec.ts +279 -0
  264. package/src/editor/RegistryBlockNode.tsx +652 -0
  265. package/src/editor/RichMarkdownEditor.collab.spec.ts +79 -0
  266. package/src/editor/RichMarkdownEditor.roundtrip.spec.ts +226 -0
  267. package/src/editor/RichMarkdownEditor.tsx +81 -0
  268. package/src/editor/RunId.ts +61 -0
  269. package/src/editor/SharedRichEditor.tsx +321 -0
  270. package/src/editor/SlashCommandMenu.tsx +310 -0
  271. package/src/editor/disableHistory.spec.ts +85 -0
  272. package/src/editor/extensions.ts +402 -0
  273. package/src/editor/gfmDoc.spec.ts +230 -0
  274. package/src/editor/gfmDoc.ts +92 -0
  275. package/src/editor/index.ts +93 -0
  276. package/src/editor/registrySlashCommands.ts +131 -0
  277. package/src/editor/structuralReorderUndo.spec.ts +144 -0
  278. package/src/editor/surgical-apply.spec.ts +279 -0
  279. package/src/editor/surgical-apply.ts +192 -0
  280. package/src/editor/useCollabReconcile.concurrent.spec.ts +422 -0
  281. package/src/editor/useCollabReconcile.idempotent.spec.ts +324 -0
  282. package/src/editor/useCollabReconcile.ts +589 -0
  283. package/src/editor.css +510 -0
  284. package/src/hooks/index.ts +2 -0
  285. package/src/hooks/use-mobile.tsx +21 -0
  286. package/src/hooks/use-toast.ts +188 -0
  287. package/src/index.ts +8 -0
  288. package/src/onboarding/index.ts +1 -0
  289. package/src/provider.tsx +47 -0
  290. package/src/sharing/VisibilityBadge.spec.ts +96 -0
  291. package/src/sharing/VisibilityBadge.tsx +37 -0
  292. package/src/sharing/index.ts +4 -0
  293. package/src/styles.css +21 -0
  294. package/src/ui/accordion.tsx +56 -0
  295. package/src/ui/alert-dialog.tsx +141 -0
  296. package/src/ui/alert.tsx +59 -0
  297. package/src/ui/aspect-ratio.tsx +5 -0
  298. package/src/ui/avatar.tsx +48 -0
  299. package/src/ui/badge.tsx +37 -0
  300. package/src/ui/breadcrumb.tsx +115 -0
  301. package/src/ui/button.tsx +78 -0
  302. package/src/ui/calendar.tsx +214 -0
  303. package/src/ui/card.tsx +86 -0
  304. package/src/ui/carousel.tsx +260 -0
  305. package/src/ui/chart.tsx +375 -0
  306. package/src/ui/checkbox.tsx +28 -0
  307. package/src/ui/collapsible.tsx +9 -0
  308. package/src/ui/command.spec.tsx +51 -0
  309. package/src/ui/command.tsx +174 -0
  310. package/src/ui/context-menu.tsx +202 -0
  311. package/src/ui/date-picker.tsx +64 -0
  312. package/src/ui/dialog.spec.tsx +43 -0
  313. package/src/ui/dialog.tsx +146 -0
  314. package/src/ui/drawer.tsx +126 -0
  315. package/src/ui/dropdown-menu.tsx +218 -0
  316. package/src/ui/form.tsx +177 -0
  317. package/src/ui/hover-card.tsx +27 -0
  318. package/src/ui/index.ts +52 -0
  319. package/src/ui/input-otp.tsx +69 -0
  320. package/src/ui/input.tsx +22 -0
  321. package/src/ui/label.tsx +24 -0
  322. package/src/ui/logo.tsx +36 -0
  323. package/src/ui/menubar.tsx +235 -0
  324. package/src/ui/navigation-menu.tsx +128 -0
  325. package/src/ui/pagination.tsx +121 -0
  326. package/src/ui/popover.tsx +44 -0
  327. package/src/ui/progress.tsx +26 -0
  328. package/src/ui/radio-group.tsx +42 -0
  329. package/src/ui/resizable.tsx +43 -0
  330. package/src/ui/scroll-area.tsx +46 -0
  331. package/src/ui/select.tsx +158 -0
  332. package/src/ui/separator.tsx +29 -0
  333. package/src/ui/sheet.tsx +160 -0
  334. package/src/ui/sidebar.tsx +777 -0
  335. package/src/ui/skeleton.tsx +18 -0
  336. package/src/ui/slider.tsx +26 -0
  337. package/src/ui/sonner.tsx +58 -0
  338. package/src/ui/spinner.tsx +17 -0
  339. package/src/ui/switch.tsx +27 -0
  340. package/src/ui/table.tsx +117 -0
  341. package/src/ui/tabs.tsx +53 -0
  342. package/src/ui/textarea.tsx +23 -0
  343. package/src/ui/toast.tsx +127 -0
  344. package/src/ui/toaster.tsx +33 -0
  345. package/src/ui/toggle-group.tsx +59 -0
  346. package/src/ui/toggle.tsx +43 -0
  347. package/src/ui/tooltip.tsx +53 -0
  348. package/src/ui/use-toast.ts +3 -0
  349. package/src/utils.ts +8 -0
@@ -0,0 +1,1004 @@
1
+ /**
2
+ * Voice dictation hook for the agent composer.
3
+ *
4
+ * Wires voice providers behind a single state machine:
5
+ * - "auto" / "openai" / "builder" / "builder-gemini" / "gemini" / "groq"
6
+ * — MediaRecorder → POST /_agent-native/transcribe-voice
7
+ * - "google-realtime"
8
+ * — MediaRecorder chunks → POST /_agent-native/transcribe-stream/session
9
+ * → managed WebSocket → Google Speech-to-Text streaming
10
+ * - "browser" — Web Speech API (low quality, offline capable)
11
+ *
12
+ * Provider preference lives in application_state under
13
+ * `voice-transcription-prefs` (`{ transcriptionMode, provider, instructions }`).
14
+ * The composer reads it on every start so settings changes take effect
15
+ * immediately without unmounting the composer.
16
+ *
17
+ * The hook exposes amplitude (0..1) and duration (ms) so the composer can
18
+ * render the Lovable-style live waveform + MM:SS timer.
19
+ */
20
+ import { useCallback, useEffect, useRef, useState } from "react";
21
+ import { applyVoiceContextReplacements, useComposerRuntimeAdapters, } from "./runtime-adapters.js";
22
+ const PREFS_KEY = "voice-transcription-prefs";
23
+ const CLEANUP_PREFS_KEY = "voice-cleanup-prefs";
24
+ const PREFS_URL = `/_agent-native/application-state/${PREFS_KEY}`;
25
+ const CLEANUP_PREFS_URL = `/_agent-native/application-state/${CLEANUP_PREFS_KEY}`;
26
+ const TRANSCRIBE_URL = "/_agent-native/transcribe-voice";
27
+ const GOOGLE_REALTIME_SESSION_URL = "/_agent-native/transcribe-stream/session";
28
+ const GOOGLE_REALTIME_WS_PROTOCOL = "google-realtime.v1";
29
+ function isVoiceProvider(value) {
30
+ return (value === "auto" ||
31
+ value === "openai" ||
32
+ value === "browser" ||
33
+ value === "google-realtime" ||
34
+ value === "builder-gemini" ||
35
+ value === "builder" ||
36
+ value === "gemini" ||
37
+ value === "groq");
38
+ }
39
+ function isTranscriptionMode(value) {
40
+ return (value === "mac-native" || value === "google-realtime" || value === "batch");
41
+ }
42
+ async function defaultProvider() {
43
+ return "auto";
44
+ }
45
+ function normalizeProviderForMode(mode, provider) {
46
+ if (mode === "mac-native")
47
+ return "browser";
48
+ if (mode === "google-realtime")
49
+ return "google-realtime";
50
+ if (mode === "batch") {
51
+ if (!provider || provider === "browser")
52
+ return "auto";
53
+ return provider === "builder" ? "builder-gemini" : provider;
54
+ }
55
+ if (!provider)
56
+ return null;
57
+ return provider === "builder" ? "builder-gemini" : provider;
58
+ }
59
+ async function readVoicePrefs(resolvePath) {
60
+ const cleanupPrefs = await readVoiceCleanupPrefs(resolvePath);
61
+ try {
62
+ const res = await fetch(resolvePath(PREFS_URL));
63
+ if (!res.ok) {
64
+ return {
65
+ provider: await defaultProvider(),
66
+ cleanupEnabled: cleanupPrefs.enabled,
67
+ };
68
+ }
69
+ const body = (await res.json());
70
+ const value = body?.value ??
71
+ body;
72
+ const mode = isTranscriptionMode(value?.transcriptionMode)
73
+ ? value.transcriptionMode
74
+ : undefined;
75
+ const p = body?.provider ??
76
+ body?.value?.provider;
77
+ const instructions = body?.instructions ??
78
+ body?.value?.instructions;
79
+ const provider = normalizeProviderForMode(mode, isVoiceProvider(p) ? p : null);
80
+ if (provider) {
81
+ return {
82
+ transcriptionMode: mode,
83
+ provider,
84
+ cleanupEnabled: cleanupPrefs.enabled,
85
+ instructions: typeof instructions === "string" ? instructions.trim() : undefined,
86
+ };
87
+ }
88
+ }
89
+ catch {
90
+ /* fall through */
91
+ }
92
+ return {
93
+ provider: await defaultProvider(),
94
+ cleanupEnabled: cleanupPrefs.enabled,
95
+ };
96
+ }
97
+ async function readVoiceCleanupPrefs(resolvePath) {
98
+ try {
99
+ const res = await fetch(resolvePath(CLEANUP_PREFS_URL));
100
+ if (!res.ok)
101
+ return {};
102
+ const body = (await res.json());
103
+ const value = body?.value ??
104
+ body;
105
+ return { enabled: value?.enabled === true };
106
+ }
107
+ catch {
108
+ return {};
109
+ }
110
+ }
111
+ function getSpeechRecognitionCtor() {
112
+ if (typeof window === "undefined")
113
+ return null;
114
+ return (window.SpeechRecognition ||
115
+ window.webkitSpeechRecognition ||
116
+ null);
117
+ }
118
+ function pickMimeType() {
119
+ if (typeof MediaRecorder === "undefined")
120
+ return "audio/webm";
121
+ const candidates = [
122
+ "audio/webm;codecs=opus",
123
+ "audio/webm",
124
+ "audio/mp4",
125
+ "audio/ogg;codecs=opus",
126
+ ];
127
+ for (const mime of candidates) {
128
+ try {
129
+ if (MediaRecorder.isTypeSupported(mime))
130
+ return mime;
131
+ }
132
+ catch {
133
+ /* ignore */
134
+ }
135
+ }
136
+ return "audio/webm";
137
+ }
138
+ async function resolveVoiceContextPack(source) {
139
+ try {
140
+ const value = typeof source === "function" ? await source() : source;
141
+ return value && typeof value === "object" ? value : undefined;
142
+ }
143
+ catch {
144
+ return undefined;
145
+ }
146
+ }
147
+ function appendVoiceContext(form, contextPack) {
148
+ if (!contextPack)
149
+ return;
150
+ try {
151
+ form.append("voiceContext", JSON.stringify(contextPack));
152
+ }
153
+ catch {
154
+ /* best effort */
155
+ }
156
+ }
157
+ export function providerForTextCleanup(provider) {
158
+ return provider === "google-realtime" ? "auto" : provider;
159
+ }
160
+ async function cleanupRecognizedText({ text, provider, instructions, contextPack, resolvePath, replaceContext, }) {
161
+ if (provider === "browser") {
162
+ return replaceContext(text, contextPack);
163
+ }
164
+ const form = new FormData();
165
+ form.append("text", text);
166
+ form.append("provider", providerForTextCleanup(provider));
167
+ if (instructions?.trim()) {
168
+ form.append("instructions", instructions.trim());
169
+ }
170
+ appendVoiceContext(form, contextPack);
171
+ const res = await fetch(resolvePath(TRANSCRIBE_URL), {
172
+ method: "POST",
173
+ body: form,
174
+ });
175
+ if (!res.ok) {
176
+ const body = await res
177
+ .json()
178
+ .catch(() => ({ error: `HTTP ${res.status}` }));
179
+ throw new Error(body.error || `Transcript cleanup failed (${res.status})`);
180
+ }
181
+ const data = (await res.json());
182
+ return (data.text ?? "").trim() || text;
183
+ }
184
+ function microphoneBlockedByPermissionsPolicy() {
185
+ if (typeof document === "undefined")
186
+ return false;
187
+ const doc = document;
188
+ const policy = doc.permissionsPolicy ?? doc.featurePolicy ?? null;
189
+ if (!policy?.allowsFeature)
190
+ return false;
191
+ try {
192
+ return !policy.allowsFeature("microphone");
193
+ }
194
+ catch {
195
+ return false;
196
+ }
197
+ }
198
+ export function voiceDictationStartErrorMessage(error) {
199
+ const name = typeof error?.name === "string"
200
+ ? error.name
201
+ : "";
202
+ const message = typeof error?.message === "string"
203
+ ? error.message
204
+ : "";
205
+ if (microphoneBlockedByPermissionsPolicy() ||
206
+ /permissions[- ]policy|feature policy/i.test(message)) {
207
+ return "This app is blocking microphone access through its browser permissions policy. Reload the app, or open it directly in a browser tab.";
208
+ }
209
+ if (name === "NotAllowedError" ||
210
+ name === "SecurityError" ||
211
+ /permission|denied|not allowed/i.test(message)) {
212
+ return "Microphone access is blocked. Click the site controls icon in the address bar, set Microphone to Allow for this app, then try again.";
213
+ }
214
+ if (name === "NotFoundError" || name === "DevicesNotFoundError") {
215
+ return "No microphone was found. Plug one in or choose a different input, then try again.";
216
+ }
217
+ if (name === "NotReadableError" || name === "TrackStartError") {
218
+ return "Your microphone is busy in another app. Close the other app or choose a different input, then try again.";
219
+ }
220
+ return message || "Could not start recording";
221
+ }
222
+ function voiceDictationSpeechErrorMessage(error) {
223
+ if (error === "not-allowed" || error === "service-not-allowed") {
224
+ return voiceDictationStartErrorMessage({
225
+ name: "NotAllowedError",
226
+ message: error,
227
+ });
228
+ }
229
+ if (error === "audio-capture") {
230
+ return "No microphone was found. Plug one in or choose a different input, then try again.";
231
+ }
232
+ return `Speech recognition error: ${error ?? "unknown"}`;
233
+ }
234
+ export function useVoiceDictation(options) {
235
+ const adapters = useComposerRuntimeAdapters();
236
+ const resolvePath = adapters.resolvePath;
237
+ const replaceContext = adapters.voice.applyContextReplacements ?? applyVoiceContextReplacements;
238
+ const { onTranscript, onError, onLiveUpdate, contextPack } = options;
239
+ const onTranscriptRef = useRef(onTranscript);
240
+ const onErrorRef = useRef(onError);
241
+ const onLiveUpdateRef = useRef(onLiveUpdate);
242
+ const contextPackRef = useRef(contextPack);
243
+ onTranscriptRef.current = onTranscript;
244
+ onErrorRef.current = onError;
245
+ onLiveUpdateRef.current = onLiveUpdate;
246
+ contextPackRef.current = contextPack;
247
+ const [state, setState] = useState("idle");
248
+ const [amplitude, setAmplitude] = useState(0);
249
+ const [durationMs, setDurationMs] = useState(0);
250
+ const [errorMessage, setErrorMessage] = useState(null);
251
+ const [provider, setProvider] = useState("auto");
252
+ // Keep refs for teardown / cross-branch access.
253
+ const mediaStreamRef = useRef(null);
254
+ const mediaRecorderRef = useRef(null);
255
+ const chunksRef = useRef([]);
256
+ const audioContextRef = useRef(null);
257
+ const analyserRef = useRef(null);
258
+ const rafRef = useRef(null);
259
+ const timerRef = useRef(null);
260
+ const startedAtRef = useRef(0);
261
+ const cancelledRef = useRef(false);
262
+ const speechRef = useRef(null);
263
+ const speechTranscriptRef = useRef("");
264
+ const activeProviderRef = useRef("browser");
265
+ // Parallel live recognition for OpenAI mode (provides instant preview while MediaRecorder captures)
266
+ const liveSpeechRef = useRef(null);
267
+ const liveTextRef = useRef("");
268
+ const realtimeSocketRef = useRef(null);
269
+ const realtimeFinalRef = useRef("");
270
+ const realtimeInterimRef = useRef("");
271
+ const realtimeStopTimeoutRef = useRef(null);
272
+ const mediaRecorderSupported = typeof window !== "undefined" &&
273
+ typeof navigator !== "undefined" &&
274
+ !!navigator.mediaDevices?.getUserMedia &&
275
+ typeof window.MediaRecorder !== "undefined";
276
+ const speechSupported = !!getSpeechRecognitionCtor();
277
+ const supported = mediaRecorderSupported || speechSupported;
278
+ const teardown = useCallback(() => {
279
+ if (rafRef.current != null) {
280
+ cancelAnimationFrame(rafRef.current);
281
+ rafRef.current = null;
282
+ }
283
+ if (timerRef.current != null) {
284
+ clearInterval(timerRef.current);
285
+ timerRef.current = null;
286
+ }
287
+ if (mediaStreamRef.current) {
288
+ for (const track of mediaStreamRef.current.getTracks()) {
289
+ track.stop();
290
+ }
291
+ mediaStreamRef.current = null;
292
+ }
293
+ if (audioContextRef.current) {
294
+ audioContextRef.current.close().catch(() => { });
295
+ audioContextRef.current = null;
296
+ }
297
+ if (speechRef.current) {
298
+ // Stop the Web Speech session before dropping the ref so the browser
299
+ // releases the mic and stops dispatching onresult events into a stale
300
+ // closure. abort() is fire-and-forget (no final result); stop() would
301
+ // deliver remaining partials but we've already cleared state.
302
+ try {
303
+ speechRef.current.abort?.();
304
+ }
305
+ catch {
306
+ /* ignore */
307
+ }
308
+ }
309
+ if (liveSpeechRef.current) {
310
+ try {
311
+ liveSpeechRef.current.abort?.();
312
+ }
313
+ catch {
314
+ /* ignore */
315
+ }
316
+ liveSpeechRef.current = null;
317
+ }
318
+ if (realtimeSocketRef.current) {
319
+ const socket = realtimeSocketRef.current;
320
+ realtimeSocketRef.current = null;
321
+ try {
322
+ socket.close();
323
+ }
324
+ catch {
325
+ /* ignore */
326
+ }
327
+ }
328
+ if (realtimeStopTimeoutRef.current) {
329
+ clearTimeout(realtimeStopTimeoutRef.current);
330
+ realtimeStopTimeoutRef.current = null;
331
+ }
332
+ analyserRef.current = null;
333
+ mediaRecorderRef.current = null;
334
+ chunksRef.current = [];
335
+ speechRef.current = null;
336
+ speechTranscriptRef.current = "";
337
+ liveTextRef.current = "";
338
+ realtimeFinalRef.current = "";
339
+ realtimeInterimRef.current = "";
340
+ setAmplitude(0);
341
+ }, []);
342
+ useEffect(() => teardown, [teardown]);
343
+ const failWith = useCallback((message) => {
344
+ setErrorMessage(message);
345
+ setState("error");
346
+ onErrorRef.current?.(message);
347
+ teardown();
348
+ }, [teardown]);
349
+ const getVoiceContextPack = useCallback(() => resolveVoiceContextPack(contextPackRef.current), []);
350
+ const startMeter = useCallback((stream) => {
351
+ try {
352
+ const AudioCtor = typeof window !== "undefined"
353
+ ? window.AudioContext || window.webkitAudioContext || null
354
+ : null;
355
+ if (!AudioCtor)
356
+ return;
357
+ const ctx = new AudioCtor();
358
+ const source = ctx.createMediaStreamSource(stream);
359
+ const analyser = ctx.createAnalyser();
360
+ analyser.fftSize = 512;
361
+ source.connect(analyser);
362
+ audioContextRef.current = ctx;
363
+ analyserRef.current = analyser;
364
+ const buffer = new Uint8Array(analyser.frequencyBinCount);
365
+ const tick = () => {
366
+ if (!analyserRef.current)
367
+ return;
368
+ analyserRef.current.getByteTimeDomainData(buffer);
369
+ let sumSquares = 0;
370
+ for (let i = 0; i < buffer.length; i++) {
371
+ const n = (buffer[i] - 128) / 128;
372
+ sumSquares += n * n;
373
+ }
374
+ const rms = Math.sqrt(sumSquares / buffer.length);
375
+ setAmplitude(Math.min(1, rms * 2.5));
376
+ rafRef.current = requestAnimationFrame(tick);
377
+ };
378
+ rafRef.current = requestAnimationFrame(tick);
379
+ }
380
+ catch {
381
+ /* analyser is best-effort */
382
+ }
383
+ }, []);
384
+ const startTimer = useCallback(() => {
385
+ startedAtRef.current = Date.now();
386
+ setDurationMs(0);
387
+ timerRef.current = setInterval(() => {
388
+ setDurationMs(Date.now() - startedAtRef.current);
389
+ }, 100);
390
+ }, []);
391
+ const startOpenAi = useCallback(async (providerPref, instructions) => {
392
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
393
+ // User may have pressed Escape (cancel) while the permission prompt was
394
+ // open. If so, stop the stream and bail before we start recording.
395
+ if (cancelledRef.current) {
396
+ for (const track of stream.getTracks())
397
+ track.stop();
398
+ cancelledRef.current = false;
399
+ setState("idle");
400
+ return;
401
+ }
402
+ mediaStreamRef.current = stream;
403
+ const mimeType = pickMimeType();
404
+ const recorder = new MediaRecorder(stream, { mimeType });
405
+ mediaRecorderRef.current = recorder;
406
+ chunksRef.current = [];
407
+ recorder.ondataavailable = (e) => {
408
+ if (e.data && e.data.size > 0)
409
+ chunksRef.current.push(e.data);
410
+ };
411
+ recorder.onstop = async () => {
412
+ const localChunks = chunksRef.current.slice();
413
+ const localMime = recorder.mimeType || mimeType;
414
+ const liveSnapshot = liveTextRef.current;
415
+ teardown();
416
+ if (cancelledRef.current) {
417
+ cancelledRef.current = false;
418
+ setState("idle");
419
+ return;
420
+ }
421
+ if (localChunks.length === 0) {
422
+ if (liveSnapshot)
423
+ onTranscriptRef.current?.(liveSnapshot.trim());
424
+ setState("idle");
425
+ return;
426
+ }
427
+ setState("transcribing");
428
+ try {
429
+ const audioBlob = new Blob(localChunks, { type: localMime });
430
+ const form = new FormData();
431
+ form.append("audio", audioBlob, `voice.${localMime.split("/")[1] ?? "webm"}`);
432
+ form.append("provider", providerPref);
433
+ if (instructions?.trim()) {
434
+ form.append("instructions", instructions.trim());
435
+ }
436
+ appendVoiceContext(form, await getVoiceContextPack());
437
+ const res = await fetch(resolvePath(TRANSCRIBE_URL), {
438
+ method: "POST",
439
+ body: form,
440
+ });
441
+ if (!res.ok) {
442
+ const body = await res
443
+ .json()
444
+ .catch(() => ({ error: `HTTP ${res.status}` }));
445
+ throw new Error(body.error || `Transcription failed (${res.status})`);
446
+ }
447
+ if (cancelledRef.current) {
448
+ cancelledRef.current = false;
449
+ setState("idle");
450
+ return;
451
+ }
452
+ const data = (await res.json());
453
+ const text = (data.text ?? "").trim();
454
+ if (text) {
455
+ onTranscriptRef.current?.(text);
456
+ }
457
+ else if (liveSnapshot) {
458
+ onTranscriptRef.current?.(liveSnapshot.trim());
459
+ }
460
+ setState("idle");
461
+ }
462
+ catch (err) {
463
+ if (cancelledRef.current) {
464
+ cancelledRef.current = false;
465
+ setState("idle");
466
+ return;
467
+ }
468
+ if (liveSnapshot) {
469
+ onTranscriptRef.current?.(liveSnapshot.trim());
470
+ setState("idle");
471
+ }
472
+ else {
473
+ failWith(err?.message ??
474
+ "Transcription failed. Check your voice transcription provider in settings.");
475
+ }
476
+ }
477
+ };
478
+ startMeter(stream);
479
+ startTimer();
480
+ setState("recording");
481
+ recorder.start();
482
+ // Start parallel Web Speech recognition for live preview text.
483
+ // This runs alongside MediaRecorder so the user sees words appear
484
+ // immediately while the server provider processes the full recording later.
485
+ const SpeechCtor = getSpeechRecognitionCtor();
486
+ if (SpeechCtor) {
487
+ const liveSpeech = new SpeechCtor();
488
+ liveSpeech.continuous = true;
489
+ liveSpeech.interimResults = true;
490
+ liveSpeech.lang =
491
+ (typeof navigator !== "undefined" && navigator.language) || "en-US";
492
+ liveSpeechRef.current = liveSpeech;
493
+ liveTextRef.current = "";
494
+ liveSpeech.onresult = (event) => {
495
+ let interim = "";
496
+ for (let i = event.resultIndex; i < event.results.length; i++) {
497
+ const result = event.results[i];
498
+ const text = result[0]?.transcript ?? "";
499
+ if (result.isFinal) {
500
+ liveTextRef.current += text;
501
+ }
502
+ else {
503
+ interim += text;
504
+ }
505
+ }
506
+ onLiveUpdateRef.current?.(liveTextRef.current, interim);
507
+ };
508
+ liveSpeech.onend = () => {
509
+ if (liveSpeechRef.current === liveSpeech) {
510
+ try {
511
+ liveSpeech.start();
512
+ }
513
+ catch {
514
+ /* ignore */
515
+ }
516
+ }
517
+ };
518
+ liveSpeech.onerror = () => { };
519
+ try {
520
+ liveSpeech.start();
521
+ }
522
+ catch {
523
+ /* best effort — live preview just won't appear */
524
+ }
525
+ }
526
+ }, [
527
+ startMeter,
528
+ startTimer,
529
+ teardown,
530
+ failWith,
531
+ getVoiceContextPack,
532
+ resolvePath,
533
+ ]);
534
+ const startBrowser = useCallback(async (prefs) => {
535
+ const Ctor = getSpeechRecognitionCtor();
536
+ if (!Ctor) {
537
+ throw new Error("Your browser doesn't support speech recognition. Add an OpenAI API key in settings for Whisper transcription.");
538
+ }
539
+ // Still request mic to drive the amplitude meter, so the UI doesn't look
540
+ // dead while the user talks. SpeechRecognition manages its own capture
541
+ // under the hood in most browsers.
542
+ let stream = null;
543
+ try {
544
+ stream = await navigator.mediaDevices.getUserMedia({ audio: true });
545
+ mediaStreamRef.current = stream;
546
+ startMeter(stream);
547
+ }
548
+ catch {
549
+ /* non-fatal — recognition can still work without our analyser */
550
+ }
551
+ if (cancelledRef.current) {
552
+ if (stream)
553
+ for (const track of stream.getTracks())
554
+ track.stop();
555
+ mediaStreamRef.current = null;
556
+ cancelledRef.current = false;
557
+ setState("idle");
558
+ return;
559
+ }
560
+ const recognition = new Ctor();
561
+ recognition.continuous = true;
562
+ recognition.interimResults = true;
563
+ recognition.lang =
564
+ (typeof navigator !== "undefined" && navigator.language) || "en-US";
565
+ speechRef.current = recognition;
566
+ speechTranscriptRef.current = "";
567
+ recognition.onresult = (event) => {
568
+ let interim = "";
569
+ for (let i = event.resultIndex; i < event.results.length; i++) {
570
+ const result = event.results[i];
571
+ const text = result[0]?.transcript ?? "";
572
+ if (result.isFinal) {
573
+ speechTranscriptRef.current += text;
574
+ }
575
+ else {
576
+ interim += text;
577
+ }
578
+ }
579
+ onLiveUpdateRef.current?.(speechTranscriptRef.current, interim);
580
+ };
581
+ recognition.onerror = (event) => {
582
+ if (event?.error === "no-speech" || event?.error === "aborted")
583
+ return;
584
+ failWith(voiceDictationSpeechErrorMessage(event?.error));
585
+ };
586
+ recognition.onend = () => {
587
+ const text = speechTranscriptRef.current.trim();
588
+ const wasCancelled = cancelledRef.current;
589
+ cancelledRef.current = false;
590
+ teardown();
591
+ if (wasCancelled || !text) {
592
+ setState("idle");
593
+ return;
594
+ }
595
+ void (async () => {
596
+ let finalText = text;
597
+ if (prefs.cleanupEnabled) {
598
+ setState("transcribing");
599
+ try {
600
+ finalText = await cleanupRecognizedText({
601
+ text,
602
+ provider: prefs.provider,
603
+ instructions: prefs.instructions,
604
+ contextPack: await getVoiceContextPack(),
605
+ resolvePath,
606
+ replaceContext,
607
+ });
608
+ }
609
+ catch {
610
+ finalText = text;
611
+ }
612
+ }
613
+ if (!cancelledRef.current && finalText) {
614
+ onTranscriptRef.current?.(finalText);
615
+ }
616
+ cancelledRef.current = false;
617
+ setState("idle");
618
+ })();
619
+ };
620
+ startTimer();
621
+ setState("recording");
622
+ recognition.start();
623
+ }, [
624
+ startMeter,
625
+ startTimer,
626
+ teardown,
627
+ failWith,
628
+ getVoiceContextPack,
629
+ replaceContext,
630
+ resolvePath,
631
+ ]);
632
+ const startGoogleRealtime = useCallback(async (prefs) => {
633
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
634
+ if (cancelledRef.current) {
635
+ for (const track of stream.getTracks())
636
+ track.stop();
637
+ cancelledRef.current = false;
638
+ setState("idle");
639
+ return;
640
+ }
641
+ const sessionRes = await fetch(resolvePath(GOOGLE_REALTIME_SESSION_URL), {
642
+ method: "POST",
643
+ headers: { "Content-Type": "application/json" },
644
+ body: JSON.stringify({
645
+ language: (typeof navigator !== "undefined" && navigator.language) || "en-US",
646
+ }),
647
+ });
648
+ const sessionBody = (await sessionRes
649
+ .json()
650
+ .catch(() => ({ error: `HTTP ${sessionRes.status}` })));
651
+ if (!sessionRes.ok ||
652
+ !sessionBody.websocketUrl ||
653
+ !sessionBody.sessionToken) {
654
+ for (const track of stream.getTracks())
655
+ track.stop();
656
+ throw new Error(sessionBody.error ||
657
+ `Could not start Google realtime transcription (${sessionRes.status})`);
658
+ }
659
+ if (cancelledRef.current) {
660
+ for (const track of stream.getTracks())
661
+ track.stop();
662
+ cancelledRef.current = false;
663
+ setState("idle");
664
+ return;
665
+ }
666
+ mediaStreamRef.current = stream;
667
+ const mimeType = pickMimeType();
668
+ const recorder = new MediaRecorder(stream, { mimeType });
669
+ mediaRecorderRef.current = recorder;
670
+ realtimeFinalRef.current = "";
671
+ realtimeInterimRef.current = "";
672
+ let finalized = false;
673
+ let recorderStarted = false;
674
+ const finish = (errorMessage) => {
675
+ if (finalized)
676
+ return;
677
+ finalized = true;
678
+ void (async () => {
679
+ const finalText = realtimeFinalRef.current.trim();
680
+ const interimText = realtimeInterimRef.current.trim();
681
+ const text = [finalText, interimText]
682
+ .filter(Boolean)
683
+ .join(" ")
684
+ .trim();
685
+ teardown();
686
+ if (cancelledRef.current) {
687
+ cancelledRef.current = false;
688
+ setState("idle");
689
+ return;
690
+ }
691
+ if (text) {
692
+ let cleanedText = text;
693
+ if (prefs.cleanupEnabled) {
694
+ setState("transcribing");
695
+ try {
696
+ cleanedText = await cleanupRecognizedText({
697
+ text,
698
+ provider: prefs.provider,
699
+ instructions: prefs.instructions,
700
+ contextPack: await getVoiceContextPack(),
701
+ resolvePath,
702
+ replaceContext,
703
+ });
704
+ }
705
+ catch {
706
+ cleanedText = text;
707
+ }
708
+ }
709
+ if (!cancelledRef.current && cleanedText) {
710
+ onTranscriptRef.current?.(cleanedText);
711
+ }
712
+ cancelledRef.current = false;
713
+ setState("idle");
714
+ return;
715
+ }
716
+ if (errorMessage) {
717
+ failWith(errorMessage);
718
+ return;
719
+ }
720
+ setState("idle");
721
+ })();
722
+ };
723
+ recorder.ondataavailable = async (event) => {
724
+ if (!event.data || event.data.size === 0)
725
+ return;
726
+ const socket = realtimeSocketRef.current;
727
+ if (!socket || socket.readyState !== WebSocket.OPEN)
728
+ return;
729
+ try {
730
+ socket.send(await event.data.arrayBuffer());
731
+ }
732
+ catch (err) {
733
+ finish(err?.message ?? "Realtime audio upload failed");
734
+ }
735
+ };
736
+ recorder.onstop = () => {
737
+ const socket = realtimeSocketRef.current;
738
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
739
+ finish();
740
+ return;
741
+ }
742
+ setState("transcribing");
743
+ try {
744
+ socket.send(JSON.stringify({ type: "stop" }));
745
+ }
746
+ catch {
747
+ finish();
748
+ return;
749
+ }
750
+ realtimeStopTimeoutRef.current = window.setTimeout(() => {
751
+ const activeSocket = realtimeSocketRef.current;
752
+ if (activeSocket?.readyState === WebSocket.OPEN) {
753
+ try {
754
+ activeSocket.close();
755
+ }
756
+ catch {
757
+ finish();
758
+ }
759
+ return;
760
+ }
761
+ finish();
762
+ }, 10000);
763
+ };
764
+ const socket = new WebSocket(sessionBody.websocketUrl, [
765
+ sessionBody.websocketProtocol || GOOGLE_REALTIME_WS_PROTOCOL,
766
+ sessionBody.sessionToken,
767
+ ]);
768
+ socket.binaryType = "arraybuffer";
769
+ realtimeSocketRef.current = socket;
770
+ socket.onmessage = (event) => {
771
+ if (cancelledRef.current) {
772
+ finish();
773
+ return;
774
+ }
775
+ const raw = typeof event.data === "string"
776
+ ? event.data
777
+ : event.data instanceof Blob
778
+ ? null
779
+ : new TextDecoder().decode(event.data);
780
+ if (!raw)
781
+ return;
782
+ let message;
783
+ try {
784
+ message = JSON.parse(raw);
785
+ }
786
+ catch {
787
+ return;
788
+ }
789
+ if (!message?.type)
790
+ return;
791
+ if (message.type === "ready") {
792
+ if (!recorderStarted) {
793
+ recorderStarted = true;
794
+ startMeter(stream);
795
+ startTimer();
796
+ setState("recording");
797
+ recorder.start(100);
798
+ }
799
+ return;
800
+ }
801
+ if (message.type === "partial") {
802
+ realtimeInterimRef.current = (message.text ?? "").trim();
803
+ onLiveUpdateRef.current?.(realtimeFinalRef.current, realtimeInterimRef.current);
804
+ return;
805
+ }
806
+ if (message.type === "final") {
807
+ const next = (message.text ?? "").trim();
808
+ if (next) {
809
+ realtimeFinalRef.current = [realtimeFinalRef.current.trim(), next]
810
+ .filter(Boolean)
811
+ .join(" ");
812
+ }
813
+ realtimeInterimRef.current = "";
814
+ onLiveUpdateRef.current?.(realtimeFinalRef.current, "");
815
+ return;
816
+ }
817
+ if (message.type === "error") {
818
+ finish(message.error || "Google realtime transcription failed");
819
+ return;
820
+ }
821
+ if (message.type === "end") {
822
+ finish();
823
+ }
824
+ };
825
+ socket.onerror = () => {
826
+ finish("Google realtime transcription connection failed");
827
+ };
828
+ socket.onclose = () => {
829
+ finish();
830
+ };
831
+ socket.onopen = () => {
832
+ if (cancelledRef.current) {
833
+ finish();
834
+ return;
835
+ }
836
+ try {
837
+ socket.send(JSON.stringify({
838
+ type: "start",
839
+ language: (typeof navigator !== "undefined" && navigator.language) ||
840
+ "en-US",
841
+ interimResults: true,
842
+ mimeType,
843
+ }));
844
+ }
845
+ catch (err) {
846
+ finish(err?.message ?? "Could not start realtime stream");
847
+ }
848
+ };
849
+ }, [
850
+ startMeter,
851
+ startTimer,
852
+ teardown,
853
+ failWith,
854
+ getVoiceContextPack,
855
+ replaceContext,
856
+ resolvePath,
857
+ ]);
858
+ const start = useCallback(async () => {
859
+ if (state === "recording" || state === "starting")
860
+ return;
861
+ setErrorMessage(null);
862
+ setState("starting");
863
+ cancelledRef.current = false;
864
+ const prefs = await readVoicePrefs(resolvePath);
865
+ const pref = prefs.provider;
866
+ setProvider(pref);
867
+ // In "auto" mode, prefer browser-native SpeechRecognition when available.
868
+ // It requires no server-side API key, streams words incrementally into the
869
+ // composer, and matches the macros-app record-button experience. Fall back
870
+ // to the server upload path only when SpeechRecognition isn't supported.
871
+ // Explicit server providers (builder, gemini, groq, openai) always use the
872
+ // MediaRecorder → server upload path regardless.
873
+ const resolvedProvider = pref === "auto" && speechSupported
874
+ ? "browser"
875
+ : pref === "auto" ||
876
+ pref === "builder" ||
877
+ pref === "builder-gemini" ||
878
+ pref === "gemini" ||
879
+ pref === "groq"
880
+ ? "openai"
881
+ : pref;
882
+ activeProviderRef.current = resolvedProvider;
883
+ try {
884
+ if (resolvedProvider === "openai") {
885
+ if (!mediaRecorderSupported) {
886
+ throw new Error("Your browser doesn't support audio recording. Use the browser provider in Settings → Voice Transcription.");
887
+ }
888
+ await startOpenAi(pref, prefs.instructions);
889
+ }
890
+ else if (resolvedProvider === "google-realtime") {
891
+ if (!mediaRecorderSupported) {
892
+ throw new Error("Your browser doesn't support audio recording, so Google realtime transcription can't start.");
893
+ }
894
+ await startGoogleRealtime(prefs);
895
+ }
896
+ else {
897
+ await startBrowser(prefs);
898
+ }
899
+ }
900
+ catch (err) {
901
+ if (cancelledRef.current) {
902
+ cancelledRef.current = false;
903
+ teardown();
904
+ setState("idle");
905
+ return;
906
+ }
907
+ failWith(voiceDictationStartErrorMessage(err));
908
+ }
909
+ }, [
910
+ state,
911
+ speechSupported,
912
+ mediaRecorderSupported,
913
+ startOpenAi,
914
+ startGoogleRealtime,
915
+ startBrowser,
916
+ failWith,
917
+ ]);
918
+ const stop = useCallback(() => {
919
+ if (state !== "recording")
920
+ return;
921
+ cancelledRef.current = false;
922
+ if ((activeProviderRef.current === "openai" ||
923
+ activeProviderRef.current === "google-realtime") &&
924
+ mediaRecorderRef.current) {
925
+ try {
926
+ mediaRecorderRef.current.stop();
927
+ }
928
+ catch {
929
+ teardown();
930
+ setState("idle");
931
+ }
932
+ }
933
+ else if (speechRef.current) {
934
+ try {
935
+ speechRef.current.stop();
936
+ }
937
+ catch {
938
+ teardown();
939
+ setState("idle");
940
+ }
941
+ }
942
+ else {
943
+ teardown();
944
+ setState("idle");
945
+ }
946
+ }, [state, teardown]);
947
+ const cancel = useCallback(() => {
948
+ if (state !== "recording" &&
949
+ state !== "starting" &&
950
+ state !== "transcribing")
951
+ return;
952
+ cancelledRef.current = true;
953
+ if ((activeProviderRef.current === "openai" ||
954
+ activeProviderRef.current === "google-realtime") &&
955
+ mediaRecorderRef.current) {
956
+ try {
957
+ mediaRecorderRef.current.stop();
958
+ }
959
+ catch {
960
+ /* ignore */
961
+ }
962
+ }
963
+ else if (speechRef.current) {
964
+ try {
965
+ speechRef.current.abort?.();
966
+ }
967
+ catch {
968
+ /* ignore */
969
+ }
970
+ }
971
+ teardown();
972
+ setState("idle");
973
+ }, [state, teardown]);
974
+ // Auto-dismiss error after 8s so a stale "permission denied" message doesn't
975
+ // sit forever after the user fixes the underlying permission. Manual dismiss
976
+ // (via dismissError) and click-to-retry both also clear the error sooner.
977
+ useEffect(() => {
978
+ if (state !== "error")
979
+ return;
980
+ const handle = setTimeout(() => {
981
+ setErrorMessage(null);
982
+ setState("idle");
983
+ }, 8000);
984
+ return () => clearTimeout(handle);
985
+ }, [state]);
986
+ const dismissError = useCallback(() => {
987
+ setErrorMessage(null);
988
+ if (state === "error")
989
+ setState("idle");
990
+ }, [state]);
991
+ return {
992
+ state,
993
+ amplitude,
994
+ durationMs,
995
+ errorMessage,
996
+ provider,
997
+ supported,
998
+ start,
999
+ stop,
1000
+ cancel,
1001
+ dismissError,
1002
+ };
1003
+ }
1004
+ //# sourceMappingURL=useVoiceDictation.js.map