@convai/web-sdk 0.1.1-beta.5 → 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 (40) hide show
  1. package/README.md +720 -213
  2. package/dist/core/AudioManager.d.ts.map +1 -1
  3. package/dist/core/AudioManager.js +10 -0
  4. package/dist/core/AudioManager.js.map +1 -1
  5. package/dist/core/ConvaiClient.d.ts +4 -0
  6. package/dist/core/ConvaiClient.d.ts.map +1 -1
  7. package/dist/core/ConvaiClient.js +28 -0
  8. package/dist/core/ConvaiClient.js.map +1 -1
  9. package/dist/core/MessageHandler.d.ts.map +1 -1
  10. package/dist/core/MessageHandler.js +1 -0
  11. package/dist/core/MessageHandler.js.map +1 -1
  12. package/dist/core/ScreenShareManager.d.ts.map +1 -1
  13. package/dist/core/ScreenShareManager.js +4 -0
  14. package/dist/core/ScreenShareManager.js.map +1 -1
  15. package/dist/core/VideoManager.d.ts.map +1 -1
  16. package/dist/core/VideoManager.js +2 -0
  17. package/dist/core/VideoManager.js.map +1 -1
  18. package/dist/core/types.d.ts +13 -1
  19. package/dist/core/types.d.ts.map +1 -1
  20. package/dist/react/components/ConvaiWidget.d.ts +3 -0
  21. package/dist/react/components/ConvaiWidget.d.ts.map +1 -1
  22. package/dist/react/components/ConvaiWidget.js +20 -12
  23. package/dist/react/components/ConvaiWidget.js.map +1 -1
  24. package/dist/react/components/index.d.ts +0 -1
  25. package/dist/react/components/index.d.ts.map +1 -1
  26. package/dist/react/components/index.js +0 -2
  27. package/dist/react/components/index.js.map +1 -1
  28. package/dist/react/hooks/useConvaiClient.d.ts +3 -0
  29. package/dist/react/hooks/useConvaiClient.d.ts.map +1 -1
  30. package/dist/react/hooks/useConvaiClient.js +34 -0
  31. package/dist/react/hooks/useConvaiClient.js.map +1 -1
  32. package/dist/vanilla/ConvaiWidget.d.ts +1 -1
  33. package/dist/vanilla/ConvaiWidget.d.ts.map +1 -1
  34. package/dist/vanilla/ConvaiWidget.js +625 -367
  35. package/dist/vanilla/ConvaiWidget.js.map +1 -1
  36. package/dist/vanilla/index.d.ts +0 -2
  37. package/dist/vanilla/index.d.ts.map +1 -1
  38. package/dist/vanilla/index.js +0 -2
  39. package/dist/vanilla/index.js.map +1 -1
  40. package/package.json +1 -1
@@ -2,9 +2,9 @@
2
2
  * Vanilla ConvaiWidget - Complete UI widget for Convai conversations
3
3
  * Ports the React ConvaiWidget to vanilla TypeScript with DOM manipulation
4
4
  */
5
- import { AudioRenderer } from './AudioRenderer';
6
- import { aeroTheme, injectGlobalStyles } from './styles';
7
- import { Icons } from './icons';
5
+ import { AudioRenderer } from "./AudioRenderer";
6
+ import { aeroTheme, injectGlobalStyles } from "./styles";
7
+ import { Icons } from "./icons";
8
8
  /**
9
9
  * Create a Convai chat widget in the specified container
10
10
  *
@@ -33,7 +33,7 @@ import { Icons } from './icons';
33
33
  * ```
34
34
  */
