diff --git a/web/src/components/ChatConversation.tsx b/web/src/components/ChatConversation.tsx
index 283f451ef9..65da5ab060 100644
--- a/web/src/components/ChatConversation.tsx
+++ b/web/src/components/ChatConversation.tsx
@@ -4,7 +4,7 @@ import styled from 'styled-components'
import ChatMessageModel from 'shared/api/models/ChatMessageModel'
-import ChatMessage from './ChatMessage'
+import ChatMessage, { Message } from './ChatMessage'
const Container = styled.div`
font-size: ${props => props.theme.fonts.hintFontSize};
@@ -15,6 +15,10 @@ const InitialMessage = styled.div`
margin-bottom: 12px;
`
+const TypingIndicatorWrapper = styled(Message)`
+ width: max-content;
+`
+
const ErrorSendingStatus = styled.div`
background-color: ${props => props.theme.colors.invalidInput};
border-radius: 5px;
@@ -29,10 +33,27 @@ type ChatConversationProps = {
className?: string
}
+type TypingIndicatorProps = {
+ isVisible: boolean
+}
+
+const TypingIndicator = ({ isVisible }: TypingIndicatorProps): ReactElement | null =>
+ isVisible ? (
+
+ ...
+
+ ) : null
+
+const TYPING_INDICATOR_TIMEOUT = 60000
+
const ChatConversation = ({ messages, hasError, className }: ChatConversationProps): ReactElement => {
const { t } = useTranslation('chat')
const [messagesCount, setMessagesCount] = useState(0)
+ const [typingIndicatorVisible, setTypingIndicatorVisible] = useState(false)
const messagesEndRef = useRef(null)
+ const isLastMessageFromUser = messages[messages.length - 1]?.userIsAuthor
+ const hasOnlyReceivedInfoMessage = messages.filter(message => !message.userIsAuthor).length === 1
+ const waitingForAnswer = isLastMessageFromUser || hasOnlyReceivedInfoMessage
useEffect(() => {
if (messagesCount < messages.length) {
@@ -41,6 +62,19 @@ const ChatConversation = ({ messages, hasError, className }: ChatConversationPro
}
}, [messages, messagesCount])
+ useEffect(() => {
+ if (waitingForAnswer) {
+ setTypingIndicatorVisible(true)
+
+ const typingIndicatorTimeout = setTimeout(() => {
+ setTypingIndicatorVisible(false)
+ }, TYPING_INDICATOR_TIMEOUT)
+
+ return () => clearTimeout(typingIndicatorTimeout)
+ }
+ return () => undefined
+ }, [waitingForAnswer])
+
return (
{messages.length > 0 ? (
@@ -53,6 +87,7 @@ const ChatConversation = ({ messages, hasError, className }: ChatConversationPro
showIcon={messages[index - 1]?.userIsAuthor !== message.userIsAuthor}
/>
))}
+
>
) : (
diff --git a/web/src/components/ChatMessage.tsx b/web/src/components/ChatMessage.tsx
index 961716258f..d28949a999 100644
--- a/web/src/components/ChatMessage.tsx
+++ b/web/src/components/ChatMessage.tsx
@@ -9,7 +9,7 @@ import { ChatBot, ChatPerson } from '../assets'
import RemoteContent from './RemoteContent'
import Icon from './base/Icon'
-const Message = styled.div`
+export const Message = styled.div`
border-radius: 5px;
padding: 8px;
border: 1px solid ${props => props.theme.colors.textDecorationColor};
diff --git a/web/src/components/__tests__/ChatConversation.spec.tsx b/web/src/components/__tests__/ChatConversation.spec.tsx
index d104569d34..568ed31657 100644
--- a/web/src/components/__tests__/ChatConversation.spec.tsx
+++ b/web/src/components/__tests__/ChatConversation.spec.tsx
@@ -1,3 +1,4 @@
+import { act } from '@testing-library/react'
import React from 'react'
import ChatMessageModel from 'shared/api/models/ChatMessageModel'
@@ -7,6 +8,7 @@ import ChatConversation from '../ChatConversation'
jest.mock('react-i18next')
window.HTMLElement.prototype.scrollIntoView = jest.fn()
+jest.useFakeTimers()
const render = (messages: ChatMessageModel[], hasError: boolean) =>
renderWithRouterAndTheme()
@@ -21,10 +23,22 @@ describe('ChatConversation', () => {
}),
new ChatMessageModel({
id: 2,
+ body: 'Willkommen in der Integreat Chat Testumgebung auf Deutsch. Unser Team antwortet werktags, während unser Chatbot zusammenfassende Antworten aus verlinkten Artikeln liefert, die Sie zur Überprüfung wichtiger Informationen lesen sollten.',
+ userIsAuthor: false,
+ automaticAnswer: true,
+ }),
+ new ChatMessageModel({
+ id: 3,
body: 'Informationen zu Ihrer Frage finden Sie auf folgenden Seiten:',
userIsAuthor: false,
automaticAnswer: false,
}),
+ new ChatMessageModel({
+ id: 4,
+ body: 'Wie kann ich mein Deutsch verbessern?',
+ userIsAuthor: true,
+ automaticAnswer: false,
+ }),
]
it('should display welcome text if conversation has not started', () => {
@@ -37,7 +51,25 @@ describe('ChatConversation', () => {
const { getByText, getByTestId } = render(testMessages, false)
expect(getByText('chat:initialMessage')).toBeTruthy()
expect(getByTestId(testMessages[0]!.id)).toBeTruthy()
+ expect(getByTestId(testMessages[2]!.id)).toBeTruthy()
+ })
+
+ it('should display typing indicator before the initial automatic answer and after for 60 seconds', () => {
+ const { getByText, queryByText, getByTestId } = render(testMessages, false)
+ expect(getByTestId(testMessages[0]!.id)).toBeTruthy()
+ expect(getByText('...')).toBeTruthy()
expect(getByTestId(testMessages[1]!.id)).toBeTruthy()
+ expect(getByText('...')).toBeTruthy()
+
+ act(() => jest.runAllTimers())
+ expect(queryByText('...')).toBeNull()
+ })
+
+ it('should display typing indicator after opening the chatbot with existing conversation for unanswered user message', () => {
+ const { queryByText, getByTestId } = render(testMessages, false)
+ expect(getByTestId(testMessages[3]!.id)).toBeTruthy()
+ act(() => jest.runAllTimers())
+ expect(queryByText('...')).toBeNull()
})
it('should display error messages if error occurs', () => {