@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
|
-
|
|
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
|
-
|
|
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(--
|
|
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(--
|
|
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:
|
|
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; /*
|
|
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; /*
|
|
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
|
*/
|