35
35
  export function createConvaiWidget(container, options) {
36
- const { convaiClient, showVideo = true, showScreenShare = true, } = options;
36
+ const { convaiClient, showVideo = true, showScreenShare = true } = options;
37
37
  // Inject global styles
38
38
  injectGlobalStyles();
39
39
  // State
@@ -42,9 +42,9 @@ export function createConvaiWidget(container, options) {
42
42
  let isVoiceMode = false;
43
43
  let isMuted = false;
44
44
  let isVideoVisible = false;
45
- let inputValue = '';
46
- let characterName = 'Character';
47
- let characterImage = '';
45
+ let inputValue = "";
46
+ let characterName = "Character";
47
+ let characterImage = "";
48
48
  let audioRenderer = null;
49
49
  // DOM elements (will be created below)
50
50
  let rootElement;
@@ -65,31 +65,39 @@ export function createConvaiWidget(container, options) {
65
65
  let dataArray = null;
66
66
  let rafId = null;
67
67
  let source = null;
68
- // Fetch character info
68
+ // Fetch character info - matches React useCharacterInfo hook
69
69
  const fetchCharacterInfo = async () => {
70
70
  if (!convaiClient.apiKey || !convaiClient.characterId)
71
71
  return;
72
72
  try {
73
- const response = await fetch(`https://api.convai.com/character/get?charID=${convaiClient.characterId}`, {
73
+ const response = await fetch("https://api.convai.com/character/get", {
74
+ method: "POST",
74
75
  headers: {
75
- 'CONVAI-API-KEY': convaiClient.apiKey
76
- }
76
+ "Content-Type": "application/json",
77
+ "CONVAI-API-KEY": convaiClient.apiKey,
78
+ },
79
+ body: JSON.stringify({ charID: convaiClient.characterId }),
77
80
  });
78
81
  if (response.ok) {
79
82
  const data = await response.json();
80
- characterName = data.character_name || 'Character';
81
- characterImage = data.model_details?.modelLink || '';
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
+ "";
82
90
  updateHeader();
83
91
  }
84
92
  }
85
93
  catch (error) {
86
- console.error('Failed to fetch character info:', error);
94
+ console.error("Failed to fetch character info:", error);
87
95
  }
88
96
  };
89
97
  // Create root structure
90
98
  const createDOM = () => {
91
- rootElement = document.createElement('div');
92
- rootElement.className = 'convai-widget';
99
+ rootElement = document.createElement("div");
100
+ rootElement.className = "convai-widget";
93
101
  rootElement.style.cssText = `
94
102
  position: fixed;
95
103
  bottom: 1.5rem;
@@ -98,7 +106,7 @@ export function createConvaiWidget(container, options) {
98
106
  font-family: ${aeroTheme.typography.fontFamily.primary};
99
107
  `;
100
108
  // Morphing container
101
- morphingContainer = document.createElement('div');
109
+ morphingContainer = document.createElement("div");
102
110
  morphingContainer.style.cssText = `
103
111
  position: relative;
104
112
  width: 4rem;
@@ -113,9 +121,10 @@ export function createConvaiWidget(container, options) {
113
121
  display: flex;
114
122
  align-items: center;
115
123
  justify-content: center;
124
+ cursor: pointer;
116
125
  `;
117
126
  // Button content (logo)
118
- buttonContent = document.createElement('div');
127
+ buttonContent = document.createElement("div");
119
128
  buttonContent.style.cssText = `
120
129
  position: absolute;
121
130
  inset: 0;
@@ -126,11 +135,11 @@ export function createConvaiWidget(container, options) {
126
135
  opacity: 1;
127
136
  transform: scale(1);
128
137
  `;
129
- const convaiLogo = Icons.ConvaiLogo('xl', 'idle');
138
+ const convaiLogo = Icons.ConvaiLogo("xl", "idle");
130
139
  convaiLogo.style.color = aeroTheme.colors.convai.light;
131
140
  buttonContent.appendChild(convaiLogo);
132
141
  // Chat content
133
- chatContent = document.createElement('div');
142
+ chatContent = document.createElement("div");
134
143
  chatContent.style.cssText = `
135
144
  position: absolute;
136
145
  inset: 0;
@@ -147,7 +156,7 @@ export function createConvaiWidget(container, options) {
147
156
  // Header
148
157
  headerElement = createHeader();
149
158
  // Content area
150
- contentElement = document.createElement('div');
159
+ contentElement = document.createElement("div");
151
160
  contentElement.style.cssText = `
152
161
  flex: 1;
153
162
  overflow-y: auto;
@@ -172,11 +181,11 @@ export function createConvaiWidget(container, options) {
172
181
  container.appendChild(floatingVideo);
173
182
  container.appendChild(rootElement);
174
183
  // Event listeners
175
- morphingContainer.addEventListener('click', handleToggle);
184
+ morphingContainer.addEventListener("click", handleToggle);
176
185
  };
177
186
  // Create Voice Mode Overlay
178
187
  const createVoiceModeOverlay = () => {
179
- const overlay = document.createElement('div');
188
+ const overlay = document.createElement("div");
180
189
  overlay.style.cssText = `
181
190
  position: absolute;
182
191
  top: 50%;
@@ -192,8 +201,8 @@ export function createConvaiWidget(container, options) {
192
201
  gap: 1.5rem;
193
202
  `;
194
203
  // Bars Container
195
- const barsContainer = document.createElement('div');
196
- barsContainer.id = 'voice-bars-container';
204
+ const barsContainer = document.createElement("div");
205
+ barsContainer.id = "voice-bars-container";
197
206
  barsContainer.style.cssText = `
198
207
  display: flex;
199
208
  align-items: center;
@@ -204,8 +213,8 @@ export function createConvaiWidget(container, options) {
204
213
  `;
205
214
  // Create 40 bars
206
215
  for (let i = 0; i < 40; i++) {
207
- const bar = document.createElement('div');
208
- bar.className = 'voice-bar';
216
+ const bar = document.createElement("div");
217
+ bar.className = "voice-bar";
209
218
  bar.style.cssText = `
210
219
  width: 3px;
211
220
  height: 15px;
@@ -218,23 +227,23 @@ export function createConvaiWidget(container, options) {
218
227
  }
219
228
  overlay.appendChild(barsContainer);
220
229
  // Status Text
221
- const statusContainer = document.createElement('div');
222
- const statusTitle = document.createElement('div');
223
- statusTitle.id = 'voice-mode-title';
230
+ const statusContainer = document.createElement("div");
231
+ const statusTitle = document.createElement("div");
232
+ statusTitle.id = "voice-mode-title";
224
233
  statusTitle.style.cssText = `
225
234
  font-size: 14px;
226
235
  font-weight: 500;
227
236
  color: ${aeroTheme.colors.text.primary};
228
237
  margin-bottom: 0.5rem;
229
238
  `;
230
- statusTitle.textContent = 'Voice Only Mode';
231
- const statusSubtitle = document.createElement('div');
232
- statusSubtitle.id = 'voice-mode-subtitle';
239
+ statusTitle.textContent = "Voice Only Mode";
240
+ const statusSubtitle = document.createElement("div");
241
+ statusSubtitle.id = "voice-mode-subtitle";
233
242
  statusSubtitle.style.cssText = `
234
243
  font-size: 12px;
235
244
  color: ${aeroTheme.colors.text.secondary};
236
245
  `;
237
- statusSubtitle.textContent = 'Press and hold the microphone to talk';
246
+ statusSubtitle.textContent = "Press and hold the microphone to talk";
238
247
  statusContainer.appendChild(statusTitle);
239
248
  statusContainer.appendChild(statusSubtitle);
240
249
  overlay.appendChild(statusContainer);
@@ -249,7 +258,7 @@ export function createConvaiWidget(container, options) {
249
258
  const updateAudioBars = () => {
250
259
  if (!voiceModeOverlay)
251
260
  return;
252
- const bars = voiceModeOverlay.querySelectorAll('.voice-bar');
261
+ const bars = voiceModeOverlay.querySelectorAll(".voice-bar");
253
262
  const isTalking = convaiClient.state.isSpeaking;
254
263
  const isListening = !convaiClient.audioControls.isAudioMuted;
255
264
  const isAnimating = isListening || isTalking;
@@ -262,8 +271,8 @@ export function createConvaiWidget(container, options) {
262
271
  : aeroTheme.colors.neutral[400];
263
272
  });
264
273
  // Update Text
265
- const title = document.getElementById('voice-mode-title');
266
- const subtitle = document.getElementById('voice-mode-subtitle');
274
+ const title = document.getElementById("voice-mode-title");
275
+ const subtitle = document.getElementById("voice-mode-subtitle");
267
276
  if (title) {
268
277
  title.textContent = isTalking
269
278
  ? "Character Speaking..."
@@ -272,9 +281,10 @@ export function createConvaiWidget(container, options) {
272
281
  : "Voice Only Mode";
273
282
  }
274
283
  if (subtitle) {
275
- subtitle.textContent = isListening || isTalking
276
- ? "Audio active"
277
- : "Press and hold the microphone to talk";
284
+ subtitle.textContent =
285
+ isListening || isTalking
286
+ ? "Audio active"
287
+ : "Press and hold the microphone to talk";
278
288
  }
279
289
  // Animation Logic - Matches React version
280
290
  if (isListening && analyzer && dataArray) {
@@ -307,8 +317,11 @@ export function createConvaiWidget(container, options) {
307
317
  // Simulate speaking bars with natural speech patterns
308
318
  const elapsed = (Date.now() - startTime) / 1000; // seconds
309
319
  // 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) => {
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) => {
312
325
  // More variation in the middle bars, less on edges for natural spread
313
326
  const position = i / 40;
314
327
  const centerWeight = 1 - Math.abs(position - 0.5) * 0.5;
@@ -341,7 +354,7 @@ export function createConvaiWidget(container, options) {
341
354
  else {
342
355
  // Reset to idle state
343
356
  bars.forEach((bar) => {
344
- bar.style.height = '15px';
357
+ bar.style.height = "15px";
345
358
  });
346
359
  }
347
360
  rafId = requestAnimationFrame(updateAudioBars);
@@ -349,9 +362,10 @@ export function createConvaiWidget(container, options) {
349
362
  const startAudioAnalysis = async () => {
350
363
  try {
351
364
  if (!audioContext) {
352
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
365
+ audioContext = new (window.AudioContext ||
366
+ window.webkitAudioContext)();
353
367
  }
354
- if (audioContext.state === 'suspended') {
368
+ if (audioContext.state === "suspended") {
355
369
  await audioContext.resume();
356
370
  }
357
371
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
@@ -372,7 +386,7 @@ export function createConvaiWidget(container, options) {
372
386
  cancelAnimationFrame(rafId);
373
387
  if (source) {
374
388
  source.disconnect();
375
- source.mediaStream.getTracks().forEach(t => t.stop());
389
+ source.mediaStream.getTracks().forEach((t) => t.stop());
376
390
  source = null;
377
391
  }
378
392
  // Keep context alive if possible, or close it? React component closes it.
@@ -380,7 +394,7 @@ export function createConvaiWidget(container, options) {
380
394
  };
381
395
  // Create header
382
396
  const createHeader = () => {
383
- const header = document.createElement('div');
397
+ const header = document.createElement("div");
384
398
  header.style.cssText = `
385
399
  display: flex;
386
400
  align-items: center;
@@ -388,16 +402,11 @@ export function createConvaiWidget(container, options) {
388
402
  padding: 1rem;
389
403
  border-bottom: 1px solid ${aeroTheme.colors.neutral[200]};
390
404
  background: white;
405
+ position: relative;
391
406
  `;
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');
407
+ // Close button on the left
408
+ const closeButton = document.createElement("button");
409
+ const chevronIcon = Icons.ChevronDown("md");
401
410
  closeButton.appendChild(chevronIcon);
402
411
  closeButton.style.cssText = `
403
412
  font-size: 1.25rem;
@@ -405,24 +414,30 @@ export function createConvaiWidget(container, options) {
405
414
  cursor: pointer;
406
415
  padding: 0.25rem;
407
416
  transition: ${aeroTheme.transitions.fast};
417
+ background: transparent;
418
+ border: none;
408
419
  `;
409
- closeButton.addEventListener('click', (e) => {
420
+ closeButton.addEventListener("click", (e) => {
410
421
  e.stopPropagation();
411
422
  handleClose();
412
423
  });
413
- const titleSection = document.createElement('div');
424
+ // Title section centered - matches React version
425
+ const titleSection = document.createElement("div");
414
426
  titleSection.style.cssText = `
427
+ position: absolute;
428
+ left: 50%;
429
+ transform: translateX(-50%);
415
430
  display: flex;
416
431
  align-items: center;
417
432
  gap: 0.5rem;
418
- flex: 1;
419
433
  font-size: ${aeroTheme.typography.fontSize.base};
420
434
  font-weight: ${aeroTheme.typography.fontWeight.semibold};
421
435
  color: ${aeroTheme.colors.text.primary};
422
436
  `;
423
- titleSection.id = 'convai-widget-title';
424
- const settingsButton = document.createElement('button');
425
- const moreIcon = Icons.MoreVertical('md');
437
+ titleSection.id = "convai-widget-title";
438
+ // Settings button on the right
439
+ const settingsButton = document.createElement("button");
440
+ const moreIcon = Icons.MoreVertical("md");
426
441
  settingsButton.appendChild(moreIcon);
427
442
  settingsButton.style.cssText = `
428
443
  font-size: 1.5rem;
@@ -430,25 +445,27 @@ export function createConvaiWidget(container, options) {
430
445
  cursor: pointer;
431
446
  padding: 0.25rem;
432
447
  transition: ${aeroTheme.transitions.fast};
448
+ background: transparent;
449
+ border: none;
450
+ margin-left: auto;
433
451
  `;
434
- settingsButton.addEventListener('click', (e) => {
452
+ settingsButton.addEventListener("click", (e) => {
435
453
  e.stopPropagation();
436
454
  handleSettingsToggle();
437
455
  });
438
- leftSection.appendChild(closeButton);
439
- leftSection.appendChild(titleSection);
440
- header.appendChild(leftSection);
456
+ header.appendChild(closeButton);
457
+ header.appendChild(titleSection);
441
458
  header.appendChild(settingsButton);
442
459
  return header;
443
460
  };
444
461
  // Update header with character info
445
462
  const updateHeader = () => {
446
- const titleSection = document.getElementById('convai-widget-title');
463
+ const titleSection = document.getElementById("convai-widget-title");
447
464
  if (!titleSection)
448
465
  return;
449
- titleSection.innerHTML = '';
466
+ titleSection.innerHTML = "";
450
467
  if (characterImage) {
451
- const img = document.createElement('img');
468
+ const img = document.createElement("img");
452
469
  img.src = characterImage;
453
470
  img.alt = characterName;
454
471
  img.style.cssText = `
@@ -462,14 +479,16 @@ export function createConvaiWidget(container, options) {
462
479
  `;
463
480
  titleSection.appendChild(img);
464
481
  }
465
- const nameSpan = document.createElement('span');
482
+ const nameSpan = document.createElement("span");
466
483
  nameSpan.textContent = characterName;
467
484
  titleSection.appendChild(nameSpan);
468
485
  // 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';
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";
473
492
  muteButton.appendChild(volumeIcon);
474
493
  muteButton.style.cssText = `
475
494
  cursor: pointer;
@@ -485,21 +504,21 @@ export function createConvaiWidget(container, options) {
485
504
  outline: none;
486
505
  transition: transform 0.1s ease-out;
487
506
  `;
488
- muteButton.addEventListener('click', (e) => {
507
+ muteButton.addEventListener("click", (e) => {
489
508
  e.stopPropagation();
490
509
  handleToggleMute();
491
510
  });
492
- muteButton.addEventListener('mouseenter', () => {
493
- muteButton.style.transform = 'scale(1.1)';
511
+ muteButton.addEventListener("mouseenter", () => {
512
+ muteButton.style.transform = "scale(1.1)";
494
513
  });
495
- muteButton.addEventListener('mouseleave', () => {
496
- muteButton.style.transform = 'scale(1)';
514
+ muteButton.addEventListener("mouseleave", () => {
515
+ muteButton.style.transform = "scale(1)";
497
516
  });
498
517
  titleSection.appendChild(muteButton);
499
518
  // Voice Mode Badge
500
519
  if (isVoiceMode) {
501
- const voiceBadge = document.createElement('span');
502
- voiceBadge.textContent = 'VOICE';
520
+ const voiceBadge = document.createElement("span");
521
+ voiceBadge.textContent = "VOICE";
503
522
  voiceBadge.style.cssText = `
504
523
  font-size: 10px;
505
524
  color: ${aeroTheme.colors.convai.light};
@@ -514,8 +533,8 @@ export function createConvaiWidget(container, options) {
514
533
  };
515
534
  // Create message list
516
535
  const createMessageList = () => {
517
- const list = document.createElement('div');
518
- list.id = 'convai-message-list';
536
+ const list = document.createElement("div");
537
+ list.id = "convai-message-list";
519
538
  list.style.cssText = `
520
539
  display: flex;
521
540
  flex-direction: column;
@@ -526,7 +545,7 @@ export function createConvaiWidget(container, options) {
526
545
  };
527
546
  // Create footer
528
547
  const createFooter = () => {
529
- const footer = document.createElement('div');
548
+ const footer = document.createElement("div");
530
549
  footer.style.cssText = `
531
550
  padding: 1rem;
532
551
  border-top: 1px solid ${aeroTheme.colors.neutral[200]};
@@ -537,9 +556,9 @@ export function createConvaiWidget(container, options) {
537
556
  position: relative;
538
557
  `;
539
558
  // 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');
559
+ const voiceExitButton = document.createElement("button");
560
+ voiceExitButton.id = "convai-voice-exit-btn";
561
+ const exitIcon = Icons.Waveform("md");
543
562
  voiceExitButton.appendChild(exitIcon);
544
563
  voiceExitButton.style.cssText = `
545
564
  width: 2.25rem;
@@ -554,14 +573,16 @@ export function createConvaiWidget(container, options) {
554
573
  margin: 0 auto;
555
574
  border: none;
556
575
  `;
557
- voiceExitButton.addEventListener('click', () => {
576
+ voiceExitButton.addEventListener("click", async () => {
577
+ convaiClient.sendInterruptMessage();
578
+ await convaiClient.audioControls.muteAudio(); // Mute on exit
558
579
  isVoiceMode = false;
559
580
  updateVoiceMode();
560
581
  });
561
582
  footer.appendChild(voiceExitButton);
562
583
  // Standard Footer Content (Mic + Input)
563
- const standardContent = document.createElement('div');
564
- standardContent.id = 'convai-footer-standard';
584
+ const standardContent = document.createElement("div");
585
+ standardContent.id = "convai-footer-standard";
565
586
  standardContent.style.cssText = `
566
587
  display: flex;
567
588
  gap: 0.5rem;
@@ -569,9 +590,9 @@ export function createConvaiWidget(container, options) {
569
590
  width: 100%;
570
591
  `;
571
592
  // Mic button
572
- const micButton = document.createElement('button');
573
- micButton.id = 'convai-mic-button';
574
- const micIcon = Icons.Mic('md');
593
+ const micButton = document.createElement("button");
594
+ micButton.id = "convai-mic-button";
595
+ const micIcon = Icons.Mic("md");
575
596
  micButton.appendChild(micIcon);
576
597
  const initialMicBackground = convaiClient.audioControls.isAudioMuted
577
598
  ? aeroTheme.colors.error[500]
@@ -586,23 +607,54 @@ export function createConvaiWidget(container, options) {
586
607
  align-items: center;
587
608
  justify-content: center;
588
609
  cursor: pointer;
589
- transition: ${aeroTheme.transitions.fast};
610
+ transition: background-color 0.2s ease-out, transform 0.15s ease-out, box-shadow 0.2s ease-out;
590
611
  flex-shrink: 0;
591
612
  border: none;
613
+ transform: scale(1);
614
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
592
615
  `;
593
- micButton.addEventListener('click', handleMicToggle);
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);
594
646
  standardContent.appendChild(micButton);
595
647
  // Input container
596
- const inputContainer = document.createElement('div');
648
+ const inputContainer = document.createElement("div");
597
649
  inputContainer.style.cssText = `
598
650
  flex: 1;
599
651
  position: relative;
600
652
  display: flex;
601
653
  align-items: center;
602
654
  `;
603
- inputElement = document.createElement('input');
604
- inputElement.type = 'text';
605
- inputElement.placeholder = 'Conversation';
655
+ inputElement = document.createElement("input");
656
+ inputElement.type = "text";
657
+ inputElement.placeholder = "Conversation";
606
658
  inputElement.style.cssText = `
607
659
  width: 100%;
608
660
  padding: 0.75rem 3rem 0.75rem 1rem;
@@ -614,18 +666,18 @@ export function createConvaiWidget(container, options) {
614
666
  transition: ${aeroTheme.transitions.fast};
615
667
  outline: none;
616
668
  `;
617
- inputElement.addEventListener('input', (e) => {
669
+ inputElement.addEventListener("input", (e) => {
618
670
  inputValue = e.target.value;
619
671
  updateSendButton();
620
672
  });
621
- inputElement.addEventListener('keypress', (e) => {
622
- if (e.key === 'Enter')
673
+ inputElement.addEventListener("keypress", (e) => {
674
+ if (e.key === "Enter")
623
675
  handleSend();
624
676
  });
625
677
  // Send button
626
- const sendButton = document.createElement('button');
627
- sendButton.id = 'convai-send-button';
628
- const sendIcon = Icons.Send('md');
678
+ const sendButton = document.createElement("button");
679
+ sendButton.id = "convai-send-button";
680
+ const sendIcon = Icons.Send("md");
629
681
  sendButton.appendChild(sendIcon);
630
682
  sendButton.style.cssText = `
631
683
  position: absolute;
@@ -642,12 +694,14 @@ export function createConvaiWidget(container, options) {
642
694
  transition: ${aeroTheme.transitions.fast};
643
695
  border: none;
644
696
  `;
645
- sendButton.addEventListener('click', () => {
697
+ sendButton.addEventListener("click", async () => {
646
698
  if (inputValue.length > 0) {
647
699
  handleSend();
648
700
  }
649
701
  else {
650
702
  // Toggle Voice Mode
703
+ convaiClient.sendInterruptMessage();
704
+ await convaiClient.audioControls.unmuteAudio(); // Unmute on enter
651
705
  isVoiceMode = true;
652
706
  updateVoiceMode();
653
707
  }
@@ -660,8 +714,8 @@ export function createConvaiWidget(container, options) {
660
714
  };
661
715
  // Create settings tray - Matches React SettingsTray component exactly
662
716
  const createSettingsTray = () => {
663
- const tray = document.createElement('div');
664
- tray.setAttribute('data-settings-tray', 'true');
717
+ const tray = document.createElement("div");
718
+ tray.setAttribute("data-settings-tray", "true");
665
719
  tray.style.cssText = `
666
720
  position: absolute;
667
721
  top: 60px;
@@ -681,9 +735,9 @@ export function createConvaiWidget(container, options) {
681
735
  overflow: hidden;
682
736
  `;
683
737
  // 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';
738
+ if (!document.getElementById("convai-widget-keyframes")) {
739
+ const style = document.createElement("style");
740
+ style.id = "convai-widget-keyframes";
687
741
  style.textContent = `
688
742
  @keyframes popIn {
689
743
  from { opacity: 0; transform: scale(0.95) translateY(-10px); }
@@ -693,7 +747,7 @@ export function createConvaiWidget(container, options) {
693
747
  document.head.appendChild(style);
694
748
  }
695
749
  // Horizontal Settings Row Container
696
- const settingsRow = document.createElement('div');
750
+ const settingsRow = document.createElement("div");
697
751
  settingsRow.style.cssText = `
698
752
  display: flex;
699
753
  flex-direction: row;
@@ -702,7 +756,7 @@ export function createConvaiWidget(container, options) {
702
756
  gap: 4px;
703
757
  `;
704
758
  const createOption = (icon, label, onClick, isActive = false, isDestructive = false) => {
705
- const btn = document.createElement('div');
759
+ const btn = document.createElement("div");
706
760
  btn.style.cssText = `
707
761
  display: flex;
708
762
  flex-direction: column;
@@ -712,85 +766,184 @@ export function createConvaiWidget(container, options) {
712
766
  cursor: pointer;
713
767
  border-radius: ${aeroTheme.borderRadius.md};
714
768
  background-color: ${isActive
715
- ? 'rgba(16, 185, 129, 0.15)'
769
+ ? "rgba(16, 185, 129, 0.15)"
716
770
  : isDestructive
717
- ? 'rgba(239, 68, 68, 0.1)'
718
- : 'transparent'};
771
+ ? "rgba(239, 68, 68, 0.1)"
772
+ : "transparent"};
719
773
  color: ${isActive
720
- ? '#10b981'
774
+ ? "#10b981"
721
775
  : isDestructive
722
- ? '#ef4444'
776
+ ? "#ef4444"
723
777
  : aeroTheme.colors.text.primary};
724
778
  transition: ${aeroTheme.transitions.fast};
725
779
  min-width: 50px;
726
780
  `;
727
781
  // Icon container
728
- const iconContainer = document.createElement('div');
782
+ const iconContainer = document.createElement("div");
729
783
  iconContainer.appendChild(icon);
730
784
  btn.appendChild(iconContainer);
731
- const span = document.createElement('span');
785
+ const span = document.createElement("span");
732
786
  span.textContent = label;
733
787
  span.style.cssText = `
734
788
  font-size: 10px;
735
789
  font-weight: 500;
736
790
  `;
737
791
  btn.appendChild(span);
738
- btn.addEventListener('click', (e) => {
792
+ btn.addEventListener("click", (e) => {
739
793
  e.stopPropagation();
740
794
  onClick();
741
795
  });
742
- btn.addEventListener('mouseover', () => {
743
- btn.style.transform = 'scale(1.02)';
796
+ btn.addEventListener("mouseover", () => {
797
+ btn.style.transform = "scale(1.02)";
744
798
  if (!isActive && !isDestructive) {
745
799
  btn.style.backgroundColor = aeroTheme.colors.neutral[100];
746
800
  }
747
801
  });
748
- btn.addEventListener('mouseout', () => {
749
- btn.style.transform = 'scale(1)';
802
+ btn.addEventListener("mouseout", () => {
803
+ btn.style.transform = "scale(1)";
750
804
  btn.style.backgroundColor = isActive
751
- ? 'rgba(16, 185, 129, 0.15)'
805
+ ? "rgba(16, 185, 129, 0.15)"
752
806
  : isDestructive
753
- ? 'rgba(239, 68, 68, 0.1)'
754
- : 'transparent';
807
+ ? "rgba(239, 68, 68, 0.1)"
808
+ : "transparent";
755
809
  });
756
810
  return btn;
757
811
  };
758
812
  // Reset
759
- const resetIcon = Icons.Redo('md');
760
- resetIcon.style.width = '18px';
761
- resetIcon.style.height = '18px';
762
- settingsRow.appendChild(createOption(resetIcon, 'Reset', handleReset));
813
+ const resetIcon = Icons.Redo("md");
814
+ resetIcon.style.width = "18px";
815
+ resetIcon.style.height = "18px";
816
+ settingsRow.appendChild(createOption(resetIcon, "Reset", handleReset));
763
817
  // 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';
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";
770
826
  settingsRow.appendChild(videoBtn);
771
827
  }
772
828
  // Screen Share
773
- if (convaiClient.connectionType === 'video' && showScreenShare) {
829
+ if (convaiClient.connectionType === "video" && showScreenShare) {
774
830
  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';
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";
780
838
  settingsRow.appendChild(shareBtn);
781
839
  }
782
840
  // Disconnect
783
- const disconnectIcon = document.createElement('span');
784
- disconnectIcon.innerHTML = '';
785
- disconnectIcon.style.fontSize = '18px';
786
- settingsRow.appendChild(createOption(disconnectIcon, 'Disconnect', handleDisconnect, false, true));
841
+ const disconnectIcon = document.createElement("span");
842
+ disconnectIcon.innerHTML = "";
843
+ disconnectIcon.style.fontSize = "18px";
844
+ settingsRow.appendChild(createOption(disconnectIcon, "Disconnect", handleDisconnect, false, true));
787
845
  tray.appendChild(settingsRow);
788
846
  return tray;
789
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
+ };
790
943
  // Create floating video - Matches React FloatingVideo component
791
944
  const createFloatingVideo = () => {
792
- const container = document.createElement('div');
793
- container.id = 'floating-video-container';
945
+ const container = document.createElement("div");
946
+ container.id = "floating-video-container";
794
947
  container.style.cssText = `
795
948
  position: fixed;
796
949
  left: 20px;
@@ -804,7 +957,7 @@ export function createConvaiWidget(container, options) {
804
957
  display: none;
805
958
  transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1), transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
806
959
  `;
807
- const wrapper = document.createElement('div');
960
+ const wrapper = document.createElement("div");
808
961
  wrapper.style.cssText = `
809
962
  background: rgba(15, 23, 42, 0.95);
810
963
  backdrop-filter: blur(20px);
@@ -813,7 +966,7 @@ export function createConvaiWidget(container, options) {
813
966
  overflow: hidden;
814
967
  `;
815
968
  // Header
816
- const header = document.createElement('div');
969
+ const header = document.createElement("div");
817
970
  header.style.cssText = `
818
971
  display: flex;
819
972
  align-items: center;
@@ -822,7 +975,7 @@ export function createConvaiWidget(container, options) {
822
975
  background: rgba(0, 0, 0, 0.3);
823
976
  border-bottom: 1px solid ${aeroTheme.colors.neutral[700]};
824
977
  `;
825
- const headerLeft = document.createElement('div');
978
+ const headerLeft = document.createElement("div");
826
979
  headerLeft.style.cssText = `
827
980
  display: flex;
828
981
  align-items: center;
@@ -831,18 +984,18 @@ export function createConvaiWidget(container, options) {
831
984
  font-size: 12px;
832
985
  font-weight: 500;
833
986
  `;
834
- const dragIcon = Icons.DragIndicator('sm');
835
- dragIcon.style.width = '16px';
836
- dragIcon.style.height = '16px';
987
+ const dragIcon = Icons.DragIndicator("sm");
988
+ dragIcon.style.width = "16px";
989
+ dragIcon.style.height = "16px";
837
990
  headerLeft.appendChild(dragIcon);
838
- const label = document.createElement('span');
839
- label.textContent = 'Your Camera';
991
+ const label = document.createElement("span");
992
+ label.textContent = "Your Camera";
840
993
  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';
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";
846
999
  closeButton.appendChild(closeIcon);
847
1000
  closeButton.style.cssText = `
848
1001
  background: transparent;
@@ -855,28 +1008,28 @@ export function createConvaiWidget(container, options) {
855
1008
  padding: 4px;
856
1009
  transition: transform 0.1s ease-out;
857
1010
  `;
858
- closeButton.addEventListener('click', (e) => {
1011
+ closeButton.addEventListener("click", (e) => {
859
1012
  e.stopPropagation();
860
1013
  convaiClient.videoControls.disableVideo();
861
1014
  });
862
- closeButton.addEventListener('mouseenter', () => {
863
- closeButton.style.transform = 'scale(1.1)';
1015
+ closeButton.addEventListener("mouseenter", () => {
1016
+ closeButton.style.transform = "scale(1.1)";
864
1017
  });
865
- closeButton.addEventListener('mouseleave', () => {
866
- closeButton.style.transform = 'scale(1)';
1018
+ closeButton.addEventListener("mouseleave", () => {
1019
+ closeButton.style.transform = "scale(1)";
867
1020
  });
868
1021
  header.appendChild(headerLeft);
869
1022
  header.appendChild(closeButton);
870
1023
  // Video Container
871
- const videoContainer = document.createElement('div');
1024
+ const videoContainer = document.createElement("div");
872
1025
  videoContainer.style.cssText = `
873
1026
  position: relative;
874
1027
  width: 100%;
875
1028
  padding-bottom: 75%;
876
1029
  background: #000;
877
1030
  `;
878
- const videoWrapper = document.createElement('div');
879
- videoWrapper.id = 'floating-video-wrapper';
1031
+ const videoWrapper = document.createElement("div");
1032
+ videoWrapper.id = "floating-video-wrapper";
880
1033
  videoWrapper.style.cssText = `
881
1034
  position: absolute;
882
1035
  top: 0;
@@ -884,8 +1037,8 @@ export function createConvaiWidget(container, options) {
884
1037
  width: 100%;
885
1038
  height: 100%;
886
1039
  `;
887
- const videoElement = document.createElement('video');
888
- videoElement.id = 'floating-video-element';
1040
+ const videoElement = document.createElement("video");
1041
+ videoElement.id = "floating-video-element";
889
1042
  videoElement.autoplay = true;
890
1043
  videoElement.playsInline = true;
891
1044
  videoElement.muted = true;
@@ -896,8 +1049,8 @@ export function createConvaiWidget(container, options) {
896
1049
  transform: scaleX(-1);
897
1050
  `;
898
1051
  // Camera off placeholder
899
- const cameraOffPlaceholder = document.createElement('div');
900
- cameraOffPlaceholder.id = 'camera-off-placeholder';
1052
+ const cameraOffPlaceholder = document.createElement("div");
1053
+ cameraOffPlaceholder.id = "camera-off-placeholder";
901
1054
  cameraOffPlaceholder.style.cssText = `
902
1055
  width: 100%;
903
1056
  height: 100%;
@@ -909,12 +1062,12 @@ export function createConvaiWidget(container, options) {
909
1062
  gap: 12px;
910
1063
  background: #000;
911
1064
  `;
912
- const cameraOffIcon = Icons.VideoOff('lg');
913
- cameraOffIcon.style.width = '48px';
914
- cameraOffIcon.style.height = '48px';
1065
+ const cameraOffIcon = Icons.VideoOff("lg");
1066
+ cameraOffIcon.style.width = "48px";
1067
+ cameraOffIcon.style.height = "48px";
915
1068
  cameraOffPlaceholder.appendChild(cameraOffIcon);
916
- const cameraOffText = document.createElement('span');
917
- cameraOffText.textContent = 'Camera is off';
1069
+ const cameraOffText = document.createElement("span");
1070
+ cameraOffText.textContent = "Camera is off";
918
1071
  cameraOffText.style.cssText = `
919
1072
  font-size: 14px;
920
1073
  text-align: center;
@@ -933,7 +1086,7 @@ export function createConvaiWidget(container, options) {
933
1086
  let startLeft = 20;
934
1087
  let startTop = 20;
935
1088
  const handleMouseDown = (e) => {
936
- if (e.target.closest('button'))
1089
+ if (e.target.closest("button"))
937
1090
  return;
938
1091
  isDragging = true;
939
1092
  startX = e.clientX;
@@ -941,7 +1094,7 @@ export function createConvaiWidget(container, options) {
941
1094
  const style = window.getComputedStyle(container);
942
1095
  startLeft = parseInt(style.left);
943
1096
  startTop = parseInt(style.top);
944
- container.style.cursor = 'grabbing';
1097
+ container.style.cursor = "grabbing";
945
1098
  e.preventDefault();
946
1099
  };
947
1100
  const handleMouseMove = (e) => {
@@ -962,12 +1115,12 @@ export function createConvaiWidget(container, options) {
962
1115
  const handleMouseUp = () => {
963
1116
  if (isDragging) {
964
1117
  isDragging = false;
965
- container.style.cursor = 'grab';
1118
+ container.style.cursor = "grab";
966
1119
  }
967
1120
  };
968
- container.addEventListener('mousedown', handleMouseDown);
969
- document.addEventListener('mousemove', handleMouseMove);
970
- document.addEventListener('mouseup', handleMouseUp);
1121
+ container.addEventListener("mousedown", handleMouseDown);
1122
+ document.addEventListener("mousemove", handleMouseMove);
1123
+ document.addEventListener("mouseup", handleMouseUp);
971
1124
  return container;
972
1125
  };
973
1126
  // Event handlers
@@ -983,7 +1136,7 @@ export function createConvaiWidget(container, options) {
983
1136
  setIsOpen(true);
984
1137
  }
985
1138
  catch (error) {
986
- console.error('Failed to connect:', error);
1139
+ console.error("Failed to connect:", error);
987
1140
  }
988
1141
  }
989
1142
  else {
@@ -997,8 +1150,8 @@ export function createConvaiWidget(container, options) {
997
1150
  const handleSend = () => {
998
1151
  if (inputValue.trim() && convaiClient.state.isConnected) {
999
1152
  convaiClient.sendUserTextMessage(inputValue);
1000
- inputValue = '';
1001
- inputElement.value = '';
1153
+ inputValue = "";
1154
+ inputElement.value = "";
1002
1155
  updateSendButton();
1003
1156
  }
1004
1157
  };
@@ -1033,7 +1186,7 @@ export function createConvaiWidget(container, options) {
1033
1186
  updateSendButton();
1034
1187
  }
1035
1188
  catch (error) {
1036
- console.error('Failed to reset:', error);
1189
+ console.error("Failed to reset:", error);
1037
1190
  }
1038
1191
  };
1039
1192
  const handleDisconnect = async () => {
@@ -1047,163 +1200,137 @@ export function createConvaiWidget(container, options) {
1047
1200
  updateSendButton();
1048
1201
  };
1049
1202
  const handleToggleVideo = async () => {
1050
- if (convaiClient.connectionType !== 'video')
1203
+ if (convaiClient.connectionType !== "video")
1051
1204
  return;
1052
1205
  try {
1053
- if (convaiClient.videoControls.isVideoEnabled) {
1206
+ if (isVideoVisible) {
1054
1207
  await convaiClient.videoControls.disableVideo();
1055
1208
  }
1056
1209
  else {
1057
1210
  await convaiClient.videoControls.enableVideo();
1058
1211
  }
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
- }
1212
+ // State will be updated via event listener below
1077
1213
  }
1078
1214
  catch (e) {
1079
- console.error("Toggle video failed", e);
1215
+ console.error("Failed to toggle video:", e);
1080
1216
  }
1081
1217
  };
1082
1218
  const handleToggleScreenShare = async () => {
1083
1219
  try {
1084
1220
  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
- }
1221
+ // State will be updated via event listener above
1101
1222
  }
1102
1223
  catch (e) {
1103
- console.error("Toggle screen share failed", e);
1224
+ console.error("Failed to toggle screen share:", e);
1104
1225
  }
1105
1226
  };
1106
1227
  const handleSettingsToggle = () => {
1228
+ // Regenerate settings tray content BEFORE opening to reflect current connection type
1229
+ if (!isSettingsOpen) {
1230
+ updateSettingsTray();
1231
+ }
1107
1232
  setIsSettingsOpen(!isSettingsOpen);
1108
1233
  };
1109
1234
  // Update functions
1110
1235
  const setIsOpen = (open) => {
1111
1236
  isOpen = open;
1112
1237
  if (open) {
1113
- morphingContainer.style.width = '400px';
1114
- morphingContainer.style.height = '600px';
1238
+ morphingContainer.style.width = "400px";
1239
+ morphingContainer.style.height = "600px";
1115
1240
  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';
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";
1123
1248
  }
1124
1249
  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';
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";
1135
1260
  }
1136
1261
  };
1137
1262
  const setIsSettingsOpen = (open) => {
1138
1263
  isSettingsOpen = open;
1139
- settingsTray.style.display = open ? 'flex' : 'none';
1264
+ settingsTray.style.display = open ? "flex" : "none";
1140
1265
  };
1141
1266
  const updateSendButton = () => {
1142
- const sendButton = document.getElementById('convai-send-button');
1267
+ const sendButton = document.getElementById("convai-send-button");
1143
1268
  if (!sendButton)
1144
1269
  return;
1145
1270
  // Clear previous content
1146
- sendButton.innerHTML = '';
1271
+ sendButton.innerHTML = "";
1147
1272
  if (inputValue.length > 0) {
1148
1273
  // Show Send Button
1149
- sendButton.appendChild(Icons.Send('md'));
1274
+ sendButton.appendChild(Icons.Send("md"));
1150
1275
  sendButton.style.background = aeroTheme.colors.text.primary;
1151
- sendButton.style.color = 'white';
1152
- sendButton.style.border = 'none';
1276
+ sendButton.style.color = "white";
1277
+ sendButton.style.border = "none";
1153
1278
  sendButton.title = "Send";
1154
1279
  }
1155
1280
  else {
1156
1281
  // Show Voice Mode Toggle
1157
- sendButton.appendChild(Icons.Waveform('md'));
1158
- sendButton.style.background = 'transparent';
1282
+ sendButton.appendChild(Icons.Waveform("md"));
1283
+ sendButton.style.background = "transparent";
1159
1284
  sendButton.style.color = aeroTheme.colors.text.primary;
1160
1285
  sendButton.style.border = `1px solid ${aeroTheme.colors.neutral[300]}`;
1161
1286
  sendButton.title = "Voice Mode";
1162
1287
  }
1163
1288
  };
1164
1289
  const updateVideoTrack = () => {
1165
- if (!floatingVideo || !convaiClient.room || !convaiClient.room.localParticipant)
1290
+ if (!floatingVideo ||
1291
+ !convaiClient.room ||
1292
+ !convaiClient.room.localParticipant)
1166
1293
  return;
1167
- const videoEl = floatingVideo.querySelector('#floating-video-element');
1168
- const placeholder = floatingVideo.querySelector('#camera-off-placeholder');
1294
+ const videoEl = floatingVideo.querySelector("#floating-video-element");
1295
+ const placeholder = floatingVideo.querySelector("#camera-off-placeholder");
1169
1296
  if (!videoEl || !placeholder)
1170
1297
  return;
1171
1298
  if (isVideoVisible && !isVoiceMode) {
1172
1299
  const tracks = Array.from(convaiClient.room.localParticipant.videoTrackPublications.values());
1173
- const videoPub = tracks.find((t) => t.kind === 'video');
1300
+ const videoPub = tracks.find((t) => t.kind === "video");
1174
1301
  if (videoPub && videoPub.track) {
1175
1302
  videoPub.track.attach(videoEl);
1176
- videoEl.style.display = 'block';
1177
- placeholder.style.display = 'none';
1303
+ videoEl.style.display = "block";
1304
+ placeholder.style.display = "none";
1178
1305
  }
1179
1306
  else {
1180
- videoEl.style.display = 'none';
1181
- placeholder.style.display = 'flex';
1307
+ videoEl.style.display = "none";
1308
+ placeholder.style.display = "flex";
1182
1309
  }
1183
1310
  }
1184
1311
  else {
1185
- videoEl.style.display = 'none';
1186
- placeholder.style.display = 'flex';
1312
+ videoEl.style.display = "none";
1313
+ placeholder.style.display = "flex";
1187
1314
  }
1188
1315
  };
1189
1316
  const updateVoiceMode = () => {
1190
- const standardFooter = document.getElementById('convai-footer-standard');
1191
- const voiceExitBtn = document.getElementById('convai-voice-exit-btn');
1317
+ const standardFooter = document.getElementById("convai-footer-standard");
1318
+ const voiceExitBtn = document.getElementById("convai-voice-exit-btn");
1192
1319
  if (isVoiceMode) {
1193
1320
  // Show Voice Overlay
1194
1321
  if (voiceModeOverlay)
1195
- voiceModeOverlay.style.display = 'flex';
1322
+ voiceModeOverlay.style.display = "flex";
1196
1323
  if (messageListElement)
1197
- messageListElement.style.display = 'none';
1324
+ messageListElement.style.display = "none";
1198
1325
  if (floatingVideo)
1199
- floatingVideo.style.display = 'none';
1326
+ floatingVideo.style.display = "none";
1200
1327
  // Footer: Show Exit Button, Hide Standard
1201
1328
  if (standardFooter)
1202
- standardFooter.style.display = 'none';
1329
+ standardFooter.style.display = "none";
1203
1330
  if (voiceExitBtn)
1204
- voiceExitBtn.style.display = 'flex';
1331
+ voiceExitBtn.style.display = "flex";
1205
1332
  if (footerElement)
1206
- footerElement.style.justifyContent = 'center';
1333
+ footerElement.style.justifyContent = "center";
1207
1334
  // Update header to show VOICE badge
1208
1335
  updateHeader();
1209
1336
  // Reset animation state for voice mode
@@ -1216,28 +1343,28 @@ export function createConvaiWidget(container, options) {
1216
1343
  else {
1217
1344
  // Hide Overlay
1218
1345
  if (voiceModeOverlay)
1219
- voiceModeOverlay.style.display = 'none';
1346
+ voiceModeOverlay.style.display = "none";
1220
1347
  // Show Video or Message List
1221
1348
  if (isVideoVisible) {
1222
1349
  if (messageListElement)
1223
- messageListElement.style.display = 'none';
1350
+ messageListElement.style.display = "none";
1224
1351
  if (floatingVideo)
1225
- floatingVideo.style.display = 'block';
1352
+ floatingVideo.style.display = "block";
1226
1353
  updateVideoTrack();
1227
1354
  }
1228
1355
  else {
1229
1356
  if (messageListElement)
1230
- messageListElement.style.display = 'flex';
1357
+ messageListElement.style.display = "flex";
1231
1358
  if (floatingVideo)
1232
- floatingVideo.style.display = 'none';
1359
+ floatingVideo.style.display = "none";
1233
1360
  }
1234
1361
  // Footer: Show Standard, Hide Exit
1235
1362
  if (standardFooter)
1236
- standardFooter.style.display = 'flex';
1363
+ standardFooter.style.display = "flex";
1237
1364
  if (voiceExitBtn)
1238
- voiceExitBtn.style.display = 'none';
1365
+ voiceExitBtn.style.display = "none";
1239
1366
  if (footerElement)
1240
- footerElement.style.justifyContent = 'flex-start';
1367
+ footerElement.style.justifyContent = "flex-start";
1241
1368
  // Update header to hide VOICE badge
1242
1369
  updateHeader();
1243
1370
  // Stop Audio Analysis
@@ -1245,12 +1372,32 @@ export function createConvaiWidget(container, options) {
1245
1372
  }
1246
1373
  };
1247
1374
  const updateMicButton = () => {
1248
- // Update mic button appearance based on mute state
1249
- const micButton = document.getElementById('convai-mic-button');
1375
+ // Update mic button appearance based on mute state with visual feedback
1376
+ const micButton = document.getElementById("convai-mic-button");
1250
1377
  if (micButton) {
1251
- micButton.style.background = convaiClient.audioControls.isAudioMuted
1378
+ const isMuted = convaiClient.audioControls.isAudioMuted;
1379
+ const newBackground = isMuted
1252
1380
  ? aeroTheme.colors.error[500]
1253
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);
1254
1401
  }
1255
1402
  };
1256
1403
  // Helper for Markdown - matches React MarkdownRenderer.tsx
@@ -1258,8 +1405,8 @@ export function createConvaiWidget(container, options) {
1258
1405
  if (!text)
1259
1406
  return;
1260
1407
  // Handle both actual newlines and \n escape sequences
1261
- const normalizedText = text.replace(/\\n/g, '\n');
1262
- const lines = normalizedText.split('\n');
1408
+ const normalizedText = text.replace(/\\n/g, "\n");
1409
+ const lines = normalizedText.split("\n");
1263
1410
  lines.forEach((line, lineIndex) => {
1264
1411
  // Process markdown within each line
1265
1412
  const processLine = (text) => {
@@ -1281,21 +1428,31 @@ export function createConvaiWidget(container, options) {
1281
1428
  }
1282
1429
  // Determine if it's bold or italic
1283
1430
  if (match[1] && match[2]) {
1284
- // Bold text (** or __)
1285
- const strong = document.createElement('strong');
1431
+ // Bold text (** or __) - matches React UserBubble styling
1432
+ const strong = document.createElement("strong");
1286
1433
  strong.textContent = match[2];
1434
+ strong.style.cssText = `
1435
+ font-weight: 600;
1436
+ color: ${aeroTheme.colors.text.primary};
1437
+ `;
1287
1438
  parts.push(strong);
1288
1439
  }
1289
1440
  else if (match[3]) {
1290
- // Italic text (*)
1291
- const em = document.createElement('em');
1441
+ // Italic text (*) - matches React UserBubble styling
1442
+ const em = document.createElement("em");
1292
1443
  em.textContent = match[3];
1444
+ em.style.cssText = `
1445
+ font-style: italic;
1446
+ `;
1293
1447
  parts.push(em);
1294
1448
  }
1295
1449
  else if (match[4]) {
1296
- // Italic text (_)
1297
- const em = document.createElement('em');
1450
+ // Italic text (_) - matches React UserBubble styling
1451
+ const em = document.createElement("em");
1298
1452
  em.textContent = match[4];
1453
+ em.style.cssText = `
1454
+ font-style: italic;
1455
+ `;
1299
1456
  parts.push(em);
1300
1457
  }
1301
1458
  lastIndex = match.index + match[0].length;
@@ -1312,19 +1469,19 @@ export function createConvaiWidget(container, options) {
1312
1469
  const processedLine = processLine(line);
1313
1470
  // Handle empty lines (creates paragraph spacing)
1314
1471
  if (line.trim() === "" && lineIndex > 0) {
1315
- container.appendChild(document.createElement('br'));
1472
+ container.appendChild(document.createElement("br"));
1316
1473
  return;
1317
1474
  }
1318
1475
  // Append the processed line
1319
1476
  if (processedLine.length > 0) {
1320
- processedLine.forEach(part => container.appendChild(part));
1477
+ processedLine.forEach((part) => container.appendChild(part));
1321
1478
  }
1322
1479
  else {
1323
1480
  container.appendChild(document.createTextNode(line));
1324
1481
  }
1325
1482
  // Add line break between lines
1326
1483
  if (lineIndex < lines.length - 1) {
1327
- container.appendChild(document.createElement('br'));
1484
+ container.appendChild(document.createElement("br"));
1328
1485
  }
1329
1486
  });
1330
1487
  };
@@ -1332,9 +1489,9 @@ export function createConvaiWidget(container, options) {
1332
1489
  if (!messageListElement)
1333
1490
  return;
1334
1491
  const messages = formatMessages();
1335
- messageListElement.innerHTML = '';
1492
+ messageListElement.innerHTML = "";
1336
1493
  if (messages.length === 0) {
1337
- const emptyState = document.createElement('div');
1494
+ const emptyState = document.createElement("div");
1338
1495
  emptyState.style.cssText = `
1339
1496
  display: flex;
1340
1497
  flex-direction: column;
@@ -1353,79 +1510,107 @@ export function createConvaiWidget(container, options) {
1353
1510
  return;
1354
1511
  }
1355
1512
  messages.forEach((msg) => {
1356
- const messageWrapper = document.createElement('div');
1513
+ // MessageWrapper - matches React UserMessageContainer
1514
+ const messageWrapper = document.createElement("div");
1357
1515
  messageWrapper.style.cssText = `
1358
1516
  display: flex;
1359
1517
  flex-direction: column;
1360
- align-items: ${msg.isUser ? 'flex-end' : 'flex-start'};
1361
- gap: 4px;
1518
+ max-width: ${msg.isUser ? "70%" : "70%"};
1519
+ margin-left: ${msg.isUser ? "auto" : "0"};
1362
1520
  width: 100%;
1521
+ isolation: isolate;
1522
+ contain: layout style paint;
1363
1523
  `;
1364
- const bubble = document.createElement('div');
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)";
1365
1532
  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};
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;
1371
1543
  line-height: 1.5;
1372
1544
  word-wrap: break-word;
1373
- box-shadow: ${aeroTheme.shadows.sm};
1374
- border-bottom-${msg.isUser ? 'right' : 'left'}-radius: 4px;
1375
- max-width: 85%;
1545
+ max-width: 100%;
1376
1546
  display: flex;
1377
1547
  flex-direction: column;
1378
- gap: 8px;
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;
1379
1555
  `;
1380
- // Header (Logo/Icon + Name) inside bubble
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
1381
1560
  if (msg.sender) {
1382
- const header = document.createElement('div');
1561
+ const header = document.createElement("div");
1383
1562
  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');
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");
1391
1572
  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
- `;
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
+ `;
1399
1581
  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';
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";
1404
1586
  userIcon.style.color = aeroTheme.colors.convai.dark;
1405
1587
  logoContainer.appendChild(userIcon);
1406
1588
  }
1407
1589
  else {
1408
1590
  // 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';
1591
+ const botLogo = Icons.ConvaiLogo("sm", "connected");
1592
+ botLogo.style.width = "16px";
1593
+ botLogo.style.height = "16px";
1594
+ botLogo.style.color = "#10b981";
1413
1595
  logoContainer.appendChild(botLogo);
1414
1596
  }
1415
- const nameLabel = document.createElement('span');
1597
+ // Name Label - matches React UserLabel exactly
1598
+ const nameLabel = document.createElement("span");
1416
1599
  nameLabel.textContent = msg.sender === "User" ? "You" : msg.sender;
1417
1600
  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
- `;
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
+ `;
1423
1608
  header.appendChild(logoContainer);
1424
1609
  header.appendChild(nameLabel);
1425
1610
  bubble.appendChild(header);
1426
1611
  }
1427
1612
  // Message Content
1428
- const contentDiv = document.createElement('div');
1613
+ const contentDiv = document.createElement("div");
1429
1614
  contentDiv.style.cssText = `
1430
1615
  white-space: pre-wrap;
1431
1616
  word-wrap: break-word;
@@ -1433,17 +1618,19 @@ export function createConvaiWidget(container, options) {
1433
1618
  renderMarkdown(msg.text, contentDiv);
1434
1619
  bubble.appendChild(contentDiv);
1435
1620
  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);
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
+ }
1447
1634
  messageListElement.appendChild(messageWrapper);
1448
1635
  });
1449
1636
  // Scroll to bottom
@@ -1479,9 +1666,17 @@ export function createConvaiWidget(container, options) {
1479
1666
  };
1480
1667
  // Setup event listeners for client state changes
1481
1668
  const setupClientListeners = () => {
1482
- convaiClient.on('stateChange', () => {
1669
+ convaiClient.on("stateChange", () => {
1483
1670
  updateHeader();
1484
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
+ }
1485
1680
  // Update speaking animation state
1486
1681
  if (convaiClient.state.isSpeaking) {
1487
1682
  if (!startTime) {
@@ -1496,11 +1691,6 @@ export function createConvaiWidget(container, options) {
1496
1691
  targetLevels = Array(40).fill(0.05);
1497
1692
  }
1498
1693
  }
1499
- // Sync video visibility with controls state
1500
- if (isVideoVisible !== convaiClient.videoControls.isVideoEnabled) {
1501
- isVideoVisible = convaiClient.videoControls.isVideoEnabled;
1502
- updateVoiceMode();
1503
- }
1504
1694
  // Auto-collapse when disconnected
1505
1695
  if (!convaiClient.state.isConnected && !convaiClient.state.isConnecting) {
1506
1696
  if (isOpen) {
@@ -1508,14 +1698,79 @@ export function createConvaiWidget(container, options) {
1508
1698
  }
1509
1699
  isMuted = false;
1510
1700
  isVoiceMode = false;
1701
+ isVideoVisible = false;
1511
1702
  updateHeader();
1512
1703
  updateVoiceMode();
1513
1704
  }
1514
1705
  });
1515
- convaiClient.on('messagesChange', () => {
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", () => {
1516
1767
  updateMessageList();
1517
1768
  });
1518
- convaiClient.on('connect', () => {
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", () => {
1519
1774
  // Initialize audio renderer on connection
1520
1775
  if (convaiClient.room && !audioRenderer) {
1521
1776
  audioRenderer = new AudioRenderer(convaiClient.room);
@@ -1523,7 +1778,7 @@ export function createConvaiWidget(container, options) {
1523
1778
  // Fetch character info
1524
1779
  fetchCharacterInfo();
1525
1780
  });
1526
- convaiClient.on('disconnect', () => {
1781
+ convaiClient.on("disconnect", () => {
1527
1782
  // Cleanup audio renderer
1528
1783
  if (audioRenderer) {
1529
1784
  audioRenderer.destroy();
@@ -1535,6 +1790,8 @@ export function createConvaiWidget(container, options) {
1535
1790
  // Initialize
1536
1791
  createDOM();
1537
1792
  setupClientListeners();
1793
+ // Set initial button state
1794
+ updateSendButton(); // Show voice mode button initially since input is empty
1538
1795
  // If already connected, initialize audio renderer and fetch character info
1539
1796
  if (convaiClient.state.isConnected && convaiClient.room) {
1540
1797
  audioRenderer = new AudioRenderer(convaiClient.room);
@@ -1550,7 +1807,7 @@ export function createConvaiWidget(container, options) {
1550
1807
  audioRenderer = null;
1551
1808
  }
1552
1809
  // Remove event listeners
1553
- morphingContainer.removeEventListener('click', handleToggle);
1810
+ morphingContainer.removeEventListener("click", handleToggle);
1554
1811
  // Remove DOM elements
1555
1812
  if (rootElement.parentElement) {
1556
1813
  rootElement.remove();
@@ -1559,11 +1816,12 @@ export function createConvaiWidget(container, options) {
1559
1816
  floatingVideo.remove();
1560
1817
  }
1561
1818
  // Unsubscribe from client events
1562
- convaiClient.off('stateChange', () => { });
1563
- convaiClient.off('messagesChange', () => { });
1564
- convaiClient.off('connect', () => { });
1565
- convaiClient.off('disconnect', () => { });
1566
- }
1819
+ convaiClient.off("stateChange", () => { });
1820
+ convaiClient.off("messagesChange", () => { });
1821
+ convaiClient.off("botReady", () => { });
1822
+ convaiClient.off("connect", () => { });
1823
+ convaiClient.off("disconnect", () => { });
1824
+ },
1567
1825
  };
1568
1826
  return widget;
1569
1827
  }