@convai/web-sdk 0.3.1-beta.3 → 0.3.2-beta.1

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