@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.
package/README.md CHANGED
@@ -127,6 +127,34 @@ Wabbit.init({
127
127
  });
128
128
  ```
129
129
 
130
+ ### Session Persistence
131
+
132
+ Control whether chat sessions persist across browser sessions:
133
+
134
+ ```javascript
135
+ Wabbit.init({
136
+ apiKey: 'pk_live_xxx',
137
+ persistSession: true, // Default: sessions persist (uses localStorage)
138
+ chat: {
139
+ enabled: true,
140
+ collectionId: 'abc123'
141
+ }
142
+ });
143
+ ```
144
+
145
+ **Options:**
146
+ - `persistSession: true` (default) - Sessions persist across browser close/reopen using **localStorage**. Users can continue conversations after reopening the browser.
147
+ - `persistSession: false` - Sessions are cleared when browser closes using **sessionStorage**. Users start fresh each browser session, but conversations persist during page refreshes.
148
+
149
+ **Use Cases:**
150
+ - **Persistent (true)**: Customer support, ongoing consultations, learning platforms
151
+ - **Ephemeral (false)**: Sensitive data, temporary assistance, demo sites
152
+
153
+ **Technical Details:**
154
+ - `true`: Session ID stored in localStorage, survives browser restart
155
+ - `false`: Session ID stored in sessionStorage, cleared on browser close but survives page refresh
156
+ - Message history fetched from server on reconnect (not stored locally)
157
+
130
158
  ## Testing
131
159
 
132
160
  ### Option 1: Copy Embed Code from Dashboard (Recommended)
@@ -266,6 +294,7 @@ Initialize the Wabbit SDK.
266
294
  **Parameters:**
267
295
  - `config.apiKey` (string, required): Your Wabbit API key
268
296
  - `config.apiUrl` (string, optional): API base URL (default: auto-detected)
297
+ - `config.persistSession` (boolean, optional): Whether to persist sessions across browser sessions (default: `true`)
269
298
  - `config.chat` (ChatConfig, optional): Chat widget configuration
270
299
  - `config.forms` (FormConfig, optional): Form widget configuration
271
300
  - `config.emailCapture` (EmailCaptureConfig, optional): Email capture configuration
@@ -274,6 +303,7 @@ Initialize the Wabbit SDK.
274
303
  ```javascript
275
304
  Wabbit.init({
276
305
  apiKey: 'pk_live_xxx',
306
+ persistSession: true, // Optional: default is true
277
307
  chat: { enabled: true, collectionId: 'abc123' }
278
308
  });
