Skip to content
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
45120ed
refactor(MessageFeed): convert to CompoundComponent and temp remove a…
shaneeza Jan 27, 2026
1277ed5
Merge branch 's/initial-message-integration' of github.com:mongodb/le…
shaneeza Jan 27, 2026
defdad3
refactor(MessageFeed): enhance component structure
shaneeza Jan 27, 2026
958d58a
feat(MessageFeed): add MessageFeedContext
shaneeza Jan 27, 2026
c701f84
docs(MessageFeed): add changeset
shaneeza Jan 27, 2026
a6d7616
fix(MessageFeed): update error message for context provider
shaneeza Jan 27, 2026
e427185
refactor(Message): undo message changes
shaneeza Jan 27, 2026
0d9295d
chore(MessageFeed): add compound-component dependency
shaneeza Jan 27, 2026
eb31dd4
chore(pnpm-lock): add compound-component to dependencies
shaneeza Jan 27, 2026
fbb16db
chore(MessageFeed): update dependencies for compound-component integr…
shaneeza Jan 27, 2026
478ee3c
refactor(MessageFeed): move shared types
shaneeza Jan 27, 2026
43fc610
Merge branch 'LG-5932-message-feed-compound' of github.com:mongodb/le…
shaneeza Jan 27, 2026
8b67331
refactor(InitialMessage): update import path for shared types and use…
shaneeza Jan 27, 2026
8926ff5
feat(InitialMessage): integrate MessageFeedContext to manage initial …
shaneeza Jan 28, 2026
bd83d5f
chore(changeset): update dependency version for @lg-chat/message-feed
shaneeza Jan 28, 2026
6441471
fix(tsconfig): add missing newline at end of file
shaneeza Jan 28, 2026
9e0a42e
fix(MessageFeedContext): handle error boundary for React 17 in contex…
shaneeza Jan 28, 2026
bebc9bf
Merge branch 'LG-5932-message-feed-compound' of github.com:mongodb/le…
shaneeza Jan 28, 2026
7dcc812
feat(InitialMessage): implement styles and update component structure…
shaneeza Jan 28, 2026
3516dd6
feat(InitialMessage): enhance initial message component with structur…
shaneeza Jan 28, 2026
87414ee
refactor(InitialMessage): replace hardcoded title and description wit…
shaneeza Jan 28, 2026
7caaef2
feat(ChatWindow): add initial message prompts and enhance message han…
shaneeza Jan 28, 2026
e41b22b
refactor(ChatWindow): remove enableHideOnSelect prop from Suggested P…
shaneeza Jan 28, 2026
5f6e619
test(InitialMessage): add unit tests for accessibility, rendering, an…
shaneeza Jan 28, 2026
1d87c3b
merge conflict
shaneeza Jan 29, 2026
1ef0ae9
test(MessageFeed): enhance tests with scrollTo mock and update query …
shaneeza Jan 29, 2026
d4adca5
refactor(InitialMessage): simplify getWrapperStyles function and remo…
shaneeza Jan 29, 2026
52846e0
chore(MessageFeed): update dependencies and tsconfig to include new L…
shaneeza Jan 29, 2026
eb70b3f
refactor(ChatWindow): rename initial message component and update pro…
shaneeza Jan 29, 2026
a33deba
chore(MessageFeed): remove unused @leafygreen-ui/hooks dependency fro…
shaneeza Jan 29, 2026
d7f10d4
refactor(MessageFeed): remove commented-out MyMessage component from …
shaneeza Jan 29, 2026
298ffc1
chore(MessageFeed): update changeset
shaneeza Jan 29, 2026
770c81f
refactor(InitialMessage): update styles for title and description com…
shaneeza Jan 29, 2026
5e005d2
refactor(InitialMessage): adjust inner wrapper styles with focus ring…
shaneeza Jan 29, 2026
bce10f3
refactor(InitialMessage): restructure inner wrapper and remove descri…
shaneeza Jan 30, 2026
c075973
refactor(InitialMessage): integrate LeafyGreenProvider for dark mode …
shaneeza Jan 30, 2026
9a145bc
refactor(InitialMessage): remove LeafyGreenProvider and streamline co…
shaneeza Jan 30, 2026
499fc40
feat(InitialMessage): add generated story for initialMessage
shaneeza Jan 30, 2026
338cbec
refactor(InitialMessage): update AssistantAvatar size and adjust prop…
shaneeza Jan 30, 2026
9db8c4d
fix(InitialMessage): export InitialMessageProps type for better type …
shaneeza Jan 30, 2026
748ef93
refactor(InitialMessage): Components to components
shaneeza Jan 30, 2026
da60688
refactor(InitialMessage): update import paths from Components to comp…
shaneeza Jan 30, 2026
730e077
feat(InitialMessage): add interactive story for message addition with…
shaneeza Feb 2, 2026
308b6b5
fix(InitialMessage): add visibility property to transition styles for…
shaneeza Feb 2, 2026
9572937
feat(MessageFeed): enhance InitialMessage stories with new message ha…
shaneeza Feb 2, 2026
72f8c20
refactor(MessageFeed): simplify initial message handling by removing …
shaneeza Feb 2, 2026
abf16ff
refactor(MessageFeed): export InitialMessageProps type for better acc…
shaneeza Feb 2, 2026
e4afa54
chore(MessageFeed): disable Chromatic snapshots for InitialMessage st…
shaneeza Feb 3, 2026
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
3 changes: 2 additions & 1 deletion .changeset/dark-pots-tell.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
---

