@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.
@@ -644,6 +644,9 @@
644
644
  this.reconnectAttempts = 0;
645
645
  this.maxReconnectAttempts = 3;
646
646
  this.reconnectDelay = 1000;
647
+ // Message queue for handling messages during disconnection
648
+ this.messageQueue = [];
649
+ this.MAX_QUEUE_SIZE = 10;
647
650
  // Event handlers
648
651
  this.onStatusChange = null;
649
652
  this.onWelcome = null;
@@ -651,6 +654,11 @@
651
654
  this.onMessageHistory = null;
652
655
  this.onError = null;
653
656
  this.onDisconnect = null;
657
+ this.onQueueOverflow = null;
658
+ // Streaming event handlers
659
+ this.onMessageStart = null;
660
+ this.onMessageChunk = null;
661
+ this.onMessageEnd = null;
654
662
  this.apiKey = apiKey;
655
663
  this.wsUrl = wsUrl;
656
664
  this.sessionId = sessionId || this.getStoredSessionId();
@@ -682,6 +690,8 @@
682
690
  this.setStatus('connected');
683
691
  this.reconnectAttempts = 0;
684
692
  console.log('[Wabbit] WebSocket connected');
693
+ // Flush any queued messages
694
+ this.flushMessageQueue();
685
695
  };
686
696
  this.ws.onmessage = (event) => {
687
697
  try {
@@ -740,7 +750,26 @@
740
750
  this.onMessageHistory(messages);
741
751
  }
742
752
  break;
753
+ case 'assistant_message_start':
754
+ // Streaming: Start of assistant message
755
+ if (this.onMessageStart) {
756
+ this.onMessageStart(data.message_id);
757
+ }
758
+ break;
759
+ case 'assistant_message_chunk':
760
+ // Streaming: Chunk of assistant message
761
+ if (this.onMessageChunk) {
762
+ this.onMessageChunk(data.message_id, data.content);
763
+ }
764
+ break;
765
+ case 'assistant_message_end':
766
+ // Streaming: End of assistant message
767
+ if (this.onMessageEnd) {
768
+ this.onMessageEnd(data.message_id, data.metadata);
769
+ }
770
+ break;
743
771
  case 'assistant_message':
772
+ // Non-streaming: Complete assistant message (backward compatibility)
744
773
  if (this.onMessage) {
745
774
  const message = {
746
775
  id: data.message_id || crypto.randomUUID(),
@@ -762,18 +791,27 @@
762
791
  }
763
792
  }
764
793
  sendMessage(content, metadata) {
765
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
766
- if (this.onError) {
767
- this.onError('Not connected to chat service');
768
- }
769
- return;
770
- }
771
794
  const message = {
772
795
  type: 'message',
773
796
  content,
774
797
  metadata: metadata || {},
775
798
  };
776
- this.ws.send(JSON.stringify(message));
799
+ // If connected, send immediately
800
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
801
+ this.ws.send(JSON.stringify(message));
802
+ return;
803
+ }
804
+ // If disconnected or reconnecting, queue the message
805
+ if (this.messageQueue.length >= this.MAX_QUEUE_SIZE) {
806
+ console.warn('[Wabbit] Message queue full, cannot queue message');
807
+ if (this.onQueueOverflow) {
808
+ this.onQueueOverflow('Message queue full. Please wait for reconnection.');
809
+ }
810
+ return;
811
+ }
812
+ // Add to queue
813
+ this.messageQueue.push({ content, metadata });
814
+ console.log(`[Wabbit] Message queued (${this.messageQueue.length}/${this.MAX_QUEUE_SIZE})`);
777
815
  }
778
816
  disconnect() {
779
817
  if (this.ws) {
@@ -795,13 +833,51 @@
795
833
  this.connect();
796
834
  }, delay);
797
835
  }
