1
+ import { useState , useRef , useContext , useEffect } from 'react'
1
2
import { useParams } from 'react-router-dom'
2
3
import useCourse from '../../hooks/useCourse'
3
4
import useUserStatus from '../../hooks/useUserStatus'
4
- import { useState } from 'react'
5
5
import useLocalStorageState from '../../hooks/useLocalStorageState'
6
6
import { DEFAULT_MODEL } from '../../../config'
7
7
import useInfoTexts from '../../hooks/useInfoTexts'
@@ -10,27 +10,25 @@ import { FileSearchResult, ResponseStreamEventData } from '../../../shared/types
10
10
import useRetryTimeout from '../../hooks/useRetryTimeout'
11
11
import { useTranslation } from 'react-i18next'
12
12
import { handleCompletionStreamError } from './error'
13
+ import { getCompletionStream } from './util'
13
14
14
- import { Box , Button , IconButton , Typography } from '@mui/material'
15
+ import { Box , Typography } from '@mui/material'
15
16
import SettingsIcon from '@mui/icons-material/Settings'
16
- import AddCommentIcon from '@mui/icons-material/AddComment'
17
17
import EmailIcon from '@mui/icons-material/Email'
18
18
import DeleteIcon from '@mui/icons-material/Delete'
19
19
20
20
import { Disclaimer } from './Disclaimer'
21
21
import { Conversation } from './Conversation'
22
22
import { ChatBox } from './ChatBox'
23
- import { getCompletionStream } from './util'
24
23
import { SystemPrompt } from './System'
25
- import { Settings } from '@mui/icons-material'
26
24
import { SettingsModal } from './SettingsModal'
27
- import { Link } from 'react-router-dom'
28
- // import { useScrollToBottom } from './useScrollToBottom'
25
+
29
26
import { CitationsBox } from './CitationsBox'
30
27
import { useRagIndices } from '../../hooks/useRagIndices'
31
28
import CourseOption from './generics/CourseOption'
32
29
import SettingsButton from './generics/SettingsButton'
33
30
31
+ import { AppContext } from '../../util/context'
34
32
35
33
export const ChatV2 = ( ) => {
36
34
const { courseId } = useParams ( )
@@ -52,7 +50,8 @@ export const ChatV2 = () => {
52
50
const [ settingsModalOpen , setSettingsModalOpen ] = useState ( false )
53
51
const [ activePromptId , setActivePromptId ] = useState ( '' )
54
52
const [ fileName , setFileName ] = useState < string > ( '' )
55
- const [ completion , setCompletion ] = useState ( '' )
53
+ const [ completion , setCompletion ] = useState < string > ( '' )
54
+ const [ isCompletionDone , setIsCompletionDone ] = useState < boolean > ( true )
56
55
const [ fileSearchResult , setFileSearchResult ] = useLocalStorageState < FileSearchResult > ( 'last-file-search' , null )
57
56
const [ streamController , setStreamController ] = useState < AbortController > ( )
58
57
const [ alertOpen , setAlertOpen ] = useState ( false )
@@ -64,6 +63,12 @@ export const ChatV2 = () => {
64
63
const [ ragIndexId , setRagIndexId ] = useState < number | null > ( null )
65
64
const ragIndex = ragIndices ?. find ( ( index ) => index . id === ragIndexId ) ?? null
66
65
66
+ const appContainerRef = useContext ( AppContext )
67
+ const chatContainerRef = useRef < HTMLDivElement > ( null )
68
+ const conversationRef = useRef < HTMLElement > ( null )
69
+ const settingsRef = useRef < HTMLElement > ( null )
70
+ const inputFieldRef = useRef < HTMLElement > ( null )
71
+
67
72
const [ setRetryTimeout , clearRetryTimeout ] = useRetryTimeout ( )
68
73
69
74
const { t, i18n } = useTranslation ( )
@@ -134,8 +139,8 @@ export const ChatV2 = () => {
134
139
} finally {
135
140
setStreamController ( undefined )
136
141
setCompletion ( '' )
142
+ setIsCompletionDone ( true )
137
143
refetchStatus ( )
138
- // inputFileRef.current.value = ''
139
144
setFileName ( '' )
140
145
clearRetryTimeout ( )
141
146
}
@@ -147,6 +152,7 @@ export const ChatV2 = () => {
147
152
setMessage ( { content : '' } )
148
153
setPrevResponse ( { id : '' } )
149
154
setCompletion ( '' )
155
+ setIsCompletionDone ( false )
150
156
setFileSearchResult ( null )
151
157
setStreamController ( new AbortController ( ) )
152
158
setRetryTimeout ( ( ) => {
@@ -200,12 +206,35 @@ export const ChatV2 = () => {
200
206
clearRetryTimeout ( )
201
207
}
202
208
209
+ useEffect ( ( ) => {
210
+ if ( ! appContainerRef . current || ! conversationRef . current || ! settingsRef . current || messages . length === 0 ) return
211
+
212
+ const lastNode = conversationRef . current . lastElementChild as HTMLElement
213
+
214
+ if ( lastNode . classList . contains ( 'message-role-assistant' ) ) {
215
+ const container = appContainerRef . current
216
+ const settingsHeight = settingsRef . current . clientHeight
217
+
218
+ const containerRect = container . getBoundingClientRect ( )
219
+ const lastNodeRect = lastNode . getBoundingClientRect ( )
220
+
221
+ const scrollTopPadding = 100
222
+ const scrollOffset = lastNodeRect . top - containerRect . top + container . scrollTop - settingsHeight - scrollTopPadding
223
+
224
+ container . scrollTo ( {
225
+ top : scrollOffset ,
226
+ behavior : 'smooth' ,
227
+ } )
228
+ }
229
+ } , [ isCompletionDone ] )
230
+
203
231
return (
204
232
< Box
205
233
sx = { {
206
234
display : 'flex' ,
207
235
flexDirection : 'row' ,
208
236
height : '100%' ,
237
+ minWidth : 1400 ,
209
238
} }
210
239
>
211
240
{ /* Course chats columns */ }
@@ -225,14 +254,17 @@ export const ChatV2 = () => {
225
254
226
255
{ /* Chat view */ }
227
256
< Box
257
+ ref = { chatContainerRef }
228
258
sx = { {
229
259
flex : 4 ,
230
260
display : 'flex' ,
231
261
position : 'relative' ,
232
262
flexDirection : 'column' ,
263
+ height : '100%' ,
233
264
} }
234
265
>
235
266
< Box
267
+ ref = { settingsRef }
236
268
sx = { {
237
269
position : 'sticky' ,
238
270
top : 0 ,
@@ -251,22 +283,40 @@ export const ChatV2 = () => {
251
283
<Settings></Settings>
252
284
</IconButton> */ }
253
285
{ /* <SettingsButton startIcon={<AddCommentIcon />}>Alustus</SettingsButton> */ }
254
- < SettingsButton startIcon = { < SettingsIcon /> } onClick = { ( ) => console . log ( 'clicked' ) } >
286
+ < SettingsButton startIcon = { < SettingsIcon /> } onClick = { ( ) => setSettingsModalOpen ( true ) } >
255
287
Keskustelun asetukset
256
288
</ SettingsButton >
257
- < SettingsButton startIcon = { < EmailIcon /> } onClick = { ( ) => console . log ( 'clicked ') } >
289
+ < SettingsButton startIcon = { < EmailIcon /> } onClick = { ( ) => alert ( 'Ei toimi vielä ') } >
258
290
Tallenna sähköpostina
259
291
</ SettingsButton >
260
292
< SettingsButton startIcon = { < DeleteIcon /> } onClick = { handleReset } >
261
293
Tyhjennä
262
294
</ SettingsButton >
263
295
</ Box >
264
296
265
- < Box sx = { { height : '100%' , display : 'flex' , flexDirection : 'column' , gap : 3 , width : '70%' , minWidth : 600 , margin : 'auto' , paddingBottom : '5rem' } } >
266
- < Conversation messages = { messages } completion = { completion } fileSearchResult = { fileSearchResult } />
297
+ < Box
298
+ sx = { {
299
+ height : '100%' ,
300
+ display : 'flex' ,
301
+ flexDirection : 'column' ,
302
+ gap : 3 ,
303
+ width : '70%' ,
304
+ margin : 'auto' ,
305
+ paddingBottom : '5rem' ,
306
+ paddingTop : '1rem' ,
307
+ } }
308
+ >
309
+ < Conversation
310
+ conversationRef = { conversationRef }
311
+ lastNodeHeight = { window . innerHeight - settingsRef . current ?. clientHeight - inputFieldRef . current ?. clientHeight }
312
+ messages = { messages }
313
+ completion = { completion }
314
+ isCompletionDone = { isCompletionDone }
315
+ fileSearchResult = { fileSearchResult }
316
+ />
267
317
</ Box >
268
318
269
- < Box sx = { { position : 'sticky' , bottom : 0 , backgroundColor : 'white' , paddingBottom : '1.5rem' } } >
319
+ < Box ref = { inputFieldRef } sx = { { position : 'sticky' , bottom : 0 , backgroundColor : 'white' , paddingBottom : '1.5rem' } } >
270
320
< ChatBox
271
321
disabled = { false }
272
322
onSubmit = { ( message ) => {
0 commit comments