Skip to content

feat: add messageArchive #369

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 13, 2025
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
673 changes: 360 additions & 313 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rustic-ai/ui-components",
"version": "0.0.45",
"version": "0.0.46",
"keywords": [
"rustic-ai",
"conversational-ui"
Expand Down
4 changes: 2 additions & 2 deletions src/components/elementRenderer/elementRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import React from 'react'
import type { ComponentMap, Message, Sender, WebSocketClient } from '../types'

interface ElementRendererProps {
sender: Sender
ws: WebSocketClient
sender?: Sender
ws?: WebSocketClient
messages: Message[]
supportedElements: ComponentMap
}
Expand Down
2 changes: 2 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import MarkedStreamingMarkdown from './markdown/markedStreamingMarkdown'
import Sound from './media/audio/sound'
import Video from './media/video/video'
import PopoverMenu from './menu/popoverMenu'
import MessageArchive from './messageArchive/messageArchive'
import CopyText from './messageCanvas/actions/copy/copyText'
import Action from './messageCanvas/actions/index'
import TextToSpeech from './messageCanvas/actions/textToSpeech/textToSpeech'
Expand Down Expand Up @@ -47,6 +48,7 @@ export {
MarkedMarkdown,
MarkedStreamingMarkdown,
MermaidViz,
MessageArchive,
MessageCanvas,
MessageSpace,
MultimodalInput,
Expand Down
5 changes: 5 additions & 0 deletions src/components/messageArchive/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import './messageArchive.css'

import MessageArchive from './messageArchive'

export default MessageArchive
33 changes: 33 additions & 0 deletions src/components/messageArchive/messageArchive.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.rustic-message-archive {
display: flex;
flex-direction: column;
flex: 1;
gap: 16px;
overflow: hidden;
}

.rustic-message-archive-loading {
display: flex;
flex: 1;
justify-content: center;
align-items: center;
}

.rustic-message-archive .rustic-message-container {
display: flex;
flex-direction: column;
flex: 1;
gap: 16px;
overflow: auto;
scroll-behavior: smooth;
scrollbar-width: none;
}

.rustic-message-archive .rustic-scroll-down-button {
position: sticky;
bottom: 16px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
width: fit-content;
}
176 changes: 176 additions & 0 deletions src/components/messageArchive/messageArchive.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import 'cypress-real-events'

import { v4 as getUUID } from 'uuid'

import {
botUser,
supportedViewports,
testUser,
} from '../../../cypress/support/variables'
import {
FCCalendar,
Image,
MarkedMarkdown,
MarkedStreamingMarkdown,
type Message,
OpenLayersMap,
StreamingText,
Table,
Text,
UniformsForm,
} from '..'
import Icon from '../icon/icon'
import MessageArchive from './messageArchive'

describe('MessageArchive Component', () => {
const supportedElements = {
text: Text,
streamingText: StreamingText,
markdown: MarkedMarkdown,
streamingMarkdown: MarkedStreamingMarkdown,
image: Image,
map: OpenLayersMap,
table: Table,
calendar: FCCalendar,
form: UniformsForm,
}

const conversationId = '1'

const agentMessageData = {
sender: botUser,
conversationId,
}

const humanMessageData = {
sender: testUser,
conversationId,
}

const messages = [
{
...humanMessageData,
id: getUUID(),
timestamp: '2024-01-02T00:00:00.000Z',
format: 'streamingMarkdown',
data: {
text: 'message 1',
},
},
{
...agentMessageData,
id: getUUID(),
timestamp: '2024-01-02T00:01:00.000Z',
format: 'text',
data: {
text: 'message 2',
},
},
{
...humanMessageData,
id: getUUID(),
timestamp: '2024-01-02T00:12:00.000Z',
format: 'text',
data: {
text: 'message 3',
},
},
]

const messageArchive = '[data-cy=message-archive]'
const messageContainer = '[data-cy=message-container]'

supportedViewports.forEach((viewport) => {
it(`renders correctly with provided messages on ${viewport} screen`, () => {
cy.viewport(viewport)
cy.mount(
<MessageArchive
getHistoricMessages={() => {
return new Promise((resolve) => {
resolve(messages)
})
}}
supportedElements={supportedElements}
getProfileComponent={(message: Message) => {
if (message.sender.name?.includes('Agent')) {
return <Icon name="smart_toy" />
} else {
return <Icon name="account_circle" />
}
}}
/>
)
const messageCanvas = '[data-cy=message-canvas]'
cy.get(messageArchive).should('exist')

messages.forEach((message, index) => {
cy.get(messageArchive).should('contain', message.data.text)
if (message.sender.name?.includes('Agent')) {
cy.get(messageCanvas)
.eq(index)
.within(() => {
cy.get('span[data-cy="smart-toy-icon"]').should('exist')
})
} else {
cy.get(messageCanvas)
.eq(index)
.within(() => {
cy.get('span[data-cy="account-circle-icon"]').should('exist')
})
}
})
})

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

cy.viewport(viewport)
cy.mount(
<div style={{ height: '200px', display: 'flex' }}>
<MessageArchive
getHistoricMessages={() => {
return new Promise((resolve) => {
resolve(messages)
})
}}
supportedElements={supportedElements}
/>
</div>
)

cy.get('p').contains('message 3').should('be.visible')
cy.get(messageArchive).contains('message 1').should('not.be.visible')
cy.get(messageContainer).scrollTo('top', { duration: 500 })
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(waitTime)
cy.get('[data-cy=scroll-down-button]').should('be.visible').realClick()

cy.get(messageArchive).then((messageList) => {
cy.wrap(messageList).within(() => {
cy.contains('message 3').should('be.visible')
})
})
})

it(`displays info message when provided on ${viewport} screen`, () => {
const infoMessageText =
'This is an important notice about this conversation'
cy.viewport(viewport)
cy.mount(
<MessageArchive
getHistoricMessages={() => {
return new Promise((resolve) => {
resolve(messages)
})
}}
supportedElements={supportedElements}
infoMessage={infoMessageText}
/>
)
const infoMessage = '[data-cy=info-message]'

cy.get(infoMessage).should('exist')
cy.get(infoMessage).should('contain', infoMessageText)
})
})
})
Loading