@wabbit-dashboard/embed 1.1.0 → 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,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
@@ -1240,10 +1288,21 @@
1240
1288
  formatMessage(content) {
1241
1289
  // Escape HTML first
1242
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>');
1243
1293
  // Simple markdown-like formatting
1244
1294
  formatted = formatted.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
1245
1295
  formatted = formatted.replace(/\*(.+?)\*/g, '<em>$1</em>');
1246
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
+ });
1247
1306
  formatted = formatted.replace(/\n/g, '<br>');
1248
1307
  return formatted;
1249
1308
  }
@@ -1796,6 +1855,20 @@
1796
1855
  line-height: 1.5;
1797
1856
  }
1798
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
+
1799
1872
  /* Typing Indicator */
1800
1873
  .wabbit-chat-typing {
1801
1874
  display: flex;
@@ -2031,7 +2104,7 @@
2031
2104
  * Chat Widget - Main class that integrates all chat components
2032
2105
  */
2033
2106
  class ChatWidget {
2034
- constructor(config) {
2107
+ constructor(config, storage) {
2035
2108
  this.wsClient = null;
2036
2109
  this.bubble = null;
2037
2110
  this.panel = null;
@@ -2041,6 +2114,7 @@
2041
2114
  this.onChatReadyCallback = null;
2042
2115
  this.chatReadyFired = false;
2043
2116
  this.config = config;
2117
+ this.storage = storage;
2044
2118
  this.onChatReadyCallback = config.onChatReady || null;
2045
2119
  }
2046
2120
  /**
@@ -2069,8 +2143,8 @@
2069
2143
  this.setupThemeWatcher();
2070
2144
  // Create WebSocket client
2071
2145
  const wsUrl = this.getWebSocketUrl();
2072
- this.wsClient = new ChatWebSocketClient(this.config.apiKey || '', wsUrl, null // Will use stored session if available
2073
- );
2146
+ this.wsClient = new ChatWebSocketClient(this.config.apiKey || '', wsUrl, null, // Will use stored session if available
2147
+ this.storage);
2074
2148
  // Set up event handlers
2075
2149
  this.setupWebSocketHandlers();
2076
2150
  // Only create bubble for widget mode (not inline)