@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.
@@ -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
- this.ws.send(JSON.stringify(message));
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: relative !important;
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
- bottom: auto !important;
1741
- right: auto !important;
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
- this.panel.addSystemMessage('Disconnected from chat service');
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