@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.
- package/dist/asset/avatar.png +0 -0
- package/dist/avatar-chat-widget.d.ts +18 -1
- package/dist/avatar-chat-widget.js +1168 -308
- package/dist/avatar-chat-widget.js.map +1 -1
- package/dist/avatar-chat-widget.umd.js +1 -1
- package/dist/avatar-chat-widget.umd.js.map +1 -1
- package/package.json +1 -1
- package/public/asset/avatar.png +0 -0
|
@@ -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(
|
|
109
|
-
get(
|
|
110
|
-
if (typeof prop === "string" && prop in
|
|
111
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
3119
|
+
* Item-aware streaming transcript: uses server-provided itemId and previousItemId
|
|
3051
3120
|
*/
|
|
3052
|
-
streamTranscript(text, role) {
|
|
3053
|
-
|
|
3054
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
3179
|
+
messageEl.appendChild(footerEl);
|
|
3069
3180
|
this.chatMessages.appendChild(messageEl);
|
|
3070
|
-
this.
|
|
3071
|
-
|
|
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
|
-
|
|
3078
|
-
if (bubbleEl) {
|
|
3079
|
-
bubbleEl.textContent += text;
|
|
3080
|
-
this.scrollToBottom();
|
|
3081
|
-
}
|
|
3185
|
+
this.appendToStreamingItem(effectiveId, role, text);
|
|
3082
3186
|
}
|
|
3083
3187
|
}
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
if (
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
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:
|
|
3294
|
+
sender: r,
|
|
3096
3295
|
timestamp: Date.now()
|
|
3097
3296
|
};
|
|
3098
|
-
this.messages.push(
|
|
3099
|
-
this.options.onMessage?.({ role:
|
|
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(
|
|
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
|
|
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:
|
|
3238
|
-
height:
|
|
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:
|
|
3242
|
-
border-radius:
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
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
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
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
|
-
.
|
|
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
|
-
.
|
|
3291
|
-
|
|
3292
|
-
|
|
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
|
-
/*
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
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
|
-
|
|
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-
|
|
3319
|
-
background: #
|
|
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
|
|
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
|
-
|
|
3595
|
+
/* Reduced from -280px to -160px because scale is smaller */
|
|
3596
|
+
bottom: -200px;
|
|
3329
3597
|
left: 50%;
|
|
3330
|
-
transform: translateX(-50%) scale(0.
|
|
3331
|
-
transform-origin: center
|
|
3332
|
-
|
|
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:
|
|
3337
|
-
height:
|
|
3604
|
+
width: 100% !important;
|
|
3605
|
+
height: 100% !important;
|
|
3338
3606
|
object-fit: contain;
|
|
3339
3607
|
}
|
|
3340
3608
|
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
height: 800px !important;
|
|
3609
|
+
/* Header Overlay */
|
|
3610
|
+
.chat-header-overlay {
|
|
3344
3611
|
position: absolute;
|
|
3345
|
-
top:
|
|
3346
|
-
left:
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
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
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
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
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
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
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
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
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
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
|
-
|
|
3375
|
-
|
|
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
|
-
|
|
3379
|
-
|
|
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
|
-
.
|
|
3383
|
-
|
|
3384
|
-
font-weight: 600;
|
|
3385
|
-
margin: 0;
|
|
3680
|
+
.theme-dark .status-badge {
|
|
3681
|
+
color: #ccc;
|
|
3386
3682
|
}
|
|
3387
3683
|
|
|
3388
|
-
.
|
|
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
|
-
|
|
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
|
-
.
|
|
3394
|
-
background: rgba(255, 255, 255, 0.
|
|
3395
|
-
|
|
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
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
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
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
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-
|
|
3409
|
-
background:
|
|
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
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
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
|
-
.
|
|
3423
|
-
|
|
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:
|
|
3774
|
+
animation: slideUp 0.3s ease;
|
|
3775
|
+
max-width: 85%; /* Restore max-width constraint */
|
|
3431
3776
|
}
|
|
3432
3777
|
|
|
3433
|
-
@keyframes
|
|
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-
|
|
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
|
-
|
|
3448
|
-
|
|
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.
|
|
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:
|
|
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:
|
|
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
|
-
.
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
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: #
|
|
3477
|
-
margin-top: 6px;
|
|
3478
|
-
padding: 0 4px;
|
|
3826
|
+
color: #9ca3af;
|
|
3479
3827
|
}
|
|
3480
3828
|
|
|
3481
|
-
/*
|
|
3482
|
-
.
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
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:
|
|
3986
|
+
align-items: center;
|
|
3987
|
+
flex: 1; /* Ensure it takes width */
|
|
3498
3988
|
}
|
|
3499
3989
|
|
|
3500
|
-
|
|
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
|
-
|
|
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:
|
|
3510
|
-
border:
|
|
3511
|
-
border
|
|
3512
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3530
|
-
|
|
3531
|
-
border-radius: 50%;
|
|
4048
|
+
background: transparent;
|
|
4049
|
+
color: #9ca3af;
|
|
3532
4050
|
border: none;
|
|
3533
|
-
|
|
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
|
-
|
|
3540
|
-
|
|
4139
|
+
background: white;
|
|
4140
|
+
border-radius: 50%; /* moved radius here */
|
|
4141
|
+
overflow: hidden; /* moved clip here for image */
|
|
3541
4142
|
}
|
|
3542
4143
|
|
|
3543
|
-
.
|
|
3544
|
-
|
|
3545
|
-
|
|
4144
|
+
.avatar-face-img {
|
|
4145
|
+
width: 100%;
|
|
4146
|
+
height: 100%;
|
|
4147
|
+
object-fit: cover;
|
|
3546
4148
|
}
|
|
3547
4149
|
|
|
3548
|
-
.
|
|
3549
|
-
|
|
3550
|
-
|
|
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
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
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
|
|
3579
|
-
<div class="avatar-
|
|
3580
|
-
|
|
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
|
-
|
|
3584
|
-
|
|
3585
|
-
<div class="chat-
|
|
3586
|
-
|
|
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
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
4330
|
+
<!-- Quick Replies -->
|
|
4331
|
+
<div class="quick-replies" id="quickReplies">
|
|
4332
|
+
<!-- Chips injected dynamically from config.suggestions -->
|
|
4333
|
+
</div>
|
|
3593
4334
|
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
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="
|
|
3634
|
-
<
|
|
3635
|
-
|
|
3636
|
-
|
|
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
|
|
3752
|
-
bubble.
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
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("
|
|
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
|
-
*
|
|
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
|
-
|
|
3934
|
-
|
|
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);
|