@convai/web-sdk 0.3.1-beta.2 → 0.3.2-beta.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 (287) hide show
  1. package/README.md +535 -1077
  2. package/dist/core/AudioManager.d.ts.map +1 -0
  3. package/dist/core/AudioManager.js +262 -0
  4. package/dist/core/AudioManager.js.map +1 -0
  5. package/dist/core/BlendshapeQueue.d.ts +128 -0
  6. package/dist/core/BlendshapeQueue.d.ts.map +1 -0
  7. package/dist/core/BlendshapeQueue.js +229 -0
  8. package/dist/core/BlendshapeQueue.js.map +1 -0
  9. package/dist/{types/core → core}/ConvaiClient.d.ts +19 -15
  10. package/dist/core/ConvaiClient.d.ts.map +1 -0
  11. package/dist/core/ConvaiClient.js +623 -0
  12. package/dist/core/ConvaiClient.js.map +1 -0
  13. package/dist/core/EventEmitter.d.ts.map +1 -0
  14. package/dist/core/EventEmitter.js +68 -0
  15. package/dist/core/EventEmitter.js.map +1 -0
  16. package/dist/{types/core → core}/MessageHandler.d.ts +7 -0
  17. package/dist/core/MessageHandler.d.ts.map +1 -0
  18. package/dist/core/MessageHandler.js +333 -0
  19. package/dist/core/MessageHandler.js.map +1 -0
  20. package/dist/core/ScreenShareManager.d.ts.map +1 -0
  21. package/dist/core/ScreenShareManager.js +207 -0
  22. package/dist/core/ScreenShareManager.js.map +1 -0
  23. package/dist/core/VideoManager.d.ts.map +1 -0
  24. package/dist/core/VideoManager.js +205 -0
  25. package/dist/core/VideoManager.js.map +1 -0
  26. package/dist/{types/core → core}/index.d.ts +2 -0
  27. package/dist/core/index.d.ts.map +1 -0
  28. package/dist/core/index.js +14 -1970
  29. package/dist/core/index.js.map +1 -0
  30. package/dist/{types/core → core}/types.d.ts +12 -21
  31. package/dist/core/types.d.ts.map +1 -0
  32. package/dist/core/types.js +2 -0
  33. package/dist/core/types.js.map +1 -0
  34. package/dist/dev.d.ts.map +1 -0
  35. package/dist/dev.js +12 -0
  36. package/dist/dev.js.map +1 -0
  37. package/dist/index.d.ts +4 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +6 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/lipsync-helpers/arkitBlendshapeHelpers.d.ts.map +1 -0
  42. package/dist/lipsync-helpers/arkitBlendshapeHelpers.js +201 -0
  43. package/dist/lipsync-helpers/arkitBlendshapeHelpers.js.map +1 -0
  44. package/dist/lipsync-helpers/arkitOrder61.d.ts.map +1 -0
  45. package/dist/lipsync-helpers/arkitOrder61.js +287 -0
  46. package/dist/lipsync-helpers/arkitOrder61.js.map +1 -0
  47. package/dist/lipsync-helpers/arkitPhonemeReference.d.ts.map +1 -0
  48. package/dist/lipsync-helpers/arkitPhonemeReference.js +362 -0
  49. package/dist/lipsync-helpers/arkitPhonemeReference.js.map +1 -0
  50. package/dist/{types/lipsync-helpers → lipsync-helpers}/index.d.ts +1 -0
  51. package/dist/lipsync-helpers/index.d.ts.map +1 -0
  52. package/dist/lipsync-helpers/index.js +20 -1165
  53. package/dist/lipsync-helpers/index.js.map +1 -0
  54. package/dist/lipsync-helpers/metahumanOrder251.d.ts +115 -0
  55. package/dist/lipsync-helpers/metahumanOrder251.d.ts.map +1 -0
  56. package/dist/lipsync-helpers/metahumanOrder251.js +432 -0
  57. package/dist/lipsync-helpers/metahumanOrder251.js.map +1 -0
  58. package/dist/lipsync-helpers/neurosyncBlendshapeMapper.d.ts.map +1 -0
  59. package/dist/lipsync-helpers/neurosyncBlendshapeMapper.js +315 -0
  60. package/dist/lipsync-helpers/neurosyncBlendshapeMapper.js.map +1 -0
  61. package/dist/react/components/ConvaiWidget.d.ts.map +1 -0
  62. package/dist/react/components/ConvaiWidget.js +505 -0
  63. package/dist/react/components/ConvaiWidget.js.map +1 -0
  64. package/dist/react/components/index.d.ts.map +1 -0
  65. package/dist/react/components/index.js +3 -0
  66. package/dist/react/components/index.js.map +1 -0
  67. package/dist/react/components/rtc-widget/components/AudioSettingsPanel.d.ts.map +1 -0
  68. package/dist/react/components/rtc-widget/components/AudioSettingsPanel.js +316 -0
  69. package/dist/react/components/rtc-widget/components/AudioSettingsPanel.js.map +1 -0
  70. package/dist/react/components/rtc-widget/components/AudioVisualizer.d.ts.map +1 -0
  71. package/dist/react/components/rtc-widget/components/AudioVisualizer.js +259 -0
  72. package/dist/react/components/rtc-widget/components/AudioVisualizer.js.map +1 -0
  73. package/dist/react/components/rtc-widget/components/ConviMessage.d.ts.map +1 -0
  74. package/dist/react/components/rtc-widget/components/ConviMessage.js +14 -0
  75. package/dist/react/components/rtc-widget/components/ConviMessage.js.map +1 -0
  76. package/dist/react/components/rtc-widget/components/FloatingVideo.d.ts.map +1 -0
  77. package/dist/react/components/rtc-widget/components/FloatingVideo.js +122 -0
  78. package/dist/react/components/rtc-widget/components/FloatingVideo.js.map +1 -0
  79. package/dist/react/components/rtc-widget/components/MarkdownRenderer.d.ts.map +1 -0
  80. package/dist/react/components/rtc-widget/components/MarkdownRenderer.js +68 -0
  81. package/dist/react/components/rtc-widget/components/MarkdownRenderer.js.map +1 -0
  82. package/dist/react/components/rtc-widget/components/MessageBubble.d.ts.map +1 -0
  83. package/dist/react/components/rtc-widget/components/MessageBubble.js +23 -0
  84. package/dist/react/components/rtc-widget/components/MessageBubble.js.map +1 -0
  85. package/dist/react/components/rtc-widget/components/MessageList.d.ts.map +1 -0
  86. package/dist/react/components/rtc-widget/components/MessageList.js +89 -0
  87. package/dist/react/components/rtc-widget/components/MessageList.js.map +1 -0
  88. package/dist/react/components/rtc-widget/components/UserMessage.d.ts.map +1 -0
  89. package/dist/react/components/rtc-widget/components/UserMessage.js +15 -0
  90. package/dist/react/components/rtc-widget/components/UserMessage.js.map +1 -0
  91. package/dist/react/components/rtc-widget/components/conviComponents/ConviButton.d.ts.map +1 -0
  92. package/dist/react/components/rtc-widget/components/conviComponents/ConviButton.js +15 -0
  93. package/dist/react/components/rtc-widget/components/conviComponents/ConviButton.js.map +1 -0
  94. package/dist/react/components/rtc-widget/components/conviComponents/ConviFooter.d.ts.map +1 -0
  95. package/dist/react/components/rtc-widget/components/conviComponents/ConviFooter.js +172 -0
  96. package/dist/react/components/rtc-widget/components/conviComponents/ConviFooter.js.map +1 -0
  97. package/dist/react/components/rtc-widget/components/conviComponents/ConviHeader.d.ts.map +1 -0
  98. package/dist/react/components/rtc-widget/components/conviComponents/ConviHeader.js +66 -0
  99. package/dist/react/components/rtc-widget/components/conviComponents/ConviHeader.js.map +1 -0
  100. package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.d.ts.map +1 -0
  101. package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.js +68 -0
  102. package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.js.map +1 -0
  103. package/dist/react/components/rtc-widget/components/conviComponents/VoiceModeOverlay.d.ts.map +1 -0
  104. package/dist/react/components/rtc-widget/components/conviComponents/VoiceModeOverlay.js +255 -0
  105. package/dist/react/components/rtc-widget/components/conviComponents/VoiceModeOverlay.js.map +1 -0
  106. package/dist/react/components/rtc-widget/components/conviComponents/index.d.ts.map +1 -0
  107. package/dist/react/components/rtc-widget/components/conviComponents/index.js +6 -0
  108. package/dist/react/components/rtc-widget/components/conviComponents/index.js.map +1 -0
  109. package/dist/react/components/rtc-widget/components/index.d.ts.map +1 -0
  110. package/dist/react/components/rtc-widget/components/index.js +15 -0
  111. package/dist/react/components/rtc-widget/components/index.js.map +1 -0
  112. package/dist/react/components/rtc-widget/index.d.ts.map +1 -0
  113. package/dist/react/components/rtc-widget/index.js +9 -0
  114. package/dist/react/components/rtc-widget/index.js.map +1 -0
  115. package/dist/react/components/rtc-widget/styles/framerConfig.d.ts.map +1 -0
  116. package/dist/react/components/rtc-widget/styles/framerConfig.js +73 -0
  117. package/dist/react/components/rtc-widget/styles/framerConfig.js.map +1 -0
  118. package/dist/react/components/rtc-widget/styles/icons.d.ts.map +1 -0
  119. package/dist/react/components/rtc-widget/styles/icons.js +257 -0
  120. package/dist/react/components/rtc-widget/styles/icons.js.map +1 -0
  121. package/dist/react/components/rtc-widget/styles/index.d.ts.map +1 -0
  122. package/dist/react/components/rtc-widget/styles/index.js +9 -0
  123. package/dist/react/components/rtc-widget/styles/index.js.map +1 -0
  124. package/dist/react/components/rtc-widget/styles/styledComponents.d.ts.map +1 -0
  125. package/dist/react/components/rtc-widget/styles/styledComponents.js +663 -0
  126. package/dist/react/components/rtc-widget/styles/styledComponents.js.map +1 -0
  127. package/dist/react/components/rtc-widget/styles/theme.d.ts.map +1 -0
  128. package/dist/react/components/rtc-widget/styles/theme.js +290 -0
  129. package/dist/react/components/rtc-widget/styles/theme.js.map +1 -0
  130. package/dist/react/components/rtc-widget/types/index.d.ts.map +1 -0
  131. package/dist/react/components/rtc-widget/types/index.js +2 -0
  132. package/dist/react/components/rtc-widget/types/index.js.map +1 -0
  133. package/dist/react/hooks/index.d.ts.map +1 -0
  134. package/dist/react/hooks/index.js +6 -0
  135. package/dist/react/hooks/index.js.map +1 -0
  136. package/dist/react/hooks/useCharacterInfo.d.ts.map +1 -0
  137. package/dist/react/hooks/useCharacterInfo.js +60 -0
  138. package/dist/react/hooks/useCharacterInfo.js.map +1 -0
  139. package/dist/react/hooks/useConvaiClient.d.ts +35 -0
  140. package/dist/react/hooks/useConvaiClient.d.ts.map +1 -0
  141. package/dist/react/hooks/useConvaiClient.js +183 -0
  142. package/dist/react/hooks/useConvaiClient.js.map +1 -0
  143. package/dist/react/hooks/useLocalCameraTrack.d.ts.map +1 -0
  144. package/dist/react/hooks/useLocalCameraTrack.js +34 -0
  145. package/dist/react/hooks/useLocalCameraTrack.js.map +1 -0
  146. package/dist/{types/react → react}/index.d.ts +0 -2
  147. package/dist/react/index.d.ts.map +1 -0
  148. package/dist/react/index.js +13 -0
  149. package/dist/react/index.js.map +1 -0
  150. package/dist/types/index.d.ts +260 -1
  151. package/dist/types/index.d.ts.map +1 -1
  152. package/dist/types/index.js +2 -0
  153. package/dist/types/index.js.map +1 -0
  154. package/dist/utils/LatencyMonitor.d.ts.map +1 -0
  155. package/dist/utils/LatencyMonitor.js +136 -0
  156. package/dist/utils/LatencyMonitor.js.map +1 -0
  157. package/dist/utils/logger.d.ts.map +1 -0
  158. package/dist/utils/logger.js +96 -0
  159. package/dist/utils/logger.js.map +1 -0
  160. package/dist/utils/speakerManagement.d.ts.map +1 -0
  161. package/dist/utils/speakerManagement.js +64 -0
  162. package/dist/utils/speakerManagement.js.map +1 -0
  163. package/dist/{types/vanilla → vanilla}/AudioRenderer.d.ts +5 -0
  164. package/dist/vanilla/AudioRenderer.d.ts.map +1 -0
  165. package/dist/vanilla/AudioRenderer.js +135 -0
  166. package/dist/vanilla/AudioRenderer.js.map +1 -0
  167. package/dist/vanilla/ConvaiWidget.d.ts.map +1 -0
  168. package/dist/vanilla/ConvaiWidget.js +1786 -0
  169. package/dist/vanilla/ConvaiWidget.js.map +1 -0
  170. package/dist/vanilla/icons.d.ts.map +1 -0
  171. package/dist/vanilla/icons.js +222 -0
  172. package/dist/vanilla/icons.js.map +1 -0
  173. package/dist/{types/vanilla → vanilla}/index.d.ts +1 -3
  174. package/dist/vanilla/index.d.ts.map +1 -0
  175. package/dist/vanilla/index.js +20 -5507
  176. package/dist/vanilla/index.js.map +1 -0
  177. package/dist/vanilla/styles.d.ts.map +1 -0
  178. package/dist/vanilla/styles.js +287 -0
  179. package/dist/vanilla/styles.js.map +1 -0
  180. package/dist/vanilla/types.d.ts +43 -0
  181. package/dist/vanilla/types.d.ts.map +1 -0
  182. package/dist/vanilla/types.js +2 -0
  183. package/dist/vanilla/types.js.map +1 -0
  184. package/package.json +33 -38
  185. package/CHANGELOG.md +0 -165
  186. package/dist/core/index.cjs +0 -1977
  187. package/dist/lipsync-helpers/index.cjs +0 -1195
  188. package/dist/types/core/AudioManager.d.ts.map +0 -1
  189. package/dist/types/core/ConvaiClient.d.ts.map +0 -1
  190. package/dist/types/core/EventEmitter.d.ts.map +0 -1
  191. package/dist/types/core/MessageHandler.d.ts.map +0 -1
  192. package/dist/types/core/ScreenShareManager.d.ts.map +0 -1
  193. package/dist/types/core/VideoManager.d.ts.map +0 -1
  194. package/dist/types/core/index.d.ts.map +0 -1
  195. package/dist/types/core/types.d.ts.map +0 -1
  196. package/dist/types/dev.d.ts.map +0 -1
  197. package/dist/types/lipsync-helpers/arkitBlendshapeHelpers.d.ts.map +0 -1
  198. package/dist/types/lipsync-helpers/arkitOrder61.d.ts.map +0 -1
  199. package/dist/types/lipsync-helpers/arkitPhonemeReference.d.ts.map +0 -1
  200. package/dist/types/lipsync-helpers/index.d.ts.map +0 -1
  201. package/dist/types/lipsync-helpers/neurosyncBlendshapeMapper.d.ts.map +0 -1
  202. package/dist/types/react/components/ConvaiWidget.d.ts.map +0 -1
  203. package/dist/types/react/components/index.d.ts.map +0 -1
  204. package/dist/types/react/components/rtc-widget/components/AudioSettingsPanel.d.ts.map +0 -1
  205. package/dist/types/react/components/rtc-widget/components/AudioVisualizer.d.ts.map +0 -1
  206. package/dist/types/react/components/rtc-widget/components/ConviMessage.d.ts.map +0 -1
  207. package/dist/types/react/components/rtc-widget/components/FloatingVideo.d.ts.map +0 -1
  208. package/dist/types/react/components/rtc-widget/components/MarkdownRenderer.d.ts.map +0 -1
  209. package/dist/types/react/components/rtc-widget/components/MessageBubble.d.ts.map +0 -1
  210. package/dist/types/react/components/rtc-widget/components/MessageList.d.ts.map +0 -1
  211. package/dist/types/react/components/rtc-widget/components/UserMessage.d.ts.map +0 -1
  212. package/dist/types/react/components/rtc-widget/components/conviComponents/ConviButton.d.ts.map +0 -1
  213. package/dist/types/react/components/rtc-widget/components/conviComponents/ConviFooter.d.ts.map +0 -1
  214. package/dist/types/react/components/rtc-widget/components/conviComponents/ConviHeader.d.ts.map +0 -1
  215. package/dist/types/react/components/rtc-widget/components/conviComponents/SettingsTray.d.ts.map +0 -1
  216. package/dist/types/react/components/rtc-widget/components/conviComponents/VoiceModeOverlay.d.ts.map +0 -1
  217. package/dist/types/react/components/rtc-widget/components/conviComponents/index.d.ts.map +0 -1
  218. package/dist/types/react/components/rtc-widget/components/index.d.ts.map +0 -1
  219. package/dist/types/react/components/rtc-widget/index.d.ts.map +0 -1
  220. package/dist/types/react/components/rtc-widget/styles/framerConfig.d.ts.map +0 -1
  221. package/dist/types/react/components/rtc-widget/styles/icons.d.ts.map +0 -1
  222. package/dist/types/react/components/rtc-widget/styles/index.d.ts.map +0 -1
  223. package/dist/types/react/components/rtc-widget/styles/styledComponents.d.ts.map +0 -1
  224. package/dist/types/react/components/rtc-widget/styles/theme.d.ts.map +0 -1
  225. package/dist/types/react/components/rtc-widget/types/index.d.ts.map +0 -1
  226. package/dist/types/react/hooks/index.d.ts.map +0 -1
  227. package/dist/types/react/hooks/useCharacterInfo.d.ts.map +0 -1
  228. package/dist/types/react/hooks/useConvaiClient.d.ts +0 -141
  229. package/dist/types/react/hooks/useConvaiClient.d.ts.map +0 -1
  230. package/dist/types/react/hooks/useLocalCameraTrack.d.ts.map +0 -1
  231. package/dist/types/react/index.d.ts.map +0 -1
  232. package/dist/types/types/index.d.ts +0 -261
  233. package/dist/types/types/index.d.ts.map +0 -1
  234. package/dist/types/utils/LatencyMonitor.d.ts.map +0 -1
  235. package/dist/types/utils/logger.d.ts.map +0 -1
  236. package/dist/types/utils/speakerManagement.d.ts.map +0 -1
  237. package/dist/types/vanilla/AudioRenderer.d.ts.map +0 -1
  238. package/dist/types/vanilla/ConvaiWidget.d.ts.map +0 -1
  239. package/dist/types/vanilla/icons.d.ts.map +0 -1
  240. package/dist/types/vanilla/index.d.ts.map +0 -1
  241. package/dist/types/vanilla/styles.d.ts.map +0 -1
  242. package/dist/types/vanilla/types.d.ts +0 -106
  243. package/dist/types/vanilla/types.d.ts.map +0 -1
  244. package/dist/umd/convai.umd.js +0 -1
  245. package/dist/vanilla/index.cjs +0 -5557
  246. /package/dist/{types/core → core}/AudioManager.d.ts +0 -0
  247. /package/dist/{types/core → core}/EventEmitter.d.ts +0 -0
  248. /package/dist/{types/core → core}/ScreenShareManager.d.ts +0 -0
  249. /package/dist/{types/core → core}/VideoManager.d.ts +0 -0
  250. /package/dist/{types/dev.d.ts → dev.d.ts} +0 -0
  251. /package/dist/{types/lipsync-helpers → lipsync-helpers}/arkitBlendshapeHelpers.d.ts +0 -0
  252. /package/dist/{types/lipsync-helpers → lipsync-helpers}/arkitOrder61.d.ts +0 -0
  253. /package/dist/{types/lipsync-helpers → lipsync-helpers}/arkitPhonemeReference.d.ts +0 -0
  254. /package/dist/{types/lipsync-helpers → lipsync-helpers}/neurosyncBlendshapeMapper.d.ts +0 -0
  255. /package/dist/{types/react → react}/components/ConvaiWidget.d.ts +0 -0
  256. /package/dist/{types/react → react}/components/index.d.ts +0 -0
  257. /package/dist/{types/react → react}/components/rtc-widget/components/AudioSettingsPanel.d.ts +0 -0
  258. /package/dist/{types/react → react}/components/rtc-widget/components/AudioVisualizer.d.ts +0 -0
  259. /package/dist/{types/react → react}/components/rtc-widget/components/ConviMessage.d.ts +0 -0
  260. /package/dist/{types/react → react}/components/rtc-widget/components/FloatingVideo.d.ts +0 -0
  261. /package/dist/{types/react → react}/components/rtc-widget/components/MarkdownRenderer.d.ts +0 -0
  262. /package/dist/{types/react → react}/components/rtc-widget/components/MessageBubble.d.ts +0 -0
  263. /package/dist/{types/react → react}/components/rtc-widget/components/MessageList.d.ts +0 -0
  264. /package/dist/{types/react → react}/components/rtc-widget/components/UserMessage.d.ts +0 -0
  265. /package/dist/{types/react → react}/components/rtc-widget/components/conviComponents/ConviButton.d.ts +0 -0
  266. /package/dist/{types/react → react}/components/rtc-widget/components/conviComponents/ConviFooter.d.ts +0 -0
  267. /package/dist/{types/react → react}/components/rtc-widget/components/conviComponents/ConviHeader.d.ts +0 -0
  268. /package/dist/{types/react → react}/components/rtc-widget/components/conviComponents/SettingsTray.d.ts +0 -0
  269. /package/dist/{types/react → react}/components/rtc-widget/components/conviComponents/VoiceModeOverlay.d.ts +0 -0
  270. /package/dist/{types/react → react}/components/rtc-widget/components/conviComponents/index.d.ts +0 -0
  271. /package/dist/{types/react → react}/components/rtc-widget/components/index.d.ts +0 -0
  272. /package/dist/{types/react → react}/components/rtc-widget/index.d.ts +0 -0
  273. /package/dist/{types/react → react}/components/rtc-widget/styles/framerConfig.d.ts +0 -0
  274. /package/dist/{types/react → react}/components/rtc-widget/styles/icons.d.ts +0 -0
  275. /package/dist/{types/react → react}/components/rtc-widget/styles/index.d.ts +0 -0
  276. /package/dist/{types/react → react}/components/rtc-widget/styles/styledComponents.d.ts +0 -0
  277. /package/dist/{types/react → react}/components/rtc-widget/styles/theme.d.ts +0 -0
  278. /package/dist/{types/react → react}/components/rtc-widget/types/index.d.ts +0 -0
  279. /package/dist/{types/react → react}/hooks/index.d.ts +0 -0
  280. /package/dist/{types/react → react}/hooks/useCharacterInfo.d.ts +0 -0
  281. /package/dist/{types/react → react}/hooks/useLocalCameraTrack.d.ts +0 -0
  282. /package/dist/{types/utils → utils}/LatencyMonitor.d.ts +0 -0
  283. /package/dist/{types/utils → utils}/logger.d.ts +0 -0
  284. /package/dist/{types/utils → utils}/speakerManagement.d.ts +0 -0
  285. /package/dist/{types/vanilla → vanilla}/ConvaiWidget.d.ts +0 -0
  286. /package/dist/{types/vanilla → vanilla}/icons.d.ts +0 -0
  287. /package/dist/{types/vanilla → vanilla}/styles.d.ts +0 -0
