Skip to content

Commit

Permalink
Merge pull request #3078 from digitalfabrik/3028--Show-user-icon-for-…
Browse files Browse the repository at this point in the history
…human-message-in-chat-after-robot

3028: Show user icon after robot message in Chat
  • Loading branch information
hannaseithe authored Feb 18, 2025
2 parents 9b3507b + 9897bc5 commit 824fdb3
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 8 deletions.
4 changes: 4 additions & 0 deletions translations/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -268,12 +268,14 @@
},
"chat": {
"de": {
"bot": "Chatbot",
"button": "Chat (beta)",
"conversationTitle": "Bitte gib deine Frage in das Textfeld ein.",
"conversationText": "Du kannst alles fragen, von lokalen Informationen bis hin zu spezifischen Anfragen bezüglich deiner Situation.",
"dataSecurity": "Datenschutz und Sicherheit: Deine Privatsphäre ist uns sehr wichtig. Alle Gespräche werden vertraulich behandelt und deine Daten werden sicher verarbeitet. Mehr Informationen zum Datenschutz findest du in unseren Datenschutzrichtlinien.",
"errorMessage": "Es ist ein Fehler bei der Kommunikation aufgetreten.",
"header": "{{appName}} Chat Support",
"human": "Berater*in",
"initialMessage": "Deine Chatanfrage wurde gesendet. Die Berater*in beantwortet deine Nachricht so schnell wie möglich.",
"inputLabel": "Meine Frage:",
"inputPlaceholder": "Ich möchte wissen...",
Expand Down Expand Up @@ -380,12 +382,14 @@
"user": "Εσείς"
},
"en": {
"bot": "Chatbot",
"button": "Chat (beta)",
"conversationTitle": "Please enter your question in the text field.",
"conversationText": "You can ask anything from local information to specific requests regarding your situation.",
"dataSecurity": "Data protection and security: Your privacy is very important to us. All conversations are treated confidentially and your data is processed securely. You can find more information on data protection in our privacy policy.",
"errorMessage": "An error has occurred during communication.",
"header": "{{appName}} Chat Support",
"human": "Advisor",
"initialMessage": "Your chat request has been sent. The advisor will answer your message as soon as possible.",
"inputLabel": "My question:",
"inputPlaceholder": "I would like to know...",
Expand Down
7 changes: 7 additions & 0 deletions web/src/__mocks__/react-inlinesvg.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React, { ReactElement } from 'react'

export default ({ src, title, ...props }: { src: string; title: string }): ReactElement => (
<svg id={src} role='img' {...props}>
<title>{title}</title>
</svg>
)
6 changes: 1 addition & 5 deletions web/src/components/ChatConversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,7 @@ const ChatConversation = ({ messages, hasError, className }: ChatConversationPro
<>
{!hasError && <InitialMessage>{t('initialMessage')}</InitialMessage>}
{messages.map((message, index) => (
<ChatMessage
message={message}
key={message.id}
showIcon={messages[index - 1]?.userIsAuthor !== message.userIsAuthor}
/>
<ChatMessage message={message} key={message.id} previousMessage={messages[index - 1]} />
))}
<TypingIndicator isVisible={typingIndicatorVisible} />
<div ref={messagesEndRef} />
Expand Down
9 changes: 6 additions & 3 deletions web/src/components/ChatMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,22 @@ const Circle = styled.div`
font-size: ${props => props.theme.fonts.decorativeFontSizeSmall};
`

type ChatMessageProps = { message: ChatMessageModel; showIcon: boolean }
type ChatMessageProps = { message: ChatMessageModel; previousMessage: ChatMessageModel | undefined }

const getIcon = (userIsAuthor: boolean, isAutomaticAnswer: boolean, t: TFunction<'chat'>): ReactElement => {
if (userIsAuthor) {
return <Circle>{t('user')}</Circle>
}
const icon = isAutomaticAnswer ? ChatBot : ChatPerson
return <Icon src={icon} />
return <Icon src={icon} title={isAutomaticAnswer ? t('bot') : t('human')} />
}

const ChatMessage = ({ message, showIcon }: ChatMessageProps): ReactElement => {
const ChatMessage = ({ message, previousMessage }: ChatMessageProps): ReactElement => {
const { t } = useTranslation('chat')
const { body, userIsAuthor, isAutomaticAnswer } = message
const hasAuthorChanged = message.userIsAuthor !== previousMessage?.userIsAuthor
const hasAutomaticAnswerChanged = message.isAutomaticAnswer !== previousMessage?.isAutomaticAnswer
const showIcon = hasAuthorChanged || hasAutomaticAnswerChanged

return (
<Container $isAuthor={userIsAuthor}>
Expand Down
73 changes: 73 additions & 0 deletions web/src/components/__tests__/ChatConversation.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { renderWithRouterAndTheme } from '../../testing/render'
import ChatConversation from '../ChatConversation'

jest.mock('react-i18next')
jest.mock('react-inlinesvg')

window.HTMLElement.prototype.scrollIntoView = jest.fn()
jest.useFakeTimers()

Expand Down Expand Up @@ -40,6 +42,50 @@ describe('ChatConversation', () => {
automaticAnswer: false,
}),
]
const testMessages2: ChatMessageModel[] = [
new ChatMessageModel({
id: 1,
body: 'Human Message 1',
userIsAuthor: false,
automaticAnswer: false,
}),
new ChatMessageModel({
id: 2,
body: 'Bot Message 1',
userIsAuthor: false,
automaticAnswer: true,
}),
new ChatMessageModel({
id: 3,
body: 'User Message 1',
userIsAuthor: true,
automaticAnswer: false,
}),
new ChatMessageModel({
id: 4,
body: 'Human Message 2',
userIsAuthor: false,
automaticAnswer: false,
}),
new ChatMessageModel({
id: 5,
body: 'Human Message 3',
userIsAuthor: false,
automaticAnswer: false,
}),
new ChatMessageModel({
id: 6,
body: 'Bot Message 2',
userIsAuthor: false,
automaticAnswer: true,
}),
new ChatMessageModel({
id: 7,
body: 'Bot Message 3',
userIsAuthor: false,
automaticAnswer: true,
}),
]

it('should display welcome text if conversation has not started', () => {
const { getByText } = render([], false)
Expand All @@ -59,6 +105,7 @@ describe('ChatConversation', () => {
expect(getByTestId(testMessages[0]!.id)).toBeTruthy()
expect(getByText('...')).toBeTruthy()
expect(getByTestId(testMessages[1]!.id)).toBeTruthy()
expect(getByText('chat:human')).toBeTruthy()
expect(getByText('...')).toBeTruthy()

act(() => jest.runAllTimers())
Expand All @@ -76,4 +123,30 @@ describe('ChatConversation', () => {
const { getByText } = render([], true)
expect(getByText('chat:errorMessage')).toBeTruthy()
})

it('should display icon after automaticAnswer or author changes', () => {
const expectedResults = [
{ icon: 'human', text: 'Human Message 1', opacity: '1' },
{ icon: 'bot', text: 'Bot Message 1', opacity: '1' },
{ icon: 'human', text: 'Human Message 2', opacity: '1' },
{ icon: 'human', text: 'Human Message 3', opacity: '0' },
{ icon: 'bot', text: 'Bot Message 2', opacity: '1' },
{ icon: 'bot', text: 'Bot Message 3', opacity: '0' },
]

const { getAllByRole } = render(testMessages2, false)
const icons = getAllByRole('img')

expect(icons).toHaveLength(6)

icons.forEach((icon, index) => {
const expected = expectedResults[index]!
const parent = icon.parentElement
const grandparent = parent?.parentElement

expect(icon.textContent).toMatch(expected.icon)
expect(grandparent?.textContent).toMatch(expected.text)
expect(parent).toHaveStyle(`opacity: ${expected.opacity}`)
})
})
})

0 comments on commit 824fdb3

Please sign in to comment.