Skip to content

Commit 8d1f9b0

Browse files
Merge pull request #3515 from RedisInsight/fe/bugfix/RI-5852-chats-history-scroll
#RI-5852 - update chat history scroll behavior
2 parents df8772a + b0cbb90 commit 8d1f9b0

File tree

7 files changed

+87
-69
lines changed

7 files changed

+87
-69
lines changed

redisinsight/api/src/constants/telemetry-events.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export enum TelemetryEvents {
8989
FeatureFlagRecalculated = 'FEATURE_FLAG_RECALCULATED',
9090

9191
// Insights
92-
InsightsRecommendationGenerated = 'INSIGHTS_RECOMMENDATION_GENERATED',
92+
InsightsTipGenerated = 'INSIGHTS_TIP_GENERATED',
9393
}
9494

9595
export enum CommandType {

redisinsight/api/src/modules/database-recommendation/database-recommendation.analytics.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ describe('DatabaseRecommendationAnalytics', () => {
3636
);
3737

3838
expect(sendEventSpy).toHaveBeenCalledWith(
39-
TelemetryEvents.InsightsRecommendationGenerated,
39+
TelemetryEvents.InsightsTipGenerated,
4040
{
4141
recommendationName: mockDatabaseRecommendation.name,
4242
databaseId: mockDatabase.id,

redisinsight/api/src/modules/database-recommendation/database-recommendation.analytics.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export class DatabaseRecommendationAnalytics extends TelemetryBaseService {
1414
sendCreatedRecommendationEvent(recommendation: DatabaseRecommendation, database: Database): void {
1515
try {
1616
this.sendEvent(
17-
TelemetryEvents.InsightsRecommendationGenerated,
17+
TelemetryEvents.InsightsTipGenerated,
1818
{
1919
recommendationName: recommendation.name,
2020
databaseId: database.id,

redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/assistance-chat/AssistanceChat.tsx

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { Ref, useCallback, useEffect, useRef, useState } from 'react'
1+
import React, { useCallback, useEffect, useState } from 'react'
22
import { useDispatch, useSelector } from 'react-redux'
33
import { useParams } from 'react-router-dom'
44
import { EuiButtonEmpty } from '@elastic/eui'
@@ -7,11 +7,12 @@ import {
77
askAssistantChatbot,
88
createAssistantChatAction,
99
getAssistantChatHistoryAction,
10-
removeAssistantChatAction, removeAssistantChatHistorySuccess,
10+
removeAssistantChatAction,
11+
removeAssistantChatHistorySuccess,
1112
sendQuestion,
1213
updateAssistantChatAgreements,
1314
} from 'uiSrc/slices/panels/aiAssistant'
14-
import { getCommandsFromQuery, Nullable, scrollIntoView } from 'uiSrc/utils'
15+
import { getCommandsFromQuery, Nullable } from 'uiSrc/utils'
1516
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
1617
import { AiChatMessage, AiChatType } from 'uiSrc/slices/interfaces/aiAssistant'
1718

@@ -32,23 +33,17 @@ const AssistanceChat = () => {
3233
const { commandsArray: REDIS_COMMANDS_ARRAY } = useSelector(appRedisCommandsSelector)
3334

3435
const [inProgressMessage, setinProgressMessage] = useState<Nullable<AiChatMessage>>(null)
35-
const scrollDivRef: Ref<HTMLDivElement> = useRef(null)
3636
const { instanceId } = useParams<{ instanceId: string }>()
3737

3838
const dispatch = useDispatch()
3939

4040
useEffect(() => {
41-
if (!id || messages.length) {
42-
scrollToBottom('auto')
43-
return
44-
}
41+
if (!id || messages.length) return
4542

46-
dispatch(getAssistantChatHistoryAction(id, () => scrollToBottom('auto')))
43+
dispatch(getAssistantChatHistoryAction(id))
4744
}, [id])
4845

4946
const handleSubmit = useCallback((message: string) => {
50-
scrollToBottom('smooth')
51-
5247
if (!agreements) {
5348
dispatch(updateAssistantChatAgreements(true))
5449
sendEventTelemetry({
@@ -95,7 +90,6 @@ const AssistanceChat = () => {
9590
{
9691
onMessage: (message: AiChatMessage) => {
9792
setinProgressMessage({ ...message })
98-
scrollToBottom('auto')
9993
},
10094
onError: (errorCode: number) => {
10195
sendEventTelemetry({
@@ -156,16 +150,6 @@ const AssistanceChat = () => {
156150
})
157151
}, [])
158152

159-
const scrollToBottom = useCallback((behavior: ScrollBehavior = 'smooth') => {
160-
requestAnimationFrame(() => {
161-
scrollIntoView(scrollDivRef?.current, {
162-
behavior,
163-
block: 'start',
164-
inline: 'start',
165-
})
166-
})
167-
}, [])
168-
169153
return (
170154
<div className={styles.wrapper} data-testid="ai-general-chat">
171155
<div className={styles.header}>
@@ -185,13 +169,12 @@ const AssistanceChat = () => {
185169
</div>
186170
<div className={styles.chatHistory}>
187171
<ChatHistory
172+
autoScroll
188173
isLoading={loading}
189174
modules={modules}
190175
initialMessage={AssistanceChatInitialMessage}
191176
inProgressMessage={inProgressMessage}
192177
history={messages}
193-
scrollDivRef={scrollDivRef}
194-
onMessageRendered={scrollToBottom}
195178
onRunCommand={onRunCommand}
196179
onRestart={onClearSession}
197180
/>

redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/expert-chat/ExpertChat.spec.tsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
aiExpertChatSelector,
1616
clearExpertChatHistory,
1717
getExpertChatHistory,
18+
getExpertChatHistorySuccess,
1819
sendExpertQuestion,
1920
updateExpertChatAgreements,
2021
} from 'uiSrc/slices/panels/aiAssistant'
@@ -57,19 +58,20 @@ jest.mock('react-router-dom', () => ({
5758
}))
5859

5960
let store: typeof mockedStore
60-
beforeEach(() => {
61-
cleanup()
62-
store = cloneDeep(mockedStoreFn())
63-
store.clearActions()
64-
})
6561

6662
describe('ExpertChat', () => {
63+
beforeEach(() => {
64+
cleanup()
65+
store = cloneDeep(mockedStoreFn())
66+
store.clearActions()
67+
})
68+
6769
it('should render', () => {
68-
expect(render(<ExpertChat />)).toBeTruthy()
70+
expect(render(<ExpertChat />, { store })).toBeTruthy()
6971
})
7072

7173
it('should proper components render by default', () => {
72-
render(<ExpertChat />)
74+
render(<ExpertChat />, { store })
7375

7476
expect(screen.getByTestId('ai-expert-restart-session-btn')).toBeInTheDocument()
7577
expect(screen.getByTestId('ai-chat-empty-history')).toBeInTheDocument()
@@ -82,7 +84,7 @@ describe('ExpertChat', () => {
8284
messages: [],
8385
agreements: []
8486
})
85-
render(<ExpertChat />)
87+
render(<ExpertChat />, { store })
8688

8789
expect(screen.getByTestId('ai-loading-spinner')).toBeInTheDocument()
8890
expect(screen.queryByTestId('ai-chat-empty-history')).not.toBeInTheDocument()
@@ -200,7 +202,8 @@ describe('ExpertChat', () => {
200202
it('should call action after click on restart session', async () => {
201203
const sendEventTelemetryMock = jest.fn();
202204
(sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock)
203-
apiService.delete = jest.fn().mockResolvedValueOnce({ status: 200 });
205+
apiService.delete = jest.fn().mockResolvedValueOnce({ status: 200 })
206+
apiService.get = jest.fn().mockResolvedValueOnce({ status: 200, data: [] });
204207

205208
(aiExpertChatSelector as jest.Mock).mockReturnValue({
206209
loading: false,
@@ -221,6 +224,7 @@ describe('ExpertChat', () => {
221224

222225
expect(store.getActions()).toEqual([
223226
...afterRenderActions,
227+
getExpertChatHistorySuccess([]),
224228
clearExpertChatHistory()
225229
])
226230

redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/expert-chat/ExpertChat.tsx

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { Ref, useCallback, useEffect, useRef, useState } from 'react'
1+
import React, { useCallback, useEffect, useRef, useState } from 'react'
22
import { useDispatch, useSelector } from 'react-redux'
33
import { useHistory, useParams } from 'react-router-dom'
44
import { EuiIcon } from '@elastic/eui'
@@ -9,7 +9,7 @@ import {
99
removeExpertChatHistoryAction,
1010
updateExpertChatAgreements,
1111
} from 'uiSrc/slices/panels/aiAssistant'
12-
import { findTutorialPath, getCommandsFromQuery, isRedisearchAvailable, Nullable, scrollIntoView } from 'uiSrc/utils'
12+
import { findTutorialPath, getCommandsFromQuery, isRedisearchAvailable, Nullable } from 'uiSrc/utils'
1313
import { connectedInstanceSelector, freeInstancesSelector } from 'uiSrc/slices/instances/instances'
1414

1515
import { sendEventTelemetry, TELEMETRY_EMPTY_VALUE, TelemetryEvent } from 'uiSrc/telemetry'
@@ -40,7 +40,6 @@ const ExpertChat = () => {
4040
const [inProgressMessage, setinProgressMessage] = useState<Nullable<AiChatMessage>>(null)
4141

4242
const currentAccountIdRef = useRef(userOAuthProfile?.id)
43-
const scrollDivRef: Ref<HTMLDivElement> = useRef(null)
4443
const { instanceId } = useParams<{ instanceId: string }>()
4544

4645
const isAgreementsAccepted = agreements.includes(instanceId) || messages.length > 0
@@ -56,16 +55,11 @@ const ExpertChat = () => {
5655
// changed account
5756
if (currentAccountIdRef.current !== userOAuthProfile?.id) {
5857
currentAccountIdRef.current = userOAuthProfile?.id
59-
dispatch(getExpertChatHistoryAction(instanceId, () => scrollToBottom('auto')))
58+
dispatch(getExpertChatHistoryAction(instanceId))
6059
return
6160
}
6261

63-
if (messages.length) {
64-
scrollToBottom('auto')
65-
return
66-
}
67-
68-
dispatch(getExpertChatHistoryAction(instanceId, () => scrollToBottom('auto')))
62+
dispatch(getExpertChatHistoryAction(instanceId))
6963
}, [instanceId, userOAuthProfile])
7064

7165
useEffect(() => {
@@ -91,8 +85,6 @@ const ExpertChat = () => {
9185
}
9286

9387
const handleSubmit = useCallback((message: string) => {
94-
scrollToBottom()
95-
9688
if (!isAgreementsAccepted) {
9789
dispatch(updateExpertChatAgreements(instanceId))
9890

@@ -109,10 +101,7 @@ const ExpertChat = () => {
109101
instanceId,
110102
message,
111103
{
112-
onMessage: (message: AiChatMessage) => {
113-
setinProgressMessage({ ...message })
114-
scrollToBottom('auto')
115-
},
104+
onMessage: (message: AiChatMessage) => setinProgressMessage({ ...message }),
116105
onError: (errorCode: number) => {
117106
sendEventTelemetry({
118107
event: TelemetryEvent.AI_CHAT_BOT_ERROR_MESSAGE_RECEIVED,
@@ -180,16 +169,6 @@ const ExpertChat = () => {
180169
})
181170
}
182171

183-
const scrollToBottom = (behavior: ScrollBehavior = 'smooth') => {
184-
setTimeout(() => {
185-
scrollIntoView(scrollDivRef?.current, {
186-
behavior,
187-
block: 'start',
188-
inline: 'start',
189-
})
190-
}, 0)
191-
}
192-
193172
const getValidationMessage = () => {
194173
if (!instanceId) {
195174
return {
@@ -226,14 +205,14 @@ const ExpertChat = () => {
226205
/>
227206
<div className={styles.chatHistory}>
228207
<ChatHistory
208+
autoScroll
229209
isLoading={loading || isLoading}
230210
modules={modules}
231211
initialMessage={isNoIndexes
232212
? <NoIndexesInitialMessage onClickTutorial={handleClickTutorial} onSuccess={getIndexes} />
233213
: EXPERT_CHAT_INITIAL_MESSAGE}
234214
inProgressMessage={inProgressMessage}
235215
history={messages}
236-
scrollDivRef={scrollDivRef}
237216
onRunCommand={onRunCommand}
238217
onRestart={onClearSession}
239218
/>

redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/chat-history/ChatHistory.tsx

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import React, { useCallback } from 'react'
1+
import React, { MutableRefObject, Ref, useCallback, useEffect, useRef } from 'react'
22
import cx from 'classnames'
33

44
import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui'
5+
import { throttle } from 'lodash'
56
import { AiChatMessage, AiChatMessageType } from 'uiSrc/slices/interfaces/aiAssistant'
6-
import { Nullable } from 'uiSrc/utils'
7+
import { Nullable, scrollIntoView } from 'uiSrc/utils'
78
import { AdditionalRedisModule } from 'apiSrc/modules/database/models/additional.redis.module'
89

910
import LoadingMessage from '../loading-message'
@@ -13,30 +14,81 @@ import ErrorMessage from '../error-message'
1314
import styles from './styles.module.scss'
1415

1516
export interface Props {
17+
autoScroll?: boolean
1618
isLoading?: boolean
1719
initialMessage: React.ReactNode
1820
inProgressMessage?: Nullable<AiChatMessage>
1921
modules?: AdditionalRedisModule[]
2022
history: AiChatMessage[]
21-
scrollDivRef: React.Ref<HTMLDivElement>
2223
onMessageRendered?: () => void
2324
onRunCommand?: (query: string) => void
2425
onRestart: () => void
2526
}
2627

28+
const SCROLL_THROTTLE_MS = 200
29+
2730
const ChatHistory = (props: Props) => {
2831
const {
32+
autoScroll,
2933
isLoading,
3034
initialMessage,
3135
inProgressMessage,
3236
modules,
3337
history = [],
34-
scrollDivRef,
3538
onMessageRendered,
3639
onRunCommand,
3740
onRestart
3841
} = props
3942

43+
const scrollDivRef: Ref<HTMLDivElement> = useRef(null)
44+
const listRef: Ref<HTMLDivElement> = useRef(null)
45+
const observerRef: MutableRefObject<Nullable<MutationObserver>> = useRef(null)
46+
const scrollBehavior = useRef<ScrollBehavior>('auto')
47+
48+
useEffect(() => {
49+
if (!autoScroll) return undefined
50+
if (!listRef.current) return undefined
51+
52+
scrollBehavior.current = inProgressMessage ? 'smooth' : 'auto'
53+
54+
if (!inProgressMessage) scrollToBottom()
55+
if (inProgressMessage?.content === '') scrollToBottomThrottled()
56+
57+
if (!observerRef.current) {
58+
const observerCallback: MutationCallback = (mutationsList) => {
59+
// eslint-disable-next-line no-restricted-syntax
60+
for (const mutation of mutationsList) {
61+
if (mutation.type === 'childList') {
62+
scrollBehavior.current === 'smooth' ? scrollToBottomThrottled() : scrollToBottom()
63+
break
64+
}
65+
}
66+
}
67+
68+
observerRef.current = new MutationObserver(observerCallback)
69+
}
70+
71+
observerRef.current.observe(listRef.current, {
72+
childList: true,
73+
subtree: true,
74+
})
75+
76+
return () => {
77+
observerRef.current?.disconnect()
78+
}
79+
}, [autoScroll, inProgressMessage, history])
80+
81+
const scrollToBottom = (behavior: ScrollBehavior = 'auto') => {
82+
requestAnimationFrame(() => {
83+
scrollIntoView(scrollDivRef?.current, {
84+
behavior,
85+
block: 'start',
86+
inline: 'start',
87+
})
88+
})
89+
}
90+
const scrollToBottomThrottled = throttle(() => scrollToBottom('smooth'), SCROLL_THROTTLE_MS)
91+
4092
const getMessage = useCallback((message?: Nullable<AiChatMessage>) => {
4193
if (!message) return null
4294

@@ -107,15 +159,15 @@ const ChatHistory = (props: Props) => {
107159

108160
return (
109161
<div className={styles.wrapper}>
110-
<div className={styles.history} data-testid="ai-chat-history">
162+
<div ref={listRef} className={styles.history} data-testid="ai-chat-history">
111163
{history.map(getMessage)}
112164
{getMessage(inProgressMessage)}
113165
{content === '' && (
114166
<div className={styles.answerWrapper}>
115167
<div className={styles.answer} data-testid="ai-loading-answer"><LoadingMessage /></div>
116168
</div>
117169
)}
118-
<div ref={scrollDivRef} />
170+
<div className={styles.scrollAnchor} ref={scrollDivRef} />
119171
</div>
120172
</div>
121173
)

0 commit comments

Comments
 (0)