@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.cjs.js
CHANGED
|
@@ -642,6 +642,9 @@ class ChatWebSocketClient {
|
|
|
642
642
|
this.reconnectAttempts = 0;
|
|
643
643
|
this.maxReconnectAttempts = 3;
|
|
644
644
|
this.reconnectDelay = 1000;
|
|
645
|
+
// Message queue for handling messages during disconnection
|
|
646
|
+
this.messageQueue = [];
|
|
647
|
+
this.MAX_QUEUE_SIZE = 10;
|
|
645
648
|
// Event handlers
|
|
646
649
|
this.onStatusChange = null;
|
|
647
650
|
this.onWelcome = null;
|
|
@@ -649,6 +652,11 @@ class ChatWebSocketClient {
|
|
|
649
652
|
this.onMessageHistory = null;
|
|
650
653
|
this.onError = null;
|
|
651
654
|
this.onDisconnect = null;
|
|
655
|
+
this.onQueueOverflow = null;
|
|
656
|
+
// Streaming event handlers
|
|
657
|
+
this.onMessageStart = null;
|
|
658
|
+
this.onMessageChunk = null;
|
|
659
|
+
this.onMessageEnd = null;
|
|
652
660
|
this.apiKey = apiKey;
|
|
653
661
|
this.wsUrl = wsUrl;
|
|
654
662
|
this.sessionId = sessionId || this.getStoredSessionId();
|
|
@@ -680,6 +688,8 @@ class ChatWebSocketClient {
|
|
|
680
688
|
this.setStatus('connected');
|
|
681
689
|
this.reconnectAttempts = 0;
|
|
682
690
|
console.log('[Wabbit] WebSocket connected');
|
|
691
|
+
// Flush any queued messages
|
|
692
|
+
this.flushMessageQueue();
|
|
683
693
|
};
|
|
684
694
|
this.ws.onmessage = (event) => {
|
|
685
695
|
try {
|
|
@@ -738,7 +748,26 @@ class ChatWebSocketClient {
|
|
|
738
748
|
this.onMessageHistory(messages);
|
|
739
749
|
}
|
|
740
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;
|
|
741
769
|
case 'assistant_message':
|
|
770
|
+
// Non-streaming: Complete assistant message (backward compatibility)
|
|
742
771
|
if (this.onMessage) {
|
|
743
772
|
const message = {
|
|
744
773
|
id: data.message_id || crypto.randomUUID(),
|
|
@@ -760,18 +789,27 @@ class ChatWebSocketClient {
|
|
|
760
789
|
}
|
|
761
790
|
}
|
|
762
791
|
sendMessage(content, metadata) {
|
|
763
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
764
|
-
if (this.onError) {
|
|
765
|
-
this.onError('Not connected to chat service');
|
|
766
|
-
}
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
792
|
const message = {
|
|
770
793
|
type: 'message',
|
|
771
794
|
content,
|
|
772
795
|
metadata: metadata || {},
|
|
773
796
|
};
|
|
774
|
-
|
|
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})`);
|
|
775
813
|
}
|
|
776
814
|
disconnect() {
|
|
777
815
|
if (this.ws) {
|
|
@@ -793,13 +831,51 @@ class ChatWebSocketClient {
|
|
|
793
831
|
this.connect();
|
|
794
832
|
}, delay);
|
|
795
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
|
+
}
|
|
796
856
|
clearSession() {
|
|
797
857
|
this.sessionId = null;
|
|
798
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 = [];
|
|
799
869
|
}
|
|
800
870
|
getStoredSessionId() {
|
|
801
871
|
return storage.get('session_id') || null;
|
|
802
872
|
}
|
|
873
|
+
/**
|
|
874
|
+
* Get current message queue size
|
|
875
|
+
*/
|
|
876
|
+
getQueueSize() {
|
|
877
|
+
return this.messageQueue.length;
|
|
878
|
+
}
|
|
803
879
|
}
|
|
804
880
|
|
|
805
881
|
/**
|
|
@@ -928,6 +1004,9 @@ class ChatPanel {
|
|
|
928
1004
|
this.isWaitingForResponse = false;
|
|
929
1005
|
this.closeButton = null;
|
|
930
1006
|
this.eventCleanup = [];
|
|
1007
|
+
this.streamingMessages = new Map();
|
|
1008
|
+
this.streamingCleanupInterval = null;
|
|
1009
|
+
this.STREAMING_TIMEOUT_MS = 30000; // 30 seconds timeout for stale streams
|
|
931
1010
|
this.options = options;
|
|
932
1011
|
}
|
|
933
1012
|
/**
|
|
@@ -1166,6 +1245,132 @@ class ChatPanel {
|
|
|
1166
1245
|
formatted = formatted.replace(/\n/g, '<br>');
|
|
1167
1246
|
return formatted;
|
|
1168
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
|
+
}
|
|
1169
1374
|
/**
|
|
1170
1375
|
* Handle send message
|
|
1171
1376
|
*/
|
|
@@ -1250,6 +1455,11 @@ class ChatPanel {
|
|
|
1250
1455
|
if (this.element) {
|
|
1251
1456
|
this.element.style.display = 'none';
|
|
1252
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
|
+
}
|
|
1253
1463
|
}
|
|
1254
1464
|
/**
|
|
1255
1465
|
* Remove the panel from DOM and cleanup event listeners
|
|
@@ -1258,6 +1468,11 @@ class ChatPanel {
|
|
|
1258
1468
|
// Run all event cleanup functions
|
|
1259
1469
|
this.eventCleanup.forEach((cleanup) => cleanup());
|
|
1260
1470
|
this.eventCleanup = [];
|
|
1471
|
+
// Clear streaming cleanup interval
|
|
1472
|
+
if (this.streamingCleanupInterval) {
|
|
1473
|
+
clearInterval(this.streamingCleanupInterval);
|
|
1474
|
+
this.streamingCleanupInterval = null;
|
|
1475
|
+
}
|
|
1261
1476
|
if (this.element) {
|
|
1262
1477
|
this.element.remove();
|
|
1263
1478
|
this.element = null;
|
|
@@ -1721,6 +1936,24 @@ function injectChatStyles(primaryColor = '#6366f1', theme) {
|
|
|
1721
1936
|
}
|
|
1722
1937
|
}
|
|
1723
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
|
+
|
|
1724
1957
|
.wabbit-chat-panel {
|
|
1725
1958
|
animation: wabbit-fade-in 0.3s ease-out;
|
|
1726
1959
|
}
|
|
@@ -1735,17 +1968,17 @@ function injectChatStyles(primaryColor = '#6366f1', theme) {
|
|
|
1735
1968
|
|
|
1736
1969
|
/* Inline Chat Panel - renders inside container instead of fixed position */
|
|
1737
1970
|
.wabbit-chat-panel.wabbit-chat-panel-inline {
|
|
1738
|
-
position:
|
|
1971
|
+
position: absolute !important;
|
|
1972
|
+
top: 0;
|
|
1973
|
+
left: 0;
|
|
1974
|
+
right: 0;
|
|
1975
|
+
bottom: 0;
|
|
1739
1976
|
width: 100%;
|
|
1740
1977
|
height: 100%;
|
|
1741
|
-
min-height: 400px;
|
|
1742
1978
|
max-height: none;
|
|
1743
1979
|
max-width: none;
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
left: auto !important;
|
|
1747
|
-
border-radius: 12px;
|
|
1748
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
1980
|
+
border-radius: 0;
|
|
1981
|
+
box-shadow: none;
|
|
1749
1982
|
animation: none; /* Disable slide-in animation for inline */
|
|
1750
1983
|
}
|
|
1751
1984
|
|
|
@@ -1928,6 +2161,26 @@ class ChatWidget {
|
|
|
1928
2161
|
this.panel.setMessages(messages);
|
|
1929
2162
|
}
|
|
1930
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)
|
|
1931
2184
|
this.wsClient.onMessage = (message) => {
|
|
1932
2185
|
if (this.panel) {
|
|
1933
2186
|
this.panel.addMessage(message);
|
|
@@ -1937,6 +2190,8 @@ class ChatWidget {
|
|
|
1937
2190
|
this.wsClient.onError = (error) => {
|
|
1938
2191
|
console.error('[Wabbit] Chat error:', error);
|
|
1939
2192
|
if (this.panel) {
|
|
2193
|
+
// Clean up any active streaming messages on error
|
|
2194
|
+
this.panel.cancelAllStreamingMessages();
|
|
1940
2195
|
this.panel.addSystemMessage(`Error: ${error}`);
|
|
1941
2196
|
this.panel.setWaitingForResponse(false);
|
|
1942
2197
|
}
|
|
@@ -1948,10 +2203,24 @@ class ChatWidget {
|
|
|
1948
2203
|
};
|
|
1949
2204
|
this.wsClient.onDisconnect = () => {
|
|
1950
2205
|
if (this.panel) {
|
|
1951
|
-
|
|
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
|
+
}
|
|
1952
2216
|
this.panel.setDisabled(true);
|
|
1953
2217
|
}
|
|
1954
2218
|
};
|
|
2219
|
+
this.wsClient.onQueueOverflow = (message) => {
|
|
2220
|
+
if (this.panel) {
|
|
2221
|
+
this.panel.addSystemMessage(message);
|
|
2222
|
+
}
|
|
2223
|
+
};
|
|
1955
2224
|
}
|
|
1956
2225
|
/**
|
|
1957
2226
|
* Handle trigger type configuration
|