@myned-ai/avatar-chat-widget 0.5.0 → 0.7.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.
@@ -41,6 +41,7 @@ declare class AvatarChatElement extends HTMLElement {
41
41
  private _isMounted;
42
42
  private _isConnected;
43
43
  private _isCollapsed;
44
+ private visualViewportHandler;
44
45
  constructor();
45
46
  /**
46
47
  * Configure the widget (call before mount)
@@ -75,10 +76,23 @@ declare class AvatarChatElement extends HTMLElement {
75
76
  * Setup UI event listeners
76
77
  */
77
78
  private setupUIEvents;
79
+ /**
80
+ * Handle mobile keyboard appearance using VisualViewport API
81
+ * Resizes the avatar to fit when keyboard opens
82
+ */
83
+ private setupMobileKeyboardHandling;
78
84
  /**
79
85
  * Escape HTML to prevent XSS in user-provided suggestions
80
86
  */
81
87
  private escapeHtml;
88
+ /**
89
+ * Generate CSS overrides for primaryColor and secondaryColor config options
90
+ */
91
+ private generateColorOverrides;
92
+ /**
93
+ * Darken a hex color by a percentage
94
+ */
95
+ private darkenColor;
82
96
  /**
83
97
  * Mark that conversation has messages (shows chat area instead of suggestions)
84
98
  */
@@ -1180,6 +1180,10 @@ class AudioInput {
1180
1180
  // 100ms at 24kHz
1181
1181
  async requestPermission() {
1182
1182
  try {
1183
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
1184
+ log$e.error("MediaDevices API not available. Microphone requires HTTPS.");
1185
+ throw new Error("Microphone requires a secure connection (HTTPS)");
1186
+ }
1183
1187
  const stream = await navigator.mediaDevices.getUserMedia({
1184
1188
  audio: {
1185
1189
  sampleRate: CONFIG.audio.input.sampleRate,
@@ -1225,6 +1229,9 @@ class AudioInput {
1225
1229
  */
1226
1230
  async startPCM16Recording(onData) {
1227
1231
  try {
1232
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
1233
+ throw new Error("Microphone requires a secure connection (HTTPS)");
1234
+ }
1228
1235
  this.mediaStream = await navigator.mediaDevices.getUserMedia({
1229
1236
  audio: {
1230
1237
  channelCount: 1,
@@ -3551,10 +3558,14 @@ class VoiceInputController {
3551
3558
  * Start voice recording
3552
3559
  */
3553
3560
  async start() {
3561
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
3562
+ const message = "Voice input requires a secure connection (HTTPS). Please use text input instead.";
3563
+ log$5.warn(message);
3564
+ alert(message);
3565
+ return;
3566
+ }
3554
3567
  try {
3555
3568
  log$5.info("Starting recording (PCM16 24kHz)");
3556
- this.protocolClient.sendAudioStreamStart();
3557
- log$5.info("Sent audio_stream_start to server");
3558
3569
  let chunkCount = 0;
3559
3570
  await this.audioInput.startRecording((audioData) => {
3560
3571
  chunkCount++;
@@ -3563,13 +3574,23 @@ class VoiceInputController {
3563
3574
  }
3564
3575
  this.protocolClient.sendAudioData(audioData);
3565
3576
  }, "pcm16");
3566
- log$5.info("Recording started successfully");
3577
+ this.protocolClient.sendAudioStreamStart();
3578
+ log$5.info("Recording started successfully, sent audio_stream_start to server");
3567
3579
  this.setRecordingState(true);
3568
3580
  } catch (error2) {
3569
3581
  log$5.error("Failed to start recording:", error2);
3570
3582
  errorBoundary.handleError(error2, "audio-input");
3571
3583
  this.options.onError?.(error2);
3572
- alert("Microphone access denied. Please enable microphone permissions.");
3584
+ const errorName = error2.name || "";
3585
+ const errorMsg = error2.message || "";
3586
+ const errorStr = `${errorName} ${errorMsg}`.toLowerCase();
3587
+ if (errorStr.includes("policy") || errorStr.includes("not allowed in this document")) {
3588
+ alert("Microphone is blocked by this website's security settings. The site owner needs to enable microphone access.");
3589
+ } else if (errorName === "NotAllowedError" || errorStr.includes("permission denied") || errorStr.includes("denied")) {
3590
+ alert("Microphone access was denied. Please allow microphone access in your browser settings to use voice input.");
3591
+ } else {
3592
+ alert("Could not access microphone. Please check your browser settings and try again.");
3593
+ }
3573
3594
  }
3574
3595
  }
3575
3596
  /**
@@ -3696,7 +3717,6 @@ class ChatManager {
3696
3717
  await this.protocolClient.connect();
3697
3718
  log$4.info("WebSocket connected");
3698
3719
  this.avatar.setChatState("Idle");
3699
- await this.audioInput.requestPermission();
3700
3720
  } catch (error2) {
3701
3721
  errorBoundary.handleError(error2, "chat-manager");
3702
3722
  log$4.error("Connection failed");
@@ -3788,6 +3808,9 @@ class ChatManager {
3788
3808
  this.useSyncPlayback = false;
3789
3809
  this.subtitleController.showRemaining();
3790
3810
  this.transcriptManager.finalizeAssistantTurn();
3811
+ setTimeout(() => {
3812
+ this.subtitleController.clear();
3813
+ }, 1500);
3791
3814
  });
3792
3815
  }
3793
3816
  setupAutoScroll() {
@@ -4231,6 +4254,7 @@ const WIDGET_STYLES = `
4231
4254
  box-sizing: border-box;
4232
4255
  --primary-color: #4B4ACF;
4233
4256
  --primary-gradient: linear-gradient(135deg, #4B4ACF 0%, #2E3A87 100%);
4257
+ --secondary-color: #1F2937;
4234
4258
  --bg-color: #ffffff;
4235
4259
  --text-color: #1F2937;
4236
4260
  --input-bg: #f5f5f7;
@@ -4286,8 +4310,10 @@ const WIDGET_STYLES = `
4286
4310
  @media (max-width: 480px) {
4287
4311
  .widget-root {
4288
4312
  width: 100vw;
4289
- height: 100vh;
4313
+ height: 100vh; /* Fallback for older browsers */
4314
+ height: 100dvh; /* Dynamic viewport height - accounts for mobile browser UI */
4290
4315
  max-height: 100vh;
4316
+ max-height: 100dvh;
4291
4317
  border-radius: 0;
4292
4318
  /* Let the mobile media query at the bottom handle the rest */
4293
4319
  padding-bottom: 90px; /* Ensure input layer space is preserved */
@@ -4394,7 +4420,7 @@ const WIDGET_STYLES = `
4394
4420
  margin: 0;
4395
4421
  font-size: 16px;
4396
4422
  font-weight: 700;
4397
- color: var(--text-color);
4423
+ color: var(--secondary-color);
4398
4424
  letter-spacing: -0.01em;
4399
4425
  }
4400
4426
 
@@ -4447,7 +4473,7 @@ const WIDGET_STYLES = `
4447
4473
  .control-btn {
4448
4474
  background: transparent;
4449
4475
  border: none;
4450
- color: var(--text-color);
4476
+ color: var(--secondary-color);
4451
4477
  width: 32px;
4452
4478
  height: 32px;
4453
4479
  border-radius: 50%;
@@ -5213,7 +5239,7 @@ const WIDGET_STYLES = `
5213
5239
  display: flex;
5214
5240
  align-items: center;
5215
5241
  justify-content: center;
5216
- background: linear-gradient(135deg, #4B4ACF 0%, #2E3A87 100%);
5242
+ background: var(--primary-gradient);
5217
5243
  color: white;
5218
5244
  }
5219
5245
 
@@ -5336,7 +5362,8 @@ const WIDGET_STYLES = `
5336
5362
 
5337
5363
  /* Avatar-focus mode on mobile: avatar takes most of the space */
5338
5364
  :host(:not(.collapsed)) [data-drawer-state="avatar-focus"] {
5339
- --avatar-height: calc(100vh - 56px - 90px) !important; /* Full height minus header and input */
5365
+ --avatar-height: calc(100vh - 56px - 90px) !important; /* Fallback */
5366
+ --avatar-height: calc(100dvh - 56px - 90px) !important; /* Full height minus header and input */
5340
5367
  background: transparent !important; /* Let avatar stage show through */
5341
5368
  }
5342
5369
 
@@ -5381,11 +5408,13 @@ const WIDGET_STYLES = `
5381
5408
 
5382
5409
  /* Text-focus mode on mobile: chat takes most of the space, avatar in corner */
5383
5410
  :host(:not(.collapsed)) [data-drawer-state="text-focus"] {
5384
- --chat-height: calc(100vh - 70px - 90px) !important; /* Full height minus header and input */
5411
+ --chat-height: calc(100vh - 70px - 90px) !important; /* Fallback */
5412
+ --chat-height: calc(100dvh - 70px - 90px) !important; /* Full height minus header and input */
5385
5413
  }
5386
5414
 
5387
5415
  :host(:not(.collapsed)) [data-drawer-state="text-focus"] .chat-section {
5388
- height: calc(100vh - 70px - 90px) !important;
5416
+ height: calc(100vh - 70px - 90px) !important; /* Fallback */
5417
+ height: calc(100dvh - 70px - 90px) !important;
5389
5418
  flex: 1;
5390
5419
  }
5391
5420
 
@@ -5480,6 +5509,47 @@ const WIDGET_STYLES = `
5480
5509
  white-space: nowrap;
5481
5510
  border-width: 0;
5482
5511
  }
5512
+
5513
+ /* ==========================================================================
5514
+ Mobile Keyboard Visible State
5515
+ Resize avatar when virtual keyboard appears on mobile
5516
+ ========================================================================== */
5517
+ @media (max-width: 480px) {
5518
+ /* When keyboard is visible, shrink the avatar section */
5519
+ .widget-root.keyboard-visible {
5520
+ --avatar-height: 180px;
5521
+ }
5522
+
5523
+ /* Scale down the avatar render and push it down */
5524
+ .widget-root.keyboard-visible .avatar-render-container {
5525
+ transform: translate(-50%, -35%) scale(0.65) !important;
5526
+ transition: transform 0.3s ease;
5527
+ }
5528
+
5529
+ /* Show subtitles but position them properly */
5530
+ .widget-root.keyboard-visible .avatar-subtitles {
5531
+ display: block !important;
5532
+ bottom: 100px !important;
5533
+ font-size: 14px;
5534
+ }
5535
+
5536
+ /* Hide mist to save space */
5537
+ .widget-root.keyboard-visible .avatar-mist-overlay {
5538
+ display: none !important;
5539
+ }
5540
+
5541
+ /* Move suggestions closer to input */
5542
+ .widget-root.keyboard-visible .avatar-suggestions {
5543
+ bottom: 95px !important;
5544
+ }
5545
+
5546
+ /* In text-focus mode with keyboard, ensure chat doesn't overflow */
5547
+ .widget-root.keyboard-visible[data-drawer-state="text-focus"] .chat-section {
5548
+ flex: 1 1 auto !important;
5549
+ min-height: 0 !important;
5550
+ height: auto !important;
5551
+ }
5552
+ }
5483
5553
  `;
5484
5554
  const LAYOUT = {
5485
5555
  /** Header bar height - contains title, status, minimize button */
@@ -5772,6 +5842,7 @@ class AvatarChatElement extends HTMLElement {
5772
5842
  this._isMounted = false;
5773
5843
  this._isConnected = false;
5774
5844
  this._isCollapsed = false;
5845
+ this.visualViewportHandler = null;
5775
5846
  this.shadow = this.attachShadow({ mode: "open" });
5776
5847
  }
5777
5848
  /**
@@ -5800,8 +5871,9 @@ class AvatarChatElement extends HTMLElement {
5800
5871
  return;
5801
5872
  }
5802
5873
  log$2.info("Mounting widget");
5874
+ const colorOverrides = this.generateColorOverrides();
5803
5875
  const styleEl = document.createElement("style");
5804
- styleEl.textContent = WIDGET_STYLES + (this.config.customStyles || "");
5876
+ styleEl.textContent = WIDGET_STYLES + colorOverrides + (this.config.customStyles || "");
5805
5877
  this.shadow.appendChild(styleEl);
5806
5878
  if (this.config.position && this.config.position !== "inline") {
5807
5879
  this.classList.add(`position-${this.config.position}`);
@@ -5835,6 +5907,7 @@ class AvatarChatElement extends HTMLElement {
5835
5907
  this.shadow.appendChild(root);
5836
5908
  this.initializeDrawer();
5837
5909
  this.setupUIEvents();
5910
+ this.setupMobileKeyboardHandling();
5838
5911
  if (!this.config.enableVoice) {
5839
5912
  const voiceBtn = this.shadow.getElementById("micBtn");
5840
5913
  if (voiceBtn) voiceBtn.style.display = "none";
@@ -6069,6 +6142,34 @@ class AvatarChatElement extends HTMLElement {
6069
6142
  });
6070
6143
  }
6071
6144
  }
6145
+ /**
6146
+ * Handle mobile keyboard appearance using VisualViewport API
6147
+ * Resizes the avatar to fit when keyboard opens
6148
+ */
6149
+ setupMobileKeyboardHandling() {
6150
+ if (!window.visualViewport) {
6151
+ return;
6152
+ }
6153
+ const widgetRoot = this.shadow.querySelector(".widget-root");
6154
+ if (!widgetRoot) {
6155
+ return;
6156
+ }
6157
+ let initialViewportHeight = window.visualViewport.height;
6158
+ const handleViewportChange = () => {
6159
+ const viewport = window.visualViewport;
6160
+ const currentHeight = viewport.height;
6161
+ const keyboardHeight = initialViewportHeight - currentHeight;
6162
+ if (keyboardHeight > 150) {
6163
+ widgetRoot.classList.add("keyboard-visible");
6164
+ } else {
6165
+ widgetRoot.classList.remove("keyboard-visible");
6166
+ initialViewportHeight = currentHeight;
6167
+ }
6168
+ };
6169
+ window.visualViewport.addEventListener("resize", handleViewportChange);
6170
+ window.visualViewport.addEventListener("scroll", handleViewportChange);
6171
+ this.visualViewportHandler = handleViewportChange;
6172
+ }
6072
6173
  /**
6073
6174
  * Escape HTML to prevent XSS in user-provided suggestions
6074
6175
  */
@@ -6077,6 +6178,36 @@ class AvatarChatElement extends HTMLElement {
6077
6178
  div.textContent = text;
6078
6179
  return div.innerHTML;
6079
6180
  }
6181
+ /**
6182
+ * Generate CSS overrides for primaryColor and secondaryColor config options
6183
+ */
6184
+ generateColorOverrides() {
6185
+ const overrides = [];
6186
+ if (this.config.primaryColor) {
6187
+ const primary = this.config.primaryColor;
6188
+ const darkerShade = this.darkenColor(primary, 0.2);
6189
+ overrides.push(`--primary-color: ${primary};`);
6190
+ overrides.push(`--primary-gradient: linear-gradient(135deg, ${primary} 0%, ${darkerShade} 100%);`);
6191
+ }
6192
+ if (this.config.secondaryColor) {
6193
+ overrides.push(`--secondary-color: ${this.config.secondaryColor};`);
6194
+ }
6195
+ if (overrides.length === 0) return "";
6196
+ return `:host { ${overrides.join(" ")} }`;
6197
+ }
6198
+ /**
6199
+ * Darken a hex color by a percentage
6200
+ */
6201
+ darkenColor(hex, percent) {
6202
+ hex = hex.replace(/^#/, "");
6203
+ let r = parseInt(hex.substring(0, 2), 16);
6204
+ let g = parseInt(hex.substring(2, 4), 16);
6205
+ let b = parseInt(hex.substring(4, 6), 16);
6206
+ r = Math.max(0, Math.floor(r * (1 - percent)));
6207
+ g = Math.max(0, Math.floor(g * (1 - percent)));
6208
+ b = Math.max(0, Math.floor(b * (1 - percent)));
6209
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
6210
+ }
6080
6211
  /**
6081
6212
  * Mark that conversation has messages (shows chat area instead of suggestions)
6082
6213
  */