Skip to content
Open
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
20 changes: 11 additions & 9 deletions src/components/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,19 +202,21 @@ export function toChatRequest(
}
}

export function getMessageIdentifier(message: Message): string {
if (message.format.startsWith('update') && message.data?.updateId) {
return message.data.updateId
} else if (message.format.includes('Response') && message.data?.inReplyTo) {
return message.data.inReplyTo
} else {
return message.id
}
}

export function getCombinedMessages(
messages: { [key: string]: Message[] },
message: Message
) {
let key

if (message.format.startsWith('update')) {
key = message.data.updateId
} else if (message.format.includes('Response')) {
key = message.data.inReplyTo
} else {
key = message.id
}
let key = getMessageIdentifier(message)

if (!key) {
return messages
Expand Down
6 changes: 5 additions & 1 deletion src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,11 @@ export {
}

export * from '../rusticTheme'
export { formatDateAndTime, getCombinedMessages } from './helper'
export {
formatDateAndTime,
getCombinedMessages,
getMessageIdentifier,
} from './helper'
export type * from './types'
export { ParticipantRole, ParticipantType } from './types'
export type * from './visualization/mermaidViz/mermaidViz.types'
Expand Down
1 change: 1 addition & 0 deletions src/components/input/baseInput/baseInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ function BaseInputElement(
data: { text: messageText },
inReplyTo: props.lastMsg?.id,
messageHistory: props.lastMsg?.messageHistory,
...(props.threads && { threads: props.threads }),
}

props.send(formattedMessage)
Expand Down
255 changes: 254 additions & 1 deletion src/components/messageArchive/messageArchive.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ describe('MessageArchive Component', () => {
})
})

it.only(`scrolls to bottom when "Go to bottom" button is clicked on ${viewport} screen`, () => {
it(`scrolls to bottom when "Go to bottom" button is clicked on ${viewport} screen`, () => {
const waitTime = 500

cy.viewport(viewport)
Expand Down Expand Up @@ -171,5 +171,258 @@ describe('MessageArchive Component', () => {
cy.get(infoMessage).should('exist')
cy.get(infoMessage).should('contain', infoMessageText)
})

it(`displays thread reply count when threadMessages are provided on ${viewport} screen`, () => {
const message1Id = 'message-1'
const message2Id = 'message-2'

const threadMessages = {
[message1Id]: [
{
...humanMessageData,
id: getUUID(),
timestamp: '2024-01-02T00:02:00.000Z',
format: 'TextFormat',
data: {
text: 'Thread reply 1',
},
},
{
...agentMessageData,
id: getUUID(),
timestamp: '2024-01-02T00:03:00.000Z',
format: 'TextFormat',
data: {
text: 'Thread reply 2',
},
},
],
[message2Id]: [
{
...humanMessageData,
id: getUUID(),
timestamp: '2024-01-02T00:14:00.000Z',
format: 'TextFormat',
data: {
text: 'Single thread reply',
},
},
],
}

cy.viewport(viewport)
cy.mount(
<MessageArchive
getHistoricMessages={() => {
return new Promise((resolve) => {
resolve([
{
...humanMessageData,
id: message1Id,
timestamp: '2024-01-02T00:00:00.000Z',
format: 'TextFormat',
data: {
text: 'First message with threads',
},
},
{
...agentMessageData,
id: message2Id,
timestamp: '2024-01-02T00:13:00.000Z',
format: 'TextFormat',
data: {
text: 'Second message with thread',
},
},
])
})
}}
threadMessages={threadMessages}
supportedElements={supportedElements}
/>
)

const expectedThreadCount = 2
cy.get('.rustic-thread-reply-count').should(
'have.length',
expectedThreadCount
)
cy.get('.rustic-thread-reply-count').first().should('contain', '2')
cy.get('.rustic-thread-reply-count').first().should('contain', 'replies')
cy.get('.rustic-thread-reply-count').last().should('contain', '1')
cy.get('.rustic-thread-reply-count').last().should('contain', 'reply')
})

it(`calls onThreadOpen when thread reply count is clicked on ${viewport} screen`, () => {
const onThreadOpen = cy.stub()
const message1Id = 'message-1'

const threadMessages = {
[message1Id]: [
{
...humanMessageData,
id: getUUID(),
timestamp: '2024-01-02T00:02:00.000Z',
format: 'TextFormat',
data: {
text: 'Thread reply',
},
},
],
}

cy.viewport(viewport)
cy.mount(
<MessageArchive
getHistoricMessages={() => {
return new Promise((resolve) => {
resolve([
{
...humanMessageData,
id: message1Id,
timestamp: '2024-01-02T00:00:00.000Z',
format: 'TextFormat',
data: {
text: 'Message with thread',
},
},
])
})
}}
threadMessages={threadMessages}
onThreadOpen={onThreadOpen}
supportedElements={supportedElements}
/>
)

cy.get('.rustic-thread-reply-count').click()
cy.wrap(onThreadOpen).should('be.calledWith', message1Id)
})

it(`highlights active thread message on ${viewport} screen`, () => {
const message1Id = 'message-1'
const message2Id = 'message-2'

const threadMessages = {
[message1Id]: [
{
...humanMessageData,
id: getUUID(),
timestamp: '2024-01-02T00:02:00.000Z',
format: 'TextFormat',
data: {
text: 'Thread reply',
},
},
],
[message2Id]: [
{
...humanMessageData,
id: getUUID(),
timestamp: '2024-01-02T00:14:00.000Z',
format: 'TextFormat',
data: {
text: 'Thread reply',
},
},
],
}

cy.viewport(viewport)
cy.mount(
<MessageArchive
getHistoricMessages={() => {
return new Promise((resolve) => {
resolve([
{
...humanMessageData,
id: message1Id,
timestamp: '2024-01-02T00:00:00.000Z',
format: 'TextFormat',
data: {
text: 'First message',
},
},
{
...agentMessageData,
id: message2Id,
timestamp: '2024-01-02T00:13:00.000Z',
format: 'TextFormat',
data: {
text: 'Second message',
},
},
])
})
}}
threadMessages={threadMessages}
activeThreadId={message1Id}
supportedElements={supportedElements}
/>
)

const messageCanvas = '[data-cy=message-canvas]'
cy.get(messageCanvas)
.first()
.find('.rustic-message-container')
.should('have.css', 'background-color')
.and('not.equal', 'rgb(255, 255, 255)')
})

it(`handles thread messages with update messages on ${viewport} screen`, () => {
const updateId = 'update-message-1'

const threadMessages = {
[updateId]: [
{
...humanMessageData,
id: getUUID(),
timestamp: '2024-01-02T00:02:00.000Z',
format: 'TextFormat',
data: {
text: 'Thread reply to update message',
},
},
],
}

cy.viewport(viewport)
cy.mount(
<MessageArchive
getHistoricMessages={() => {
return new Promise((resolve) => {
resolve([
{
...agentMessageData,
id: getUUID(),
timestamp: '2024-01-02T00:00:00.000Z',
format: 'updateMarkdownFormat',
data: {
text: 'First part',
updateId: updateId,
},
},
{
...agentMessageData,
id: getUUID(),
timestamp: '2024-01-02T00:01:00.000Z',
format: 'updateMarkdownFormat',
data: {
text: ' of message',
updateId: updateId,
},
},
])
})
}}
threadMessages={threadMessages}
supportedElements={supportedElements}
/>
)

cy.get('.rustic-thread-reply-count').should('have.length', 1)
cy.get('.rustic-thread-reply-count').should('contain', '1')
cy.get('.rustic-thread-reply-count').should('contain', 'reply')
})
})
})
19 changes: 17 additions & 2 deletions src/components/messageArchive/messageArchive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Box from '@mui/system/Box'
import React, { type ReactNode, useEffect, useRef, useState } from 'react'

