@myned-ai/avatar-chat-widget 0.2.0 → 0.3.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.
@@ -105,13 +105,13 @@ function deepMerge(target, source) {
105
105
  function setConfig(config) {
106
106
  runtimeConfig = deepMerge(runtimeConfig, config);
107
107
  }
108
- const CONFIG = new Proxy(runtimeConfig, {
109
- get(target, prop) {
110
- if (typeof prop === "string" && prop in target) {
111
- return target[prop];
108
+ const CONFIG = new Proxy({}, {
109
+ get(_target2, prop) {
110
+ if (typeof prop === "string" && prop in runtimeConfig) {
111
+ return runtimeConfig[prop];
112
112
  }
113
113
  if (typeof prop === "symbol") {
114
- return target[prop];
114
+ return runtimeConfig[prop];
115
115
  }
116
116
  throw new Error(`Invalid config property: ${String(prop)}`);
117
117
  },
@@ -129,17 +129,7 @@ const LogLevel = {
129
129
  Info: 3,
130
130
  Debug: 4
131
131
  };
132
- const isDevelopment = () => {
133
- try {
134
- const meta = import.meta;
135
- if (meta?.env) {
136
- return meta.env.DEV === true || meta.env.MODE === "development";
137
- }
138
- } catch {
139
- }
140
- return false;
141
- };
142
- const DEFAULT_LEVEL = isDevelopment() ? LogLevel.Debug : LogLevel.Info;
132
+ const DEFAULT_LEVEL = LogLevel.Debug;
143
133
  class Logger {
144
134
  constructor() {
145
135
  this.level = DEFAULT_LEVEL;
@@ -840,9 +830,8 @@ class AuthService {
840
830
  }
841
831
  const log$c = logger.scope("SocketService");
842
832
  class SocketService extends EventEmitter {
843
- constructor(url = CONFIG.websocket.url) {
833
+ constructor(url) {
844
834
  super();
845
- this.url = url;
846
835
  this.ws = null;
847
836
  this.connectionState = "disconnected";
848
837
  this.reconnectAttempts = 0;
@@ -853,6 +842,7 @@ class SocketService extends EventEmitter {
853
842
  this.maxReconnectAttempts = 10;
854
843
  this.isIntentionallyClosed = false;
855
844
  this.useBinaryProtocol = false;
845
+ this.url = url ?? CONFIG.websocket.url;
856
846
  this.authService = new AuthService();
857
847
  errorBoundary.registerHandler("websocket", (error2) => {
858
848
  this.emit("error", error2);
@@ -932,6 +922,16 @@ class SocketService extends EventEmitter {
932
922
  message = JSON.parse(event.data);
933
923
  }
934
924
  this.emit("message", message);
925
+ try {
926
+ const meta = { type: message.type };
927
+ if (message.frameIndex !== void 0) meta.frameIndex = message.frameIndex;
928
+ if (message.timestamp !== void 0) meta.timestamp = message.timestamp;
929
+ if (message.itemId !== void 0) meta.itemId = message.itemId;
930
+ if (event.data instanceof ArrayBuffer) meta.byteLength = event.data.byteLength;
931
+ log$c.debug("Incoming message", meta);
932
+ } catch (e) {
933
+ log$c.warn("Failed to log incoming message meta", e);
934
+ }
935
935
  this.emit(message.type, message);
936
936
  } catch (error2) {
937
937
  errorBoundary.handleError(error2, "websocket");
@@ -1942,7 +1942,13 @@ class AudioOutput {
1942
1942
  }
1943
1943
  const chunk = this.audioBuffer.pop();
1944
1944
  if (!chunk) {
1945
- log$8.warn("Audio buffer underrun");
1945
+ log$8.warn("Audio buffer underrun", { bufferSize: this.audioBuffer.size });
1946
+ try {
1947
+ const stats = this.getBufferStats();
1948
+ log$8.debug("Adaptive buffer stats on underrun:", stats);
1949
+ } catch (e) {
1950
+ log$8.warn("Failed to get buffer stats on underrun", e);
1951
+ }
1946
1952
  this.adaptiveBuffer.recordUnderrun();
1947
1953
  this.isPlaying = false;
1948
1954
  return;
@@ -1997,7 +2003,7 @@ class AudioOutput {
1997
2003
  this.isPlaying = false;
1998
2004
  for (const source of this.activeSourceNodes) {
1999
2005
  try {
2000
- source.stop();
2006
+ source.stop(0);
2001
2007
  source.disconnect();
2002
2008
  } catch {
2003
2009
  }
@@ -2362,6 +2368,7 @@ class SyncPlayback {
2362
2368
  this.audioStartTime = 0;
2363
2369
  this.activeSourceNodes = /* @__PURE__ */ new Set();
2364
2370
  this.currentFrameIndex = 0;
2371
+ this.lastReceivedFrameIndex = null;
2365
2372
  this.lastBlendshapeUpdate = 0;
2366
2373
  this.onBlendshapeUpdate = null;
2367
2374
  this.onPlaybackEnd = null;
@@ -2416,6 +2423,14 @@ class SyncPlayback {
2416
2423
  if (sampleRate) {
2417
2424
  this.sampleRate = sampleRate;
2418
2425
  }
2426
+ AudioContextManager.resume().catch(() => {
2427
+ });
2428
+ try {
2429
+ const ctx = this.audioContext;
2430
+ log$5.debug("Server sampleRate:", this.sampleRate, "AudioContext sampleRate:", ctx.sampleRate);
2431
+ } catch (e) {
2432
+ log$5.warn("Unable to read AudioContext sampleRate for diagnostics", e);
2433
+ }
2419
2434
  this.frameBuffer = [];
2420
2435
  this.scheduledFrames = [];
2421
2436
  this.currentFrameIndex = 0;
@@ -2426,7 +2441,7 @@ class SyncPlayback {
2426
2441
  this.currentWeights = { ...this.neutralWeights };
2427
2442
  for (const source of this.activeSourceNodes) {
2428
2443
  try {
2429
- source.stop();
2444
+ source.stop(0);
2430
2445
  source.disconnect();
2431
2446
  } catch {
2432
2447
  }
@@ -2440,6 +2455,15 @@ class SyncPlayback {
2440
2455
  if (this.isStopped) {
2441
2456
  return;
2442
2457
  }
2458
+ if (typeof frame.frameIndex === "number") {
2459
+ if (this.lastReceivedFrameIndex !== null) {
2460
+ const expected = this.lastReceivedFrameIndex + 1;
2461
+ if (frame.frameIndex !== expected) {
2462
+ log$5.warn("SyncPlayback frame index gap/detect:", { last: this.lastReceivedFrameIndex, got: frame.frameIndex, expected });
2463
+ }
2464
+ }
2465
+ this.lastReceivedFrameIndex = frame.frameIndex;
2466
+ }
2443
2467
  this.frameBuffer.push(frame);
2444
2468
  if (frame.frameIndex === 0) {
2445
2469
  log$5.debug("First sync frame added:", frame.audio.byteLength, "bytes audio");
@@ -2620,12 +2644,14 @@ class SyncPlayback {
2620
2644
  }
2621
2645
  for (const source of this.activeSourceNodes) {
2622
2646
  try {
2623
- source.stop();
2647
+ source.stop(0);
2624
2648
  source.disconnect();
2625
2649
  } catch {
2626
2650
  }
2627
2651
  }
2628
2652
  this.activeSourceNodes.clear();
2653
+ AudioContextManager.suspend().catch(() => {
2654
+ });
2629
2655
  this.currentWeights = { ...this.neutralWeights };
2630
2656
  if (this.onBlendshapeUpdate) {
2631
2657
  this.onBlendshapeUpdate(this.neutralWeights);
@@ -2747,10 +2773,16 @@ class ChatManager {
2747
2773
  constructor(avatar, options = {}) {
2748
2774
  this.currentSessionId = null;
2749
2775
  this.messages = [];
2776
+ this.typingIndicator = null;
2750
2777
  this.isRecording = false;
2751
2778
  this.animationFrameId = null;
2752
2779
  this.useSyncPlayback = false;
2753
- this.currentStreamingMessage = null;
2780
+ this.streamingByItem = /* @__PURE__ */ new Map();
2781
+ this.latestItemForRole = {};
2782
+ this.bufferedDeltas = /* @__PURE__ */ new Map();
2783
+ this.bufferTimeouts = /* @__PURE__ */ new Map();
2784
+ this.BUFFER_WAIT_MS = 200;
2785
+ this.SHOW_ASSISTANT_STREAMING = false;
2754
2786
  this.avatar = avatar;
2755
2787
  this.options = options;
2756
2788
  this.userId = this.generateUserId();
@@ -2772,10 +2804,21 @@ class ChatManager {
2772
2804
  this.chatInput = options.chatInput || root.getElementById("chatInput");
2773
2805
  this.sendBtn = options.sendBtn || root.getElementById("sendBtn");
2774
2806
  this.micBtn = options.micBtn || root.getElementById("micBtn");
2807
+ this.typingIndicator = root.getElementById("typingIndicator");
2775
2808
  this.setupEventListeners();
2776
2809
  this.setupWebSocketHandlers();
2777
2810
  this.startBlendshapeSync();
2778
2811
  }
2812
+ setTyping(typing) {
2813
+ if (this.typingIndicator) {
2814
+ if (typing) {
2815
+ this.typingIndicator.classList.remove("hidden");
2816
+ this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
2817
+ } else {
2818
+ this.typingIndicator.classList.add("hidden");
2819
+ }
2820
+ }
2821
+ }
2779
2822
  async initialize() {
2780
2823
  FeatureDetection.logCapabilities();
2781
2824
  try {
@@ -2823,6 +2866,7 @@ class ChatManager {
2823
2866
  });
2824
2867
  this.socketService.on("text", (message) => {
2825
2868
  if (message.type === "text") {
2869
+ this.setTyping(false);
2826
2870
  this.addMessage(message.data, "assistant");
2827
2871
  }
2828
2872
  });
@@ -2840,6 +2884,7 @@ class ChatManager {
2840
2884
  });
2841
2885
  this.socketService.on("audio_start", (message) => {
2842
2886
  if (message.type === "audio_start") {
2887
+ this.setTyping(false);
2843
2888
  log$3.info("Audio start received:", message.sessionId);
2844
2889
  this.currentSessionId = message.sessionId;
2845
2890
  this.syncPlayback.startSession(message.sessionId, message.sampleRate);
@@ -2852,6 +2897,10 @@ class ChatManager {
2852
2897
  });
2853
2898
  this.socketService.on("audio_chunk", (message) => {
2854
2899
  if (message.type === "audio_chunk") {
2900
+ if (message.sessionId && this.currentSessionId && message.sessionId !== this.currentSessionId) {
2901
+ log$3.debug("Ignoring audio_chunk from stale session:", message.sessionId);
2902
+ return;
2903
+ }
2855
2904
  let audioData;
2856
2905
  if (message.data instanceof ArrayBuffer) {
2857
2906
  audioData = message.data;
@@ -2872,6 +2921,10 @@ class ChatManager {
2872
2921
  });
2873
2922
  this.socketService.on("sync_frame", (message) => {
2874
2923
  if (message.type === "sync_frame") {
2924
+ if (message.sessionId && this.currentSessionId && message.sessionId !== this.currentSessionId) {
2925
+ log$3.debug("Ignoring sync_frame from stale session:", message.sessionId, "current:", this.currentSessionId);
2926
+ return;
2927
+ }
2875
2928
  this.useSyncPlayback = true;
2876
2929
  const audioData = this.decodeBase64ToArrayBuffer(message.audio);
2877
2930
  if (message.frameIndex === 0) {
@@ -2888,11 +2941,18 @@ class ChatManager {
2888
2941
  });
2889
2942
  this.socketService.on("blendshape", (message) => {
2890
2943
  if (message.type === "blendshape") {
2944
+ if (message.sessionId && this.currentSessionId && message.sessionId !== this.currentSessionId) {
2945
+ return;
2946
+ }
2891
2947
  this.blendshapeBuffer.addFrame(message.weights, message.timestamp);
2892
2948
  }
2893
2949
  });
2894
2950
  this.socketService.on("audio_end", (message) => {
2895
2951
  if (message.type === "audio_end") {
2952
+ if (message.sessionId && this.currentSessionId && message.sessionId !== this.currentSessionId) {
2953
+ log$3.debug("Ignoring audio_end from stale session:", message.sessionId);
2954
+ return;
2955
+ }
2896
2956
  log$3.info("Audio end received");
2897
2957
  if (this.useSyncPlayback) {
2898
2958
  this.syncPlayback.endSession(message.sessionId);
@@ -2909,21 +2969,28 @@ class ChatManager {
2909
2969
  this.syncPlayback.stop();
2910
2970
  this.audioOutput.stop();
2911
2971
  this.blendshapeBuffer.clear();
2912
- this.finalizeStreamingMessage();
2972
+ const interruptedItem = message.itemId;
2973
+ this.finalizeStreamingMessage(interruptedItem, "assistant", true);
2913
2974
  this.avatar.disableLiveBlendshapes();
2914
2975
  this.avatar.setChatState("Hello");
2915
2976
  }
2916
2977
  });
2917
2978
  this.socketService.on("transcript_delta", (message) => {
2918
2979
  if (message.type === "transcript_delta" && message.text) {
2980
+ console.log("transcript_delta raw:", { role: message.role, text: message.text });
2919
2981
  const role = message.role === "assistant" ? "assistant" : "user";
2920
- this.streamTranscript(message.text, role);
2982
+ const itemId = message.itemId;
2983
+ const previousItemId = message.previousItemId;
2984
+ this.streamTranscript(message.text, role, itemId, previousItemId);
2921
2985
  }
2922
2986
  });
2923
2987
  this.socketService.on("transcript_done", (message) => {
2924
2988
  if (message.type === "transcript_done") {
2925
2989
  log$3.debug(`Transcript complete [${message.role}]: ${message.text}`);
2926
- this.finalizeStreamingMessage();
2990
+ const role = message.role === "assistant" ? "assistant" : "user";
2991
+ const itemId = message.itemId;
2992
+ const interrupted = !!message.interrupted;
2993
+ this.finalizeStreamingMessage(itemId, role, interrupted);
2927
2994
  }
2928
2995
  });
2929
2996
  this.socketService.on("error", (error2) => {
@@ -2961,6 +3028,7 @@ class ChatManager {
2961
3028
  timestamp: Date.now()
2962
3029
  };
2963
3030
  this.socketService.send(message);
3031
+ this.setTyping(true);
2964
3032
  }
2965
3033
  async toggleVoiceInput() {
2966
3034
  if (!this.isRecording) {
@@ -3014,6 +3082,7 @@ class ChatManager {
3014
3082
  this.isRecording = false;
3015
3083
  this.micBtn.classList.remove("recording");
3016
3084
  this.micBtn.setAttribute("aria-pressed", "false");
3085
+ this.setTyping(true);
3017
3086
  }
3018
3087
  startBlendshapeSync() {
3019
3088
  const sync = () => {
@@ -3047,58 +3116,233 @@ class ChatManager {
3047
3116
  this.options.onMessage?.({ role: sender, text });
3048
3117
  }
3049
3118
  /**
3050
- * Stream transcript text in real-time (word by word)
3119
+ * Item-aware streaming transcript: uses server-provided itemId and previousItemId
3051
3120
  */
3052
- streamTranscript(text, role) {
3053
- if (!this.currentStreamingMessage || this.currentStreamingMessage.role !== role) {
3054
- const messageId = Date.now().toString();
3121
+ streamTranscript(text, role, itemId, previousItemId) {
3122
+ const effectiveId = itemId || `${role}_${Date.now().toString()}`;
3123
+ if (role === "assistant" && !this.SHOW_ASSISTANT_STREAMING) {
3124
+ const buf = this.bufferedDeltas.get(effectiveId) || [];
3125
+ buf.push(text);
3126
+ this.bufferedDeltas.set(effectiveId, buf);
3127
+ if (!this.bufferTimeouts.has(effectiveId)) {
3128
+ const t = window.setTimeout(() => {
3129
+ this.flushBufferedDeltas(effectiveId, role, effectiveId);
3130
+ }, Math.max(this.BUFFER_WAIT_MS, 2e3));
3131
+ this.bufferTimeouts.set(effectiveId, t);
3132
+ }
3133
+ return;
3134
+ }
3135
+ if (previousItemId && !this.streamingByItem.has(previousItemId) && !this.latestItemForRole[role]) {
3136
+ const buf = this.bufferedDeltas.get(effectiveId) || [];
3137
+ buf.push(text);
3138
+ this.bufferedDeltas.set(effectiveId, buf);
3139
+ if (this.bufferTimeouts.has(effectiveId)) {
3140
+ clearTimeout(this.bufferTimeouts.get(effectiveId));
3141
+ }
3142
+ const t = window.setTimeout(() => {
3143
+ this.flushBufferedDeltas(
3144
+ effectiveId,
3145
+ role,
3146
+ effectiveId
3147
+ /* fallback id */
3148
+ );
3149
+ }, this.BUFFER_WAIT_MS);
3150
+ this.bufferTimeouts.set(effectiveId, t);
3151
+ return;
3152
+ }
3153
+ if (this.bufferedDeltas.has(effectiveId)) {
3154
+ const buffered = this.bufferedDeltas.get(effectiveId) || [];
3155
+ for (const part of buffered) {
3156
+ this.appendToStreamingItem(effectiveId, role, part);
3157
+ }
3158
+ this.bufferedDeltas.delete(effectiveId);
3159
+ const to = this.bufferTimeouts.get(effectiveId);
3160
+ if (to) {
3161
+ clearTimeout(to);
3162
+ this.bufferTimeouts.delete(effectiveId);
3163
+ }
3164
+ }
3165
+ if (!this.streamingByItem.has(effectiveId)) {
3055
3166
  const messageEl = document.createElement("div");
3056
3167
  messageEl.className = `message ${role}`;
3057
- messageEl.dataset.id = messageId;
3168
+ messageEl.dataset.id = effectiveId;
3058
3169
  const bubbleEl = document.createElement("div");
3059
3170
  bubbleEl.className = "message-bubble";
3060
3171
  bubbleEl.textContent = text;
3172
+ const footerEl = document.createElement("div");
3173
+ footerEl.className = "message-footer";
3061
3174
  const timeEl = document.createElement("div");
3062
3175
  timeEl.className = "message-time";
3063
- timeEl.textContent = (/* @__PURE__ */ new Date()).toLocaleTimeString([], {
3064
- hour: "2-digit",
3065
- minute: "2-digit"
3066
- });
3176
+ timeEl.textContent = (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
3177
+ footerEl.appendChild(timeEl);
3067
3178
  messageEl.appendChild(bubbleEl);
3068
- messageEl.appendChild(timeEl);
3179
+ messageEl.appendChild(footerEl);
3069
3180
  this.chatMessages.appendChild(messageEl);
3070
- this.currentStreamingMessage = {
3071
- id: messageId,
3072
- element: messageEl,
3073
- role
3074
- };
3181
+ this.streamingByItem.set(effectiveId, { role, element: messageEl });
3182
+ this.latestItemForRole[role] = effectiveId;
3075
3183
  this.scrollToBottom();
3076
3184
  } else {
3077
- const bubbleEl = this.currentStreamingMessage.element.querySelector(".message-bubble");
3078
- if (bubbleEl) {
3079
- bubbleEl.textContent += text;
3080
- this.scrollToBottom();
3081
- }
3185
+ this.appendToStreamingItem(effectiveId, role, text);
3082
3186
  }
3083
3187
  }
3084
- /**
3085
- * Finalize the streaming message and add it to messages array
3086
- */
3087
- finalizeStreamingMessage() {
3088
- if (!this.currentStreamingMessage) return;
3089
- const bubbleEl = this.currentStreamingMessage.element.querySelector(".message-bubble");
3090
- const text = bubbleEl?.textContent || "";
3091
- if (text) {
3092
- const message = {
3093
- id: this.currentStreamingMessage.id,
3188
+ appendToStreamingItem(id, role, text) {
3189
+ const entry = this.streamingByItem.get(id);
3190
+ if (!entry) return;
3191
+ const bubbleEl = entry.element.querySelector(".message-bubble");
3192
+ if (bubbleEl) {
3193
+ bubbleEl.textContent += text;
3194
+ this.scrollToBottom();
3195
+ }
3196
+ }
3197
+ flushBufferedDeltas(bufferKey, role, fallbackId) {
3198
+ const parts = this.bufferedDeltas.get(bufferKey);
3199
+ if (!parts) return;
3200
+ if (role === "assistant" && !this.SHOW_ASSISTANT_STREAMING) {
3201
+ try {
3202
+ const text = parts.join("");
3203
+ const msg = {
3204
+ id: fallbackId,
3205
+ text,
3206
+ sender: "assistant",
3207
+ timestamp: Date.now()
3208
+ };
3209
+ this.messages.push(msg);
3210
+ this.options.onMessage?.({ role: "assistant", text });
3211
+ this.renderMessage(msg);
3212
+ const el = this.chatMessages.lastElementChild;
3213
+ if (el) {
3214
+ el.classList.add("finalized");
3215
+ el.dataset.finalized = "true";
3216
+ }
3217
+ } catch (err) {
3218
+ log$3.error("Failed to finalize buffered assistant parts:", err);
3219
+ }
3220
+ } else {
3221
+ for (const p of parts) {
3222
+ this.streamTranscript(p, role, fallbackId);
3223
+ }
3224
+ }
3225
+ this.bufferedDeltas.delete(bufferKey);
3226
+ const to = this.bufferTimeouts.get(bufferKey);
3227
+ if (to) {
3228
+ clearTimeout(to);
3229
+ this.bufferTimeouts.delete(bufferKey);
3230
+ }
3231
+ }
3232
+ finalizeStreamingMessage(itemId, role, interrupted = false) {
3233
+ if (itemId) {
3234
+ const entry = this.streamingByItem.get(itemId);
3235
+ if (entry) {
3236
+ const bubbleEl = entry.element.querySelector(".message-bubble");
3237
+ const text = bubbleEl?.textContent || "";
3238
+ const msg = {
3239
+ id: itemId,
3240
+ text,
3241
+ sender: entry.role,
3242
+ timestamp: Date.now()
3243
+ };
3244
+ this.messages.push(msg);
3245
+ this.options.onMessage?.({ role: entry.role, text });
3246
+ entry.element.classList.add("finalized");
3247
+ entry.element.dataset.finalized = "true";
3248
+ if (entry.role === "assistant") {
3249
+ this.addFeedbackButtons(entry.element);
3250
+ }
3251
+ this.streamingByItem.delete(itemId);
3252
+ if (this.latestItemForRole[entry.role] === itemId) {
3253
+ delete this.latestItemForRole[entry.role];
3254
+ }
3255
+ return;
3256
+ }
3257
+ const buffered = this.bufferedDeltas.get(itemId);
3258
+ if (buffered && buffered.length) {
3259
+ const text = buffered.join("");
3260
+ const msg = {
3261
+ id: itemId,
3262
+ text,
3263
+ sender: role || "assistant",
3264
+ timestamp: Date.now()
3265
+ };
3266
+ this.messages.push(msg);
3267
+ this.options.onMessage?.({ role: msg.sender, text });
3268
+ this.renderMessage(msg);
3269
+ const el = this.chatMessages.lastElementChild;
3270
+ if (el) {
3271
+ el.classList.add("finalized");
3272
+ el.dataset.finalized = "true";
3273
+ }
3274
+ this.bufferedDeltas.delete(itemId);
3275
+ const to = this.bufferTimeouts.get(itemId);
3276
+ if (to) {
3277
+ clearTimeout(to);
3278
+ this.bufferTimeouts.delete(itemId);
3279
+ }
3280
+ return;
3281
+ }
3282
+ }
3283
+ const rolesToFinalize = role ? [role] : ["user", "assistant"];
3284
+ for (const r of rolesToFinalize) {
3285
+ const latestId = this.latestItemForRole[r];
3286
+ if (!latestId) continue;
3287
+ const entry = this.streamingByItem.get(latestId);
3288
+ if (!entry) continue;
3289
+ const bubbleEl = entry.element.querySelector(".message-bubble");
3290
+ const text = bubbleEl?.textContent || "";
3291
+ const msg = {
3292
+ id: latestId,
3094
3293
  text,
3095
- sender: this.currentStreamingMessage.role,
3294
+ sender: r,
3096
3295
  timestamp: Date.now()
3097
3296
  };
3098
- this.messages.push(message);
3099
- this.options.onMessage?.({ role: this.currentStreamingMessage.role, text });
3297
+ this.messages.push(msg);
3298
+ this.options.onMessage?.({ role: r, text });
3299
+ entry.element.classList.add("finalized");
3300
+ entry.element.dataset.finalized = "true";
3301
+ if (r === "assistant") {
3302
+ this.addFeedbackButtons(entry.element);
3303
+ }
3304
+ this.streamingByItem.delete(latestId);
3305
+ delete this.latestItemForRole[r];
3306
+ }
3307
+ if (role) {
3308
+ let foundKey = null;
3309
+ for (const k of Array.from(this.bufferedDeltas.keys())) {
3310
+ if (k.startsWith(`${role}_`)) {
3311
+ foundKey = k;
3312
+ break;
3313
+ }
3314
+ }
3315
+ if (foundKey) {
3316
+ const parts = this.bufferedDeltas.get(foundKey) || [];
3317
+ const text = parts.join("");
3318
+ const msg = {
3319
+ id: foundKey,
3320
+ text,
3321
+ sender: role,
3322
+ timestamp: Date.now()
3323
+ };
3324
+ this.messages.push(msg);
3325
+ this.options.onMessage?.({ role, text });
3326
+ this.renderMessage(msg);
3327
+ const el = this.chatMessages.lastElementChild;
3328
+ if (el) {
3329
+ el.classList.add("finalized");
3330
+ el.dataset.finalized = "true";
3331
+ }
3332
+ this.bufferedDeltas.delete(foundKey);
3333
+ const to = this.bufferTimeouts.get(foundKey);
3334
+ if (to) {
3335
+ clearTimeout(to);
3336
+ this.bufferTimeouts.delete(foundKey);
3337
+ }
3338
+ }
3339
+ }
3340
+ if (interrupted) {
3341
+ const assistantId = this.latestItemForRole.assistant;
3342
+ if (assistantId) {
3343
+ this.finalizeStreamingMessage(assistantId, "assistant", false);
3344
+ }
3100
3345
  }
3101
- this.currentStreamingMessage = null;
3102
3346
  }
3103
3347
  renderMessage(message) {
3104
3348
  const messageEl = document.createElement("div");
@@ -3106,16 +3350,54 @@ class ChatManager {
3106
3350
  const bubbleEl = document.createElement("div");
3107
3351
  bubbleEl.className = "message-bubble";
3108
3352
  bubbleEl.textContent = message.text;
3353
+ const footerEl = document.createElement("div");
3354
+ footerEl.className = "message-footer";
3109
3355
  const timeEl = document.createElement("div");
3110
3356
  timeEl.className = "message-time";
3111
3357
  timeEl.textContent = new Date(message.timestamp).toLocaleTimeString([], {
3112
3358
  hour: "2-digit",
3113
3359
  minute: "2-digit"
3114
3360
  });
3361
+ footerEl.appendChild(timeEl);
3115
3362
  messageEl.appendChild(bubbleEl);
3116
- messageEl.appendChild(timeEl);
3363
+ messageEl.appendChild(footerEl);
3364
+ if (message.sender === "assistant") {
3365
+ this.addFeedbackButtons(messageEl);
3366
+ }
3117
3367
  this.chatMessages.appendChild(messageEl);
3118
3368
  }
3369
+ addFeedbackButtons(messageEl) {
3370
+ let footerEl = messageEl.querySelector(".message-footer");
3371
+ if (!footerEl) {
3372
+ footerEl = document.createElement("div");
3373
+ footerEl.className = "message-footer";
3374
+ messageEl.appendChild(footerEl);
3375
+ }
3376
+ const feedbackContainer = document.createElement("div");
3377
+ feedbackContainer.className = "message-feedback";
3378
+ const upBtn = document.createElement("button");
3379
+ upBtn.className = "feedback-btn";
3380
+ upBtn.setAttribute("aria-label", "Helpful");
3381
+ upBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"></path></svg>`;
3382
+ const downBtn = document.createElement("button");
3383
+ downBtn.className = "feedback-btn";
3384
+ downBtn.setAttribute("aria-label", "Not helpful");
3385
+ downBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h3a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-3"></path></svg>`;
3386
+ const toggleFeedback = (btn, otherBtn) => {
3387
+ if (btn.classList.contains("selected")) {
3388
+ btn.classList.remove("selected");
3389
+ } else {
3390
+ btn.classList.add("selected");
3391
+ otherBtn.classList.remove("selected");
3392
+ feedbackContainer.classList.add("active");
3393
+ }
3394
+ };
3395
+ upBtn.addEventListener("click", () => toggleFeedback(upBtn, downBtn));
3396
+ downBtn.addEventListener("click", () => toggleFeedback(downBtn, upBtn));
3397
+ feedbackContainer.appendChild(upBtn);
3398
+ feedbackContainer.appendChild(downBtn);
3399
+ footerEl.appendChild(feedbackContainer);
3400
+ }
3119
3401
  scrollToBottom() {
3120
3402
  this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
3121
3403
  }
@@ -3140,6 +3422,12 @@ class ChatManager {
3140
3422
  log$3.debug("Stopping recording...");
3141
3423
  this.stopRecording();
3142
3424
  }
3425
+ if (this.animationFrameId !== null) {
3426
+ cancelAnimationFrame(this.animationFrameId);
3427
+ this.animationFrameId = null;
3428
+ }
3429
+ log$3.debug("Invalidating session:", this.currentSessionId);
3430
+ this.currentSessionId = null;
3143
3431
  log$3.debug("Stopping audio playback and clearing buffers...");
3144
3432
  this.syncPlayback.stop();
3145
3433
  this.audioOutput.stop();
@@ -3166,6 +3454,9 @@ class ChatManager {
3166
3454
  if ("resume" in this.avatar && typeof this.avatar.resume === "function") {
3167
3455
  this.avatar.resume();
3168
3456
  }
3457
+ if (this.animationFrameId === null) {
3458
+ this.startBlendshapeSync();
3459
+ }
3169
3460
  }
3170
3461
  openChat() {
3171
3462
  const widget = document.getElementById("chatWidget");
@@ -3218,9 +3509,16 @@ const WIDGET_STYLES = `
3218
3509
  line-height: 1.5;
3219
3510
  color: #333;
3220
3511
  box-sizing: border-box;
3512
+ --primary-color: #4B4ACF;
3513
+ --primary-gradient: linear-gradient(135deg, #4B4ACF 0%, #2E3A87 100%);
3514
+ --bg-color: #ffffff;
3515
+ --text-color: #333;
3516
+ --input-bg: #f5f5f7;
3517
+ --border-color: #e0e0e0;
3518
+ --avatar-stage-height: 42%;
3221
3519
  }
3222
3520
 
3223
- :host *, :host *::before, :host *::after {
3521
+ :host * {
3224
3522
  box-sizing: inherit;
3225
3523
  }
3226
3524
 
@@ -3229,330 +3527,708 @@ const WIDGET_STYLES = `
3229
3527
  :host(.position-bottom-left) { position: fixed; bottom: 20px; left: 20px; z-index: 999999; }
3230
3528
  :host(.position-top-right) { position: fixed; top: 20px; right: 20px; z-index: 999999; }
3231
3529
  :host(.position-top-left) { position: fixed; top: 20px; left: 20px; z-index: 999999; }
3232
- :host(.position-inline) { position: relative; }
3530
+ :host(.position-inline) { position: relative; height: 540px; width: 350px; }
3233
3531
  :host(.hidden) { display: none !important; }
3234
3532
 
3235
3533
  /* Main container */
3236
3534
  .widget-root {
3237
- width: 100%;
3238
- height: 100%;
3535
+ width: 350px;
3536
+ height: 540px; /* Taller for immersive view */
3537
+ max-height: 80vh;
3239
3538
  display: flex;
3240
3539
  flex-direction: column;
3241
- background: white;
3242
- border-radius: 12px;
3243
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
3244
- overflow: visible;
3245
- transition: transform 0.3s ease;
3540
+ background: var(--bg-color);
3541
+ border-radius: 20px;
3542
+ /* Softer shadow */
3543
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12), 0 4px 8px rgba(0, 0, 0, 0.05);
3544
+ overflow: hidden;
3545
+ transition: transform 0.3s cubic-bezier(0.19, 1, 0.22, 1), opacity 0.3s ease;
3246
3546
  position: relative;
3547
+ border: 1px solid rgba(0,0,0,0.08);
3247
3548
  }
3248
3549
 
3249
- .widget-root.minimized {
3250
- transform: scale(0);
3251
- opacity: 0;
3252
- pointer-events: none;
3253
- }
3254
-
3255
- .widget-root.theme-dark {
3256
- background: #1a1a2e;
3257
- color: #e0e0e0;
3258
- }
3259
-
3260
- /* Collapsed bubble state */
3261
- :host(.collapsed) {
3262
- width: 60px !important;
3263
- height: 60px !important;
3264
- }
3265
-
3266
- .chat-bubble {
3267
- width: 60px;
3268
- height: 60px;
3269
- border-radius: 50%;
3270
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
3271
- box-shadow: 0 4px 20px rgba(102, 126, 234, 0.4);
3272
- cursor: pointer;
3273
- display: flex;
3274
- align-items: center;
3275
- justify-content: center;
3276
- transition: transform 0.3s ease, box-shadow 0.3s ease;
3277
- }
3278
-
3279
- .chat-bubble:hover {
3280
- transform: scale(1.1);
3281
- box-shadow: 0 6px 25px rgba(102, 126, 234, 0.5);
3550
+ @media (max-width: 480px) {
3551
+ .widget-root {
3552
+ width: 100vw;
3553
+ height: 100vh;
3554
+ max-height: 100vh;
3555
+ border-radius: 0;
3556
+ bottom: 0;
3557
+ right: 0;
3558
+ }
3282
3559
  }
3283
3560
 
3284
- .chat-bubble.hidden {
3285
- transform: scale(0);
3561
+ .widget-root.minimized {
3562
+ transform: translateY(20px) scale(0.9);
3286
3563
  opacity: 0;
3287
3564
  pointer-events: none;
3288
3565
  }
3289
3566
 
3290
- .chat-bubble svg {
3291
- width: 28px;
3292
- height: 28px;
3567
+ .widget-root.theme-dark {
3568
+ --bg-color: #0f111a;
3569
+ --text-color: #ffffff;
3570
+ --input-bg: #1e2130;
3571
+ --border-color: #2a2e42;
3293
3572
  color: white;
3294
3573
  }
3295
3574
 
3296
- /* Avatar Circle - Breaking out at top */
3297
- .avatar-circle {
3298
- width: 120px;
3299
- height: 120px;
3300
- border-radius: 50%;
3301
- background: #ffffff;
3302
- border: 3px solid #667eea;
3575
+ /* ==========================================================================
3576
+ Avatar Stage (Top Half)
3577
+ ========================================================================== */
3578
+ .avatar-stage {
3579
+ height: var(--avatar-stage-height);
3580
+ position: relative;
3581
+ background: radial-gradient(circle at center 40%, #f0f4ff 0%, #ffffff 80%);
3303
3582
  overflow: hidden;
3304
- position: absolute;
3305
- left: -35px;
3306
- top: -40px;
3307
- z-index: 1001;
3308
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
3309
- transition: opacity 0.3s ease, transform 0.3s ease;
3310
- }
3311
-
3312
- .widget-root.minimized .avatar-circle {
3313
- opacity: 0;
3314
- pointer-events: none;
3315
- transform: scale(0.5);
3583
+ flex-shrink: 0;
3316
3584
  }
3317
3585
 
3318
- .theme-dark .avatar-circle {
3319
- background: #1a1a2e;
3320
- border-color: #764ba2;
3586
+ .theme-dark .avatar-stage {
3587
+ background: radial-gradient(circle at center 30%, #2d3748 0%, #0f111a 100%);
3321
3588
  }
3322
3589
 
3323
- /* Avatar render container - high-res rendering scaled down */
3590
+ /* Avatar Render Container (Injected by LazyAvatar) */
3324
3591
  .avatar-render-container {
3325
3592
  width: 800px;
3326
3593
  height: 800px;
3327
3594
  position: absolute;
3328
- top: -80px;
3595
+ /* Reduced from -280px to -160px because scale is smaller */
3596
+ bottom: -200px;
3329
3597
  left: 50%;
3330
- transform: translateX(-50%) scale(0.38);
3331
- transform-origin: center top;
3332
- will-change: transform;
3598
+ transform: translateX(-50%) scale(0.70);
3599
+ transform-origin: center bottom;
3600
+ pointer-events: none; /* Allow interaction with header overlay */
3333
3601
  }
3334
3602
 
3335
3603
  .avatar-render-container canvas {
3336
- width: 800px !important;
3337
- height: 800px !important;
3604
+ width: 100% !important;
3605
+ height: 100% !important;
3338
3606
  object-fit: contain;
3339
3607
  }
3340
3608
 
3341
- .avatar-circle > canvas {
3342
- width: 800px !important;
3343
- height: 800px !important;
3609
+ /* Header Overlay */
3610
+ .chat-header-overlay {
3344
3611
  position: absolute;
3345
- top: -80px;
3346
- left: 50%;
3347
- transform: translateX(-50%) scale(0.38);
3348
- transform-origin: center top;
3349
- object-fit: contain;
3612
+ top: 0;
3613
+ left: 0;
3614
+ right: 0;
3615
+ padding: 12px 16px; /* Reduced padding to push content to top */
3616
+ display: flex;
3617
+ justify-content: space-between;
3618
+ align-items: flex-start;
3619
+ z-index: 10;
3620
+ /* Removed heavy linear gradient for cleaner look */
3621
+ background: transparent;
3350
3622
  }
3351
3623
 
3352
- /* Speaking indicator */
3353
- .avatar-circle.speaking {
3354
- border-color: #27ae60;
3355
- box-shadow: 0 6px 30px rgba(0, 0, 0, 0.3), 0 0 0 5px rgba(39, 174, 96, 0.3);
3356
- animation: speakPulse 1s infinite;
3624
+ .header-info {
3625
+ /* Removed the "Square" background that covered hair */
3626
+ background: transparent;
3627
+ backdrop-filter: none;
3628
+ -webkit-backdrop-filter: none;
3629
+ padding: 0;
3630
+ border: none;
3631
+ box-shadow: none;
3632
+ display: flex;
3633
+ flex-direction: column;
3357
3634
  }
3358
3635
 
3359
- @keyframes speakPulse {
3360
- 0%, 100% { box-shadow: 0 6px 30px rgba(0, 0, 0, 0.3), 0 0 0 5px rgba(39, 174, 96, 0.3); }
3361
- 50% { box-shadow: 0 6px 30px rgba(0, 0, 0, 0.3), 0 0 0 10px rgba(39, 174, 96, 0.1); }
3636
+ .header-info h3 {
3637
+ margin: 0;
3638
+ font-size: 16px; /* Slightly larger for readability without bg */
3639
+ font-weight: 700;
3640
+ color: #1a1a1a;
3641
+ /* Added white glow for readability over avatar hair/bg */
3642
+ text-shadow: 0 0 10px rgba(255,255,255,0.8), 0 0 2px rgba(255,255,255,1);
3643
+ letter-spacing: -0.01em;
3362
3644
  }
3363
3645
 
3364
- /* Chat Header */
3365
- .chat-header {
3366
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
3646
+ .theme-dark .header-info {
3647
+ background: transparent;
3648
+ border-color: transparent;
3649
+ }
3650
+
3651
+ .theme-dark .header-info h3 {
3367
3652
  color: white;
3368
- padding: 14px;
3369
- padding-left: 95px;
3370
- border-radius: 12px 12px 0 0;
3653
+ text-shadow: 0 1px 4px rgba(0,0,0,0.8);
3654
+ }
3655
+
3656
+ .status-badge {
3371
3657
  display: flex;
3372
- justify-content: space-between;
3373
3658
  align-items: center;
3374
- cursor: pointer;
3375
- position: relative;
3659
+ gap: 6px;
3660
+ font-size: 11px;
3661
+ font-weight: 600;
3662
+ color: var(--text-color);
3663
+ padding: 2px 0;
3664
+ margin-top: 2px;
3665
+ /* Add subtle shadow for readability */
3666
+ text-shadow: 0 0 8px rgba(255,255,255,0.8);
3376
3667
  }
3377
3668
 
3378
- .theme-dark .chat-header {
3379
- background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
3669
+ /* Green Dot */
3670
+ .status-badge::before {
3671
+ content: '';
3672
+ display: block;
3673
+ width: 6px;
3674
+ height: 6px;
3675
+ background-color: #10b981;
3676
+ border-radius: 50%;
3677
+ box-shadow: 0 0 4px #10b981;
3380
3678
  }
3381
3679
 
3382
- .chat-header h3 {
3383
- font-size: 15px;
3384
- font-weight: 600;
3385
- margin: 0;
3680
+ .theme-dark .status-badge {
3681
+ color: #ccc;
3386
3682
  }
3387
3683
 
3388
- .chat-header-buttons {
3684
+ .control-btn {
3685
+ background: rgba(255, 255, 255, 0.4);
3686
+ backdrop-filter: blur(12px);
3687
+ -webkit-backdrop-filter: blur(12px);
3688
+ border: 1px solid rgba(255,255,255,0.4);
3689
+ color: #1a1a1a;
3690
+ width: 32px;
3691
+ height: 32px;
3692
+ border-radius: 50%;
3389
3693
  display: flex;
3390
- gap: 8px;
3694
+ align-items: center;
3695
+ justify-content: center;
3696
+ cursor: pointer;
3697
+ transition: all 0.2s;
3698
+ box-shadow: 0 4px 12px rgba(0,0,0,0.05);
3391
3699
  }
3392
3700
 
3393
- .chat-header-buttons button {
3394
- background: rgba(255, 255, 255, 0.2);
3395
- border: none;
3701
+ .control-btn:hover {
3702
+ background: rgba(255, 255, 255, 0.8);
3703
+ transform: scale(1.05);
3704
+ }
3705
+
3706
+ .theme-dark .control-btn {
3396
3707
  color: white;
3397
- width: 28px;
3398
- height: 28px;
3399
- border-radius: 50%;
3400
- cursor: pointer;
3401
- font-size: 14px;
3708
+ background: rgba(0, 0, 0, 0.4);
3709
+ border-color: rgba(255,255,255,0.1);
3710
+ }
3711
+
3712
+ /* ==========================================================================
3713
+ Chat Interface (Bottom Half)
3714
+ ========================================================================== */
3715
+ .chat-interface {
3716
+ flex: 1;
3402
3717
  display: flex;
3403
- align-items: center;
3404
- justify-content: center;
3405
- transition: background 0.2s;
3718
+ flex-direction: column;
3719
+ background: rgba(255, 255, 255, 0.95); /* Slightly translucent */
3720
+ backdrop-filter: blur(20px);
3721
+ -webkit-backdrop-filter: blur(20px);
3722
+ position: relative;
3723
+ z-index: 5;
3724
+ /* Removed Sheet Overlap (-24px margin & radius) */
3725
+ margin-top: 0;
3726
+ border-top: 1px solid rgba(0,0,0,0.05); /* Clean straight separation */
3727
+ overflow: hidden; /* Added to constrain child elements */
3728
+ min-height: 0; /* Added to ensure flex container can shrink if needed */
3729
+ }
3730
+
3731
+ /* Fade overlay at the top of chat interface */
3732
+ .chat-interface::before {
3733
+ content: '';
3734
+ position: absolute;
3735
+ top: 0;
3736
+ left: 0;
3737
+ right: 0;
3738
+ height: 24px;
3739
+ background: linear-gradient(to bottom, rgba(255,255,255,1) 0%, rgba(255,255,255,0) 100%);
3740
+ z-index: 10; /* Above messages */
3741
+ pointer-events: none;
3406
3742
  }
3407
3743
 
3408
- .chat-header-buttons button:hover {
3409
- background: rgba(255, 255, 255, 0.3);
3744
+ .theme-dark .chat-interface::before {
3745
+ background: linear-gradient(to bottom, var(--bg-color) 0%, transparent 100%);
3410
3746
  }
3411
3747
 
3412
- /* Messages Container */
3413
3748
  .chat-messages {
3414
3749
  flex: 1;
3750
+ /* Ensure min-height is 0 so flex scrolling works properly */
3751
+ min-height: 0;
3415
3752
  overflow-y: auto;
3416
- padding: 14px;
3417
- min-height: 250px;
3418
- max-height: 320px;
3419
- background: #fafafa;
3753
+ /* Added top specific padding to account for the fade overlay */
3754
+ padding: 24px 20px 20px;
3755
+ display: flex;
3756
+ flex-direction: column;
3757
+ gap: 12px;
3758
+ /* Scrollbar styling */
3759
+ scrollbar-width: thin;
3760
+ scrollbar-color: rgba(0,0,0,0.1) transparent;
3420
3761
  }
3421
3762
 
3422
- .theme-dark .chat-messages {
3423
- background: #16213e;
3763
+ .chat-messages::-webkit-scrollbar {
3764
+ width: 4px;
3765
+ }
3766
+ .chat-messages::-webkit-scrollbar-thumb {
3767
+ background-color: rgba(0,0,0,0.1);
3768
+ border-radius: 4px;
3424
3769
  }
3425
3770
 
3426
3771
  .message {
3427
- margin-bottom: 16px;
3428
3772
  display: flex;
3429
3773
  flex-direction: column;
3430
- animation: fadeIn 0.3s ease;
3774
+ animation: slideUp 0.3s ease;
3775
+ max-width: 85%; /* Restore max-width constraint */
3431
3776
  }
3432
3777
 
3433
- @keyframes fadeIn {
3778
+ @keyframes slideUp {
3434
3779
  from { opacity: 0; transform: translateY(10px); }
3435
3780
  to { opacity: 1; transform: translateY(0); }
3436
3781
  }
3437
3782
 
3438
3783
  .message.user {
3784
+ align-self: flex-end;
3439
3785
  align-items: flex-end;
3440
3786
  }
3441
3787
 
3442
3788
  .message.assistant {
3443
- align-items: flex-start;
3789
+ align-self: flex-start;
3790
+ align-items: stretch; /* Ensure footer spans full width of the bubble */
3444
3791
  }
3445
3792
 
3446
3793
  .message-bubble {
3447
- max-width: 80%;
3448
- padding: 10px 14px;
3449
- border-radius: 16px;
3450
- word-wrap: break-word;
3794
+ padding: 10px 16px;
3795
+ border-radius: 18px;
3451
3796
  font-size: 14px;
3452
- line-height: 1.45;
3797
+ line-height: 1.5;
3798
+ box-shadow: 0 1px 2px rgba(0,0,0,0.05);
3453
3799
  }
3454
3800
 
3455
3801
  .message.user .message-bubble {
3456
- background: #667eea;
3802
+ background: var(--primary-gradient); /* Modern Gradient Bubble */
3457
3803
  color: white;
3458
3804
  border-bottom-right-radius: 4px;
3805
+ box-shadow: 0 4px 12px rgba(75, 74, 207, 0.25); /* Colored shadow for depth */
3459
3806
  }
3460
3807
 
3461
3808
  .message.assistant .message-bubble {
3462
3809
  background: white;
3463
- color: #333;
3464
- border: 1px solid #e0e0e0;
3810
+ color: var(--text-color);
3465
3811
  border-bottom-left-radius: 4px;
3812
+ border: 1px solid var(--border-color);
3466
3813
  }
3467
3814
 
3468
- .theme-dark .message.assistant .message-bubble {
3469
- background: #1a1a2e;
3470
- color: #e0e0e0;
3471
- border-color: #333;
3815
+ .message-footer {
3816
+ display: flex;
3817
+ justify-content: space-between;
3818
+ align-items: center;
3819
+ margin-top: 4px;
3820
+ padding: 0 4px; /* Align with bubble curvature */
3821
+ min-height: 20px;
3472
3822
  }
3473
3823
 
3474
3824
  .message-time {
3475
3825
  font-size: 11px;
3476
- color: #999;
3477
- margin-top: 6px;
3478
- padding: 0 4px;
3826
+ color: #9ca3af;
3479
3827
  }
3480
3828
 
3481
- /* Input Area */
3482
- .chat-input-area {
3483
- padding: 12px;
3484
- background: white;
3485
- border-top: 1px solid #e0e0e0;
3486
- border-radius: 0 0 12px 12px;
3829
+ /* Response Feedback (Thumbs Up/Down) */
3830
+ .message-feedback {
3831
+ display: flex;
3832
+ gap: 8px;
3833
+ opacity: 0;
3834
+ transform: translateY(-5px);
3835
+ transition: opacity 0.3s, transform 0.3s;
3836
+ pointer-events: none; /* Disabled when hidden */
3837
+ }
3838
+
3839
+ /* Show feedback only when message is hovered or active */
3840
+ .message.assistant:hover .message-feedback,
3841
+ .message.assistant .message-feedback.active {
3842
+ opacity: 1;
3843
+ transform: translateY(0);
3844
+ pointer-events: auto;
3845
+ }
3846
+
3847
+ .feedback-btn {
3848
+ background: transparent;
3849
+ border: none;
3850
+ cursor: pointer;
3851
+ padding: 4px;
3852
+ color: #9ca3af;
3853
+ transition: all 0.2s;
3854
+ border-radius: 4px;
3855
+ display: flex;
3856
+ align-items: center;
3857
+ justify-content: center;
3487
3858
  }
3488
3859
 
3489
- .theme-dark .chat-input-area {
3860
+ .feedback-btn:hover {
3861
+ background: #f3f4f6;
3862
+ color: #4b5563;
3863
+ }
3864
+
3865
+ .feedback-btn.selected {
3866
+ color: var(--primary-color);
3867
+ background: #e0e7ff;
3868
+ }
3869
+
3870
+ .feedback-btn:focus-visible {
3871
+ outline: 2px solid var(--primary-color);
3872
+ outline-offset: 2px;
3873
+ }
3874
+
3875
+ .feedback-btn svg {
3876
+ width: 14px;
3877
+ height: 14px;
3878
+ }
3879
+
3880
+ .theme-dark .message-feedback .feedback-btn {
3881
+ color: #6b7280;
3882
+ }
3883
+
3884
+ .theme-dark .message-feedback .feedback-btn:hover {
3885
+ background: #2a2e42;
3886
+ color: #9ca3af;
3887
+ }
3888
+
3889
+ .theme-dark .message-feedback .feedback-btn.selected {
3890
+ color: var(--primary-color);
3891
+ background: rgba(75, 74, 207, 0.2);
3892
+ }
3893
+
3894
+ .theme-dark .message.assistant .message-bubble {
3490
3895
  background: #1a1a2e;
3491
- border-top-color: #333;
3896
+ color: #e0e0e0;
3897
+ }
3898
+
3899
+ /* Quick Replies */
3900
+ .quick-replies {
3901
+ display: flex;
3902
+ flex-direction: column; /* Vertical layout */
3903
+ align-items: flex-end; /* Align to right like user bubbles */
3904
+ gap: 14px; /* Increased gap to spread them out */
3905
+ /* Increased vertical padding to use more empty chat space */
3906
+ padding: 20px 16px 16px;
3907
+ transition: opacity 0.3s ease, transform 0.3s ease;
3908
+ }
3909
+
3910
+ .quick-replies.hidden {
3911
+ display: none;
3912
+ }
3913
+
3914
+ .suggestion-chip {
3915
+ background: var(--input-bg);
3916
+ border: 1px solid var(--border-color);
3917
+ color: var(--primary-color);
3918
+ padding: 6px 12px;
3919
+ border-radius: 16px;
3920
+ font-size: 13px;
3921
+ cursor: pointer;
3922
+ transition: all 0.2s;
3923
+ flex-shrink: 0;
3924
+ font-weight: 500;
3925
+ }
3926
+
3927
+ .suggestion-chip:hover {
3928
+ background: var(--primary-color);
3929
+ color: white;
3930
+ border-color: var(--primary-color);
3931
+ transform: translateY(-2px);
3932
+ box-shadow: 0 4px 12px rgba(75, 74, 207, 0.25);
3933
+ }
3934
+
3935
+ .theme-dark .suggestion-chip {
3936
+ background: #2a2e42;
3937
+ border-color: #3f445e;
3938
+ color: #e0e0e0;
3939
+ }
3940
+ .theme-dark .suggestion-chip:hover {
3941
+ background: var(--primary-color);
3942
+ color: white;
3943
+ }
3944
+
3945
+ /* Typing Indicator */
3946
+ .typing-indicator.hidden {
3947
+ display: none;
3948
+ }
3949
+
3950
+ .typing-dots {
3951
+ display: flex;
3952
+ gap: 4px;
3953
+ padding: 4px 2px;
3954
+ }
3955
+
3956
+ .typing-dots span {
3957
+ width: 6px;
3958
+ height: 6px;
3959
+ background: #b0b0b0;
3960
+ border-radius: 50%;
3961
+ animation: typingBounce 1.4s infinite ease-in-out both;
3962
+ }
3963
+
3964
+ .typing-dots span:nth-child(1) { animation-delay: -0.32s; }
3965
+ .typing-dots span:nth-child(2) { animation-delay: -0.16s; }
3966
+
3967
+ @keyframes typingBounce {
3968
+ 0%, 80%, 100% { transform: scale(0); }
3969
+ 40% { transform: scale(1); }
3970
+ }
3971
+
3972
+ /* Chat Input Area */
3973
+ .chat-input-area {
3974
+ padding: 16px;
3975
+ background: var(--bg-color);
3976
+ border-top: 1px solid var(--border-color);
3492
3977
  }
3493
3978
 
3494
3979
  .chat-input-wrapper {
3980
+ margin-bottom: 8px;
3981
+ }
3982
+
3983
+ .chat-input-controls {
3495
3984
  display: flex;
3496
3985
  gap: 10px;
3497
- align-items: flex-end;
3986
+ align-items: center;
3987
+ flex: 1; /* Ensure it takes width */
3498
3988
  }
3499
3989
 
3500
- .chat-input-controls {
3990
+ /* Button Swapping Logic -> Co-existence Logic */
3991
+
3992
+ /* Mic: Always visible */
3993
+ .chat-input-controls #micBtn {
3501
3994
  display: flex;
3502
- gap: 6px;
3503
3995
  align-items: center;
3504
- flex: 1;
3996
+ justify-content: center;
3997
+ background: transparent; /* Subtle background */
3998
+ width: 40px;
3999
+ height: 40px;
4000
+ color: #6b7280; /* Gray when inactive */
4001
+ margin-left: 4px;
4002
+ }
4003
+ .chat-input-controls #micBtn:hover {
4004
+ background: #f3f4f6;
4005
+ color: var(--primary-color);
4006
+ transform: scale(1.05);
4007
+ }
4008
+
4009
+ /* Send: Hidden when empty, Popped In when text exists */
4010
+ .chat-input-controls:not(.has-text) #sendBtn {
4011
+ display: none;
4012
+ }
4013
+
4014
+ .chat-input-controls.has-text #sendBtn {
4015
+ display: flex;
4016
+ animation: popIn 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
4017
+ }
4018
+
4019
+ /* Remove old conflicting hidden rules */
4020
+ .chat-input-controls.has-text #micBtn {
4021
+ display: flex; /* Keep visible! */
4022
+ }
4023
+
4024
+
4025
+ @keyframes popIn {
4026
+ from { transform: scale(0); opacity: 0; }
4027
+ to { transform: scale(1); opacity: 1; }
3505
4028
  }
3506
4029
 
3507
4030
  #chatInput {
3508
4031
  flex: 1;
3509
- padding: 10px 14px;
3510
- border: 1px solid #e0e0e0;
3511
- border-radius: 20px;
3512
- font-size: 14px;
4032
+ padding: 12px 16px;
4033
+ border-radius: 24px;
4034
+ border: 1px solid var(--border-color);
4035
+ background: var(--input-bg);
4036
+ color: var(--text-color);
3513
4037
  outline: none;
3514
- transition: border-color 0.2s;
3515
4038
  font-family: inherit;
4039
+ transition: box-shadow 0.2s;
3516
4040
  }
3517
4041
 
3518
4042
  #chatInput:focus {
3519
- border-color: #667eea;
3520
- }
3521
-
3522
- .theme-dark #chatInput {
3523
- background: #16213e;
3524
- border-color: #333;
3525
- color: #e0e0e0;
4043
+ box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
4044
+ border-color: var(--primary-color);
3526
4045
  }
3527
4046
 
3528
4047
  .input-button {
3529
- width: 38px;
3530
- height: 38px;
3531
- border-radius: 50%;
4048
+ background: transparent;
4049
+ color: #9ca3af;
3532
4050
  border: none;
3533
- background: #667eea;
4051
+ cursor: pointer;
4052
+ padding: 8px;
4053
+ border-radius: 50%;
4054
+ transition: all 0.2s;
4055
+ }
4056
+
4057
+ .input-button:hover {
4058
+ background: var(--input-bg);
4059
+ color: var(--primary-color);
4060
+ }
4061
+
4062
+ .input-button#micBtn {
4063
+ color: var(--text-color);
4064
+ }
4065
+ .input-button.recording {
4066
+ color: #e74c3c !important;
4067
+ background: rgba(231, 76, 60, 0.1);
4068
+ animation: recordPulse 1.5s infinite;
4069
+ }
4070
+
4071
+ .input-button#sendBtn {
4072
+ background: var(--primary-color);
3534
4073
  color: white;
4074
+ padding: 10px;
4075
+ }
4076
+ .input-button#sendBtn:hover {
4077
+ transform: scale(1.05);
4078
+ }
4079
+
4080
+ .branding {
4081
+ text-align: center;
4082
+ font-size: 10px;
4083
+ color: #9ca3af;
4084
+ margin-top: 4px;
4085
+ }
4086
+
4087
+ .branding a {
4088
+ color: #7986cb;
4089
+ text-decoration: none;
4090
+ }
4091
+ .branding a:hover {
4092
+ text-decoration: underline;
4093
+ }
4094
+
4095
+ /* ==========================================================================
4096
+ Launcher Bubble (New Face Design)
4097
+ ========================================================================== */
4098
+ :host(.collapsed) {
4099
+ width: auto !important;
4100
+ height: auto !important;
4101
+ bottom: 20px !important;
4102
+ right: 20px !important;
4103
+ top: auto !important;
4104
+ left: auto !important;
4105
+ background: transparent !important;
4106
+ box-shadow: none !important;
4107
+ }
4108
+
4109
+ .bubble-container {
4110
+ position: relative;
4111
+ display: flex;
4112
+ align-items: center;
4113
+ justify-content: flex-end;
4114
+ }
4115
+
4116
+ .chat-bubble {
4117
+ width: 64px;
4118
+ height: 64px;
4119
+ border-radius: 50%;
4120
+ background: var(--bg-color);
4121
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
3535
4122
  cursor: pointer;
4123
+ position: relative;
4124
+ /* Removed overflow: hidden so status dot can sit on the rim */
4125
+ transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
4126
+ z-index: 20;
4127
+ }
4128
+
4129
+ .chat-bubble:hover {
4130
+ transform: scale(1.1);
4131
+ }
4132
+
4133
+ .bubble-avatar-preview {
4134
+ width: 100%;
4135
+ height: 100%;
3536
4136
  display: flex;
3537
4137
  align-items: center;
3538
4138
  justify-content: center;
3539
- transition: all 0.2s;
3540
- flex-shrink: 0;
4139
+ background: white;
4140
+ border-radius: 50%; /* moved radius here */
4141
+ overflow: hidden; /* moved clip here for image */
3541
4142
  }
3542
4143
 
3543
- .input-button:hover:not(:disabled) {
3544
- background: #5568d3;
3545
- transform: scale(1.05);
4144
+ .avatar-face-img {
4145
+ width: 100%;
4146
+ height: 100%;
4147
+ object-fit: cover;
3546
4148
  }
3547
4149
 
3548
- .input-button:disabled {
3549
- background: #ccc;
3550
- cursor: not-allowed;
4150
+ .status-dot {
4151
+ position: absolute;
4152
+ bottom: 0;
4153
+ right: 0;
4154
+ width: 14px; /* Slightly larger */
4155
+ height: 14px;
4156
+ background: #10b981;
4157
+ border: 2px solid white;
4158
+ border-radius: 50%;
4159
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
4160
+ z-index: 5;
3551
4161
  }
3552
4162
 
3553
- .input-button.recording {
3554
- background: #e74c3c;
3555
- animation: recordPulse 1.5s infinite;
4163
+ /* Tooltip (Proactive Reach Out) */
4164
+ .bubble-tooltip-wrapper {
4165
+ position: absolute;
4166
+ right: 74px; /* Left of the bubble */
4167
+ top: 50%;
4168
+ transform: translateY(-50%);
4169
+ pointer-events: none;
4170
+ width: max-content; /* Ensure it takes needed space */
4171
+ max-width: 240px; /* Increased to allow 2 lines */
4172
+ display: flex;
4173
+ justify-content: flex-end;
4174
+ }
4175
+
4176
+ .bubble-tooltip {
4177
+ pointer-events: auto;
4178
+ background: white;
4179
+ color: #333;
4180
+ padding: 10px 14px; /* Slightly more compact */
4181
+ border-radius: 12px;
4182
+ box-shadow: 0 8px 24px rgba(0,0,0,0.12);
4183
+ font-size: 13px; /* Slightly smaller text */
4184
+ font-weight: 500;
4185
+ display: flex;
4186
+ align-items: center;
4187
+ gap: 10px;
4188
+ opacity: 0;
4189
+ transform: translateX(10px);
4190
+ animation: tooltipSlideIn 0.5s cubic-bezier(0.19, 1, 0.22, 1) 1.5s forwards;
4191
+ position: relative;
4192
+ }
4193
+
4194
+ .bubble-tooltip.hidden {
4195
+ display: none;
4196
+ }
4197
+
4198
+ .bubble-tooltip::after {
4199
+ content: '';
4200
+ position: absolute;
4201
+ right: -6px;
4202
+ top: 50%;
4203
+ width: 12px;
4204
+ height: 12px;
4205
+ background: white;
4206
+ transform: translateY(-50%) rotate(45deg); /* Diamond shape, centered */
4207
+ border-radius: 2px;
4208
+ }
4209
+
4210
+ .tooltip-close {
4211
+ background: none;
4212
+ border: none;
4213
+ color: var(--text-muted, #9ca3af);
4214
+ cursor: pointer;
4215
+ font-size: 18px;
4216
+ padding: 0;
4217
+ line-height: 1;
4218
+ display: flex;
4219
+ align-items: center;
4220
+ justify-content: center;
4221
+ width: 20px;
4222
+ height: 20px;
4223
+ border-radius: 50%;
4224
+ }
4225
+ .tooltip-close:hover {
4226
+ background: var(--input-bg);
4227
+ color: var(--text-color);
4228
+ }
4229
+
4230
+ @keyframes tooltipSlideIn {
4231
+ to { opacity: 1; transform: translateX(0); }
3556
4232
  }
3557
4233
 
3558
4234
  @keyframes recordPulse {
@@ -3560,6 +4236,47 @@ const WIDGET_STYLES = `
3560
4236
  50% { transform: scale(1.1); }
3561
4237
  }
3562
4238
 
4239
+ /* ==========================================================================
4240
+ Mobile Full Screen Takeover
4241
+ ========================================================================== */
4242
+ @media (max-width: 480px) {
4243
+ :host(:not(.collapsed)) {
4244
+ position: fixed !important;
4245
+ top: 0 !important;
4246
+ left: 0 !important;
4247
+ right: 0 !important;
4248
+ bottom: 0 !important;
4249
+ width: 100% !important;
4250
+ height: 100% !important;
4251
+ max-width: none !important;
4252
+ max-height: none !important;
4253
+ border-radius: 0 !important;
4254
+ z-index: 9999999 !important;
4255
+ }
4256
+
4257
+ :host(:not(.collapsed)) .widget-root {
4258
+ width: 100% !important;
4259
+ height: 100% !important;
4260
+ border-radius: 0 !important;
4261
+ border: none !important;
4262
+ }
4263
+
4264
+ /* Adjust container sizes for mobile */
4265
+ :host(:not(.collapsed)) .avatar-stage {
4266
+ height: 40%; /* Decreased from 50% to favor chat bubbles */
4267
+ }
4268
+
4269
+ :host(:not(.collapsed)) .avatar-render-container {
4270
+ transform: translateX(-50%) scale(0.65); /* Adjusted for mobile */
4271
+ bottom: -150px; /* Adjusted coordinate */
4272
+ }
4273
+
4274
+ /* Make header bigger on mobile */
4275
+ :host(:not(.collapsed)) .chat-header-overlay {
4276
+ padding: 16px;
4277
+ }
4278
+ }
4279
+
3563
4280
  /* Accessibility */
3564
4281
  .visually-hidden {
3565
4282
  position: absolute;
@@ -3575,65 +4292,88 @@ const WIDGET_STYLES = `
3575
4292
  `;
3576
4293
  const WIDGET_TEMPLATE = `
3577
4294
  <div class="widget-root">
3578
- <!-- Avatar Circle - Breaking out at top -->
3579
- <div class="avatar-circle" id="avatarCircle" aria-label="AI Avatar">
3580
- <div class="avatar-placeholder">👤</div>
4295
+ <!-- Immersive Avatar Stage (Top 42%) -->
4296
+ <div class="avatar-stage" id="avatarContainer" aria-label="AI Avatar Scene">
4297
+ <!-- Header Overlay -->
4298
+ <div class="chat-header-overlay">
4299
+ <div class="header-info">
4300
+ <h3>Nyx Assistant</h3>
4301
+ <span class="status-badge">Live</span>
4302
+ </div>
4303
+ <div class="chat-header-buttons">
4304
+ <button id="minimizeBtn" class="control-btn" aria-label="Minimize chat" title="Minimize">
4305
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
4306
+ <line x1="18" y1="6" x2="6" y2="18"></line>
4307
+ <line x1="6" y1="6" x2="18" y2="18"></line>
4308
+ </svg>
4309
+ </button>
4310
+ </div>
4311
+ </div>
4312
+
4313
+ <!-- Avatar Canvas gets injected here by code -->
4314
+ <div class="avatar-placeholder"></div>
3581
4315
  </div>
3582
4316
 
3583
- <div class="chat-header" role="button" tabindex="0" aria-label="Toggle chat window">
3584
- <h3>Nyx Assistant</h3>
3585
- <div class="chat-header-buttons">
3586
- <button id="minimizeBtn" aria-label="Minimize chat" title="Minimize">−</button>
4317
+ <!-- Chat Interface (Bottom 58%) -->
4318
+ <div class="chat-interface">
4319
+ <div class="chat-messages" id="chatMessages" role="log" aria-live="polite">
4320
+ <!-- Messages injected here -->
4321
+ <div id="typingIndicator" class="message assistant typing-indicator hidden">
4322
+ <div class="message-bubble">
4323
+ <div class="typing-dots">
4324
+ <span></span><span></span><span></span>
4325
+ </div>
4326
+ </div>
4327
+ </div>
3587
4328
  </div>
3588
- </div>
3589
4329
 
3590
- <div class="chat-messages" id="chatMessages" role="log" aria-live="polite" aria-atomic="false">
3591
- <!-- Messages will be added here dynamically -->
3592
- </div>
4330
+ <!-- Quick Replies -->
4331
+ <div class="quick-replies" id="quickReplies">
4332
+ <!-- Chips injected dynamically from config.suggestions -->
4333
+ </div>
3593
4334
 
3594
- <div class="chat-input-area">
3595
- <div class="chat-input-wrapper">
3596
- <div class="chat-input-controls">
3597
- <input
3598
- type="text"
3599
- id="chatInput"
3600
- placeholder="Type a message..."
3601
- aria-label="Chat message input"
3602
- autocomplete="off"
3603
- />
3604
- <button
3605
- id="micBtn"
3606
- class="input-button"
3607
- aria-label="Voice input"
3608
- title="Voice input"
3609
- >
3610
- <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
3611
- <path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/>
3612
- <path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
3613
- <line x1="12" x2="12" y1="19" y2="22"/>
3614
- </svg>
3615
- </button>
3616
- <button
3617
- id="sendBtn"
3618
- class="input-button"
3619
- aria-label="Send message"
3620
- title="Send"
3621
- >
3622
- <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
3623
- <path d="M22 2 11 13"/>
3624
- <path d="M22 2 15 22 11 13 2 9 22 2z"/>
3625
- </svg>
3626
- </button>
4335
+ <div class="chat-input-area">
4336
+ <div class="chat-input-wrapper">
4337
+ <div class="chat-input-controls">
4338
+ <input type="text" id="chatInput" placeholder="Ask me anything..." aria-label="Message input" autocomplete="off" />
4339
+
4340
+ <!-- Mic Button (Prominent) -->
4341
+ <button id="micBtn" class="input-button" aria-label="Voice input">
4342
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
4343
+ <path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/>
4344
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
4345
+ <line x1="12" x2="12" y1="19" y2="22"/>
4346
+ </svg>
4347
+ </button>
4348
+
4349
+ <!-- Send Button -->
4350
+ <button id="sendBtn" class="input-button" aria-label="Send message">
4351
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
4352
+ <path d="M22 2 11 13"/>
4353
+ <path d="M22 2 15 22 11 13 2 9 22 2z"/>
4354
+ </svg>
4355
+ </button>
4356
+ </div>
3627
4357
  </div>
4358
+ <div class="branding">Powered by <a href="https://www.myned.ai" target="_blank" rel="noopener noreferrer">Myned AI</a></div>
3628
4359
  </div>
3629
4360
  </div>
3630
4361
  </div>
3631
4362
  `;
3632
4363
  const BUBBLE_TEMPLATE = `
3633
- <div class="chat-bubble" role="button" aria-label="Open chat" tabindex="0">
3634
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
3635
- <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
3636
- </svg>
4364
+ <div class="bubble-container">
4365
+ <div class="bubble-tooltip-wrapper">
4366
+ <div class="bubble-tooltip" id="bubbleTooltip">
4367
+ <span class="tooltip-text" id="tooltipText"></span>
4368
+ <button class="tooltip-close" id="tooltipClose" aria-label="Close tooltip">×</button>
4369
+ </div>
4370
+ </div>
4371
+ <div class="chat-bubble" id="chatBubble" role="button" aria-label="Open chat" tabindex="0">
4372
+ <div class="bubble-avatar-preview">
4373
+ <img src="./asset/avatar.png" class="avatar-face-img" alt="Nyx Avatar" />
4374
+ <div class="status-dot"></div>
4375
+ </div>
4376
+ </div>
3637
4377
  </div>
3638
4378
  `;
3639
4379
  const DEFAULT_CONFIG$1 = {
@@ -3645,7 +4385,19 @@ const DEFAULT_CONFIG$1 = {
3645
4385
  enableVoice: true,
3646
4386
  enableText: true,
3647
4387
  authEnabled: false,
3648
- logLevel: "error"
4388
+ logLevel: "error",
4389
+ suggestions: [
4390
+ "What is your story?",
4391
+ "What services do you provide?",
4392
+ "Can I book a meeting?"
4393
+ ],
4394
+ tooltipText: "Hi! 👋 Ask me anything."
4395
+ };
4396
+ const UI_DELAY = {
4397
+ /** Visual feedback delay before triggering send action */
4398
+ CHIP_CLICK_SEND: 200,
4399
+ /** Delay to allow ChatManager to process before UI cleanup */
4400
+ INPUT_CLEANUP: 50
3649
4401
  };
3650
4402
  const log$2 = logger.scope("Widget");
3651
4403
  const DEFAULT_CONFIG = {
@@ -3748,18 +4500,33 @@ class AvatarChatElement extends HTMLElement {
3748
4500
  if (style) this.shadow.appendChild(style);
3749
4501
  const container = document.createElement("div");
3750
4502
  container.innerHTML = BUBBLE_TEMPLATE;
3751
- const bubble = container.firstElementChild;
3752
- bubble.addEventListener("click", () => this.expand());
3753
- bubble.addEventListener("keypress", (e) => {
3754
- if (e.key === "Enter") this.expand();
3755
- });
3756
- this.shadow.appendChild(bubble);
4503
+ const wrapper = container.firstElementChild;
4504
+ const bubble = wrapper.querySelector("#chatBubble");
4505
+ if (bubble) {
4506
+ bubble.addEventListener("click", () => this.expand());
4507
+ bubble.addEventListener("keypress", (e) => {
4508
+ if (e.key === "Enter") this.expand();
4509
+ });
4510
+ }
4511
+ const closeBtn = wrapper.querySelector("#tooltipClose");
4512
+ const tooltip = wrapper.querySelector("#bubbleTooltip");
4513
+ const tooltipTextEl = wrapper.querySelector("#tooltipText");
4514
+ if (tooltipTextEl && this.config.tooltipText) {
4515
+ tooltipTextEl.textContent = this.config.tooltipText;
4516
+ }
4517
+ if (closeBtn && tooltip) {
4518
+ closeBtn.addEventListener("click", (e) => {
4519
+ e.stopPropagation();
4520
+ tooltip.classList.add("hidden");
4521
+ });
4522
+ }
4523
+ this.shadow.appendChild(wrapper);
3757
4524
  }
3758
4525
  /**
3759
4526
  * Initialize avatar renderer
3760
4527
  */
3761
4528
  async initializeAvatar() {
3762
- const avatarContainer = this.shadow.getElementById("avatarCircle");
4529
+ const avatarContainer = this.shadow.getElementById("avatarContainer");
3763
4530
  if (!avatarContainer) {
3764
4531
  log$2.error("Avatar container not found");
3765
4532
  return;
@@ -3815,6 +4582,7 @@ class AvatarChatElement extends HTMLElement {
3815
4582
  },
3816
4583
  onMessage: (msg) => {
3817
4584
  this.config.onMessage?.(msg);
4585
+ this.expandWidgetHeight();
3818
4586
  },
3819
4587
  onError: (err) => {
3820
4588
  this.config.onError?.(err);
@@ -3842,6 +4610,82 @@ class AvatarChatElement extends HTMLElement {
3842
4610
  };
3843
4611
  this.themeMediaQuery.addEventListener("change", this.themeChangeHandler);
3844
4612
  }
4613
+ const chatInput = this.shadow.getElementById("chatInput");
4614
+ const inputControls = this.shadow.querySelector(".chat-input-controls");
4615
+ if (chatInput && inputControls) {
4616
+ chatInput.addEventListener("input", () => {
4617
+ if (chatInput.value.trim().length > 0) {
4618
+ inputControls.classList.add("has-text");
4619
+ } else {
4620
+ inputControls.classList.remove("has-text");
4621
+ }
4622
+ });
4623
+ }
4624
+ const quickReplies = this.shadow.getElementById("quickReplies");
4625
+ const sendBtn = this.shadow.getElementById("sendBtn");
4626
+ const micBtn = this.shadow.getElementById("micBtn");
4627
+ if (quickReplies && this.config.suggestions && this.config.suggestions.length > 0) {
4628
+ quickReplies.innerHTML = this.config.suggestions.map((text) => `<button class="suggestion-chip">${this.escapeHtml(text)}</button>`).join("");
4629
+ }
4630
+ if (quickReplies && chatInput && sendBtn) {
4631
+ const hideSuggestions = () => {
4632
+ quickReplies.classList.add("hidden");
4633
+ };
4634
+ quickReplies.addEventListener("click", (e) => {
4635
+ const target = e.target;
4636
+ if (target.classList.contains("suggestion-chip")) {
4637
+ this.expandWidgetHeight();
4638
+ hideSuggestions();
4639
+ const text = target.textContent;
4640
+ if (text) {
4641
+ chatInput.value = text;
4642
+ inputControls?.classList.add("has-text");
4643
+ setTimeout(() => sendBtn.click(), UI_DELAY.CHIP_CLICK_SEND);
4644
+ }
4645
+ }
4646
+ });
4647
+ sendBtn.addEventListener("click", () => {
4648
+ this.expandWidgetHeight();
4649
+ hideSuggestions();
4650
+ setTimeout(() => {
4651
+ if (chatInput.value.trim() === "") {
4652
+ inputControls?.classList.remove("has-text");
4653
+ }
4654
+ }, UI_DELAY.INPUT_CLEANUP);
4655
+ });
4656
+ micBtn?.addEventListener("click", () => {
4657
+ this.expandWidgetHeight();
4658
+ hideSuggestions();
4659
+ });
4660
+ chatInput.addEventListener("keydown", (e) => {
4661
+ if (e.key === "Enter") {
4662
+ this.expandWidgetHeight();
4663
+ hideSuggestions();
4664
+ setTimeout(() => {
4665
+ if (chatInput.value.trim() === "") {
4666
+ inputControls?.classList.remove("has-text");
4667
+ }
4668
+ }, UI_DELAY.INPUT_CLEANUP);
4669
+ }
4670
+ });
4671
+ }
4672
+ }
4673
+ /**
4674
+ * Escape HTML to prevent XSS in user-provided suggestions
4675
+ */
4676
+ escapeHtml(text) {
4677
+ const div = document.createElement("div");
4678
+ div.textContent = text;
4679
+ return div.innerHTML;
4680
+ }
4681
+ /**
4682
+ * Expand the widget to full height (called on first interaction)
4683
+ */
4684
+ expandWidgetHeight() {
4685
+ const root = this.shadow.querySelector(".widget-root");
4686
+ if (root && !root.classList.contains("expanded")) {
4687
+ root.classList.add("expanded");
4688
+ }
3845
4689
  }
3846
4690
  /**
3847
4691
  * Update connection status (stub - connection indicator removed from UI)
@@ -3928,10 +4772,19 @@ class AvatarChatElement extends HTMLElement {
3928
4772
  return this.chatManager.reconnect();
3929
4773
  }
3930
4774
  /**
3931
- * Cleanup
4775
+ * Web Component lifecycle: Called when element is removed from DOM
4776
+ * Ensures cleanup happens even if element is removed externally (not via destroy())
3932
4777
  */
3933
- destroy() {
3934
- log$2.info("Destroying widget");
4778
+ disconnectedCallback() {
4779
+ if (this._isMounted) {
4780
+ log$2.info("Widget removed from DOM - cleaning up resources");
4781
+ this.cleanup();
4782
+ }
4783
+ }
4784
+ /**
4785
+ * Internal cleanup logic (shared by destroy() and disconnectedCallback())
4786
+ */
4787
+ cleanup() {
3935
4788
  if (this.themeMediaQuery && this.themeChangeHandler) {
3936
4789
  this.themeMediaQuery.removeEventListener("change", this.themeChangeHandler);
3937
4790
  this.themeMediaQuery = null;
@@ -3946,10 +4799,17 @@ class AvatarChatElement extends HTMLElement {
3946
4799
  this.avatar = null;
3947
4800
  }
3948
4801
  this.shadow.innerHTML = "";
3949
- this.remove();
3950
4802
  this._isMounted = false;
3951
4803
  this._isConnected = false;
3952
4804
  }
4805
+ /**
4806
+ * Cleanup and remove from DOM
4807
+ */
4808
+ destroy() {
4809
+ log$2.info("Destroying widget");
4810
+ this.cleanup();
4811
+ this.remove();
4812
+ }
3953
4813
  }
3954
4814
  if (typeof customElements !== "undefined" && !customElements.get("avatar-chat-widget")) {
3955
4815
  customElements.define("avatar-chat-widget", AvatarChatElement);