- [LG-5932](https://jira.mongodb.org/browse/LG-5932): Refactor to use `CompoundComponent` pattern
- [LG-5934](https://jira.mongodb.org/browse/LG-5934): add `MessageFeedProvider` and `useMessageFeedContext`
- [LG-5934](https://jira.mongodb.org/browse/LG-5934): add `MessageFeedProvider` and `useMessageFeedContext`
- [LG-5935](https://jira.mongodb.org/browse/LG-5935): add `MessageFeed.InitialMessage` component
98 changes: 97 additions & 1 deletion chat/chat-window/src/ChatWindow.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,6 @@ const WithMessagePromptsComponent = ({
<LeafyGreenChatProvider assistantName={assistantName}>
<ChatWindow {...props}>
<MessageFeed>
<div style={{ flex: 1 }} aria-hidden="true" />
{messages.map((messageFields, index) => (
<Message
key={messageFields.id}
Expand Down Expand Up @@ -263,6 +262,103 @@ export const WithMessagePrompts: StoryObj<ChatWindowStoryProps> = {
},
};

const WithInitialMessageWithMessagePromptsComponent = ({
assistantName,
...props
}: ChatWindowStoryProps) => {
const [messages, setMessages] = useState<Array<any>>([]);
const [selectedPromptIndex, setSelectedPromptIndex] = useState<
number | undefined
>();

const prompts = [
'What is MongoDB?',
'How do I create a database?',
'Can you explain indexes?',
];

const handlePromptSelect = (index: number) => {
setSelectedPromptIndex(index);
const selectedPrompt = prompts[index];

// Add user message with selected prompt
const userMessage = {
id: messages.length,
messageBody: selectedPrompt,
isSender: true,
};

// Add assistant response
const assistantMessage = {
id: messages.length + 1,
messageBody: `Great question! Let me explain about "${selectedPrompt}"...`,
isSender: false,
};

setMessages(prev => [...prev, userMessage, assistantMessage]);
};

const handleMessageSend = (messageBody: string) => {
const newMessage = {
id: messages.length,
messageBody,
isSender: true,
};
setMessages(prev => [...prev, newMessage]);
};

return (
<LeafyGreenChatProvider assistantName={assistantName}>
<ChatWindow {...props}>
<MessageFeed>
<MessageFeed.InitialMessage>
{/* TODO: will replace with MessageFeed.MessagePrompts in next PR */}
<MessagePrompts
label="Suggested Prompts"
onClickRefresh={() => {
// eslint-disable-next-line no-console
console.log('Refresh prompts');
setSelectedPromptIndex(undefined);
}}
enableHideOnSelect={false}
>
{prompts.map((prompt, promptIndex) => (
<MessagePrompt
key={prompt}
selected={selectedPromptIndex === promptIndex}
onClick={() => handlePromptSelect(promptIndex)}
data-testid={`prompt-${promptIndex}`}
>
{prompt}
</MessagePrompt>
))}
</MessagePrompts>
</MessageFeed.InitialMessage>
{messages.map(messageFields => (
<Message
key={messageFields.id}
sourceType="markdown"
isSender={messageFields.isSender}
messageBody={messageFields.messageBody}
/>
))}
</MessageFeed>
<InputBar onMessageSend={handleMessageSend} />
</ChatWindow>
</LeafyGreenChatProvider>
);
};

export const WithInitialMessageWithMessagePrompts: StoryObj<ChatWindowStoryProps> =
{
render: WithInitialMessageWithMessagePromptsComponent,
parameters: {
chromatic: {
disableSnapshot: true,
},
},
};

const ChatDrawerContent = ({ assistantName }: { assistantName?: string }) => {
const [messages, setMessages] = useState<Array<any>>(baseMessages);

Expand Down
2 changes: 2 additions & 0 deletions chat/message-feed/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
"access": "public"
},
"dependencies": {
"@leafygreen-ui/avatar": "workspace:^",
"@leafygreen-ui/button": "workspace:^",
"@leafygreen-ui/compound-component": "workspace:^",
"@leafygreen-ui/emotion": "workspace:^",
"@leafygreen-ui/icon": "workspace:^",
"@leafygreen-ui/lib": "workspace:^",
"@leafygreen-ui/palette": "workspace:^",
"@leafygreen-ui/tokens": "workspace:^",
"@leafygreen-ui/typography": "workspace:^",
"@lg-chat/message": "workspace:^",
"@lg-chat/message-rating": "workspace:^",
"react-intersection-observer": "^8.25.1",
Expand Down
103 changes: 103 additions & 0 deletions chat/message-feed/src/MessageFeed.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { MessagePrompt, MessagePrompts } from '@lg-chat/message-prompts';
import { MessageRating } from '@lg-chat/message-rating';
import { storybookArgTypes, StoryMetaType } from '@lg-tools/storybook-utils';
import { StoryFn, StoryObj } from '@storybook/react';
import { expect, userEvent, waitFor, within } from '@storybook/test';

import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider';

import { InitialMessageProps } from './components/InitialMessage/InitialMessage.types';
import {
baseMessages,
type MessageFields,
Expand Down Expand Up @@ -161,6 +163,107 @@ const ChangingMessagesComponent = ({ darkMode, ...rest }: MessageFeedProps) => {
);
};

export const InitialMessage = ({ ...rest }: MessageFeedProps) => {
return (
<div>
<MessageFeed style={{ width: 700, height: 400 }} {...rest}>
<MessageFeed.InitialMessage>
Filler content for initial message
</MessageFeed.InitialMessage>
</MessageFeed>
</div>
);
};

export const InitialMessageWithMessages = ({ ...rest }: MessageFeedProps) => {
return (
<div>
<MessageFeed style={{ width: 700, height: 400 }} {...rest}>
<MessageFeed.InitialMessage>
Filler content for initial message
</MessageFeed.InitialMessage>
{baseMessages.slice(0, 2).map(message => {
const { id, messageBody, userName } = message as MessageFields;
return (
<Message
key={id}
sourceType="markdown"
isSender={!!userName}
messageBody={messageBody}
/>
);
})}
</MessageFeed>
</div>
);
};

export const InitialMessageWithNewMessage = ({ ...rest }: MessageFeedProps) => {
const [messages, setMessages] = useState<Array<any>>([]);

const handleButtonClick = () => {
setMessages([...messages, baseMessages[1]]);
};

return (
<div>
<MessageFeed style={{ width: 700, height: 400 }} {...rest}>
<MessageFeed.InitialMessage>
Filler content for initial message
</MessageFeed.InitialMessage>
{messages.map(message => {
const { id, messageBody, userName } = message as MessageFields;
return (
<Message
key={id}
sourceType="markdown"
isSender={!!userName}
messageBody={messageBody}
/>
);
})}
</MessageFeed>
<button
data-testid="add-message-button"
onClick={() => handleButtonClick()}
>
Click me to add a message
</button>
</div>
);
};

export const ChangingMessages: StoryObj<MessageFeedProps> = {
render: ChangingMessagesComponent,
};

export const InitialMessageTransition: StoryObj<InitialMessageProps> = {
render: InitialMessageWithNewMessage,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

expect(
canvas.getByText('Filler content for initial message'),
).toBeVisible();

// Click add message button
const addMessageButton = canvas.getByTestId('add-message-button');

// Click the message button to add a message
await userEvent.click(addMessageButton);

await waitFor(() =>
expect(
canvas.getByText('Filler content for initial message'),
).not.toBeVisible(),
);
await waitFor(() =>
expect(canvas.getByText(baseMessages[1].messageBody)).toBeVisible(),
);
},
parameters: {
chromatic: {
delay: 300,
},
},
};
121 changes: 121 additions & 0 deletions chat/message-feed/src/MessageFeed/MessageFeed.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import React from 'react';
import { render, screen } from '@testing-library/react';

import {
INITIAL_MESSAGE_DESCRIPTION,
INITIAL_MESSAGE_TITLE,
} from '../components/InitialMessage/constants';

import { MessageFeed } from './MessageFeed';

jest.mock('@lg-chat/lg-markdown', () => ({
LGMarkdown: jest.fn(({ children }) => <div>{children}</div>),
}));

describe('MessageFeed', () => {
const originalScrollTo = Element.prototype.scrollTo;

beforeAll(() => {
// Mock scrollTo since it's not implemented in JSDOM
Element.prototype.scrollTo = jest.fn(function mock(
this: Element,
x?: number | ScrollToOptions,
y?: number,
) {
if (typeof x === 'object' && x !== null) {
// Handle ScrollToOptions
if ('top' in x) {
(this as HTMLElement).scrollTop = x.top ?? 0;
}

if ('left' in x) {
(this as HTMLElement).scrollLeft = x.left ?? 0;
}
} else if (typeof x === 'number' && typeof y === 'number') {
// Handle two number arguments
(this as HTMLElement).scrollLeft = x;
(this as HTMLElement).scrollTop = y;
}
});
});

afterAll(() => {
// Restore original scrollTo if it existed, otherwise delete the mock
if (originalScrollTo) {
Element.prototype.scrollTo = originalScrollTo;
} else {
delete (Element.prototype as Partial<Element>).scrollTo;
}
});

test('renders children', () => {
render(
<MessageFeed>
<div>Hello, how can I help you today?</div>
<div>Hello, fellow message</div>
</MessageFeed>,
);
expect(
screen.getByText('Hello, how can I help you today?'),
).toBeInTheDocument();
expect(screen.getByText('Hello, fellow message')).toBeInTheDocument();
});

test('renders the initial message if the initial message is a child and there are no other children', () => {
render(
<MessageFeed>
<MessageFeed.InitialMessage>
<div>I heard you like MongoDB</div>
</MessageFeed.InitialMessage>
</MessageFeed>,
);
expect(screen.getByText(INITIAL_MESSAGE_TITLE)).toBeInTheDocument();
expect(screen.getByText(INITIAL_MESSAGE_DESCRIPTION)).toBeInTheDocument();
expect(screen.getByText('I heard you like MongoDB')).toBeInTheDocument();
});

test('hides the initial message if the initial message is a child and there are other children', () => {
render(
<MessageFeed>
<MessageFeed.InitialMessage>
<div>I heard you like MongoDB</div>
</MessageFeed.InitialMessage>
<div>Hello, fellow message</div>
</MessageFeed>,
);
expect(screen.queryByText(INITIAL_MESSAGE_TITLE)).not.toBeVisible();
expect(screen.queryByText(INITIAL_MESSAGE_DESCRIPTION)).not.toBeVisible();
expect(screen.queryByText('I heard you like MongoDB')).not.toBeVisible();
expect(screen.getByText('Hello, fellow message')).toBeInTheDocument();
});

test('hides the initial message when a new message is added', () => {
const { rerender } = render(
<MessageFeed>
<MessageFeed.InitialMessage>
<div>I heard you like MongoDB</div>
</MessageFeed.InitialMessage>
</MessageFeed>,
);

expect(screen.getByText(INITIAL_MESSAGE_TITLE)).toBeInTheDocument();
expect(screen.getByText(INITIAL_MESSAGE_DESCRIPTION)).toBeInTheDocument();
expect(screen.getByText('I heard you like MongoDB')).toBeInTheDocument();

rerender(
<MessageFeed>
<MessageFeed.InitialMessage>
<div>I heard you like MongoDB</div>
</MessageFeed.InitialMessage>
<div>Hello, how can I help you today?</div>
</MessageFeed>,
);

expect(screen.getByText(INITIAL_MESSAGE_TITLE)).not.toBeVisible();
expect(screen.getByText(INITIAL_MESSAGE_DESCRIPTION)).not.toBeVisible();
expect(screen.getByText('I heard you like MongoDB')).not.toBeVisible();
expect(
screen.getByText('Hello, how can I help you today?'),
).toBeInTheDocument();
});
});
Loading
Loading