@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.esm.js
CHANGED
|
@@ -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,110 +673,21 @@ 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;
|
|
639
686
|
this.maxReconnectAttempts = 3;
|
|
640
687
|
this.reconnectDelay = 1000;
|
|
688
|
+
// Message queue for handling messages during disconnection
|
|
689
|
+
this.messageQueue = [];
|
|
690
|
+
this.MAX_QUEUE_SIZE = 10;
|
|
641
691
|
// Event handlers
|
|
642
692
|
this.onStatusChange = null;
|
|
643
693
|
this.onWelcome = null;
|
|
@@ -645,8 +695,14 @@ class ChatWebSocketClient {
|
|
|
645
695
|
this.onMessageHistory = null;
|
|
646
696
|
this.onError = null;
|
|
647
697
|
this.onDisconnect = null;
|
|
698
|
+
this.onQueueOverflow = null;
|
|
699
|
+
// Streaming event handlers
|
|
700
|
+
this.onMessageStart = null;
|
|
701
|
+
this.onMessageChunk = null;
|
|
702
|
+
this.onMessageEnd = null;
|
|
648
703
|
this.apiKey = apiKey;
|
|
649
704
|
this.wsUrl = wsUrl;
|
|
705
|
+
this.storage = storage;
|
|
650
706
|
this.sessionId = sessionId || this.getStoredSessionId();
|
|
651
707
|
}
|
|
652
708
|
setStatus(status) {
|
|
@@ -676,6 +732,8 @@ class ChatWebSocketClient {
|
|
|
676
732
|
this.setStatus('connected');
|
|
677
733
|
this.reconnectAttempts = 0;
|
|
678
734
|
console.log('[Wabbit] WebSocket connected');
|
|
735
|
+
// Flush any queued messages
|
|
736
|
+
this.flushMessageQueue();
|
|
679
737
|
};
|
|
680
738
|
this.ws.onmessage = (event) => {
|
|
681
739
|
try {
|
|
@@ -713,9 +771,9 @@ class ChatWebSocketClient {
|
|
|
713
771
|
switch (data.type) {
|
|
714
772
|
case 'welcome':
|
|
715
773
|
this.sessionId = data.session_id;
|
|
716
|
-
// Store session ID in localStorage
|
|
774
|
+
// Store session ID in storage (localStorage or sessionStorage)
|
|
717
775
|
if (this.sessionId) {
|
|
718
|
-
storage.set('session_id', this.sessionId);
|
|
776
|
+
this.storage.set('session_id', this.sessionId);
|
|
719
777
|
}
|
|
720
778
|
if (this.onWelcome) {
|
|
721
779
|
this.onWelcome(data.session_id, data.collection_id, data.message || 'Connected');
|
|
@@ -734,7 +792,26 @@ class ChatWebSocketClient {
|
|
|
734
792
|
this.onMessageHistory(messages);
|
|
735
793
|
}
|
|
736
794
|
break;
|
|
795
|
+
case 'assistant_message_start':
|
|
796
|
+
// Streaming: Start of assistant message
|
|
797
|
+
if (this.onMessageStart) {
|
|
798
|
+
this.onMessageStart(data.message_id);
|
|
799
|
+
}
|
|
800
|
+
break;
|
|
801
|
+
case 'assistant_message_chunk':
|
|
802
|
+
// Streaming: Chunk of assistant message
|
|
803
|
+
if (this.onMessageChunk) {
|
|
804
|
+
this.onMessageChunk(data.message_id, data.content);
|
|
805
|
+
}
|
|
806
|
+
break;
|
|
807
|
+
case 'assistant_message_end':
|
|
808
|
+
// Streaming: End of assistant message
|
|
809
|
+
if (this.onMessageEnd) {
|
|
810
|
+
this.onMessageEnd(data.message_id, data.metadata);
|
|
811
|
+
}
|
|
812
|
+
break;
|
|
737
813
|
case 'assistant_message':
|
|
814
|
+
// Non-streaming: Complete assistant message (backward compatibility)
|
|
738
815
|
if (this.onMessage) {
|
|
739
816
|
const message = {
|
|
740
817
|
id: data.message_id || crypto.randomUUID(),
|
|
@@ -756,18 +833,27 @@ class ChatWebSocketClient {
|
|
|
756
833
|
}
|
|
757
834
|
}
|
|
758
835
|
sendMessage(content, metadata) {
|
|
759
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
760
|
-
if (this.onError) {
|
|
761
|
-
this.onError('Not connected to chat service');
|
|
762
|
-
}
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
765
836
|
const message = {
|
|
766
837
|
type: 'message',
|
|
767
838
|
content,
|
|
768
839
|
metadata: metadata || {},
|
|
769
840
|
};
|
|
770
|
-
|
|
841
|
+
// If connected, send immediately
|
|
842
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
843
|
+
this.ws.send(JSON.stringify(message));
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
// If disconnected or reconnecting, queue the message
|
|
847
|
+
if (this.messageQueue.length >= this.MAX_QUEUE_SIZE) {
|
|
848
|
+
console.warn('[Wabbit] Message queue full, cannot queue message');
|
|
849
|
+
if (this.onQueueOverflow) {
|
|
850
|
+
this.onQueueOverflow('Message queue full. Please wait for reconnection.');
|
|
851
|
+
}
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
// Add to queue
|
|
855
|
+
this.messageQueue.push({ content, metadata });
|
|
856
|
+
console.log(`[Wabbit] Message queued (${this.messageQueue.length}/${this.MAX_QUEUE_SIZE})`);
|
|
771
857
|
}
|
|
772
858
|
disconnect() {
|
|
773
859
|
if (this.ws) {
|
|
@@ -789,12 +875,50 @@ class ChatWebSocketClient {
|
|
|
789
875
|
this.connect();
|
|
790
876
|
}, delay);
|
|
791
877
|
}
|
|
878
|
+
/**
|
|
879
|
+
* Flush queued messages after connection is established
|
|
880
|
+
*/
|
|
881
|
+
flushMessageQueue() {
|
|
882
|
+
if (this.messageQueue.length === 0) {
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
console.log(`[Wabbit] Flushing ${this.messageQueue.length} queued message(s)`);
|
|
886
|
+
// Send all queued messages in order
|
|
887
|
+
const queuedMessages = [...this.messageQueue];
|
|
888
|
+
this.messageQueue = [];
|
|
889
|
+
queuedMessages.forEach(({ content, metadata }) => {
|
|
890
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
891
|
+
const message = {
|
|
892
|
+
type: 'message',
|
|
893
|
+
content,
|
|
894
|
+
metadata: metadata || {},
|
|
895
|
+
};
|
|
896
|
+
this.ws.send(JSON.stringify(message));
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
}
|
|
792
900
|
clearSession() {
|
|
793
901
|
this.sessionId = null;
|
|
794
|
-
storage.remove('session_id');
|
|
902
|
+
this.storage.remove('session_id');
|
|
903
|
+
// Clear message queue only when user explicitly starts new session
|
|
904
|
+
// Queue should persist across reconnection attempts
|
|
905
|
+
this.messageQueue = [];
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Clear the message queue
|
|
909
|
+
* Only call this when explicitly abandoning queued messages
|
|
910
|
+
*/
|
|
911
|
+
clearMessageQueue() {
|
|
912
|
+
this.messageQueue = [];
|
|
795
913
|
}
|
|
796
914
|
getStoredSessionId() {
|
|
797
|
-
return storage.get('session_id') || null;
|
|
915
|
+
return this.storage.get('session_id') || null;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Get current message queue size
|
|
919
|
+
*/
|
|
920
|
+
getQueueSize() {
|
|
921
|
+
return this.messageQueue.length;
|
|
798
922
|
}
|
|
799
923
|
}
|
|
800
924
|
|
|
@@ -924,6 +1048,9 @@ class ChatPanel {
|
|
|
924
1048
|
this.isWaitingForResponse = false;
|
|
925
1049
|
this.closeButton = null;
|
|
926
1050
|
this.eventCleanup = [];
|
|
1051
|
+
this.streamingMessages = new Map();
|
|
1052
|
+
this.streamingCleanupInterval = null;
|
|
1053
|
+
this.STREAMING_TIMEOUT_MS = 30000; // 30 seconds timeout for stale streams
|
|
927
1054
|
this.options = options;
|
|
928
1055
|
}
|
|
929
1056
|
/**
|
|
@@ -1155,13 +1282,150 @@ class ChatPanel {
|
|
|
1155
1282
|
formatMessage(content) {
|
|
1156
1283
|
// Escape HTML first
|
|
1157
1284
|
let formatted = escapeHtml(content);
|
|
1285
|
+
// Markdown links [text](url) - must come before URL auto-linking
|
|
1286
|
+
formatted = formatted.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
|
1158
1287
|
// Simple markdown-like formatting
|
|
1159
1288
|
formatted = formatted.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
1160
1289
|
formatted = formatted.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
1161
1290
|
formatted = formatted.replace(/`(.+?)`/g, '<code style="background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px;">$1</code>');
|
|
1291
|
+
// Auto-link URLs (not already inside an href attribute)
|
|
1292
|
+
formatted = formatted.replace(/(?<!href=")(https?:\/\/[^\s<]+)/g, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>');
|
|
1293
|
+
// Auto-link email addresses
|
|
1294
|
+
formatted = formatted.replace(/(?<!["\/])([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g, '<a href="mailto:$1">$1</a>');
|
|
1295
|
+
// Auto-link phone numbers (international format with +)
|
|
1296
|
+
formatted = formatted.replace(/(?<!["\/])(\+\d[\d\s-]{7,})/g, (_match, phone) => {
|
|
1297
|
+
const cleanPhone = phone.replace(/[\s-]/g, '');
|
|
1298
|
+
return `<a href="tel:${cleanPhone}">${phone}</a>`;
|
|
1299
|
+
});
|
|
1162
1300
|
formatted = formatted.replace(/\n/g, '<br>');
|
|
1163
1301
|
return formatted;
|
|
1164
1302
|
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Start a streaming assistant message
|
|
1305
|
+
*/
|
|
1306
|
+
startStreamingMessage(messageId) {
|
|
1307
|
+
if (!this.messagesContainer)
|
|
1308
|
+
return;
|
|
1309
|
+
// Create message container
|
|
1310
|
+
const messageDiv = createElement('div', {
|
|
1311
|
+
class: 'wabbit-chat-message wabbit-chat-message-assistant wabbit-chat-message-streaming',
|
|
1312
|
+
'data-message-id': messageId,
|
|
1313
|
+
});
|
|
1314
|
+
const bubble = createElement('div', { class: 'wabbit-chat-message-bubble' });
|
|
1315
|
+
const content = createElement('div', { class: 'wabbit-chat-message-content' });
|
|
1316
|
+
// Add streaming cursor
|
|
1317
|
+
content.innerHTML = '<span class="wabbit-chat-cursor">▋</span>';
|
|
1318
|
+
bubble.appendChild(content);
|
|
1319
|
+
messageDiv.appendChild(bubble);
|
|
1320
|
+
this.messagesContainer.appendChild(messageDiv);
|
|
1321
|
+
// Store reference with timestamp for timeout tracking
|
|
1322
|
+
this.streamingMessages.set(messageId, {
|
|
1323
|
+
element: messageDiv,
|
|
1324
|
+
content: '',
|
|
1325
|
+
startTime: Date.now()
|
|
1326
|
+
});
|
|
1327
|
+
// Start cleanup interval if not already running
|
|
1328
|
+
if (!this.streamingCleanupInterval) {
|
|
1329
|
+
this.streamingCleanupInterval = window.setInterval(() => {
|
|
1330
|
+
this.cleanupStaleStreamingMessages();
|
|
1331
|
+
}, 5000); // Check every 5 seconds
|
|
1332
|
+
}
|
|
1333
|
+
this.scrollToBottom();
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Append chunk to streaming message
|
|
1337
|
+
*/
|
|
1338
|
+
appendToStreamingMessage(messageId, chunk) {
|
|
1339
|
+
const streaming = this.streamingMessages.get(messageId);
|
|
1340
|
+
if (!streaming) {
|
|
1341
|
+
console.warn('[ChatPanel] No streaming message found for ID:', messageId);
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
// Append to content
|
|
1345
|
+
streaming.content += chunk;
|
|
1346
|
+
// Update DOM
|
|
1347
|
+
const contentDiv = streaming.element.querySelector('.wabbit-chat-message-content');
|
|
1348
|
+
if (contentDiv) {
|
|
1349
|
+
// Format the content and add cursor
|
|
1350
|
+
contentDiv.innerHTML = this.formatMessage(streaming.content) + '<span class="wabbit-chat-cursor">▋</span>';
|
|
1351
|
+
}
|
|
1352
|
+
this.scrollToBottom();
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Finish streaming message
|
|
1356
|
+
*/
|
|
1357
|
+
finishStreamingMessage(messageId, metadata) {
|
|
1358
|
+
const streaming = this.streamingMessages.get(messageId);
|
|
1359
|
+
if (!streaming) {
|
|
1360
|
+
// Don't warn - this is expected if cleanup already removed it or on error
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
// Remove streaming class and cursor
|
|
1364
|
+
streaming.element.classList.remove('wabbit-chat-message-streaming');
|
|
1365
|
+
const contentDiv = streaming.element.querySelector('.wabbit-chat-message-content');
|
|
1366
|
+
if (contentDiv) {
|
|
1367
|
+
// Remove cursor, keep formatted content
|
|
1368
|
+
contentDiv.innerHTML = this.formatMessage(streaming.content);
|
|
1369
|
+
}
|
|
1370
|
+
// Add to messages array
|
|
1371
|
+
const message = {
|
|
1372
|
+
id: messageId,
|
|
1373
|
+
role: 'assistant',
|
|
1374
|
+
content: streaming.content,
|
|
1375
|
+
timestamp: new Date(),
|
|
1376
|
+
metadata,
|
|
1377
|
+
};
|
|
1378
|
+
this.messages.push(message);
|
|
1379
|
+
// Clean up
|
|
1380
|
+
this.streamingMessages.delete(messageId);
|
|
1381
|
+
this.scrollToBottom();
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Cancel a streaming message (e.g., on error)
|
|
1385
|
+
* Cleans up the streaming state without adding to message history
|
|
1386
|
+
*/
|
|
1387
|
+
cancelStreamingMessage(messageId) {
|
|
1388
|
+
const streaming = this.streamingMessages.get(messageId);
|
|
1389
|
+
if (!streaming) {
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
// Remove the streaming message element from DOM
|
|
1393
|
+
streaming.element.remove();
|
|
1394
|
+
// Clean up from map
|
|
1395
|
+
this.streamingMessages.delete(messageId);
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Cancel all active streaming messages
|
|
1399
|
+
* Useful when connection drops or on error
|
|
1400
|
+
*/
|
|
1401
|
+
cancelAllStreamingMessages() {
|
|
1402
|
+
this.streamingMessages.forEach((streaming) => {
|
|
1403
|
+
streaming.element.remove();
|
|
1404
|
+
});
|
|
1405
|
+
this.streamingMessages.clear();
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Cleanup stale streaming messages that have exceeded timeout
|
|
1409
|
+
* Runs periodically to prevent memory leaks from abandoned streams
|
|
1410
|
+
*/
|
|
1411
|
+
cleanupStaleStreamingMessages() {
|
|
1412
|
+
const now = Date.now();
|
|
1413
|
+
const staleIds = [];
|
|
1414
|
+
this.streamingMessages.forEach((streaming, messageId) => {
|
|
1415
|
+
if (now - streaming.startTime > this.STREAMING_TIMEOUT_MS) {
|
|
1416
|
+
console.warn('[ChatPanel] Cleaning up stale streaming message:', messageId);
|
|
1417
|
+
streaming.element.remove();
|
|
1418
|
+
staleIds.push(messageId);
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
// Remove stale entries from map
|
|
1422
|
+
staleIds.forEach(id => this.streamingMessages.delete(id));
|
|
1423
|
+
// Stop cleanup interval if no more streaming messages
|
|
1424
|
+
if (this.streamingMessages.size === 0 && this.streamingCleanupInterval) {
|
|
1425
|
+
clearInterval(this.streamingCleanupInterval);
|
|
1426
|
+
this.streamingCleanupInterval = null;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1165
1429
|
/**
|
|
1166
1430
|
* Handle send message
|
|
1167
1431
|
*/
|
|
@@ -1246,6 +1510,11 @@ class ChatPanel {
|
|
|
1246
1510
|
if (this.element) {
|
|
1247
1511
|
this.element.style.display = 'none';
|
|
1248
1512
|
}
|
|
1513
|
+
// Stop streaming cleanup interval when hidden to prevent memory leaks
|
|
1514
|
+
if (this.streamingCleanupInterval) {
|
|
1515
|
+
clearInterval(this.streamingCleanupInterval);
|
|
1516
|
+
this.streamingCleanupInterval = null;
|
|
1517
|
+
}
|
|
1249
1518
|
}
|
|
1250
1519
|
/**
|
|
1251
1520
|
* Remove the panel from DOM and cleanup event listeners
|
|
@@ -1254,6 +1523,11 @@ class ChatPanel {
|
|
|
1254
1523
|
// Run all event cleanup functions
|
|
1255
1524
|
this.eventCleanup.forEach((cleanup) => cleanup());
|
|
1256
1525
|
this.eventCleanup = [];
|
|
1526
|
+
// Clear streaming cleanup interval
|
|
1527
|
+
if (this.streamingCleanupInterval) {
|
|
1528
|
+
clearInterval(this.streamingCleanupInterval);
|
|
1529
|
+
this.streamingCleanupInterval = null;
|
|
1530
|
+
}
|
|
1257
1531
|
if (this.element) {
|
|
1258
1532
|
this.element.remove();
|
|
1259
1533
|
this.element = null;
|
|
@@ -1575,6 +1849,20 @@ function injectChatStyles(primaryColor = '#6366f1', theme) {
|
|
|
1575
1849
|
line-height: 1.5;
|
|
1576
1850
|
}
|
|
1577
1851
|
|
|
1852
|
+
.wabbit-chat-message-content a {
|
|
1853
|
+
color: var(--wabbit-primary);
|
|
1854
|
+
text-decoration: underline;
|
|
1855
|
+
text-underline-offset: 2px;
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
.wabbit-chat-message-content a:hover {
|
|
1859
|
+
opacity: 0.8;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
.wabbit-chat-message-user .wabbit-chat-message-content a {
|
|
1863
|
+
color: inherit;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1578
1866
|
/* Typing Indicator */
|
|
1579
1867
|
.wabbit-chat-typing {
|
|
1580
1868
|
display: flex;
|
|
@@ -1717,6 +2005,24 @@ function injectChatStyles(primaryColor = '#6366f1', theme) {
|
|
|
1717
2005
|
}
|
|
1718
2006
|
}
|
|
1719
2007
|
|
|
2008
|
+
/* Streaming Cursor */
|
|
2009
|
+
.wabbit-chat-cursor {
|
|
2010
|
+
display: inline-block;
|
|
2011
|
+
color: var(--wabbit-primary);
|
|
2012
|
+
font-weight: bold;
|
|
2013
|
+
animation: wabbit-cursor-blink 1s infinite;
|
|
2014
|
+
margin-left: 2px;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
@keyframes wabbit-cursor-blink {
|
|
2018
|
+
0%, 50% {
|
|
2019
|
+
opacity: 1;
|
|
2020
|
+
}
|
|
2021
|
+
51%, 100% {
|
|
2022
|
+
opacity: 0;
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1720
2026
|
.wabbit-chat-panel {
|
|
1721
2027
|
animation: wabbit-fade-in 0.3s ease-out;
|
|
1722
2028
|
}
|
|
@@ -1731,17 +2037,17 @@ function injectChatStyles(primaryColor = '#6366f1', theme) {
|
|
|
1731
2037
|
|
|
1732
2038
|
/* Inline Chat Panel - renders inside container instead of fixed position */
|
|
1733
2039
|
.wabbit-chat-panel.wabbit-chat-panel-inline {
|
|
1734
|
-
position:
|
|
2040
|
+
position: absolute !important;
|
|
2041
|
+
top: 0;
|
|
2042
|
+
left: 0;
|
|
2043
|
+
right: 0;
|
|
2044
|
+
bottom: 0;
|
|
1735
2045
|
width: 100%;
|
|
1736
2046
|
height: 100%;
|
|
1737
|
-
min-height: 400px;
|
|
1738
2047
|
max-height: none;
|
|
1739
2048
|
max-width: none;
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
left: auto !important;
|
|
1743
|
-
border-radius: 12px;
|
|
1744
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
2049
|
+
border-radius: 0;
|
|
2050
|
+
box-shadow: none;
|
|
1745
2051
|
animation: none; /* Disable slide-in animation for inline */
|
|
1746
2052
|
}
|
|
1747
2053
|
|
|
@@ -1792,7 +2098,7 @@ function adjustColor(color, amount) {
|
|
|
1792
2098
|
* Chat Widget - Main class that integrates all chat components
|
|
1793
2099
|
*/
|
|
1794
2100
|
class ChatWidget {
|
|
1795
|
-
constructor(config) {
|
|
2101
|
+
constructor(config, storage) {
|
|
1796
2102
|
this.wsClient = null;
|
|
1797
2103
|
this.bubble = null;
|
|
1798
2104
|
this.panel = null;
|
|
@@ -1802,6 +2108,7 @@ class ChatWidget {
|
|
|
1802
2108
|
this.onChatReadyCallback = null;
|
|
1803
2109
|
this.chatReadyFired = false;
|
|
1804
2110
|
this.config = config;
|
|
2111
|
+
this.storage = storage;
|
|
1805
2112
|
this.onChatReadyCallback = config.onChatReady || null;
|
|
1806
2113
|
}
|
|
1807
2114
|
/**
|
|
@@ -1830,8 +2137,8 @@ class ChatWidget {
|
|
|
1830
2137
|
this.setupThemeWatcher();
|
|
1831
2138
|
// Create WebSocket client
|
|
1832
2139
|
const wsUrl = this.getWebSocketUrl();
|
|
1833
|
-
this.wsClient = new ChatWebSocketClient(this.config.apiKey || '', wsUrl, null // Will use stored session if available
|
|
1834
|
-
);
|
|
2140
|
+
this.wsClient = new ChatWebSocketClient(this.config.apiKey || '', wsUrl, null, // Will use stored session if available
|
|
2141
|
+
this.storage);
|
|
1835
2142
|
// Set up event handlers
|
|
1836
2143
|
this.setupWebSocketHandlers();
|
|
1837
2144
|
// Only create bubble for widget mode (not inline)
|
|
@@ -1924,6 +2231,26 @@ class ChatWidget {
|
|
|
1924
2231
|
this.panel.setMessages(messages);
|
|
1925
2232
|
}
|
|
1926
2233
|
};
|
|
2234
|
+
// Streaming message handlers
|
|
2235
|
+
this.wsClient.onMessageStart = (messageId) => {
|
|
2236
|
+
console.log('[Wabbit] Streaming message started:', messageId);
|
|
2237
|
+
if (this.panel) {
|
|
2238
|
+
this.panel.startStreamingMessage(messageId);
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
this.wsClient.onMessageChunk = (messageId, chunk) => {
|
|
2242
|
+
if (this.panel) {
|
|
2243
|
+
this.panel.appendToStreamingMessage(messageId, chunk);
|
|
2244
|
+
}
|
|
2245
|
+
};
|
|
2246
|
+
this.wsClient.onMessageEnd = (messageId, metadata) => {
|
|
2247
|
+
console.log('[Wabbit] Streaming message ended:', messageId);
|
|
2248
|
+
if (this.panel) {
|
|
2249
|
+
this.panel.finishStreamingMessage(messageId, metadata);
|
|
2250
|
+
this.panel.setWaitingForResponse(false);
|
|
2251
|
+
}
|
|
2252
|
+
};
|
|
2253
|
+
// Non-streaming message handler (backward compatibility)
|
|
1927
2254
|
this.wsClient.onMessage = (message) => {
|
|
1928
2255
|
if (this.panel) {
|
|
1929
2256
|
this.panel.addMessage(message);
|
|
@@ -1933,6 +2260,8 @@ class ChatWidget {
|
|
|
1933
2260
|
this.wsClient.onError = (error) => {
|
|
1934
2261
|
console.error('[Wabbit] Chat error:', error);
|
|
1935
2262
|
if (this.panel) {
|
|
2263
|
+
// Clean up any active streaming messages on error
|
|
2264
|
+
this.panel.cancelAllStreamingMessages();
|
|
1936
2265
|
this.panel.addSystemMessage(`Error: ${error}`);
|
|
1937
2266
|
this.panel.setWaitingForResponse(false);
|
|
1938
2267
|
}
|
|
@@ -1944,10 +2273,24 @@ class ChatWidget {
|
|
|
1944
2273
|
};
|
|
1945
2274
|
this.wsClient.onDisconnect = () => {
|
|
1946
2275
|
if (this.panel) {
|
|
1947
|
-
|
|
2276
|
+
// Clean up any active streaming messages on disconnect
|
|
2277
|
+
this.panel.cancelAllStreamingMessages();
|
|
2278
|
+
// Check if there are queued messages
|
|
2279
|
+
const queueSize = this.wsClient.getQueueSize();
|
|
2280
|
+
if (queueSize > 0) {
|
|
2281
|
+
this.panel.addSystemMessage(`Disconnected from chat service. ${queueSize} message(s) will be sent when reconnected.`);
|
|
2282
|
+
}
|
|
2283
|
+
else {
|
|
2284
|
+
this.panel.addSystemMessage('Disconnected from chat service');
|
|
2285
|
+
}
|
|
1948
2286
|
this.panel.setDisabled(true);
|
|
1949
2287
|
}
|
|
1950
2288
|
};
|
|
2289
|
+
this.wsClient.onQueueOverflow = (message) => {
|
|
2290
|
+
if (this.panel) {
|
|
2291
|
+
this.panel.addSystemMessage(message);
|
|
2292
|
+
}
|
|
2293
|
+
};
|
|
1951
2294
|
}
|
|
1952
2295
|
/**
|
|
1953
2296
|
* Handle trigger type configuration
|