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