@wabbit-dashboard/embed 1.0.16 → 1.1.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/wabbit-embed.cjs.js +371 -106
- package/dist/wabbit-embed.cjs.js.map +1 -1
- package/dist/wabbit-embed.d.ts +50 -1
- package/dist/wabbit-embed.esm.js +371 -106
- package/dist/wabbit-embed.esm.js.map +1 -1
- package/dist/wabbit-embed.umd.js +371 -106
- package/dist/wabbit-embed.umd.js.map +1 -1
- package/dist/wabbit-embed.umd.min.js +1 -1
- package/dist/wabbit-embed.umd.min.js.map +1 -1
- package/package.json +1 -1
package/dist/wabbit-embed.cjs.js
CHANGED
|
@@ -161,99 +161,41 @@ class Wabbit {
|
|
|
161
161
|
const mergedConfig = mergeConfig(config);
|
|
162
162
|
Wabbit.instance = new Wabbit(mergedConfig);
|
|
163
163
|
// Store email capture widget reference for use in callbacks
|
|
164
|
-
// This allows chat widget to trigger email capture without tight coupling
|
|
165
164
|
let emailCaptureWidgetRef = null;
|
|
166
|
-
// Track if email capture is enabled (determined by server config)
|
|
167
|
-
let emailCaptureEnabled = false;
|
|
168
165
|
// Initialize widgets based on merged config
|
|
169
166
|
if (mergedConfig.chat?.enabled && mergedConfig.chat) {
|
|
170
167
|
const chat = mergedConfig.chat;
|
|
171
168
|
const collectionId = chat.collectionId;
|
|
169
|
+
const userOnChatReady = chat.onChatReady || config.onChatReady;
|
|
172
170
|
// Derive chat service API URL from WebSocket URL
|
|
173
|
-
// The email capture config endpoint is on the chat service, not the dashboard
|
|
174
171
|
const wsUrl = mergedConfig.wsUrl || chat.wsUrl || 'wss://chat.insourcedata.ai/ws/chat';
|
|
175
172
|
const chatServiceUrl = wsUrl
|
|
176
173
|
.replace(/^wss:\/\//, 'https://')
|
|
177
174
|
.replace(/^ws:\/\//, 'http://')
|
|
178
|
-
.replace(/\/ws\/chat.*$/, '');
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
.then(res => res.json())
|
|
182
|
-
.then(data => {
|
|
183
|
-
if (data.success && data.config?.enabled) {
|
|
184
|
-
return data.config;
|
|
185
|
-
}
|
|
186
|
-
return null;
|
|
187
|
-
})
|
|
188
|
-
.catch(err => {
|
|
189
|
-
console.warn('[Wabbit] Failed to fetch email capture config:', err);
|
|
190
|
-
return null;
|
|
191
|
-
});
|
|
192
|
-
// Import ChatWidget dynamically to avoid circular dependencies
|
|
193
|
-
Promise.resolve().then(function () { return ChatWidget$1; }).then(async ({ ChatWidget }) => {
|
|
194
|
-
// Check if instance was destroyed during async import (e.g., React StrictMode)
|
|
175
|
+
.replace(/\/ws\/chat.*$/, '');
|
|
176
|
+
// Import and initialize ChatWidget immediately (don't wait for email capture config)
|
|
177
|
+
Promise.resolve().then(function () { return ChatWidget$1; }).then(({ ChatWidget }) => {
|
|
195
178
|
if (!Wabbit.instance) {
|
|
196
179
|
console.warn('[Wabbit] Instance was destroyed before chat widget could initialize');
|
|
197
180
|
return;
|
|
198
181
|
}
|
|
199
|
-
// Wait for email capture config to be fetched
|
|
200
|
-
const serverEmailConfig = await emailCaptureConfigPromise;
|
|
201
|
-
emailCaptureEnabled = !!serverEmailConfig;
|
|
202
|
-
// Initialize email capture widget if server config says it's enabled
|
|
203
|
-
if (serverEmailConfig) {
|
|
204
|
-
const { EmailCaptureWidget } = await Promise.resolve().then(function () { return EmailCaptureWidget$1; });
|
|
205
|
-
if (!Wabbit.instance) {
|
|
206
|
-
console.warn('[Wabbit] Instance was destroyed before email capture widget could initialize');
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
// Convert server config format to widget config format
|
|
210
|
-
const triggerAfterMessages = serverEmailConfig.trigger?.type === 'message_count'
|
|
211
|
-
? serverEmailConfig.trigger.messageCount
|
|
212
|
-
: 3;
|
|
213
|
-
// Build fields array from server config
|
|
214
|
-
const fields = ['email'];
|
|
215
|
-
if (serverEmailConfig.fields?.name?.enabled)
|
|
216
|
-
fields.push('name');
|
|
217
|
-
if (serverEmailConfig.fields?.company?.enabled)
|
|
218
|
-
fields.push('company');
|
|
219
|
-
const emailCaptureConfig = {
|
|
220
|
-
enabled: true,
|
|
221
|
-
triggerAfterMessages,
|
|
222
|
-
title: serverEmailConfig.modal?.title,
|
|
223
|
-
description: serverEmailConfig.modal?.description,
|
|
224
|
-
fields,
|
|
225
|
-
onCapture: mergedConfig.emailCapture?.onCapture
|
|
226
|
-
};
|
|
227
|
-
const emailCaptureWidget = new EmailCaptureWidget(emailCaptureConfig);
|
|
228
|
-
emailCaptureWidget.init();
|
|
229
|
-
Wabbit.instance.emailCaptureWidget = emailCaptureWidget;
|
|
230
|
-
emailCaptureWidgetRef = emailCaptureWidget;
|
|
231
|
-
console.log('[Wabbit] Email capture initialized from server config');
|
|
232
|
-
}
|
|
233
|
-
const userOnChatReady = chat.onChatReady || config.onChatReady;
|
|
234
182
|
const chatConfig = {
|
|
235
183
|
enabled: chat.enabled,
|
|
236
184
|
collectionId: chat.collectionId,
|
|
237
|
-
// Use chat-specific apiKey/apiUrl if provided, otherwise inherit from WabbitConfig
|
|
238
185
|
apiKey: chat.apiKey || mergedConfig.apiKey,
|
|
239
186
|
apiUrl: chat.apiUrl || mergedConfig.apiUrl,
|
|
240
|
-
wsUrl: mergedConfig.wsUrl || chat.wsUrl,
|
|
241
|
-
// Embedding mode
|
|
187
|
+
wsUrl: mergedConfig.wsUrl || chat.wsUrl,
|
|
242
188
|
mode: chat.mode,
|
|
243
189
|
container: chat.container,
|
|
244
|
-
// Widget mode options
|
|
245
190
|
position: chat.position,
|
|
246
191
|
triggerType: chat.triggerType,
|
|
247
192
|
triggerDelay: chat.triggerDelay,
|
|
248
|
-
// Appearance options
|
|
249
193
|
theme: chat.theme,
|
|
250
194
|
primaryColor: chat.primaryColor,
|
|
251
195
|
welcomeMessage: chat.welcomeMessage,
|
|
252
196
|
placeholder: chat.placeholder,
|
|
253
|
-
// Header customization
|
|
254
197
|
showHeader: chat.showHeader,
|
|
255
198
|
headerTitle: chat.headerTitle,
|
|
256
|
-
// Callbacks
|
|
257
199
|
onChatReady: () => {
|
|
258
200
|
// Set WebSocket client on email capture widget when chat is ready
|
|
259
201
|
if (emailCaptureWidgetRef && Wabbit.instance?.chatWidget) {
|
|
@@ -262,24 +204,68 @@ class Wabbit {
|
|
|
262
204
|
emailCaptureWidgetRef.setWebSocketClient(chatWidget.wsClient);
|
|
263
205
|
}
|
|
264
206
|
}
|
|
265
|
-
// Call user's callback
|
|
266
207
|
if (userOnChatReady) {
|
|
267
208
|
userOnChatReady();
|
|
268
209
|
}
|
|
269
210
|
},
|
|
270
|
-
//
|
|
271
|
-
onUserMessage:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
emailCaptureWidgetRef.handleMessage();
|
|
275
|
-
}
|
|
211
|
+
// Always set onUserMessage - it checks if emailCaptureWidgetRef exists at call time
|
|
212
|
+
onUserMessage: () => {
|
|
213
|
+
if (emailCaptureWidgetRef) {
|
|
214
|
+
emailCaptureWidgetRef.handleMessage();
|
|
276
215
|
}
|
|
277
|
-
|
|
216
|
+
}
|
|
278
217
|
};
|
|
279
218
|
const chatWidget = new ChatWidget(chatConfig);
|
|
280
219
|
chatWidget.init();
|
|
281
220
|
Wabbit.instance.chatWidget = chatWidget;
|
|
282
221
|
});
|
|
222
|
+
// Fetch email capture config from server (in parallel with chat init)
|
|
223
|
+
fetch(`${chatServiceUrl}/api/email-capture/config/${collectionId}`)
|
|
224
|
+
.then(res => res.json())
|
|
225
|
+
.then(async (data) => {
|
|
226
|
+
if (!data.success || !data.config?.enabled) {
|
|
227
|
+
return; // Email capture not enabled
|
|
228
|
+
}
|
|
229
|
+
if (!Wabbit.instance) {
|
|
230
|
+
return; // Instance was destroyed
|
|
231
|
+
}
|
|
232
|
+
const serverConfig = data.config;
|
|
233
|
+
const { EmailCaptureWidget } = await Promise.resolve().then(function () { return EmailCaptureWidget$1; });
|
|
234
|
+
if (!Wabbit.instance) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
// Convert server config to widget config
|
|
238
|
+
const triggerAfterMessages = serverConfig.trigger?.type === 'message_count'
|
|
239
|
+
? serverConfig.trigger.messageCount
|
|
240
|
+
: 3;
|
|
241
|
+
const fields = ['email'];
|
|
242
|
+
if (serverConfig.fields?.name?.enabled)
|
|
243
|
+
fields.push('name');
|
|
244
|
+
if (serverConfig.fields?.company?.enabled)
|
|
245
|
+
fields.push('company');
|
|
246
|
+
const emailCaptureConfig = {
|
|
247
|
+
enabled: true,
|
|
248
|
+
triggerAfterMessages,
|
|
249
|
+
title: serverConfig.modal?.title,
|
|
250
|
+
description: serverConfig.modal?.description,
|
|
251
|
+
fields,
|
|
252
|
+
onCapture: mergedConfig.emailCapture?.onCapture
|
|
253
|
+
};
|
|
254
|
+
const emailCaptureWidget = new EmailCaptureWidget(emailCaptureConfig);
|
|
255
|
+
emailCaptureWidget.init();
|
|
256
|
+
Wabbit.instance.emailCaptureWidget = emailCaptureWidget;
|
|
257
|
+
emailCaptureWidgetRef = emailCaptureWidget;
|
|
258
|
+
// If chat is already ready, set the WebSocket client now
|
|
259
|
+
if (Wabbit.instance.chatWidget) {
|
|
260
|
+
const chatWidget = Wabbit.instance.chatWidget;
|
|
261
|
+
if (chatWidget.wsClient) {
|
|
262
|
+
emailCaptureWidget.setWebSocketClient(chatWidget.wsClient);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
.catch(err => {
|
|
267
|
+
console.warn('[Wabbit] Failed to fetch email capture config:', err);
|
|
268
|
+
});
|
|
283
269
|
}
|
|
284
270
|
if (mergedConfig.forms?.enabled && mergedConfig.forms) {
|
|
285
271
|
// Import FormWidget dynamically to avoid circular dependencies
|
|
@@ -656,6 +642,9 @@ class ChatWebSocketClient {
|
|
|
656
642
|
this.reconnectAttempts = 0;
|
|
657
643
|
this.maxReconnectAttempts = 3;
|
|
658
644
|
this.reconnectDelay = 1000;
|
|
645
|
+
// Message queue for handling messages during disconnection
|
|
646
|
+
this.messageQueue = [];
|
|
647
|
+
this.MAX_QUEUE_SIZE = 10;
|
|
659
648
|
// Event handlers
|
|
660
649
|
this.onStatusChange = null;
|
|
661
650
|
this.onWelcome = null;
|
|
@@ -663,6 +652,11 @@ class ChatWebSocketClient {
|
|
|
663
652
|
this.onMessageHistory = null;
|
|
664
653
|
this.onError = null;
|
|
665
654
|
this.onDisconnect = null;
|
|
655
|
+
this.onQueueOverflow = null;
|
|
656
|
+
// Streaming event handlers
|
|
657
|
+
this.onMessageStart = null;
|
|
658
|
+
this.onMessageChunk = null;
|
|
659
|
+
this.onMessageEnd = null;
|
|
666
660
|
this.apiKey = apiKey;
|
|
667
661
|
this.wsUrl = wsUrl;
|
|
668
662
|
this.sessionId = sessionId || this.getStoredSessionId();
|
|
@@ -694,6 +688,8 @@ class ChatWebSocketClient {
|
|
|
694
688
|
this.setStatus('connected');
|
|
695
689
|
this.reconnectAttempts = 0;
|
|
696
690
|
console.log('[Wabbit] WebSocket connected');
|
|
691
|
+
// Flush any queued messages
|
|
692
|
+
this.flushMessageQueue();
|
|
697
693
|
};
|
|
698
694
|
this.ws.onmessage = (event) => {
|
|
699
695
|
try {
|
|
@@ -752,7 +748,26 @@ class ChatWebSocketClient {
|
|
|
752
748
|
this.onMessageHistory(messages);
|
|
753
749
|
}
|
|
754
750
|
break;
|
|
751
|
+
case 'assistant_message_start':
|
|
752
|
+
// Streaming: Start of assistant message
|
|
753
|
+
if (this.onMessageStart) {
|
|
754
|
+
this.onMessageStart(data.message_id);
|
|
755
|
+
}
|
|
756
|
+
break;
|
|
757
|
+
case 'assistant_message_chunk':
|
|
758
|
+
// Streaming: Chunk of assistant message
|
|
759
|
+
if (this.onMessageChunk) {
|
|
760
|
+
this.onMessageChunk(data.message_id, data.content);
|
|
761
|
+
}
|
|
762
|
+
break;
|
|
763
|
+
case 'assistant_message_end':
|
|
764
|
+
// Streaming: End of assistant message
|
|
765
|
+
if (this.onMessageEnd) {
|
|
766
|
+
this.onMessageEnd(data.message_id, data.metadata);
|
|
767
|
+
}
|
|
768
|
+
break;
|
|
755
769
|
case 'assistant_message':
|
|
770
|
+
// Non-streaming: Complete assistant message (backward compatibility)
|
|
756
771
|
if (this.onMessage) {
|
|
757
772
|
const message = {
|
|
758
773
|
id: data.message_id || crypto.randomUUID(),
|
|
@@ -774,18 +789,27 @@ class ChatWebSocketClient {
|
|
|
774
789
|
}
|
|
775
790
|
}
|
|
776
791
|
sendMessage(content, metadata) {
|
|
777
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
778
|
-
if (this.onError) {
|
|
779
|
-
this.onError('Not connected to chat service');
|
|
780
|
-
}
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
792
|
const message = {
|
|
784
793
|
type: 'message',
|
|
785
794
|
content,
|
|
786
795
|
metadata: metadata || {},
|
|
787
796
|
};
|
|
788
|
-
|
|
797
|
+
// If connected, send immediately
|
|
798
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
799
|
+
this.ws.send(JSON.stringify(message));
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
// If disconnected or reconnecting, queue the message
|
|
803
|
+
if (this.messageQueue.length >= this.MAX_QUEUE_SIZE) {
|
|
804
|
+
console.warn('[Wabbit] Message queue full, cannot queue message');
|
|
805
|
+
if (this.onQueueOverflow) {
|
|
806
|
+
this.onQueueOverflow('Message queue full. Please wait for reconnection.');
|
|
807
|
+
}
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
// Add to queue
|
|
811
|
+
this.messageQueue.push({ content, metadata });
|
|
812
|
+
console.log(`[Wabbit] Message queued (${this.messageQueue.length}/${this.MAX_QUEUE_SIZE})`);
|
|
789
813
|
}
|
|
790
814
|
disconnect() {
|
|
791
815
|
if (this.ws) {
|
|
@@ -807,13 +831,51 @@ class ChatWebSocketClient {
|
|
|
807
831
|
this.connect();
|
|
808
832
|
}, delay);
|
|
809
833
|
}
|
|
834
|
+
/**
|
|
835
|
+
* Flush queued messages after connection is established
|
|
836
|
+
*/
|
|
837
|
+
flushMessageQueue() {
|
|
838
|
+
if (this.messageQueue.length === 0) {
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
console.log(`[Wabbit] Flushing ${this.messageQueue.length} queued message(s)`);
|
|
842
|
+
// Send all queued messages in order
|
|
843
|
+
const queuedMessages = [...this.messageQueue];
|
|
844
|
+
this.messageQueue = [];
|
|
845
|
+
queuedMessages.forEach(({ content, metadata }) => {
|
|
846
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
847
|
+
const message = {
|
|
848
|
+
type: 'message',
|
|
849
|
+
content,
|
|
850
|
+
metadata: metadata || {},
|
|
851
|
+
};
|
|
852
|
+
this.ws.send(JSON.stringify(message));
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
}
|
|
810
856
|
clearSession() {
|
|
811
857
|
this.sessionId = null;
|
|
812
858
|
storage.remove('session_id');
|
|
859
|
+
// Clear message queue only when user explicitly starts new session
|
|
860
|
+
// Queue should persist across reconnection attempts
|
|
861
|
+
this.messageQueue = [];
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Clear the message queue
|
|
865
|
+
* Only call this when explicitly abandoning queued messages
|
|
866
|
+
*/
|
|
867
|
+
clearMessageQueue() {
|
|
868
|
+
this.messageQueue = [];
|
|
813
869
|
}
|
|
814
870
|
getStoredSessionId() {
|
|
815
871
|
return storage.get('session_id') || null;
|
|
816
872
|
}
|
|
873
|
+
/**
|
|
874
|
+
* Get current message queue size
|
|
875
|
+
*/
|
|
876
|
+
getQueueSize() {
|
|
877
|
+
return this.messageQueue.length;
|
|
878
|
+
}
|
|
817
879
|
}
|
|
818
880
|
|
|
819
881
|
/**
|
|
@@ -942,6 +1004,9 @@ class ChatPanel {
|
|
|
942
1004
|
this.isWaitingForResponse = false;
|
|
943
1005
|
this.closeButton = null;
|
|
944
1006
|
this.eventCleanup = [];
|
|
1007
|
+
this.streamingMessages = new Map();
|
|
1008
|
+
this.streamingCleanupInterval = null;
|
|
1009
|
+
this.STREAMING_TIMEOUT_MS = 30000; // 30 seconds timeout for stale streams
|
|
945
1010
|
this.options = options;
|
|
946
1011
|
}
|
|
947
1012
|
/**
|
|
@@ -1128,8 +1193,9 @@ class ChatPanel {
|
|
|
1128
1193
|
<div class="wabbit-chat-typing-dot"></div>
|
|
1129
1194
|
`;
|
|
1130
1195
|
this.messagesContainer.appendChild(typing);
|
|
1131
|
-
this.scrollToBottom();
|
|
1132
1196
|
}
|
|
1197
|
+
// Always scroll after changing typing indicator state
|
|
1198
|
+
this.scrollToBottom();
|
|
1133
1199
|
}
|
|
1134
1200
|
this.updateSendButtonState();
|
|
1135
1201
|
}
|
|
@@ -1179,6 +1245,132 @@ class ChatPanel {
|
|
|
1179
1245
|
formatted = formatted.replace(/\n/g, '<br>');
|
|
1180
1246
|
return formatted;
|
|
1181
1247
|
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Start a streaming assistant message
|
|
1250
|
+
*/
|
|
1251
|
+
startStreamingMessage(messageId) {
|
|
1252
|
+
if (!this.messagesContainer)
|
|
1253
|
+
return;
|
|
1254
|
+
// Create message container
|
|
1255
|
+
const messageDiv = createElement('div', {
|
|
1256
|
+
class: 'wabbit-chat-message wabbit-chat-message-assistant wabbit-chat-message-streaming',
|
|
1257
|
+
'data-message-id': messageId,
|
|
1258
|
+
});
|
|
1259
|
+
const bubble = createElement('div', { class: 'wabbit-chat-message-bubble' });
|
|
1260
|
+
const content = createElement('div', { class: 'wabbit-chat-message-content' });
|
|
1261
|
+
// Add streaming cursor
|
|
1262
|
+
content.innerHTML = '<span class="wabbit-chat-cursor">▋</span>';
|
|
1263
|
+
bubble.appendChild(content);
|
|
1264
|
+
messageDiv.appendChild(bubble);
|
|
1265
|
+
this.messagesContainer.appendChild(messageDiv);
|
|
1266
|
+
// Store reference with timestamp for timeout tracking
|
|
1267
|
+
this.streamingMessages.set(messageId, {
|
|
1268
|
+
element: messageDiv,
|
|
1269
|
+
content: '',
|
|
1270
|
+
startTime: Date.now()
|
|
1271
|
+
});
|
|
1272
|
+
// Start cleanup interval if not already running
|
|
1273
|
+
if (!this.streamingCleanupInterval) {
|
|
1274
|
+
this.streamingCleanupInterval = window.setInterval(() => {
|
|
1275
|
+
this.cleanupStaleStreamingMessages();
|
|
1276
|
+
}, 5000); // Check every 5 seconds
|
|
1277
|
+
}
|
|
1278
|
+
this.scrollToBottom();
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Append chunk to streaming message
|
|
1282
|
+
*/
|
|
1283
|
+
appendToStreamingMessage(messageId, chunk) {
|
|
1284
|
+
const streaming = this.streamingMessages.get(messageId);
|
|
1285
|
+
if (!streaming) {
|
|
1286
|
+
console.warn('[ChatPanel] No streaming message found for ID:', messageId);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
// Append to content
|
|
1290
|
+
streaming.content += chunk;
|
|
1291
|
+
// Update DOM
|
|
1292
|
+
const contentDiv = streaming.element.querySelector('.wabbit-chat-message-content');
|
|
1293
|
+
if (contentDiv) {
|
|
1294
|
+
// Format the content and add cursor
|
|
1295
|
+
contentDiv.innerHTML = this.formatMessage(streaming.content) + '<span class="wabbit-chat-cursor">▋</span>';
|
|
1296
|
+
}
|
|
1297
|
+
this.scrollToBottom();
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Finish streaming message
|
|
1301
|
+
*/
|
|
1302
|
+
finishStreamingMessage(messageId, metadata) {
|
|
1303
|
+
const streaming = this.streamingMessages.get(messageId);
|
|
1304
|
+
if (!streaming) {
|
|
1305
|
+
// Don't warn - this is expected if cleanup already removed it or on error
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
// Remove streaming class and cursor
|
|
1309
|
+
streaming.element.classList.remove('wabbit-chat-message-streaming');
|
|
1310
|
+
const contentDiv = streaming.element.querySelector('.wabbit-chat-message-content');
|
|
1311
|
+
if (contentDiv) {
|
|
1312
|
+
// Remove cursor, keep formatted content
|
|
1313
|
+
contentDiv.innerHTML = this.formatMessage(streaming.content);
|
|
1314
|
+
}
|
|
1315
|
+
// Add to messages array
|
|
1316
|
+
const message = {
|
|
1317
|
+
id: messageId,
|
|
1318
|
+
role: 'assistant',
|
|
1319
|
+
content: streaming.content,
|
|
1320
|
+
timestamp: new Date(),
|
|
1321
|
+
metadata,
|
|
1322
|
+
};
|
|
1323
|
+
this.messages.push(message);
|
|
1324
|
+
// Clean up
|
|
1325
|
+
this.streamingMessages.delete(messageId);
|
|
1326
|
+
this.scrollToBottom();
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Cancel a streaming message (e.g., on error)
|
|
1330
|
+
* Cleans up the streaming state without adding to message history
|
|
1331
|
+
*/
|
|
1332
|
+
cancelStreamingMessage(messageId) {
|
|
1333
|
+
const streaming = this.streamingMessages.get(messageId);
|
|
1334
|
+
if (!streaming) {
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
// Remove the streaming message element from DOM
|
|
1338
|
+
streaming.element.remove();
|
|
1339
|
+
// Clean up from map
|
|
1340
|
+
this.streamingMessages.delete(messageId);
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Cancel all active streaming messages
|
|
1344
|
+
* Useful when connection drops or on error
|
|
1345
|
+
*/
|
|
1346
|
+
cancelAllStreamingMessages() {
|
|
1347
|
+
this.streamingMessages.forEach((streaming) => {
|
|
1348
|
+
streaming.element.remove();
|
|
1349
|
+
});
|
|
1350
|
+
this.streamingMessages.clear();
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Cleanup stale streaming messages that have exceeded timeout
|
|
1354
|
+
* Runs periodically to prevent memory leaks from abandoned streams
|
|
1355
|
+
*/
|
|
1356
|
+
cleanupStaleStreamingMessages() {
|
|
1357
|
+
const now = Date.now();
|
|
1358
|
+
const staleIds = [];
|
|
1359
|
+
this.streamingMessages.forEach((streaming, messageId) => {
|
|
1360
|
+
if (now - streaming.startTime > this.STREAMING_TIMEOUT_MS) {
|
|
1361
|
+
console.warn('[ChatPanel] Cleaning up stale streaming message:', messageId);
|
|
1362
|
+
streaming.element.remove();
|
|
1363
|
+
staleIds.push(messageId);
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
// Remove stale entries from map
|
|
1367
|
+
staleIds.forEach(id => this.streamingMessages.delete(id));
|
|
1368
|
+
// Stop cleanup interval if no more streaming messages
|
|
1369
|
+
if (this.streamingMessages.size === 0 && this.streamingCleanupInterval) {
|
|
1370
|
+
clearInterval(this.streamingCleanupInterval);
|
|
1371
|
+
this.streamingCleanupInterval = null;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1182
1374
|
/**
|
|
1183
1375
|
* Handle send message
|
|
1184
1376
|
*/
|
|
@@ -1211,29 +1403,38 @@ class ChatPanel {
|
|
|
1211
1403
|
}
|
|
1212
1404
|
}
|
|
1213
1405
|
/**
|
|
1214
|
-
* Scroll to bottom of messages -
|
|
1406
|
+
* Scroll to bottom of messages - forces scroll with multiple attempts
|
|
1215
1407
|
*/
|
|
1216
1408
|
scrollToBottom() {
|
|
1217
1409
|
if (!this.messagesContainer)
|
|
1218
1410
|
return;
|
|
1219
1411
|
const container = this.messagesContainer;
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1412
|
+
const forceScroll = () => {
|
|
1413
|
+
// Force the container to recognize its scroll height
|
|
1414
|
+
const scrollHeight = container.scrollHeight;
|
|
1415
|
+
const clientHeight = container.clientHeight;
|
|
1416
|
+
// Only scroll if there's content to scroll
|
|
1417
|
+
if (scrollHeight > clientHeight) {
|
|
1418
|
+
container.scrollTop = scrollHeight;
|
|
1419
|
+
}
|
|
1420
|
+
else {
|
|
1421
|
+
// If heights are equal, set to a large value to force bottom
|
|
1422
|
+
container.scrollTop = 999999;
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
// Immediate
|
|
1426
|
+
forceScroll();
|
|
1427
|
+
// After next frame
|
|
1223
1428
|
requestAnimationFrame(() => {
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
requestAnimationFrame(() => {
|
|
1227
|
-
container.scrollTop = container.scrollHeight;
|
|
1228
|
-
});
|
|
1429
|
+
forceScroll();
|
|
1430
|
+
requestAnimationFrame(forceScroll);
|
|
1229
1431
|
});
|
|
1230
|
-
//
|
|
1231
|
-
setTimeout(
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
setTimeout(
|
|
1235
|
-
|
|
1236
|
-
}, 150);
|
|
1432
|
+
// Multiple delayed attempts
|
|
1433
|
+
setTimeout(forceScroll, 10);
|
|
1434
|
+
setTimeout(forceScroll, 50);
|
|
1435
|
+
setTimeout(forceScroll, 100);
|
|
1436
|
+
setTimeout(forceScroll, 200);
|
|
1437
|
+
setTimeout(forceScroll, 500);
|
|
1237
1438
|
}
|
|
1238
1439
|
/**
|
|
1239
1440
|
* Show the panel
|
|
@@ -1254,6 +1455,11 @@ class ChatPanel {
|
|
|
1254
1455
|
if (this.element) {
|
|
1255
1456
|
this.element.style.display = 'none';
|
|
1256
1457
|
}
|
|
1458
|
+
// Stop streaming cleanup interval when hidden to prevent memory leaks
|
|
1459
|
+
if (this.streamingCleanupInterval) {
|
|
1460
|
+
clearInterval(this.streamingCleanupInterval);
|
|
1461
|
+
this.streamingCleanupInterval = null;
|
|
1462
|
+
}
|
|
1257
1463
|
}
|
|
1258
1464
|
/**
|
|
1259
1465
|
* Remove the panel from DOM and cleanup event listeners
|
|
@@ -1262,6 +1468,11 @@ class ChatPanel {
|
|
|
1262
1468
|
// Run all event cleanup functions
|
|
1263
1469
|
this.eventCleanup.forEach((cleanup) => cleanup());
|
|
1264
1470
|
this.eventCleanup = [];
|
|
1471
|
+
// Clear streaming cleanup interval
|
|
1472
|
+
if (this.streamingCleanupInterval) {
|
|
1473
|
+
clearInterval(this.streamingCleanupInterval);
|
|
1474
|
+
this.streamingCleanupInterval = null;
|
|
1475
|
+
}
|
|
1265
1476
|
if (this.element) {
|
|
1266
1477
|
this.element.remove();
|
|
1267
1478
|
this.element = null;
|
|
@@ -1725,6 +1936,24 @@ function injectChatStyles(primaryColor = '#6366f1', theme) {
|
|
|
1725
1936
|
}
|
|
1726
1937
|
}
|
|
1727
1938
|
|
|
1939
|
+
/* Streaming Cursor */
|
|
1940
|
+
.wabbit-chat-cursor {
|
|
1941
|
+
display: inline-block;
|
|
1942
|
+
color: var(--wabbit-primary);
|
|
1943
|
+
font-weight: bold;
|
|
1944
|
+
animation: wabbit-cursor-blink 1s infinite;
|
|
1945
|
+
margin-left: 2px;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
@keyframes wabbit-cursor-blink {
|
|
1949
|
+
0%, 50% {
|
|
1950
|
+
opacity: 1;
|
|
1951
|
+
}
|
|
1952
|
+
51%, 100% {
|
|
1953
|
+
opacity: 0;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1728
1957
|
.wabbit-chat-panel {
|
|
1729
1958
|
animation: wabbit-fade-in 0.3s ease-out;
|
|
1730
1959
|
}
|
|
@@ -1739,24 +1968,25 @@ function injectChatStyles(primaryColor = '#6366f1', theme) {
|
|
|
1739
1968
|
|
|
1740
1969
|
/* Inline Chat Panel - renders inside container instead of fixed position */
|
|
1741
1970
|
.wabbit-chat-panel.wabbit-chat-panel-inline {
|
|
1742
|
-
position:
|
|
1971
|
+
position: absolute !important;
|
|
1972
|
+
top: 0;
|
|
1973
|
+
left: 0;
|
|
1974
|
+
right: 0;
|
|
1975
|
+
bottom: 0;
|
|
1743
1976
|
width: 100%;
|
|
1744
1977
|
height: 100%;
|
|
1745
|
-
min-height: 400px;
|
|
1746
1978
|
max-height: none;
|
|
1747
1979
|
max-width: none;
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
left: auto !important;
|
|
1751
|
-
border-radius: 12px;
|
|
1752
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
1980
|
+
border-radius: 0;
|
|
1981
|
+
box-shadow: none;
|
|
1753
1982
|
animation: none; /* Disable slide-in animation for inline */
|
|
1754
1983
|
}
|
|
1755
1984
|
|
|
1756
1985
|
/* Inline mode messages area fills available space */
|
|
1757
1986
|
.wabbit-chat-panel-inline .wabbit-chat-messages {
|
|
1758
|
-
flex: 1;
|
|
1759
|
-
min-height:
|
|
1987
|
+
flex: 1 1 0; /* flex-grow, flex-shrink, flex-basis: 0 for proper sizing */
|
|
1988
|
+
min-height: 0; /* Critical for nested flex scroll to work */
|
|
1989
|
+
overflow-y: auto !important;
|
|
1760
1990
|
}
|
|
1761
1991
|
|
|
1762
1992
|
/* Full-height variant for inline mode (used in standalone chat pages) */
|
|
@@ -1931,6 +2161,26 @@ class ChatWidget {
|
|
|
1931
2161
|
this.panel.setMessages(messages);
|
|
1932
2162
|
}
|
|
1933
2163
|
};
|
|
2164
|
+
// Streaming message handlers
|
|
2165
|
+
this.wsClient.onMessageStart = (messageId) => {
|
|
2166
|
+
console.log('[Wabbit] Streaming message started:', messageId);
|
|
2167
|
+
if (this.panel) {
|
|
2168
|
+
this.panel.startStreamingMessage(messageId);
|
|
2169
|
+
}
|
|
2170
|
+
};
|
|
2171
|
+
this.wsClient.onMessageChunk = (messageId, chunk) => {
|
|
2172
|
+
if (this.panel) {
|
|
2173
|
+
this.panel.appendToStreamingMessage(messageId, chunk);
|
|
2174
|
+
}
|
|
2175
|
+
};
|
|
2176
|
+
this.wsClient.onMessageEnd = (messageId, metadata) => {
|
|
2177
|
+
console.log('[Wabbit] Streaming message ended:', messageId);
|
|
2178
|
+
if (this.panel) {
|
|
2179
|
+
this.panel.finishStreamingMessage(messageId, metadata);
|
|
2180
|
+
this.panel.setWaitingForResponse(false);
|
|
2181
|
+
}
|
|
2182
|
+
};
|
|
2183
|
+
// Non-streaming message handler (backward compatibility)
|
|
1934
2184
|
this.wsClient.onMessage = (message) => {
|
|
1935
2185
|
if (this.panel) {
|
|
1936
2186
|
this.panel.addMessage(message);
|
|
@@ -1940,6 +2190,8 @@ class ChatWidget {
|
|
|
1940
2190
|
this.wsClient.onError = (error) => {
|
|
1941
2191
|
console.error('[Wabbit] Chat error:', error);
|
|
1942
2192
|
if (this.panel) {
|
|
2193
|
+
// Clean up any active streaming messages on error
|
|
2194
|
+
this.panel.cancelAllStreamingMessages();
|
|
1943
2195
|
this.panel.addSystemMessage(`Error: ${error}`);
|
|
1944
2196
|
this.panel.setWaitingForResponse(false);
|
|
1945
2197
|
}
|
|
@@ -1951,10 +2203,24 @@ class ChatWidget {
|
|
|
1951
2203
|
};
|
|
1952
2204
|
this.wsClient.onDisconnect = () => {
|
|
1953
2205
|
if (this.panel) {
|
|
1954
|
-
|
|
2206
|
+
// Clean up any active streaming messages on disconnect
|
|
2207
|
+
this.panel.cancelAllStreamingMessages();
|
|
2208
|
+
// Check if there are queued messages
|
|
2209
|
+
const queueSize = this.wsClient.getQueueSize();
|
|
2210
|
+
if (queueSize > 0) {
|
|
2211
|
+
this.panel.addSystemMessage(`Disconnected from chat service. ${queueSize} message(s) will be sent when reconnected.`);
|
|
2212
|
+
}
|
|
2213
|
+
else {
|
|
2214
|
+
this.panel.addSystemMessage('Disconnected from chat service');
|
|
2215
|
+
}
|
|
1955
2216
|
this.panel.setDisabled(true);
|
|
1956
2217
|
}
|
|
1957
2218
|
};
|
|
2219
|
+
this.wsClient.onQueueOverflow = (message) => {
|
|
2220
|
+
if (this.panel) {
|
|
2221
|
+
this.panel.addSystemMessage(message);
|
|
2222
|
+
}
|
|
2223
|
+
};
|
|
1958
2224
|
}
|
|
1959
2225
|
/**
|
|
1960
2226
|
* Handle trigger type configuration
|
|
@@ -3744,7 +4010,6 @@ class EmailCaptureWidget {
|
|
|
3744
4010
|
if (this.emailCaptured) {
|
|
3745
4011
|
return;
|
|
3746
4012
|
}
|
|
3747
|
-
// Only count user messages
|
|
3748
4013
|
this.messageCount++;
|
|
3749
4014
|
const triggerAfter = this.config.triggerAfterMessages || 3;
|
|
3750
4015
|
if (this.messageCount >= triggerAfter) {
|