@wabbit-dashboard/embed 1.1.0 → 1.1.2

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,105 +673,13 @@ 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;
@@ -655,6 +702,7 @@ class ChatWebSocketClient {
655
702
  this.onMessageEnd = null;
656
703
  this.apiKey = apiKey;
657
704
  this.wsUrl = wsUrl;
705
+ this.storage = storage;
658
706
  this.sessionId = sessionId || this.getStoredSessionId();
659
707
  }
660
708
  setStatus(status) {
@@ -723,9 +771,9 @@ class ChatWebSocketClient {
723
771
  switch (data.type) {
724
772
  case 'welcome':
725
773
  this.sessionId = data.session_id;
726
- // Store session ID in localStorage
774
+ // Store session ID in storage (localStorage or sessionStorage)
727
775
  if (this.sessionId) {
728
- storage.set('session_id', this.sessionId);
776
+ this.storage.set('session_id', this.sessionId);
729
777
  }
730
778
  if (this.onWelcome) {
731
779
  this.onWelcome(data.session_id, data.collection_id, data.message || 'Connected');
@@ -851,7 +899,7 @@ class ChatWebSocketClient {
851
899
  }
852
900
  clearSession() {
853
901
  this.sessionId = null;
854
- storage.remove('session_id');
902
+ this.storage.remove('session_id');
855
903
  // Clear message queue only when user explicitly starts new session
856
904
  // Queue should persist across reconnection attempts
857
905
  this.messageQueue = [];
@@ -864,7 +912,7 @@ class ChatWebSocketClient {
864
912
  this.messageQueue = [];
865
913
  }
866
914
  getStoredSessionId() {
867
- return storage.get('session_id') || null;
915
+ return this.storage.get('session_id') || null;
868
916
  }
869
917
  /**
870
918
  * Get current message queue size
@@ -999,6 +1047,7 @@ class ChatPanel {
999
1047
  this.messages = [];
1000
1048
  this.isWaitingForResponse = false;
1001
1049
  this.closeButton = null;
1050
+ this.statusIndicator = null;
1002
1051
  this.eventCleanup = [];
1003
1052
  this.streamingMessages = new Map();
1004
1053
  this.streamingCleanupInterval = null;
@@ -1134,6 +1183,14 @@ class ChatPanel {
1134
1183
  this.sendButton?.removeEventListener('click', sendClickHandler);
1135
1184
  });
1136
1185
  this.updateSendButtonState();
1186
+ this.statusIndicator = createElement('div', {
1187
+ class: 'wabbit-chat-status-indicator',
1188
+ 'data-status': 'connecting',
1189
+ title: 'Connecting...',
1190
+ 'aria-label': 'Connection status: connecting',
1191
+ role: 'status'
1192
+ });
1193
+ inputWrapper.appendChild(this.statusIndicator);
1137
1194
  inputWrapper.appendChild(this.inputElement);
1138
1195
  inputWrapper.appendChild(this.sendButton);
1139
1196
  inputArea.appendChild(inputWrapper);
@@ -1205,6 +1262,22 @@ class ChatPanel {
1205
1262
  this.updateSendButtonState();
1206
1263
  }
1207
1264
  }
1265
+ /**
1266
+ * Update the connection status indicator
1267
+ */
1268
+ setConnectionStatus(status) {
1269
+ if (!this.statusIndicator)
1270
+ return;
1271
+ this.statusIndicator.setAttribute('data-status', status);
1272
+ const labels = {
1273
+ connected: 'Connected',
1274
+ disconnected: 'Disconnected',
1275
+ connecting: 'Connecting...',
1276
+ reconnecting: 'Reconnecting...',
1277
+ };
1278
+ this.statusIndicator.setAttribute('title', labels[status]);
1279
+ this.statusIndicator.setAttribute('aria-label', `Connection status: ${labels[status].toLowerCase()}`);
1280
+ }
1208
1281
  /**
1209
1282
  * Render a single message
1210
1283
  */
@@ -1234,10 +1307,21 @@ class ChatPanel {
1234
1307
  formatMessage(content) {
1235
1308
  // Escape HTML first
1236
1309
  let formatted = escapeHtml(content);
1310
+ // Markdown links [text](url) - must come before URL auto-linking
1311
+ formatted = formatted.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
1237
1312
  // Simple markdown-like formatting
1238
1313
  formatted = formatted.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
1239
1314
  formatted = formatted.replace(/\*(.+?)\*/g, '<em>$1</em>');
1240
1315
  formatted = formatted.replace(/`(.+?)`/g, '<code style="background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px;">$1</code>');
1316
+ // Auto-link URLs (not already inside an href attribute)
1317
+ formatted = formatted.replace(/(?<!href=")(https?:\/\/[^\s<]+)/g, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>');
1318
+ // Auto-link email addresses
1319
+ formatted = formatted.replace(/(?<!["\/])([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g, '<a href="mailto:$1">$1</a>');
1320
+ // Auto-link phone numbers (international format with +)
1321
+ formatted = formatted.replace(/(?<!["\/])(\+\d[\d\s-]{7,})/g, (_match, phone) => {
1322
+ const cleanPhone = phone.replace(/[\s-]/g, '');
1323
+ return `<a href="tel:${cleanPhone}">${phone}</a>`;
1324
+ });
1241
1325
  formatted = formatted.replace(/\n/g, '<br>');
1242
1326
  return formatted;
1243
1327
  }
@@ -1476,6 +1560,7 @@ class ChatPanel {
1476
1560
  this.inputElement = null;
1477
1561
  this.sendButton = null;
1478
1562
  this.closeButton = null;
1563
+ this.statusIndicator = null;
1479
1564
  }
1480
1565
  }
1481
1566
  }
@@ -1790,6 +1875,20 @@ function injectChatStyles(primaryColor = '#6366f1', theme) {
1790
1875
  line-height: 1.5;
1791
1876
  }
1792
1877
 
1878
+ .wabbit-chat-message-content a {
1879
+ color: var(--wabbit-primary);
1880
+ text-decoration: underline;
1881
+ text-underline-offset: 2px;
1882
+ }
1883
+
1884
+ .wabbit-chat-message-content a:hover {
1885
+ opacity: 0.8;
1886
+ }
1887
+
1888
+ .wabbit-chat-message-user .wabbit-chat-message-content a {
1889
+ color: inherit;
1890
+ }
1891
+
1793
1892
  /* Typing Indicator */
1794
1893
  .wabbit-chat-typing {
1795
1894
  display: flex;
@@ -1902,6 +2001,35 @@ function injectChatStyles(primaryColor = '#6366f1', theme) {
1902
2001
  height: 20px;
1903
2002
  }
1904
2003
 
2004
+ /* Connection Status Indicator */
2005
+ .wabbit-chat-status-indicator {
2006
+ width: 8px;
2007
+ height: 8px;
2008
+ border-radius: 50%;
2009
+ flex-shrink: 0;
2010
+ align-self: center;
2011
+ transition: background-color 0.3s ease;
2012
+ }
2013
+
2014
+ .wabbit-chat-status-indicator[data-status="connected"] {
2015
+ background-color: #22c55e;
2016
+ }
2017
+
2018
+ .wabbit-chat-status-indicator[data-status="disconnected"] {
2019
+ background-color: #ef4444;
2020
+ }
2021
+
2022
+ .wabbit-chat-status-indicator[data-status="connecting"],
2023
+ .wabbit-chat-status-indicator[data-status="reconnecting"] {
2024
+ background-color: #f59e0b;
2025
+ animation: wabbit-status-pulse 1.5s ease-in-out infinite;
2026
+ }
2027
+
2028
+ @keyframes wabbit-status-pulse {
2029
+ 0%, 100% { opacity: 1; }
2030
+ 50% { opacity: 0.3; }
2031
+ }
2032
+
1905
2033
  /* Scrollbar */
1906
2034
  .wabbit-chat-messages::-webkit-scrollbar {
1907
2035
  width: 6px;
@@ -2025,7 +2153,7 @@ function adjustColor(color, amount) {
2025
2153
  * Chat Widget - Main class that integrates all chat components
2026
2154
  */
2027
2155
  class ChatWidget {
2028
- constructor(config) {
2156
+ constructor(config, storage) {
2029
2157
  this.wsClient = null;
2030
2158
  this.bubble = null;
2031
2159
  this.panel = null;
@@ -2035,6 +2163,7 @@ class ChatWidget {
2035
2163
  this.onChatReadyCallback = null;
2036
2164
  this.chatReadyFired = false;
2037
2165
  this.config = config;
2166
+ this.storage = storage;
2038
2167
  this.onChatReadyCallback = config.onChatReady || null;
2039
2168
  }
2040
2169
  /**
@@ -2063,8 +2192,8 @@ class ChatWidget {
2063
2192
  this.setupThemeWatcher();
2064
2193
  // Create WebSocket client
2065
2194
  const wsUrl = this.getWebSocketUrl();
2066
- this.wsClient = new ChatWebSocketClient(this.config.apiKey || '', wsUrl, null // Will use stored session if available
2067
- );
2195
+ this.wsClient = new ChatWebSocketClient(this.config.apiKey || '', wsUrl, null, // Will use stored session if available
2196
+ this.storage);
2068
2197
  // Set up event handlers
2069
2198
  this.setupWebSocketHandlers();
2070
2199
  // Only create bubble for widget mode (not inline)
@@ -2186,36 +2315,24 @@ class ChatWidget {
2186
2315
  this.wsClient.onError = (error) => {
2187
2316
  console.error('[Wabbit] Chat error:', error);
2188
2317
  if (this.panel) {
2189
- // Clean up any active streaming messages on error
2190
2318
  this.panel.cancelAllStreamingMessages();
2191
- this.panel.addSystemMessage(`Error: ${error}`);
2192
2319
  this.panel.setWaitingForResponse(false);
2193
2320
  }
2194
2321
  };
2195
2322
  this.wsClient.onStatusChange = (status) => {
2196
2323
  if (this.panel) {
2197
2324
  this.panel.setDisabled(status !== 'connected');
2325
+ this.panel.setConnectionStatus(status);
2198
2326
  }
2199
2327
  };
2200
2328
  this.wsClient.onDisconnect = () => {
2201
2329
  if (this.panel) {
2202
- // Clean up any active streaming messages on disconnect
2203
2330
  this.panel.cancelAllStreamingMessages();
2204
- // Check if there are queued messages
2205
- const queueSize = this.wsClient.getQueueSize();
2206
- if (queueSize > 0) {
2207
- this.panel.addSystemMessage(`Disconnected from chat service. ${queueSize} message(s) will be sent when reconnected.`);
2208
- }
2209
- else {
2210
- this.panel.addSystemMessage('Disconnected from chat service');
2211
- }
2212
2331
  this.panel.setDisabled(true);
2213
2332
  }
2214
2333
  };
2215
2334
  this.wsClient.onQueueOverflow = (message) => {
2216
- if (this.panel) {
2217
- this.panel.addSystemMessage(message);
2218
- }
2335
+ console.warn('[Wabbit] Queue overflow:', message);
2219
2336
  };
2220
2337
  }
2221
2338
  /**