836
+ /**
837
+ * Flush queued messages after connection is established
838
+ */
839
+ flushMessageQueue() {
840
+ if (this.messageQueue.length === 0) {
841
+ return;
842
+ }
843
+ console.log(`[Wabbit] Flushing ${this.messageQueue.length} queued message(s)`);
844
+ // Send all queued messages in order
845
+ const queuedMessages = [...this.messageQueue];
846
+ this.messageQueue = [];
847
+ queuedMessages.forEach(({ content, metadata }) => {
848
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
849
+ const message = {
850
+ type: 'message',
851
+ content,
852
+ metadata: metadata || {},
853
+ };
854
+ this.ws.send(JSON.stringify(message));
855
+ }
856
+ });
857
+ }
798
858
  clearSession() {
799
859
  this.sessionId = null;
800
860
  storage.remove('session_id');
861
+ // Clear message queue only when user explicitly starts new session
862
+ // Queue should persist across reconnection attempts
863
+ this.messageQueue = [];
864
+ }
865
+ /**
866
+ * Clear the message queue
867
+ * Only call this when explicitly abandoning queued messages
868
+ */
869
+ clearMessageQueue() {
870
+ this.messageQueue = [];
801
871
  }
802
872
  getStoredSessionId() {
803
873
  return storage.get('session_id') || null;
804
874
  }
875
+ /**
876
+ * Get current message queue size
877
+ */
878
+ getQueueSize() {
879
+ return this.messageQueue.length;
880
+ }
805
881
  }
806
882
 
807
883
  /**
@@ -930,6 +1006,9 @@
930
1006
  this.isWaitingForResponse = false;
931
1007
  this.closeButton = null;
932
1008
  this.eventCleanup = [];
1009
+ this.streamingMessages = new Map();
1010
+ this.streamingCleanupInterval = null;
1011
+ this.STREAMING_TIMEOUT_MS = 30000; // 30 seconds timeout for stale streams
933
1012
  this.options = options;
934
1013
  }
935
1014
  /**
@@ -1168,6 +1247,132 @@
1168
1247
  formatted = formatted.replace(/\n/g, '<br>');
1169
1248
  return formatted;
1170
1249
  }
1250
+ /**
1251
+ * Start a streaming assistant message
1252
+ */
1253
+ startStreamingMessage(messageId) {
1254
+ if (!this.messagesContainer)
1255
+ return;
1256
+ // Create message container
1257
+ const messageDiv = createElement('div', {
1258
+ class: 'wabbit-chat-message wabbit-chat-message-assistant wabbit-chat-message-streaming',
1259
+ 'data-message-id': messageId,
1260
+ });
1261
+ const bubble = createElement('div', { class: 'wabbit-chat-message-bubble' });
1262
+ const content = createElement('div', { class: 'wabbit-chat-message-content' });
1263
+ // Add streaming cursor
1264
+ content.innerHTML = '<span class="wabbit-chat-cursor">▋</span>';
1265
+ bubble.appendChild(content);
1266
+ messageDiv.appendChild(bubble);
1267
+ this.messagesContainer.appendChild(messageDiv);
1268
+ // Store reference with timestamp for timeout tracking
1269
+ this.streamingMessages.set(messageId, {
1270
+ element: messageDiv,
1271
+ content: '',
1272
+ startTime: Date.now()
1273
+ });
1274
+ // Start cleanup interval if not already running
1275
+ if (!this.streamingCleanupInterval) {
1276
+ this.streamingCleanupInterval = window.setInterval(() => {
1277
+ this.cleanupStaleStreamingMessages();
1278
+ }, 5000); // Check every 5 seconds
1279
+ }
1280
+ this.scrollToBottom();
1281
+ }
1282
+ /**
1283
+ * Append chunk to streaming message
1284
+ */
1285
+ appendToStreamingMessage(messageId, chunk) {
1286
+ const streaming = this.streamingMessages.get(messageId);
1287
+ if (!streaming) {
1288
+ console.warn('[ChatPanel] No streaming message found for ID:', messageId);
1289
+ return;
1290
+ }
1291
+ // Append to content
1292
+ streaming.content += chunk;
1293
+ // Update DOM
1294
+ const contentDiv = streaming.element.querySelector('.wabbit-chat-message-content');
1295
+ if (contentDiv) {
1296
+ // Format the content and add cursor
1297
+ contentDiv.innerHTML = this.formatMessage(streaming.content) + '<span class="wabbit-chat-cursor">▋</span>';
1298
+ }
1299
+ this.scrollToBottom();
1300
+ }
1301
+ /**
1302
+ * Finish streaming message
1303
+ */
1304
+ finishStreamingMessage(messageId, metadata) {
1305
+ const streaming = this.streamingMessages.get(messageId);
1306
+ if (!streaming) {
1307
+ // Don't warn - this is expected if cleanup already removed it or on error
1308
+ return;
1309
+ }
1310
+ // Remove streaming class and cursor
1311
+ streaming.element.classList.remove('wabbit-chat-message-streaming');
1312
+ const contentDiv = streaming.element.querySelector('.wabbit-chat-message-content');
1313
+ if (contentDiv) {
1314
+ // Remove cursor, keep formatted content
1315
+ contentDiv.innerHTML = this.formatMessage(streaming.content);
1316
+ }
1317
+ // Add to messages array
1318
+ const message = {
1319
+ id: messageId,
1320
+ role: 'assistant',
1321
+ content: streaming.content,
1322
+ timestamp: new Date(),
1323
+ metadata,
1324
+ };
1325
+ this.messages.push(message);
1326
+ // Clean up
1327
+ this.streamingMessages.delete(messageId);
1328
+ this.scrollToBottom();
1329
+ }
1330
+ /**
1331
+ * Cancel a streaming message (e.g., on error)
1332
+ * Cleans up the streaming state without adding to message history
1333
+ */
1334
+ cancelStreamingMessage(messageId) {
1335
+ const streaming = this.streamingMessages.get(messageId);
1336
+ if (!streaming) {
1337
+ return;
1338
+ }
1339
+ // Remove the streaming message element from DOM
1340
+ streaming.element.remove();
1341
+ // Clean up from map
1342
+ this.streamingMessages.delete(messageId);
1343
+ }
1344
+ /**
1345
+ * Cancel all active streaming messages
1346
+ * Useful when connection drops or on error
1347
+ */
1348
+ cancelAllStreamingMessages() {
1349
+ this.streamingMessages.forEach((streaming) => {
1350
+ streaming.element.remove();
1351
+ });
1352
+ this.streamingMessages.clear();
1353
+ }
1354
+ /**
1355
+ * Cleanup stale streaming messages that have exceeded timeout
1356
+ * Runs periodically to prevent memory leaks from abandoned streams
1357
+ */
1358
+ cleanupStaleStreamingMessages() {
1359
+ const now = Date.now();
1360
+ const staleIds = [];
1361
+ this.streamingMessages.forEach((streaming, messageId) => {
1362
+ if (now - streaming.startTime > this.STREAMING_TIMEOUT_MS) {
1363
+ console.warn('[ChatPanel] Cleaning up stale streaming message:', messageId);
1364
+ streaming.element.remove();
1365
+ staleIds.push(messageId);
1366
+ }
1367
+ });
1368
+ // Remove stale entries from map
1369
+ staleIds.forEach(id => this.streamingMessages.delete(id));
1370
+ // Stop cleanup interval if no more streaming messages
1371
+ if (this.streamingMessages.size === 0 && this.streamingCleanupInterval) {
1372
+ clearInterval(this.streamingCleanupInterval);
1373
+ this.streamingCleanupInterval = null;
1374
+ }
1375
+ }
1171
1376
  /**
1172
1377
  * Handle send message
1173
1378
  */
