-
Notifications
You must be signed in to change notification settings - Fork 178
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(opentrons-ai-client): Prompt Preview (#16508)
# Overview This PR adds the Prompt Review component that will be consumed by the Create Protocol flow in the new AI Client. ![image](https://github.com/user-attachments/assets/afdf6be4-b675-495c-9f4f-2fdc79a23eaf) ## Test Plan and Hands on Testing - Basic scenarios manually tested - Unit tests ## Changelog - Add Opentrons AI Client Prompt Review component ## Review requests - Validate if this approach for creating components fits the project structure and best practices ## Risk assessment - No risk
- Loading branch information
1 parent
1b0a5a5
commit 908ad7c
Showing
6 changed files
with
416 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
84 changes: 84 additions & 0 deletions
84
opentrons-ai-client/src/molecules/PromptPreview/PromptPreview.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import { I18nextProvider } from 'react-i18next' | ||
import { COLORS, Flex, SPACING } from '@opentrons/components' | ||
import { i18n } from '../../i18n' | ||
import type { Meta, StoryObj } from '@storybook/react' | ||
import { PromptPreview } from '.' | ||
|
||
const meta: Meta<typeof PromptPreview> = { | ||
title: 'AI/molecules/PromptPreview', | ||
component: PromptPreview, | ||
decorators: [ | ||
Story => ( | ||
<I18nextProvider i18n={i18n}> | ||
<Flex | ||
backgroundColor={COLORS.grey10} | ||
padding={SPACING.spacing40} | ||
width={'596px'} | ||
> | ||
<Story /> | ||
</Flex> | ||
</I18nextProvider> | ||
), | ||
], | ||
} | ||
export default meta | ||
type Story = StoryObj<typeof PromptPreview> | ||
|
||
export const PromptPreviewExample: Story = { | ||
args: { | ||
isSubmitButtonEnabled: false, | ||
handleSubmit: () => { | ||
alert('Submit button clicked') | ||
}, | ||
promptPreviewData: [ | ||
{ | ||
title: 'Application', | ||
items: [ | ||
'Cherrypicking', | ||
'I have a Chlorine Reagent Set (Total), Ultra Low Range', | ||
], | ||
}, | ||
{ | ||
title: 'Instruments', | ||
items: [ | ||
'Opentrons Flex', | ||
'Flex 1-Channel 50 uL', | ||
'Flex 8-Channel 1000 uL', | ||
], | ||
}, | ||
{ | ||
title: 'Modules', | ||
items: [ | ||
'Thermocycler GEN2', | ||
'Heater-Shaker with Universal Flat Adaptor', | ||
], | ||
}, | ||
{ | ||
title: 'Labware and Liquids', | ||
items: [ | ||
'Opentrons 96 Well Plate', | ||
'Thermocycler GEN2', | ||
'Opentrons 96 Deep Well Plate', | ||
'Liquid 1: In commodo lectus nec erat commodo blandit. Etiam leo dui, porttitor vel imperdiet sed, tristique nec nisl. Maecenas pulvinar sapien quis sodales imperdiet.', | ||
'Liquid 2: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', | ||
], | ||
}, | ||
{ | ||
title: 'Steps', | ||
items: [ | ||
'Fill the first column of a Elisa plate with 100 uL of Liquid 1', | ||
'Fill the second column of a Elisa plate with 100 uL of Liquid 2', | ||
], | ||
}, | ||
], | ||
}, | ||
} | ||
|
||
export const PromptPreviewPlaceholderMessage: Story = { | ||
args: { | ||
isSubmitButtonEnabled: false, | ||
handleSubmit: () => { | ||
alert('Submit button clicked') | ||
}, | ||
}, | ||
} |
109 changes: 109 additions & 0 deletions
109
opentrons-ai-client/src/molecules/PromptPreview/__tests__/PromptPreview.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import { screen } from '@testing-library/react' | ||
import { describe, it, vi, beforeEach, expect } from 'vitest' | ||
import { renderWithProviders } from '../../../__testing-utils__' | ||
import { i18n } from '../../../i18n' | ||
import { PromptPreview } from '..' | ||
|
||
const PROMPT_PREVIEW_PLACEHOLDER_MESSAGE = | ||
'As you complete the sections on the left, your prompt will be built here. When all requirements are met you will be able to generate the protocol.' | ||
|
||
const mockHandleClick = vi.fn() | ||
|
||
const render = (props: React.ComponentProps<typeof PromptPreview>) => { | ||
return renderWithProviders(<PromptPreview {...props} />, { | ||
i18nInstance: i18n, | ||
}) | ||
} | ||
|
||
describe('PromptPreview', () => { | ||
let props: React.ComponentProps<typeof PromptPreview> | ||
|
||
beforeEach(() => { | ||
props = { | ||
isSubmitButtonEnabled: false, | ||
handleSubmit: () => { | ||
mockHandleClick() | ||
}, | ||
promptPreviewData: [ | ||
{ | ||
title: 'Test Section 1', | ||
items: ['item1', 'item2'], | ||
}, | ||
{ | ||
title: 'Test Section 2', | ||
items: ['item3', 'item4'], | ||
}, | ||
], | ||
} | ||
}) | ||
|
||
it('should render the PromptPreview component', () => { | ||
render(props) | ||
|
||
expect(screen.getByText('Prompt')).toBeInTheDocument() | ||
}) | ||
|
||
it('should render the submit button', () => { | ||
render(props) | ||
|
||
expect(screen.getByText('Submit prompt')).toBeInTheDocument() | ||
}) | ||
|
||
it('should render the placeholder message when all sections are empty', () => { | ||
props.promptPreviewData = [ | ||
{ | ||
title: 'Test Section 1', | ||
items: [], | ||
}, | ||
{ | ||
title: 'Test Section 2', | ||
items: [], | ||
}, | ||
] | ||
render(props) | ||
|
||
expect( | ||
screen.getByText(PROMPT_PREVIEW_PLACEHOLDER_MESSAGE) | ||
).toBeInTheDocument() | ||
}) | ||
|
||
it('should not render the placeholder message when at least one section has items', () => { | ||
render(props) | ||
|
||
expect( | ||
screen.queryByText(PROMPT_PREVIEW_PLACEHOLDER_MESSAGE) | ||
).not.toBeInTheDocument() | ||
}) | ||
|
||
it('should render the sections with items', () => { | ||
render(props) | ||
|
||
expect(screen.getByText('Test Section 1')).toBeInTheDocument() | ||
expect(screen.getByText('Test Section 2')).toBeInTheDocument() | ||
}) | ||
|
||
it('should display submit button disabled when isSubmitButtonEnabled is false', () => { | ||
render(props) | ||
|
||
expect(screen.getByRole('button', { name: 'Submit prompt' })).toBeDisabled() | ||
}) | ||
|
||
it('should display submit button enabled when isSubmitButtonEnabled is true', () => { | ||
props.isSubmitButtonEnabled = true | ||
render(props) | ||
|
||
expect( | ||
screen.getByRole('button', { name: 'Submit prompt' }) | ||
).not.toBeDisabled() | ||
}) | ||
|
||
it('should call handleSubmit when the submit button is clicked', () => { | ||
props.isSubmitButtonEnabled = true | ||
render(props) | ||
|
||
const submitButton = screen.getByRole('button', { name: 'Submit prompt' }) | ||
submitButton.click() | ||
|
||
expect(mockHandleClick).toHaveBeenCalled() | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import styled from 'styled-components' | ||
import { | ||
Flex, | ||
StyledText, | ||
LargeButton, | ||
COLORS, | ||
JUSTIFY_SPACE_BETWEEN, | ||
DIRECTION_COLUMN, | ||
SIZE_AUTO, | ||
DIRECTION_ROW, | ||
ALIGN_CENTER, | ||
SPACING, | ||
} from '@opentrons/components' | ||
import { PromptPreviewSection } from '../PromptPreviewSection' | ||
import type { PromptPreviewSectionProps } from '../PromptPreviewSection' | ||
import { useTranslation } from 'react-i18next' | ||
|
||
interface PromptPreviewProps { | ||
isSubmitButtonEnabled?: boolean | ||
handleSubmit: () => void | ||
promptPreviewData: PromptPreviewSectionProps[] | ||
} | ||
|
||
const PromptPreviewContainer = styled(Flex)` | ||
flex-direction: ${DIRECTION_COLUMN}; | ||
width: 100%; | ||
height: ${SIZE_AUTO}; | ||
padding-top: ${SPACING.spacing8}; | ||
background-color: ${COLORS.transparent}; | ||
` | ||
|
||
const PromptPreviewHeading = styled(Flex)` | ||
flex-direction: ${DIRECTION_ROW}; | ||
justify-content: ${JUSTIFY_SPACE_BETWEEN}; | ||
align-items: ${ALIGN_CENTER}; | ||
margin-bottom: ${SPACING.spacing16}; | ||
` | ||
|
||
const PromptPreviewPlaceholderMessage = styled(StyledText)` | ||
padding: 82px 73px; | ||
color: ${COLORS.grey60}; | ||
text-align: ${ALIGN_CENTER}; | ||
` | ||
|
||
export function PromptPreview({ | ||
isSubmitButtonEnabled = false, | ||
handleSubmit, | ||
promptPreviewData = [], | ||
}: PromptPreviewProps): JSX.Element { | ||
const { t } = useTranslation('protocol_generator') | ||
|
||
const areAllSectionsEmpty = (): boolean => { | ||
return promptPreviewData.every(section => section.items.length === 0) | ||
} | ||
|
||
return ( | ||
<PromptPreviewContainer> | ||
<PromptPreviewHeading> | ||
<StyledText desktopStyle="headingLargeBold">Prompt</StyledText> | ||
<LargeButton | ||
buttonText={t('prompt_preview_submit_button')} | ||
disabled={!isSubmitButtonEnabled} | ||
onClick={handleSubmit} | ||
/> | ||
</PromptPreviewHeading> | ||
|
||
{areAllSectionsEmpty() && ( | ||
<PromptPreviewPlaceholderMessage desktopStyle="headingSmallRegular"> | ||
{t('prompt_preview_placeholder_message')} | ||
</PromptPreviewPlaceholderMessage> | ||
)} | ||
|
||
{Object.values(promptPreviewData).map( | ||
(section, index) => | ||
section.items.length > 0 && ( | ||
<PromptPreviewSection | ||
key={`section-${index}`} | ||
title={section.title} | ||
items={section.items} | ||
itemMaxWidth={index <= 2 ? '33.33%' : '100%'} | ||
/> | ||
) | ||
)} | ||
</PromptPreviewContainer> | ||
) | ||
} |
60 changes: 60 additions & 0 deletions
60
...rons-ai-client/src/molecules/PromptPreviewSection/__tests__/PromptPreviewSection.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import type * as React from 'react' | ||
import { screen } from '@testing-library/react' | ||
import { describe, it, beforeEach, expect } from 'vitest' | ||
import { renderWithProviders } from '../../../__testing-utils__' | ||
import { i18n } from '../../../i18n' | ||
|
||
import { PromptPreviewSection } from '../index' | ||
|
||
const render = (props: React.ComponentProps<typeof PromptPreviewSection>) => { | ||
return renderWithProviders(<PromptPreviewSection {...props} />, { | ||
i18nInstance: i18n, | ||
}) | ||
} | ||
|
||
describe('PromptPreviewSection', () => { | ||
let props: React.ComponentProps<typeof PromptPreviewSection> | ||
|
||
beforeEach(() => { | ||
props = { | ||
title: 'Test Section', | ||
items: ['test item 1', 'test item 2'], | ||
} | ||
}) | ||
|
||
it('should render the PromptPreviewSection component', () => { | ||
render(props) | ||
|
||
expect(screen.getByText('Test Section')).toBeInTheDocument() | ||
}) | ||
|
||
it('should render the section title', () => { | ||
render(props) | ||
|
||
expect(screen.getByText('Test Section')).toBeInTheDocument() | ||
}) | ||
|
||
it('should render the items', () => { | ||
render(props) | ||
|
||
expect(screen.getByText('test item 1')).toBeInTheDocument() | ||
expect(screen.getByText('test item 2')).toBeInTheDocument() | ||
}) | ||
|
||
it("should not render the item tag if it's an empty string", () => { | ||
props.items = ['test item 1', ''] | ||
render(props) | ||
|
||
const items = screen.getAllByTestId('Tag_default') | ||
expect(items).toHaveLength(1) | ||
}) | ||
|
||
it('should render the item with the correct max item width', () => { | ||
props.items = ['test item 1 long text long text long text long text'] | ||
props.itemMaxWidth = '23%' | ||
render(props) | ||
|
||
const item = screen.getByTestId('item-tag-wrapper-0') | ||
expect(item).toHaveStyle({ maxWidth: '23%' }) | ||
}) | ||
}) |
Oops, something went wrong.