279
309
  ```
@@ -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,105 +677,13 @@ 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;
@@ -659,6 +706,7 @@ class ChatWebSocketClient {
659
706
  this.onMessageEnd = null;
660
707
  this.apiKey = apiKey;
661
708
  this.wsUrl = wsUrl;
709
+ this.storage = storage;
662
710
  this.sessionId = sessionId || this.getStoredSessionId();
663
711
  }
664
712
  setStatus(status) {
@@ -727,9 +775,9 @@ class ChatWebSocketClient {
727
775
  switch (data.type) {
728
776
  case 'welcome':
729
777
  this.sessionId = data.session_id;
730
- // Store session ID in localStorage
778
+ // Store session ID in storage (localStorage or sessionStorage)
731
779
  if (this.sessionId) {
732
- storage.set('session_id', this.sessionId);
780
+ this.storage.set('session_id', this.sessionId);
733
781
  }
734
782
  if (this.onWelcome) {
735
783
  this.onWelcome(data.session_id, data.collection_id, data.message || 'Connected');
@@ -855,7 +903,7 @@ class ChatWebSocketClient {
855
903
  }
856
904
  clearSession() {
857
905
  this.sessionId = null;
858
- storage.remove('session_id');
906
+ this.storage.remove('session_id');
859
907
  // Clear message queue only when user explicitly starts new session
860
908
  // Queue should persist across reconnection attempts
861
909
  this.messageQueue = [];
@@ -868,7 +916,7 @@ class ChatWebSocketClient {
868
916
  this.messageQueue = [];
869
917
  }
870
918
  getStoredSessionId() {
871
- return storage.get('session_id') || null;
919
+ return this.storage.get('session_id') || null;
872
920
  }
873
921
  /**
874
922
  * Get current message queue size
@@ -1003,6 +1051,7 @@ class ChatPanel {
1003
1051
  this.messages = [];
1004
1052
  this.isWaitingForResponse = false;
1005
1053
  this.closeButton = null;
1054
+ this.statusIndicator = null;
1006
1055
  this.eventCleanup = [];
1007
1056
  this.streamingMessages = new Map();
1008
1057
  this.streamingCleanupInterval = null;
@@ -1138,6 +1187,14 @@ class ChatPanel {
1138
1187
  this.sendButton?.removeEventListener('click', sendClickHandler);
1139
1188
  });
1140
1189
  this.updateSendButtonState();
1190
+ this.statusIndicator = createElement('div', {
1191
+ class: 'wabbit-chat-status-indicator',
1192
+ 'data-status': 'connecting',
1193
+ title: 'Connecting...',
1194
+ 'aria-label': 'Connection status: connecting',
1195
+ role: 'status'
1196
+ });
1197
+ inputWrapper.appendChild(this.statusIndicator);
1141
1198
  inputWrapper.appendChild(this.inputElement);
1142
1199
  inputWrapper.appendChild(this.sendButton);
1143
1200
  inputArea.appendChild(inputWrapper);
@@ -1209,6 +1266,22 @@ class ChatPanel {
1209
1266
  this.updateSendButtonState();
1210
1267
  }
1211
1268
  }
1269
+ /**
1270
+ * Update the connection status indicator
1271
+ */
1272
+ setConnectionStatus(status) {
1273
+ if (!this.statusIndicator)
1274
+ return;
1275
+ this.statusIndicator.setAttribute('data-status', status);
1276
+ const labels = {
1277
+ connected: 'Connected',
1278
+ disconnected: 'Disconnected',
1279
+ connecting: 'Connecting...',
1280
+ reconnecting: 'Reconnecting...',
1281
+ };
1282
+ this.statusIndicator.setAttribute('title', labels[status]);
1283
+ this.statusIndicator.setAttribute('aria-label', `Connection status: ${labels[status].toLowerCase()}`);
1284
+ }
1212
1285
  /**
1213
1286
  * Render a single message
1214
1287
  */
@@ -1238,10 +1311,21 @@ class ChatPanel {
1238
1311
  formatMessage(content) {
1239
1312
  // Escape HTML first
1240
1313
  let formatted = escapeHtml(content);
1314
+ // Markdown links [text](url) - must come before URL auto-linking
1315
+ formatted = formatted.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
1241
1316
  // Simple markdown-like formatting
1242
1317
  formatted = formatted.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
1243
1318
  formatted = formatted.replace(/\*(.+?)\*/g, '<em>$1</em>');
1244
1319
  formatted = formatted.replace(/`(.+?)`/g, '<code style="background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px;">$1</code>');
1320
+ // Auto-link URLs (not already inside an href attribute)
1321
+ formatted = formatted.replace(/(?<!href=")(https?:\/\/[^\s<]+)/g, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>');
1322
+ // Auto-link email addresses
1323
+ formatted = formatted.replace(/(?<!["\/])([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g, '<a href="mailto:$1">$1</a>');
1324
+ // Auto-link phone numbers (international format with +)
1325
+ formatted = formatted.replace(/(?<!["\/])(\+\d[\d\s-]{7,})/g, (_match, phone) => {
1326
+ const cleanPhone = phone.replace(/[\s-]/g, '');
1327
+ return `<a href="tel:${cleanPhone}">${phone}</a>`;
1328
+ });
1245
1329
  formatted = formatted.replace(/\n/g, '<br>');
1246
1330
  return formatted;
1247
1331
  }
@@ -1480,6 +1564,7 @@ class ChatPanel {
1480
1564
  this.inputElement = null;
1481
1565
  this.sendButton = null;
1482
1566
  this.closeButton = null;
1567
+ this.statusIndicator = null;
1483
1568
  }
1484
1569
  }
1485
1570
  }
@@ -1794,6 +1879,20 @@ function injectChatStyles(primaryColor = '#6366f1', theme) {
1794
1879
  line-height: 1.5;
1795
1880
  }
1796
1881
 
1882
+ .wabbit-chat-message-content a {
1883
+ color: var(--wabbit-primary);
1884
+ text-decoration: underline;
1885
+ text-underline-offset: 2px;
1886
+ }
1887
+
1888
+ .wabbit-chat-message-content a:hover {
1889
+ opacity: 0.8;
1890
+ }
1891
+
1892
+ .wabbit-chat-message-user .wabbit-chat-message-content a {
1893
+ color: inherit;
1894
+ }
1895
+
1797
1896
  /* Typing Indicator */
1798
1897
  .wabbit-chat-typing {
1799
1898
  display: flex;
@@ -1906,6 +2005,35 @@ function injectChatStyles(primaryColor = '#6366f1', theme) {
1906
2005
  height: 20px;
1907
2006
  }
1908
2007
 
2008
+ /* Connection Status Indicator */
2009
+ .wabbit-chat-status-indicator {
2010
+ width: 8px;
2011
+ height: 8px;
2012
+ border-radius: 50%;
2013
+ flex-shrink: 0;
2014
+ align-self: center;
2015
+ transition: background-color 0.3s ease;
2016
+ }
2017
+
2018
+ .wabbit-chat-status-indicator[data-status="connected"] {
2019
+ background-color: #22c55e;
2020
+ }
2021
+
2022
+ .wabbit-chat-status-indicator[data-status="disconnected"] {
2023
+ background-color: #ef4444;
2024
+ }
2025
+
2026
+ .wabbit-chat-status-indicator[data-status="connecting"],
2027
+ .wabbit-chat-status-indicator[data-status="reconnecting"] {
2028
+ background-color: #f59e0b;
2029
+ animation: wabbit-status-pulse 1.5s ease-in-out infinite;
2030
+ }
2031
+
2032
+ @keyframes wabbit-status-pulse {
2033
+ 0%, 100% { opacity: 1; }
2034
+ 50% { opacity: 0.3; }
2035
+ }
2036
+
1909
2037
  /* Scrollbar */
1910
2038
  .wabbit-chat-messages::-webkit-scrollbar {
1911
2039
  width: 6px;
@@ -2029,7 +2157,7 @@ function adjustColor(color, amount) {
2029
2157
  * Chat Widget - Main class that integrates all chat components
2030
2158
  */
2031
2159
  class ChatWidget {
2032
- constructor(config) {
2160
+ constructor(config, storage) {
2033
2161
  this.wsClient = null;
2034
2162
  this.bubble = null;
2035
2163
  this.panel = null;
@@ -2039,6 +2167,7 @@ class ChatWidget {
2039
2167
  this.onChatReadyCallback = null;
2040
2168
  this.chatReadyFired = false;
2041
2169
  this.config = config;
2170
+ this.storage = storage;
2042
2171
  this.onChatReadyCallback = config.onChatReady || null;
2043
2172
  }
2044
2173
  /**
@@ -2067,8 +2196,8 @@ class ChatWidget {
2067
2196
  this.setupThemeWatcher();
2068
2197
  // Create WebSocket client
2069
2198
  const wsUrl = this.getWebSocketUrl();
2070
- this.wsClient = new ChatWebSocketClient(this.config.apiKey || '', wsUrl, null // Will use stored session if available
2071
- );
2199
+ this.wsClient = new ChatWebSocketClient(this.config.apiKey || '', wsUrl, null, // Will use stored session if available
2200
+ this.storage);
2072
2201
  // Set up event handlers
2073
2202
  this.setupWebSocketHandlers();
2074
2203
  // Only create bubble for widget mode (not inline)
@@ -2190,36 +2319,24 @@ class ChatWidget {
2190
2319
  this.wsClient.onError = (error) => {
2191
2320
  console.error('[Wabbit] Chat error:', error);
2192
2321
  if (this.panel) {
2193
- // Clean up any active streaming messages on error
2194
2322
  this.panel.cancelAllStreamingMessages();
2195
- this.panel.addSystemMessage(`Error: ${error}`);
2196
2323
  this.panel.setWaitingForResponse(false);
2197
2324
  }
2198
2325
  };
2199
2326
  this.wsClient.onStatusChange = (status) => {
2200
2327
  if (this.panel) {
2201
2328
  this.panel.setDisabled(status !== 'connected');
2329
+ this.panel.setConnectionStatus(status);
2202
2330
  }
2203
2331
  };
2204
2332
  this.wsClient.onDisconnect = () => {
2205
2333
  if (this.panel) {
2206
- // Clean up any active streaming messages on disconnect
2207
2334
  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
- }
2216
2335
  this.panel.setDisabled(true);
2217
2336
  }
2218
2337
  };
2219
2338
  this.wsClient.onQueueOverflow = (message) => {
2220
- if (this.panel) {
2221
- this.panel.addSystemMessage(message);
2222
- }
2339
+ console.warn('[Wabbit] Queue overflow:', message);
2223
2340
  };
2224
2341
  }
2225
2342
  /**