Skip to content

Commit 6c94ce9

Browse files
committed
finnish autoscroll
1 parent 69a24e8 commit 6c94ce9

File tree

3 files changed

+133
-47
lines changed

3 files changed

+133
-47
lines changed

src/client/components/ChatV2/ChatV2.tsx

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import { useState, useRef, useContext, useEffect } from 'react'
12
import { useParams } from 'react-router-dom'
23
import useCourse from '../../hooks/useCourse'
34
import useUserStatus from '../../hooks/useUserStatus'
4-
import { useState } from 'react'
55
import useLocalStorageState from '../../hooks/useLocalStorageState'
66
import { DEFAULT_MODEL } from '../../../config'
77
import useInfoTexts from '../../hooks/useInfoTexts'
@@ -10,27 +10,25 @@ import { FileSearchResult, ResponseStreamEventData } from '../../../shared/types
1010
import useRetryTimeout from '../../hooks/useRetryTimeout'
1111
import { useTranslation } from 'react-i18next'
1212
import { handleCompletionStreamError } from './error'
13+
import { getCompletionStream } from './util'
1314

14-
import { Box, Button, IconButton, Typography } from '@mui/material'
15+
import { Box, Typography } from '@mui/material'
1516
import SettingsIcon from '@mui/icons-material/Settings'
16-
import AddCommentIcon from '@mui/icons-material/AddComment'
1717
import EmailIcon from '@mui/icons-material/Email'
1818
import DeleteIcon from '@mui/icons-material/Delete'
1919

2020
import { Disclaimer } from './Disclaimer'
2121
import { Conversation } from './Conversation'
2222
import { ChatBox } from './ChatBox'
23-
import { getCompletionStream } from './util'
2423
import { SystemPrompt } from './System'
25-
import { Settings } from '@mui/icons-material'
2624
import { SettingsModal } from './SettingsModal'
27-
import { Link } from 'react-router-dom'
28-
// import { useScrollToBottom } from './useScrollToBottom'
25+
2926
import { CitationsBox } from './CitationsBox'
3027
import { useRagIndices } from '../../hooks/useRagIndices'
3128
import CourseOption from './generics/CourseOption'
3229
import SettingsButton from './generics/SettingsButton'
3330

31+
import { AppContext } from '../../util/context'
3432

