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