@@ -1252,6 +1457,11 @@
1252
1457
  if (this.element) {
1253
1458
  this.element.style.display = 'none';
1254
1459
  }
1460
+ // Stop streaming cleanup interval when hidden to prevent memory leaks
1461
+ if (this.streamingCleanupInterval) {
1462
+ clearInterval(this.streamingCleanupInterval);
1463
+ this.streamingCleanupInterval = null;
1464
+ }
1255
1465
  }
1256
1466
  /**
1257
1467
  * Remove the panel from DOM and cleanup event listeners
@@ -1260,6 +1470,11 @@
1260
1470
  // Run all event cleanup functions
1261
1471
  this.eventCleanup.forEach((cleanup) => cleanup());
1262
1472
  this.eventCleanup = [];
1473
+ // Clear streaming cleanup interval
1474
+ if (this.streamingCleanupInterval) {
1475
+ clearInterval(this.streamingCleanupInterval);
1476
+ this.streamingCleanupInterval = null;
1477
+ }
1263
1478
  if (this.element) {
1264
1479
  this.element.remove();
1265
1480
  this.element = null;
@@ -1723,6 +1938,24 @@
1723
1938
  }
1724
1939
  }
1725
1940
 
1941
+ /* Streaming Cursor */
1942
+ .wabbit-chat-cursor {
1943
+ display: inline-block;
1944
+ color: var(--wabbit-primary);
1945
+ font-weight: bold;
1946
+ animation: wabbit-cursor-blink 1s infinite;
1947
+ margin-left: 2px;
1948
+ }
1949
+
1950
+ @keyframes wabbit-cursor-blink {
1951
+ 0%, 50% {
1952
+ opacity: 1;
1953
+ }
1954
+ 51%, 100% {
1955
+ opacity: 0;
1956
+ }
1957
+ }
1958
+
1726
1959
  .wabbit-chat-panel {
1727
1960
  animation: wabbit-fade-in 0.3s ease-out;
1728
1961
  }
