andy-widget-react 2.1.1 → 2.2.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 +1 -1
- package/dist/index.cjs +191 -363
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +191 -363
- package/dist/index.esm.js.map +1 -1
- package/dist/types/types/index.d.ts +1 -10
- package/package.json +2 -2
- package/src/AndyChat.tsx +49 -36
- package/src/components/MessageInput.tsx +3 -2
- package/src/components/MessageList.tsx +5 -3
- package/src/hooks/useAndyChat.ts +30 -13
- package/src/types/index.ts +1 -11
package/dist/index.cjs
CHANGED
|
@@ -762,6 +762,84 @@ const initializeConfig = (packageName = 'unknown') => {
|
|
|
762
762
|
}
|
|
763
763
|
};
|
|
764
764
|
|
|
765
|
+
/** Stream a reply while the server restores this session's conversation history. */
|
|
766
|
+
const sendMessageForWidget = async (embedId, sessionId, message, options = {}) => {
|
|
767
|
+
const response = await fetch(`${getApiBaseUrl()}/chat/stream`, {
|
|
768
|
+
method: 'POST',
|
|
769
|
+
headers: { 'Content-Type': 'application/json' },
|
|
770
|
+
signal: options.signal,
|
|
771
|
+
body: JSON.stringify({
|
|
772
|
+
chatbotId: embedId,
|
|
773
|
+
sessionId,
|
|
774
|
+
historySource: 'session',
|
|
775
|
+
messages: [{ role: 'user', parts: [{ type: 'text', text: message }] }]
|
|
776
|
+
})
|
|
777
|
+
});
|
|
778
|
+
if (!response.ok || !response.headers.get('content-type')?.includes('text/event-stream')) {
|
|
779
|
+
const data = await response.json().catch(() => null);
|
|
780
|
+
throw new Error(data?.error || data?.message || 'Chat is temporarily unavailable.')
|
|
781
|
+
}
|
|
782
|
+
if (!response.body) throw new Error('Missing reply stream.')
|
|
783
|
+
|
|
784
|
+
const reader = response.body.getReader();
|
|
785
|
+
const decoder = new TextDecoder();
|
|
786
|
+
let buffer = '';
|
|
787
|
+
let eventData = [];
|
|
788
|
+
let text = '';
|
|
789
|
+
let published = '';
|
|
790
|
+
let complete = false;
|
|
791
|
+
let waiting = false;
|
|
792
|
+
const event = () => {
|
|
793
|
+
if (!eventData.length) return
|
|
794
|
+
const data = eventData.join('\n');
|
|
795
|
+
eventData = [];
|
|
796
|
+
if (data === '[DONE]') { complete = true; return }
|
|
797
|
+
const part = JSON.parse(data);
|
|
798
|
+
if (part.type === 'error' || part.type === 'abort') {
|
|
799
|
+
throw new Error(part.errorText || 'Reply interrupted.')
|
|
800
|
+
}
|
|
801
|
+
if (part.type === 'data-handoff' && part.data?.status === 'waiting') {
|
|
802
|
+
waiting = true;
|
|
803
|
+
return
|
|
804
|
+
}
|
|
805
|
+
if (part.type === 'text-start' && text && !text.endsWith('\n')) text += '\n';
|
|
806
|
+
if (part.type === 'text-delta') {
|
|
807
|
+
if (typeof part.delta !== 'string') throw new Error('Invalid reply text.')
|
|
808
|
+
text += part.delta;
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
const line = value => {
|
|
812
|
+
const cleaned = value.endsWith('\r') ? value.slice(0, -1) : value;
|
|
813
|
+
if (!cleaned) event();
|
|
814
|
+
else if (cleaned.startsWith('data:')) eventData.push(cleaned.slice(5).replace(/^ /, ''));
|
|
815
|
+
};
|
|
816
|
+
try {
|
|
817
|
+
while (!complete) {
|
|
818
|
+
const { value, done } = await reader.read();
|
|
819
|
+
buffer += decoder.decode(value, { stream: !done });
|
|
820
|
+
let newline;
|
|
821
|
+
while ((newline = buffer.indexOf('\n')) !== -1) {
|
|
822
|
+
line(buffer.slice(0, newline));
|
|
823
|
+
buffer = buffer.slice(newline + 1);
|
|
824
|
+
if (complete) break
|
|
825
|
+
}
|
|
826
|
+
// Publish once per received chunk, including the first useful text.
|
|
827
|
+
if (!waiting && text.trim() && text !== published) {
|
|
828
|
+
published = text;
|
|
829
|
+
options.onText?.(text);
|
|
830
|
+
}
|
|
831
|
+
if (done) break
|
|
832
|
+
}
|
|
833
|
+
if (!complete) throw new Error('Reply stream ended before completion.')
|
|
834
|
+
if (waiting) return { status: 'waiting' }
|
|
835
|
+
if (!text.trim()) throw new Error('The reply was empty.')
|
|
836
|
+
return { status: 'agent', text }
|
|
837
|
+
} finally {
|
|
838
|
+
await reader.cancel().catch(() => {});
|
|
839
|
+
reader.releaseLock();
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
|
|
765
843
|
/**
|
|
766
844
|
* AndyChat API client
|
|
767
845
|
* Handles all communication with the AndyChat backend API
|
|
@@ -986,60 +1064,6 @@ const fetchChatbotConfig = async embedId => {
|
|
|
986
1064
|
}
|
|
987
1065
|
};
|
|
988
1066
|
|
|
989
|
-
/**
|
|
990
|
-
* Sends a user message to the API
|
|
991
|
-
* @param {string} embedId - The chatbot embed ID
|
|
992
|
-
* @param {string} sessionId - The current session ID
|
|
993
|
-
* @param {string} message - The user's message
|
|
994
|
-
* @returns {Promise<string>} The bot's response text
|
|
995
|
-
*/
|
|
996
|
-
const sendMessageForWidget = async (embedId, sessionId, message) => {
|
|
997
|
-
try {
|
|
998
|
-
const apiBaseUrl = getApiBaseUrl();
|
|
999
|
-
const response = await fetch(`${apiBaseUrl}/messages`, {
|
|
1000
|
-
method: 'POST',
|
|
1001
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1002
|
-
body: JSON.stringify({
|
|
1003
|
-
chatbotId: embedId,
|
|
1004
|
-
sessionId: sessionId,
|
|
1005
|
-
currentUserMessage: {
|
|
1006
|
-
role: 'user',
|
|
1007
|
-
content: message
|
|
1008
|
-
}
|
|
1009
|
-
})
|
|
1010
|
-
});
|
|
1011
|
-
|
|
1012
|
-
if (response.ok) {
|
|
1013
|
-
const data = await response.json();
|
|
1014
|
-
if (data.error || data.limitStatus) {
|
|
1015
|
-
throw new Error(data.error || 'Chat is temporarily unavailable.')
|
|
1016
|
-
}
|
|
1017
|
-
if (data.handoffStatus === 'paused') {
|
|
1018
|
-
return { status: 'waiting' }
|
|
1019
|
-
}
|
|
1020
|
-
if (typeof data.botResponse !== 'string' || !data.botResponse.trim()) {
|
|
1021
|
-
throw new Error('Unexpected response format from server.')
|
|
1022
|
-
}
|
|
1023
|
-
return { status: 'agent', text: data.botResponse }
|
|
1024
|
-
} else {
|
|
1025
|
-
// Handle HTTP errors (e.g., 500, 404)
|
|
1026
|
-
let errorData;
|
|
1027
|
-
try {
|
|
1028
|
-
errorData = await response.json();
|
|
1029
|
-
} catch (e) {
|
|
1030
|
-
// If parsing error JSON fails, use status text
|
|
1031
|
-
// Also, if response.text() is available and gives more info, consider using it.
|
|
1032
|
-
const responseText = response.text ? await response.text() : '';
|
|
1033
|
-
throw new Error(response.statusText || `Server returned an error. ${responseText}`.trim())
|
|
1034
|
-
}
|
|
1035
|
-
throw new Error(errorData.error || errorData.message || response.statusText || 'Server returned an error.')
|
|
1036
|
-
}
|
|
1037
|
-
} catch (error) {
|
|
1038
|
-
console.error('AndyChat: Message request failed.');
|
|
1039
|
-
throw error
|
|
1040
|
-
}
|
|
1041
|
-
};
|
|
1042
|
-
|
|
1043
1067
|
/**
|
|
1044
1068
|
* Visitor information collection is disabled until Andy exposes a durable public submission
|
|
1045
1069
|
* contract. This explicit failure prevents a Widget from reporting a mocked success.
|
|
@@ -1473,9 +1497,6 @@ const generateDesignTokenVariablesString = () => {
|
|
|
1473
1497
|
--andy-chat-avatar-size: 32px;
|
|
1474
1498
|
--andy-chat-avatar-size-small: 24px;
|
|
1475
1499
|
|
|
1476
|
-
/* Keyboard handling */
|
|
1477
|
-
--andy-keyboard-height: 0px;
|
|
1478
|
-
|
|
1479
1500
|
/* === THEMABLE VARIABLES (Default Light Theme) === */
|
|
1480
1501
|
--andy-themable-background: var(--andy-color-background);
|
|
1481
1502
|
--andy-themable-background-secondary: var(--andy-color-white);
|
|
@@ -2133,6 +2154,10 @@ const getCoreStructuralCssString = () => {
|
|
|
2133
2154
|
}
|
|
2134
2155
|
|
|
2135
2156
|
/* Message Sub-Components */
|
|
2157
|
+
.andy-message__stream {
|
|
2158
|
+
white-space: pre-wrap;
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2136
2161
|
.andy-message__content {
|
|
2137
2162
|
line-height: 1.4;
|
|
2138
2163
|
margin: 0;
|
|
@@ -2855,11 +2880,6 @@ const getCoreStructuralCssString = () => {
|
|
|
2855
2880
|
}
|
|
2856
2881
|
}
|
|
2857
2882
|
|
|
2858
|
-
/* No special handling for keyboard - let browser handle it naturally */
|
|
2859
|
-
.andy-chat-window--keyboard-open {
|
|
2860
|
-
/* Browser handles positioning automatically with our margin-based approach */
|
|
2861
|
-
}
|
|
2862
|
-
|
|
2863
2883
|
/* Ensure the window content is properly laid out */
|
|
2864
2884
|
.andy-chat-window__messages {
|
|
2865
2885
|
flex: 1 1 auto;
|
|
@@ -2977,17 +2997,12 @@ const getCoreStructuralCssString = () => {
|
|
|
2977
2997
|
}
|
|
2978
2998
|
}
|
|
2979
2999
|
|
|
2980
|
-
/* Desktop
|
|
3000
|
+
/* Desktop */
|
|
2981
3001
|
@media (min-width: 521px) {
|
|
2982
3002
|
/* Desktop retains original positioning behavior */
|
|
2983
3003
|
.andy-chat-window {
|
|
2984
3004
|
transition: transform 0.3s ease-out;
|
|
2985
3005
|
}
|
|
2986
|
-
|
|
2987
|
-
/* Optional: Add keyboard handling for desktop if needed */
|
|
2988
|
-
.andy-chat-window--keyboard-open {
|
|
2989
|
-
/* No transform on desktop - keyboard doesn't affect viewport */
|
|
2990
|
-
}
|
|
2991
3006
|
}
|
|
2992
3007
|
|
|
2993
3008
|
/* Orientation */
|
|
@@ -3479,244 +3494,6 @@ const UI_STRINGS = {
|
|
|
3479
3494
|
*/
|
|
3480
3495
|
const getUiStrings = lang => UI_STRINGS[lang] || UI_STRINGS.en;
|
|
3481
3496
|
|
|
3482
|
-
/**
|
|
3483
|
-
* Keyboard detection utility for handling mobile virtual keyboards
|
|
3484
|
-
* Provides cross-browser support for detecting keyboard open/close events
|
|
3485
|
-
*/
|
|
3486
|
-
|
|
3487
|
-
class KeyboardDetector {
|
|
3488
|
-
constructor() {
|
|
3489
|
-
this.isKeyboardOpen = false;
|
|
3490
|
-
this.keyboardHeight = 0;
|
|
3491
|
-
this.listeners = new Set();
|
|
3492
|
-
this.inputElement = null;
|
|
3493
|
-
this.viewportHeight = window.innerHeight;
|
|
3494
|
-
this.visualViewport = window.visualViewport;
|
|
3495
|
-
|
|
3496
|
-
this.initializeDetection();
|
|
3497
|
-
}
|
|
3498
|
-
|
|
3499
|
-
/**
|
|
3500
|
-
* Initialize keyboard detection
|
|
3501
|
-
*/
|
|
3502
|
-
initializeDetection() {
|
|
3503
|
-
// Use Visual Viewport API if available (most modern browsers)
|
|
3504
|
-
if (this.visualViewport) {
|
|
3505
|
-
this.visualViewport.addEventListener('resize', this.handleViewportResize.bind(this));
|
|
3506
|
-
this.visualViewport.addEventListener('scroll', this.handleViewportScroll.bind(this));
|
|
3507
|
-
} else {
|
|
3508
|
-
// Fallback for older browsers
|
|
3509
|
-
window.addEventListener('resize', this.handleWindowResize.bind(this));
|
|
3510
|
-
}
|
|
3511
|
-
|
|
3512
|
-
// Additional detection via focus/blur events
|
|
3513
|
-
document.addEventListener('focusin', this.handleFocusIn.bind(this), true);
|
|
3514
|
-
document.addEventListener('focusout', this.handleFocusOut.bind(this), true);
|
|
3515
|
-
|
|
3516
|
-
// Orientation change handler
|
|
3517
|
-
window.addEventListener('orientationchange', this.handleOrientationChange.bind(this));
|
|
3518
|
-
}
|
|
3519
|
-
|
|
3520
|
-
/**
|
|
3521
|
-
* Handle visual viewport resize (modern browsers)
|
|
3522
|
-
*/
|
|
3523
|
-
handleViewportResize() {
|
|
3524
|
-
const currentHeight = this.visualViewport.height;
|
|
3525
|
-
const heightDifference = window.innerHeight - currentHeight;
|
|
3526
|
-
|
|
3527
|
-
// If viewport shrunk by more than 100px, keyboard is likely open
|
|
3528
|
-
if (heightDifference > 100) {
|
|
3529
|
-
this.setKeyboardOpen(true, heightDifference);
|
|
3530
|
-
} else if (heightDifference < 50) {
|
|
3531
|
-
this.setKeyboardOpen(false, 0);
|
|
3532
|
-
}
|
|
3533
|
-
}
|
|
3534
|
-
|
|
3535
|
-
/**
|
|
3536
|
-
* Handle visual viewport scroll
|
|
3537
|
-
*/
|
|
3538
|
-
handleViewportScroll() {
|
|
3539
|
-
// On iOS, viewport scrolls when keyboard opens
|
|
3540
|
-
if (this.visualViewport.offsetTop > 0) {
|
|
3541
|
-
this.setKeyboardOpen(true, this.visualViewport.offsetTop);
|
|
3542
|
-
}
|
|
3543
|
-
}
|
|
3544
|
-
|
|
3545
|
-
/**
|
|
3546
|
-
* Handle window resize (fallback for older browsers)
|
|
3547
|
-
*/
|
|
3548
|
-
handleWindowResize() {
|
|
3549
|
-
const currentHeight = window.innerHeight;
|
|
3550
|
-
const heightDifference = this.viewportHeight - currentHeight;
|
|
3551
|
-
|
|
3552
|
-
// Significant height reduction likely means keyboard opened
|
|
3553
|
-
if (heightDifference > 100) {
|
|
3554
|
-
this.setKeyboardOpen(true, heightDifference);
|
|
3555
|
-
} else if (Math.abs(heightDifference) < 50) {
|
|
3556
|
-
this.setKeyboardOpen(false, 0);
|
|
3557
|
-
this.viewportHeight = currentHeight;
|
|
3558
|
-
}
|
|
3559
|
-
}
|
|
3560
|
-
|
|
3561
|
-
/**
|
|
3562
|
-
* Handle input focus
|
|
3563
|
-
*/
|
|
3564
|
-
handleFocusIn(e) {
|
|
3565
|
-
const isInput = ['input', 'textarea'].includes(e.target.tagName.toLowerCase());
|
|
3566
|
-
if (isInput) {
|
|
3567
|
-
this.inputElement = e.target;
|
|
3568
|
-
// Delay to allow keyboard animation
|
|
3569
|
-
setTimeout(() => {
|
|
3570
|
-
if (this.inputElement === e.target) {
|
|
3571
|
-
this.checkKeyboardStatus();
|
|
3572
|
-
}
|
|
3573
|
-
}, 300);
|
|
3574
|
-
}
|
|
3575
|
-
}
|
|
3576
|
-
|
|
3577
|
-
/**
|
|
3578
|
-
* Handle input blur
|
|
3579
|
-
*/
|
|
3580
|
-
handleFocusOut(e) {
|
|
3581
|
-
const isInput = ['input', 'textarea'].includes(e.target.tagName.toLowerCase());
|
|
3582
|
-
if (isInput && this.inputElement === e.target) {
|
|
3583
|
-
this.inputElement = null;
|
|
3584
|
-
// Delay to allow keyboard animation
|
|
3585
|
-
setTimeout(() => {
|
|
3586
|
-
if (!this.inputElement) {
|
|
3587
|
-
this.setKeyboardOpen(false, 0);
|
|
3588
|
-
}
|
|
3589
|
-
}, 300);
|
|
3590
|
-
}
|
|
3591
|
-
}
|
|
3592
|
-
|
|
3593
|
-
/**
|
|
3594
|
-
* Handle orientation change
|
|
3595
|
-
*/
|
|
3596
|
-
handleOrientationChange() {
|
|
3597
|
-
// Reset viewport height after orientation change
|
|
3598
|
-
setTimeout(() => {
|
|
3599
|
-
this.viewportHeight = window.innerHeight;
|
|
3600
|
-
this.checkKeyboardStatus();
|
|
3601
|
-
}, 500);
|
|
3602
|
-
}
|
|
3603
|
-
|
|
3604
|
-
/**
|
|
3605
|
-
* Check keyboard status (for browsers without Visual Viewport API)
|
|
3606
|
-
*/
|
|
3607
|
-
checkKeyboardStatus() {
|
|
3608
|
-
if (!this.visualViewport) {
|
|
3609
|
-
const currentHeight = window.innerHeight;
|
|
3610
|
-
const heightDifference = this.viewportHeight - currentHeight;
|
|
3611
|
-
|
|
3612
|
-
if (this.inputElement && heightDifference > 100) {
|
|
3613
|
-
this.setKeyboardOpen(true, heightDifference);
|
|
3614
|
-
}
|
|
3615
|
-
}
|
|
3616
|
-
}
|
|
3617
|
-
|
|
3618
|
-
/**
|
|
3619
|
-
* Set keyboard state and notify listeners
|
|
3620
|
-
*/
|
|
3621
|
-
setKeyboardOpen(isOpen, height) {
|
|
3622
|
-
const changed = this.isKeyboardOpen !== isOpen || this.keyboardHeight !== height;
|
|
3623
|
-
|
|
3624
|
-
this.isKeyboardOpen = isOpen;
|
|
3625
|
-
this.keyboardHeight = height;
|
|
3626
|
-
|
|
3627
|
-
if (changed) {
|
|
3628
|
-
this.notifyListeners();
|
|
3629
|
-
}
|
|
3630
|
-
}
|
|
3631
|
-
|
|
3632
|
-
/**
|
|
3633
|
-
* Add listener for keyboard state changes
|
|
3634
|
-
*/
|
|
3635
|
-
addListener(callback) {
|
|
3636
|
-
this.listeners.add(callback);
|
|
3637
|
-
// Immediately notify with current state
|
|
3638
|
-
callback({
|
|
3639
|
-
isOpen: this.isKeyboardOpen,
|
|
3640
|
-
height: this.keyboardHeight
|
|
3641
|
-
});
|
|
3642
|
-
}
|
|
3643
|
-
|
|
3644
|
-
/**
|
|
3645
|
-
* Remove listener
|
|
3646
|
-
*/
|
|
3647
|
-
removeListener(callback) {
|
|
3648
|
-
this.listeners.delete(callback);
|
|
3649
|
-
}
|
|
3650
|
-
|
|
3651
|
-
/**
|
|
3652
|
-
* Notify all listeners of state change
|
|
3653
|
-
*/
|
|
3654
|
-
notifyListeners() {
|
|
3655
|
-
const state = {
|
|
3656
|
-
isOpen: this.isKeyboardOpen,
|
|
3657
|
-
height: this.keyboardHeight
|
|
3658
|
-
};
|
|
3659
|
-
|
|
3660
|
-
this.listeners.forEach(callback => {
|
|
3661
|
-
try {
|
|
3662
|
-
callback(state);
|
|
3663
|
-
} catch (error) {
|
|
3664
|
-
console.error('Error in keyboard detector listener:', error);
|
|
3665
|
-
}
|
|
3666
|
-
});
|
|
3667
|
-
}
|
|
3668
|
-
|
|
3669
|
-
/**
|
|
3670
|
-
* Get current keyboard state
|
|
3671
|
-
*/
|
|
3672
|
-
getState() {
|
|
3673
|
-
return {
|
|
3674
|
-
isOpen: this.isKeyboardOpen,
|
|
3675
|
-
height: this.keyboardHeight
|
|
3676
|
-
}
|
|
3677
|
-
}
|
|
3678
|
-
|
|
3679
|
-
/**
|
|
3680
|
-
* Cleanup
|
|
3681
|
-
*/
|
|
3682
|
-
destroy() {
|
|
3683
|
-
if (this.visualViewport) {
|
|
3684
|
-
this.visualViewport.removeEventListener('resize', this.handleViewportResize.bind(this));
|
|
3685
|
-
this.visualViewport.removeEventListener('scroll', this.handleViewportScroll.bind(this));
|
|
3686
|
-
} else {
|
|
3687
|
-
window.removeEventListener('resize', this.handleWindowResize.bind(this));
|
|
3688
|
-
}
|
|
3689
|
-
|
|
3690
|
-
document.removeEventListener('focusin', this.handleFocusIn.bind(this), true);
|
|
3691
|
-
document.removeEventListener('focusout', this.handleFocusOut.bind(this), true);
|
|
3692
|
-
window.removeEventListener('orientationchange', this.handleOrientationChange.bind(this));
|
|
3693
|
-
|
|
3694
|
-
this.listeners.clear();
|
|
3695
|
-
}
|
|
3696
|
-
}
|
|
3697
|
-
|
|
3698
|
-
// Create singleton instance
|
|
3699
|
-
let keyboardDetectorInstance = null;
|
|
3700
|
-
|
|
3701
|
-
/**
|
|
3702
|
-
* Get or create keyboard detector instance
|
|
3703
|
-
*/
|
|
3704
|
-
function getKeyboardDetector() {
|
|
3705
|
-
if (!keyboardDetectorInstance) {
|
|
3706
|
-
keyboardDetectorInstance = new KeyboardDetector();
|
|
3707
|
-
}
|
|
3708
|
-
return keyboardDetectorInstance
|
|
3709
|
-
}
|
|
3710
|
-
|
|
3711
|
-
/**
|
|
3712
|
-
* Update CSS custom property on host element
|
|
3713
|
-
*/
|
|
3714
|
-
function updateKeyboardHeight(hostElement, height) {
|
|
3715
|
-
if (hostElement) {
|
|
3716
|
-
hostElement.style.setProperty('--andy-keyboard-height', `${height}px`);
|
|
3717
|
-
}
|
|
3718
|
-
}
|
|
3719
|
-
|
|
3720
3497
|
/**
|
|
3721
3498
|
* Mobile detection utility for AndyChat
|
|
3722
3499
|
* Provides reliable mobile device detection with multiple methods
|
|
@@ -3771,12 +3548,12 @@ const callConvex = async (convexUrl, kind, path, wireArgs, keepalive = false) =>
|
|
|
3771
3548
|
const sortMessages = messages => [...messages].sort((left, right) =>
|
|
3772
3549
|
left.createdAt - right.createdAt || String(left.id).localeCompare(String(right.id)));
|
|
3773
3550
|
|
|
3774
|
-
const createBrowserHandoff = (chatbotId, bootstrap, onProjection) => {
|
|
3551
|
+
const createBrowserHandoff = (chatbotId, bootstrap, onProjection, fresh = false) => {
|
|
3775
3552
|
if (!bootstrap?.convexUrl || !bootstrap.surfaceKey || !bootstrap.surfaceProof) return null
|
|
3776
3553
|
const storageKey = identityKey(chatbotId);
|
|
3777
3554
|
let identity;
|
|
3778
3555
|
try {
|
|
3779
|
-
const stored = JSON.parse(window.sessionStorage.getItem(storageKey) || 'null');
|
|
3556
|
+
const stored = fresh ? null : JSON.parse(window.sessionStorage.getItem(storageKey) || 'null');
|
|
3780
3557
|
identity = stored?.sessionId && stored?.capability && stored.surfaceKey === bootstrap.surfaceKey
|
|
3781
3558
|
? { sessionId: stored.sessionId, capability: stored.capability }
|
|
3782
3559
|
: randomIdentity();
|
|
@@ -3865,6 +3642,14 @@ const createBrowserHandoff = (chatbotId, bootstrap, onProjection) => {
|
|
|
3865
3642
|
if (active) onProjection({ offline: true, identity, messages: [] });
|
|
3866
3643
|
}
|
|
3867
3644
|
}, 15000);
|
|
3645
|
+
const stop = () => {
|
|
3646
|
+
if (!active) return
|
|
3647
|
+
active = false;
|
|
3648
|
+
window.clearInterval(timer);
|
|
3649
|
+
document.removeEventListener('visibilitychange', onVisibility);
|
|
3650
|
+
window.removeEventListener('pagehide', release);
|
|
3651
|
+
void release();
|
|
3652
|
+
};
|
|
3868
3653
|
return {
|
|
3869
3654
|
identity: { sessionId: identity.sessionId },
|
|
3870
3655
|
recordVisitorMessage: content => {
|
|
@@ -3886,14 +3671,11 @@ const createBrowserHandoff = (chatbotId, bootstrap, onProjection) => {
|
|
|
3886
3671
|
acknowledging.delete(messageId);
|
|
3887
3672
|
}
|
|
3888
3673
|
},
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
window.removeEventListener('pagehide', release);
|
|
3895
|
-
void release();
|
|
3896
|
-
}
|
|
3674
|
+
restart: () => {
|
|
3675
|
+
stop();
|
|
3676
|
+
return createBrowserHandoff(chatbotId, bootstrap, onProjection, true)
|
|
3677
|
+
},
|
|
3678
|
+
stop
|
|
3897
3679
|
}
|
|
3898
3680
|
};
|
|
3899
3681
|
|
|
@@ -3907,6 +3689,8 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
|
|
|
3907
3689
|
const [isOpen, setIsOpen] = react.useState(initialOpen);
|
|
3908
3690
|
const [messages, setMessages] = react.useState([]);
|
|
3909
3691
|
const [isLoading, setIsLoading] = react.useState(false);
|
|
3692
|
+
const [replyStarted, setReplyStarted] = react.useState(false);
|
|
3693
|
+
const activeReplyRef = react.useRef(null);
|
|
3910
3694
|
const [sessionId, setSessionId] = react.useState(() => generateSessionId());
|
|
3911
3695
|
const handoffRef = react.useRef(null);
|
|
3912
3696
|
const pendingHandoffIdsRef = react.useRef([]);
|
|
@@ -3930,6 +3714,11 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
|
|
|
3930
3714
|
const handoff = createBrowserHandoff(config.id, config.bootstrap, projection => {
|
|
3931
3715
|
if (projection.offline)
|
|
3932
3716
|
return;
|
|
3717
|
+
const paused = projection.control === 'requested' || projection.control === 'human';
|
|
3718
|
+
if (paused)
|
|
3719
|
+
activeReplyRef.current?.abort();
|
|
3720
|
+
else if (activeReplyRef.current)
|
|
3721
|
+
return;
|
|
3933
3722
|
setSessionId(projection.identity.sessionId);
|
|
3934
3723
|
pendingHandoffIdsRef.current = projection.pendingProjectionIds || [];
|
|
3935
3724
|
const projectedMessages = projection.messages.map(message => ({ id: message.id, content: message.content, sender: message.role === 'user' ? 'user' : 'bot', timestamp: new Date(message.createdAt).toISOString(), isHuman: message.sentByHuman }));
|
|
@@ -3941,8 +3730,11 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
|
|
|
3941
3730
|
: projectedMessages);
|
|
3942
3731
|
});
|
|
3943
3732
|
handoffRef.current = handoff;
|
|
3733
|
+
if (handoff)
|
|
3734
|
+
setSessionId(handoff.identity.sessionId);
|
|
3944
3735
|
return () => {
|
|
3945
|
-
|
|
3736
|
+
activeReplyRef.current?.abort();
|
|
3737
|
+
handoffRef.current?.stop();
|
|
3946
3738
|
handoffRef.current = null;
|
|
3947
3739
|
};
|
|
3948
3740
|
}, [config?.bootstrap, config?.id]);
|
|
@@ -3991,22 +3783,7 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
|
|
|
3991
3783
|
portalRef.current = portalTarget;
|
|
3992
3784
|
}
|
|
3993
3785
|
setShadowReady(true);
|
|
3994
|
-
const keyboardDetector = getKeyboardDetector();
|
|
3995
|
-
const keyboardListener = ({ isOpen: kbOpen, height }) => {
|
|
3996
|
-
updateKeyboardHeight(containerRef.current, height);
|
|
3997
|
-
const chatWindow = shadowRef.current?.querySelector('.andy-chat-window');
|
|
3998
|
-
if (chatWindow) {
|
|
3999
|
-
if (kbOpen && height > 0) {
|
|
4000
|
-
chatWindow.classList.add('andy-chat-window--keyboard-open');
|
|
4001
|
-
}
|
|
4002
|
-
else {
|
|
4003
|
-
chatWindow.classList.remove('andy-chat-window--keyboard-open');
|
|
4004
|
-
}
|
|
4005
|
-
}
|
|
4006
|
-
};
|
|
4007
|
-
keyboardDetector.addListener(keyboardListener);
|
|
4008
3786
|
return () => {
|
|
4009
|
-
keyboardDetector.removeListener(keyboardListener);
|
|
4010
3787
|
shadowRef.current = null;
|
|
4011
3788
|
portalRef.current = null;
|
|
4012
3789
|
setShadowReady(false);
|
|
@@ -4264,11 +4041,13 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
|
|
|
4264
4041
|
}, [isCollectingUserInfo, messages]);
|
|
4265
4042
|
const sendUserMessage = react.useCallback(async (rawMessageText, options = {}) => {
|
|
4266
4043
|
const messageText = typeof rawMessageText === 'string' ? rawMessageText.trim() : '';
|
|
4267
|
-
if (!messageText) {
|
|
4044
|
+
if (!messageText || activeReplyRef.current) {
|
|
4268
4045
|
return;
|
|
4269
4046
|
}
|
|
4270
4047
|
const { hidePredefined = true } = options;
|
|
4271
4048
|
const timestamp = Date.now();
|
|
4049
|
+
const controller = new AbortController();
|
|
4050
|
+
activeReplyRef.current = controller;
|
|
4272
4051
|
const userMessage = {
|
|
4273
4052
|
id: timestamp,
|
|
4274
4053
|
content: messageText,
|
|
@@ -4282,18 +4061,30 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
|
|
|
4282
4061
|
}
|
|
4283
4062
|
try {
|
|
4284
4063
|
setIsLoading(true);
|
|
4285
|
-
|
|
4064
|
+
setReplyStarted(false);
|
|
4065
|
+
const showReply = (content, isStreaming) => {
|
|
4066
|
+
if (controller.signal.aborted)
|
|
4067
|
+
return;
|
|
4068
|
+
setReplyStarted(true);
|
|
4069
|
+
const reply = { id: timestamp + 1, content, sender: 'bot', timestamp: new Date(timestamp).toISOString(), isStreaming };
|
|
4070
|
+
setMessages(prev => prev.some(message => message.id === reply.id)
|
|
4071
|
+
? prev.map(message => message.id === reply.id ? reply : message)
|
|
4072
|
+
: [...prev, reply]);
|
|
4073
|
+
};
|
|
4074
|
+
const response = await sendMessageForWidget(embedId, sessionId, messageText, {
|
|
4075
|
+
signal: controller.signal,
|
|
4076
|
+
onText: text => showReply(text, true)
|
|
4077
|
+
});
|
|
4078
|
+
if (controller.signal.aborted)
|
|
4079
|
+
return;
|
|
4286
4080
|
if (response.status === 'waiting') {
|
|
4287
|
-
setMessages(prev =>
|
|
4081
|
+
setMessages(prev => {
|
|
4082
|
+
const visible = prev.filter(message => message.id !== timestamp + 1);
|
|
4083
|
+
return visible.some(message => message.isWaiting) ? visible : [...visible, { id: 'handoff-waiting', content: strings.handoffWaiting, sender: 'bot', timestamp: new Date().toISOString(), isWaiting: true }];
|
|
4084
|
+
});
|
|
4288
4085
|
return;
|
|
4289
4086
|
}
|
|
4290
|
-
|
|
4291
|
-
id: timestamp + 1,
|
|
4292
|
-
content: response.text,
|
|
4293
|
-
sender: 'bot',
|
|
4294
|
-
timestamp: new Date().toISOString()
|
|
4295
|
-
};
|
|
4296
|
-
setMessages(prev => [...prev, botMessage]);
|
|
4087
|
+
showReply(response.text, false);
|
|
4297
4088
|
if (config?.collectUserInfo?.enabled && !userInfoCollected && !isCollectingUserInfo) {
|
|
4298
4089
|
setPendingMessage(messageText);
|
|
4299
4090
|
setIsCollectingUserInfo(true);
|
|
@@ -4308,6 +4099,8 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
|
|
|
4308
4099
|
}
|
|
4309
4100
|
}
|
|
4310
4101
|
catch {
|
|
4102
|
+
if (controller.signal.aborted)
|
|
4103
|
+
return;
|
|
4311
4104
|
console.error('AndyChat: Message request failed.');
|
|
4312
4105
|
const errorMessage = {
|
|
4313
4106
|
id: timestamp + 3,
|
|
@@ -4316,10 +4109,14 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
|
|
|
4316
4109
|
timestamp: new Date().toISOString(),
|
|
4317
4110
|
isError: true
|
|
4318
4111
|
};
|
|
4319
|
-
setMessages(prev => [...prev, errorMessage]);
|
|
4112
|
+
setMessages(prev => [...prev.filter(message => message.id !== timestamp + 1), errorMessage]);
|
|
4320
4113
|
}
|
|
4321
4114
|
finally {
|
|
4322
|
-
|
|
4115
|
+
if (activeReplyRef.current === controller) {
|
|
4116
|
+
activeReplyRef.current = null;
|
|
4117
|
+
setIsLoading(false);
|
|
4118
|
+
setReplyStarted(false);
|
|
4119
|
+
}
|
|
4323
4120
|
}
|
|
4324
4121
|
}, [config, embedId, sessionId, userInfoCollected, isCollectingUserInfo]);
|
|
4325
4122
|
const handleSendMessage = react.useCallback((messageText) => {
|
|
@@ -4481,21 +4278,27 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
|
|
|
4481
4278
|
if (message.isLoading) {
|
|
4482
4279
|
return (jsxRuntime.jsx("div", { className: 'andy-message-container andy-message-container--bot andy-message-container--loading', children: jsxRuntime.jsx("div", { className: 'andy-message andy-message--bot', children: jsxRuntime.jsxs("div", { className: 'andy-typing-indicator', children: [jsxRuntime.jsx("div", { className: 'andy-typing-indicator__dot' }), jsxRuntime.jsx("div", { className: 'andy-typing-indicator__dot' }), jsxRuntime.jsx("div", { className: 'andy-typing-indicator__dot' })] }) }) }, message.id));
|
|
4483
4280
|
}
|
|
4484
|
-
return (jsxRuntime.jsxs("div", { className: `andy-message-container andy-message-container--${message.sender} ${message.isWaiting ? 'andy-message-container--waiting' : ''}`, children: [message.sender === 'bot' && (jsxRuntime.jsxs("div", { className: `andy-message andy-message--bot ${message.isHuman ? 'andy-message--operator' : ''} ${message.isWaiting ? 'andy-message--waiting' : ''} ${message.isError ? 'andy-message--error' : ''}`, children: [jsxRuntime.jsxs("div", { className: 'andy-bot-header', children: [jsxRuntime.jsx("div", { className: 'andy-bot-header__icon', children: config?.logoUrl ? (jsxRuntime.jsx("img", { src: config.logoUrl, alt: 'Bot Avatar', className: 'andy-bot-header__icon-image' })) : (jsxRuntime.jsx("img", { src: CHAT_BUBBLE_DEFAULT_ICON_URL, alt: 'Bot Avatar', className: 'andy-bot-header__icon-image andy-bot-header__icon-image--default' })) }), jsxRuntime.jsx("span", { className: 'andy-bot-header__name', children: message.isHuman ? strings.teamName : config?.name || 'Andy bot' })] }), jsxRuntime.jsx("div", { className: 'andy-message__content', children: jsxRuntime.jsx("div", { dangerouslySetInnerHTML: { __html: parseMarkdown(message.content) } }) })] })), message.sender === 'user' && (jsxRuntime.jsx("div", { className: 'andy-message andy-message--user', children: jsxRuntime.jsx("div", { className: 'andy-message__content', children: jsxRuntime.jsx("p", { children: message.content }) }) }))] }, message.id));
|
|
4485
|
-
}), isLoading && (jsxRuntime.jsx("div", { className: 'andy-message-container andy-message-container--bot andy-message-container--typing', children: jsxRuntime.jsx("div", { className: 'andy-message andy-message--bot andy-message--typing', children: jsxRuntime.jsxs("div", { className: 'andy-typing-indicator', children: [jsxRuntime.jsx("div", { className: 'andy-typing-indicator__dot' }), jsxRuntime.jsx("div", { className: 'andy-typing-indicator__dot' }), jsxRuntime.jsx("div", { className: 'andy-typing-indicator__dot' })] }) }) }))] }), jsxRuntime.jsxs("div", { className: 'andy-chat-window__bottom', children: [activeQuestions &&
|
|
4281
|
+
return (jsxRuntime.jsxs("div", { className: `andy-message-container andy-message-container--${message.sender} ${message.isWaiting ? 'andy-message-container--waiting' : ''}`, children: [message.sender === 'bot' && (jsxRuntime.jsxs("div", { className: `andy-message andy-message--bot ${message.isHuman ? 'andy-message--operator' : ''} ${message.isWaiting ? 'andy-message--waiting' : ''} ${message.isError ? 'andy-message--error' : ''}`, children: [jsxRuntime.jsxs("div", { className: 'andy-bot-header', children: [jsxRuntime.jsx("div", { className: 'andy-bot-header__icon', children: config?.logoUrl ? (jsxRuntime.jsx("img", { src: config.logoUrl, alt: 'Bot Avatar', className: 'andy-bot-header__icon-image' })) : (jsxRuntime.jsx("img", { src: CHAT_BUBBLE_DEFAULT_ICON_URL, alt: 'Bot Avatar', className: 'andy-bot-header__icon-image andy-bot-header__icon-image--default' })) }), jsxRuntime.jsx("span", { className: 'andy-bot-header__name', children: message.isHuman ? strings.teamName : config?.name || 'Andy bot' })] }), jsxRuntime.jsx("div", { className: 'andy-message__content', children: message.isStreaming ? jsxRuntime.jsx("div", { className: 'andy-message__stream', children: message.content }) : jsxRuntime.jsx("div", { dangerouslySetInnerHTML: { __html: parseMarkdown(message.content) } }) })] })), message.sender === 'user' && (jsxRuntime.jsx("div", { className: 'andy-message andy-message--user', children: jsxRuntime.jsx("div", { className: 'andy-message__content', children: jsxRuntime.jsx("p", { children: message.content }) }) }))] }, message.id));
|
|
4282
|
+
}), isLoading && !replyStarted && (jsxRuntime.jsx("div", { className: 'andy-message-container andy-message-container--bot andy-message-container--typing', children: jsxRuntime.jsx("div", { className: 'andy-message andy-message--bot andy-message--typing', children: jsxRuntime.jsxs("div", { className: 'andy-typing-indicator', children: [jsxRuntime.jsx("div", { className: 'andy-typing-indicator__dot' }), jsxRuntime.jsx("div", { className: 'andy-typing-indicator__dot' }), jsxRuntime.jsx("div", { className: 'andy-typing-indicator__dot' })] }) }) }))] }), jsxRuntime.jsxs("div", { className: 'andy-chat-window__bottom', children: [activeQuestions &&
|
|
4486
4283
|
activeQuestions.length > 0 &&
|
|
4487
4284
|
showPredefinedQuestions && (jsxRuntime.jsxs("div", { className: 'andy-predefined-questions', id: 'andy-predefined-questions', children: [jsxRuntime.jsx("div", { className: 'andy-predefined-questions__header', children: jsxRuntime.jsx("p", { className: 'andy-predefined-questions__title', children: strings.predefinedQuestionsTitle }) }), jsxRuntime.jsx("div", { className: 'andy-predefined-questions__list', children: activeQuestions.map((q, index) => (jsxRuntime.jsxs("button", { type: 'button', className: 'andy-predefined-question', "data-question": q.question, disabled: isCollectingUserInfo || isLoading || !config, children: [jsxRuntime.jsx("span", { className: 'andy-predefined-question__icon', children: "\uD83D\uDCAC" }), jsxRuntime.jsx("span", { className: 'andy-predefined-question__text', children: q.question })] }, index))) })] })), jsxRuntime.jsx("div", { className: 'andy-chat-window__input', children: jsxRuntime.jsx("div", { className: 'andy-input-container', children: jsxRuntime.jsxs("div", { className: 'andy-input', children: [jsxRuntime.jsx("input", { type: 'text', className: 'andy-input__field', id: 'andy-message-input', placeholder: config ? strings.inputPlaceholder : '...', "aria-label": strings.messageInput, onKeyDown: e => {
|
|
4285
|
+
if (e.key === 'Enter' && activeReplyRef.current) {
|
|
4286
|
+
e.preventDefault();
|
|
4287
|
+
return;
|
|
4288
|
+
}
|
|
4488
4289
|
if (e.key === 'Enter' && e.target.value.trim()) {
|
|
4489
4290
|
handleSendMessage(e.target.value);
|
|
4490
4291
|
e.target.value = '';
|
|
4491
4292
|
}
|
|
4492
4293
|
}, disabled: isCollectingUserInfo || !config }), jsxRuntime.jsx("button", { type: 'button', className: 'andy-input__button', id: 'andy-send-button', "aria-label": strings.sendMessage, onClick: () => {
|
|
4294
|
+
if (activeReplyRef.current)
|
|
4295
|
+
return;
|
|
4493
4296
|
const input = shadowRef.current?.getElementById('andy-message-input');
|
|
4494
4297
|
if (input && input.value.trim()) {
|
|
4495
4298
|
handleSendMessage(input.value);
|
|
4496
4299
|
input.value = '';
|
|
4497
4300
|
}
|
|
4498
|
-
}, disabled: isCollectingUserInfo || !config, children: jsxRuntime.jsx("svg", { className: 'andy-input__button-icon', width: '18', height: '18', viewBox: '0 0 24 24', fill: 'currentColor', xmlns: 'http://www.w3.org/2000/svg', children: jsxRuntime.jsx("path", { d: 'M2.01 21L23 12 2.01 3 2 10l15 2-15 2z' }) }) })] }) }) }), config?.showBranding !== false && (jsxRuntime.jsx("footer", { className: 'andy-chat-window__footer', children: jsxRuntime.jsx("a", { href: getFooterUrl(), target: '_blank', rel: 'noopener noreferrer', className: 'andy-chat-window__footer-link', children: jsxRuntime.jsx("span", { className: 'andy-chat-window__footer-text', children: FOOTER_TEXT }) }) }))] })] }), portalRef.current), showWelcomePopup &&
|
|
4301
|
+
}, disabled: isCollectingUserInfo || isLoading || !config, children: jsxRuntime.jsx("svg", { className: 'andy-input__button-icon', width: '18', height: '18', viewBox: '0 0 24 24', fill: 'currentColor', xmlns: 'http://www.w3.org/2000/svg', children: jsxRuntime.jsx("path", { d: 'M2.01 21L23 12 2.01 3 2 10l15 2-15 2z' }) }) })] }) }) }), config?.showBranding !== false && (jsxRuntime.jsx("footer", { className: 'andy-chat-window__footer', children: jsxRuntime.jsx("a", { href: getFooterUrl(), target: '_blank', rel: 'noopener noreferrer', className: 'andy-chat-window__footer-link', children: jsxRuntime.jsx("span", { className: 'andy-chat-window__footer-text', children: FOOTER_TEXT }) }) }))] })] }), portalRef.current), showWelcomePopup &&
|
|
4499
4302
|
!isOpen &&
|
|
4500
4303
|
ReactDOM.createPortal(jsxRuntime.jsxs("div", { className: `andy-welcome-popup ${config?.orientation === 'left' ? 'andychat-popup-left' : ''}`, style: { display: 'block' }, role: 'dialog', "aria-label": strings.welcomeMessage, children: [jsxRuntime.jsxs("div", { className: 'andy-welcome-popup__content', children: [jsxRuntime.jsx("div", { className: 'andy-welcome-popup__message', children: config?.showWelcomePopup?.message ||
|
|
4501
4304
|
config?.welcomeMessage ||
|
|
@@ -4516,6 +4319,11 @@ const useAndyChat = (embedId) => {
|
|
|
4516
4319
|
const [showingWelcomePopup, setShowingWelcomePopup] = react.useState(false);
|
|
4517
4320
|
const [showingUserInfoForm, setShowingUserInfoForm] = react.useState(false);
|
|
4518
4321
|
const [welcomePopupShown, setWelcomePopupShown] = react.useState(false);
|
|
4322
|
+
const activeReplyRef = react.useRef(null);
|
|
4323
|
+
react.useEffect(() => () => {
|
|
4324
|
+
activeReplyRef.current?.abort();
|
|
4325
|
+
activeReplyRef.current = null;
|
|
4326
|
+
}, [embedId]);
|
|
4519
4327
|
const welcomeTimeoutRef = react.useRef(null);
|
|
4520
4328
|
const userInfoTimeoutRef = react.useRef(null);
|
|
4521
4329
|
const initializeChatbot = react.useCallback(async () => {
|
|
@@ -4587,9 +4395,11 @@ const useAndyChat = (embedId) => {
|
|
|
4587
4395
|
});
|
|
4588
4396
|
}, [config, welcomePopupShown, userInfoCollected]);
|
|
4589
4397
|
const sendMessage = react.useCallback(async (messageText) => {
|
|
4590
|
-
if (!messageText.trim() || !config || !sessionId) {
|
|
4398
|
+
if (!messageText.trim() || !config || !sessionId || activeReplyRef.current) {
|
|
4591
4399
|
return;
|
|
4592
4400
|
}
|
|
4401
|
+
const controller = new AbortController();
|
|
4402
|
+
activeReplyRef.current = controller;
|
|
4593
4403
|
const userMessage = {
|
|
4594
4404
|
id: Date.now(),
|
|
4595
4405
|
content: messageText,
|
|
@@ -4597,20 +4407,29 @@ const useAndyChat = (embedId) => {
|
|
|
4597
4407
|
timestamp: new Date().toISOString()
|
|
4598
4408
|
};
|
|
4599
4409
|
setMessages(prev => [...prev, userMessage]);
|
|
4410
|
+
const replyId = Date.now() + 1;
|
|
4411
|
+
const showReply = (content, isStreaming) => {
|
|
4412
|
+
if (controller.signal.aborted)
|
|
4413
|
+
return;
|
|
4414
|
+
const reply = { id: replyId, content, sender: 'bot', timestamp: new Date().toISOString(), isStreaming };
|
|
4415
|
+
setMessages(prev => prev.some(message => message.id === replyId)
|
|
4416
|
+
? prev.map(message => message.id === replyId ? reply : message)
|
|
4417
|
+
: [...prev, reply]);
|
|
4418
|
+
};
|
|
4600
4419
|
try {
|
|
4601
|
-
const response = await sendMessageForWidget(embedId, sessionId, messageText);
|
|
4602
|
-
if (
|
|
4420
|
+
const response = await sendMessageForWidget(embedId, sessionId, messageText, { signal: controller.signal, onText: text => showReply(text, true) });
|
|
4421
|
+
if (controller.signal.aborted)
|
|
4422
|
+
return;
|
|
4423
|
+
if (response.status === 'waiting') {
|
|
4424
|
+
setMessages(prev => prev.filter(message => message.id !== replyId));
|
|
4603
4425
|
return response;
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
content: response.text || 'I apologize, but I encountered an error processing your request.',
|
|
4607
|
-
sender: 'bot',
|
|
4608
|
-
timestamp: new Date().toISOString()
|
|
4609
|
-
};
|
|
4610
|
-
setMessages(prev => [...prev, botMessage]);
|
|
4426
|
+
}
|
|
4427
|
+
showReply(response.text, false);
|
|
4611
4428
|
return response;
|
|
4612
4429
|
}
|
|
4613
4430
|
catch (err) {
|
|
4431
|
+
if (controller.signal.aborted)
|
|
4432
|
+
return;
|
|
4614
4433
|
console.error('Error sending message:', err);
|
|
4615
4434
|
const errorMessage = {
|
|
4616
4435
|
id: Date.now() + 1,
|
|
@@ -4619,9 +4438,13 @@ const useAndyChat = (embedId) => {
|
|
|
4619
4438
|
timestamp: new Date().toISOString(),
|
|
4620
4439
|
isError: true
|
|
4621
4440
|
};
|
|
4622
|
-
setMessages(prev => [...prev, errorMessage]);
|
|
4441
|
+
setMessages(prev => [...prev.filter(message => message.id !== replyId), errorMessage]);
|
|
4623
4442
|
throw err;
|
|
4624
4443
|
}
|
|
4444
|
+
finally {
|
|
4445
|
+
if (activeReplyRef.current === controller)
|
|
4446
|
+
activeReplyRef.current = null;
|
|
4447
|
+
}
|
|
4625
4448
|
}, [embedId, sessionId, config]);
|
|
4626
4449
|
const showFaq = react.useCallback(() => {
|
|
4627
4450
|
setShowingFaq(true);
|
|
@@ -4660,6 +4483,8 @@ const useAndyChat = (embedId) => {
|
|
|
4660
4483
|
}
|
|
4661
4484
|
}, [embedId, sessionId]);
|
|
4662
4485
|
const startNewSession = react.useCallback(() => {
|
|
4486
|
+
activeReplyRef.current?.abort();
|
|
4487
|
+
activeReplyRef.current = null;
|
|
4663
4488
|
const newSessionId = generateSessionId();
|
|
4664
4489
|
setSessionId(newSessionId);
|
|
4665
4490
|
{
|
|
@@ -4729,9 +4554,11 @@ const MessageComponent = ({ message, botName }) => {
|
|
|
4729
4554
|
.filter(Boolean)
|
|
4730
4555
|
.join(' ');
|
|
4731
4556
|
const messageClasses = ['andy-message', `andy-message--${message.sender}`].filter(Boolean).join(' ');
|
|
4732
|
-
const messageContent = isBot ? parseMarkdown(message.content) : message.content;
|
|
4557
|
+
const messageContent = isBot && !message.isStreaming ? parseMarkdown(message.content) : message.content;
|
|
4733
4558
|
if (isBot) {
|
|
4734
|
-
return (jsxRuntime.jsxs("div", { className: containerClasses, children: [jsxRuntime.jsxs("div", { className: 'andy-bot-header', children: [jsxRuntime.jsx("div", { className: 'andy-bot-header__icon', children: jsxRuntime.jsxs("svg", { width: '16', height: '16', viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round', children: [jsxRuntime.jsx("path", { d: 'M12 6V2H8' }), jsxRuntime.jsx("path", { d: 'm8 18-4 4V8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2Z' }), jsxRuntime.jsx("path", { d: 'M2 12h2' }), jsxRuntime.jsx("path", { d: 'M9 11v2' }), jsxRuntime.jsx("path", { d: 'M15 11v2' }), jsxRuntime.jsx("path", { d: 'M20 12h2' })] }) }), jsxRuntime.jsx("span", { className: 'andy-bot-header__name', children: botName || 'Andy bot' })] }), jsxRuntime.jsx("div", { className: messageClasses, children:
|
|
4559
|
+
return (jsxRuntime.jsxs("div", { className: containerClasses, children: [jsxRuntime.jsxs("div", { className: 'andy-bot-header', children: [jsxRuntime.jsx("div", { className: 'andy-bot-header__icon', children: jsxRuntime.jsxs("svg", { width: '16', height: '16', viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round', children: [jsxRuntime.jsx("path", { d: 'M12 6V2H8' }), jsxRuntime.jsx("path", { d: 'm8 18-4 4V8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2Z' }), jsxRuntime.jsx("path", { d: 'M2 12h2' }), jsxRuntime.jsx("path", { d: 'M9 11v2' }), jsxRuntime.jsx("path", { d: 'M15 11v2' }), jsxRuntime.jsx("path", { d: 'M20 12h2' })] }) }), jsxRuntime.jsx("span", { className: 'andy-bot-header__name', children: botName || 'Andy bot' })] }), jsxRuntime.jsx("div", { className: messageClasses, children: message.isStreaming
|
|
4560
|
+
? jsxRuntime.jsx("div", { className: 'andy-message__content andy-message__stream', children: message.content })
|
|
4561
|
+
: jsxRuntime.jsx("div", { className: 'andy-message__content', dangerouslySetInnerHTML: { __html: messageContent } }) })] }));
|
|
4735
4562
|
}
|
|
4736
4563
|
return (jsxRuntime.jsx("div", { className: containerClasses, children: jsxRuntime.jsx("div", { className: messageClasses, children: jsxRuntime.jsx("div", { className: 'andy-message__content', children: jsxRuntime.jsx("p", { children: messageContent }) }) }) }));
|
|
4737
4564
|
};
|
|
@@ -4746,7 +4573,7 @@ const MessageList = ({ messages, isLoading, logoUrl, botName }) => {
|
|
|
4746
4573
|
});
|
|
4747
4574
|
}
|
|
4748
4575
|
}, [messages, isLoading]);
|
|
4749
|
-
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [messages.map(message => (jsxRuntime.jsx(MessageComponent, { message: message, logoUrl: logoUrl, botName: botName }, message.id || message.timestamp))), isLoading && jsxRuntime.jsx(TypingIndicator, {}), messages.length === 0 && !isLoading && (jsxRuntime.jsxs("div", { className: 'andy-empty-state', children: [jsxRuntime.jsx("div", { className: 'andy-empty-state__icon', children: "\uD83D\uDCAC" }), jsxRuntime.jsx("div", { className: 'andy-empty-state__text', children: "Start a conversation by typing a message below" })] })), jsxRuntime.jsx("div", { ref: messagesEndRef })] }));
|
|
4576
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [messages.map(message => (jsxRuntime.jsx(MessageComponent, { message: message, logoUrl: logoUrl, botName: botName }, message.id || message.timestamp))), isLoading && !messages.some(message => message.isStreaming) && jsxRuntime.jsx(TypingIndicator, {}), messages.length === 0 && !isLoading && (jsxRuntime.jsxs("div", { className: 'andy-empty-state', children: [jsxRuntime.jsx("div", { className: 'andy-empty-state__icon', children: "\uD83D\uDCAC" }), jsxRuntime.jsx("div", { className: 'andy-empty-state__text', children: "Start a conversation by typing a message below" })] })), jsxRuntime.jsx("div", { ref: messagesEndRef })] }));
|
|
4750
4577
|
};
|
|
4751
4578
|
|
|
4752
4579
|
const MessageInput = ({ onSendMessage, disabled = false, placeholder = 'Type your message here...' }) => {
|
|
@@ -4761,14 +4588,15 @@ const MessageInput = ({ onSendMessage, disabled = false, placeholder = 'Type you
|
|
|
4761
4588
|
}
|
|
4762
4589
|
try {
|
|
4763
4590
|
setIsSubmitting(true);
|
|
4764
|
-
await onSendMessage(trimmedMessage);
|
|
4765
4591
|
setMessage('');
|
|
4592
|
+
await onSendMessage(trimmedMessage);
|
|
4766
4593
|
if (inputRef.current) {
|
|
4767
4594
|
inputRef.current.focus();
|
|
4768
4595
|
}
|
|
4769
4596
|
}
|
|
4770
4597
|
catch (error) {
|
|
4771
4598
|
console.error('Error sending message:', error);
|
|
4599
|
+
setMessage(current => current || trimmedMessage);
|
|
4772
4600
|
}
|
|
4773
4601
|
finally {
|
|
4774
4602
|
setIsSubmitting(false);
|
|
@@ -4785,7 +4613,7 @@ const MessageInput = ({ onSendMessage, disabled = false, placeholder = 'Type you
|
|
|
4785
4613
|
};
|
|
4786
4614
|
const isInputDisabled = disabled || isSubmitting;
|
|
4787
4615
|
const canSend = message.trim().length > 0 && !isInputDisabled;
|
|
4788
|
-
return (jsxRuntime.jsxs("div", { className: 'andy-input', children: [jsxRuntime.jsx("input", { ref: inputRef, type: 'text', value: message, onChange: handleInputChange, onKeyDown: handleKeyDown, placeholder: placeholder, disabled:
|
|
4616
|
+
return (jsxRuntime.jsxs("div", { className: 'andy-input', children: [jsxRuntime.jsx("input", { ref: inputRef, type: 'text', value: message, onChange: handleInputChange, onKeyDown: handleKeyDown, placeholder: placeholder, disabled: disabled, maxLength: 1000, autoComplete: 'off', id: 'andy-input' }), jsxRuntime.jsx("button", { type: 'button', onClick: () => handleSubmit({ preventDefault: () => { } }), disabled: !canSend, id: 'andy-send', children: isSubmitting ? ('...') : (jsxRuntime.jsx("svg", { width: '18', height: '18', viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', children: jsxRuntime.jsx("path", { d: 'M2.01 21L23 12 2.01 3 2 10l15 2-15 2z', fill: 'white' }) })) })] }));
|
|
4789
4617
|
};
|
|
4790
4618
|
|
|
4791
4619
|
const ChatWindow = ({ config, messages, isLoading, onClose, onSendMessage }) => {
|