@wabbit-dashboard/embed 1.0.17 → 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 +284 -15
- package/dist/wabbit-embed.cjs.js.map +1 -1
- package/dist/wabbit-embed.d.ts +49 -0
- package/dist/wabbit-embed.esm.js +284 -15
- package/dist/wabbit-embed.esm.js.map +1 -1
- package/dist/wabbit-embed.umd.js +284 -15
- 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.esm.js
CHANGED
|
@@ -638,6 +638,9 @@ class ChatWebSocketClient {
|
|
|
638
638
|
this.reconnectAttempts = 0;
|
|
639
639
|
this.maxReconnectAttempts = 3;
|
|
640
640
|
this.reconnectDelay = 1000;
|
|
641
|
+
// Message queue for handling messages during disconnection
|
|
642
|
+
this.messageQueue = [];
|
|
643
|
+
this.MAX_QUEUE_SIZE = 10;
|
|
641
644
|
// Event handlers
|
|
642
645
|
this.onStatusChange = null;
|
|
643
646
|
this.onWelcome = null;
|
|
@@ -645,6 +648,11 @@ class ChatWebSocketClient {
|
|
|
645
648
|
this.onMessageHistory = null;
|
|
646
649
|
this.onError = null;
|
|
647
650
|
this.onDisconnect = null;
|
|
651
|
+
this.onQueueOverflow = null;
|
|
652
|
+
// Streaming event handlers
|
|
653
|
+
this.onMessageStart = null;
|
|
654
|
+
this.onMessageChunk = null;
|
|
655
|
+
this.onMessageEnd = null;
|
|
648
656
|
this.apiKey = apiKey;
|
|
649
657
|
this.wsUrl = wsUrl;
|
|
650
658
|
this.sessionId = sessionId || this.getStoredSessionId();
|
|
@@ -676,6 +684,8 @@ class ChatWebSocketClient {
|
|
|
676
684
|
this.setStatus('connected');
|
|
677
685
|
this.reconnectAttempts = 0;
|
|
678
686
|
console.log('[Wabbit] WebSocket connected');
|
|
687
|
+
// Flush any queued messages
|
|
688
|
+
this.flushMessageQueue();
|
|
679
689
|
};
|
|
680
690
|
this.ws.onmessage = (event) => {
|
|
681
691
|
try {
|
|
@@ -734,7 +744,26 @@ class ChatWebSocketClient {
|
|
|
734
744
|
this.onMessageHistory(messages);
|
|
735
745
|
}
|
|
736
746
|
break;
|
|
747
|
+
case 'assistant_message_start':
|
|
748
|
+
// Streaming: Start of assistant message
|
|
749
|
+
if (this.onMessageStart) {
|
|
750
|
+
this.onMessageStart(data.message_id);
|
|
751
|
+
}
|
|
752
|
+
break;
|
|
753
|
+
case 'assistant_message_chunk':
|
|
754
|
+
// Streaming: Chunk of assistant message
|
|
755
|
+
if (this.onMessageChunk) {
|
|
756
|
+
this.onMessageChunk(data.message_id, data.content);
|
|
757
|
+
}
|
|
758
|
+
break;
|
|
759
|
+
case 'assistant_message_end':
|
|
760
|
+
// Streaming: End of assistant message
|
|
761
|
+
if (this.onMessageEnd) {
|
|
762
|
+
this.onMessageEnd(data.message_id, data.metadata);
|
|
763
|
+
}
|
|
764
|
+
break;
|
|
737
765
|
case 'assistant_message':
|
|
766
|
+
// Non-streaming: Complete assistant message (backward compatibility)
|
|
738
767
|
if (this.onMessage) {
|
|
739
768
|
const message = {
|
|
740
769
|
id: data.message_id || crypto.randomUUID(),
|
|
@@ -756,18 +785,27 @@ class ChatWebSocketClient {
|
|
|
756
785
|
}
|
|
757
786
|
}
|
|
758
787
|
sendMessage(content, metadata) {
|
|
759
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
760
|
-
if (this.onError) {
|
|
761
|
-
this.onError('Not connected to chat service');
|
|
762
|
-
}
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
765
788
|
const message = {
|
|
766
789
|
type: 'message',
|
|
767
790
|
content,
|
|
768
791
|
metadata: metadata || {},
|
|
769
792
|
};
|
|
770
|
-
|
|
793
|
+
// If connected, send immediately
|
|
794
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
795
|
+
this.ws.send(JSON.stringify(message));
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
// If disconnected or reconnecting, queue the message
|
|
799
|
+
if (this.messageQueue.length >= this.MAX_QUEUE_SIZE) {
|
|
800
|
+
console.warn('[Wabbit] Message queue full, cannot queue message');
|
|
801
|
+
if (this.onQueueOverflow) {
|
|
802
|
+
this.onQueueOverflow('Message queue full. Please wait for reconnection.');
|
|
803
|
+
}
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
// Add to queue
|
|
807
|
+
this.messageQueue.push({ content, metadata });
|
|
808
|
+
console.log(`[Wabbit] Message queued (${this.messageQueue.length}/${this.MAX_QUEUE_SIZE})`);
|
|
771
809
|
}
|
|
772
810
|
disconnect() {
|
|
773
811
|
if (this.ws) {
|
|
@@ -789,13 +827,51 @@ class ChatWebSocketClient {
|
|
|
789
827
|
this.connect();
|
|
790
828
|
}, delay);
|
|
791
829
|
}
|
|
830
|
+
/**
|
|
831
|
+
* Flush queued messages after connection is established
|
|
832
|
+
*/
|
|
833
|
+
flushMessageQueue() {
|
|
834
|
+
if (this.messageQueue.length === 0) {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
console.log(`[Wabbit] Flushing ${this.messageQueue.length} queued message(s)`);
|
|
838
|
+
// Send all queued messages in order
|
|
839
|
+
const queuedMessages = [...this.messageQueue];
|
|
840
|
+
this.messageQueue = [];
|
|
841
|
+
queuedMessages.forEach(({ content, metadata }) => {
|
|
842
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
843
|
+
const message = {
|
|
844
|
+
type: 'message',
|
|
845
|
+
content,
|
|
846
|
+
metadata: metadata || {},
|
|
847
|
+
};
|
|
848
|
+
this.ws.send(JSON.stringify(message));
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
}
|
|
792
852
|
clearSession() {
|
|
793
853
|
this.sessionId = null;
|
|
794
854
|
storage.remove('session_id');
|
|
855
|
+
// Clear message queue only when user explicitly starts new session
|
|
856
|
+
// Queue should persist across reconnection attempts
|
|
857
|
+
this.messageQueue = [];
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Clear the message queue
|
|
861
|
+
* Only call this when explicitly abandoning queued messages
|
|
862
|
+
*/
|
|
863
|
+
clearMessageQueue() {
|
|
864
|
+
this.messageQueue = [];
|
|
795
865
|
}
|
|
796
866
|
getStoredSessionId() {
|
|
797
867
|
return storage.get('session_id') || null;
|
|
798
868
|
}
|
|
869
|
+
/**
|
|
870
|
+
* Get current message queue size
|
|
871
|
+
*/
|
|
872
|
+
getQueueSize() {
|
|
873
|
+
return this.messageQueue.length;
|
|
874
|
+
}
|
|
799
875
|
}
|
|
800
876
|
|
|
801
877
|
/**
|
|
@@ -924,6 +1000,9 @@ class ChatPanel {
|
|
|
924
1000
|
this.isWaitingForResponse = false;
|
|
925
1001
|
this.closeButton = null;
|
|
926
1002
|
this.eventCleanup = [];
|
|
1003
|
+
this.streamingMessages = new Map();
|
|
1004
|
+
this.streamingCleanupInterval = null;
|
|
1005
|
+
this.STREAMING_TIMEOUT_MS = 30000; // 30 seconds timeout for stale streams
|
|
927
1006
|
this.options = options;
|
|
928
1007
|
}
|
|
929
1008
|
/**
|
|
@@ -1162,6 +1241,132 @@ class ChatPanel {
|
|
|
1162
1241
|
formatted = formatted.replace(/\n/g, '<br>');
|
|
1163
1242
|
return formatted;
|
|
1164
1243
|
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Start a streaming assistant message
|
|
1246
|
+
*/
|
|
1247
|
+
startStreamingMessage(messageId) {
|
|
1248
|
+
if (!this.messagesContainer)
|
|
1249
|
+
return;
|
|
1250
|
+
// Create message container
|
|
1251
|
+
const messageDiv = createElement('div', {
|
|
1252
|
+
class: 'wabbit-chat-message wabbit-chat-message-assistant wabbit-chat-message-streaming',
|
|
1253
|
+
'data-message-id': messageId,
|
|
1254
|
+
});
|
|
1255
|
+
const bubble = createElement('div', { class: 'wabbit-chat-message-bubble' });
|
|
1256
|
+
const content = createElement('div', { class: 'wabbit-chat-message-content' });
|
|
1257
|
+
// Add streaming cursor
|
|
1258
|
+
content.innerHTML = '<span class="wabbit-chat-cursor">▋</span>';
|
|
1259
|
+
bubble.appendChild(content);
|
|
1260
|
+
messageDiv.appendChild(bubble);
|
|
1261
|
+
this.messagesContainer.appendChild(messageDiv);
|
|
1262
|
+
// Store reference with timestamp for timeout tracking
|
|
1263
|
+
this.streamingMessages.set(messageId, {
|
|
1264
|
+
element: messageDiv,
|
|
1265
|
+
content: '',
|
|
1266
|
+
startTime: Date.now()
|
|
1267
|
+
});
|
|
1268
|
+
// Start cleanup interval if not already running
|
|
1269
|
+
if (!this.streamingCleanupInterval) {
|
|
1270
|
+
this.streamingCleanupInterval = window.setInterval(() => {
|
|
1271
|
+
this.cleanupStaleStreamingMessages();
|
|
1272
|
+
}, 5000); // Check every 5 seconds
|
|
1273
|
+
}
|
|
1274
|
+
this.scrollToBottom();
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Append chunk to streaming message
|
|
1278
|
+
*/
|
|
1279
|
+
appendToStreamingMessage(messageId, chunk) {
|
|
1280
|
+
const streaming = this.streamingMessages.get(messageId);
|
|
1281
|
+
if (!streaming) {
|
|
1282
|
+
console.warn('[ChatPanel] No streaming message found for ID:', messageId);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
// Append to content
|
|
1286
|
+
streaming.content += chunk;
|
|
1287
|
+
// Update DOM
|
|
1288
|
+
const contentDiv = streaming.element.querySelector('.wabbit-chat-message-content');
|
|
1289
|
+
if (contentDiv) {
|
|
1290
|
+
// Format the content and add cursor
|
|
1291
|
+
contentDiv.innerHTML = this.formatMessage(streaming.content) + '<span class="wabbit-chat-cursor">▋</span>';
|
|
1292
|
+
}
|
|
1293
|
+
this.scrollToBottom();
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Finish streaming message
|
|
1297
|
+
*/
|
|
1298
|
+
finishStreamingMessage(messageId, metadata) {
|
|
1299
|
+
const streaming = this.streamingMessages.get(messageId);
|
|
1300
|
+
if (!streaming) {
|
|
1301
|
+
// Don't warn - this is expected if cleanup already removed it or on error
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
// Remove streaming class and cursor
|
|
1305
|
+
streaming.element.classList.remove('wabbit-chat-message-streaming');
|
|
1306
|
+
const contentDiv = streaming.element.querySelector('.wabbit-chat-message-content');
|
|
1307
|
+
if (contentDiv) {
|
|
1308
|
+
// Remove cursor, keep formatted content
|
|
1309
|
+
contentDiv.innerHTML = this.formatMessage(streaming.content);
|
|
1310
|
+
}
|
|
1311
|
+
// Add to messages array
|
|
1312
|
+
const message = {
|
|
1313
|
+
id: messageId,
|
|
1314
|
+
role: 'assistant',
|
|
1315
|
+
content: streaming.content,
|
|
1316
|
+
timestamp: new Date(),
|
|
1317
|
+
metadata,
|
|
1318
|
+
};
|
|
1319
|
+
this.messages.push(message);
|
|
1320
|
+
// Clean up
|
|
1321
|
+
this.streamingMessages.delete(messageId);
|
|
1322
|
+
this.scrollToBottom();
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Cancel a streaming message (e.g., on error)
|
|
1326
|
+
* Cleans up the streaming state without adding to message history
|
|
1327
|
+
*/
|
|
1328
|
+
cancelStreamingMessage(messageId) {
|
|
1329
|
+
const streaming = this.streamingMessages.get(messageId);
|
|
1330
|
+
if (!streaming) {
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
// Remove the streaming message element from DOM
|
|
1334
|
+
streaming.element.remove();
|
|
1335
|
+
// Clean up from map
|
|
1336
|
+
this.streamingMessages.delete(messageId);
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Cancel all active streaming messages
|
|
1340
|
+
* Useful when connection drops or on error
|
|
1341
|
+
*/
|
|
1342
|
+
cancelAllStreamingMessages() {
|
|
1343
|
+
this.streamingMessages.forEach((streaming) => {
|
|
1344
|
+
streaming.element.remove();
|
|
1345
|
+
});
|
|
1346
|
+
this.streamingMessages.clear();
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Cleanup stale streaming messages that have exceeded timeout
|
|
1350
|
+
* Runs periodically to prevent memory leaks from abandoned streams
|
|
1351
|
+
*/
|
|
1352
|
+
cleanupStaleStreamingMessages() {
|
|
1353
|
+
const now = Date.now();
|
|
1354
|
+
const staleIds = [];
|
|
1355
|
+
this.streamingMessages.forEach((streaming, messageId) => {
|
|
1356
|
+
if (now - streaming.startTime > this.STREAMING_TIMEOUT_MS) {
|
|
1357
|
+
console.warn('[ChatPanel] Cleaning up stale streaming message:', messageId);
|
|
1358
|
+
streaming.element.remove();
|
|
1359
|
+
staleIds.push(messageId);
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
// Remove stale entries from map
|
|
1363
|
+
staleIds.forEach(id => this.streamingMessages.delete(id));
|
|
1364
|
+
// Stop cleanup interval if no more streaming messages
|
|
1365
|
+
if (this.streamingMessages.size === 0 && this.streamingCleanupInterval) {
|
|
1366
|
+
clearInterval(this.streamingCleanupInterval);
|
|
1367
|
+
this.streamingCleanupInterval = null;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1165
1370
|
/**
|
|
1166
1371
|
* Handle send message
|
|
1167
1372
|
*/
|
|
@@ -1246,6 +1451,11 @@ class ChatPanel {
|
|
|
1246
1451
|
if (this.element) {
|
|
1247
1452
|
this.element.style.display = 'none';
|
|
1248
1453
|
}
|
|
1454
|
+
// Stop streaming cleanup interval when hidden to prevent memory leaks
|
|
1455
|
+
if (this.streamingCleanupInterval) {
|
|
1456
|
+
clearInterval(this.streamingCleanupInterval);
|
|
1457
|
+
this.streamingCleanupInterval = null;
|
|
1458
|
+
}
|
|
1249
1459
|
}
|
|
1250
1460
|
/**
|
|
1251
1461
|
* Remove the panel from DOM and cleanup event listeners
|
|
@@ -1254,6 +1464,11 @@ class ChatPanel {
|
|
|
1254
1464
|
// Run all event cleanup functions
|
|
1255
1465
|
this.eventCleanup.forEach((cleanup) => cleanup());
|
|
1256
1466
|
this.eventCleanup = [];
|
|
1467
|
+
// Clear streaming cleanup interval
|
|
1468
|
+
if (this.streamingCleanupInterval) {
|
|
1469
|
+
clearInterval(this.streamingCleanupInterval);
|
|
1470
|
+
this.streamingCleanupInterval = null;
|
|
1471
|
+
}
|
|
1257
1472
|
if (this.element) {
|
|
1258
1473
|
this.element.remove();
|
|
1259
1474
|
this.element = null;
|
|
@@ -1717,6 +1932,24 @@ function injectChatStyles(primaryColor = '#6366f1', theme) {
|
|
|
1717
1932
|
}
|
|
1718
1933
|
}
|
|
1719
1934
|
|
|
1935
|
+
/* Streaming Cursor */
|
|
1936
|
+
.wabbit-chat-cursor {
|
|
1937
|
+
display: inline-block;
|
|
1938
|
+
color: var(--wabbit-primary);
|
|
1939
|
+
font-weight: bold;
|
|
1940
|
+
animation: wabbit-cursor-blink 1s infinite;
|
|
1941
|
+
margin-left: 2px;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
@keyframes wabbit-cursor-blink {
|
|
1945
|
+
0%, 50% {
|
|
1946
|
+
opacity: 1;
|
|
1947
|
+
}
|
|
1948
|
+
51%, 100% {
|
|
1949
|
+
opacity: 0;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1720
1953
|
.wabbit-chat-panel {
|
|
1721
1954
|
animation: wabbit-fade-in 0.3s ease-out;
|
|
1722
1955
|
}
|
|
@@ -1731,17 +1964,17 @@ function injectChatStyles(primaryColor = '#6366f1', theme) {
|
|
|
1731
1964
|
|
|
1732
1965
|
/* Inline Chat Panel - renders inside container instead of fixed position */
|
|
1733
1966
|
.wabbit-chat-panel.wabbit-chat-panel-inline {
|
|
1734
|
-
position:
|
|
1967
|
+
position: absolute !important;
|
|
1968
|
+
top: 0;
|
|
1969
|
+
left: 0;
|
|
1970
|
+
right: 0;
|
|
1971
|
+
bottom: 0;
|
|
1735
1972
|
width: 100%;
|
|
1736
1973
|
height: 100%;
|
|
1737
|
-
min-height: 400px;
|
|
1738
1974
|
max-height: none;
|
|
1739
1975
|
max-width: none;
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
left: auto !important;
|
|
1743
|
-
border-radius: 12px;
|
|
1744
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
1976
|
+
border-radius: 0;
|
|
1977
|
+
box-shadow: none;
|
|
1745
1978
|
animation: none; /* Disable slide-in animation for inline */
|
|
1746
1979
|
}
|
|
1747
1980
|
|
|
@@ -1924,6 +2157,26 @@ class ChatWidget {
|
|
|
1924
2157
|
this.panel.setMessages(messages);
|
|
1925
2158
|
}
|
|
1926
2159
|
};
|
|
2160
|
+
// Streaming message handlers
|
|
2161
|
+
this.wsClient.onMessageStart = (messageId) => {
|
|
2162
|
+
console.log('[Wabbit] Streaming message started:', messageId);
|
|
2163
|
+
if (this.panel) {
|
|
2164
|
+
this.panel.startStreamingMessage(messageId);
|
|
2165
|
+
}
|
|
2166
|
+
};
|
|
2167
|
+
this.wsClient.onMessageChunk = (messageId, chunk) => {
|
|
2168
|
+
if (this.panel) {
|
|
2169
|
+
this.panel.appendToStreamingMessage(messageId, chunk);
|
|
2170
|
+
}
|
|
2171
|
+
};
|
|
2172
|
+
this.wsClient.onMessageEnd = (messageId, metadata) => {
|
|
2173
|
+
console.log('[Wabbit] Streaming message ended:', messageId);
|
|
2174
|
+
if (this.panel) {
|
|
2175
|
+
this.panel.finishStreamingMessage(messageId, metadata);
|
|
2176
|
+
this.panel.setWaitingForResponse(false);
|
|
2177
|
+
}
|
|
2178
|
+
};
|
|
2179
|
+
// Non-streaming message handler (backward compatibility)
|
|
1927
2180
|
this.wsClient.onMessage = (message) => {
|
|
1928
2181
|
if (this.panel) {
|
|
1929
2182
|
this.panel.addMessage(message);
|
|
@@ -1933,6 +2186,8 @@ class ChatWidget {
|
|
|
1933
2186
|
this.wsClient.onError = (error) => {
|
|
1934
2187
|
console.error('[Wabbit] Chat error:', error);
|
|
1935
2188
|
if (this.panel) {
|
|
2189
|
+
// Clean up any active streaming messages on error
|
|
2190
|
+
this.panel.cancelAllStreamingMessages();
|
|
1936
2191
|
this.panel.addSystemMessage(`Error: ${error}`);
|
|
1937
2192
|
this.panel.setWaitingForResponse(false);
|
|
1938
2193
|
}
|
|
@@ -1944,10 +2199,24 @@ class ChatWidget {
|
|
|
1944
2199
|
};
|
|
1945
2200
|
this.wsClient.onDisconnect = () => {
|
|
1946
2201
|
if (this.panel) {
|
|
1947
|
-
|
|
2202
|
+
// Clean up any active streaming messages on disconnect
|
|
2203
|
+
this.panel.cancelAllStreamingMessages();
|
|
2204
|
+
// Check if there are queued messages
|
|
2205
|
+
const queueSize = this.wsClient.getQueueSize();
|
|
2206
|
+
if (queueSize > 0) {
|
|
2207
|
+
this.panel.addSystemMessage(`Disconnected from chat service. ${queueSize} message(s) will be sent when reconnected.`);
|
|
2208
|
+
}
|
|
2209
|
+
else {
|
|
2210
|
+
this.panel.addSystemMessage('Disconnected from chat service');
|
|
2211
|
+
}
|
|
1948
2212
|
this.panel.setDisabled(true);
|
|
1949
2213
|
}
|
|
1950
2214
|
};
|
|
2215
|
+
this.wsClient.onQueueOverflow = (message) => {
|
|
2216
|
+
if (this.panel) {
|
|
2217
|
+
this.panel.addSystemMessage(message);
|
|
2218
|
+
}
|
|
2219
|
+
};
|
|
1951
2220
|
}
|
|
1952
2221
|
/**
|
|
1953
2222
|
* Handle trigger type configuration
|