3533
export const ChatV2 = () => {
3634
const { courseId } = useParams()
@@ -52,7 +50,8 @@ export const ChatV2 = () => {
5250
const [settingsModalOpen, setSettingsModalOpen] = useState(false)
5351
const [activePromptId, setActivePromptId] = useState('')
5452
const [fileName, setFileName] = useState<string>('')
55-
const [completion, setCompletion] = useState('')
53+
const [completion, setCompletion] = useState<string>('')
54+
const [isCompletionDone, setIsCompletionDone] = useState<boolean>(true)
5655
const [fileSearchResult, setFileSearchResult] = useLocalStorageState<FileSearchResult>('last-file-search', null)
5756
const [streamController, setStreamController] = useState<AbortController>()
5857
const [alertOpen, setAlertOpen] = useState(false)
@@ -64,6 +63,12 @@ export const ChatV2 = () => {
6463
const [ragIndexId, setRagIndexId] = useState<number | null>(null)
6564
const ragIndex = ragIndices?.find((index) => index.id === ragIndexId) ?? null
6665

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+
6772
const [setRetryTimeout, clearRetryTimeout] = useRetryTimeout()
6873

6974
const { t, i18n } = useTranslation()
@@ -134,8 +139,8 @@ export const ChatV2 = () => {
134139
} finally {
135140
setStreamController(undefined)
136141
setCompletion('')
142+
setIsCompletionDone(true)
137143
refetchStatus()
138-
// inputFileRef.current.value = ''
139144
setFileName('')
140145
clearRetryTimeout()
141146
}
@@ -147,6 +152,7 @@ export const ChatV2 = () => {
147152
setMessage({ content: '' })
148153
setPrevResponse({ id: '' })
149154
setCompletion('')
155+
setIsCompletionDone(false)
150156
setFileSearchResult(null)
151157
setStreamController(new AbortController())
152158
setRetryTimeout(() => {
@@ -200,12 +206,35 @@ export const ChatV2 = () => {
200206
clearRetryTimeout()
201207
}
202208

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+
203231
return (
204232
<Box
205233
sx={{
206234
display: 'flex',
207235
flexDirection: 'row',
208236
height: '100%',
237+
minWidth: 1400,
209238
}}
210239
>
211240
{/* Course chats columns */}
@@ -225,14 +254,17 @@ export const ChatV2 = () => {
225254

226255
{/* Chat view */}
227256
<Box
257+
ref={chatContainerRef}
228258
sx={{
229259
flex: 4,
230260
display: 'flex',
231261
position: 'relative',
232262
flexDirection: 'column',
263+
height: '100%',
233264
}}
234265
>
235266
<Box
267+
ref={settingsRef}
236268
sx={{
237269
position: 'sticky',
238270
top: 0,
@@ -251,22 +283,40 @@ export const ChatV2 = () => {
251283
<Settings></Settings>
252284
</IconButton> */}
253285
{/* <SettingsButton startIcon={<AddCommentIcon />}>Alustus</SettingsButton> */}
254-
<SettingsButton startIcon={<SettingsIcon />} onClick={() => console.log('clicked')}>
286+
<SettingsButton startIcon={<SettingsIcon />} onClick={() => setSettingsModalOpen(true)}>
255287
Keskustelun asetukset
256288
</SettingsButton>
257-
<SettingsButton startIcon={<EmailIcon />} onClick={() => console.log('clicked')}>
289+
<SettingsButton startIcon={<EmailIcon />} onClick={() => alert('Ei toimi vielä')}>
258290
Tallenna sähköpostina
259291
</SettingsButton>
260292
<SettingsButton startIcon={<DeleteIcon />} onClick={handleReset}>
261293
Tyhjennä
262294
</SettingsButton>
263295
</Box>
264296

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+
/>
267317
</Box>
268318

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' }}>
270320
<ChatBox
271321
disabled={false}
272322
onSubmit={(message) => {

src/client/components/ChatV2/Conversation.tsx

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,52 @@ import remarkGfm from 'remark-gfm'
55
import { FileSearchResult } from '../../../shared/types'
66
import { ConversationSplash } from './generics/ConversationSplash'
77

8-
const MessageItem = ({ message }: { message: Message }) => (
8+
const dotStyle = (delay: number) => ({
9+
width: 4,
10+
height: 4,
11+
margin: '0 4px',
12+
borderRadius: '50%',
13+
backgroundColor: '#666',
14+
animation: 'bounceWave 1.2s infinite',
15+
animationDelay: `${delay}s`,
16+
})
17+
18+
export const MessageLoading = ({ lastNodeHeight }: { lastNodeHeight: number }) => (
19+
<div
20+
className="message-role-assistant"
21+
style={{
22+
height: lastNodeHeight,
23+
display: 'flex',
24+
padding: '2rem',
25+
}}
26+
>
27+
<style>
28+
{`
29+
@keyframes bounceWave {
30+
0%, 80%, 100% {
31+
transform: translateY(0);
32+
}
33+
40% {
34+
transform: translateY(-8px);
35+
}
36+
}
37+
`}
38+
</style>
39+
<div style={dotStyle(0)} />
40+
<div style={dotStyle(0.15)} />
41+
<div style={dotStyle(0.3)} />
42+
</div>
43+
)
44+
45+
const MessageItem = ({ message, isLastAssistantNode, lastNodeHeight }: { message: Message; isLastAssistantNode: boolean; lastNodeHeight: number }) => (
946
<Box
47+
className={`message-role-${message.role}`}
1048
sx={{
1149
alignSelf: message.role === 'assistant' ? 'flex-start' : 'flex-end',
1250
backgroundColor: message.role === 'assistant' ? 'transparent' : '#efefef',
1351
padding: '0 1.5rem',
1452
borderRadius: '0.6rem',
53+
height: isLastAssistantNode ? lastNodeHeight : 'auto',
1554
}}
1655
>
1756
<Typography>
@@ -20,12 +59,34 @@ const MessageItem = ({ message }: { message: Message }) => (
2059
</Box>
2160
)
2261

23-
export const Conversation = ({ messages, completion, fileSearchResult }: { messages: Message[]; completion: string; fileSearchResult: FileSearchResult }) => (
24-
<>
62+
export const Conversation = ({
63+
conversationRef,
64+
lastNodeHeight,
65+
messages,
66+
completion,
67+
isCompletionDone,
68+
fileSearchResult,
69+
}: {
70+
conversationRef: React.RefObject<HTMLElement>
71+
lastNodeHeight: number
72+
messages: Message[]
73+
completion: string
74+
isCompletionDone: boolean
75+
fileSearchResult: FileSearchResult
76+
}) => (
77+
<Box style={{ height: '100%', display: 'flex', flexDirection: 'column', gap: 20 }} ref={conversationRef}>
2578
{messages.length === 0 && <ConversationSplash />}
26-
{messages.map((message, idx) => (
27-
<MessageItem key={idx} message={message} />
28-
))}
29-
{completion && <MessageItem message={{ role: 'assistant', content: completion, fileSearchResult }} />}
30-
</>
79+
{messages.map((message, idx) => {
80+
const isLastAssistantNode = idx === messages.length - 1 && message.role === 'assistant'
81+
82+
return <MessageItem key={idx} message={message} isLastAssistantNode={isLastAssistantNode} lastNodeHeight={lastNodeHeight} />
83+
})}
84+
{!isCompletionDone &&
85+
messages.length > 0 &&
86+
(completion.length > 0 ? (
87+
<MessageItem message={{ role: 'assistant', content: completion, fileSearchResult }} isLastAssistantNode={true} lastNodeHeight={lastNodeHeight} />
88+
) : (
89+
<MessageLoading lastNodeHeight={lastNodeHeight} />
90+
))}
91+
</Box>
3192
)

src/client/components/ChatV2/useScrollToBottom.ts

Lines changed: 0 additions & 25 deletions
This file was deleted.

0 commit comments

Comments
 (0)