@@ -0,0 +1,1786 @@
1
+ /**
2
+ * Vanilla ConvaiWidget - Complete UI widget for Convai conversations
3
+ * Ports the React ConvaiWidget to vanilla TypeScript with DOM manipulation
4
+ */
5
+ import { AudioRenderer } from "./AudioRenderer";
6
+ import { aeroTheme, injectGlobalStyles } from "./styles";
7
+ import { Icons } from "./icons";
8
+ /**
9
+ * Create a Convai chat widget in the specified container
10
+ *
11
+ * @param container - HTML element to attach the widget to
12
+ * @param options - Widget configuration options
13
+ * @returns VanillaWidget instance with destroy method
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { ConvaiClient, createConvaiWidget } from '@convai/web-sdk/vanilla';
18
+ *
19
+ * const client = new ConvaiClient();
20
+ * await client.connect({
21
+ * apiKey: 'your-api-key',
22
+ * characterId: 'your-character-id'
23
+ * });
24
+ *
25
+ * const widget = createConvaiWidget(document.body, {
26
+ * convaiClient: client,
27
+ * showVideo: true,
28
+ * showScreenShare: true,
29
+ * defaultVoiceMode: true
30
+ * });
31
+ *
32
+ * // Later, cleanup
33
+ * widget.destroy();
34
+ * ```
35
+ */
36
+ export function createConvaiWidget(container, options) {
37
+ const { convaiClient, showVideo = true, showScreenShare = true, defaultVoiceMode = true } = options;
38
+ // Inject global styles
39
+ injectGlobalStyles();
40
+ // State
41
+ let isOpen = false;
42
+ let isSettingsOpen = false;
43
+ let isVoiceMode = defaultVoiceMode;
44
+ let isMuted = false;
45
+ let isVideoVisible = false;
46
+ let inputValue = "";
47
+ let characterName = "Character";
48
+ let characterImage = "";
49
+ let audioRenderer = null;
50
+ let hasEnteredDefaultVoiceMode = false;
51
+ // DOM elements (will be created below)
52
+ let rootElement;
53
+ let morphingContainer;
54
+ let buttonContent;
55
+ let chatContent;
56
+ let headerElement;
57
+ let contentElement;
58
+ let footerElement;
59
+ let inputElement;
60
+ let messageListElement;
61
+ let settingsTray;
62
+ let floatingVideo;
63
+ let voiceModeOverlay;
64
+ // Audio Analysis State
65
+ let audioContext = null;
66
+ let analyzer = null;
67
+ let dataArray = null;
68
+ let rafId = null;
69
+ let source = null;
70
+ // Fetch character info - matches React useCharacterInfo hook
71
+ const fetchCharacterInfo = async () => {
72
+ if (!convaiClient.apiKey || !convaiClient.characterId)
73
+ return;
74
+ try {
75
+ const response = await fetch("https://api.convai.com/character/get", {
76
+ method: "POST",
77
+ headers: {
78
+ "Content-Type": "application/json",
79
+ "CONVAI-API-KEY": convaiClient.apiKey,
80
+ },
81
+ body: JSON.stringify({ charID: convaiClient.characterId }),
82
+ });
83
+ if (response.ok) {
84
+ const data = await response.json();
85
+ // Extract character name and image with fallbacks - matches React version
86
+ characterName = data.character_name || "Convi";
87
+ characterImage =
88
+ data.model_details?.METAHUMAN?.avatar_image_square ||
89
+ data.model_details?.METAHUMAN?.avatar_image ||
90
+ data.model_details?.modelPlaceholder ||
91
+ "";
92
+ updateHeader();
93
+ }
94
+ }
95
+ catch (error) {
96
+ console.error("Failed to fetch character info:", error);
97
+ }
98
+ };
99
+ // Create root structure
100
+ const createDOM = () => {
101
+ rootElement = document.createElement("div");
102
+ rootElement.className = "convai-widget";
103
+ rootElement.style.cssText = `
104
+ position: fixed;
105
+ bottom: 1.5rem;
106
+ right: 1.5rem;
107
+ z-index: ${aeroTheme.zIndex.modal};
108
+ font-family: ${aeroTheme.typography.fontFamily.primary};
109
+ `;
110
+ // Morphing container
111
+ morphingContainer = document.createElement("div");
112
+ morphingContainer.style.cssText = `
113
+ position: relative;
114
+ width: 4rem;
115
+ height: 4rem;
116
+ background: ${aeroTheme.colors.glass.backdrop};
117
+ backdrop-filter: ${aeroTheme.glass.backdrop};
118
+ border: ${aeroTheme.glass.border};
119
+ border-radius: 50%;
120
+ box-shadow: ${aeroTheme.shadows.glass};
121
+ transition: all 0.3s ease-in-out;
122
+ overflow: hidden;
123
+ display: flex;
124
+ align-items: center;
125
+ justify-content: center;
126
+ cursor: pointer;
127
+ `;
128
+ // Button content (logo)
129
+ buttonContent = document.createElement("div");
130
+ buttonContent.style.cssText = `
131
+ position: absolute;
132
+ inset: 0;
133
+ display: flex;
134
+ align-items: center;
135
+ justify-content: center;
136
+ transition: opacity 0.3s, transform 0.3s;
137
+ opacity: 1;
138
+ transform: scale(1);
139
+ `;
140
+ const convaiLogo = Icons.ConvaiLogo("xl", "idle");
141
+ convaiLogo.style.color = aeroTheme.colors.convai.light;
142
+ buttonContent.appendChild(convaiLogo);
143
+ // Chat content
144
+ chatContent = document.createElement("div");
145
+ chatContent.style.cssText = `
146
+ position: absolute;
147
+ inset: 0;
148
+ display: flex;
149
+ flex-direction: column;
150
+ opacity: 0;
151
+ transform: scale(0.8);
152
+ transition: opacity 0.2s, transform 0.2s;
153
+ pointer-events: none;
154
+ `;
155
+ // Voice Mode Overlay
156
+ voiceModeOverlay = createVoiceModeOverlay();
157
+ chatContent.appendChild(voiceModeOverlay);
158
+ // Header
159
+ headerElement = createHeader();
160
+ // Content area
161
+ contentElement = document.createElement("div");
162
+ contentElement.style.cssText = `
163
+ flex: 1;
164
+ overflow-y: auto;
165
+ padding: 1rem;
166
+ background: transparent;
167
+ `;
168
+ messageListElement = createMessageList();
169
+ contentElement.appendChild(messageListElement);
170
+ // Footer
171
+ footerElement = createFooter();
172
+ chatContent.appendChild(headerElement);
173
+ chatContent.appendChild(contentElement);
174
+ chatContent.appendChild(footerElement);
175
+ morphingContainer.appendChild(buttonContent);
176
+ morphingContainer.appendChild(chatContent);
177
+ rootElement.appendChild(morphingContainer);
178
+ // Settings tray
179
+ settingsTray = createSettingsTray();
180
+ rootElement.appendChild(settingsTray);
181
+ // Floating video
182
+ floatingVideo = createFloatingVideo();
183
+ container.appendChild(floatingVideo);
184
+ container.appendChild(rootElement);
185
+ // Event listeners
186
+ morphingContainer.addEventListener("click", handleToggle);
187
+ };
188
+ // Create Voice Mode Overlay
189
+ const createVoiceModeOverlay = () => {
190
+ const overlay = document.createElement("div");
191
+ overlay.style.cssText = `
192
+ position: absolute;
193
+ top: 50%;
194
+ left: 50%;
195
+ transform: translate(-50%, -50%);
196
+ text-align: center;
197
+ padding: 1rem;
198
+ z-index: 10;
199
+ pointer-events: auto;
200
+ display: none;
201
+ flex-direction: column;
202
+ align-items: center;
203
+ gap: 1.5rem;
204
+ `;
205
+ // Bars Container
206
+ const barsContainer = document.createElement("div");
207
+ barsContainer.id = "voice-bars-container";
208
+ barsContainer.style.cssText = `
209
+ display: flex;
210
+ align-items: center;
211
+ justify-content: center;
212
+ gap: 2px;
213
+ height: 80px;
214
+ max-width: 300px;
215
+ `;
216
+ // Create 40 bars
217
+ for (let i = 0; i < 40; i++) {
218
+ const bar = document.createElement("div");
219
+ bar.className = "voice-bar";
220
+ bar.style.cssText = `
221
+ width: 3px;
222
+ height: 15px;
223
+ background-color: ${aeroTheme.colors.neutral[400]};
224
+ border-radius: 1.5px;
225
+ transition: height 0.08s ease-out, background-color 0.2s;
226
+ transform-origin: center;
227
+ `;
228
+ barsContainer.appendChild(bar);
229
+ }
230
+ overlay.appendChild(barsContainer);
231
+ // Status Text
232
+ const statusContainer = document.createElement("div");
233
+ const statusTitle = document.createElement("div");
234
+ statusTitle.id = "voice-mode-title";
235
+ statusTitle.style.cssText = `
236
+ font-size: 14px;
237
+ font-weight: 500;
238
+ color: ${aeroTheme.colors.text.primary};
239
+ margin-bottom: 0.5rem;
240
+ `;
241
+ statusTitle.textContent = "Voice Only Mode";
242
+ const statusSubtitle = document.createElement("div");
243
+ statusSubtitle.id = "voice-mode-subtitle";
244
+ statusSubtitle.style.cssText = `
245
+ font-size: 12px;
246
+ color: ${aeroTheme.colors.text.secondary};
247
+ `;
248
+ statusSubtitle.textContent = "Press and hold the microphone to talk";
249
+ statusContainer.appendChild(statusTitle);
250
+ statusContainer.appendChild(statusSubtitle);
251
+ overlay.appendChild(statusContainer);
252
+ return overlay;
253
+ };
254
+ // Audio Analysis State for Voice Mode
255
+ let audioLevels = Array(40).fill(0);
256
+ let targetLevels = Array(40).fill(0.05);
257
+ let currentLevels = Array(40).fill(0.05);
258
+ let startTime = 0;
259
+ // Audio Analysis Logic
260
+ const updateAudioBars = () => {
261
+ if (!voiceModeOverlay)
262
+ return;
263
+ const bars = voiceModeOverlay.querySelectorAll(".voice-bar");
264
+ const isTalking = convaiClient.state.isSpeaking;
265
+ const isListening = !convaiClient.audioControls.isAudioMuted;
266
+ const isAnimating = isListening || isTalking;
267
+ // Update colors based on state
268
+ bars.forEach((bar) => {
269
+ bar.style.backgroundColor = isTalking
270
+ ? aeroTheme.colors.convai.light
271
+ : isListening
272
+ ? aeroTheme.colors.text.primary
273
+ : aeroTheme.colors.neutral[400];
274
+ });
275
+ // Update Text
276
+ const title = document.getElementById("voice-mode-title");
277
+ const subtitle = document.getElementById("voice-mode-subtitle");
278
+ if (title) {
279
+ title.textContent = isTalking
280
+ ? "Character Speaking..."
281
+ : isListening
282
+ ? "Listening..."
283
+ : "Voice Only Mode";
284
+ }
285
+ if (subtitle) {
286
+ subtitle.textContent =
287
+ isListening || isTalking
288
+ ? "Audio active"
289
+ : "Press and hold the microphone to talk";
290
+ }
291
+ // Animation Logic - Matches React version
292
+ if (isListening && analyzer && dataArray) {
293
+ // Use time domain data (waveform) instead of frequency
294
+ // @ts-ignore - TypeScript strict mode issue with Uint8Array type
295
+ analyzer.getByteTimeDomainData(dataArray);
296
+ // Calculate RMS (Root Mean Square) for volume
297
+ let sum = 0;
298
+ for (let i = 0; i < dataArray.length; i++) {
299
+ const normalized = (dataArray[i] - 128) / 128; // Center around 0
300
+ sum += normalized * normalized;
301
+ }
302
+ const rms = Math.sqrt(sum / dataArray.length);
303
+ // Apply some scaling and clamping
304
+ const volume = Math.min(1, rms * 3); // Boost sensitivity
305
+ // Create left-to-right wave effect
306
+ const minHeight = 8;
307
+ const maxHeight = 70;
308
+ bars.forEach((bar, i) => {
309
+ // Progressive wave from left to right
310
+ const position = i / 40; // 0 to 1 from left to right
311
+ const wavePhase = Date.now() / 300 + position * Math.PI * 2;
312
+ const waveVariation = Math.sin(wavePhase) * 0.15 + 0.85; // 0.7 to 1.0
313
+ const level = volume * waveVariation;
314
+ const height = minHeight + level * (maxHeight - minHeight);
315
+ bar.style.height = `${Math.max(minHeight, height)}px`;
316
+ });
317
+ }
318
+ else if (isTalking) {
319
+ // Simulate speaking bars with natural speech patterns
320
+ const elapsed = (Date.now() - startTime) / 1000; // seconds
321
+ // Generate new random target levels occasionally (simulating syllables/words)
322
+ if (Math.random() < 0.08) {
323
+ // 8% chance per frame = ~5 times per second
324
+ targetLevels = Array(40)
325
+ .fill(0)
326
+ .map((_, i) => {
327
+ // More variation in the middle bars, less on edges for natural spread
328
+ const position = i / 40;
329
+ const centerWeight = 1 - Math.abs(position - 0.5) * 0.5;
330
+ // Random peaks and valleys like speech patterns
331
+ const randomPeak = 0.2 + Math.random() * 0.7; // 0.2 to 0.9
332
+ // Add some neighbor correlation so bars don't jump independently
333
+ const prevTarget = targetLevels[i] || 0.3;
334
+ const correlation = prevTarget * 0.4 + randomPeak * 0.6;
335
+ return correlation * centerWeight;
336
+ });
337
+ }
338
+ // Smoothly interpolate current levels toward targets (organic movement)
339
+ currentLevels = currentLevels.map((current, i) => {
340
+ const target = targetLevels[i];
341
+ const speed = 0.2; // Smooth but responsive
342
+ return current + (target - current) * speed;
343
+ });
344
+ // Apply a gentle fade-in for the first 0.3 seconds
345
+ const fadeIn = Math.min(1, elapsed / 0.3);
346
+ const minHeight = 8;
347
+ const maxHeight = 70;
348
+ bars.forEach((bar, i) => {
349
+ // Small random jitter for micro-variation
350
+ const microJitter = 0.95 + Math.random() * 0.1; // 0.95 to 1.05
351
+ const level = Math.max(0.05, Math.min(1, currentLevels[i] * fadeIn * microJitter));
352
+ const height = minHeight + level * (maxHeight - minHeight);
353
+ bar.style.height = `${Math.max(minHeight, height)}px`;
354
+ });
355
+ }
356
+ else {
357
+ // Reset to idle state
358
+ bars.forEach((bar) => {
359
+ bar.style.height = "15px";
360
+ });
361
+ }
362
+ rafId = requestAnimationFrame(updateAudioBars);
363
+ };
364
+ const startAudioAnalysis = async () => {
365
+ try {
366
+ if (!audioContext) {
367
+ audioContext = new (window.AudioContext ||
368
+ window.webkitAudioContext)();
369
+ }
370
+ if (audioContext.state === "suspended") {
371
+ await audioContext.resume();
372
+ }
373
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
374
+ analyzer = audioContext.createAnalyser();
375
+ analyzer.fftSize = 256;
376
+ analyzer.smoothingTimeConstant = 0.7;
377
+ source = audioContext.createMediaStreamSource(stream);
378
+ source.connect(analyzer);
379
+ dataArray = new Uint8Array(analyzer.fftSize);
380
+ updateAudioBars();
381
+ }
382
+ catch (e) {
383
+ console.error("Audio analysis setup failed:", e);
384
+ }
385
+ };
386
+ const stopAudioAnalysis = () => {
387
+ if (rafId)
388
+ cancelAnimationFrame(rafId);
389
+ if (source) {
390
+ source.disconnect();
391
+ source.mediaStream.getTracks().forEach((t) => t.stop());
392
+ source = null;
393
+ }
394
+ // Keep context alive if possible, or close it? React component closes it.
395
+ // We can keep it for reuse or close. Let's keep it simple.
396
+ };
397
+ // Create header
398
+ const createHeader = () => {
399
+ const header = document.createElement("div");
400
+ header.style.cssText = `
401
+ display: flex;
402
+ align-items: center;
403
+ justify-content: space-between;
404
+ padding: 1rem;
405
+ border-bottom: 1px solid ${aeroTheme.colors.neutral[200]};
406
+ background: white;
407
+ position: relative;
408
+ `;
409
+ // Close button on the left
410
+ const closeButton = document.createElement("button");
411
+ const chevronIcon = Icons.ChevronDown("md");
412
+ closeButton.appendChild(chevronIcon);
413
+ closeButton.style.cssText = `
414
+ font-size: 1.25rem;
415
+ color: ${aeroTheme.colors.text.secondary};
416
+ cursor: pointer;
417
+ padding: 0.25rem;
418
+ transition: ${aeroTheme.transitions.fast};
419
+ background: transparent;
420
+ border: none;
421
+ `;
422
+ closeButton.addEventListener("click", (e) => {
423
+ e.stopPropagation();
424
+ handleClose();
425
+ });
426
+ // Title section centered - matches React version
427
+ const titleSection = document.createElement("div");
428
+ titleSection.style.cssText = `
429
+ position: absolute;
430
+ left: 50%;
431
+ transform: translateX(-50%);
432
+ display: flex;
433
+ align-items: center;
434
+ gap: 0.5rem;
435
+ font-size: ${aeroTheme.typography.fontSize.base};
436
+ font-weight: ${aeroTheme.typography.fontWeight.semibold};
437
+ color: ${aeroTheme.colors.text.primary};
438
+ `;
439
+ titleSection.id = "convai-widget-title";
440
+ // Settings button on the right
441
+ const settingsButton = document.createElement("button");
442
+ const moreIcon = Icons.MoreVertical("md");
443
+ settingsButton.appendChild(moreIcon);
444
+ settingsButton.style.cssText = `
445
+ font-size: 1.5rem;
446
+ color: ${aeroTheme.colors.text.secondary};
447
+ cursor: pointer;
448
+ padding: 0.25rem;
449
+ transition: ${aeroTheme.transitions.fast};
450
+ background: transparent;
451
+ border: none;
452
+ margin-left: auto;
453
+ `;
454
+ settingsButton.addEventListener("click", (e) => {
455
+ e.stopPropagation();
456
+ handleSettingsToggle();
457
+ });
458
+ header.appendChild(closeButton);
459
+ header.appendChild(titleSection);
460
+ header.appendChild(settingsButton);
461
+ return header;
462
+ };
463
+ // Update header with character info
464
+ const updateHeader = () => {
465
+ const titleSection = document.getElementById("convai-widget-title");
466
+ if (!titleSection)
467
+ return;
468
+ titleSection.innerHTML = "";
469
+ if (characterImage) {
470
+ const img = document.createElement("img");
471
+ img.src = characterImage;
472
+ img.alt = characterName;
473
+ img.style.cssText = `
474
+ width: 1.5rem;
475
+ height: 1.5rem;
476
+ border-radius: 50%;
477
+ object-fit: cover;
478
+ border: 1.5px solid ${getBotStatusColor().color};
479
+ box-shadow: 0 0 6px ${getBotStatusColor().color}40;
480
+ transition: all 0.3s ease;
481
+ `;
482
+ titleSection.appendChild(img);
483
+ }
484
+ const nameSpan = document.createElement("span");
485
+ nameSpan.textContent = characterName;
486
+ titleSection.appendChild(nameSpan);
487
+ // Mute button
488
+ const muteButton = document.createElement("button");
489
+ muteButton.innerHTML = "";
490
+ const volumeIcon = isMuted
491
+ ? Icons.VolumeMute("sm")
492
+ : Icons.VolumeHigh("sm");
493
+ volumeIcon.style.color = isMuted ? "#919EABA6" : "#0E7360";
494
+ muteButton.appendChild(volumeIcon);
495
+ muteButton.style.cssText = `
496
+ cursor: pointer;
497
+ background: transparent;
498
+ border: none;
499
+ box-shadow: none;
500
+ width: auto;
501
+ height: auto;
502
+ padding: 0;
503
+ display: inline-flex;
504
+ align-items: center;
505
+ margin-left: 0.5rem;
506
+ outline: none;
507
+ transition: transform 0.1s ease-out;
508
+ `;
509
+ muteButton.addEventListener("click", (e) => {
510
+ e.stopPropagation();
511
+ handleToggleMute();
512
+ });
513
+ muteButton.addEventListener("mouseenter", () => {
514
+ muteButton.style.transform = "scale(1.1)";
515
+ });
516
+ muteButton.addEventListener("mouseleave", () => {
517
+ muteButton.style.transform = "scale(1)";
518
+ });
519
+ titleSection.appendChild(muteButton);
520
+ // Voice Mode Badge
521
+ if (isVoiceMode) {
522
+ const voiceBadge = document.createElement("span");
523
+ voiceBadge.textContent = "VOICE";
524
+ voiceBadge.style.cssText = `
525
+ font-size: 10px;
526
+ color: ${aeroTheme.colors.convai.light};
527
+ font-weight: 500;
528
+ margin-left: 8px;
529
+ padding: 2px 6px;
530
+ border-radius: 4px;
531
+ background-color: ${aeroTheme.colors.convai.light}20;
532
+ `;
533
+ titleSection.appendChild(voiceBadge);
534
+ }
535
+ };
536
+ // Create message list
537
+ const createMessageList = () => {
538
+ const list = document.createElement("div");
539
+ list.id = "convai-message-list";
540
+ list.style.cssText = `
541
+ display: flex;
542
+ flex-direction: column;
543
+ gap: 0.75rem;
544
+ min-height: 100%;
545
+ `;
546
+ return list;
547
+ };
548
+ // Create footer
549
+ const createFooter = () => {
550
+ const footer = document.createElement("div");
551
+ footer.style.cssText = `
552
+ padding: 1rem;
553
+ border-top: 1px solid ${aeroTheme.colors.neutral[200]};
554
+ background: white;
555
+ display: flex;
556
+ gap: 0.5rem;
557
+ align-items: center;
558
+ position: relative;
559
+ `;
560
+ // Voice Mode Exit Button (Initially hidden)
561
+ const voiceExitButton = document.createElement("button");
562
+ voiceExitButton.id = "convai-voice-exit-btn";
563
+ const exitIcon = Icons.PhoneOff("md");
564
+ voiceExitButton.appendChild(exitIcon);
565
+ voiceExitButton.style.cssText = `
566
+ width: 2.25rem;
567
+ height: 2.25rem;
568
+ border-radius: 50%;
569
+ background: ${aeroTheme.colors.error[500]};
570
+ color: white;
571
+ display: none; /* Hidden by default */
572
+ align-items: center;
573
+ justify-content: center;
574
+ cursor: pointer;
575
+ margin: 0 auto;
576
+ border: none;
577
+ `;
578
+ voiceExitButton.addEventListener("click", async () => {
579
+ convaiClient.sendInterruptMessage();
580
+ await convaiClient.audioControls.muteAudio(); // Mute on exit
581
+ isVoiceMode = false;
582
+ updateVoiceMode();
583
+ });
584
+ footer.appendChild(voiceExitButton);
585
+ // Standard Footer Content (Mic + Input)
586
+ const standardContent = document.createElement("div");
587
+ standardContent.id = "convai-footer-standard";
588
+ standardContent.style.cssText = `
589
+ display: flex;
590
+ gap: 0.5rem;
591
+ align-items: center;
592
+ width: 100%;
593
+ `;
594
+ // Input container (microphone button removed from text mode)
595
+ const inputContainer = document.createElement("div");
596
+ inputContainer.style.cssText = `
597
+ flex: 1;
598
+ position: relative;
599
+ display: flex;
600
+ align-items: center;
601
+ `;
602
+ inputElement = document.createElement("input");
603
+ inputElement.type = "text";
604
+ inputElement.placeholder = "Conversation";
605
+ inputElement.style.cssText = `
606
+ width: 100%;
607
+ padding: 0.75rem 3rem 0.75rem 1rem;
608
+ border-radius: ${aeroTheme.borderRadius.full};
609
+ border: 1px solid ${aeroTheme.colors.neutral[300]};
610
+ background: ${aeroTheme.colors.glass.medium};
611
+ color: ${aeroTheme.colors.text.primary};
612
+ font-size: ${aeroTheme.typography.fontSize.sm};
613
+ transition: ${aeroTheme.transitions.fast};
614
+ outline: none;
615
+ `;
616
+ inputElement.addEventListener("input", (e) => {
617
+ inputValue = e.target.value;
618
+ updateSendButton();
619
+ });
620
+ inputElement.addEventListener("keypress", (e) => {
621
+ if (e.key === "Enter")
622
+ handleSend();
623
+ });
624
+ // Send button
625
+ const sendButton = document.createElement("button");
626
+ sendButton.id = "convai-send-button";
627
+ const sendIcon = Icons.Send("md");
628
+ sendButton.appendChild(sendIcon);
629
+ sendButton.style.cssText = `
630
+ position: absolute;
631
+ right: 0.375rem;
632
+ width: 2.25rem;
633
+ height: 2.25rem;
634
+ border-radius: 50%;
635
+ background: transparent; /* Initial transparent for voice toggle */
636
+ color: ${aeroTheme.colors.text.primary};
637
+ display: flex;
638
+ align-items: center;
639
+ justify-content: center;
640
+ cursor: pointer;
641
+ transition: ${aeroTheme.transitions.fast};
642
+ border: none;
643
+ `;
644
+ sendButton.addEventListener("click", async () => {
645
+ if (inputValue.length > 0) {
646
+ handleSend();
647
+ }
648
+ else {
649
+ // Toggle Voice Mode
650
+ convaiClient.sendInterruptMessage();
651
+ await convaiClient.audioControls.unmuteAudio(); // Unmute on enter
652
+ isVoiceMode = true;
653
+ updateVoiceMode();
654
+ }
655
+ });
656
+ inputContainer.appendChild(inputElement);
657
+ inputContainer.appendChild(sendButton);
658
+ standardContent.appendChild(inputContainer);
659
+ footer.appendChild(standardContent);
660
+ return footer;
661
+ };
662
+ // Create settings tray - Matches React SettingsTray component exactly
663
+ const createSettingsTray = () => {
664
+ const tray = document.createElement("div");
665
+ tray.setAttribute("data-settings-tray", "true");
666
+ tray.style.cssText = `
667
+ position: absolute;
668
+ top: 60px;
669
+ right: 16px;
670
+ background: white;
671
+ border-radius: ${aeroTheme.borderRadius.xl};
672
+ box-shadow: ${aeroTheme.shadows.xl};
673
+ padding: 0;
674
+ display: none;
675
+ flex-direction: row;
676
+ align-items: center;
677
+ gap: 0;
678
+ z-index: 9999;
679
+ min-width: auto;
680
+ transform-origin: top right;
681
+ animation: popIn 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
682
+ overflow: hidden;
683
+ `;
684
+ // Add animation keyframes if not present
685
+ if (!document.getElementById("convai-widget-keyframes")) {
686
+ const style = document.createElement("style");
687
+ style.id = "convai-widget-keyframes";
688
+ style.textContent = `
689
+ @keyframes popIn {
690
+ from { opacity: 0; transform: scale(0.95) translateY(-10px); }
691
+ to { opacity: 1; transform: scale(1) translateY(0); }
692
+ }
693
+ `;
694
+ document.head.appendChild(style);
695
+ }
696
+ // Horizontal Settings Row Container
697
+ const settingsRow = document.createElement("div");
698
+ settingsRow.style.cssText = `
699
+ display: flex;
700
+ flex-direction: row;
701
+ align-items: center;
702
+ padding: 8px;
703
+ gap: 4px;
704
+ `;
705
+ const createOption = (icon, label, onClick, isActive = false, isDestructive = false) => {
706
+ const btn = document.createElement("div");
707
+ btn.style.cssText = `
708
+ display: flex;
709
+ flex-direction: column;
710
+ align-items: center;
711
+ gap: 4px;
712
+ padding: 8px 12px;
713
+ cursor: pointer;
714
+ border-radius: ${aeroTheme.borderRadius.md};
715
+ background-color: ${isActive
716
+ ? "rgba(16, 185, 129, 0.15)"
717
+ : isDestructive
718
+ ? "rgba(239, 68, 68, 0.1)"
719
+ : "transparent"};
720
+ color: ${isActive
721
+ ? "#10b981"
722
+ : isDestructive
723
+ ? "#ef4444"
724
+ : aeroTheme.colors.text.primary};
725
+ transition: ${aeroTheme.transitions.fast};
726
+ min-width: 50px;
727
+ `;
728
+ // Icon container
729
+ const iconContainer = document.createElement("div");
730
+ iconContainer.appendChild(icon);
731
+ btn.appendChild(iconContainer);
732
+ const span = document.createElement("span");
733
+ span.textContent = label;
734
+ span.style.cssText = `
735
+ font-size: 10px;
736
+ font-weight: 500;
737
+ `;
738
+ btn.appendChild(span);
739
+ btn.addEventListener("click", (e) => {
740
+ e.stopPropagation();
741
+ onClick();
742
+ });
743
+ btn.addEventListener("mouseover", () => {
744
+ btn.style.transform = "scale(1.02)";
745
+ if (!isActive && !isDestructive) {
746
+ btn.style.backgroundColor = aeroTheme.colors.neutral[100];
747
+ }
748
+ });
749
+ btn.addEventListener("mouseout", () => {
750
+ btn.style.transform = "scale(1)";
751
+ btn.style.backgroundColor = isActive
752
+ ? "rgba(16, 185, 129, 0.15)"
753
+ : isDestructive
754
+ ? "rgba(239, 68, 68, 0.1)"
755
+ : "transparent";
756
+ });
757
+ return btn;
758
+ };
759
+ // Reset
760
+ const resetIcon = Icons.Redo("md");
761
+ resetIcon.style.width = "18px";
762
+ resetIcon.style.height = "18px";
763
+ settingsRow.appendChild(createOption(resetIcon, "Reset", handleReset));
764
+ // Video
765
+ if (convaiClient.connectionType === "video" && showVideo) {
766
+ const videoIcon = isVideoVisible
767
+ ? Icons.Video("md")
768
+ : Icons.VideoOff("md");
769
+ videoIcon.style.width = "18px";
770
+ videoIcon.style.height = "18px";
771
+ const videoBtn = createOption(videoIcon, "Video", handleToggleVideo, isVideoVisible);
772
+ videoBtn.id = "convai-settings-video-btn";
773
+ settingsRow.appendChild(videoBtn);
774
+ }
775
+ // Screen Share
776
+ if (convaiClient.connectionType === "video" && showScreenShare) {
777
+ const isSharing = convaiClient.screenShareControls.isScreenShareActive;
778
+ const shareIcon = isSharing
779
+ ? Icons.StopScreenShare("md")
780
+ : Icons.ScreenShare("md");
781
+ shareIcon.style.width = "18px";
782
+ shareIcon.style.height = "18px";
783
+ const shareBtn = createOption(shareIcon, "Screen", handleToggleScreenShare, isSharing);
784
+ shareBtn.id = "convai-settings-share-btn";
785
+ settingsRow.appendChild(shareBtn);
786
+ }
787
+ // Disconnect
788
+ const disconnectIcon = document.createElement("span");
789
+ disconnectIcon.innerHTML = "⏻";
790
+ disconnectIcon.style.fontSize = "18px";
791
+ settingsRow.appendChild(createOption(disconnectIcon, "Disconnect", handleDisconnect, false, true));
792
+ tray.appendChild(settingsRow);
793
+ return tray;
794
+ };
795
+ // Update settings tray content to reflect current connection state
796
+ const updateSettingsTray = () => {
797
+ const settingsRow = settingsTray.querySelector("div");
798
+ if (!settingsRow)
799
+ return;
800
+ // Clear existing content
801
+ settingsRow.innerHTML = "";
802
+ const createOption = (icon, label, onClick, isActive = false, isDestructive = false) => {
803
+ const btn = document.createElement("div");
804
+ btn.style.cssText = `
805
+ display: flex;
806
+ flex-direction: column;
807
+ align-items: center;
808
+ gap: 4px;
809
+ padding: 8px 12px;
810
+ cursor: pointer;
811
+ border-radius: ${aeroTheme.borderRadius.md};
812
+ background-color: ${isActive
813
+ ? "rgba(16, 185, 129, 0.15)"
814
+ : isDestructive
815
+ ? "rgba(239, 68, 68, 0.1)"
816
+ : "transparent"};
817
+ color: ${isActive
818
+ ? "#10b981"
819
+ : isDestructive
820
+ ? "#ef4444"
821
+ : aeroTheme.colors.text.primary};
822
+ transition: ${aeroTheme.transitions.fast};
823
+ min-width: 50px;
824
+ `;
825
+ // Icon container
826
+ const iconContainer = document.createElement("div");
827
+ iconContainer.appendChild(icon);
828
+ btn.appendChild(iconContainer);
829
+ const span = document.createElement("span");
830
+ span.textContent = label;
831
+ span.style.cssText = `
832
+ font-size: 10px;
833
+ font-weight: 500;
834
+ `;
835
+ btn.appendChild(span);
836
+ btn.addEventListener("click", (e) => {
837
+ e.stopPropagation();
838
+ onClick();
839
+ });
840
+ btn.addEventListener("mouseover", () => {
841
+ btn.style.transform = "scale(1.02)";
842
+ if (!isActive && !isDestructive) {
843
+ btn.style.backgroundColor = aeroTheme.colors.neutral[100];
844
+ }
845
+ });
846
+ btn.addEventListener("mouseout", () => {
847
+ btn.style.transform = "scale(1)";
848
+ btn.style.backgroundColor = isActive
849
+ ? "rgba(16, 185, 129, 0.15)"
850
+ : isDestructive
851
+ ? "rgba(239, 68, 68, 0.1)"
852
+ : "transparent";
853
+ });
854
+ return btn;
855
+ };
856
+ // Reset
857
+ const resetIcon = Icons.Redo("md");
858
+ resetIcon.style.width = "18px";
859
+ resetIcon.style.height = "18px";
860
+ settingsRow.appendChild(createOption(resetIcon, "Reset", handleReset));
861
+ // Video - Always show if showVideo is true and connection type is video
862
+ if (convaiClient.connectionType === "video" && showVideo) {
863
+ const videoIcon = isVideoVisible
864
+ ? Icons.Video("md")
865
+ : Icons.VideoOff("md");
866
+ videoIcon.style.width = "18px";
867
+ videoIcon.style.height = "18px";
868
+ const videoBtn = createOption(videoIcon, "Video", handleToggleVideo, isVideoVisible);
869
+ videoBtn.id = "convai-settings-video-btn";
870
+ settingsRow.appendChild(videoBtn);
871
+ }
872
+ // Screen Share - Always show if showScreenShare is true and connection type is video
873
+ if (convaiClient.connectionType === "video" && showScreenShare) {
874
+ const isSharing = convaiClient.screenShareControls.isScreenShareActive;
875
+ const shareIcon = isSharing
876
+ ? Icons.StopScreenShare("md")
877
+ : Icons.ScreenShare("md");
878
+ shareIcon.style.width = "18px";
879
+ shareIcon.style.height = "18px";
880
+ const shareBtn = createOption(shareIcon, "Screen", handleToggleScreenShare, isSharing);
881
+ shareBtn.id = "convai-settings-share-btn";
882
+ settingsRow.appendChild(shareBtn);
883
+ }
884
+ // Disconnect
885
+ const disconnectIcon = document.createElement("span");
886
+ disconnectIcon.innerHTML = "⏻";
887
+ disconnectIcon.style.fontSize = "18px";
888
+ settingsRow.appendChild(createOption(disconnectIcon, "Disconnect", handleDisconnect, false, true));
889
+ };
890
+ // Create floating video - Matches React FloatingVideo component
891
+ const createFloatingVideo = () => {
892
+ const container = document.createElement("div");
893
+ container.id = "floating-video-container";
894
+ container.style.cssText = `
895
+ position: fixed;
896
+ left: 20px;
897
+ top: 20px;
898
+ z-index: 10000;
899
+ width: 320px;
900
+ border-radius: ${aeroTheme.borderRadius.lg};
901
+ overflow: hidden;
902
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
903
+ cursor: grab;
904
+ display: none;
905
+ transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1), transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
906
+ `;
907
+ const wrapper = document.createElement("div");
908
+ wrapper.style.cssText = `
909
+ background: rgba(15, 23, 42, 0.95);
910
+ backdrop-filter: blur(20px);
911
+ border: 1px solid ${aeroTheme.colors.neutral[700]};
912
+ border-radius: ${aeroTheme.borderRadius.lg};
913
+ overflow: hidden;
914
+ `;
915
+ // Header
916
+ const header = document.createElement("div");
917
+ header.style.cssText = `
918
+ display: flex;
919
+ align-items: center;
920
+ justify-content: space-between;
921
+ padding: 8px 12px;
922
+ background: rgba(0, 0, 0, 0.3);
923
+ border-bottom: 1px solid ${aeroTheme.colors.neutral[700]};
924
+ `;
925
+ const headerLeft = document.createElement("div");
926
+ headerLeft.style.cssText = `
927
+ display: flex;
928
+ align-items: center;
929
+ gap: 8px;
930
+ color: ${aeroTheme.colors.neutral[300]};
931
+ font-size: 12px;
932
+ font-weight: 500;
933
+ `;
934
+ const dragIcon = Icons.DragIndicator("sm");
935
+ dragIcon.style.width = "16px";
936
+ dragIcon.style.height = "16px";
937
+ headerLeft.appendChild(dragIcon);
938
+ const label = document.createElement("span");
939
+ label.textContent = "Your Camera";
940
+ headerLeft.appendChild(label);
941
+ const closeButton = document.createElement("button");
942
+ closeButton.innerHTML = "";
943
+ const closeIcon = Icons.Close("md");
944
+ closeIcon.style.width = "20px";
945
+ closeIcon.style.height = "20px";
946
+ closeButton.appendChild(closeIcon);
947
+ closeButton.style.cssText = `
948
+ background: transparent;
949
+ border: none;
950
+ color: ${aeroTheme.colors.neutral[400]};
951
+ cursor: pointer;
952
+ display: flex;
953
+ align-items: center;
954
+ justify-content: center;
955
+ padding: 4px;
956
+ transition: transform 0.1s ease-out;
957
+ `;
958
+ closeButton.addEventListener("click", (e) => {
959
+ e.stopPropagation();
960
+ convaiClient.videoControls.disableVideo();
961
+ });
962
+ closeButton.addEventListener("mouseenter", () => {
963
+ closeButton.style.transform = "scale(1.1)";
964
+ });
965
+ closeButton.addEventListener("mouseleave", () => {
966
+ closeButton.style.transform = "scale(1)";
967
+ });
968
+ header.appendChild(headerLeft);
969
+ header.appendChild(closeButton);
970
+ // Video Container
971
+ const videoContainer = document.createElement("div");
972
+ videoContainer.style.cssText = `
973
+ position: relative;
974
+ width: 100%;
975
+ padding-bottom: 75%;
976
+ background: #000;
977
+ `;
978
+ const videoWrapper = document.createElement("div");
979
+ videoWrapper.id = "floating-video-wrapper";
980
+ videoWrapper.style.cssText = `
981
+ position: absolute;
982
+ top: 0;
983
+ left: 0;
984
+ width: 100%;
985
+ height: 100%;
986
+ `;
987
+ const videoElement = document.createElement("video");
988
+ videoElement.id = "floating-video-element";
989
+ videoElement.autoplay = true;
990
+ videoElement.playsInline = true;
991
+ videoElement.muted = true;
992
+ videoElement.style.cssText = `
993
+ width: 100%;
994
+ height: 100%;
995
+ object-fit: cover;
996
+ transform: scaleX(-1);
997
+ `;
998
+ // Camera off placeholder
999
+ const cameraOffPlaceholder = document.createElement("div");
1000
+ cameraOffPlaceholder.id = "camera-off-placeholder";
1001
+ cameraOffPlaceholder.style.cssText = `
1002
+ width: 100%;
1003
+ height: 100%;
1004
+ display: flex;
1005
+ flex-direction: column;
1006
+ align-items: center;
1007
+ justify-content: center;
1008
+ color: ${aeroTheme.colors.neutral[400]};
1009
+ gap: 12px;
1010
+ background: #000;
1011
+ `;
1012
+ const cameraOffIcon = Icons.VideoOff("lg");
1013
+ cameraOffIcon.style.width = "48px";
1014
+ cameraOffIcon.style.height = "48px";
1015
+ cameraOffPlaceholder.appendChild(cameraOffIcon);
1016
+ const cameraOffText = document.createElement("span");
1017
+ cameraOffText.textContent = "Camera is off";
1018
+ cameraOffText.style.cssText = `
1019
+ font-size: 14px;
1020
+ text-align: center;
1021
+ `;
1022
+ cameraOffPlaceholder.appendChild(cameraOffText);
1023
+ videoWrapper.appendChild(videoElement);
1024
+ videoWrapper.appendChild(cameraOffPlaceholder);
1025
+ videoContainer.appendChild(videoWrapper);
1026
+ wrapper.appendChild(header);
1027
+ wrapper.appendChild(videoContainer);
1028
+ container.appendChild(wrapper);
1029
+ // Add drag functionality
1030
+ let isDragging = false;
1031
+ let startX = 0;
1032
+ let startY = 0;
1033
+ let startLeft = 20;
1034
+ let startTop = 20;
1035
+ const handleMouseDown = (e) => {
1036
+ if (e.target.closest("button"))
1037
+ return;
1038
+ isDragging = true;
1039
+ startX = e.clientX;
1040
+ startY = e.clientY;
1041
+ const style = window.getComputedStyle(container);
1042
+ startLeft = parseInt(style.left);
1043
+ startTop = parseInt(style.top);
1044
+ container.style.cursor = "grabbing";
1045
+ e.preventDefault();
1046
+ };
1047
+ const handleMouseMove = (e) => {
1048
+ if (!isDragging)
1049
+ return;
1050
+ const deltaX = e.clientX - startX;
1051
+ const deltaY = e.clientY - startY;
1052
+ const newLeft = startLeft + deltaX;
1053
+ const newTop = startTop + deltaY;
1054
+ // Keep within bounds
1055
+ const maxX = window.innerWidth - 320;
1056
+ const maxY = window.innerHeight - 240;
1057
+ const boundedLeft = Math.max(0, Math.min(newLeft, maxX));
1058
+ const boundedTop = Math.max(0, Math.min(newTop, maxY));
1059
+ container.style.left = `${boundedLeft}px`;
1060
+ container.style.top = `${boundedTop}px`;
1061
+ };
1062
+ const handleMouseUp = () => {
1063
+ if (isDragging) {
1064
+ isDragging = false;
1065
+ container.style.cursor = "grab";
1066
+ }
1067
+ };
1068
+ container.addEventListener("mousedown", handleMouseDown);
1069
+ document.addEventListener("mousemove", handleMouseMove);
1070
+ document.addEventListener("mouseup", handleMouseUp);
1071
+ return container;
1072
+ };
1073
+ // Event handlers
1074
+ const handleToggle = async () => {
1075
+ if (isOpen) {
1076
+ // Just close if already open
1077
+ return;
1078
+ }
1079
+ // Connect on first click if not already connected/connecting
1080
+ if (!convaiClient.state.isConnected && !convaiClient.state.isConnecting) {
1081
+ try {
1082
+ await convaiClient.connect();
1083
+ setIsOpen(true);
1084
+ }
1085
+ catch (error) {
1086
+ console.error("Failed to connect:", error);
1087
+ }
1088
+ }
1089
+ else {
1090
+ // Just toggle open/close if already connected
1091
+ setIsOpen(!isOpen);
1092
+ }
1093
+ };
1094
+ const handleClose = () => {
1095
+ setIsOpen(false);
1096
+ };
1097
+ const handleSend = () => {
1098
+ if (inputValue.trim() && convaiClient.state.isConnected) {
1099
+ convaiClient.sendUserTextMessage(inputValue);
1100
+ inputValue = "";
1101
+ inputElement.value = "";
1102
+ updateSendButton();
1103
+ }
1104
+ };
1105
+ // Microphone toggle removed - only controlled via voice mode now
1106
+ const handleToggleMute = () => {
1107
+ isMuted = !isMuted;
1108
+ if (convaiClient.room) {
1109
+ const remoteParticipants = Array.from(convaiClient.room.remoteParticipants.values());
1110
+ remoteParticipants.forEach((participant) => {
1111
+ participant.audioTrackPublications.forEach((publication) => {
1112
+ if (publication.track) {
1113
+ publication.track.setMuted(isMuted);
1114
+ }
1115
+ });
1116
+ });
1117
+ }
1118
+ updateHeader();
1119
+ };
1120
+ const handleReset = async () => {
1121
+ setIsSettingsOpen(false);
1122
+ try {
1123
+ await convaiClient.disconnect();
1124
+ convaiClient.resetSession();
1125
+ isMuted = false;
1126
+ isVoiceMode = false;
1127
+ hasEnteredDefaultVoiceMode = false;
1128
+ updateHeader();
1129
+ updateVoiceMode();
1130
+ updateSendButton();
1131
+ }
1132
+ catch (error) {
1133
+ console.error("Failed to reset:", error);
1134
+ }
1135
+ };
1136
+ const handleDisconnect = async () => {
1137
+ setIsSettingsOpen(false);
1138
+ await convaiClient.disconnect();
1139
+ isMuted = false;
1140
+ isVoiceMode = false;
1141
+ hasEnteredDefaultVoiceMode = false;
1142
+ updateHeader();
1143
+ updateVoiceMode();
1144
+ updateSendButton();
1145
+ };
1146
+ const handleToggleVideo = async () => {
1147
+ if (convaiClient.connectionType !== "video")
1148
+ return;
1149
+ try {
1150
+ if (isVideoVisible) {
1151
+ await convaiClient.videoControls.disableVideo();
1152
+ }
1153
+ else {
1154
+ await convaiClient.videoControls.enableVideo();
1155
+ }
1156
+ // State will be updated via event listener below
1157
+ }
1158
+ catch (e) {
1159
+ console.error("Failed to toggle video:", e);
1160
+ }
1161
+ };
1162
+ const handleToggleScreenShare = async () => {
1163
+ try {
1164
+ await convaiClient.screenShareControls.toggleScreenShare();
1165
+ // State will be updated via event listener above
1166
+ }
1167
+ catch (e) {
1168
+ console.error("Failed to toggle screen share:", e);
1169
+ }
1170
+ };
1171
+ const handleSettingsToggle = () => {
1172
+ // Regenerate settings tray content BEFORE opening to reflect current connection type
1173
+ if (!isSettingsOpen) {
1174
+ updateSettingsTray();
1175
+ }
1176
+ setIsSettingsOpen(!isSettingsOpen);
1177
+ };
1178
+ // Update functions
1179
+ const setIsOpen = async (open) => {
1180
+ isOpen = open;
1181
+ if (open) {
1182
+ morphingContainer.style.width = "400px";
1183
+ morphingContainer.style.height = "600px";
1184
+ morphingContainer.style.borderRadius = aeroTheme.borderRadius.xl;
1185
+ morphingContainer.style.cursor = "default";
1186
+ buttonContent.style.opacity = "0";
1187
+ buttonContent.style.transform = "scale(0.8)";
1188
+ buttonContent.style.pointerEvents = "none";
1189
+ chatContent.style.opacity = "1";
1190
+ chatContent.style.transform = "scale(1)";
1191
+ chatContent.style.pointerEvents = "auto";
1192
+ // Auto-enter voice mode if defaultVoiceMode is true and not already entered
1193
+ if (defaultVoiceMode && !hasEnteredDefaultVoiceMode && convaiClient.state.isConnected) {
1194
+ setTimeout(async () => {
1195
+ try {
1196
+ await convaiClient.audioControls.unmuteAudio();
1197
+ isVoiceMode = true;
1198
+ hasEnteredDefaultVoiceMode = true;
1199
+ updateVoiceMode();
1200
+ }
1201
+ catch (error) {
1202
+ console.error("Failed to enter voice mode on open:", error);
1203
+ }
1204
+ }, 100);
1205
+ }
1206
+ }
1207
+ else {
1208
+ morphingContainer.style.width = "4rem";
1209
+ morphingContainer.style.height = "4rem";
1210
+ morphingContainer.style.borderRadius = "50%";
1211
+ morphingContainer.style.cursor = "pointer";
1212
+ buttonContent.style.opacity = "1";
1213
+ buttonContent.style.transform = "scale(1)";
1214
+ buttonContent.style.pointerEvents = "auto";
1215
+ chatContent.style.opacity = "0";
1216
+ chatContent.style.transform = "scale(0.8)";
1217
+ chatContent.style.pointerEvents = "none";
1218
+ }
1219
+ };
1220
+ const setIsSettingsOpen = (open) => {
1221
+ isSettingsOpen = open;
1222
+ settingsTray.style.display = open ? "flex" : "none";
1223
+ };
1224
+ const updateSendButton = () => {
1225
+ const sendButton = document.getElementById("convai-send-button");
1226
+ if (!sendButton)
1227
+ return;
1228
+ // Clear previous content
1229
+ sendButton.innerHTML = "";
1230
+ if (inputValue.length > 0) {
1231
+ // Show Send Button
1232
+ sendButton.appendChild(Icons.Send("md"));
1233
+ sendButton.style.background = aeroTheme.colors.text.primary;
1234
+ sendButton.style.color = "white";
1235
+ sendButton.style.border = "none";
1236
+ sendButton.title = "Send";
1237
+ }
1238
+ else {
1239
+ // Show Voice Mode Toggle
1240
+ sendButton.appendChild(Icons.Waveform("md"));
1241
+ sendButton.style.background = "transparent";
1242
+ sendButton.style.color = aeroTheme.colors.text.primary;
1243
+ sendButton.style.border = `1px solid ${aeroTheme.colors.neutral[300]}`;
1244
+ sendButton.title = "Voice Mode";
1245
+ }
1246
+ };
1247
+ const updateVideoTrack = () => {
1248
+ if (!floatingVideo ||
1249
+ !convaiClient.room ||
1250
+ !convaiClient.room.localParticipant)
1251
+ return;
1252
+ const videoEl = floatingVideo.querySelector("#floating-video-element");
1253
+ const placeholder = floatingVideo.querySelector("#camera-off-placeholder");
1254
+ if (!videoEl || !placeholder)
1255
+ return;
1256
+ if (isVideoVisible && !isVoiceMode) {
1257
+ const tracks = Array.from(convaiClient.room.localParticipant.videoTrackPublications.values());
1258
+ const videoPub = tracks.find((t) => t.kind === "video");
1259
+ if (videoPub && videoPub.track) {
1260
+ videoPub.track.attach(videoEl);
1261
+ videoEl.style.display = "block";
1262
+ placeholder.style.display = "none";
1263
+ }
1264
+ else {
1265
+ videoEl.style.display = "none";
1266
+ placeholder.style.display = "flex";
1267
+ }
1268
+ }
1269
+ else {
1270
+ videoEl.style.display = "none";
1271
+ placeholder.style.display = "flex";
1272
+ }
1273
+ };
1274
+ const updateVoiceMode = async () => {
1275
+ const standardFooter = document.getElementById("convai-footer-standard");
1276
+ const voiceExitBtn = document.getElementById("convai-voice-exit-btn");
1277
+ if (isVoiceMode) {
1278
+ // Show Voice Overlay
1279
+ if (voiceModeOverlay)
1280
+ voiceModeOverlay.style.display = "flex";
1281
+ if (messageListElement)
1282
+ messageListElement.style.display = "none";
1283
+ if (floatingVideo)
1284
+ floatingVideo.style.display = "none";
1285
+ // Footer: Show Exit Button, Hide Standard
1286
+ if (standardFooter)
1287
+ standardFooter.style.display = "none";
1288
+ if (voiceExitBtn)
1289
+ voiceExitBtn.style.display = "flex";
1290
+ if (footerElement)
1291
+ footerElement.style.justifyContent = "center";
1292
+ // Update header to show VOICE badge
1293
+ updateHeader();
1294
+ // Reset animation state for voice mode
1295
+ startTime = Date.now();
1296
+ currentLevels = Array(40).fill(0.05);
1297
+ targetLevels = Array(40).fill(0.05);
1298
+ // Start Audio Analysis
1299
+ startAudioAnalysis();
1300
+ }
1301
+ else {
1302
+ // Ensure microphone is muted when not in voice mode
1303
+ if (!convaiClient.audioControls.isAudioMuted) {
1304
+ await convaiClient.audioControls.muteAudio();
1305
+ }
1306
+ // Hide Overlay
1307
+ if (voiceModeOverlay)
1308
+ voiceModeOverlay.style.display = "none";
1309
+ // Show Video or Message List
1310
+ if (isVideoVisible) {
1311
+ if (messageListElement)
1312
+ messageListElement.style.display = "none";
1313
+ if (floatingVideo)
1314
+ floatingVideo.style.display = "block";
1315
+ updateVideoTrack();
1316
+ }
1317
+ else {
1318
+ if (messageListElement)
1319
+ messageListElement.style.display = "flex";
1320
+ if (floatingVideo)
1321
+ floatingVideo.style.display = "none";
1322
+ }
1323
+ // Footer: Show Standard, Hide Exit
1324
+ if (standardFooter)
1325
+ standardFooter.style.display = "flex";
1326
+ if (voiceExitBtn)
1327
+ voiceExitBtn.style.display = "none";
1328
+ if (footerElement)
1329
+ footerElement.style.justifyContent = "flex-start";
1330
+ // Update header to hide VOICE badge
1331
+ updateHeader();
1332
+ // Stop Audio Analysis
1333
+ stopAudioAnalysis();
1334
+ }
1335
+ };
1336
+ // Microphone button removed - only controlled via voice mode now
1337
+ // Helper for Markdown - matches React MarkdownRenderer.tsx
1338
+ const renderMarkdown = (text, container) => {
1339
+ if (!text)
1340
+ return;
1341
+ // Handle both actual newlines and \n escape sequences
1342
+ const normalizedText = text.replace(/\\n/g, "\n");
1343
+ const lines = normalizedText.split("\n");
1344
+ lines.forEach((line, lineIndex) => {
1345
+ // Process markdown within each line
1346
+ const processLine = (text) => {
1347
+ const parts = [];
1348
+ let remaining = text;
1349
+ // Process bold and italic markdown
1350
+ // Bold: **text** or __text__
1351
+ // Italic: *text* or _text_
1352
+ const markdownRegex = /(\*\*|__)(.*?)\1|\*([^*]+)\*|_([^_]+)_/g;
1353
+ let lastIndex = 0;
1354
+ let match;
1355
+ while ((match = markdownRegex.exec(remaining)) !== null) {
1356
+ // Add text before the match
1357
+ if (match.index > lastIndex) {
1358
+ const textBefore = remaining.substring(lastIndex, match.index);
1359
+ if (textBefore) {
1360
+ parts.push(document.createTextNode(textBefore));
1361
+ }
1362
+ }
1363
+ // Determine if it's bold or italic
1364
+ if (match[1] && match[2]) {
1365
+ // Bold text (** or __) - matches React UserBubble styling
1366
+ const strong = document.createElement("strong");
1367
+ strong.textContent = match[2];
1368
+ strong.style.cssText = `
1369
+ font-weight: 600;
1370
+ color: ${aeroTheme.colors.text.primary};
1371
+ `;
1372
+ parts.push(strong);
1373
+ }
1374
+ else if (match[3]) {
1375
+ // Italic text (*) - matches React UserBubble styling
1376
+ const em = document.createElement("em");
1377
+ em.textContent = match[3];
1378
+ em.style.cssText = `
1379
+ font-style: italic;
1380
+ `;
1381
+ parts.push(em);
1382
+ }
1383
+ else if (match[4]) {
1384
+ // Italic text (_) - matches React UserBubble styling
1385
+ const em = document.createElement("em");
1386
+ em.textContent = match[4];
1387
+ em.style.cssText = `
1388
+ font-style: italic;
1389
+ `;
1390
+ parts.push(em);
1391
+ }
1392
+ lastIndex = match.index + match[0].length;
1393
+ }
1394
+ // Add remaining text
1395
+ if (lastIndex < remaining.length) {
1396
+ const textAfter = remaining.substring(lastIndex);
1397
+ if (textAfter) {
1398
+ parts.push(document.createTextNode(textAfter));
1399
+ }
1400
+ }
1401
+ return parts;
1402
+ };
1403
+ const processedLine = processLine(line);
1404
+ // Handle empty lines (creates paragraph spacing)
1405
+ if (line.trim() === "" && lineIndex > 0) {
1406
+ container.appendChild(document.createElement("br"));
1407
+ return;
1408
+ }
1409
+ // Append the processed line
1410
+ if (processedLine.length > 0) {
1411
+ processedLine.forEach((part) => container.appendChild(part));
1412
+ }
1413
+ else {
1414
+ container.appendChild(document.createTextNode(line));
1415
+ }
1416
+ // Add line break between lines
1417
+ if (lineIndex < lines.length - 1) {
1418
+ container.appendChild(document.createElement("br"));
1419
+ }
1420
+ });
1421
+ };
1422
+ const updateMessageList = () => {
1423
+ if (!messageListElement)
1424
+ return;
1425
+ const messages = formatMessages();
1426
+ messageListElement.innerHTML = "";
1427
+ if (messages.length === 0) {
1428
+ const emptyState = document.createElement("div");
1429
+ emptyState.style.cssText = `
1430
+ display: flex;
1431
+ flex-direction: column;
1432
+ align-items: center;
1433
+ justify-content: center;
1434
+ height: 100%;
1435
+ color: ${aeroTheme.colors.text.secondary};
1436
+ text-align: center;
1437
+ padding: 2rem;
1438
+ `;
1439
+ emptyState.innerHTML = `
1440
+ <p style="font-weight: 500; margin-bottom: 8px;">Start a conversation</p>
1441
+ <p style="font-size: 12px;">Type a message below to begin chatting</p>
1442
+ `;
1443
+ messageListElement.appendChild(emptyState);
1444
+ return;
1445
+ }
1446
+ messages.forEach((msg) => {
1447
+ // MessageWrapper - matches React UserMessageContainer
1448
+ const messageWrapper = document.createElement("div");
1449
+ messageWrapper.style.cssText = `
1450
+ display: flex;
1451
+ flex-direction: column;
1452
+ max-width: ${msg.isUser ? "70%" : "70%"};
1453
+ margin-left: ${msg.isUser ? "auto" : "0"};
1454
+ width: 100%;
1455
+ isolation: isolate;
1456
+ contain: layout style paint;
1457
+ `;
1458
+ const bubble = document.createElement("div");
1459
+ // Add data attribute to identify user messages for debugging/styling
1460
+ if (msg.isUser) {
1461
+ bubble.setAttribute("data-message-type", "user");
1462
+ }
1463
+ // Set base styles first - explicitly set background to match React UserBubble exactly
1464
+ // CRITICAL: Use the exact same background for both user and bot messages
1465
+ const backgroundColor = "rgba(252, 252, 253, 0.95)";
1466
+ bubble.style.cssText = `
1467
+ padding: 12px;
1468
+ border-radius: ${msg.isUser ? "12px 12px 4px 12px" : "12px 12px 12px 4px"};
1469
+ background: ${backgroundColor};
1470
+ background-color: ${backgroundColor};
1471
+ backdrop-filter: blur(20px) saturate(180%);
1472
+ border: 1px solid rgba(0, 0, 0, 0.06);
1473
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.8) inset, 0 2px 8px rgba(0, 0, 0, 0.08);
1474
+ color: ${aeroTheme.colors.text.primary};
1475
+ font-family: ${aeroTheme.typography.fontFamily.body};
1476
+ font-size: 14px;
1477
+ line-height: 1.5;
1478
+ word-wrap: break-word;
1479
+ max-width: 100%;
1480
+ display: flex;
1481
+ flex-direction: column;
1482
+ font-synthesis: none;
1483
+ text-rendering: optimizeLegibility;
1484
+ -webkit-font-smoothing: antialiased;
1485
+ -moz-osx-font-smoothing: grayscale;
1486
+ -webkit-text-size-adjust: 100%;
1487
+ isolation: isolate;
1488
+ contain: layout style paint;
1489
+ `;
1490
+ // Set background with !important to override any other styles (this is critical)
1491
+ bubble.style.setProperty("background", backgroundColor, "important");
1492
+ bubble.style.setProperty("background-color", backgroundColor, "important");
1493
+ // Header (Logo/Icon + Name) inside bubble - matches React MessageHeader
1494
+ if (msg.sender) {
1495
+ const header = document.createElement("div");
1496
+ header.style.cssText = `
1497
+ display: flex;
1498
+ align-items: center;
1499
+ gap: ${aeroTheme.spacing.xs};
1500
+ margin-bottom: ${aeroTheme.spacing.xs};
1501
+ isolation: isolate;
1502
+ contain: layout style paint;
1503
+ `;
1504
+ // Logo/Icon - matches React LogoWrapper
1505
+ const logoContainer = document.createElement("div");
1506
+ logoContainer.style.cssText = `
1507
+ width: 1rem;
1508
+ height: 1rem;
1509
+ display: flex;
1510
+ align-items: center;
1511
+ justify-content: center;
1512
+ isolation: isolate;
1513
+ contain: layout style paint;
1514
+ `;
1515
+ if (msg.isUser) {
1516
+ // User Icon - FaUser from react-icons/fa, matches React exactly
1517
+ const userIcon = Icons.User("sm");
1518
+ userIcon.style.width = "16px";
1519
+ userIcon.style.height = "16px";
1520
+ userIcon.style.color = aeroTheme.colors.convai.dark;
1521
+ logoContainer.appendChild(userIcon);
1522
+ }
1523
+ else {
1524
+ // Bot Logo - ConvaiLogo sm size with connected state
1525
+ const botLogo = Icons.ConvaiLogo("sm", "connected");
1526
+ botLogo.style.width = "16px";
1527
+ botLogo.style.height = "16px";
1528
+ botLogo.style.color = "#10b981";
1529
+ logoContainer.appendChild(botLogo);
1530
+ }
1531
+ // Name Label - matches React UserLabel exactly
1532
+ const nameLabel = document.createElement("span");
1533
+ nameLabel.textContent = msg.sender === "User" ? "You" : msg.sender;
1534
+ nameLabel.style.cssText = `
1535
+ font-size: 0.75rem;
1536
+ color: ${msg.isUser ? aeroTheme.colors.convai.dark : "#10b981"};
1537
+ font-weight: 500;
1538
+ font-family: ${aeroTheme.typography.fontFamily.body};
1539
+ isolation: isolate;
1540
+ contain: layout style paint;
1541
+ `;
1542
+ header.appendChild(logoContainer);
1543
+ header.appendChild(nameLabel);
1544
+ bubble.appendChild(header);
1545
+ }
1546
+ // Message Content
1547
+ const contentDiv = document.createElement("div");
1548
+ contentDiv.style.cssText = `
1549
+ white-space: pre-wrap;
1550
+ word-wrap: break-word;
1551
+ `;
1552
+ renderMarkdown(msg.text, contentDiv);
1553
+ bubble.appendChild(contentDiv);
1554
+ messageWrapper.appendChild(bubble);
1555
+ // Timestamp - matches React exactly
1556
+ if (msg.timestamp) {
1557
+ const timestamp = document.createElement("span");
1558
+ timestamp.textContent = msg.timestamp;
1559
+ timestamp.style.cssText = `
1560
+ font-size: 0.75rem;
1561
+ color: ${aeroTheme.colors.text.secondary};
1562
+ margin-top: 4px;
1563
+ align-self: ${msg.isUser ? "flex-end" : "flex-start"};
1564
+ font-family: ${aeroTheme.typography.fontFamily.body};
1565
+ `;
1566
+ messageWrapper.appendChild(timestamp);
1567
+ }
1568
+ messageListElement.appendChild(messageWrapper);
1569
+ });
1570
+ // Scroll to bottom
1571
+ messageListElement.scrollTop = messageListElement.scrollHeight;
1572
+ };
1573
+ const getBotStatusColor = () => {
1574
+ if (!convaiClient.state.isConnected) {
1575
+ return { color: "#ef4444", label: "Disconnected" };
1576
+ }
1577
+ if (convaiClient.state.isConnecting || !convaiClient.isBotReady) {
1578
+ return { color: "#f59e0b", label: "Connecting" };
1579
+ }
1580
+ return { color: "#10b981", label: "Connected" };
1581
+ };
1582
+ const formatMessages = () => {
1583
+ const filteredMessages = convaiClient.chatMessages.filter((msg) => (msg.type === "user-transcription" ||
1584
+ msg.type === "user-llm-text" ||
1585
+ msg.type === "bot-llm-text" ||
1586
+ msg.type === "bot-emotion") &&
1587
+ msg.content &&
1588
+ msg.content.trim().length > 0);
1589
+ return filteredMessages.map((msg) => ({
1590
+ id: msg.id,
1591
+ text: msg.content,
1592
+ isUser: msg.type.includes("user"),
1593
+ timestamp: new Date(msg.timestamp).toLocaleTimeString([], {
1594
+ hour: "2-digit",
1595
+ minute: "2-digit",
1596
+ }),
1597
+ sender: msg.type.includes("user") ? "You" : characterName,
1598
+ showLogo: msg.type.includes("bot"),
1599
+ }));
1600
+ };
1601
+ // Setup event listeners for client state changes
1602
+ const setupClientListeners = () => {
1603
+ convaiClient.on("stateChange", () => {
1604
+ updateHeader();
1605
+ // Update pulse animation based on connecting state
1606
+ if (convaiClient.state.isConnecting) {
1607
+ morphingContainer.style.animation =
1608
+ "convai-pulse 2s ease-in-out infinite";
1609
+ }
1610
+ else {
1611
+ morphingContainer.style.animation = "none";
1612
+ }
1613
+ // Update speaking animation state
1614
+ if (convaiClient.state.isSpeaking) {
1615
+ if (!startTime) {
1616
+ startTime = Date.now();
1617
+ }
1618
+ }
1619
+ else {
1620
+ // Reset when not speaking
1621
+ if (startTime && !convaiClient.state.isSpeaking) {
1622
+ startTime = 0;
1623
+ currentLevels = Array(40).fill(0.05);
1624
+ targetLevels = Array(40).fill(0.05);
1625
+ }
1626
+ }
1627
+ // Auto-collapse when disconnected
1628
+ if (!convaiClient.state.isConnected && !convaiClient.state.isConnecting) {
1629
+ if (isOpen) {
1630
+ setIsOpen(false);
1631
+ }
1632
+ isMuted = false;
1633
+ isVoiceMode = false;
1634
+ isVideoVisible = false;
1635
+ hasEnteredDefaultVoiceMode = false;
1636
+ updateHeader();
1637
+ updateVoiceMode();
1638
+ }
1639
+ // Auto-enter voice mode on first connection if defaultVoiceMode is true
1640
+ if (convaiClient.state.isConnected &&
1641
+ defaultVoiceMode &&
1642
+ isOpen &&
1643
+ !hasEnteredDefaultVoiceMode &&
1644
+ !isVoiceMode) {
1645
+ setTimeout(async () => {
1646
+ try {
1647
+ await convaiClient.audioControls.unmuteAudio();
1648
+ isVoiceMode = true;
1649
+ hasEnteredDefaultVoiceMode = true;
1650
+ updateVoiceMode();
1651
+ }
1652
+ catch (error) {
1653
+ console.error("Failed to enter voice mode:", error);
1654
+ }
1655
+ }, 100);
1656
+ }
1657
+ });
1658
+ // Audio state changes now only affect voice mode (microphone button removed)
1659
+ // Video state change listener
1660
+ convaiClient.videoControls.on("videoStateChange", (videoState) => {
1661
+ if (videoState.isVideoEnabled !== undefined) {
1662
+ isVideoVisible = videoState.isVideoEnabled;
1663
+ updateVoiceMode();
1664
+ // Update tray button state
1665
+ const videoBtn = document.getElementById("convai-settings-video-btn");
1666
+ if (videoBtn) {
1667
+ const isActive = isVideoVisible;
1668
+ videoBtn.style.backgroundColor = isActive
1669
+ ? "rgba(16, 185, 129, 0.15)"
1670
+ : "transparent";
1671
+ videoBtn.style.color = isActive
1672
+ ? "#10b981"
1673
+ : aeroTheme.colors.text.primary;
1674
+ // Update Icon - first child is icon container
1675
+ const iconContainer = videoBtn.querySelector("div");
1676
+ if (iconContainer) {
1677
+ iconContainer.innerHTML = "";
1678
+ const icon = isActive ? Icons.Video("md") : Icons.VideoOff("md");
1679
+ icon.style.width = "18px";
1680
+ icon.style.height = "18px";
1681
+ iconContainer.appendChild(icon);
1682
+ }
1683
+ }
1684
+ }
1685
+ });
1686
+ // Screen share state change listener
1687
+ convaiClient.screenShareControls.on("screenShareStateChange", (screenShareState) => {
1688
+ if (screenShareState.isScreenShareActive !== undefined) {
1689
+ const isSharing = screenShareState.isScreenShareActive;
1690
+ const shareBtn = document.getElementById("convai-settings-share-btn");
1691
+ if (shareBtn) {
1692
+ const isActive = isSharing;
1693
+ shareBtn.style.backgroundColor = isActive
1694
+ ? "rgba(16, 185, 129, 0.15)"
1695
+ : "transparent";
1696
+ shareBtn.style.color = isActive
1697
+ ? "#10b981"
1698
+ : aeroTheme.colors.text.primary;
1699
+ // Update Icon - first child is icon container
1700
+ const iconContainer = shareBtn.querySelector("div");
1701
+ if (iconContainer) {
1702
+ iconContainer.innerHTML = "";
1703
+ const icon = isActive
1704
+ ? Icons.StopScreenShare("md")
1705
+ : Icons.ScreenShare("md");
1706
+ icon.style.width = "18px";
1707
+ icon.style.height = "18px";
1708
+ iconContainer.appendChild(icon);
1709
+ }
1710
+ }
1711
+ }
1712
+ });
1713
+ convaiClient.on("messagesChange", () => {
1714
+ updateMessageList();
1715
+ });
1716
+ // Bot ready listener - updates UI when bot becomes ready
1717
+ convaiClient.on("botReady", () => {
1718
+ updateHeader(); // Update header to show green status
1719
+ });
1720
+ convaiClient.on("connect", () => {
1721
+ // Initialize audio renderer on connection
1722
+ if (convaiClient.room && !audioRenderer) {
1723
+ audioRenderer = new AudioRenderer(convaiClient.room);
1724
+ }
1725
+ // Fetch character info
1726
+ fetchCharacterInfo();
1727
+ });
1728
+ convaiClient.on("disconnect", () => {
1729
+ // Cleanup audio renderer
1730
+ if (audioRenderer) {
1731
+ audioRenderer.destroy();
1732
+ audioRenderer = null;
1733
+ }
1734
+ updateMessageList();
1735
+ });
1736
+ };
1737
+ // Initialize
1738
+ createDOM();
1739
+ setupClientListeners();
1740
+ // Set initial button state
1741
+ updateSendButton(); // Show voice mode button initially since input is empty
1742
+ // Set initial voice mode UI if defaultVoiceMode is true
1743
+ if (defaultVoiceMode) {
1744
+ updateVoiceMode();
1745
+ }
1746
+ // If already connected, initialize audio renderer and fetch character info
1747
+ if (convaiClient.state.isConnected && convaiClient.room) {
1748
+ audioRenderer = new AudioRenderer(convaiClient.room);
1749
+ fetchCharacterInfo();
1750
+ }
1751
+ // Return widget instance
1752
+ const widget = {
1753
+ element: rootElement,
1754
+ destroy: () => {
1755
+ // Cleanup audio renderer
1756
+ if (audioRenderer) {
1757
+ audioRenderer.destroy();
1758
+ audioRenderer = null;
1759
+ }
1760
+ // Remove event listeners
1761
+ morphingContainer.removeEventListener("click", handleToggle);
1762
+ // Remove DOM elements
1763
+ if (rootElement.parentElement) {
1764
+ rootElement.remove();
1765
+ }
1766
+ if (floatingVideo.parentElement) {
1767
+ floatingVideo.remove();
1768
+ }
1769
+ // Unsubscribe from client events
1770
+ convaiClient.off("stateChange", () => { });
1771
+ convaiClient.off("messagesChange", () => { });
1772
+ convaiClient.off("botReady", () => { });
1773
+ convaiClient.off("connect", () => { });
1774
+ convaiClient.off("disconnect", () => { });
1775
+ },
1776
+ };
1777
+ return widget;
1778
+ }
1779
+ /**
1780
+ * Destroy a Convai widget instance
1781
+ * @param widget - Widget instance to destroy
1782
+ */
1783
+ export function destroyConvaiWidget(widget) {
1784
+ widget.destroy();
1785
+ }
1786
+ //# sourceMappingURL=ConvaiWidget.js.map