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