@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.
@@ -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,105 +679,13 @@
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;
@@ -661,6 +708,7 @@
661
708
  this.onMessageEnd = null;
662
709
  this.apiKey = apiKey;
663
710
  this.wsUrl = wsUrl;
711
+ this.storage = storage;
664
712
  this.sessionId = sessionId || this.getStoredSessionId();
665
713
  }
666
714
  setStatus(status) {
@@ -729,9 +777,9 @@
729
777
  switch (data.type) {
730
778
  case 'welcome':
731
779
  this.sessionId = data.session_id;
732
- // Store session ID in localStorage
780
+ // Store session ID in storage (localStorage or sessionStorage)
733
781
  if (this.sessionId) {
734
- storage.set('session_id', this.sessionId);
782
+ this.storage.set('session_id', this.sessionId);
735
783
  }
736
784
  if (this.onWelcome) {
737
785
  this.onWelcome(data.session_id, data.collection_id, data.message || 'Connected');
@@ -857,7 +905,7 @@
857
905
  }
858
906
  clearSession() {
859
907
  this.sessionId = null;
860
- storage.remove('session_id');
908
+ this.storage.remove('session_id');
861
909
  // Clear message queue only when user explicitly starts new session
862
910
  // Queue should persist across reconnection attempts
863
911
  this.messageQueue = [];
@@ -870,7 +918,7 @@
870
918
  this.messageQueue = [];
871
919
  }
872
920
  getStoredSessionId() {
873
- return storage.get('session_id') || null;
921
+ return this.storage.get('session_id') || null;
874
922
  }
875
923
  /**
876
924
  * Get current message queue size
@@ -1005,6 +1053,7 @@
1005
1053
  this.messages = [];
1006
1054
  this.isWaitingForResponse = false;
1007
1055
  this.closeButton = null;
1056
+ this.statusIndicator = null;
1008
1057
  this.eventCleanup = [];
1009
1058
  this.streamingMessages = new Map();
1010
1059
  this.streamingCleanupInterval = null;
@@ -1140,6 +1189,14 @@
1140
1189
  this.sendButton?.removeEventListener('click', sendClickHandler);
1141
1190
  });
1142
1191
  this.updateSendButtonState();
1192
+ this.statusIndicator = createElement('div', {
1193
+ class: 'wabbit-chat-status-indicator',
1194
+ 'data-status': 'connecting',
1195
+ title: 'Connecting...',
1196
+ 'aria-label': 'Connection status: connecting',
1197
+ role: 'status'
1198
+ });
1199
+ inputWrapper.appendChild(this.statusIndicator);
1143
1200
  inputWrapper.appendChild(this.inputElement);
1144
1201
  inputWrapper.appendChild(this.sendButton);
1145
1202
  inputArea.appendChild(inputWrapper);
@@ -1211,6 +1268,22 @@
1211
1268
  this.updateSendButtonState();
1212
1269
  }
1213
1270
  }
1271
+ /**
1272
+ * Update the connection status indicator
1273
+ */
1274
+ setConnectionStatus(status) {
1275
+ if (!this.statusIndicator)
1276
+ return;
1277
+ this.statusIndicator.setAttribute('data-status', status);
1278
+ const labels = {
1279
+ connected: 'Connected',
1280
+ disconnected: 'Disconnected',
1281
+ connecting: 'Connecting...',
1282
+ reconnecting: 'Reconnecting...',
1283
+ };
1284
+ this.statusIndicator.setAttribute('title', labels[status]);
1285
+ this.statusIndicator.setAttribute('aria-label', `Connection status: ${labels[status].toLowerCase()}`);
1286
+ }
1214
1287
  /**
1215
1288
  * Render a single message
1216
1289
  */
@@ -1240,10 +1313,21 @@
1240
1313
  formatMessage(content) {
1241
1314
  // Escape HTML first
1242
1315
  let formatted = escapeHtml(content);
1316
+ // Markdown links [text](url) - must come before URL auto-linking
1317
+ formatted = formatted.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
1243
1318
  // Simple markdown-like formatting
1244
1319
  formatted = formatted.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
1245
1320
  formatted = formatted.replace(/\*(.+?)\*/g, '<em>$1</em>');
1246
1321
  formatted = formatted.replace(/`(.+?)`/g, '<code style="background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px;">$1</code>');
1322
+ // Auto-link URLs (not already inside an href attribute)
1323
+ formatted = formatted.replace(/(?<!href=")(https?:\/\/[^\s<]+)/g, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>');
1324
+ // Auto-link email addresses
1325
+ formatted = formatted.replace(/(?<!["\/])([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g, '<a href="mailto:$1">$1</a>');
1326
+ // Auto-link phone numbers (international format with +)
1327
+ formatted = formatted.replace(/(?<!["\/])(\+\d[\d\s-]{7,})/g, (_match, phone) => {
1328
+ const cleanPhone = phone.replace(/[\s-]/g, '');
1329
+ return `<a href="tel:${cleanPhone}">${phone}</a>`;
1330
+ });
1247
1331
  formatted = formatted.replace(/\n/g, '<br>');
1248
1332
  return formatted;
1249
1333
  }
@@ -1482,6 +1566,7 @@
1482
1566
  this.inputElement = null;
1483
1567
  this.sendButton = null;
1484
1568
  this.closeButton = null;
1569
+ this.statusIndicator = null;
1485
1570
  }
1486
1571
  }
1487
1572
  }
@@ -1796,6 +1881,20 @@
1796
1881
  line-height: 1.5;
1797
1882
  }
1798
1883
 
1884
+ .wabbit-chat-message-content a {
1885
+ color: var(--wabbit-primary);
1886
+ text-decoration: underline;
1887
+ text-underline-offset: 2px;
1888
+ }
1889
+
1890
+ .wabbit-chat-message-content a:hover {
1891
+ opacity: 0.8;
1892
+ }
1893
+
1894
+ .wabbit-chat-message-user .wabbit-chat-message-content a {
1895
+ color: inherit;
1896
+ }
1897
+
1799
1898
  /* Typing Indicator */
1800
1899
  .wabbit-chat-typing {
1801
1900
  display: flex;
@@ -1908,6 +2007,35 @@
1908
2007
  height: 20px;
1909
2008
  }
1910
2009
 
2010
+ /* Connection Status Indicator */
2011
+ .wabbit-chat-status-indicator {
2012
+ width: 8px;
2013
+ height: 8px;
2014
+ border-radius: 50%;
2015
+ flex-shrink: 0;
2016
+ align-self: center;
2017
+ transition: background-color 0.3s ease;
2018
+ }
2019
+
2020
+ .wabbit-chat-status-indicator[data-status="connected"] {
2021
+ background-color: #22c55e;
2022
+ }
2023
+
2024
+ .wabbit-chat-status-indicator[data-status="disconnected"] {
2025
+ background-color: #ef4444;
2026
+ }
2027
+
2028
+ .wabbit-chat-status-indicator[data-status="connecting"],
2029
+ .wabbit-chat-status-indicator[data-status="reconnecting"] {
2030
+ background-color: #f59e0b;
2031
+ animation: wabbit-status-pulse 1.5s ease-in-out infinite;
2032
+ }
2033
+
2034
+ @keyframes wabbit-status-pulse {
2035
+ 0%, 100% { opacity: 1; }
2036
+ 50% { opacity: 0.3; }
2037
+ }
2038
+
1911
2039
  /* Scrollbar */
1912
2040
  .wabbit-chat-messages::-webkit-scrollbar {
1913
2041
  width: 6px;
@@ -2031,7 +2159,7 @@
2031
2159
  * Chat Widget - Main class that integrates all chat components
2032
2160
  */
2033
2161
  class ChatWidget {
2034
- constructor(config) {
2162
+ constructor(config, storage) {
2035
2163
  this.wsClient = null;
2036
2164
  this.bubble = null;
2037
2165
  this.panel = null;
@@ -2041,6 +2169,7 @@
2041
2169
  this.onChatReadyCallback = null;
2042
2170
  this.chatReadyFired = false;
2043
2171
  this.config = config;
2172
+ this.storage = storage;
2044
2173
  this.onChatReadyCallback = config.onChatReady || null;
2045
2174
  }
2046
2175
  /**
@@ -2069,8 +2198,8 @@
2069
2198
  this.setupThemeWatcher();
2070
2199
  // Create WebSocket client
2071
2200
  const wsUrl = this.getWebSocketUrl();
2072
- this.wsClient = new ChatWebSocketClient(this.config.apiKey || '', wsUrl, null // Will use stored session if available
2073
- );
2201
+ this.wsClient = new ChatWebSocketClient(this.config.apiKey || '', wsUrl, null, // Will use stored session if available
2202
+ this.storage);
2074
2203
  // Set up event handlers
2075
2204
  this.setupWebSocketHandlers();
2076
2205
  // Only create bubble for widget mode (not inline)
@@ -2192,36 +2321,24 @@
2192
2321
  this.wsClient.onError = (error) => {
2193
2322
  console.error('[Wabbit] Chat error:', error);
2194
2323
  if (this.panel) {
2195
- // Clean up any active streaming messages on error
2196
2324
  this.panel.cancelAllStreamingMessages();
2197
- this.panel.addSystemMessage(`Error: ${error}`);
2198
2325
  this.panel.setWaitingForResponse(false);
2199
2326
  }
2200
2327
  };
2201
2328
  this.wsClient.onStatusChange = (status) => {
2202
2329
  if (this.panel) {
2203
2330
  this.panel.setDisabled(status !== 'connected');
2331
+ this.panel.setConnectionStatus(status);
2204
2332
  }
2205
2333
  };
2206
2334
  this.wsClient.onDisconnect = () => {
2207
2335
  if (this.panel) {
2208
- // Clean up any active streaming messages on disconnect
2209
2336
  this.panel.cancelAllStreamingMessages();
2210
- // Check if there are queued messages
2211
- const queueSize = this.wsClient.getQueueSize();
2212
- if (queueSize > 0) {
2213
- this.panel.addSystemMessage(`Disconnected from chat service. ${queueSize} message(s) will be sent when reconnected.`);
2214
- }
2215
- else {
2216
- this.panel.addSystemMessage('Disconnected from chat service');
2217
- }
2218
2337
  this.panel.setDisabled(true);
2219
2338
  }
2220
2339
  };
2221
2340
  this.wsClient.onQueueOverflow = (message) => {
2222
- if (this.panel) {
2223
- this.panel.addSystemMessage(message);
2224
- }
2341
+ console.warn('[Wabbit] Queue overflow:', message);
2225
2342
  };
2226
2343
  }
2227
2344
  /**