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