@wabbit-dashboard/embed 1.0.17 → 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.
- package/README.md +30 -0
- package/dist/wabbit-embed.cjs.js +459 -116
- package/dist/wabbit-embed.cjs.js.map +1 -1
- package/dist/wabbit-embed.d.ts +93 -42
- package/dist/wabbit-embed.esm.js +459 -116
- package/dist/wabbit-embed.esm.js.map +1 -1
- package/dist/wabbit-embed.umd.js +459 -116
- package/dist/wabbit-embed.umd.js.map +1 -1
- package/dist/wabbit-embed.umd.min.js +1 -1
- package/dist/wabbit-embed.umd.min.js.map +1 -1
- package/package.json +1 -1
package/dist/wabbit-embed.umd.js
CHANGED
|
@@ -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,110 +679,21 @@
|
|
|
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;
|
|
645
692
|
this.maxReconnectAttempts = 3;
|
|
646
693
|
this.reconnectDelay = 1000;
|
|
694
|
+
// Message queue for handling messages during disconnection
|
|
695
|
+
this.messageQueue = [];
|
|
696
|
+
this.MAX_QUEUE_SIZE = 10;
|
|
647
697
|
// Event handlers
|
|
648
698
|
this.onStatusChange = null;
|
|
649
699
|
this.onWelcome = null;
|
|
@@ -651,8 +701,14 @@
|
|
|
651
701
|
this.onMessageHistory = null;
|
|
652
702
|
this.onError = null;
|
|
653
703
|
this.onDisconnect = null;
|
|
704
|
+
this.onQueueOverflow = null;
|
|
705
|
+
// Streaming event handlers
|
|
706
|
+
this.onMessageStart = null;
|
|
707
|
+
this.onMessageChunk = null;
|
|
708
|
+
this.onMessageEnd = null;
|
|
654
709
|
this.apiKey = apiKey;
|
|
655
710
|
this.wsUrl = wsUrl;
|
|
711
|
+
this.storage = storage;
|
|
656
712
|
this.sessionId = sessionId || this.getStoredSessionId();
|
|
657
713
|
}
|
|
658
714
|
setStatus(status) {
|
|
@@ -682,6 +738,8 @@
|
|
|
682
738
|
this.setStatus('connected');
|
|
683
739
|
this.reconnectAttempts = 0;
|
|
684
740
|
console.log('[Wabbit] WebSocket connected');
|
|
741
|
+
// Flush any queued messages
|
|
742
|
+
this.flushMessageQueue();
|
|
685
743
|
};
|
|
686
744
|
this.ws.onmessage = (event) => {
|
|
687
745
|
try {
|
|
@@ -719,9 +777,9 @@
|
|
|
719
777
|
switch (data.type) {
|
|
720
778
|
case 'welcome':
|
|
721
779
|
this.sessionId = data.session_id;
|
|
722
|
-
// Store session ID in localStorage
|
|
780
|
+
// Store session ID in storage (localStorage or sessionStorage)
|
|
723
781
|
if (this.sessionId) {
|
|
724
|
-
storage.set('session_id', this.sessionId);
|
|
782
|
+
this.storage.set('session_id', this.sessionId);
|
|
725
783
|
}
|
|
726
784
|
if (this.onWelcome) {
|
|
727
785
|
this.onWelcome(data.session_id, data.collection_id, data.message || 'Connected');
|
|
@@ -740,7 +798,26 @@
|
|
|
740
798
|
this.onMessageHistory(messages);
|
|
741
799
|
}
|
|
742
800
|
break;
|
|
801
|
+
case 'assistant_message_start':
|
|
802
|
+
// Streaming: Start of assistant message
|
|
803
|
+
if (this.onMessageStart) {
|
|
804
|
+
this.onMessageStart(data.message_id);
|
|
805
|
+
}
|
|
806
|
+
break;
|
|
807
|
+
case 'assistant_message_chunk':
|
|
808
|
+
// Streaming: Chunk of assistant message
|
|
809
|
+
if (this.onMessageChunk) {
|
|
810
|
+
this.onMessageChunk(data.message_id, data.content);
|
|
811
|
+
}
|
|
812
|
+
break;
|
|
813
|
+
case 'assistant_message_end':
|
|
814
|
+
// Streaming: End of assistant message
|
|
815
|
+
if (this.onMessageEnd) {
|
|
816
|
+
this.onMessageEnd(data.message_id, data.metadata);
|
|
817
|
+
}
|
|
818
|
+
break;
|
|
743
819
|
case 'assistant_message':
|
|
820
|
+
// Non-streaming: Complete assistant message (backward compatibility)
|
|
744
821
|
if (this.onMessage) {
|
|
745
822
|
const message = {
|
|
746
823
|
id: data.message_id || crypto.randomUUID(),
|
|
@@ -762,18 +839,27 @@
|
|
|
762
839
|
}
|
|
763
840
|
}
|
|
764
841
|
sendMessage(content, metadata) {
|
|
765
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
766
|
-
if (this.onError) {
|
|
767
|
-
this.onError('Not connected to chat service');
|
|
768
|
-
}
|
|
769
|
-
return;
|
|
770
|
-
}
|
|
771
842
|
const message = {
|
|
772
843
|
type: 'message',
|
|
773
844
|
content,
|
|
774
845
|
metadata: metadata || {},
|
|
775
846
|
};
|
|
776
|
-
|
|
847
|
+
// If connected, send immediately
|
|
848
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
849
|
+
this.ws.send(JSON.stringify(message));
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
// If disconnected or reconnecting, queue the message
|
|
853
|
+
if (this.messageQueue.length >= this.MAX_QUEUE_SIZE) {
|
|
854
|
+
console.warn('[Wabbit] Message queue full, cannot queue message');
|
|
855
|
+
if (this.onQueueOverflow) {
|
|
856
|
+
this.onQueueOverflow('Message queue full. Please wait for reconnection.');
|
|
857
|
+
}
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
// Add to queue
|
|
861
|
+
this.messageQueue.push({ content, metadata });
|
|
862
|
+
console.log(`[Wabbit] Message queued (${this.messageQueue.length}/${this.MAX_QUEUE_SIZE})`);
|
|
777
863
|
}
|
|
778
864
|
disconnect() {
|
|
779
865
|
if (this.ws) {
|
|
@@ -795,12 +881,50 @@
|
|
|
795
881
|
this.connect();
|
|
796
882
|
}, delay);
|
|
797
883
|
}
|
|
884
|
+
/**
|
|
885
|
+
* Flush queued messages after connection is established
|
|
886
|
+
*/
|
|
887
|
+
flushMessageQueue() {
|
|
888
|
+
if (this.messageQueue.length === 0) {
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
console.log(`[Wabbit] Flushing ${this.messageQueue.length} queued message(s)`);
|
|
892
|
+
// Send all queued messages in order
|
|
893
|
+
const queuedMessages = [...this.messageQueue];
|
|
894
|
+
this.messageQueue = [];
|
|
895
|
+
queuedMessages.forEach(({ content, metadata }) => {
|
|
896
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
897
|
+
const message = {
|
|
898
|
+
type: 'message',
|
|
899
|
+
content,
|
|
900
|
+
metadata: metadata || {},
|
|
901
|
+
};
|
|
902
|
+
this.ws.send(JSON.stringify(message));
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
}
|
|
798
906
|
clearSession() {
|
|
799
907
|
this.sessionId = null;
|
|
800
|
-
storage.remove('session_id');
|
|
908
|
+
this.storage.remove('session_id');
|
|
909
|
+
// Clear message queue only when user explicitly starts new session
|
|
910
|
+
// Queue should persist across reconnection attempts
|
|
911
|
+
this.messageQueue = [];
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Clear the message queue
|
|
915
|
+
* Only call this when explicitly abandoning queued messages
|
|
916
|
+
*/
|
|
917
|
+
clearMessageQueue() {
|
|
918
|
+
this.messageQueue = [];
|
|
801
919
|
}
|
|
802
920
|
getStoredSessionId() {
|
|
803
|
-
return storage.get('session_id') || null;
|
|
921
|
+
return this.storage.get('session_id') || null;
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Get current message queue size
|
|
925
|
+
*/
|
|
926
|
+
getQueueSize() {
|
|
927
|
+
return this.messageQueue.length;
|
|
804
928
|
}
|
|
805
929
|
}
|
|
806
930
|
|
|
@@ -930,6 +1054,9 @@
|
|
|
930
1054
|
this.isWaitingForResponse = false;
|
|
931
1055
|
this.closeButton = null;
|
|
932
1056
|
this.eventCleanup = [];
|
|
1057
|
+
this.streamingMessages = new Map();
|
|
1058
|
+
this.streamingCleanupInterval = null;
|
|
1059
|
+
this.STREAMING_TIMEOUT_MS = 30000; // 30 seconds timeout for stale streams
|
|
933
1060
|
this.options = options;
|
|
934
1061
|
}
|
|
935
1062
|
/**
|
|
@@ -1161,13 +1288,150 @@
|
|
|
1161
1288
|
formatMessage(content) {
|
|
1162
1289
|
// Escape HTML first
|
|
1163
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>');
|
|
1164
1293
|
// Simple markdown-like formatting
|
|
1165
1294
|
formatted = formatted.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
1166
1295
|
formatted = formatted.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
1167
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
|
+
});
|
|
1168
1306
|
formatted = formatted.replace(/\n/g, '<br>');
|
|
1169
1307
|
return formatted;
|
|
1170
1308
|
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Start a streaming assistant message
|
|
1311
|
+
*/
|
|
1312
|
+
startStreamingMessage(messageId) {
|
|
1313
|
+
if (!this.messagesContainer)
|
|
1314
|
+
return;
|
|
1315
|
+
// Create message container
|
|
1316
|
+
const messageDiv = createElement('div', {
|
|
1317
|
+
class: 'wabbit-chat-message wabbit-chat-message-assistant wabbit-chat-message-streaming',
|
|
1318
|
+
'data-message-id': messageId,
|
|
1319
|
+
});
|
|
1320
|
+
const bubble = createElement('div', { class: 'wabbit-chat-message-bubble' });
|
|
1321
|
+
const content = createElement('div', { class: 'wabbit-chat-message-content' });
|
|
1322
|
+
// Add streaming cursor
|
|
1323
|
+
content.innerHTML = '<span class="wabbit-chat-cursor">▋</span>';
|
|
1324
|
+
bubble.appendChild(content);
|
|
1325
|
+
messageDiv.appendChild(bubble);
|
|
1326
|
+
this.messagesContainer.appendChild(messageDiv);
|
|
1327
|
+
// Store reference with timestamp for timeout tracking
|
|
1328
|
+
this.streamingMessages.set(messageId, {
|
|
1329
|
+
element: messageDiv,
|
|
1330
|
+
content: '',
|
|
1331
|
+
startTime: Date.now()
|
|
1332
|
+
});
|
|
1333
|
+
// Start cleanup interval if not already running
|
|
1334
|
+
if (!this.streamingCleanupInterval) {
|
|
1335
|
+
this.streamingCleanupInterval = window.setInterval(() => {
|
|
1336
|
+
this.cleanupStaleStreamingMessages();
|
|
1337
|
+
}, 5000); // Check every 5 seconds
|
|
1338
|
+
}
|
|
1339
|
+
this.scrollToBottom();
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Append chunk to streaming message
|
|
1343
|
+
*/
|
|
1344
|
+
appendToStreamingMessage(messageId, chunk) {
|
|
1345
|
+
const streaming = this.streamingMessages.get(messageId);
|
|
1346
|
+
if (!streaming) {
|
|
1347
|
+
console.warn('[ChatPanel] No streaming message found for ID:', messageId);
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
// Append to content
|
|
1351
|
+
streaming.content += chunk;
|
|
1352
|
+
// Update DOM
|
|
1353
|
+
const contentDiv = streaming.element.querySelector('.wabbit-chat-message-content');
|
|
1354
|
+
if (contentDiv) {
|
|
1355
|
+
// Format the content and add cursor
|
|
1356
|
+
contentDiv.innerHTML = this.formatMessage(streaming.content) + '<span class="wabbit-chat-cursor">▋</span>';
|
|
1357
|
+
}
|
|
1358
|
+
this.scrollToBottom();
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Finish streaming message
|
|
1362
|
+
*/
|
|
1363
|
+
finishStreamingMessage(messageId, metadata) {
|
|
1364
|
+
const streaming = this.streamingMessages.get(messageId);
|
|
1365
|
+
if (!streaming) {
|
|
1366
|
+
// Don't warn - this is expected if cleanup already removed it or on error
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
// Remove streaming class and cursor
|
|
1370
|
+
streaming.element.classList.remove('wabbit-chat-message-streaming');
|
|
1371
|
+
const contentDiv = streaming.element.querySelector('.wabbit-chat-message-content');
|
|
1372
|
+
if (contentDiv) {
|
|
1373
|
+
// Remove cursor, keep formatted content
|
|
1374
|
+
contentDiv.innerHTML = this.formatMessage(streaming.content);
|
|
1375
|
+
}
|
|
1376
|
+
// Add to messages array
|
|
1377
|
+
const message = {
|
|
1378
|
+
id: messageId,
|
|
1379
|
+
role: 'assistant',
|
|
1380
|
+
content: streaming.content,
|
|
1381
|
+
timestamp: new Date(),
|
|
1382
|
+
metadata,
|
|
1383
|
+
};
|
|
1384
|
+
this.messages.push(message);
|
|
1385
|
+
// Clean up
|
|
1386
|
+
this.streamingMessages.delete(messageId);
|
|
1387
|
+
this.scrollToBottom();
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Cancel a streaming message (e.g., on error)
|
|
1391
|
+
* Cleans up the streaming state without adding to message history
|
|
1392
|
+
*/
|
|
1393
|
+
cancelStreamingMessage(messageId) {
|
|
1394
|
+
const streaming = this.streamingMessages.get(messageId);
|
|
1395
|
+
if (!streaming) {
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
// Remove the streaming message element from DOM
|
|
1399
|
+
streaming.element.remove();
|
|
1400
|
+
// Clean up from map
|
|
1401
|
+
this.streamingMessages.delete(messageId);
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Cancel all active streaming messages
|
|
1405
|
+
* Useful when connection drops or on error
|
|
1406
|
+
*/
|
|
1407
|
+
cancelAllStreamingMessages() {
|
|
1408
|
+
this.streamingMessages.forEach((streaming) => {
|
|
1409
|
+
streaming.element.remove();
|
|
1410
|
+
});
|
|
1411
|
+
this.streamingMessages.clear();
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Cleanup stale streaming messages that have exceeded timeout
|
|
1415
|
+
* Runs periodically to prevent memory leaks from abandoned streams
|
|
1416
|
+
*/
|
|
1417
|
+
cleanupStaleStreamingMessages() {
|
|
1418
|
+
const now = Date.now();
|
|
1419
|
+
const staleIds = [];
|
|
1420
|
+
this.streamingMessages.forEach((streaming, messageId) => {
|
|
1421
|
+
if (now - streaming.startTime > this.STREAMING_TIMEOUT_MS) {
|
|
1422
|
+
console.warn('[ChatPanel] Cleaning up stale streaming message:', messageId);
|
|
1423
|
+
streaming.element.remove();
|
|
1424
|
+
staleIds.push(messageId);
|
|
1425
|
+
}
|
|
1426
|
+
});
|
|
1427
|
+
// Remove stale entries from map
|
|
1428
|
+
staleIds.forEach(id => this.streamingMessages.delete(id));
|
|
1429
|
+
// Stop cleanup interval if no more streaming messages
|
|
1430
|
+
if (this.streamingMessages.size === 0 && this.streamingCleanupInterval) {
|
|
1431
|
+
clearInterval(this.streamingCleanupInterval);
|
|
1432
|
+
this.streamingCleanupInterval = null;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1171
1435
|
/**
|
|
1172
1436
|
* Handle send message
|
|
1173
1437
|
*/
|
|
@@ -1252,6 +1516,11 @@
|
|
|
1252
1516
|
if (this.element) {
|
|
1253
1517
|
this.element.style.display = 'none';
|
|
1254
1518
|
}
|
|
1519
|
+
// Stop streaming cleanup interval when hidden to prevent memory leaks
|
|
1520
|
+
if (this.streamingCleanupInterval) {
|
|
1521
|
+
clearInterval(this.streamingCleanupInterval);
|
|
1522
|
+
this.streamingCleanupInterval = null;
|
|
1523
|
+
}
|
|
1255
1524
|
}
|
|
1256
1525
|
/**
|
|
1257
1526
|
* Remove the panel from DOM and cleanup event listeners
|
|
@@ -1260,6 +1529,11 @@
|
|
|
1260
1529
|
// Run all event cleanup functions
|
|
1261
1530
|
this.eventCleanup.forEach((cleanup) => cleanup());
|
|
1262
1531
|
this.eventCleanup = [];
|
|
1532
|
+
// Clear streaming cleanup interval
|
|
1533
|
+
if (this.streamingCleanupInterval) {
|
|
1534
|
+
clearInterval(this.streamingCleanupInterval);
|
|
1535
|
+
this.streamingCleanupInterval = null;
|
|
1536
|
+
}
|
|
1263
1537
|
if (this.element) {
|
|
1264
1538
|
this.element.remove();
|
|
1265
1539
|
this.element = null;
|
|
@@ -1581,6 +1855,20 @@
|
|
|
1581
1855
|
line-height: 1.5;
|
|
1582
1856
|
}
|
|
1583
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
|
+
|
|
1584
1872
|
/* Typing Indicator */
|
|
1585
1873
|
.wabbit-chat-typing {
|
|
1586
1874
|
display: flex;
|
|
@@ -1723,6 +2011,24 @@
|
|
|
1723
2011
|
}
|
|
1724
2012
|
}
|
|
1725
2013
|
|
|
2014
|
+
/* Streaming Cursor */
|
|
2015
|
+
.wabbit-chat-cursor {
|
|
2016
|
+
display: inline-block;
|
|
2017
|
+
color: var(--wabbit-primary);
|
|
2018
|
+
font-weight: bold;
|
|
2019
|
+
animation: wabbit-cursor-blink 1s infinite;
|
|
2020
|
+
margin-left: 2px;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
@keyframes wabbit-cursor-blink {
|
|
2024
|
+
0%, 50% {
|
|
2025
|
+
opacity: 1;
|
|
2026
|
+
}
|
|
2027
|
+
51%, 100% {
|
|
2028
|
+
opacity: 0;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
|
|
1726
2032
|
.wabbit-chat-panel {
|
|
1727
2033
|
animation: wabbit-fade-in 0.3s ease-out;
|
|
1728
2034
|
}
|
|
@@ -1737,17 +2043,17 @@
|
|
|
1737
2043
|
|
|
1738
2044
|
/* Inline Chat Panel - renders inside container instead of fixed position */
|
|
1739
2045
|
.wabbit-chat-panel.wabbit-chat-panel-inline {
|
|
1740
|
-
position:
|
|
2046
|
+
position: absolute !important;
|
|
2047
|
+
top: 0;
|
|
2048
|
+
left: 0;
|
|
2049
|
+
right: 0;
|
|
2050
|
+
bottom: 0;
|
|
1741
2051
|
width: 100%;
|
|
1742
2052
|
height: 100%;
|
|
1743
|
-
min-height: 400px;
|
|
1744
2053
|
max-height: none;
|
|
1745
2054
|
max-width: none;
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
left: auto !important;
|
|
1749
|
-
border-radius: 12px;
|
|
1750
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
2055
|
+
border-radius: 0;
|
|
2056
|
+
box-shadow: none;
|
|
1751
2057
|
animation: none; /* Disable slide-in animation for inline */
|
|
1752
2058
|
}
|
|
1753
2059
|
|
|
@@ -1798,7 +2104,7 @@
|
|
|
1798
2104
|
* Chat Widget - Main class that integrates all chat components
|
|
1799
2105
|
*/
|
|
1800
2106
|
class ChatWidget {
|
|
1801
|
-
constructor(config) {
|
|
2107
|
+
constructor(config, storage) {
|
|
1802
2108
|
this.wsClient = null;
|
|
1803
2109
|
this.bubble = null;
|
|
1804
2110
|
this.panel = null;
|
|
@@ -1808,6 +2114,7 @@
|
|
|
1808
2114
|
this.onChatReadyCallback = null;
|
|
1809
2115
|
this.chatReadyFired = false;
|
|
1810
2116
|
this.config = config;
|
|
2117
|
+
this.storage = storage;
|
|
1811
2118
|
this.onChatReadyCallback = config.onChatReady || null;
|
|
1812
2119
|
}
|
|
1813
2120
|
/**
|
|
@@ -1836,8 +2143,8 @@
|
|
|
1836
2143
|
this.setupThemeWatcher();
|
|
1837
2144
|
// Create WebSocket client
|
|
1838
2145
|
const wsUrl = this.getWebSocketUrl();
|
|
1839
|
-
this.wsClient = new ChatWebSocketClient(this.config.apiKey || '', wsUrl, null // Will use stored session if available
|
|
1840
|
-
);
|
|
2146
|
+
this.wsClient = new ChatWebSocketClient(this.config.apiKey || '', wsUrl, null, // Will use stored session if available
|
|
2147
|
+
this.storage);
|
|
1841
2148
|
// Set up event handlers
|
|
1842
2149
|
this.setupWebSocketHandlers();
|
|
1843
2150
|
// Only create bubble for widget mode (not inline)
|
|
@@ -1930,6 +2237,26 @@
|
|
|
1930
2237
|
this.panel.setMessages(messages);
|
|
1931
2238
|
}
|
|
1932
2239
|
};
|
|
2240
|
+
// Streaming message handlers
|
|
2241
|
+
this.wsClient.onMessageStart = (messageId) => {
|
|
2242
|
+
console.log('[Wabbit] Streaming message started:', messageId);
|
|
2243
|
+
if (this.panel) {
|
|
2244
|
+
this.panel.startStreamingMessage(messageId);
|
|
2245
|
+
}
|
|
2246
|
+
};
|
|
2247
|
+
this.wsClient.onMessageChunk = (messageId, chunk) => {
|
|
2248
|
+
if (this.panel) {
|
|
2249
|
+
this.panel.appendToStreamingMessage(messageId, chunk);
|
|
2250
|
+
}
|
|
2251
|
+
};
|
|
2252
|
+
this.wsClient.onMessageEnd = (messageId, metadata) => {
|
|
2253
|
+
console.log('[Wabbit] Streaming message ended:', messageId);
|
|
2254
|
+
if (this.panel) {
|
|
2255
|
+
this.panel.finishStreamingMessage(messageId, metadata);
|
|
2256
|
+
this.panel.setWaitingForResponse(false);
|
|
2257
|
+
}
|
|
2258
|
+
};
|
|
2259
|
+
// Non-streaming message handler (backward compatibility)
|
|
1933
2260
|
this.wsClient.onMessage = (message) => {
|
|
1934
2261
|
if (this.panel) {
|
|
1935
2262
|
this.panel.addMessage(message);
|
|
@@ -1939,6 +2266,8 @@
|
|
|
1939
2266
|
this.wsClient.onError = (error) => {
|
|
1940
2267
|
console.error('[Wabbit] Chat error:', error);
|
|
1941
2268
|
if (this.panel) {
|
|
2269
|
+
// Clean up any active streaming messages on error
|
|
2270
|
+
this.panel.cancelAllStreamingMessages();
|
|
1942
2271
|
this.panel.addSystemMessage(`Error: ${error}`);
|
|
1943
2272
|
this.panel.setWaitingForResponse(false);
|
|
1944
2273
|
}
|
|
@@ -1950,10 +2279,24 @@
|
|
|
1950
2279
|
};
|
|
1951
2280
|
this.wsClient.onDisconnect = () => {
|
|
1952
2281
|
if (this.panel) {
|
|
1953
|
-
|
|
2282
|
+
// Clean up any active streaming messages on disconnect
|
|
2283
|
+
this.panel.cancelAllStreamingMessages();
|
|
2284
|
+
// Check if there are queued messages
|
|
2285
|
+
const queueSize = this.wsClient.getQueueSize();
|
|
2286
|
+
if (queueSize > 0) {
|
|
2287
|
+
this.panel.addSystemMessage(`Disconnected from chat service. ${queueSize} message(s) will be sent when reconnected.`);
|
|
2288
|
+
}
|
|
2289
|
+
else {
|
|
2290
|
+
this.panel.addSystemMessage('Disconnected from chat service');
|
|
2291
|
+
}
|
|
1954
2292
|
this.panel.setDisabled(true);
|
|
1955
2293
|
}
|
|
1956
2294
|
};
|
|
2295
|
+
this.wsClient.onQueueOverflow = (message) => {
|
|
2296
|
+
if (this.panel) {
|
|
2297
|
+
this.panel.addSystemMessage(message);
|
|
2298
|
+
}
|
|
2299
|
+
};
|
|
1957
2300
|
}
|
|
1958
2301
|
/**
|
|
1959
2302
|
* Handle trigger type configuration
|