@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,856 @@
1
+ import { Button } from "@agent-native/toolkit/ui/button";
2
+ import {
3
+ Select,
4
+ SelectContent,
5
+ SelectGroup,
6
+ SelectItem,
7
+ SelectTrigger,
8
+ } from "@agent-native/toolkit/ui/select";
9
+ import {
10
+ IconAlertTriangle,
11
+ IconBrain,
12
+ IconLanguage,
13
+ IconLoader2,
14
+ IconMicrophone,
15
+ IconPhoneOff,
16
+ IconSettings,
17
+ IconVolume,
18
+ } from "@tabler/icons-react";
19
+ import {
20
+ type MouseEvent,
21
+ useCallback,
22
+ useEffect,
23
+ useId,
24
+ useRef,
25
+ useState,
26
+ useSyncExternalStore,
27
+ } from "react";
28
+
29
+ import {
30
+ Popover,
31
+ PopoverAnchor,
32
+ PopoverContent,
33
+ PopoverTrigger,
34
+ } from "../ui/popover.js";
35
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
36
+ import { cn } from "../utils.js";
37
+ import {
38
+ createRealtimeVoiceAudioLevelStore,
39
+ type RealtimeVoiceAudioLevelStore,
40
+ } from "./realtime-voice-audio-level.js";
41
+
42
+ export type RealtimeVoiceModeState =
43
+ | "connecting"
44
+ | "listening"
45
+ | "speaking"
46
+ | "working"
47
+ | "error"
48
+ | "ending";
49
+
50
+ /**
51
+ * User-visible copy stays outside the shared component so host catalogs remain
52
+ * the source of truth. Callers should provide these values through `useT()`.
53
+ */
54
+ export interface RealtimeVoiceModeCopy {
55
+ entryButtonLabel: string;
56
+ promptTitle: string;
57
+ promptDescription: string;
58
+ setupTitle: string;
59
+ setupDescription: string;
60
+ connectBuilder: string;
61
+ useOpenAiKey: string;
62
+ startWithOpenAiKey: string;
63
+ startVoiceMode: string;
64
+ keepDictating: string;
65
+ showChat: string;
66
+ hideChat: string;
67
+ endVoiceMode: string;
68
+ voiceSettings: string;
69
+ settings: {
70
+ microphone: string;
71
+ defaultMicrophone: string;
72
+ microphoneSwitchFailed: string;
73
+ language: string;
74
+ autoLanguage: string;
75
+ languages: Record<
76
+ Exclude<
77
+ import("./useRealtimeVoiceMode.js").RealtimeVoiceLanguage,
78
+ "auto"
79
+ >,
80
+ string
81
+ >;
82
+ intelligence: string;
83
+ intelligenceLevels: Record<
84
+ import("./useRealtimeVoiceMode.js").RealtimeVoiceIntelligence,
85
+ string
86
+ >;
87
+ voiceStyle: string;
88
+ voiceChangePending: string;
89
+ voiceDescriptions: Record<
90
+ import("./useRealtimeVoiceMode.js").RealtimeVoice,
91
+ string
92
+ >;
93
+ };
94
+ status: Record<RealtimeVoiceModeState, string>;
95
+ errors: {
96
+ unsupported: string;
97
+ responseFailed: string;
98
+ sessionFailed: string;
99
+ channelDisconnected: string;
100
+ connectionTimedOut?: string;
101
+ connectionFailed: string;
102
+ offerFailed: string;
103
+ };
104
+ }
105
+
106
+ export interface RealtimeVoiceModeEntryProps {
107
+ copy: RealtimeVoiceModeCopy;
108
+ disabled?: boolean;
109
+ providerStatusPending?: boolean;
110
+ open?: boolean;
111
+ onOpenChange?: (open: boolean) => void;
112
+ onStartVoiceMode: () => void;
113
+ onKeepDictating: () => void;
114
+ setupRequired?: boolean;
115
+ openAiConfigured?: boolean;
116
+ connectingBuilder?: boolean;
117
+ onConnectBuilder?: () => void;
118
+ onUseOpenAiKey?: () => void;
119
+ className?: string;
120
+ }
121
+
122
+ /**
123
+ * Composer mic entry point for apps that support a full-duplex voice session.
124
+ * The first click offers voice mode without silently changing the existing
125
+ * editable-dictation behavior.
126
+ */
127
+ export function RealtimeVoiceModeEntry({
128
+ copy,
129
+ disabled,
130
+ providerStatusPending = false,
131
+ open: controlledOpen,
132
+ onOpenChange,
133
+ onStartVoiceMode,
134
+ onKeepDictating,
135
+ setupRequired = false,
136
+ openAiConfigured = false,
137
+ connectingBuilder = false,
138
+ onConnectBuilder,
139
+ onUseOpenAiKey,
140
+ className,
141
+ }: RealtimeVoiceModeEntryProps) {
142
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
143
+ const open = controlledOpen ?? uncontrolledOpen;
144
+ const titleId = useId();
145
+ const descriptionId = useId();
146
+ const [collisionBoundary, setCollisionBoundary] =
147
+ useState<HTMLElement | null>(null);
148
+
149
+ const setTriggerNode = useCallback((node: HTMLButtonElement | null) => {
150
+ const nextBoundary =
151
+ node?.closest<HTMLElement>(".agent-panel-root") ?? null;
152
+ setCollisionBoundary((currentBoundary) =>
153
+ currentBoundary === nextBoundary ? currentBoundary : nextBoundary,
154
+ );
155
+ }, []);
156
+
157
+ const setOpen = (nextOpen: boolean) => {
158
+ if (controlledOpen === undefined) setUncontrolledOpen(nextOpen);
159
+ onOpenChange?.(nextOpen);
160
+ };
161
+
162
+ const choose = (callback: () => void) => {
163
+ setOpen(false);
164
+ callback();
165
+ };
166
+
167
+ return (
168
+ <Popover open={open} onOpenChange={setOpen}>
169
+ <Tooltip>
170
+ <TooltipTrigger asChild>
171
+ <PopoverTrigger asChild>
172
+ <Button
173
+ ref={setTriggerNode}
174
+ type="button"
175
+ variant="ghost"
176
+ size="icon"
177
+ disabled={disabled}
178
+ aria-label={copy.entryButtonLabel}
179
+ aria-expanded={open}
180
+ className={cn(
181
+ "size-7 shrink-0 text-muted-foreground hover:text-foreground",
182
+ className,
183
+ )}
184
+ >
185
+ <IconMicrophone />
186
+ </Button>
187
+ </PopoverTrigger>
188
+ </TooltipTrigger>
189
+ <TooltipContent>{copy.entryButtonLabel}</TooltipContent>
190
+ </Tooltip>
191
+
192
+ <PopoverContent
193
+ side="top"
194
+ align="end"
195
+ sideOffset={10}
196
+ collisionBoundary={collisionBoundary ?? undefined}
197
+ collisionPadding={16}
198
+ data-collision-boundary={collisionBoundary ? "agent-panel" : "viewport"}
199
+ className={cn(
200
+ "p-4",
201
+ setupRequired
202
+ ? "w-[min(calc(100vw-2rem),var(--radix-popover-content-available-width,30rem),30rem)]"
203
+ : "w-[min(calc(100vw-2rem),var(--radix-popover-content-available-width,22rem),22rem)]",
204
+ )}
205
+ aria-labelledby={titleId}
206
+ aria-describedby={descriptionId}
207
+ >
208
+ <div className="grid gap-3">
209
+ <div className="grid gap-1">
210
+ <h2 id={titleId} className="text-sm font-semibold text-foreground">
211
+ {setupRequired ? copy.setupTitle : copy.promptTitle}
212
+ </h2>
213
+ <p
214
+ id={descriptionId}
215
+ className="text-sm leading-relaxed text-muted-foreground"
216
+ >
217
+ {setupRequired ? copy.setupDescription : copy.promptDescription}
218
+ </p>
219
+ </div>
220
+
221
+ <div className="flex flex-col-reverse gap-2 sm:flex-row sm:flex-wrap sm:justify-end">
222
+ <Button
223
+ type="button"
224
+ variant="ghost"
225
+ size="sm"
226
+ onClick={() => choose(onKeepDictating)}
227
+ >
228
+ {copy.keepDictating}
229
+ </Button>
230
+ {setupRequired ? (
231
+ <>
232
+ <Button
233
+ type="button"
234
+ variant="outline"
235
+ size="sm"
236
+ onClick={() => choose(onUseOpenAiKey ?? onStartVoiceMode)}
237
+ >
238
+ {openAiConfigured
239
+ ? copy.startWithOpenAiKey
240
+ : copy.useOpenAiKey}
241
+ </Button>
242
+ <Button
243
+ type="button"
244
+ size="sm"
245
+ className="whitespace-nowrap"
246
+ disabled={connectingBuilder}
247
+ onClick={() => choose(onConnectBuilder ?? onStartVoiceMode)}
248
+ >
249
+ {connectingBuilder ? (
250
+ <IconLoader2 className="animate-spin" />
251
+ ) : null}
252
+ {copy.connectBuilder}
253
+ </Button>
254
+ </>
255
+ ) : (
256
+ <Button
257
+ type="button"
258
+ size="sm"
259
+ disabled={providerStatusPending}
260
+ onClick={() => choose(onStartVoiceMode)}
261
+ >
262
+ {providerStatusPending ? (
263
+ <IconLoader2 className="animate-spin" />
264
+ ) : (
265
+ <IconMicrophone />
266
+ )}
267
+ {copy.startVoiceMode}
268
+ </Button>
269
+ )}
270
+ </div>
271
+ </div>
272
+ </PopoverContent>
273
+ </Popover>
274
+ );
275
+ }
276
+
277
+ export interface RealtimeVoiceModeDockProps {
278
+ state: RealtimeVoiceModeState;
279
+ copy: RealtimeVoiceModeCopy;
280
+ chatVisible: boolean;
281
+ audioLevels?: RealtimeVoiceAudioLevelStore;
282
+ onToggleChat: () => void;
283
+ onEndVoiceMode: () => void;
284
+ settings?: RealtimeVoiceModeInlineSettings;
285
+ errorMessage?: string | null;
286
+ className?: string;
287
+ }
288
+
289
+ export interface RealtimeVoiceModeSettingOption {
290
+ value: string;
291
+ label: string;
292
+ description?: string;
293
+ }
294
+
295
+ export interface RealtimeVoiceModeSelectSetting {
296
+ label: string;
297
+ value: string;
298
+ options: readonly RealtimeVoiceModeSettingOption[];
299
+ onValueChange: (value: string) => void;
300
+ disabled?: boolean;
301
+ }
302
+
303
+ export interface RealtimeVoiceModeInlineSettings {
304
+ dialogLabel: string;
305
+ appliesNextConversationNote?: string;
306
+ microphoneError?: string;
307
+ microphone: RealtimeVoiceModeSelectSetting;
308
+ language: RealtimeVoiceModeSelectSetting;
309
+ intelligence: RealtimeVoiceModeSelectSetting;
310
+ voiceStyle: RealtimeVoiceModeSelectSetting;
311
+ }
312
+
313
+ const SILENT_AUDIO_LEVELS = createRealtimeVoiceAudioLevelStore();
314
+ const WAVEFORM_WEIGHTS = [0.55, 0.82, 1, 0.82, 0.55];
315
+ const AUDIO_ACTIVITY_THRESHOLD = 0.035;
316
+
317
+ function usePrefersReducedMotion(): boolean {
318
+ const [reduced, setReduced] = useState(false);
319
+ useEffect(() => {
320
+ if (typeof window === "undefined" || !window.matchMedia) return;
321
+ const query = window.matchMedia("(prefers-reduced-motion: reduce)");
322
+ const update = () => setReduced(query.matches);
323
+ update();
324
+ query.addEventListener?.("change", update);
325
+ return () => query.removeEventListener?.("change", update);
326
+ }, []);
327
+ return reduced;
328
+ }
329
+
330
+ function VoiceWaveform({
331
+ level,
332
+ reducedMotion,
333
+ activity,
334
+ }: {
335
+ level: number;
336
+ reducedMotion: boolean;
337
+ activity: "idle" | "user" | "assistant";
338
+ }) {
339
+ const visibleLevel = reducedMotion ? 0.45 : level;
340
+ return (
341
+ <span
342
+ aria-hidden="true"
343
+ className="flex h-6 items-center justify-center gap-0.5"
344
+ data-realtime-voice-waveform="true"
345
+ data-realtime-voice-waveform-activity={activity}
346
+ >
347
+ {WAVEFORM_WEIGHTS.map((weight, index) => (
348
+ <span
349
+ key={index}
350
+ className="h-5 w-0.5 origin-center rounded-full bg-current transition-transform duration-75 ease-out motion-reduce:transition-none"
351
+ style={{
352
+ transform: `scaleY(${0.2 + visibleLevel * 0.8 * weight})`,
353
+ }}
354
+ />
355
+ ))}
356
+ </span>
357
+ );
358
+ }
359
+
360
+ function VoiceConnectingIndicator() {
361
+ return (
362
+ <IconLoader2
363
+ aria-hidden="true"
364
+ className="size-6 animate-spin motion-reduce:animate-none"
365
+ data-realtime-voice-connecting-indicator="true"
366
+ />
367
+ );
368
+ }
369
+
370
+ const ORB_STATE_CLASSES: Record<RealtimeVoiceModeState, string> = {
371
+ connecting:
372
+ "bg-background/65 text-foreground ring-border/60 hover:bg-background/75",
373
+ listening:
374
+ "bg-background/65 text-foreground ring-border/60 hover:bg-background/75",
375
+ speaking:
376
+ "bg-background/65 text-foreground ring-border/60 hover:bg-background/75",
377
+ working:
378
+ "bg-background/65 text-foreground ring-border/60 hover:bg-background/75",
379
+ error:
380
+ "bg-destructive/20 text-destructive ring-destructive/30 hover:bg-destructive/25",
381
+ ending: "cursor-wait bg-background/65 text-muted-foreground ring-border/60",
382
+ };
383
+
384
+ function useChatPanelTranslation(chatVisible: boolean): number {
385
+ const [translation, setTranslation] = useState(0);
386
+
387
+ useEffect(() => {
388
+ if (!chatVisible || typeof window === "undefined") {
389
+ setTranslation(0);
390
+ return;
391
+ }
392
+
393
+ const direction = window.getComputedStyle(
394
+ document.documentElement,
395
+ ).direction;
396
+ const inlineEnd = direction === "rtl" ? "left" : "right";
397
+
398
+ const update = () => {
399
+ const panels = Array.from(
400
+ document.querySelectorAll<HTMLElement>(
401
+ `.agent-sidebar-panel[data-agent-sidebar-position="${inlineEnd}"][data-agent-sidebar-state="open"]`,
402
+ ),
403
+ );
404
+ const panel = panels.find((candidate) => {
405
+ const rect = candidate.getBoundingClientRect();
406
+ return rect.width > 0 && rect.height > 0;
407
+ });
408
+
409
+ if (!panel) {
410
+ setTranslation(0);
411
+ return;
412
+ }
413
+
414
+ // A fullscreen chat has no unobscured side to move into. Keep the dock
415
+ // at its normal edge so it stays reachable instead of pinning it to the
416
+ // opposite side of the same overlay.
417
+ if (panel.dataset.agentSidebarLayout === "fullscreen") {
418
+ setTranslation(0);
419
+ return;
420
+ }
421
+
422
+ const rect = panel.getBoundingClientRect();
423
+ const overlap =
424
+ inlineEnd === "right"
425
+ ? Math.max(0, window.innerWidth - rect.left)
426
+ : Math.max(0, rect.right);
427
+ const maximumShift = Math.max(0, window.innerWidth - 96);
428
+ const distance = Math.min(overlap, maximumShift);
429
+ setTranslation(inlineEnd === "right" ? -distance : distance);
430
+ };
431
+
432
+ update();
433
+ const frame = window.requestAnimationFrame(update);
434
+ window.addEventListener("resize", update);
435
+
436
+ const resizeObserver =
437
+ typeof ResizeObserver === "undefined" ? null : new ResizeObserver(update);
438
+ const observedPanels = new WeakSet<HTMLElement>();
439
+ const observePanels = () => {
440
+ document
441
+ .querySelectorAll<HTMLElement>(".agent-sidebar-panel")
442
+ .forEach((panel) => {
443
+ if (!observedPanels.has(panel)) {
444
+ observedPanels.add(panel);
445
+ resizeObserver?.observe(panel);
446
+ }
447
+ });
448
+ };
449
+ observePanels();
450
+
451
+ const mutationObserver = new MutationObserver(() => {
452
+ observePanels();
453
+ update();
454
+ });
455
+ mutationObserver.observe(document.body, {
456
+ attributes: true,
457
+ attributeFilter: [
458
+ "data-agent-sidebar-layout",
459
+ "data-agent-sidebar-state",
460
+ "style",
461
+ ],
462
+ childList: true,
463
+ subtree: true,
464
+ });
465
+
466
+ return () => {
467
+ window.cancelAnimationFrame(frame);
468
+ window.removeEventListener("resize", update);
469
+ resizeObserver?.disconnect();
470
+ mutationObserver.disconnect();
471
+ };
472
+ }, [chatVisible]);
473
+
474
+ return translation;
475
+ }
476
+
477
+ function VoiceSettingRow({
478
+ icon: Icon,
479
+ setting,
480
+ onSelectOpenChange,
481
+ }: {
482
+ icon: typeof IconLanguage;
483
+ setting: RealtimeVoiceModeSelectSetting;
484
+ onSelectOpenChange: (open: boolean) => void;
485
+ }) {
486
+ const selected = setting.options.find(
487
+ (option) => option.value === setting.value,
488
+ );
489
+
490
+ return (
491
+ <Select
492
+ value={setting.value}
493
+ onValueChange={setting.onValueChange}
494
+ onOpenChange={onSelectOpenChange}
495
+ disabled={setting.disabled}
496
+ >
497
+ <SelectTrigger
498
+ aria-label={`${setting.label}: ${selected?.label ?? setting.value}`}
499
+ className="h-11 rounded-none border-0 bg-transparent px-3 shadow-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
500
+ >
501
+ <div className="flex min-w-0 flex-1 items-center gap-2.5">
502
+ <Icon className="size-4 shrink-0 text-muted-foreground" />
503
+ <span className="shrink-0 text-sm font-medium text-foreground">
504
+ {setting.label}
505
+ </span>
506
+ <span className="ms-auto min-w-0 truncate text-end text-sm text-muted-foreground">
507
+ {selected?.label ?? setting.value}
508
+ </span>
509
+ </div>
510
+ </SelectTrigger>
511
+ <SelectContent
512
+ align="end"
513
+ className="min-w-56"
514
+ data-realtime-voice-setting-options="true"
515
+ >
516
+ <SelectGroup>
517
+ {setting.options.map((option) => (
518
+ <SelectItem key={option.value} value={option.value}>
519
+ <span className="grid gap-0.5">
520
+ <span>{option.label}</span>
521
+ {option.description ? (
522
+ <span className="text-xs text-muted-foreground">
523
+ {option.description}
524
+ </span>
525
+ ) : null}
526
+ </span>
527
+ </SelectItem>
528
+ ))}
529
+ </SelectGroup>
530
+ </SelectContent>
531
+ </Select>
532
+ );
533
+ }
534
+
535
+ function VoiceInlineSettings({
536
+ settings,
537
+ disabled,
538
+ onSelectOpenChange,
539
+ }: {
540
+ settings: RealtimeVoiceModeInlineSettings;
541
+ disabled: boolean;
542
+ onSelectOpenChange: (open: boolean) => void;
543
+ }) {
544
+ const selectedVoice = settings.voiceStyle.options.find(
545
+ (option) => option.value === settings.voiceStyle.value,
546
+ );
547
+
548
+ return (
549
+ <div data-realtime-voice-settings="true">
550
+ <h2 className="sr-only">{settings.dialogLabel}</h2>
551
+ <div className="grid gap-0.5 px-4 pb-3 pt-4 text-center">
552
+ <div className="truncate text-base font-semibold text-foreground">
553
+ {selectedVoice?.label ?? settings.voiceStyle.value}
554
+ </div>
555
+ {selectedVoice?.description ? (
556
+ <div className="truncate text-xs text-muted-foreground">
557
+ {selectedVoice.description}
558
+ </div>
559
+ ) : null}
560
+ </div>
561
+ <div className="mx-2 mb-2 overflow-hidden rounded-xl border border-border/70 bg-background/70">
562
+ <VoiceSettingRow
563
+ icon={IconMicrophone}
564
+ setting={{
565
+ ...settings.microphone,
566
+ disabled: disabled || settings.microphone.disabled,
567
+ }}
568
+ onSelectOpenChange={onSelectOpenChange}
569
+ />
570
+ <div className="mx-3 h-px bg-border/70" />
571
+ <VoiceSettingRow
572
+ icon={IconLanguage}
573
+ setting={{
574
+ ...settings.language,
575
+ disabled: disabled || settings.language.disabled,
576
+ }}
577
+ onSelectOpenChange={onSelectOpenChange}
578
+ />
579
+ <div className="mx-3 h-px bg-border/70" />
580
+ <VoiceSettingRow
581
+ icon={IconBrain}
582
+ setting={{
583
+ ...settings.intelligence,
584
+ disabled: disabled || settings.intelligence.disabled,
585
+ }}
586
+ onSelectOpenChange={onSelectOpenChange}
587
+ />
588
+ <div className="mx-3 h-px bg-border/70" />
589
+ <VoiceSettingRow
590
+ icon={IconVolume}
591
+ setting={{
592
+ ...settings.voiceStyle,
593
+ disabled: disabled || settings.voiceStyle.disabled,
594
+ }}
595
+ onSelectOpenChange={onSelectOpenChange}
596
+ />
597
+ </div>
598
+ {settings.microphoneError ? (
599
+ <p className="px-4 pb-3 text-center text-xs text-destructive">
600
+ {settings.microphoneError}
601
+ </p>
602
+ ) : null}
603
+ {settings.appliesNextConversationNote ? (
604
+ <p className="px-4 pb-3 text-center text-xs text-muted-foreground">
605
+ {settings.appliesNextConversationNote}
606
+ </p>
607
+ ) : null}
608
+ </div>
609
+ );
610
+ }
611
+
612
+ /**
613
+ * Persistent voice-session control. Toggling the main orb only changes chat
614
+ * visibility; ending the realtime session is intentionally a separate action.
615
+ */
616
+ export function RealtimeVoiceModeDock({
617
+ state,
618
+ copy,
619
+ chatVisible,
620
+ audioLevels = SILENT_AUDIO_LEVELS,
621
+ onToggleChat,
622
+ onEndVoiceMode,
623
+ settings,
624
+ errorMessage,
625
+ className,
626
+ }: RealtimeVoiceModeDockProps) {
627
+ const statusId = useId();
628
+ const controlsId = useId();
629
+ const [controlsOpen, setControlsOpen] = useState(false);
630
+ const [settingsOpen, setSettingsOpen] = useState(false);
631
+ const selectInteractionRef = useRef(false);
632
+ const selectInteractionFrameRef = useRef<number | null>(null);
633
+ const levels = useSyncExternalStore(
634
+ audioLevels.subscribe,
635
+ audioLevels.getSnapshot,
636
+ audioLevels.getSnapshot,
637
+ );
638
+ const reducedMotion = usePrefersReducedMotion();
639
+ // Microphone metering begins while the SDP request is still in flight. Keep
640
+ // the connecting affordance authoritative until WebRTC is established;
641
+ // otherwise speaking into the mic replaces the loader with a waveform and
642
+ // makes a stalled connection look like a live call.
643
+ const connected =
644
+ state === "listening" || state === "speaking" || state === "working";
645
+ const activity = connected
646
+ ? levels.output > AUDIO_ACTIVITY_THRESHOLD
647
+ ? "assistant"
648
+ : levels.input > AUDIO_ACTIVITY_THRESHOLD
649
+ ? "user"
650
+ : "idle"
651
+ : "idle";
652
+ const activityLevel = activity === "assistant" ? levels.output : levels.input;
653
+ const toggleLabel = chatVisible ? copy.hideChat : copy.showChat;
654
+ const ending = state === "ending";
655
+ const errorDetailVisible = state === "error" && Boolean(errorMessage);
656
+ const controlsVisible = controlsOpen || settingsOpen;
657
+ const chatPanelTranslation = useChatPanelTranslation(chatVisible);
658
+
659
+ const handleSelectOpenChange = useCallback((open: boolean) => {
660
+ if (selectInteractionFrameRef.current !== null) {
661
+ window.cancelAnimationFrame(selectInteractionFrameRef.current);
662
+ selectInteractionFrameRef.current = null;
663
+ }
664
+ selectInteractionRef.current = true;
665
+ if (!open) {
666
+ // Radix closes the portalled Select before its focus/outside events have
667
+ // fully settled. Keep the parent protected through the current frame.
668
+ selectInteractionFrameRef.current = window.requestAnimationFrame(() => {
669
+ selectInteractionRef.current = false;
670
+ selectInteractionFrameRef.current = null;
671
+ });
672
+ }
673
+ }, []);
674
+
675
+ useEffect(
676
+ () => () => {
677
+ if (selectInteractionFrameRef.current !== null) {
678
+ window.cancelAnimationFrame(selectInteractionFrameRef.current);
679
+ }
680
+ },
681
+ [],
682
+ );
683
+
684
+ const closeControlsUnlessFocused = (event: MouseEvent<HTMLDivElement>) => {
685
+ if (settingsOpen) return;
686
+ if (!event.currentTarget.contains(document.activeElement)) {
687
+ setControlsOpen(false);
688
+ }
689
+ };
690
+
691
+ return (
692
+ <div
693
+ className={cn(
694
+ "pointer-events-none fixed bottom-4 end-4 flex max-w-[calc(100vw-2rem)] flex-col items-end gap-2 transition-transform duration-200 ease-[var(--ease-collapse)] motion-reduce:transition-none",
695
+ className,
696
+ )}
697
+ style={{
698
+ zIndex: 270,
699
+ transform: `translateX(${chatPanelTranslation}px)`,
700
+ }}
701
+ data-realtime-voice-state={state}
702
+ data-realtime-voice-activity={activity}
703
+ data-realtime-voice-chat-offset={chatPanelTranslation}
704
+ >
705
+ {errorDetailVisible ? (
706
+ <div
707
+ role="alert"
708
+ className="pointer-events-auto max-w-xs rounded-lg border border-destructive/30 bg-background px-3 py-2 text-sm text-destructive shadow-md"
709
+ >
710
+ {errorMessage}
711
+ </div>
712
+ ) : null}
713
+
714
+ <div
715
+ className="group pointer-events-auto flex items-center gap-2"
716
+ onMouseEnter={() => setControlsOpen(true)}
717
+ onMouseLeave={closeControlsUnlessFocused}
718
+ onFocusCapture={() => setControlsOpen(true)}
719
+ onBlurCapture={(event) => {
720
+ if (
721
+ !event.currentTarget.contains(event.relatedTarget as Node | null)
722
+ ) {
723
+ setControlsOpen(false);
724
+ }
725
+ }}
726
+ >
727
+ <div
728
+ id={controlsId}
729
+ data-realtime-voice-controls={controlsVisible ? "open" : "closed"}
730
+ className={cn(
731
+ "flex items-center gap-1 rounded-full border border-border/70 bg-background/95 p-1 shadow-lg backdrop-blur-md transition-[transform,opacity] duration-150 ease-out motion-reduce:transition-none",
732
+ controlsVisible
733
+ ? "pointer-events-auto translate-x-0 opacity-100"
734
+ : "pointer-events-none opacity-0 ltr:translate-x-2 rtl:-translate-x-2 group-hover:pointer-events-auto group-hover:translate-x-0 group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:translate-x-0 group-focus-within:opacity-100",
735
+ )}
736
+ >
737
+ <span
738
+ id={statusId}
739
+ role={state === "error" && !errorDetailVisible ? "alert" : "status"}
740
+ aria-live={
741
+ state === "error" && !errorDetailVisible ? "assertive" : "polite"
742
+ }
743
+ className="sr-only"
744
+ >
745
+ {copy.status[state]}
746
+ </span>
747
+
748
+ {settings ? (
749
+ <Popover open={settingsOpen} onOpenChange={setSettingsOpen}>
750
+ <Tooltip>
751
+ <TooltipTrigger asChild>
752
+ <PopoverAnchor asChild>
753
+ <Button
754
+ type="button"
755
+ variant="ghost"
756
+ size="icon"
757
+ disabled={ending}
758
+ onClick={() => setSettingsOpen((open) => !open)}
759
+ aria-label={copy.voiceSettings}
760
+ aria-expanded={settingsOpen}
761
+ className="size-8 rounded-full text-muted-foreground transition-transform duration-150 ease-out active:scale-[0.97]"
762
+ >
763
+ <IconSettings />
764
+ </Button>
765
+ </PopoverAnchor>
766
+ </TooltipTrigger>
767
+ <TooltipContent>{copy.voiceSettings}</TooltipContent>
768
+ </Tooltip>
769
+ <PopoverContent
770
+ side="top"
771
+ align="end"
772
+ sideOffset={10}
773
+ className="w-[min(20rem,calc(100vw-2rem))] overflow-hidden p-0"
774
+ onOpenAutoFocus={(event) => event.preventDefault()}
775
+ onInteractOutside={(event) => {
776
+ const target = event.target;
777
+ if (
778
+ selectInteractionRef.current ||
779
+ (target instanceof Element &&
780
+ target.closest(
781
+ '[data-realtime-voice-setting-options="true"]',
782
+ ))
783
+ ) {
784
+ event.preventDefault();
785
+ }
786
+ }}
787
+ >
788
+ <VoiceInlineSettings
789
+ settings={settings}
790
+ disabled={ending}
791
+ onSelectOpenChange={handleSelectOpenChange}
792
+ />
793
+ </PopoverContent>
794
+ </Popover>
795
+ ) : null}
796
+
797
+ <Tooltip>
798
+ <TooltipTrigger asChild>
799
+ <Button
800
+ type="button"
801
+ variant="ghost"
802
+ size="icon"
803
+ disabled={ending}
804
+ onClick={onEndVoiceMode}
805
+ aria-label={copy.endVoiceMode}
806
+ className="size-8 rounded-full text-destructive transition-transform duration-150 ease-out hover:bg-destructive/10 hover:text-destructive active:scale-[0.97]"
807
+ >
808
+ <IconPhoneOff />
809
+ </Button>
810
+ </TooltipTrigger>
811
+ <TooltipContent>{copy.endVoiceMode}</TooltipContent>
812
+ </Tooltip>
813
+ </div>
814
+
815
+ <Tooltip>
816
+ <TooltipTrigger asChild>
817
+ <Button
818
+ type="button"
819
+ size="icon"
820
+ disabled={ending}
821
+ onClick={() => {
822
+ setControlsOpen(true);
823
+ onToggleChat();
824
+ }}
825
+ aria-label={toggleLabel}
826
+ aria-pressed={chatVisible}
827
+ data-realtime-voice-chat-toggle="orb"
828
+ aria-describedby={statusId}
829
+ aria-controls={controlsId}
830
+ aria-expanded={controlsVisible}
831
+ className={cn(
832
+ "relative isolate size-16 overflow-visible rounded-full ring-1 backdrop-blur-xl transition-transform duration-150 ease-out focus-visible:ring-offset-2 active:scale-[0.97] motion-reduce:transition-none",
833
+ ORB_STATE_CLASSES[state],
834
+ )}
835
+ >
836
+ <span className="relative z-10 flex items-center justify-center">
837
+ {state === "connecting" ? (
838
+ <VoiceConnectingIndicator />
839
+ ) : state !== "error" ? (
840
+ <VoiceWaveform
841
+ level={activityLevel}
842
+ reducedMotion={reducedMotion}
843
+ activity={activity}
844
+ />
845
+ ) : (
846
+ <IconAlertTriangle />
847
+ )}
848
+ </span>
849
+ </Button>
850
+ </TooltipTrigger>
851
+ <TooltipContent>{toggleLabel}</TooltipContent>
852
+ </Tooltip>
853
+ </div>
854
+ </div>
855
+ );
856
+ }