@wabbit-dashboard/embed 1.0.17 → 1.1.1

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.
@@ -116,6 +116,143 @@ function mergeConfig(config) {
116
116
  };
117
117
  }
118
118
 
119
+ /**
120
+ * localStorage wrapper with type safety
121
+ */
122
+ /**
123
+ * Memory storage implementation (clears on page reload)
124
+ */
125
+ class MemoryStorage {
126
+ constructor() {
127
+ this.store = new Map();
128
+ }
129
+ getItem(key) {
130
+ return this.store.get(key) ?? null;
131
+ }
132
+ setItem(key, value) {
133
+ this.store.set(key, value);
134
+ }
135
+ removeItem(key) {
136
+ this.store.delete(key);
137
+ }
138
+ clear() {
139
+ this.store.clear();
140
+ }
141
+ get length() {
142
+ return this.store.size;
143
+ }
144
+ key(index) {
145
+ const keys = Array.from(this.store.keys());
146
+ return keys[index] ?? null;
147
+ }
148
+ }
149
+ /**
150
+ * Safe storage wrapper
151
+ */
152
+ class SafeStorage {
153
+ constructor(storage = localStorage, prefix = 'wabbit_') {
154
+ this.storage = storage;
155
+ this.prefix = prefix;
156
+ }
157
+ /**
158
+ * Get item from storage
159
+ */
160
+ get(key) {
161
+ try {
162
+ const item = this.storage.getItem(this.prefix + key);
163
+ if (item === null) {
164
+ return null;
165
+ }
166
+ return JSON.parse(item);
167
+ }
168
+ catch (error) {
169
+ console.error(`[Wabbit] Failed to get storage item "${key}":`, error);
170
+ return null;
171
+ }
172
+ }
173
+ /**
174
+ * Set item in storage
175
+ */
176
+ set(key, value) {
177
+ try {
178
+ this.storage.setItem(this.prefix + key, JSON.stringify(value));
179
+ return true;
180
+ }
181
+ catch (error) {
182
+ console.error(`[Wabbit] Failed to set storage item "${key}":`, error);
183
+ return false;
184
+ }
185
+ }
186
+ /**
187
+ * Remove item from storage
188
+ */
189
+ remove(key) {
190
+ this.storage.removeItem(this.prefix + key);
191
+ }
192
+ /**
193
+ * Clear all items with prefix
194
+ */
195
+ clear() {
196
+ if (this.storage.length !== undefined && this.storage.key) {
197
+ const keys = [];
198
+ for (let i = 0; i < this.storage.length; i++) {
199
+ const key = this.storage.key(i);
200
+ if (key && key.startsWith(this.prefix)) {
201
+ keys.push(key);
202
+ }
203
+ }
204
+ keys.forEach((key) => this.storage.removeItem(key));
205
+ }
206
+ else {
207
+ // Fallback: try to clear common keys
208
+ const commonKeys = ['session_id', 'email_capture_dismissed'];
209
+ commonKeys.forEach((key) => this.remove(key));
210
+ }
211
+ }
212
+ }
213
+ // Export singleton instance (for backward compatibility)
214
+ const storage = new SafeStorage();
215
+ /**
216
+ * Create a storage instance with the specified backend
217
+ *
218
+ * @param persistSession - Whether to persist sessions across browser sessions
219
+ * @returns SafeStorage instance with appropriate backend
220
+ */
221
+ function createStorage(persistSession = true) {
222
+ let storageBackend;
223
+ if (persistSession) {
224
+ // Use localStorage (persists across browser sessions)
225
+ storageBackend = typeof window !== 'undefined' ? window.localStorage : new MemoryStorage();
226
+ }
227
+ else {
228
+ // Use sessionStorage (clears when browser closes)
229
+ storageBackend = typeof window !== 'undefined' ? window.sessionStorage : new MemoryStorage();
230
+ }
231
+ return new SafeStorage(storageBackend);
232
+ }
233
+ /**
234
+ * Get item from storage (simple string getter)
235
+ */
236
+ function getStorageItem(key) {
237
+ try {
238
+ return localStorage.getItem('wabbit_' + key);
239
+ }
240
+ catch {
241
+ return null;
242
+ }
243
+ }
244
+ /**
245
+ * Set item in storage (simple string setter)
246
+ */
247
+ function setStorageItem(key, value) {
248
+ try {
249
+ localStorage.setItem('wabbit_' + key, value);
250
+ }
251
+ catch (error) {
252
+ console.error(`[Wabbit] Failed to set storage item "${key}":`, error);
253
+ }
254
+ }
255
+
119
256
  /**
120
257
  * Main Wabbit SDK class
121
258
  *
@@ -132,6 +269,8 @@ class Wabbit {
132
269
  this.formsWidget = null; // FormWidget | null
133
270
  this.emailCaptureWidget = null; // EmailCapture | null
134
271
  this.config = config;
272
+ // Create storage instance with appropriate backend based on persistSession
273
+ this.storage = createStorage(config.persistSession ?? true);
135
274
  }
136
275
  /**
137
276
  * Initialize the Wabbit SDK
@@ -211,7 +350,7 @@ class Wabbit {
211
350
  }
212
351
  }
213
352
  };
214
- const chatWidget = new ChatWidget(chatConfig);
353
+ const chatWidget = new ChatWidget(chatConfig, Wabbit.instance.storage);
215
354
  chatWidget.init();
216
355
  Wabbit.instance.chatWidget = chatWidget;
217
356
  });
@@ -534,110 +673,21 @@ class Wabbit {
534
673
  }
535
674
  Wabbit.instance = null;
536
675
 
537
- /**
538
- * localStorage wrapper with type safety
539
- */
540
- /**
541
- * Safe storage wrapper
542
- */
543
- class SafeStorage {
544
- constructor(storage = localStorage, prefix = 'wabbit_') {
545
- this.storage = storage;
546
- this.prefix = prefix;
547
- }
548
- /**
549
- * Get item from storage
550
- */
551
- get(key) {
552
- try {
553
- const item = this.storage.getItem(this.prefix + key);
554
- if (item === null) {
555
- return null;
556
- }
557
- return JSON.parse(item);
558
- }
559
- catch (error) {
560
- console.error(`[Wabbit] Failed to get storage item "${key}":`, error);
561
- return null;
562
- }
563
- }
564
- /**
565
- * Set item in storage
566
- */
567
- set(key, value) {
568
- try {
569
- this.storage.setItem(this.prefix + key, JSON.stringify(value));
570
- return true;
571
- }
572
- catch (error) {
573
- console.error(`[Wabbit] Failed to set storage item "${key}":`, error);
574
- return false;
575
- }
576
- }
577
- /**
578
- * Remove item from storage
579
- */
580
- remove(key) {
581
- this.storage.removeItem(this.prefix + key);
582
- }
583
- /**
584
- * Clear all items with prefix
585
- */
586
- clear() {
587
- if (this.storage.length !== undefined && this.storage.key) {
588
- const keys = [];
589
- for (let i = 0; i < this.storage.length; i++) {
590
- const key = this.storage.key(i);
591
- if (key && key.startsWith(this.prefix)) {
592
- keys.push(key);
593
- }
594
- }
595
- keys.forEach((key) => this.storage.removeItem(key));
596
- }
597
- else {
598
- // Fallback: try to clear common keys
599
- const commonKeys = ['session_id', 'email_capture_dismissed'];
600
- commonKeys.forEach((key) => this.remove(key));
601
- }
602
- }
603
- }
604
- // Export singleton instance
605
- const storage = new SafeStorage();
606
- /**
607
- * Get item from storage (simple string getter)
608
- */
609
- function getStorageItem(key) {
610
- try {
611
- return localStorage.getItem('wabbit_' + key);
612
- }
613
- catch {
614
- return null;
615
- }
616
- }
617
- /**
618
- * Set item in storage (simple string setter)
619
- */
620
- function setStorageItem(key, value) {
621
- try {
622
- localStorage.setItem('wabbit_' + key, value);
623
- }
624
- catch (error) {
625
- console.error(`[Wabbit] Failed to set storage item "${key}":`, error);
626
- }
627
- }
628
-
629
676
  /**
630
677
  * WebSocket client for chat functionality
631
678
  *
632
679
  * Based on demo-website/src/lib/websocket.ts but without React dependencies
633
680
  */
