Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ import {
import {
aiAssistantChatSelector,
createAssistantChat,
getAssistantChatHistory, removeAssistantChatHistory,
sendQuestion
getAssistantChatHistory,
removeAssistantChatHistory,
sendQuestion, updateAssistantChatAgreements
} from 'uiSrc/slices/panels/aiAssistant'
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
import { AiChatType } from 'uiSrc/slices/interfaces/aiAssistant'
Expand All @@ -30,7 +31,8 @@ jest.mock('uiSrc/slices/panels/aiAssistant', () => ({
...jest.requireActual('uiSrc/slices/panels/aiAssistant'),
aiAssistantChatSelector: jest.fn().mockReturnValue({
id: '',
messages: []
messages: [],
agreements: true
})
}))

Expand Down Expand Up @@ -63,7 +65,8 @@ describe('AssistanceChat', () => {
it('should get history', () => {
(aiAssistantChatSelector as jest.Mock).mockReturnValue({
id: '1',
messages: []
messages: [],
agreements: true
})
render(<AssistanceChat />, { store })

Expand All @@ -73,7 +76,8 @@ describe('AssistanceChat', () => {
it('should call action to create an id after submit first message', () => {
(aiAssistantChatSelector as jest.Mock).mockReturnValue({
id: '',
messages: []
messages: [],
agreements: true
})
render(<AssistanceChat />, { store })

Expand All @@ -95,7 +99,8 @@ describe('AssistanceChat', () => {

(aiAssistantChatSelector as jest.Mock).mockReturnValue({
id: '1',
messages: []
messages: [],
agreements: true
})
render(<AssistanceChat />, { store })

Expand Down Expand Up @@ -124,13 +129,65 @@ describe('AssistanceChat', () => {
(sendEventTelemetry as jest.Mock).mockRestore()
})

it('should show agreements', async () => {
const sendEventTelemetryMock = jest.fn();
(sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock);

(aiAssistantChatSelector as jest.Mock).mockReturnValue({
id: '1',
messages: [],
agreements: false
})
render(<AssistanceChat />, { store })

const afterRenderActions = [...store.getActions()]

act(() => {
fireEvent.change(
screen.getByTestId('ai-message-textarea'),
{ target: { value: 'test' } }
)
})

fireEvent.click(screen.getByTestId('ai-submit-message-btn'))

await waitForEuiPopoverVisible()

expect(sendEventTelemetry).toBeCalledWith({
event: TelemetryEvent.AI_CHAT_BOT_TERMS_DISPLAYED,
eventData: {
chat: AiChatType.Assistance
}
});
(sendEventTelemetry as jest.Mock).mockRestore()

act(() => {
fireEvent.click(screen.getByTestId('ai-accept-agreements'))
})

expect(sendEventTelemetry).toBeCalledWith({
event: TelemetryEvent.AI_CHAT_BOT_TERMS_ACCEPTED,
eventData: {
chat: AiChatType.Assistance,
}
});
(sendEventTelemetry as jest.Mock).mockRestore()

expect(store.getActions()).toEqual([
...afterRenderActions,
updateAssistantChatAgreements(true),
sendQuestion(expect.objectContaining({ content: 'test' }))
])
})

it('should call action after click on restart session', async () => {
const sendEventTelemetryMock = jest.fn();
(sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock);

(aiAssistantChatSelector as jest.Mock).mockReturnValue({
id: '1',
messages: [{}]
messages: [{}],
agreements: true
})

render(<AssistanceChat />, { store })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import {
askAssistantChatbot,
createAssistantChatAction,
getAssistantChatHistoryAction,
removeAssistantChatAction,
removeAssistantChatAction, removeAssistantChatHistorySuccess,
sendQuestion,
updateAssistantChatAgreements,
} from 'uiSrc/slices/panels/aiAssistant'
import { getCommandsFromQuery, Nullable, scrollIntoView } from 'uiSrc/utils'
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
Expand All @@ -18,12 +19,15 @@ import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'
import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands'

import { generateHumanMessage } from 'uiSrc/utils/transformers/chatbot'
import { AssistanceChatInitialMessage, ChatHistory, ChatForm, RestartChat } from '../shared'

import { CustomErrorCodes } from 'uiSrc/constants'
import { ASSISTANCE_CHAT_AGREEMENTS } from '../texts'
import { AssistanceChatInitialMessage, ChatForm, ChatHistory, RestartChat } from '../shared'

import styles from './styles.module.scss'

const AssistanceChat = () => {
const { id, messages } = useSelector(aiAssistantChatSelector)
const { id, messages, agreements, loading } = useSelector(aiAssistantChatSelector)
const { modules, provider } = useSelector(connectedInstanceSelector)
const { commandsArray: REDIS_COMMANDS_ARRAY } = useSelector(appRedisCommandsSelector)

Expand All @@ -45,24 +49,44 @@ const AssistanceChat = () => {
const handleSubmit = useCallback((message: string) => {
scrollToBottom('smooth')

if (!agreements) {
dispatch(updateAssistantChatAgreements(true))
sendEventTelemetry({
event: TelemetryEvent.AI_CHAT_BOT_TERMS_ACCEPTED,
eventData: {
chat: AiChatType.Assistance,
}
})
}

if (!id) {
dispatch(
createAssistantChatAction(
(chatId) => sendChatMessage(chatId, message),
// if cannot create a chat - just put message with error
() => dispatch(
sendQuestion({
...generateHumanMessage(message),
error: { statusCode: 500 },
() => {
dispatch(
sendQuestion({
...generateHumanMessage(message),
error: { statusCode: 500, errorCode: CustomErrorCodes.GeneralAiUnexpectedError },
})
)

sendEventTelemetry({
event: TelemetryEvent.AI_CHAT_BOT_ERROR_MESSAGE_RECEIVED,
eventData: {
chat: AiChatType.Assistance,
errorCode: 500
}
})
)
}
)
)
return
}

sendChatMessage(id, message)
}, [id])
}, [id, agreements])

const sendChatMessage = (chatId: string, message: string) => {
dispatch(askAssistantChatbot(
Expand Down Expand Up @@ -95,7 +119,10 @@ const AssistanceChat = () => {
}

const onClearSession = useCallback(() => {
if (!id) return
if (!id) {
dispatch(removeAssistantChatHistorySuccess())
return
}

dispatch(removeAssistantChatAction(id))

Expand All @@ -120,6 +147,15 @@ const AssistanceChat = () => {
})
}, [instanceId, provider])

const handleAgreementsDisplay = useCallback(() => {
sendEventTelemetry({
event: TelemetryEvent.AI_CHAT_BOT_TERMS_DISPLAYED,
eventData: {
chat: AiChatType.Assistance,
}
})
}, [])

const scrollToBottom = useCallback((behavior: ScrollBehavior = 'smooth') => {
requestAnimationFrame(() => {
scrollIntoView(scrollDivRef?.current, {
Expand Down Expand Up @@ -149,6 +185,7 @@ const AssistanceChat = () => {
</div>
<div className={styles.chatHistory}>
<ChatHistory
isLoading={loading}
modules={modules}
initialMessage={AssistanceChatInitialMessage}
inProgressMessage={inProgressMessage}
Expand All @@ -161,6 +198,8 @@ const AssistanceChat = () => {
</div>
<div className={styles.chatForm}>
<ChatForm
onAgreementsDisplayed={handleAgreementsDisplay}
agreements={!agreements ? ASSISTANCE_CHAT_AGREEMENTS : undefined}
placeholder="Ask me about Redis"
isDisabled={!!inProgressMessage}
onSubmit={handleSubmit}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ import {
clearExpertChatHistory,
getExpertChatHistory,
sendExpertQuestion,
updateExpertChatAgreements,
} from 'uiSrc/slices/panels/aiAssistant'
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
import { AiChatType } from 'uiSrc/slices/interfaces/aiAssistant'
import { apiService } from 'uiSrc/services'
import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'
import { RedisDefaultModules } from 'uiSrc/slices/interfaces'
import { loadList } from 'uiSrc/slices/browser/redisearch'
import ExpertChat from './ExpertChat'

jest.mock('uiSrc/telemetry', () => ({
Expand All @@ -31,10 +35,25 @@ jest.mock('uiSrc/slices/panels/aiAssistant', () => ({
...jest.requireActual('uiSrc/slices/panels/aiAssistant'),
aiExpertChatSelector: jest.fn().mockReturnValue({
loading: false,
messages: []
messages: [],
agreements: []
})
}))

jest.mock('uiSrc/slices/instances/instances', () => ({
...jest.requireActual('uiSrc/slices/instances/instances'),
connectedInstanceSelector: jest.fn().mockReturnValue({
modules: []
}),
}))

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: () => ({
instanceId: 'instanceId',
}),
}))

let store: typeof mockedStore
beforeEach(() => {
cleanup()
Expand All @@ -58,7 +77,8 @@ describe('ExpertChat', () => {
it('should show loading', () => {
(aiExpertChatSelector as jest.Mock).mockReturnValue({
loading: true,
messages: []
messages: [],
agreements: []
})
render(<ExpertChat />)

Expand All @@ -72,13 +92,29 @@ describe('ExpertChat', () => {
expect(store.getActions()).toEqual([getExpertChatHistory()])
})

it('should call fetch indexes', () => {
(aiExpertChatSelector as jest.Mock).mockReturnValue({
loading: true,
messages: [],
agreements: []
});
(connectedInstanceSelector as jest.Mock).mockImplementation(() => ({
modules: [{ name: RedisDefaultModules.FT }, { name: RedisDefaultModules.ReJSON }]
}))

render(<ExpertChat />, { store })

expect(store.getActions()).toEqual([getExpertChatHistory(), loadList()])
})

it('should call action after submit message', () => {
const sendEventTelemetryMock = jest.fn();
(sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock);

(aiExpertChatSelector as jest.Mock).mockReturnValue({
loading: false,
messages: []
messages: [],
agreements: ['instanceId']
})
render(<ExpertChat />, { store })

Expand Down Expand Up @@ -107,14 +143,67 @@ describe('ExpertChat', () => {
(sendEventTelemetry as jest.Mock).mockRestore()
})

it('should show agreements after click submit', async () => {
const sendEventTelemetryMock = jest.fn();
(sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock);

(aiExpertChatSelector as jest.Mock).mockReturnValue({
loading: false,
messages: [],
agreements: []
})
render(<ExpertChat />, { store })

const afterRenderActions = [...store.getActions()]

act(() => {
fireEvent.change(
screen.getByTestId('ai-message-textarea'),
{ target: { value: 'test' } }
)
})

fireEvent.click(screen.getByTestId('ai-submit-message-btn'))

await waitForEuiPopoverVisible()

expect(sendEventTelemetry).toBeCalledWith({
event: TelemetryEvent.AI_CHAT_BOT_TERMS_DISPLAYED,
eventData: {
chat: AiChatType.Query
}
});
(sendEventTelemetry as jest.Mock).mockRestore()

act(() => {
fireEvent.click(screen.getByTestId('ai-accept-agreements'))
})

expect(sendEventTelemetry).toBeCalledWith({
event: TelemetryEvent.AI_CHAT_BOT_TERMS_ACCEPTED,
eventData: {
chat: AiChatType.Query,
databaseId: 'instanceId'
}
});
(sendEventTelemetry as jest.Mock).mockRestore()

expect(store.getActions()).toEqual([
...afterRenderActions,
updateExpertChatAgreements('instanceId'),
sendExpertQuestion(expect.objectContaining({ content: 'test' }))
])
})

it('should call action after click on restart session', async () => {
const sendEventTelemetryMock = jest.fn();
(sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock)
apiService.delete = jest.fn().mockResolvedValueOnce({ status: 200 });

(aiExpertChatSelector as jest.Mock).mockReturnValue({
loading: false,
messages: [{}]
messages: [{}],
agreements: ['instanceId']
})

render(<ExpertChat />, { store })
Expand Down
Loading