andy-widget-react 2.1.0 → 2.2.0
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/dist/index.cjs +214 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +214 -102
- package/dist/index.esm.js.map +1 -1
- package/dist/types/types/index.d.ts +1 -0
- package/package.json +2 -2
- package/src/AndyChat.tsx +59 -24
- 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 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "andy-widget-react",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Official React Widget for embeddable Andy agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -67,6 +67,6 @@
|
|
|
67
67
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"andy-widget-core": "2.
|
|
70
|
+
"andy-widget-core": "2.2.0"
|
|
71
71
|
}
|
|
72
72
|
}
|
package/src/AndyChat.tsx
CHANGED
|
@@ -95,6 +95,8 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
95
95
|
const [isOpen, setIsOpen] = useState(initialOpen)
|
|
96
96
|
const [messages, setMessages] = useState<Message[]>([])
|
|
97
97
|
const [isLoading, setIsLoading] = useState(false)
|
|
98
|
+
const [replyStarted, setReplyStarted] = useState(false)
|
|
99
|
+
const activeReplyRef = useRef<AbortController | null>(null)
|
|
98
100
|
const [sessionId, setSessionId] = useState(() => generateSessionId())
|
|
99
101
|
const handoffRef = useRef<ReturnType<typeof createBrowserHandoff>>(null)
|
|
100
102
|
const pendingHandoffIdsRef = useRef<string[]>([])
|
|
@@ -118,16 +120,23 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
118
120
|
handoffRef.current?.stop()
|
|
119
121
|
const handoff = createBrowserHandoff(config.id, config.bootstrap, projection => {
|
|
120
122
|
if (projection.offline) return
|
|
123
|
+
const paused = projection.control === 'requested' || projection.control === 'human'
|
|
124
|
+
if (paused) activeReplyRef.current?.abort()
|
|
125
|
+
else if (activeReplyRef.current) return
|
|
121
126
|
setSessionId(projection.identity.sessionId)
|
|
122
127
|
pendingHandoffIdsRef.current = projection.pendingProjectionIds || []
|
|
123
128
|
const projectedMessages = projection.messages.map(message => ({ id: message.id, content: message.content, sender: message.role === 'user' ? 'user' as const : 'bot' as const, timestamp: new Date(message.createdAt).toISOString(), isHuman: message.sentByHuman }))
|
|
129
|
+
// An empty agent projection does not mean the client-only greeting is gone.
|
|
130
|
+
if (projection.control === 'agent' && projectedMessages.length === 0) return
|
|
124
131
|
setMessages(projection.control === 'requested' || projection.control === 'human'
|
|
125
132
|
? [...projectedMessages, { id: 'handoff-waiting', content: strings.handoffWaiting, sender: 'bot', timestamp: new Date().toISOString(), isWaiting: true }]
|
|
126
133
|
: projectedMessages)
|
|
127
134
|
})
|
|
128
135
|
handoffRef.current = handoff
|
|
136
|
+
if (handoff) setSessionId(handoff.identity.sessionId)
|
|
129
137
|
return () => {
|
|
130
|
-
|
|
138
|
+
activeReplyRef.current?.abort()
|
|
139
|
+
handoffRef.current?.stop()
|
|
131
140
|
handoffRef.current = null
|
|
132
141
|
}
|
|
133
142
|
}, [config?.bootstrap, config?.id])
|
|
@@ -485,7 +494,13 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
485
494
|
}, [isCollectingUserInfo, messages])
|
|
486
495
|
|
|
487
496
|
const handleNewChat = () => {
|
|
488
|
-
|
|
497
|
+
activeReplyRef.current?.abort()
|
|
498
|
+
activeReplyRef.current = null
|
|
499
|
+
setIsLoading(false)
|
|
500
|
+
setReplyStarted(false)
|
|
501
|
+
handoffRef.current = handoffRef.current?.restart() || null
|
|
502
|
+
pendingHandoffIdsRef.current = []
|
|
503
|
+
setSessionId(handoffRef.current?.identity.sessionId || generateSessionId())
|
|
489
504
|
|
|
490
505
|
if (config?.welcomeMessage) {
|
|
491
506
|
const greeting = createGreetingMessage(config)
|
|
@@ -511,12 +526,14 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
511
526
|
const sendUserMessage = useCallback(
|
|
512
527
|
async (rawMessageText: string, options: { hidePredefined?: boolean } = {}) => {
|
|
513
528
|
const messageText = typeof rawMessageText === 'string' ? rawMessageText.trim() : ''
|
|
514
|
-
if (!messageText) {
|
|
529
|
+
if (!messageText || activeReplyRef.current) {
|
|
515
530
|
return
|
|
516
531
|
}
|
|
517
532
|
|
|
518
533
|
const { hidePredefined = true } = options
|
|
519
534
|
const timestamp = Date.now()
|
|
535
|
+
const controller = new AbortController()
|
|
536
|
+
activeReplyRef.current = controller
|
|
520
537
|
|
|
521
538
|
const userMessage: Message = {
|
|
522
539
|
id: timestamp,
|
|
@@ -533,19 +550,29 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
533
550
|
|
|
534
551
|
try {
|
|
535
552
|
setIsLoading(true)
|
|
536
|
-
|
|
553
|
+
setReplyStarted(false)
|
|
554
|
+
const showReply = (content: string, isStreaming: boolean) => {
|
|
555
|
+
if (controller.signal.aborted) return
|
|
556
|
+
setReplyStarted(true)
|
|
557
|
+
const reply: Message = { id: timestamp + 1, content, sender: 'bot', timestamp: new Date(timestamp).toISOString(), isStreaming }
|
|
558
|
+
setMessages(prev => prev.some(message => message.id === reply.id)
|
|
559
|
+
? prev.map(message => message.id === reply.id ? reply : message)
|
|
560
|
+
: [...prev, reply])
|
|
561
|
+
}
|
|
562
|
+
const response = await sendMessageToAPI(embedId!, sessionId, messageText, {
|
|
563
|
+
signal: controller.signal,
|
|
564
|
+
onText: text => showReply(text, true)
|
|
565
|
+
})
|
|
566
|
+
if (controller.signal.aborted) return
|
|
537
567
|
if (response.status === 'waiting') {
|
|
538
|
-
setMessages(prev =>
|
|
568
|
+
setMessages(prev => {
|
|
569
|
+
const visible = prev.filter(message => message.id !== timestamp + 1)
|
|
570
|
+
return visible.some(message => message.isWaiting) ? visible : [...visible, { id: 'handoff-waiting', content: strings.handoffWaiting, sender: 'bot', timestamp: new Date().toISOString(), isWaiting: true }]
|
|
571
|
+
})
|
|
539
572
|
return
|
|
540
573
|
}
|
|
541
574
|
|
|
542
|
-
|
|
543
|
-
id: timestamp + 1,
|
|
544
|
-
content: response.text,
|
|
545
|
-
sender: 'bot',
|
|
546
|
-
timestamp: new Date().toISOString()
|
|
547
|
-
}
|
|
548
|
-
setMessages(prev => [...prev, botMessage])
|
|
575
|
+
showReply(response.text, false)
|
|
549
576
|
|
|
550
577
|
if (config?.collectUserInfo?.enabled && !userInfoCollected && !isCollectingUserInfo) {
|
|
551
578
|
setPendingMessage(messageText)
|
|
@@ -561,6 +588,7 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
561
588
|
setMessages(prev => [...prev, formMessage])
|
|
562
589
|
}
|
|
563
590
|
} catch {
|
|
591
|
+
if (controller.signal.aborted) return
|
|
564
592
|
console.error('AndyChat: Message request failed.')
|
|
565
593
|
const errorMessage: Message = {
|
|
566
594
|
id: timestamp + 3,
|
|
@@ -569,9 +597,13 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
569
597
|
timestamp: new Date().toISOString(),
|
|
570
598
|
isError: true
|
|
571
599
|
}
|
|
572
|
-
setMessages(prev => [...prev, errorMessage])
|
|
600
|
+
setMessages(prev => [...prev.filter(message => message.id !== timestamp + 1), errorMessage])
|
|
573
601
|
} finally {
|
|
574
|
-
|
|
602
|
+
if (activeReplyRef.current === controller) {
|
|
603
|
+
activeReplyRef.current = null
|
|
604
|
+
setIsLoading(false)
|
|
605
|
+
setReplyStarted(false)
|
|
606
|
+
}
|
|
575
607
|
}
|
|
576
608
|
},
|
|
577
609
|
[config, embedId, sessionId, userInfoCollected, isCollectingUserInfo]
|
|
@@ -696,7 +728,7 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
696
728
|
bubble = document.createElement('button')
|
|
697
729
|
bubble.type = 'button'
|
|
698
730
|
bubble.className = 'andy-chat-bubble'
|
|
699
|
-
bubble.setAttribute('aria-label',
|
|
731
|
+
bubble.setAttribute('aria-label', strings.openChat)
|
|
700
732
|
|
|
701
733
|
const tooltipHTML = tooltip
|
|
702
734
|
? `<span class="andy-chat-bubble__tooltip" role="tooltip">${tooltip}</span>`
|
|
@@ -713,6 +745,7 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
713
745
|
const bubbleLogoUrl = config?.logoUrl || CHAT_BUBBLE_DEFAULT_ICON_URL
|
|
714
746
|
|
|
715
747
|
if (bubble) {
|
|
748
|
+
bubble.setAttribute('aria-label', isOpen ? strings.closeChat : strings.openChat)
|
|
716
749
|
bubble.className = 'andy-chat-bubble'
|
|
717
750
|
if (isPillStyle) {
|
|
718
751
|
bubble.classList.add('andy-chat-bubble--pill')
|
|
@@ -775,7 +808,7 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
775
808
|
type='button'
|
|
776
809
|
className='andy-header__back'
|
|
777
810
|
id='andy-back-button'
|
|
778
|
-
aria-label=
|
|
811
|
+
aria-label={strings.goBack}
|
|
779
812
|
onClick={() => setIsOpen(false)}
|
|
780
813
|
>
|
|
781
814
|
<svg
|
|
@@ -821,7 +854,7 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
821
854
|
type='button'
|
|
822
855
|
className='andy-header__close'
|
|
823
856
|
id='andy-close-button'
|
|
824
|
-
aria-label=
|
|
857
|
+
aria-label={strings.closeChat}
|
|
825
858
|
onClick={() => setIsOpen(false)}
|
|
826
859
|
>
|
|
827
860
|
<svg
|
|
@@ -960,7 +993,7 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
960
993
|
</span>
|
|
961
994
|
</div>
|
|
962
995
|
<div className='andy-message__content'>
|
|
963
|
-
<div dangerouslySetInnerHTML={{ __html: parseMarkdown(message.content) }} />
|
|
996
|
+
{message.isStreaming ? <div className='andy-message__stream'>{message.content}</div> : <div dangerouslySetInnerHTML={{ __html: parseMarkdown(message.content) }} />}
|
|
964
997
|
</div>
|
|
965
998
|
</div>
|
|
966
999
|
)}
|
|
@@ -975,7 +1008,7 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
975
1008
|
)
|
|
976
1009
|
})}
|
|
977
1010
|
|
|
978
|
-
{isLoading && (
|
|
1011
|
+
{isLoading && !replyStarted && (
|
|
979
1012
|
<div className='andy-message-container andy-message-container--bot andy-message-container--typing'>
|
|
980
1013
|
<div className='andy-message andy-message--bot andy-message--typing'>
|
|
981
1014
|
<div className='andy-typing-indicator'>
|
|
@@ -1021,8 +1054,9 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
1021
1054
|
className='andy-input__field'
|
|
1022
1055
|
id='andy-message-input'
|
|
1023
1056
|
placeholder={config ? strings.inputPlaceholder : '...'}
|
|
1024
|
-
aria-label=
|
|
1057
|
+
aria-label={strings.messageInput}
|
|
1025
1058
|
onKeyDown={e => {
|
|
1059
|
+
if (e.key === 'Enter' && activeReplyRef.current) { e.preventDefault(); return }
|
|
1026
1060
|
if (e.key === 'Enter' && (e.target as HTMLInputElement).value.trim()) {
|
|
1027
1061
|
handleSendMessage((e.target as HTMLInputElement).value)
|
|
1028
1062
|
;(e.target as HTMLInputElement).value = ''
|
|
@@ -1034,8 +1068,9 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
1034
1068
|
type='button'
|
|
1035
1069
|
className='andy-input__button'
|
|
1036
1070
|
id='andy-send-button'
|
|
1037
|
-
aria-label=
|
|
1071
|
+
aria-label={strings.sendMessage}
|
|
1038
1072
|
onClick={() => {
|
|
1073
|
+
if (activeReplyRef.current) return
|
|
1039
1074
|
const input = shadowRef.current?.getElementById(
|
|
1040
1075
|
'andy-message-input'
|
|
1041
1076
|
) as HTMLInputElement | null
|
|
@@ -1044,7 +1079,7 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
1044
1079
|
input.value = ''
|
|
1045
1080
|
}
|
|
1046
1081
|
}}
|
|
1047
|
-
disabled={isCollectingUserInfo || !config}
|
|
1082
|
+
disabled={isCollectingUserInfo || isLoading || !config}
|
|
1048
1083
|
>
|
|
1049
1084
|
<svg
|
|
1050
1085
|
className='andy-input__button-icon'
|
|
@@ -1084,7 +1119,7 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
1084
1119
|
className={`andy-welcome-popup ${config?.orientation === 'left' ? 'andychat-popup-left' : ''}`}
|
|
1085
1120
|
style={{ display: 'block' }}
|
|
1086
1121
|
role='dialog'
|
|
1087
|
-
aria-label=
|
|
1122
|
+
aria-label={strings.welcomeMessage}
|
|
1088
1123
|
>
|
|
1089
1124
|
<div className='andy-welcome-popup__content'>
|
|
1090
1125
|
<div className='andy-welcome-popup__message'>
|
|
@@ -1095,7 +1130,7 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
1095
1130
|
<button
|
|
1096
1131
|
type='button'
|
|
1097
1132
|
className='andy-welcome-popup__close'
|
|
1098
|
-
aria-label=
|
|
1133
|
+
aria-label={strings.closeWelcomeMessage}
|
|
1099
1134
|
onClick={() => setShowWelcomePopup(false)}
|
|
1100
1135
|
>
|
|
1101
1136
|
<svg
|
|
@@ -25,14 +25,15 @@ const MessageInput: React.FC<MessageInputProps> = ({
|
|
|
25
25
|
|
|
26
26
|
try {
|
|
27
27
|
setIsSubmitting(true)
|
|
28
|
-
await onSendMessage(trimmedMessage)
|
|
29
28
|
setMessage('')
|
|
29
|
+
await onSendMessage(trimmedMessage)
|
|
30
30
|
|
|
31
31
|
if (inputRef.current) {
|
|
32
32
|
inputRef.current.focus()
|
|
33
33
|
}
|
|
34
34
|
} catch (error) {
|
|
35
35
|
console.error('Error sending message:', error)
|
|
36
|
+
setMessage(current => current || trimmedMessage)
|
|
36
37
|
} finally {
|
|
37
38
|
setIsSubmitting(false)
|
|
38
39
|
}
|
|
@@ -61,7 +62,7 @@ const MessageInput: React.FC<MessageInputProps> = ({
|
|
|
61
62
|
onChange={handleInputChange}
|
|
62
63
|
onKeyDown={handleKeyDown}
|
|
63
64
|
placeholder={placeholder}
|
|
64
|
-
disabled={
|
|
65
|
+
disabled={disabled}
|
|
65
66
|
maxLength={1000}
|
|
66
67
|
autoComplete='off'
|
|
67
68
|
id='andy-input'
|
|
@@ -17,7 +17,7 @@ const MessageComponent: React.FC<MessageProps> = ({ message, botName }) => {
|
|
|
17
17
|
|
|
18
18
|
const messageClasses = ['andy-message', `andy-message--${message.sender}`].filter(Boolean).join(' ')
|
|
19
19
|
|
|
20
|
-
const messageContent = isBot ? parseMarkdown(message.content) : message.content
|
|
20
|
+
const messageContent = isBot && !message.isStreaming ? parseMarkdown(message.content) : message.content
|
|
21
21
|
|
|
22
22
|
if (isBot) {
|
|
23
23
|
return (
|
|
@@ -46,7 +46,9 @@ const MessageComponent: React.FC<MessageProps> = ({ message, botName }) => {
|
|
|
46
46
|
<span className='andy-bot-header__name'>{botName || 'Andy bot'}</span>
|
|
47
47
|
</div>
|
|
48
48
|
<div className={messageClasses}>
|
|
49
|
-
|
|
49
|
+
{message.isStreaming
|
|
50
|
+
? <div className='andy-message__content andy-message__stream'>{message.content}</div>
|
|
51
|
+
: <div className='andy-message__content' dangerouslySetInnerHTML={{ __html: messageContent }} />}
|
|
50
52
|
</div>
|
|
51
53
|
</div>
|
|
52
54
|
)
|
|
@@ -105,7 +107,7 @@ const MessageList: React.FC<MessageListProps> = ({ messages, isLoading, logoUrl,
|
|
|
105
107
|
/>
|
|
106
108
|
))}
|
|
107
109
|
|
|
108
|
-
{isLoading && <TypingIndicator />}
|
|
110
|
+
{isLoading && !messages.some(message => message.isStreaming) && <TypingIndicator />}
|
|
109
111
|
|
|
110
112
|
{messages.length === 0 && !isLoading && (
|
|
111
113
|
<div className='andy-empty-state'>
|
package/src/hooks/useAndyChat.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useCallback, useRef } from 'react'
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
fetchChatbotConfig,
|
|
4
4
|
generateSessionId,
|
|
@@ -31,6 +31,12 @@ export const useAndyChat = (embedId: string): UseAndyChatReturn => {
|
|
|
31
31
|
const [showingUserInfoForm, setShowingUserInfoForm] = useState(false)
|
|
32
32
|
const [welcomePopupShown, setWelcomePopupShown] = useState(false)
|
|
33
33
|
|
|
34
|
+
const activeReplyRef = useRef<AbortController | null>(null)
|
|
35
|
+
useEffect(() => () => {
|
|
36
|
+
activeReplyRef.current?.abort()
|
|
37
|
+
activeReplyRef.current = null
|
|
38
|
+
}, [embedId])
|
|
39
|
+
|
|
34
40
|
const welcomeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
35
41
|
const userInfoTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
36
42
|
|
|
@@ -127,10 +133,12 @@ export const useAndyChat = (embedId: string): UseAndyChatReturn => {
|
|
|
127
133
|
|
|
128
134
|
const sendMessage = useCallback(
|
|
129
135
|
async (messageText: string) => {
|
|
130
|
-
if (!messageText.trim() || !config || !sessionId) {
|
|
136
|
+
if (!messageText.trim() || !config || !sessionId || activeReplyRef.current) {
|
|
131
137
|
return
|
|
132
138
|
}
|
|
133
139
|
|
|
140
|
+
const controller = new AbortController()
|
|
141
|
+
activeReplyRef.current = controller
|
|
134
142
|
const userMessage: Message = {
|
|
135
143
|
id: Date.now(),
|
|
136
144
|
content: messageText,
|
|
@@ -140,21 +148,26 @@ export const useAndyChat = (embedId: string): UseAndyChatReturn => {
|
|
|
140
148
|
|
|
141
149
|
setMessages(prev => [...prev, userMessage])
|
|
142
150
|
|
|
151
|
+
const replyId = Date.now() + 1
|
|
152
|
+
const showReply = (content: string, isStreaming: boolean) => {
|
|
153
|
+
if (controller.signal.aborted) return
|
|
154
|
+
const reply: Message = { id: replyId, content, sender: 'bot', timestamp: new Date().toISOString(), isStreaming }
|
|
155
|
+
setMessages(prev => prev.some(message => message.id === replyId)
|
|
156
|
+
? prev.map(message => message.id === replyId ? reply : message)
|
|
157
|
+
: [...prev, reply])
|
|
158
|
+
}
|
|
143
159
|
try {
|
|
144
|
-
const response = await sendMessageToAPI(embedId, sessionId, messageText)
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
content: response.text || 'I apologize, but I encountered an error processing your request.',
|
|
150
|
-
sender: 'bot',
|
|
151
|
-
timestamp: new Date().toISOString()
|
|
160
|
+
const response = await sendMessageToAPI(embedId, sessionId, messageText, { signal: controller.signal, onText: text => showReply(text, true) })
|
|
161
|
+
if (controller.signal.aborted) return
|
|
162
|
+
if (response.status === 'waiting') {
|
|
163
|
+
setMessages(prev => prev.filter(message => message.id !== replyId))
|
|
164
|
+
return response
|
|
152
165
|
}
|
|
153
|
-
|
|
154
|
-
setMessages(prev => [...prev, botMessage])
|
|
166
|
+
showReply(response.text, false)
|
|
155
167
|
|
|
156
168
|
return response
|
|
157
169
|
} catch (err) {
|
|
170
|
+
if (controller.signal.aborted) return
|
|
158
171
|
console.error('Error sending message:', err)
|
|
159
172
|
|
|
160
173
|
const errorMessage: Message = {
|
|
@@ -165,8 +178,10 @@ export const useAndyChat = (embedId: string): UseAndyChatReturn => {
|
|
|
165
178
|
isError: true
|
|
166
179
|
}
|
|
167
180
|
|
|
168
|
-
setMessages(prev => [...prev, errorMessage])
|
|
181
|
+
setMessages(prev => [...prev.filter(message => message.id !== replyId), errorMessage])
|
|
169
182
|
throw err
|
|
183
|
+
} finally {
|
|
184
|
+
if (activeReplyRef.current === controller) activeReplyRef.current = null
|
|
170
185
|
}
|
|
171
186
|
},
|
|
172
187
|
[embedId, sessionId, config]
|
|
@@ -220,6 +235,8 @@ export const useAndyChat = (embedId: string): UseAndyChatReturn => {
|
|
|
220
235
|
)
|
|
221
236
|
|
|
222
237
|
const startNewSession = useCallback(() => {
|
|
238
|
+
activeReplyRef.current?.abort()
|
|
239
|
+
activeReplyRef.current = null
|
|
223
240
|
const newSessionId = generateSessionId()
|
|
224
241
|
setSessionId(newSessionId)
|
|
225
242
|
|