@@ -7,7 +7,7 @@ import { useContext, useEffect, useRef, useState } from 'react'
7
7
import { useTranslation } from 'react-i18next'
8
8
import { useParams } from 'react-router-dom'
9
9
import { ALLOWED_FILE_TYPES , DEFAULT_ASSISTANT_INSTRUCTIONS , DEFAULT_MODEL , DEFAULT_MODEL_TEMPERATURE , FREE_MODEL , validModels } from '../../../config'
10
- import type { FileSearchCompletedData , ResponseStreamEventData } from '../../../shared/types'
10
+ import type { FileSearchCompletedData } from '../../../shared/types'
11
11
import { getLanguageValue } from '../../../shared/utils'
12
12
import useCourse from '../../hooks/useCourse'
13
13
import useInfoTexts from '../../hooks/useInfoTexts'
@@ -18,7 +18,6 @@ import useUserStatus from '../../hooks/useUserStatus'
18
18
import type { Message } from '../../types'
19
19
import { AppContext } from '../../util/AppContext'
20
20
import { ChatBox } from './ChatBox'
21
- import { FileSearchInfo } from './CitationsBox'
22
21
import { Conversation } from './Conversation'
23
22
import { DisclaimerModal } from './Disclaimer'
24
23
import { handleCompletionStreamError } from './error'
@@ -28,16 +27,16 @@ import { SettingsModal } from './SettingsModal'
28
27
import { getCompletionStream } from './util'
29
28
import { OutlineButtonBlack } from './generics/Buttons'
30
29
import useCurrentUser from '../../hooks/useCurrentUser'
30
+ import { useChatStream } from './useChatStream'
31
31
import Annotations from './Annotations'
32
- import { enqueueSnackbar } from 'notistack'
33
32
34
33
export const ChatV2 = ( ) => {
35
34
const { courseId } = useParams ( )
36
35
37
36
const { data : course } = useCourse ( courseId )
38
37
39
38
const { ragIndices } = useRagIndices ( courseId )
40
- const { infoTexts, isLoading : infoTextsLoading } = useInfoTexts ( )
39
+ const { infoTexts } = useInfoTexts ( )
41
40
42
41
const { userStatus, isLoading : statusLoading , refetch : refetchStatus } = useUserStatus ( courseId )
43
42
@@ -65,19 +64,13 @@ export const ChatV2 = () => {
65
64
const [ fileSearch , setFileSearch ] = useLocalStorageState < FileSearchCompletedData > ( `${ localStoragePrefix } -last-file-search` )
66
65
67
66
// App States
68
- const [ isFileSearching , setIsFileSearching ] = useState < boolean > ( false )
69
67
const [ settingsModalOpen , setSettingsModalOpen ] = useState < boolean > ( false )
70
68
const [ fileName , setFileName ] = useState < string > ( '' )
71
69
const [ tokenUsageWarning , setTokenUsageWarning ] = useState < string > ( '' )
72
70
const [ tokenUsageAlertOpen , setTokenUsageAlertOpen ] = useState < boolean > ( false )
73
71
const [ allowedModels , setAllowedModels ] = useState < string [ ] > ( [ ] )
74
72
const [ saveConsent , setSaveConsent ] = useState < boolean > ( false )
75
73
76
- // Chat Streaming states
77
- const [ completion , setCompletion ] = useState < string > ( '' )
78
- const [ isCompletionDone , setIsCompletionDone ] = useState < boolean > ( true )
79
- const [ streamController , setStreamController ] = useState < AbortController > ( )
80
-
81
74
// RAG states
82
75
const [ ragIndexId , setRagIndexId ] = useState < number | undefined > ( )
83
76
const [ ragDisplay , setRagDisplay ] = useState < boolean > ( true )
@@ -93,106 +86,27 @@ export const ChatV2 = () => {
93
86
94
87
const [ setRetryTimeout , clearRetryTimeout ] = useRetryTimeout ( )
95
88
96
- const decoder = new TextDecoder ( )
97
89
const { t, i18n } = useTranslation ( )
98
- const { language } = i18n
99
-
100
- const disclaimerInfo = infoTexts ?. find ( ( infoText ) => infoText . name === 'disclaimer' ) ?. text [ language ] ?? null
101
90
102
- const processStream = async ( stream : ReadableStream ) => {
103
- let content = ''
104
- let error = ''
105
- let fileSearch : FileSearchCompletedData
91
+ const disclaimerInfo = infoTexts ?. find ( ( infoText ) => infoText . name === 'disclaimer' ) ?. text [ i18n . language ] ?? null
106
92
107
- try {
108
- const reader = stream . getReader ( )
109
-
110
- while ( true ) {
111
- const { value, done } = await reader . read ( )
112
- if ( done ) break
113
-
114
- const data = decoder . decode ( value )
115
-
116
- let accumulatedChunk = ''
117
- for ( const chunk of data . split ( '\n' ) ) {
118
- if ( ! chunk || chunk . trim ( ) . length === 0 ) continue
119
-
120
- let parsedChunk : ResponseStreamEventData | undefined
121
- try {
122
- parsedChunk = JSON . parse ( chunk )
123
- } catch ( e : any ) {
124
- console . error ( 'Error' , e )
125
- console . error ( 'Could not parse the chunk:' , chunk )
126
- accumulatedChunk += chunk
127
-
128
- try {
129
- parsedChunk = JSON . parse ( accumulatedChunk )
130
- accumulatedChunk = ''
131
- } catch ( e : any ) {
132
- console . error ( 'Error' , e )
133
- console . error ( 'Could not parse the accumulated chunk:' , accumulatedChunk )
134
- }
135
- }
136
-
137
- if ( ! parsedChunk ) continue
138
-
139
- switch ( parsedChunk . type ) {
140
- case 'writing' :
141
- setCompletion ( ( prev ) => prev + parsedChunk . text )
142
- content += parsedChunk . text
143
- break
144
-
145
- case 'annotation' :
146
- console . log ( 'Received annotation:' , parsedChunk . annotation )
147
- break
148
-
149
- case 'fileSearchStarted' :
150
- setIsFileSearching ( true )
151
- break
152
-
153
- case 'fileSearchDone' :
154
- fileSearch = parsedChunk . fileSearch
155
- setFileSearch ( parsedChunk . fileSearch )
156
- setIsFileSearching ( false )
157
- break
158
-
159
- case 'error' :
160
- error += parsedChunk . error
161
- break
162
-
163
- case 'complete' :
164
- setPrevResponse ( { id : parsedChunk . prevResponseId } )
165
- break
166
-
167
- default :
168
- break
169
- }
170
- }
93
+ const { processStream, completion, isStreaming, isFileSearching, streamController } = useChatStream ( {
94
+ onComplete : ( { message, previousResponseId } ) => {
95
+ if ( previousResponseId ) {
96
+ setPrevResponse ( { id : previousResponseId } )
171
97
}
172
- } catch ( err : any ) {
173
- handleCompletionStreamError ( err , fileName )
174
- error += '\nResponse stream was interrupted'
175
- } finally {
176
- if ( content . length > 0 ) {
177
- setMessages ( ( prev : Message [ ] ) =>
178
- prev . concat ( {
179
- role : 'assistant' ,
180
- content,
181
- error : error . length > 0 ? error : undefined ,
182
- fileSearchResult : fileSearch ,
183
- } ) ,
184
- )
98
+ if ( message . content . length > 0 ) {
99
+ setMessages ( ( prev : Message [ ] ) => prev . concat ( message ) )
100
+ refetchStatus ( )
185
101
}
186
-
187
- setStreamController ( undefined )
188
- setCompletion ( '' )
189
- setIsCompletionDone ( true )
190
- refetchStatus ( )
191
- setFileName ( '' )
192
- setIsFileSearching ( false )
193
- clearRetryTimeout ( )
194
- }
195
- }
102
+ } ,
103
+ onError : ( error ) => {
104
+ handleCompletionStreamError ( error , fileName )
105
+ } ,
106
+ onFileSearchComplete : ( fileSearch ) => {
107
+ setFileSearch ( fileSearch )
108
+ } ,
109
+ } )
196
110
197
111
const handleSubmit = async ( message : string , ignoreTokenUsageWarning : boolean ) => {
198
112
const formData = new FormData ( )
@@ -215,15 +129,11 @@ export const ChatV2 = () => {
215
129
216
130
setMessages ( newMessages )
217
131
setPrevResponse ( { id : '' } )
218
- setCompletion ( '' )
219
- setIsCompletionDone ( false )
220
132
if ( fileInputRef . current ) {
221
133
fileInputRef . current . value = ''
222
134
}
223
135
setFileName ( '' )
224
136
setFileSearch ( undefined )
225
- setIsFileSearching ( false )
226
- setStreamController ( new AbortController ( ) )
227
137
setRetryTimeout ( ( ) => {
228
138
if ( streamController ) {
229
139
streamController . abort ( )
@@ -277,8 +187,6 @@ export const ChatV2 = () => {
277
187
if ( window . confirm ( 'Are you sure you want to empty this conversation?' ) ) {
278
188
setMessages ( [ ] )
279
189
setPrevResponse ( { id : '' } )
280
- setCompletion ( '' )
281
- setIsCompletionDone ( true )
282
190
if ( ! ragDisplay ) {
283
191
handleRagDisplay ( )
284
192
}
@@ -287,7 +195,6 @@ export const ChatV2 = () => {
287
195
}
288
196
setFileName ( '' )
289
197
setFileSearch ( undefined )
290
- setStreamController ( undefined )
291
198
setTokenUsageWarning ( '' )
292
199
setTokenUsageAlertOpen ( false )
293
200
setRetryTimeout ( ( ) => {
@@ -300,8 +207,6 @@ export const ChatV2 = () => {
300
207
}
301
208
302
209
const handleCancel = ( ) => {
303
- setIsCompletionDone ( true )
304
- setStreamController ( undefined )
305
210
setTokenUsageWarning ( '' )
306
211
setTokenUsageAlertOpen ( false )
307
212
clearRetryTimeout ( )
@@ -310,7 +215,7 @@ export const ChatV2 = () => {
310
215
useEffect ( ( ) => {
311
216
// Scrolls to bottom on initial load only
312
217
if ( ! appContainerRef ?. current || ! conversationRef . current || messages . length === 0 ) return
313
- if ( isCompletionDone ) {
218
+ if ( ! isStreaming ) {
314
219
const container = appContainerRef ?. current
315
220
if ( container ) {
316
221
container . scrollTo ( {
@@ -327,7 +232,7 @@ export const ChatV2 = () => {
327
232
328
233
const lastNode = conversationRef . current . lastElementChild as HTMLElement
329
234
330
- if ( lastNode . classList . contains ( 'message-role-assistant' ) && ! isCompletionDone ) {
235
+ if ( lastNode . classList . contains ( 'message-role-assistant' ) && isStreaming ) {
331
236
const container = appContainerRef . current
332
237
333
238
const containerRect = container . getBoundingClientRect ( )
@@ -341,7 +246,7 @@ export const ChatV2 = () => {
341
246
behavior : 'smooth' ,
342
247
} )
343
248
}
344
- } , [ isCompletionDone ] )
249
+ } , [ isStreaming ] )
345
250
346
251
useEffect ( ( ) => {
347
252
if ( ! userStatus ) return
@@ -369,7 +274,7 @@ export const ChatV2 = () => {
369
274
}
370
275
} , [ userStatus , course ] )
371
276
372
- if ( statusLoading || infoTextsLoading ) return null
277
+ if ( statusLoading ) return null
373
278
374
279
if ( course && course . usageLimit === 0 ) {
375
280
return (
@@ -499,13 +404,13 @@ export const ChatV2 = () => {
499
404
>
500
405
< Alert severity = "info" > { t ( 'chat:testUseInfo' ) } </ Alert >
501
406
< Conversation
502
- courseName = { course && getLanguageValue ( course . name , language ) }
407
+ courseName = { course && getLanguageValue ( course . name , i18n . language ) }
503
408
courseDate = { course ?. activityPeriod }
504
409
conversationRef = { conversationRef }
505
410
expandedNodeHeight = { window . innerHeight - ( inputFieldRef . current ?. clientHeight ?? 0 ) - 300 }
506
411
messages = { messages }
507
412
completion = { completion }
508
- isCompletionDone = { isCompletionDone }
413
+ isCompletionDone = { ! isStreaming }
509
414
setActiveFileSearchResult = { setActiveFileSearchResult }
510
415
/>
511
416
</ Box >
@@ -523,7 +428,7 @@ export const ChatV2 = () => {
523
428
} }
524
429
>
525
430
< ChatBox
526
- disabled = { ! isCompletionDone }
431
+ disabled = { isStreaming }
527
432
currentModel = { activeModel . name }
528
433
availableModels = { allowedModels }
529
434
fileInputRef = { fileInputRef }
@@ -545,8 +450,6 @@ export const ChatV2 = () => {
545
450
546
451
{ /* Annotations columns ----------------------------------------------------------------------------------------------------- */ }
547
452
548
-
549
- { /* LEGACY - keep here for legacy when new annotations flow is work in progres */ }
550
453
{ /* {showFileSearch && (
551
454
<FileSearchInfo
552
455
isFileSearching={isFileSearching}
@@ -568,9 +471,7 @@ export const ChatV2 = () => {
568
471
borderLeft : activeFileSearchResult ? '1px solid rgba(0, 0, 0, 0.12)' : 'none' ,
569
472
} }
570
473
>
571
- < Box sx = { { position : 'sticky' , top : 65 , padding : '2rem' } } >
572
- { activeFileSearchResult && < Annotations fileSearchResult = { activeFileSearchResult } /> }
573
- </ Box >
474
+ < Box sx = { { position : 'sticky' , top : 65 , padding : '2rem' } } > { activeFileSearchResult && < Annotations fileSearchResult = { activeFileSearchResult } /> } </ Box >
574
475
</ Box >
575
476
576
477
{ /* Modals --------------------------------------*/ }
0 commit comments