import ElementRenderer from '../elementRenderer/elementRenderer'
import { getCombinedMessages } from '../helper'
import { getCombinedMessages, getMessageIdentifier } from '../helper'
import Icon from '../icon/icon'
import MessageCanvas, {
type MessageContainerProps,
Expand All @@ -29,6 +29,10 @@ export interface MessageArchiveProps extends MessageContainerProps {
disableAutoScroll?: boolean
/** If true, disables the scroll down button */
disableScrollButton?: boolean
/** The ID of the active thread message */
activeThreadId?: string
/** A record mapping message IDs to their thread messages */
threadMessages?: Record<string, Message[]>
}

/**
Expand Down Expand Up @@ -196,10 +200,16 @@ export default function MessageArchive({
{Object.keys(chatMessages).map((key, index) => {
const messages = chatMessages[key]
const latestMessage = messages[messages.length - 1]
const firstMessage = messages[0]
const hasResponse = latestMessage.format.includes('Response')
const inReplyTo = hasResponse && {
inReplyTo: messages[0],
inReplyTo: firstMessage,
}

const messageIdentifier = getMessageIdentifier(firstMessage)
const threadReplies = props.threadMessages?.[messageIdentifier]
const threadReplyCount = threadReplies?.length

return (
<MessageCanvas
key={key}
Expand All @@ -208,6 +218,11 @@ export default function MessageArchive({
getActionsComponent={props.getActionsComponent}
getProfileComponent={props.getProfileComponent}
ref={index === currentMessagesLength - 1 ? scrollEndRef : null}
{...(threadReplies && {
threadReplyCount: threadReplyCount,
onThreadOpen: () => props.onThreadOpen?.(messageIdentifier),
isActiveThread: props.activeThreadId === messageIdentifier,
})}
>
<ElementRenderer
messages={hasResponse ? [messages[0]] : messages}
Expand Down
17 changes: 17 additions & 0 deletions src/components/messageCanvas/messageCanvas.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@
justify-content: flex-end;
}

.rustic-message-canvas .rustic-thread-reply-icon {
font-size: 14px;
}

.rustic-message-canvas .rustic-thread-reply-count {
display: flex;
align-items: center;
align-self: flex-end;
gap: 8px;
cursor: pointer;
width: fit-content;
}

@media (max-width: 900px) {
.rustic-message-canvas .rustic-message-actions-container {
bottom: -20px;
Expand All @@ -41,4 +54,8 @@
flex: 1;
justify-content: flex-end;
}

.rustic-message-canvas .rustic-thread-reply-count {
align-self: flex-start;
}
}
Loading