634
681
  class ChatWebSocketClient {
635
- constructor(apiKey, wsUrl, sessionId = null) {
682
+ constructor(apiKey, wsUrl, sessionId = null, storage) {
636
683
  this.ws = null;
637
684
  this.status = 'disconnected';
638
685
  this.reconnectAttempts = 0;
639
686
  this.maxReconnectAttempts = 3;
640
687
  this.reconnectDelay = 1000;
688
+ // Message queue for handling messages during disconnection
689
+ this.messageQueue = [];
690
+ this.MAX_QUEUE_SIZE = 10;
641
691
  // Event handlers
642
692
  this.onStatusChange = null;
643
693
  this.onWelcome = null;
@@ -645,8 +695,14 @@ class ChatWebSocketClient {
645
695
  this.onMessageHistory = null;
646
696
  this.onError = null;
647
697
  this.onDisconnect = null;
698
+ this.onQueueOverflow = null;
699
+ // Streaming event handlers
700
+ this.onMessageStart = null;
701
+ this.onMessageChunk = null;
702
+ this.onMessageEnd = null;
648
703
  this.apiKey = apiKey;
649
704
  this.wsUrl = wsUrl;
705
+ this.storage = storage;
650
706
  this.sessionId = sessionId || this.getStoredSessionId();
651
707
  }
652
708
  setStatus(status) {
@@ -676,6 +732,8 @@ class ChatWebSocketClient {
676
732
  this.setStatus('connected');
677
733
  this.reconnectAttempts = 0;
678
734
  console.log('[Wabbit] WebSocket connected');
735
+ // Flush any queued messages
736
+ this.flushMessageQueue();
679
737
  };
680
738
  this.ws.onmessage = (event) => {
681
739
  try {
@@ -713,9 +771,9 @@ class ChatWebSocketClient {
713
771
  switch (data.type) {
714
772
  case 'welcome':
715
773
  this.sessionId = data.session_id;
716
- // Store session ID in localStorage
774
+ // Store session ID in storage (localStorage or sessionStorage)
717
775
  if (this.sessionId) {
718
- storage.set('session_id', this.sessionId);
776
+ this.storage.set('session_id', this.sessionId);
719
777
  }
720
778
  if (this.onWelcome) {
721
779
  this.onWelcome(data.session_id, data.collection_id, data.message || 'Connected');
@@ -734,7 +792,26 @@ class ChatWebSocketClient {
734
792
  this.onMessageHistory(messages);
735
793
  }
736
794
  break;
795
+ case 'assistant_message_start':
796
+ // Streaming: Start of assistant message
797
+ if (this.onMessageStart) {
798
+ this.onMessageStart(data.message_id);
799
+ }
800
+ break;
801
+ case 'assistant_message_chunk':
802
+ // Streaming: Chunk of assistant message
803
+ if (this.onMessageChunk) {
804
+ this.onMessageChunk(data.message_id, data.content);
805
+ }
806
+ break;
807
+ case 'assistant_message_end':
808
+ // Streaming: End of assistant message
809
+ if (this.onMessageEnd) {
810
+ this.onMessageEnd(data.message_id, data.metadata);
811
+ }
812
+ break;
737
813
  case 'assistant_message':
814
+ // Non-streaming: Complete assistant message (backward compatibility)
738
815
  if (this.onMessage) {
739
816
  const message = {
740
817
  id: data.message_id || crypto.randomUUID(),
@@ -756,18 +833,27 @@ class ChatWebSocketClient {
756
833
  }
757
834
  }
758
835
  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
836
  const message = {
766
837
  type: 'message',
767
838
  content,
768
839
  metadata: metadata || {},
769
840
  };
770
- this.ws.send(JSON.stringify(message));
841
+ // If connected, send immediately
842
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
843
+ this.ws.send(JSON.stringify(message));
844
+ return;
845
+ }
846
+ // If disconnected or reconnecting, queue the message
847
+ if (this.messageQueue.length >= this.MAX_QUEUE_SIZE) {
848
+ console.warn('[Wabbit] Message queue full, cannot queue message');
849
+ if (this.onQueueOverflow) {
850
+ this.onQueueOverflow('Message queue full. Please wait for reconnection.');
851
+ }
852
+ return;
853
+ }
854
+ // Add to queue
855
+ this.messageQueue.push({ content, metadata });
856
+ console.log(`[Wabbit] Message queued (${this.messageQueue.length}/${this.MAX_QUEUE_SIZE})`);
771
857
  }
772
858
  disconnect() {
773
859
  if (this.ws) {
@@ -789,12 +875,50 @@ class ChatWebSocketClient {
789
875
  this.connect();
790
876
  }, delay);
791
877
  }
878
+ /**
879
+ * Flush queued messages after connection is established
880
+ */
881
+ flushMessageQueue() {
882
+ if (this.messageQueue.length === 0) {
883
+ return;
884
+ }
885
+ console.log(`[Wabbit] Flushing ${this.messageQueue.length} queued message(s)`);
886
+ // Send all queued messages in order
887
+ const queuedMessages = [...this.messageQueue];
888
+ this.messageQueue = [];
889
+ queuedMessages.forEach(({ content, metadata }) => {
890
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
891
+ const message = {
892
+ type: 'message',
893
+ content,
894
+ metadata: metadata || {},
895
+ };
896
+ this.ws.send(JSON.stringify(message));
897
+ }
898
+ });
899
+ }
792
900
  clearSession() {
793
901
  this.sessionId = null;
794
- storage.remove('session_id');
902
+ this.storage.remove('session_id');
903
+ // Clear message queue only when user explicitly starts new session
904
+ // Queue should persist across reconnection attempts
905
+ this.messageQueue = [];
906
+ }
907
+ /**
908
+ * Clear the message queue
909
+ * Only call this when explicitly abandoning queued messages
910
+ */
911
+ clearMessageQueue() {
912
+ this.messageQueue = [];
795
913
  }
796
914
  getStoredSessionId() {
797
- return storage.get('session_id') || null;
915
+ return this.storage.get('session_id') || null;
916
+ }
917
+ /**
918
+ * Get current message queue size
919
+ */
920
+ getQueueSize() {
921
+ return this.messageQueue.length;
798
922
  }
799
923
  }
800
924
 
@@ -924,6 +1048,9 @@ class ChatPanel {
924
1048
  this.isWaitingForResponse = false;
925
1049
  this.closeButton = null;
926
1050
  this.eventCleanup = [];
1051
+ this.streamingMessages = new Map();
1052
+ this.streamingCleanupInterval = null;
1053
+ this.STREAMING_TIMEOUT_MS = 30000; // 30 seconds timeout for stale streams
927
1054
  this.options = options;
928
1055
  }
929
1056
  /**
@@ -1155,13 +1282,150 @@ class ChatPanel {
1155
1282
  formatMessage(content) {
1156
1283
  // Escape HTML first
1157
1284
  let formatted = escapeHtml(content);
1285
+ // Markdown links [text](url) - must come before URL auto-linking
1286
+ formatted = formatted.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
1158
1287
  // Simple markdown-like formatting
1159
1288
  formatted = formatted.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
1160
1289
  formatted = formatted.replace(/\*(.+?)\*/g, '<em>$1</em>');
1161
1290
  formatted = formatted.replace(/`(.+?)`/g, '<code style="background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px;">$1</code>');
1291
+ // Auto-link URLs (not already inside an href attribute)
1292
+ formatted = formatted.replace(/(?<!href=")(https?:\/\/[^\s<]+)/g, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>');
1293
+ // Auto-link email addresses
1294
+ formatted = formatted.replace(/(?<!["\/])([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g, '<a href="mailto:$1">$1</a>');
1295
+ // Auto-link phone numbers (international format with +)
1296
+ formatted = formatted.replace(/(?<!["\/])(\+\d[\d\s-]{7,})/g, (_match, phone) => {
1297
+ const cleanPhone = phone.replace(/[\s-]/g, '');
1298
+ return `<a href="tel:${cleanPhone}">${phone}</a>`;
1299
+ });
1162
1300
  formatted = formatted.replace(/\n/g, '<br>');
1163
1301
  return formatted;
1164
1302
  }
1303
+ /**
1304
+ * Start a streaming assistant message
1305
+ */
1306
+ startStreamingMessage(messageId) {
1307
+ if (!this.messagesContainer)
1308
+ return;
1309
+ // Create message container
1310
+ const messageDiv = createElement('div', {
1311
+ class: 'wabbit-chat-message wabbit-chat-message-assistant wabbit-chat-message-streaming',
1312
+ 'data-message-id': messageId,
1313
+ });
1314
+ const bubble = createElement('div', { class: 'wabbit-chat-message-bubble' });
1315
+ const content = createElement('div', { class: 'wabbit-chat-message-content' });
1316
+ // Add streaming cursor
1317
+ content.innerHTML = '<span class="wabbit-chat-cursor">▋</span>';
1318
+ bubble.appendChild(content);
1319
+ messageDiv.appendChild(bubble);
1320
+ this.messagesContainer.appendChild(messageDiv);
1321
+ // Store reference with timestamp for timeout tracking
1322
+ this.streamingMessages.set(messageId, {
1323
+ element: messageDiv,
1324
+ content: '',
1325
+ startTime: Date.now()
1326
+ });
1327
+ // Start cleanup interval if not already running
1328
+ if (!this.streamingCleanupInterval) {
1329
+ this.streamingCleanupInterval = window.setInterval(() => {
1330
+ this.cleanupStaleStreamingMessages();
1331
+ }, 5000); // Check every 5 seconds
1332
+ }
1333
+ this.scrollToBottom();
1334
+ }
1335
+ /**
1336
+ * Append chunk to streaming message
1337
+ */
1338
+ appendToStreamingMessage(messageId, chunk) {
1339
+ const streaming = this.streamingMessages.get(messageId);
1340
+ if (!streaming) {
1341
+ console.warn('[ChatPanel] No streaming message found for ID:', messageId);
1342
+ return;
1343
+ }
1344
+ // Append to content
1345
+ streaming.content += chunk;
1346
+ // Update DOM
1347
+ const contentDiv = streaming.element.querySelector('.wabbit-chat-message-content');
1348
+ if (contentDiv) {
1349
+ // Format the content and add cursor
1350
+ contentDiv.innerHTML = this.formatMessage(streaming.content) + '<span class="wabbit-chat-cursor">▋</span>';
1351
+ }
1352
+ this.scrollToBottom();
1353
+ }
1354
+ /**
1355
+ * Finish streaming message
1356
+ */
1357
+ finishStreamingMessage(messageId, metadata) {
1358
+ const streaming = this.streamingMessages.get(messageId);
1359
+ if (!streaming) {
1360
+ // Don't warn - this is expected if cleanup already removed it or on error
1361
+ return;
1362
+ }
1363
+ // Remove streaming class and cursor
1364
+ streaming.element.classList.remove('wabbit-chat-message-streaming');
1365
+ const contentDiv = streaming.element.querySelector('.wabbit-chat-message-content');
1366
+ if (contentDiv) {
1367
+ // Remove cursor, keep formatted content
1368
+ contentDiv.innerHTML = this.formatMessage(streaming.content);
1369
+ }
1370
+ // Add to messages array
1371
+ const message = {
1372
+ id: messageId,
1373
+ role: 'assistant',
1374
+ content: streaming.content,
1375
+ timestamp: new Date(),
1376
+ metadata,
1377
+ };
1378
+ this.messages.push(message);
1379
+ // Clean up
1380
+ this.streamingMessages.delete(messageId);
1381
+ this.scrollToBottom();
1382
+ }
1383
+ /**
1384
+ * Cancel a streaming message (e.g., on error)
1385
+ * Cleans up the streaming state without adding to message history
1386
+ */
1387
+ cancelStreamingMessage(messageId) {
1388
+ const streaming = this.streamingMessages.get(messageId);
1389
+ if (!streaming) {
1390
+ return;
1391
+ }
1392
+ // Remove the streaming message element from DOM
1393
+ streaming.element.remove();
1394
+ // Clean up from map
1395
+ this.streamingMessages.delete(messageId);
1396
+ }
1397
+ /**
1398
+ * Cancel all active streaming messages
1399
+ * Useful when connection drops or on error
1400
+ */
1401
+ cancelAllStreamingMessages() {
1402
+ this.streamingMessages.forEach((streaming) => {
1403
+ streaming.element.remove();
1404
+ });
1405
+ this.streamingMessages.clear();
1406
+ }
1407
+ /**
1408
+ * Cleanup stale streaming messages that have exceeded timeout
1409
+ * Runs periodically to prevent memory leaks from abandoned streams
1410
+ */
1411
+ cleanupStaleStreamingMessages() {
1412
+ const now = Date.now();
1413
+ const staleIds = [];
1414
+ this.streamingMessages.forEach((streaming, messageId) => {
1415
+ if (now - streaming.startTime > this.STREAMING_TIMEOUT_MS) {
1416
+ console.warn('[ChatPanel] Cleaning up stale streaming message:', messageId);
1417
+ streaming.element.remove();
1418
+ staleIds.push(messageId);
1419
+ }
1420
+ });
1421
+ // Remove stale entries from map
1422
+ staleIds.forEach(id => this.streamingMessages.delete(id));
1423
+ // Stop cleanup interval if no more streaming messages
1424
+ if (this.streamingMessages.size === 0 && this.streamingCleanupInterval) {
1425
+ clearInterval(this.streamingCleanupInterval);
1426
+ this.streamingCleanupInterval = null;
1427
+ }
1428
+ }
1165
1429
  /**
1166
1430
  * Handle send message
1167
1431
  */
@@ -1246,6 +1510,11 @@ class ChatPanel {
1246
1510
  if (this.element) {
1247
1511
  this.element.style.display = 'none';
1248
1512
  }
1513
+ // Stop streaming cleanup interval when hidden to prevent memory leaks
1514
+ if (this.streamingCleanupInterval) {
1515
+ clearInterval(this.streamingCleanupInterval);
1516
+ this.streamingCleanupInterval = null;
1517
+ }
1249
1518
  }
1250
1519
  /**
1251
1520
  * Remove the panel from DOM and cleanup event listeners
@@ -1254,6 +1523,11 @@ class ChatPanel {
1254
1523
  // Run all event cleanup functions
1255
1524
  this.eventCleanup.forEach((cleanup) => cleanup());
1256
1525
  this.eventCleanup = [];
1526
+ // Clear streaming cleanup interval
1527
+ if (this.streamingCleanupInterval) {
1528
+ clearInterval(this.streamingCleanupInterval);
1529
+ this.streamingCleanupInterval = null;
1530
+ }
1257
1531
  if (this.element) {
1258
1532
  this.element.remove();
1259
1533
  this.element = null;
@@ -1575,6 +1849,20 @@ function injectChatStyles(primaryColor = '#6366f1', theme) {
1575
1849
  line-height: 1.5;
1576
1850
  }
1577
1851
 
1852
+ .wabbit-chat-message-content a {
1853
+ color: var(--wabbit-primary);
1854
+ text-decoration: underline;
1855
+ text-underline-offset: 2px;
1856
+ }
1857
+
1858
+ .wabbit-chat-message-content a:hover {
1859
+ opacity: 0.8;
1860
+ }
1861
+
1862
+ .wabbit-chat-message-user .wabbit-chat-message-content a {
1863
+ color: inherit;
1864
+ }
1865
+
1578
1866
  /* Typing Indicator */
1579
1867
  .wabbit-chat-typing {
1580
1868
  display: flex;
@@ -1717,6 +2005,24 @@ function injectChatStyles(primaryColor = '#6366f1', theme) {
1717
2005
  }
1718
2006
  }
1719
2007
 
2008
+ /* Streaming Cursor */
2009
+ .wabbit-chat-cursor {
2010
+ display: inline-block;
2011
+ color: var(--wabbit-primary);
2012
+ font-weight: bold;
2013
+ animation: wabbit-cursor-blink 1s infinite;
2014
+ margin-left: 2px;
2015
+ }
2016
+
2017
+ @keyframes wabbit-cursor-blink {
2018
+ 0%, 50% {
2019
+ opacity: 1;
2020
+ }
2021
+ 51%, 100% {
2022
+ opacity: 0;
2023
+ }
2024
+ }
2025
+
1720
2026
  .wabbit-chat-panel {
1721
2027
  animation: wabbit-fade-in 0.3s ease-out;
1722
2028
  }
@@ -1731,17 +2037,17 @@ function injectChatStyles(primaryColor = '#6366f1', theme) {
1731
2037
 
1732
2038
  /* Inline Chat Panel - renders inside container instead of fixed position */
1733
2039
  .wabbit-chat-panel.wabbit-chat-panel-inline {
1734
- position: relative !important;
2040
+ position: absolute !important;
2041
+ top: 0;
2042
+ left: 0;
2043
+ right: 0;
2044
+ bottom: 0;
1735
2045
  width: 100%;
1736
2046
  height: 100%;
1737
- min-height: 400px;
1738
2047
  max-height: none;
1739
2048
  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);
2049
+ border-radius: 0;
2050
+ box-shadow: none;
1745
2051
  animation: none; /* Disable slide-in animation for inline */
1746
2052
  }
1747
2053
 
@@ -1792,7 +2098,7 @@ function adjustColor(color, amount) {
1792
2098
  * Chat Widget - Main class that integrates all chat components
1793
2099
  */
1794
2100
  class ChatWidget {
1795
- constructor(config) {
2101
+ constructor(config, storage) {
1796
2102
  this.wsClient = null;
1797
2103
  this.bubble = null;
1798
2104
  this.panel = null;
@@ -1802,6 +2108,7 @@ class ChatWidget {
1802
2108
  this.onChatReadyCallback = null;
1803
2109
  this.chatReadyFired = false;
1804
2110
  this.config = config;
2111
+ this.storage = storage;
1805
2112
  this.onChatReadyCallback = config.onChatReady || null;
1806
2113
  }
1807
2114
  /**
@@ -1830,8 +2137,8 @@ class ChatWidget {
1830
2137
  this.setupThemeWatcher();
1831
2138
  // Create WebSocket client
1832
2139
  const wsUrl = this.getWebSocketUrl();
1833
- this.wsClient = new ChatWebSocketClient(this.config.apiKey || '', wsUrl, null // Will use stored session if available
1834
- );
2140
+ this.wsClient = new ChatWebSocketClient(this.config.apiKey || '', wsUrl, null, // Will use stored session if available
2141
+ this.storage);
1835
2142
  // Set up event handlers
1836
2143
  this.setupWebSocketHandlers();
1837
2144
  // Only create bubble for widget mode (not inline)
@@ -1924,6 +2231,26 @@ class ChatWidget {
1924
2231
  this.panel.setMessages(messages);
1925
2232
  }
1926
2233
  };
2234
+ // Streaming message handlers
2235
+ this.wsClient.onMessageStart = (messageId) => {
2236
+ console.log('[Wabbit] Streaming message started:', messageId);
2237
+ if (this.panel) {
2238
+ this.panel.startStreamingMessage(messageId);
2239
+ }
2240
+ };
2241
+ this.wsClient.onMessageChunk = (messageId, chunk) => {
2242
+ if (this.panel) {
2243
+ this.panel.appendToStreamingMessage(messageId, chunk);
2244
+ }
2245
+ };
2246
+ this.wsClient.onMessageEnd = (messageId, metadata) => {
2247
+ console.log('[Wabbit] Streaming message ended:', messageId);
2248
+ if (this.panel) {
2249
+ this.panel.finishStreamingMessage(messageId, metadata);
2250
+ this.panel.setWaitingForResponse(false);
2251
+ }
2252
+ };
2253
+ // Non-streaming message handler (backward compatibility)
1927
2254
  this.wsClient.onMessage = (message) => {
1928
2255
  if (this.panel) {
1929
2256
  this.panel.addMessage(message);
@@ -1933,6 +2260,8 @@ class ChatWidget {
1933
2260
  this.wsClient.onError = (error) => {
1934
2261
  console.error('[Wabbit] Chat error:', error);
1935
2262
  if (this.panel) {
2263
+ // Clean up any active streaming messages on error
2264
+ this.panel.cancelAllStreamingMessages();
1936
2265
  this.panel.addSystemMessage(`Error: ${error}`);
1937
2266
  this.panel.setWaitingForResponse(false);
1938
2267
  }
@@ -1944,10 +2273,24 @@ class ChatWidget {
1944
2273
  };
1945
2274
  this.wsClient.onDisconnect = () => {
1946
2275
  if (this.panel) {
1947
- this.panel.addSystemMessage('Disconnected from chat service');
2276
+ // Clean up any active streaming messages on disconnect
2277
+ this.panel.cancelAllStreamingMessages();
2278
+ // Check if there are queued messages
2279
+ const queueSize = this.wsClient.getQueueSize();
2280
+ if (queueSize > 0) {
2281
+ this.panel.addSystemMessage(`Disconnected from chat service. ${queueSize} message(s) will be sent when reconnected.`);
2282
+ }
2283
+ else {
2284
+ this.panel.addSystemMessage('Disconnected from chat service');
2285
+ }
1948
2286
  this.panel.setDisabled(true);
1949
2287
  }
1950
2288
  };
2289
+ this.wsClient.onQueueOverflow = (message) => {
2290
+ if (this.panel) {
2291
+ this.panel.addSystemMessage(message);
2292
+ }
2293
+ };
1951
2294
  }
1952
2295
  /**
1953
2296
  * Handle trigger type configuration