@@ -1737,17 +1970,17 @@
1737
1970
 
1738
1971
  /* Inline Chat Panel - renders inside container instead of fixed position */
1739
1972
  .wabbit-chat-panel.wabbit-chat-panel-inline {
1740
- position: relative !important;
1973
+ position: absolute !important;
1974
+ top: 0;
1975
+ left: 0;
1976
+ right: 0;
1977
+ bottom: 0;
1741
1978
  width: 100%;
1742
1979
  height: 100%;
1743
- min-height: 400px;
1744
1980
  max-height: none;
1745
1981
  max-width: none;
1746
- bottom: auto !important;
1747
- right: auto !important;
1748
- left: auto !important;
1749
- border-radius: 12px;
1750
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1982
+ border-radius: 0;
1983
+ box-shadow: none;
1751
1984
  animation: none; /* Disable slide-in animation for inline */
1752
1985
  }
1753
1986
 
@@ -1930,6 +2163,26 @@
1930
2163
  this.panel.setMessages(messages);
1931
2164
  }
1932
2165
  };
2166
+ // Streaming message handlers
2167
+ this.wsClient.onMessageStart = (messageId) => {
2168
+ console.log('[Wabbit] Streaming message started:', messageId);
2169
+ if (this.panel) {
2170
+ this.panel.startStreamingMessage(messageId);
2171
+ }
2172
+ };
2173
+ this.wsClient.onMessageChunk = (messageId, chunk) => {
2174
+ if (this.panel) {
2175
+ this.panel.appendToStreamingMessage(messageId, chunk);
2176
+ }
2177
+ };
2178
+ this.wsClient.onMessageEnd = (messageId, metadata) => {
2179
+ console.log('[Wabbit] Streaming message ended:', messageId);
2180
+ if (this.panel) {
2181
+ this.panel.finishStreamingMessage(messageId, metadata);
2182
+ this.panel.setWaitingForResponse(false);
2183
+ }
2184
+ };
2185
+ // Non-streaming message handler (backward compatibility)
1933
2186
  this.wsClient.onMessage = (message) => {
1934
2187
  if (this.panel) {
1935
2188
  this.panel.addMessage(message);
@@ -1939,6 +2192,8 @@
1939
2192
  this.wsClient.onError = (error) => {
1940
2193
  console.error('[Wabbit] Chat error:', error);
1941
2194
  if (this.panel) {
2195
+ // Clean up any active streaming messages on error
2196
+ this.panel.cancelAllStreamingMessages();
1942
2197
  this.panel.addSystemMessage(`Error: ${error}`);
1943
2198
  this.panel.setWaitingForResponse(false);
1944
2199
  }
@@ -1950,10 +2205,24 @@
1950
2205
  };
1951
2206
  this.wsClient.onDisconnect = () => {
1952
2207
  if (this.panel) {
1953
- this.panel.addSystemMessage('Disconnected from chat service');
2208
+ // Clean up any active streaming messages on disconnect
2209
+ this.panel.cancelAllStreamingMessages();
2210
+ // Check if there are queued messages
2211
+ const queueSize = this.wsClient.getQueueSize();
2212
+ if (queueSize > 0) {
2213
+ this.panel.addSystemMessage(`Disconnected from chat service. ${queueSize} message(s) will be sent when reconnected.`);
2214
+ }
2215
+ else {
2216
+ this.panel.addSystemMessage('Disconnected from chat service');
2217
+ }
1954
2218
  this.panel.setDisabled(true);
1955
2219
  }
1956
2220
  };
2221
+ this.wsClient.onQueueOverflow = (message) => {
2222
+ if (this.panel) {
2223
+ this.panel.addSystemMessage(message);
2224
+ }
2225
+ };
1957
2226
  }
1958
2227
  /**
1959
2228
  * Handle trigger type configuration