Skip to content

Commit dd93c96

Browse files
committed
#RI-5852 - update chat history scroll behavior
1 parent efef880 commit dd93c96

File tree

6 files changed

+77
-61
lines changed

6 files changed

+77
-61
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: 6 additions & 25 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,15 @@ 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-
}
45-
46-
dispatch(getAssistantChatHistoryAction(id, () => scrollToBottom('auto')))
41+
dispatch(getAssistantChatHistoryAction(id))
4742
}, [id])
4843

4944
const handleSubmit = useCallback((message: string) => {
50-
scrollToBottom('smooth')
51-
5245
if (!agreements) {
5346
dispatch(updateAssistantChatAgreements(true))
5447
sendEventTelemetry({
@@ -95,7 +88,6 @@ const AssistanceChat = () => {
9588
{
9689
onMessage: (message: AiChatMessage) => {
9790
setinProgressMessage({ ...message })
98-
scrollToBottom('auto')
9991
},
10092
onError: (errorCode: number) => {
10193
sendEventTelemetry({
@@ -156,16 +148,6 @@ const AssistanceChat = () => {
156148
})
157149
}, [])
158150

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-
169151
return (
170152
<div className={styles.wrapper} data-testid="ai-general-chat">
171153
<div className={styles.header}>
@@ -185,13 +167,12 @@ const AssistanceChat = () => {
185167
</div>
186168
<div className={styles.chatHistory}>
187169
<ChatHistory
170+
autoScroll
188171
isLoading={loading}
189172
modules={modules}
190173
initialMessage={AssistanceChatInitialMessage}
191174
inProgressMessage={inProgressMessage}
192175
history={messages}
193-
scrollDivRef={scrollDivRef}
194-
onMessageRendered={scrollToBottom}
195176
onRunCommand={onRunCommand}
196177
onRestart={onClearSession}
197178
/>

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

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

108164
return (
109165
<div className={styles.wrapper}>
110-
<div className={styles.history} data-testid="ai-chat-history">
166+
<div ref={listRef} className={styles.history} data-testid="ai-chat-history">
111167
{history.map(getMessage)}
112168
{getMessage(inProgressMessage)}
113169
{content === '' && (
114170
<div className={styles.answerWrapper}>
115171
<div className={styles.answer} data-testid="ai-loading-answer"><LoadingMessage /></div>
116172
</div>
117173
)}
118-
<div ref={scrollDivRef} />
174+
<div className={styles.scrollAnchor} ref={scrollDivRef} />
119175
</div>
120176
</div>
121177
)

0 commit comments

Comments
 (0)