@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.cjs.js
CHANGED
|
@@ -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,110 +677,21 @@ 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;
|
|
643
690
|
this.maxReconnectAttempts = 3;
|
|
644
691
|
this.reconnectDelay = 1000;
|
|
692
|
+
// Message queue for handling messages during disconnection
|
|
693
|
+
this.messageQueue = [];
|
|
694
|
+
this.MAX_QUEUE_SIZE = 10;
|
|
645
695
|
// Event handlers
|
|
646
696
|
this.onStatusChange = null;
|
|
647
697
|
this.onWelcome = null;
|
|
@@ -649,8 +699,14 @@ class ChatWebSocketClient {
|
|
|
649
699
|
this.onMessageHistory = null;
|
|
650
700
|
this.onError = null;
|
|
651
701
|
this.onDisconnect = null;
|
|
702
|
+
this.onQueueOverflow = null;
|
|
703
|
+
// Streaming event handlers
|
|
704
|
+
this.onMessageStart = null;
|
|
705
|
+
this.onMessageChunk = null;
|
|
706
|
+
this.onMessageEnd = null;
|
|
652
707
|
this.apiKey = apiKey;
|
|
653
708
|
this.wsUrl = wsUrl;
|
|
709
|
+
this.storage = storage;
|
|
654
710
|
this.sessionId = sessionId || this.getStoredSessionId();
|
|
655
711
|
}
|
|
656
712
|
setStatus(status) {
|
|
@@ -680,6 +736,8 @@ class ChatWebSocketClient {
|
|
|
680
736
|
this.setStatus('connected');
|
|
681
737
|
this.reconnectAttempts = 0;
|
|
682
738
|
console.log('[Wabbit] WebSocket connected');
|
|
739
|
+
// Flush any queued messages
|
|
740
|
+
this.flushMessageQueue();
|
|
683
741
|
};
|
|
684
742
|
this.ws.onmessage = (event) => {
|
|
685
743
|
try {
|
|
@@ -717,9 +775,9 @@ class ChatWebSocketClient {
|
|
|
717
775
|
switch (data.type) {
|
|
718
776
|
case 'welcome':
|
|
719
777
|
this.sessionId = data.session_id;
|
|
720
|
-
// Store session ID in localStorage
|
|
778
|
+
// Store session ID in storage (localStorage or sessionStorage)
|
|
721
779
|
if (this.sessionId) {
|
|
722
|
-
storage.set('session_id', this.sessionId);
|
|
780
|
+
this.storage.set('session_id', this.sessionId);
|
|
723
781
|
}
|
|
724
782
|
if (this.onWelcome) {
|
|
725
783
|
this.onWelcome(data.session_id, data.collection_id, data.message || 'Connected');
|
|
@@ -738,7 +796,26 @@ class ChatWebSocketClient {
|
|
|
738
796
|
this.onMessageHistory(messages);
|
|
739
797
|
}
|
|
740
798
|
break;
|
|
799
|
+
case 'assistant_message_start':
|
|
800
|
+
// Streaming: Start of assistant message
|
|
801
|
+
if (this.onMessageStart) {
|
|
802
|
+
this.onMessageStart(data.message_id);
|
|
803
|
+
}
|
|
804
|
+
break;
|
|
805
|
+
case 'assistant_message_chunk':
|
|
806
|
+
// Streaming: Chunk of assistant message
|
|
807
|
+
if (this.onMessageChunk) {
|
|
808
|
+
this.onMessageChunk(data.message_id, data.content);
|
|
809
|
+
}
|
|
810
|
+
break;
|
|
811
|
+
case 'assistant_message_end':
|
|
812
|
+
// Streaming: End of assistant message
|
|
813
|
+
if (this.onMessageEnd) {
|
|
814
|
+
this.onMessageEnd(data.message_id, data.metadata);
|
|
815
|
+
}
|
|
816
|
+
break;
|
|
741
817
|
case 'assistant_message':
|
|
818
|
+
// Non-streaming: Complete assistant message (backward compatibility)
|
|
742
819
|
if (this.onMessage) {
|
|
743
820
|
const message = {
|
|
744
821
|
id: data.message_id || crypto.randomUUID(),
|
|
@@ -760,18 +837,27 @@ class ChatWebSocketClient {
|
|
|
760
837
|
}
|
|
761
838
|
}
|
|
762
839
|
sendMessage(content, metadata) {
|
|
763
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
764
|
-
if (this.onError) {
|
|
765
|
-
this.onError('Not connected to chat service');
|
|
766
|
-
}
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
840
|
const message = {
|
|
770
841
|
type: 'message',
|
|
771
842
|
content,
|
|
772
843
|
metadata: metadata || {},
|
|
773
844
|
};
|
|
774
|
-
|
|
845
|
+
// If connected, send immediately
|
|
846
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
847
|
+
this.ws.send(JSON.stringify(message));
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
// If disconnected or reconnecting, queue the message
|
|
851
|
+
if (this.messageQueue.length >= this.MAX_QUEUE_SIZE) {
|
|
852
|
+
console.warn('[Wabbit] Message queue full, cannot queue message');
|
|
853
|
+
if (this.onQueueOverflow) {
|
|
854
|
+
this.onQueueOverflow('Message queue full. Please wait for reconnection.');
|
|
855
|
+
}
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
// Add to queue
|
|
859
|
+
this.messageQueue.push({ content, metadata });
|
|
860
|
+
console.log(`[Wabbit] Message queued (${this.messageQueue.length}/${this.MAX_QUEUE_SIZE})`);
|
|
775
861
|
}
|
|
776
862
|
disconnect() {
|
|
777
863
|
if (this.ws) {
|
|
@@ -793,12 +879,50 @@ class ChatWebSocketClient {
|
|
|
793
879
|
this.connect();
|
|
794
880
|
}, delay);
|
|
795
881
|
}
|
|
882
|
+
/**
|
|
883
|
+
* Flush queued messages after connection is established
|
|
884
|
+
*/
|
|
885
|
+
flushMessageQueue() {
|
|
886
|
+
if (this.messageQueue.length === 0) {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
console.log(`[Wabbit] Flushing ${this.messageQueue.length} queued message(s)`);
|
|
890
|
+
// Send all queued messages in order
|
|
891
|
+
const queuedMessages = [...this.messageQueue];
|
|
892
|
+
this.messageQueue = [];
|
|
893
|
+
queuedMessages.forEach(({ content, metadata }) => {
|
|
894
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
895
|
+
const message = {
|
|
896
|
+
type: 'message',
|
|
897
|
+
content,
|
|
898
|
+
metadata: metadata || {},
|
|
899
|
+
};
|
|
900
|
+
this.ws.send(JSON.stringify(message));
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
}
|
|
796
904
|
clearSession() {
|
|
797
905
|
this.sessionId = null;
|
|
798
|
-
storage.remove('session_id');
|
|
906
|
+
this.storage.remove('session_id');
|
|
907
|
+
// Clear message queue only when user explicitly starts new session
|
|
908
|
+
// Queue should persist across reconnection attempts
|
|
909
|
+
this.messageQueue = [];
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Clear the message queue
|
|
913
|
+
* Only call this when explicitly abandoning queued messages
|
|
914
|
+
*/
|
|
915
|
+
clearMessageQueue() {
|
|
916
|
+
this.messageQueue = [];
|
|
799
917
|
}
|
|
800
918
|
getStoredSessionId() {
|
|
801
|
-
return storage.get('session_id') || null;
|
|
919
|
+
return this.storage.get('session_id') || null;
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Get current message queue size
|
|
923
|
+
*/
|
|
924
|
+
getQueueSize() {
|
|
925
|
+
return this.messageQueue.length;
|
|
802
926
|
}
|
|
803
927
|
}
|
|
804
928
|
|
|
@@ -928,6 +1052,9 @@ class ChatPanel {
|
|
|
928
1052
|
this.isWaitingForResponse = false;
|
|
929
1053
|
this.closeButton = null;
|
|
930
1054
|
this.eventCleanup = [];
|
|
1055
|
+
this.streamingMessages = new Map();
|
|
1056
|
+
this.streamingCleanupInterval = null;
|
|
1057
|
+
this.STREAMING_TIMEOUT_MS = 30000; // 30 seconds timeout for stale streams
|
|
931
1058
|
this.options = options;
|
|
932
1059
|
}
|
|
933
1060
|
/**
|
|
@@ -1159,13 +1286,150 @@ class ChatPanel {
|
|
|
1159
1286
|
formatMessage(content) {
|
|
1160
1287
|
// Escape HTML first
|
|
1161
1288
|
let formatted = escapeHtml(content);
|
|
1289
|
+
// Markdown links [text](url) - must come before URL auto-linking
|
|
1290
|
+
formatted = formatted.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
|
1162
1291
|
// Simple markdown-like formatting
|
|
1163
1292
|
formatted = formatted.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
1164
1293
|
formatted = formatted.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
1165
1294
|
formatted = formatted.replace(/`(.+?)`/g, '<code style="background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px;">$1</code>');
|
|
1295
|
+
// Auto-link URLs (not already inside an href attribute)
|
|
1296
|
+
formatted = formatted.replace(/(?<!href=")(https?:\/\/[^\s<]+)/g, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>');
|
|
1297
|
+
// Auto-link email addresses
|
|
1298
|
+
formatted = formatted.replace(/(?<!["\/])([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g, '<a href="mailto:$1">$1</a>');
|
|
1299
|
+
// Auto-link phone numbers (international format with +)
|
|
1300
|
+
formatted = formatted.replace(/(?<!["\/])(\+\d[\d\s-]{7,})/g, (_match, phone) => {
|
|
1301
|
+
const cleanPhone = phone.replace(/[\s-]/g, '');
|
|
1302
|
+
return `<a href="tel:${cleanPhone}">${phone}</a>`;
|
|
1303
|
+
});
|
|
1166
1304
|
formatted = formatted.replace(/\n/g, '<br>');
|
|
1167
1305
|
return formatted;
|
|
1168
1306
|
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Start a streaming assistant message
|
|
1309
|
+
*/
|
|
1310
|
+
startStreamingMessage(messageId) {
|
|
1311
|
+
if (!this.messagesContainer)
|
|
1312
|
+
return;
|
|
1313
|
+
// Create message container
|
|
1314
|
+
const messageDiv = createElement('div', {
|
|
1315
|
+
class: 'wabbit-chat-message wabbit-chat-message-assistant wabbit-chat-message-streaming',
|
|
1316
|
+
'data-message-id': messageId,
|
|
1317
|
+
});
|
|
1318
|
+
const bubble = createElement('div', { class: 'wabbit-chat-message-bubble' });
|
|
1319
|
+
const content = createElement('div', { class: 'wabbit-chat-message-content' });
|
|
1320
|
+
// Add streaming cursor
|
|
1321
|
+
content.innerHTML = '<span class="wabbit-chat-cursor">▋</span>';
|
|
1322
|
+
bubble.appendChild(content);
|
|
1323
|
+
messageDiv.appendChild(bubble);
|
|
1324
|
+
this.messagesContainer.appendChild(messageDiv);
|
|
1325
|
+
// Store reference with timestamp for timeout tracking
|
|
1326
|
+
this.streamingMessages.set(messageId, {
|
|
1327
|
+
element: messageDiv,
|
|
1328
|
+
content: '',
|
|
1329
|
+
startTime: Date.now()
|
|
1330
|
+
});
|
|
1331
|
+
// Start cleanup interval if not already running
|
|
1332
|
+
if (!this.streamingCleanupInterval) {
|
|
1333
|
+
this.streamingCleanupInterval = window.setInterval(() => {
|
|
1334
|
+
this.cleanupStaleStreamingMessages();
|
|
1335
|
+
}, 5000); // Check every 5 seconds
|
|
1336
|
+
}
|
|
1337
|
+
this.scrollToBottom();
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Append chunk to streaming message
|
|
1341
|
+
*/
|
|
1342
|
+
appendToStreamingMessage(messageId, chunk) {
|
|
1343
|
+
const streaming = this.streamingMessages.get(messageId);
|
|
1344
|
+
if (!streaming) {
|
|
1345
|
+
console.warn('[ChatPanel] No streaming message found for ID:', messageId);
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
// Append to content
|
|
1349
|
+
streaming.content += chunk;
|
|
1350
|
+
// Update DOM
|
|
1351
|
+
const contentDiv = streaming.element.querySelector('.wabbit-chat-message-content');
|
|
1352
|
+
if (contentDiv) {
|
|
1353
|
+
// Format the content and add cursor
|
|
1354
|
+
contentDiv.innerHTML = this.formatMessage(streaming.content) + '<span class="wabbit-chat-cursor">▋</span>';
|
|
1355
|
+
}
|
|
1356
|
+
this.scrollToBottom();
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Finish streaming message
|
|
1360
|
+
*/
|
|
1361
|
+
finishStreamingMessage(messageId, metadata) {
|
|
1362
|
+
const streaming = this.streamingMessages.get(messageId);
|
|
1363
|
+
if (!streaming) {
|
|
1364
|
+
// Don't warn - this is expected if cleanup already removed it or on error
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
// Remove streaming class and cursor
|
|
1368
|
+
streaming.element.classList.remove('wabbit-chat-message-streaming');
|
|
1369
|
+
const contentDiv = streaming.element.querySelector('.wabbit-chat-message-content');
|
|
1370
|
+
if (contentDiv) {
|
|
1371
|
+
// Remove cursor, keep formatted content
|
|
1372
|
+
contentDiv.innerHTML = this.formatMessage(streaming.content);
|
|
1373
|
+
}
|
|
1374
|
+
// Add to messages array
|
|
1375
|
+
const message = {
|
|
1376
|
+
id: messageId,
|
|
1377
|
+
role: 'assistant',
|
|
1378
|
+
content: streaming.content,
|
|
1379
|
+
timestamp: new Date(),
|
|
1380
|
+
metadata,
|
|
1381
|
+
};
|
|
1382
|
+
this.messages.push(message);
|
|
1383
|
+
// Clean up
|
|
1384
|
+
this.streamingMessages.delete(messageId);
|
|
1385
|
+
this.scrollToBottom();
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Cancel a streaming message (e.g., on error)
|
|
1389
|
+
* Cleans up the streaming state without adding to message history
|
|
1390
|
+
*/
|
|
1391
|
+
cancelStreamingMessage(messageId) {
|
|
1392
|
+
const streaming = this.streamingMessages.get(messageId);
|
|
1393
|
+
if (!streaming) {
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
// Remove the streaming message element from DOM
|
|
1397
|
+
streaming.element.remove();
|
|
1398
|
+
// Clean up from map
|
|
1399
|
+
this.streamingMessages.delete(messageId);
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Cancel all active streaming messages
|
|
1403
|
+
* Useful when connection drops or on error
|
|
1404
|
+
*/
|
|
1405
|
+
cancelAllStreamingMessages() {
|
|
1406
|
+
this.streamingMessages.forEach((streaming) => {
|
|
1407
|
+
streaming.element.remove();
|
|
1408
|
+
});
|
|
1409
|
+
this.streamingMessages.clear();
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Cleanup stale streaming messages that have exceeded timeout
|
|
1413
|
+
* Runs periodically to prevent memory leaks from abandoned streams
|
|
1414
|
+
*/
|
|
1415
|
+
cleanupStaleStreamingMessages() {
|
|
1416
|
+
const now = Date.now();
|
|
1417
|
+
const staleIds = [];
|
|
1418
|
+
this.streamingMessages.forEach((streaming, messageId) => {
|
|
1419
|
+
if (now - streaming.startTime > this.STREAMING_TIMEOUT_MS) {
|
|
1420
|
+
console.warn('[ChatPanel] Cleaning up stale streaming message:', messageId);
|
|
1421
|
+
streaming.element.remove();
|
|
1422
|
+
staleIds.push(messageId);
|
|
1423
|
+
}
|
|
1424
|
+
});
|
|
1425
|
+
// Remove stale entries from map
|
|
1426
|
+
staleIds.forEach(id => this.streamingMessages.delete(id));
|
|
1427
|
+
// Stop cleanup interval if no more streaming messages
|
|
1428
|
+
if (this.streamingMessages.size === 0 && this.streamingCleanupInterval) {
|
|
1429
|
+
clearInterval(this.streamingCleanupInterval);
|
|
1430
|
+
this.streamingCleanupInterval = null;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1169
1433
|
/**
|
|
1170
1434
|
* Handle send message
|
|
1171
1435
|
*/
|
|
@@ -1250,6 +1514,11 @@ class ChatPanel {
|
|
|
1250
1514
|
if (this.element) {
|
|
1251
1515
|
this.element.style.display = 'none';
|
|
1252
1516
|
}
|
|
1517
|
+
// Stop streaming cleanup interval when hidden to prevent memory leaks
|
|
1518
|
+
if (this.streamingCleanupInterval) {
|
|
1519
|
+
clearInterval(this.streamingCleanupInterval);
|
|
1520
|
+
this.streamingCleanupInterval = null;
|
|
1521
|
+
}
|
|
1253
1522
|
}
|
|
1254
1523
|
/**
|
|
1255
1524
|
* Remove the panel from DOM and cleanup event listeners
|
|
@@ -1258,6 +1527,11 @@ class ChatPanel {
|
|
|
1258
1527
|
// Run all event cleanup functions
|
|
1259
1528
|
this.eventCleanup.forEach((cleanup) => cleanup());
|
|
1260
1529
|
this.eventCleanup = [];
|
|
1530
|
+
// Clear streaming cleanup interval
|
|
1531
|
+
if (this.streamingCleanupInterval) {
|
|
1532
|
+
clearInterval(this.streamingCleanupInterval);
|
|
1533
|
+
this.streamingCleanupInterval = null;
|
|
1534
|
+
}
|
|
1261
1535
|
if (this.element) {
|
|
1262
1536
|
this.element.remove();
|
|
1263
1537
|
this.element = null;
|
|
@@ -1579,6 +1853,20 @@ function injectChatStyles(primaryColor = '#6366f1', theme) {
|
|
|
1579
1853
|
line-height: 1.5;
|
|
1580
1854
|
}
|
|
1581
1855
|
|
|
1856
|
+
.wabbit-chat-message-content a {
|
|
1857
|
+
color: var(--wabbit-primary);
|
|
1858
|
+
text-decoration: underline;
|
|
1859
|
+
text-underline-offset: 2px;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
.wabbit-chat-message-content a:hover {
|
|
1863
|
+
opacity: 0.8;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
.wabbit-chat-message-user .wabbit-chat-message-content a {
|
|
1867
|
+
color: inherit;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1582
1870
|
/* Typing Indicator */
|
|
1583
1871
|
.wabbit-chat-typing {
|
|
1584
1872
|
display: flex;
|
|
@@ -1721,6 +2009,24 @@ function injectChatStyles(primaryColor = '#6366f1', theme) {
|
|
|
1721
2009
|
}
|
|
1722
2010
|
}
|
|
1723
2011
|
|
|
2012
|
+
/* Streaming Cursor */
|
|
2013
|
+
.wabbit-chat-cursor {
|
|
2014
|
+
display: inline-block;
|
|
2015
|
+
color: var(--wabbit-primary);
|
|
2016
|
+
font-weight: bold;
|
|
2017
|
+
animation: wabbit-cursor-blink 1s infinite;
|
|
2018
|
+
margin-left: 2px;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
@keyframes wabbit-cursor-blink {
|
|
2022
|
+
0%, 50% {
|
|
2023
|
+
opacity: 1;
|
|
2024
|
+
}
|
|
2025
|
+
51%, 100% {
|
|
2026
|
+
opacity: 0;
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
|
|
1724
2030
|
.wabbit-chat-panel {
|
|
1725
2031
|
animation: wabbit-fade-in 0.3s ease-out;
|
|
1726
2032
|
}
|
|
@@ -1735,17 +2041,17 @@ function injectChatStyles(primaryColor = '#6366f1', theme) {
|
|
|
1735
2041
|
|
|
1736
2042
|
/* Inline Chat Panel - renders inside container instead of fixed position */
|
|
1737
2043
|
.wabbit-chat-panel.wabbit-chat-panel-inline {
|
|
1738
|
-
position:
|
|
2044
|
+
position: absolute !important;
|
|
2045
|
+
top: 0;
|
|
2046
|
+
left: 0;
|
|
2047
|
+
right: 0;
|
|
2048
|
+
bottom: 0;
|
|
1739
2049
|
width: 100%;
|
|
1740
2050
|
height: 100%;
|
|
1741
|
-
min-height: 400px;
|
|
1742
2051
|
max-height: none;
|
|
1743
2052
|
max-width: none;
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
left: auto !important;
|
|
1747
|
-
border-radius: 12px;
|
|
1748
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
2053
|
+
border-radius: 0;
|
|
2054
|
+
box-shadow: none;
|
|
1749
2055
|
animation: none; /* Disable slide-in animation for inline */
|
|
1750
2056
|
}
|
|
1751
2057
|
|
|
@@ -1796,7 +2102,7 @@ function adjustColor(color, amount) {
|
|
|
1796
2102
|
* Chat Widget - Main class that integrates all chat components
|
|
1797
2103
|
*/
|
|
1798
2104
|
class ChatWidget {
|
|
1799
|
-
constructor(config) {
|
|
2105
|
+
constructor(config, storage) {
|
|
1800
2106
|
this.wsClient = null;
|
|
1801
2107
|
this.bubble = null;
|
|
1802
2108
|
this.panel = null;
|
|
@@ -1806,6 +2112,7 @@ class ChatWidget {
|
|
|
1806
2112
|
this.onChatReadyCallback = null;
|
|
1807
2113
|
this.chatReadyFired = false;
|
|
1808
2114
|
this.config = config;
|
|
2115
|
+
this.storage = storage;
|
|
1809
2116
|
this.onChatReadyCallback = config.onChatReady || null;
|
|
1810
2117
|
}
|
|
1811
2118
|
/**
|
|
@@ -1834,8 +2141,8 @@ class ChatWidget {
|
|
|
1834
2141
|
this.setupThemeWatcher();
|
|
1835
2142
|
// Create WebSocket client
|
|
1836
2143
|
const wsUrl = this.getWebSocketUrl();
|
|
1837
|
-
this.wsClient = new ChatWebSocketClient(this.config.apiKey || '', wsUrl, null // Will use stored session if available
|
|
1838
|
-
);
|
|
2144
|
+
this.wsClient = new ChatWebSocketClient(this.config.apiKey || '', wsUrl, null, // Will use stored session if available
|
|
2145
|
+
this.storage);
|
|
1839
2146
|
// Set up event handlers
|
|
1840
2147
|
this.setupWebSocketHandlers();
|
|
1841
2148
|
// Only create bubble for widget mode (not inline)
|
|
@@ -1928,6 +2235,26 @@ class ChatWidget {
|
|
|
1928
2235
|
this.panel.setMessages(messages);
|
|
1929
2236
|
}
|
|
1930
2237
|
};
|
|
2238
|
+
// Streaming message handlers
|
|
2239
|
+
this.wsClient.onMessageStart = (messageId) => {
|
|
2240
|
+
console.log('[Wabbit] Streaming message started:', messageId);
|
|
2241
|
+
if (this.panel) {
|
|
2242
|
+
this.panel.startStreamingMessage(messageId);
|
|
2243
|
+
}
|
|
2244
|
+
};
|
|
2245
|
+
this.wsClient.onMessageChunk = (messageId, chunk) => {
|
|
2246
|
+
if (this.panel) {
|
|
2247
|
+
this.panel.appendToStreamingMessage(messageId, chunk);
|
|
2248
|
+
}
|
|
2249
|
+
};
|
|
2250
|
+
this.wsClient.onMessageEnd = (messageId, metadata) => {
|
|
2251
|
+
console.log('[Wabbit] Streaming message ended:', messageId);
|
|
2252
|
+
if (this.panel) {
|
|
2253
|
+
this.panel.finishStreamingMessage(messageId, metadata);
|
|
2254
|
+
this.panel.setWaitingForResponse(false);
|
|
2255
|
+
}
|
|
2256
|
+
};
|
|
2257
|
+
// Non-streaming message handler (backward compatibility)
|
|
1931
2258
|
this.wsClient.onMessage = (message) => {
|
|
1932
2259
|
if (this.panel) {
|
|
1933
2260
|
this.panel.addMessage(message);
|
|
@@ -1937,6 +2264,8 @@ class ChatWidget {
|
|
|
1937
2264
|
this.wsClient.onError = (error) => {
|
|
1938
2265
|
console.error('[Wabbit] Chat error:', error);
|
|
1939
2266
|
if (this.panel) {
|
|
2267
|
+
// Clean up any active streaming messages on error
|
|
2268
|
+
this.panel.cancelAllStreamingMessages();
|
|
1940
2269
|
this.panel.addSystemMessage(`Error: ${error}`);
|
|
1941
2270
|
this.panel.setWaitingForResponse(false);
|
|
1942
2271
|
}
|
|
@@ -1948,10 +2277,24 @@ class ChatWidget {
|
|
|
1948
2277
|
};
|
|
1949
2278
|
this.wsClient.onDisconnect = () => {
|
|
1950
2279
|
if (this.panel) {
|
|
1951
|
-
|
|
2280
|
+
// Clean up any active streaming messages on disconnect
|
|
2281
|
+
this.panel.cancelAllStreamingMessages();
|
|
2282
|
+
// Check if there are queued messages
|
|
2283
|
+
const queueSize = this.wsClient.getQueueSize();
|
|
2284
|
+
if (queueSize > 0) {
|
|
2285
|
+
this.panel.addSystemMessage(`Disconnected from chat service. ${queueSize} message(s) will be sent when reconnected.`);
|
|
2286
|
+
}
|
|
2287
|
+
else {
|
|
2288
|
+
this.panel.addSystemMessage('Disconnected from chat service');
|
|
2289
|
+
}
|
|
1952
2290
|
this.panel.setDisabled(true);
|
|
1953
2291
|
}
|
|
1954
2292
|
};
|
|
2293
|
+
this.wsClient.onQueueOverflow = (message) => {
|
|
2294
|
+
if (this.panel) {
|
|
2295
|
+
this.panel.addSystemMessage(message);
|
|
2296
|
+
}
|
|
2297
|
+
};
|
|
1955
2298
|
}
|
|
1956
2299
|
/**
|
|
1957
2300
|
* Handle trigger type configuration
|