Skip to content

Commit

Permalink
feat(opentrons-ai-client): Prompt Preview (#16508)
Browse files Browse the repository at this point in the history
# 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
fbelginetw authored Oct 21, 2024
1 parent 1b0a5a5 commit 908ad7c
Show file tree
Hide file tree
Showing 6 changed files with 416 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,7 @@
"well_allocations": "Well allocations: Describe where liquids should go in labware.",
"what_if_you": "<span>What if you don’t provide all of those pieces of information? <bold>OpentronsAI asks you to provide it!</bold></span>",
"what_typeof_protocol": "What type of protocol do you need?",
"you": "You"
"you": "You",
"prompt_preview_submit_button": "Submit prompt",
"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."
}
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')
},
},
}
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()
})
})
86 changes: 86 additions & 0 deletions opentrons-ai-client/src/molecules/PromptPreview/index.tsx
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>
)
}
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%' })
})
})
Loading

0 comments on commit 908ad7c

Please sign in to comment.