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