@wabbit-dashboard/embed 1.0.16 → 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.
@@ -161,99 +161,41 @@ class Wabbit {
161
161
  const mergedConfig = mergeConfig(config);
162
162
  Wabbit.instance = new Wabbit(mergedConfig);
163
163
  // Store email capture widget reference for use in callbacks
164
- // This allows chat widget to trigger email capture without tight coupling
165
164
  let emailCaptureWidgetRef = null;
166
- // Track if email capture is enabled (determined by server config)
167
- let emailCaptureEnabled = false;
168
165
  // Initialize widgets based on merged config
169
166
  if (mergedConfig.chat?.enabled && mergedConfig.chat) {
170
167
  const chat = mergedConfig.chat;
171
168
  const collectionId = chat.collectionId;
169
+ const userOnChatReady = chat.onChatReady || config.onChatReady;
172
170
  // Derive chat service API URL from WebSocket URL
173
- // The email capture config endpoint is on the chat service, not the dashboard
174
171
  const wsUrl = mergedConfig.wsUrl || chat.wsUrl || 'wss://chat.insourcedata.ai/ws/chat';
175
172
  const chatServiceUrl = wsUrl
176
173
  .replace(/^wss:\/\//, 'https://')
177
174
  .replace(/^ws:\/\//, 'http://')
178
- .replace(/\/ws\/chat.*$/, ''); // Remove the path, keep just the base URL
179
- // Fetch email capture config from chat service (in parallel with chat widget import)
180
- const emailCaptureConfigPromise = fetch(`${chatServiceUrl}/api/email-capture/config/${collectionId}`)
181
- .then(res => res.json())
182
- .then(data => {
183
- if (data.success && data.config?.enabled) {
184
- return data.config;
185
- }
186
- return null;
187
- })
188
- .catch(err => {
189
- console.warn('[Wabbit] Failed to fetch email capture config:', err);
190
- return null;
191
- });
192
- // Import ChatWidget dynamically to avoid circular dependencies
193
- Promise.resolve().then(function () { return ChatWidget$1; }).then(async ({ ChatWidget }) => {
194
- // Check if instance was destroyed during async import (e.g., React StrictMode)
175
+ .replace(/\/ws\/chat.*$/, '');
176
+ // Import and initialize ChatWidget immediately (don't wait for email capture config)
177
+ Promise.resolve().then(function () { return ChatWidget$1; }).then(({ ChatWidget }) => {
195
178
  if (!Wabbit.instance) {
196
179
  console.warn('[Wabbit] Instance was destroyed before chat widget could initialize');
197
180
  return;
198
181
  }
199
- // Wait for email capture config to be fetched
200
- const serverEmailConfig = await emailCaptureConfigPromise;
201
- emailCaptureEnabled = !!serverEmailConfig;
202
- // Initialize email capture widget if server config says it's enabled
203
- if (serverEmailConfig) {
204
- const { EmailCaptureWidget } = await Promise.resolve().then(function () { return EmailCaptureWidget$1; });
205
- if (!Wabbit.instance) {
206
- console.warn('[Wabbit] Instance was destroyed before email capture widget could initialize');
207
- return;
208
- }
209
- // Convert server config format to widget config format
210
- const triggerAfterMessages = serverEmailConfig.trigger?.type === 'message_count'
211
- ? serverEmailConfig.trigger.messageCount
212
- : 3;
213
- // Build fields array from server config
214
- const fields = ['email'];
215
- if (serverEmailConfig.fields?.name?.enabled)
216
- fields.push('name');
217
- if (serverEmailConfig.fields?.company?.enabled)
218
- fields.push('company');
219
- const emailCaptureConfig = {
220
- enabled: true,
221
- triggerAfterMessages,
222
- title: serverEmailConfig.modal?.title,
223
- description: serverEmailConfig.modal?.description,
224
- fields,
225
- onCapture: mergedConfig.emailCapture?.onCapture
226
- };
227
- const emailCaptureWidget = new EmailCaptureWidget(emailCaptureConfig);
228
- emailCaptureWidget.init();
229
- Wabbit.instance.emailCaptureWidget = emailCaptureWidget;
230
- emailCaptureWidgetRef = emailCaptureWidget;
231
- console.log('[Wabbit] Email capture initialized from server config');
232
- }
233
- const userOnChatReady = chat.onChatReady || config.onChatReady;
234
182
  const chatConfig = {
235
183
  enabled: chat.enabled,
236
184
  collectionId: chat.collectionId,
237
- // Use chat-specific apiKey/apiUrl if provided, otherwise inherit from WabbitConfig
238
185
  apiKey: chat.apiKey || mergedConfig.apiKey,
239
186
  apiUrl: chat.apiUrl || mergedConfig.apiUrl,
240
- wsUrl: mergedConfig.wsUrl || chat.wsUrl, // Use global wsUrl or chat-specific wsUrl
241
- // Embedding mode
187
+ wsUrl: mergedConfig.wsUrl || chat.wsUrl,
242
188
  mode: chat.mode,
243
189
  container: chat.container,
244
- // Widget mode options
245
190
  position: chat.position,
246
191
  triggerType: chat.triggerType,
247
192
  triggerDelay: chat.triggerDelay,
248
- // Appearance options
249
193
  theme: chat.theme,
250
194
  primaryColor: chat.primaryColor,
251
195
  welcomeMessage: chat.welcomeMessage,
252
196
  placeholder: chat.placeholder,
253
- // Header customization
254
197
  showHeader: chat.showHeader,
255
198
  headerTitle: chat.headerTitle,
256
- // Callbacks
257
199
  onChatReady: () => {
258
200
  // Set WebSocket client on email capture widget when chat is ready
259
201
  if (emailCaptureWidgetRef && Wabbit.instance?.chatWidget) {
@@ -262,24 +204,68 @@ class Wabbit {
262
204
  emailCaptureWidgetRef.setWebSocketClient(chatWidget.wsClient);
263
205
  }
264
206
  }
265
- // Call user's callback
266
207
  if (userOnChatReady) {
267
208
  userOnChatReady();
268
209
  }
269
210
  },
270
- // Trigger email capture when user sends a message (if enabled by server)
271
- onUserMessage: emailCaptureEnabled
272
- ? () => {
273
- if (emailCaptureWidgetRef) {
274
- emailCaptureWidgetRef.handleMessage();
275
- }
211
+ // Always set onUserMessage - it checks if emailCaptureWidgetRef exists at call time
212
+ onUserMessage: () => {
213
+ if (emailCaptureWidgetRef) {
214
+ emailCaptureWidgetRef.handleMessage();
276
215
  }
277
- : undefined
216
+ }
278
217
  };
279
218
  const chatWidget = new ChatWidget(chatConfig);
280
219
  chatWidget.init();
281
220
  Wabbit.instance.chatWidget = chatWidget;
282
221
  });
222
+ // Fetch email capture config from server (in parallel with chat init)
223
+ fetch(`${chatServiceUrl}/api/email-capture/config/${collectionId}`)
224
+ .then(res => res.json())
225
+ .then(async (data) => {
226
+ if (!data.success || !data.config?.enabled) {
227
+ return; // Email capture not enabled
228
+ }
229
+ if (!Wabbit.instance) {
230
+ return; // Instance was destroyed
231
+ }
232
+ const serverConfig = data.config;
233
+ const { EmailCaptureWidget } = await Promise.resolve().then(function () { return EmailCaptureWidget$1; });
234
+ if (!Wabbit.instance) {
235
+ return;
236
+ }
237
+ // Convert server config to widget config
238
+ const triggerAfterMessages = serverConfig.trigger?.type === 'message_count'
239
+ ? serverConfig.trigger.messageCount
240
+ : 3;
241
+ const fields = ['email'];
242
+ if (serverConfig.fields?.name?.enabled)
243
+ fields.push('name');
244
+ if (serverConfig.fields?.company?.enabled)
245
+ fields.push('company');
246
+ const emailCaptureConfig = {
247
+ enabled: true,
248
+ triggerAfterMessages,
249
+ title: serverConfig.modal?.title,
250
+ description: serverConfig.modal?.description,
251
+ fields,
252
+ onCapture: mergedConfig.emailCapture?.onCapture
253
+ };
254
+ const emailCaptureWidget = new EmailCaptureWidget(emailCaptureConfig);
255
+ emailCaptureWidget.init();
256
+ Wabbit.instance.emailCaptureWidget = emailCaptureWidget;
257
+ emailCaptureWidgetRef = emailCaptureWidget;
258
+ // If chat is already ready, set the WebSocket client now
259
+ if (Wabbit.instance.chatWidget) {
260
+ const chatWidget = Wabbit.instance.chatWidget;
261
+ if (chatWidget.wsClient) {
262
+ emailCaptureWidget.setWebSocketClient(chatWidget.wsClient);
263
+ }
264
+ }
265
+ })
266
+ .catch(err => {
267
+ console.warn('[Wabbit] Failed to fetch email capture config:', err);
268
+ });
283
269
  }
284
270
  if (mergedConfig.forms?.enabled && mergedConfig.forms) {
285
271
  // Import FormWidget dynamically to avoid circular dependencies
@@ -656,6 +642,9 @@ class ChatWebSocketClient {
656
642
  this.reconnectAttempts = 0;
657
643
  this.maxReconnectAttempts = 3;
658
644
  this.reconnectDelay = 1000;
645
+ // Message queue for handling messages during disconnection
646
+ this.messageQueue = [];
647
+ this.MAX_QUEUE_SIZE = 10;
659
648
  // Event handlers
660
649
  this.onStatusChange = null;
661
650
  this.onWelcome = null;
@@ -663,6 +652,11 @@ class ChatWebSocketClient {
663
652
  this.onMessageHistory = null;
664
653
  this.onError = null;
665
654
  this.onDisconnect = null;
655
+ this.onQueueOverflow = null;
656
+ // Streaming event handlers
657
+ this.onMessageStart = null;
658
+ this.onMessageChunk = null;
659
+ this.onMessageEnd = null;
666
660
  this.apiKey = apiKey;
667
661
  this.wsUrl = wsUrl;
668
662
  this.sessionId = sessionId || this.getStoredSessionId();
@@ -694,6 +688,8 @@ class ChatWebSocketClient {
694
688
  this.setStatus('connected');
695
689
  this.reconnectAttempts = 0;
696
690
  console.log('[Wabbit] WebSocket connected');
691
+ // Flush any queued messages
692
+ this.flushMessageQueue();
697
693
  };
698
694
  this.ws.onmessage = (event) => {
699
695
  try {
@@ -752,7 +748,26 @@ class ChatWebSocketClient {
752
748
  this.onMessageHistory(messages);
753
749
  }
754
750
  break;
751
+ case 'assistant_message_start':
752
+ // Streaming: Start of assistant message
753
+ if (this.onMessageStart) {
754
+ this.onMessageStart(data.message_id);
755
+ }
756
+ break;
757
+ case 'assistant_message_chunk':
758
+ // Streaming: Chunk of assistant message
759
+ if (this.onMessageChunk) {
760
+ this.onMessageChunk(data.message_id, data.content);
761
+ }
762
+ break;
763
+ case 'assistant_message_end':
764
+ // Streaming: End of assistant message
765
+ if (this.onMessageEnd) {
766
+ this.onMessageEnd(data.message_id, data.metadata);
767
+ }
768
+ break;
755
769
  case 'assistant_message':
770
+ // Non-streaming: Complete assistant message (backward compatibility)
756
771
  if (this.onMessage) {
757
772
  const message = {
758
773
  id: data.message_id || crypto.randomUUID(),
@@ -774,18 +789,27 @@ class ChatWebSocketClient {
774
789
  }
775
790
  }
776
791
  sendMessage(content, metadata) {
777
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
778
- if (this.onError) {
779
- this.onError('Not connected to chat service');
780
- }
781
- return;
782
- }
783
792
  const message = {
784
793
  type: 'message',
785
794
  content,
786
795
  metadata: metadata || {},
787
796
  };
788
- this.ws.send(JSON.stringify(message));
797
+ // If connected, send immediately
798
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
799
+ this.ws.send(JSON.stringify(message));
800
+ return;
801
+ }
802
+ // If disconnected or reconnecting, queue the message
803
+ if (this.messageQueue.length >= this.MAX_QUEUE_SIZE) {
804
+ console.warn('[Wabbit] Message queue full, cannot queue message');
805
+ if (this.onQueueOverflow) {
806
+ this.onQueueOverflow('Message queue full. Please wait for reconnection.');
807
+ }
808
+ return;
809
+ }
810
+ // Add to queue
811
+ this.messageQueue.push({ content, metadata });
812
+ console.log(`[Wabbit] Message queued (${this.messageQueue.length}/${this.MAX_QUEUE_SIZE})`);
789
813
  }
790
814
  disconnect() {
791
815
  if (this.ws) {
@@ -807,13 +831,51 @@ class ChatWebSocketClient {
807
831
  this.connect();
808
832
  }, delay);
809
833
  }
834
+ /**
835
+ * Flush queued messages after connection is established
836
+ */
837
+ flushMessageQueue() {
838
+ if (this.messageQueue.length === 0) {
839
+ return;
840
+ }
841
+ console.log(`[Wabbit] Flushing ${this.messageQueue.length} queued message(s)`);
842
+ // Send all queued messages in order
843
+ const queuedMessages = [...this.messageQueue];
844
+ this.messageQueue = [];
845
+ queuedMessages.forEach(({ content, metadata }) => {
846
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
847
+ const message = {
848
+ type: 'message',
849
+ content,
850
+ metadata: metadata || {},
851
+ };
852
+ this.ws.send(JSON.stringify(message));
853
+ }
854
+ });
855
+ }
810
856
  clearSession() {
811
857
  this.sessionId = null;
812
858
  storage.remove('session_id');
859
+ // Clear message queue only when user explicitly starts new session
860
+ // Queue should persist across reconnection attempts
861
+ this.messageQueue = [];
862
+ }
863
+ /**
864
+ * Clear the message queue
865
+ * Only call this when explicitly abandoning queued messages
866
+ */
867
+ clearMessageQueue() {
868
+ this.messageQueue = [];
813
869
  }
814
870
  getStoredSessionId() {
815
871
  return storage.get('session_id') || null;
816
872
  }
873
+ /**
874
+ * Get current message queue size
875
+ */
876
+ getQueueSize() {
877
+ return this.messageQueue.length;
878
+ }
817
879
  }
818
880
 
819
881
  /**
@@ -942,6 +1004,9 @@ class ChatPanel {
942
1004
  this.isWaitingForResponse = false;
943
1005
  this.closeButton = null;
944
1006
  this.eventCleanup = [];
1007
+ this.streamingMessages = new Map();
1008
+ this.streamingCleanupInterval = null;
1009
+ this.STREAMING_TIMEOUT_MS = 30000; // 30 seconds timeout for stale streams
945
1010
  this.options = options;
946
1011
  }
947
1012
  /**
@@ -1128,8 +1193,9 @@ class ChatPanel {
1128
1193
  <div class="wabbit-chat-typing-dot"></div>
1129
1194
  `;
1130
1195
  this.messagesContainer.appendChild(typing);
1131
- this.scrollToBottom();
1132
1196
  }
1197
+ // Always scroll after changing typing indicator state
1198
+ this.scrollToBottom();
1133
1199
  }
1134
1200
  this.updateSendButtonState();
1135
1201
  }
@@ -1179,6 +1245,132 @@ class ChatPanel {
1179
1245
  formatted = formatted.replace(/\n/g, '<br>');
1180
1246
  return formatted;
1181
1247
  }
1248
+ /**
1249
+ * Start a streaming assistant message
1250
+ */
1251
+ startStreamingMessage(messageId) {
1252
+ if (!this.messagesContainer)
1253
+ return;
1254
+ // Create message container
1255
+ const messageDiv = createElement('div', {
1256
+ class: 'wabbit-chat-message wabbit-chat-message-assistant wabbit-chat-message-streaming',
1257
+ 'data-message-id': messageId,
1258
+ });
1259
+ const bubble = createElement('div', { class: 'wabbit-chat-message-bubble' });
1260
+ const content = createElement('div', { class: 'wabbit-chat-message-content' });
1261
+ // Add streaming cursor
1262
+ content.innerHTML = '<span class="wabbit-chat-cursor">▋</span>';
1263
+ bubble.appendChild(content);
1264
+ messageDiv.appendChild(bubble);
1265
+ this.messagesContainer.appendChild(messageDiv);
1266
+ // Store reference with timestamp for timeout tracking
1267
+ this.streamingMessages.set(messageId, {
1268
+ element: messageDiv,
1269
+ content: '',
1270
+ startTime: Date.now()
1271
+ });
1272
+ // Start cleanup interval if not already running
1273
+ if (!this.streamingCleanupInterval) {
1274
+ this.streamingCleanupInterval = window.setInterval(() => {
1275
+ this.cleanupStaleStreamingMessages();
1276
+ }, 5000); // Check every 5 seconds
1277
+ }
1278
+ this.scrollToBottom();
1279
+ }
1280
+ /**
1281
+ * Append chunk to streaming message
1282
+ */
1283
+ appendToStreamingMessage(messageId, chunk) {
1284
+ const streaming = this.streamingMessages.get(messageId);
1285
+ if (!streaming) {
1286
+ console.warn('[ChatPanel] No streaming message found for ID:', messageId);
1287
+ return;
1288
+ }
1289
+ // Append to content
1290
+ streaming.content += chunk;
1291
+ // Update DOM
1292
+ const contentDiv = streaming.element.querySelector('.wabbit-chat-message-content');
1293
+ if (contentDiv) {
1294
+ // Format the content and add cursor
1295
+ contentDiv.innerHTML = this.formatMessage(streaming.content) + '<span class="wabbit-chat-cursor">▋</span>';
1296
+ }
1297
+ this.scrollToBottom();
1298
+ }
1299
+ /**
1300
+ * Finish streaming message
1301
+ */
1302
+ finishStreamingMessage(messageId, metadata) {
1303
+ const streaming = this.streamingMessages.get(messageId);
1304
+ if (!streaming) {
1305
+ // Don't warn - this is expected if cleanup already removed it or on error
1306
+ return;
1307
+ }
1308
+ // Remove streaming class and cursor
1309
+ streaming.element.classList.remove('wabbit-chat-message-streaming');
1310
+ const contentDiv = streaming.element.querySelector('.wabbit-chat-message-content');
1311
+ if (contentDiv) {
1312
+ // Remove cursor, keep formatted content
1313
+ contentDiv.innerHTML = this.formatMessage(streaming.content);
1314
+ }
1315
+ // Add to messages array
1316
+ const message = {
1317
+ id: messageId,
1318
+ role: 'assistant',
1319
+ content: streaming.content,
1320
+ timestamp: new Date(),
1321
+ metadata,
1322
+ };
1323
+ this.messages.push(message);
1324
+ // Clean up
1325
+ this.streamingMessages.delete(messageId);
1326
+ this.scrollToBottom();
1327
+ }
1328
+ /**
1329
+ * Cancel a streaming message (e.g., on error)
1330
+ * Cleans up the streaming state without adding to message history
1331
+ */
1332
+ cancelStreamingMessage(messageId) {
1333
+ const streaming = this.streamingMessages.get(messageId);
1334
+ if (!streaming) {
1335
+ return;
1336
+ }
1337
+ // Remove the streaming message element from DOM
1338
+ streaming.element.remove();
1339
+ // Clean up from map
1340
+ this.streamingMessages.delete(messageId);
1341
+ }
1342
+ /**
1343
+ * Cancel all active streaming messages
1344
+ * Useful when connection drops or on error
1345
+ */
1346
+ cancelAllStreamingMessages() {
1347
+ this.streamingMessages.forEach((streaming) => {
1348
+ streaming.element.remove();
1349
+ });
1350
+ this.streamingMessages.clear();
1351
+ }
1352
+ /**
1353
+ * Cleanup stale streaming messages that have exceeded timeout
1354
+ * Runs periodically to prevent memory leaks from abandoned streams
1355
+ */
1356
+ cleanupStaleStreamingMessages() {
1357
+ const now = Date.now();
1358
+ const staleIds = [];
1359
+ this.streamingMessages.forEach((streaming, messageId) => {
1360
+ if (now - streaming.startTime > this.STREAMING_TIMEOUT_MS) {
1361
+ console.warn('[ChatPanel] Cleaning up stale streaming message:', messageId);
1362
+ streaming.element.remove();
1363
+ staleIds.push(messageId);
1364
+ }
1365
+ });
1366
+ // Remove stale entries from map
1367
+ staleIds.forEach(id => this.streamingMessages.delete(id));
1368
+ // Stop cleanup interval if no more streaming messages
1369
+ if (this.streamingMessages.size === 0 && this.streamingCleanupInterval) {
1370
+ clearInterval(this.streamingCleanupInterval);
1371
+ this.streamingCleanupInterval = null;
1372
+ }
1373
+ }
1182
1374
  /**
1183
1375
  * Handle send message
1184
1376
  */
@@ -1211,29 +1403,38 @@ class ChatPanel {
1211
1403
  }
1212
1404
  }
1213
1405
  /**
1214
- * Scroll to bottom of messages - uses multiple techniques to ensure it works
1406
+ * Scroll to bottom of messages - forces scroll with multiple attempts
1215
1407
  */
1216
1408
  scrollToBottom() {
1217
1409
  if (!this.messagesContainer)
1218
1410
  return;
1219
1411
  const container = this.messagesContainer;
1220
- // Immediate scroll
1221
- container.scrollTop = container.scrollHeight;
1222
- // Use requestAnimationFrame to scroll after next paint
1412
+ const forceScroll = () => {
1413
+ // Force the container to recognize its scroll height
1414
+ const scrollHeight = container.scrollHeight;
1415
+ const clientHeight = container.clientHeight;
1416
+ // Only scroll if there's content to scroll
1417
+ if (scrollHeight > clientHeight) {
1418
+ container.scrollTop = scrollHeight;
1419
+ }
1420
+ else {
1421
+ // If heights are equal, set to a large value to force bottom
1422
+ container.scrollTop = 999999;
1423
+ }
1424
+ };
1425
+ // Immediate
1426
+ forceScroll();
1427
+ // After next frame
1223
1428
  requestAnimationFrame(() => {
1224
- container.scrollTop = container.scrollHeight;
1225
- // And again after another frame to be extra sure
1226
- requestAnimationFrame(() => {
1227
- container.scrollTop = container.scrollHeight;
1228
- });
1429
+ forceScroll();
1430
+ requestAnimationFrame(forceScroll);
1229
1431
  });
1230
- // Also use setTimeout as a fallback for slower renders
1231
- setTimeout(() => {
1232
- container.scrollTop = container.scrollHeight;
1233
- }, 50);
1234
- setTimeout(() => {
1235
- container.scrollTop = container.scrollHeight;
1236
- }, 150);
1432
+ // Multiple delayed attempts
1433
+ setTimeout(forceScroll, 10);
1434
+ setTimeout(forceScroll, 50);
1435
+ setTimeout(forceScroll, 100);
1436
+ setTimeout(forceScroll, 200);
1437
+ setTimeout(forceScroll, 500);
1237
1438
  }
1238
1439
  /**
1239
1440
  * Show the panel
@@ -1254,6 +1455,11 @@ class ChatPanel {
1254
1455
  if (this.element) {
1255
1456
  this.element.style.display = 'none';
1256
1457
  }
1458
+ // Stop streaming cleanup interval when hidden to prevent memory leaks
1459
+ if (this.streamingCleanupInterval) {
1460
+ clearInterval(this.streamingCleanupInterval);
1461
+ this.streamingCleanupInterval = null;
1462
+ }
1257
1463
  }
1258
1464
  /**
1259
1465
  * Remove the panel from DOM and cleanup event listeners
@@ -1262,6 +1468,11 @@ class ChatPanel {
1262
1468
  // Run all event cleanup functions
1263
1469
  this.eventCleanup.forEach((cleanup) => cleanup());
1264
1470
  this.eventCleanup = [];
1471
+ // Clear streaming cleanup interval
1472
+ if (this.streamingCleanupInterval) {
1473
+ clearInterval(this.streamingCleanupInterval);
1474
+ this.streamingCleanupInterval = null;
1475
+ }
1265
1476
  if (this.element) {
1266
1477
  this.element.remove();
1267
1478
  this.element = null;
@@ -1725,6 +1936,24 @@ function injectChatStyles(primaryColor = '#6366f1', theme) {
1725
1936
  }
1726
1937
  }
1727
1938
 
1939
+ /* Streaming Cursor */
1940
+ .wabbit-chat-cursor {
1941
+ display: inline-block;
1942
+ color: var(--wabbit-primary);
1943
+ font-weight: bold;
1944
+ animation: wabbit-cursor-blink 1s infinite;
1945
+ margin-left: 2px;
1946
+ }
1947
+
1948
+ @keyframes wabbit-cursor-blink {
1949
+ 0%, 50% {
1950
+ opacity: 1;
1951
+ }
1952
+ 51%, 100% {
1953
+ opacity: 0;
1954
+ }
1955
+ }
1956
+
1728
1957
  .wabbit-chat-panel {
1729
1958
  animation: wabbit-fade-in 0.3s ease-out;
1730
1959
  }
@@ -1739,24 +1968,25 @@ function injectChatStyles(primaryColor = '#6366f1', theme) {
1739
1968
 
1740
1969
  /* Inline Chat Panel - renders inside container instead of fixed position */
1741
1970
  .wabbit-chat-panel.wabbit-chat-panel-inline {
1742
- position: relative !important;
1971
+ position: absolute !important;
1972
+ top: 0;
1973
+ left: 0;
1974
+ right: 0;
1975
+ bottom: 0;
1743
1976
  width: 100%;
1744
1977
  height: 100%;
1745
- min-height: 400px;
1746
1978
  max-height: none;
1747
1979
  max-width: none;
1748
- bottom: auto !important;
1749
- right: auto !important;
1750
- left: auto !important;
1751
- border-radius: 12px;
1752
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1980
+ border-radius: 0;
1981
+ box-shadow: none;
1753
1982
  animation: none; /* Disable slide-in animation for inline */
1754
1983
  }
1755
1984
 
1756
1985
  /* Inline mode messages area fills available space */
1757
1986
  .wabbit-chat-panel-inline .wabbit-chat-messages {
1758
- flex: 1;
1759
- min-height: 200px;
1987
+ flex: 1 1 0; /* flex-grow, flex-shrink, flex-basis: 0 for proper sizing */
1988
+ min-height: 0; /* Critical for nested flex scroll to work */
1989
+ overflow-y: auto !important;
1760
1990
  }
1761
1991
 
1762
1992
  /* Full-height variant for inline mode (used in standalone chat pages) */
@@ -1931,6 +2161,26 @@ class ChatWidget {
1931
2161
  this.panel.setMessages(messages);
1932
2162
  }
1933
2163
  };
2164
+ // Streaming message handlers
2165
+ this.wsClient.onMessageStart = (messageId) => {
2166
+ console.log('[Wabbit] Streaming message started:', messageId);
2167
+ if (this.panel) {
2168
+ this.panel.startStreamingMessage(messageId);
2169
+ }
2170
+ };
2171
+ this.wsClient.onMessageChunk = (messageId, chunk) => {
2172
+ if (this.panel) {
2173
+ this.panel.appendToStreamingMessage(messageId, chunk);
2174
+ }
2175
+ };
2176
+ this.wsClient.onMessageEnd = (messageId, metadata) => {
2177
+ console.log('[Wabbit] Streaming message ended:', messageId);
2178
+ if (this.panel) {
2179
+ this.panel.finishStreamingMessage(messageId, metadata);
2180
+ this.panel.setWaitingForResponse(false);
2181
+ }
2182
+ };
2183
+ // Non-streaming message handler (backward compatibility)
1934
2184
  this.wsClient.onMessage = (message) => {
1935
2185
  if (this.panel) {
1936
2186
  this.panel.addMessage(message);
@@ -1940,6 +2190,8 @@ class ChatWidget {
1940
2190
  this.wsClient.onError = (error) => {
1941
2191
  console.error('[Wabbit] Chat error:', error);
1942
2192
  if (this.panel) {
2193
+ // Clean up any active streaming messages on error
2194
+ this.panel.cancelAllStreamingMessages();
1943
2195
  this.panel.addSystemMessage(`Error: ${error}`);
1944
2196
  this.panel.setWaitingForResponse(false);
1945
2197
  }
@@ -1951,10 +2203,24 @@ class ChatWidget {
1951
2203
  };
1952
2204
  this.wsClient.onDisconnect = () => {
1953
2205
  if (this.panel) {
1954
- this.panel.addSystemMessage('Disconnected from chat service');
2206
+ // Clean up any active streaming messages on disconnect
2207
+ this.panel.cancelAllStreamingMessages();
2208
+ // Check if there are queued messages
2209
+ const queueSize = this.wsClient.getQueueSize();
2210
+ if (queueSize > 0) {
2211
+ this.panel.addSystemMessage(`Disconnected from chat service. ${queueSize} message(s) will be sent when reconnected.`);
2212
+ }
2213
+ else {
2214
+ this.panel.addSystemMessage('Disconnected from chat service');
2215
+ }
1955
2216
  this.panel.setDisabled(true);
1956
2217
  }
1957
2218
  };
2219
+ this.wsClient.onQueueOverflow = (message) => {
2220
+ if (this.panel) {
2221
+ this.panel.addSystemMessage(message);
2222
+ }
2223
+ };
1958
2224
  }
1959
2225
  /**
1960
2226
  * Handle trigger type configuration
@@ -3744,7 +4010,6 @@ class EmailCaptureWidget {
3744
4010
  if (this.emailCaptured) {
3745
4011
  return;
3746
4012
  }
3747
- // Only count user messages
3748
4013
  this.messageCount++;
3749
4014
  const triggerAfter = this.config.triggerAfterMessages || 3;
3750
4015
  if (this.messageCount >= triggerAfter) {