@convai/web-sdk 0.1.1-beta.3 → 0.1.1-beta.5

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