Skip to content
Merged
Show file tree
Hide file tree
Changes from 38 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { axe } from 'jest-axe';

import { MessageFeedProvider } from '../../MessageFeedContext';

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

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

const renderInitialMessage = ({
shouldHideInitialMessage = false,
...rest
}: InitialMessageProps & { shouldHideInitialMessage?: boolean }) => {
return render(
<MessageFeedProvider shouldHideInitialMessage={shouldHideInitialMessage}>
<InitialMessage {...rest} />
</MessageFeedProvider>,
);
};

describe('InitialMessage', () => {
describe('a11y', () => {
test('does not have basic accessibility issues', async () => {
const { container } = renderInitialMessage({});
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});

test('renders the title and description', () => {
renderInitialMessage({});
expect(screen.getByText(INITIAL_MESSAGE_TITLE)).toBeInTheDocument();
expect(screen.getByText(INITIAL_MESSAGE_DESCRIPTION)).toBeInTheDocument();
});

test('renders the children', () => {
renderInitialMessage({
children: <div>I heard you like MongoDB</div>,
});
expect(screen.getByText('I heard you like MongoDB')).toBeInTheDocument();
});

test('is hidden when the shouldHideInitialMessage is true', () => {
renderInitialMessage({
shouldHideInitialMessage: true,
children: <div>I heard you like MongoDB</div>,
});
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();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import { storybookArgTypes, StoryMetaType } from '@lg-tools/storybook-utils';
import { StoryFn } from '@storybook/react';

import { MessageFeedProvider } from '../../MessageFeedContext';

import { InitialMessage, InitialMessageProps } from '.';

const meta: StoryMetaType<typeof InitialMessage> = {
title: 'Composition/Chat/MessageFeed/InitialMessage',
component: InitialMessage,
parameters: {
default: 'LiveExample',
generate: {
combineArgs: {
darkMode: [false, true],
},
decorator: Instance => {
return (
<MessageFeedProvider shouldHideInitialMessage={false}>
<Instance />
</MessageFeedProvider>
);
},
},
},
argTypes: {
darkMode: storybookArgTypes.darkMode,
},
args: {
children: <div>I heard you like MongoDB</div>, // TODO: will replace with a real content in the next PR
},
decorators: [
Instance => (
<MessageFeedProvider shouldHideInitialMessage={false}>
<Instance />
</MessageFeedProvider>
),
],
};

export default meta;

const Template: StoryFn<InitialMessageProps> = props => (
<InitialMessage {...props} />
);

export const LiveExample = {
render: Template,
args: {},
};

export const Generated = () => {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { css, cx } from '@leafygreen-ui/emotion';
import { spacing, transitionDuration, typeScales } from '@leafygreen-ui/tokens';

const FOCUS_RING_WIDTH = 4;

const baseOuterWrapperStyles = css`
display: grid;
grid-template-rows: 1fr;
gap: ${spacing[200]}px;
`;

const transitionStyles = css`
transform-origin: top left;
transition-property: grid-template-rows, opacity, transform;
transition-duration: ${transitionDuration.slower}ms;
transition-timing-function: ease-out;
`;

const hiddenWrapperStyles = css`
grid-template-rows: 0fr;
opacity: 0;
transform: scale(0.8);
`;

export const getWrapperStyles = ({ shouldHide }: { shouldHide: boolean }) =>
cx(baseOuterWrapperStyles, transitionStyles, {
[hiddenWrapperStyles]: shouldHide,
});

export const titleStyles = css`
font-size: ${typeScales.body2.fontSize}px;
line-height: ${typeScales.body2.lineHeight}px;
`;

export const innerWrapperStyles = css`
overflow: hidden;
margin: -${FOCUS_RING_WIDTH}px;
padding: ${FOCUS_RING_WIDTH}px;
display: flex;
flex-direction: column;
gap: ${spacing[400]}px;
`;
58 changes: 58 additions & 0 deletions chat/message-feed/src/Components/InitialMessage/InitialMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { forwardRef } from 'react';
import { Message } from '@lg-chat/message';

import { AssistantAvatar } from '@leafygreen-ui/avatar';
import { CompoundSubComponent } from '@leafygreen-ui/compound-component';
import { Body } from '@leafygreen-ui/typography';

import { useMessageFeedContext } from '../../MessageFeedContext';
import { MessageFeedSubcomponentProperty } from '../../shared.types';

import {
INITIAL_MESSAGE_DESCRIPTION,
INITIAL_MESSAGE_TITLE,
} from './constants';
import {
getWrapperStyles,
innerWrapperStyles,
titleStyles,
} from './InitialMessage.styles';
import { type InitialMessageProps } from './InitialMessage.types';
/**
* Renders an initial message in the message feed.
*
* @returns The rendered initial message component.
*/
export const InitialMessage = CompoundSubComponent(
// eslint-disable-next-line react/display-name
forwardRef<HTMLDivElement, InitialMessageProps>(
({ children, ...rest }, fwdRef) => {
const { shouldHideInitialMessage } = useMessageFeedContext();

return (
<Message sourceType="markdown" isSender={false} ref={fwdRef} {...rest}>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we don't actually use the messageBody, we don't need to set sourceType or markdownProps

<div
className={getWrapperStyles({
shouldHide: shouldHideInitialMessage,
})}
>
<div className={innerWrapperStyles}>
<div>
<AssistantAvatar size={20} />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<AssistantAvatar size={20} />
<AssistantAvatar size={AvatarSize.Large} />

<Body weight="semiBold" className={titleStyles}>
{INITIAL_MESSAGE_TITLE}
</Body>
<Body>{INITIAL_MESSAGE_DESCRIPTION}</Body>
</div>
{children}
</div>
</div>
</Message>
);
},
),
{
displayName: 'InitialMessage',
key: MessageFeedSubcomponentProperty.InitialMessage,
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { MessageProps } from '@lg-chat/message';

export interface InitialMessageProps
extends Omit<MessageProps, 'messageBody' | 'isSender'> {}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we also omit markdownProps and sourceType? they don't seem to be relevant to this case

3 changes: 3 additions & 0 deletions chat/message-feed/src/Components/InitialMessage/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const INITIAL_MESSAGE_TITLE = 'Hello! How can I help you?';
export const INITIAL_MESSAGE_DESCRIPTION =
"I'm here to give expert guidance and recommendations for all things MongoDB.";
2 changes: 2 additions & 0 deletions chat/message-feed/src/Components/InitialMessage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { InitialMessage } from './InitialMessage';
export { type InitialMessageProps } from './InitialMessage.types';
1 change: 1 addition & 0 deletions chat/message-feed/src/Components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { InitialMessage } from './InitialMessage';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: lowercase /components subdir name

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also need to export InitialMessageProps type

Loading
Loading