Skip to content
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

feat(opentrons-ai-client): Prompt Preview #16508

Merged
merged 8 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
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,105 @@
import { screen } from '@testing-library/react'
import { describe, it, vi, beforeEach, expect } from 'vitest'
import { renderWithProviders } from '../../../__testing-utils__'
import { i18n } from '../../../i18n'
import { PROMPT_PREVIEW_PLACEHOLDER_MESSAGE, PromptPreview } from '..'

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()
})
})
85 changes: 85 additions & 0 deletions opentrons-ai-client/src/molecules/PromptPreview/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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'

export 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.'

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 areAllSectionsEmpty = (): boolean => {
return promptPreviewData.every(section => section.items.length === 0)
}

return (
<PromptPreviewContainer>
<PromptPreviewHeading>
<StyledText desktopStyle="headingLargeBold">Prompt</StyledText>
<LargeButton
buttonText="Submit prompt"
disabled={!isSubmitButtonEnabled}
onClick={handleSubmit}
/>
</PromptPreviewHeading>

{areAllSectionsEmpty() && (
<PromptPreviewPlaceholderMessage desktopStyle="headingSmallRegular">
{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}
/>
)
)}
</PromptPreviewContainer>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import styled from 'styled-components'
import {
Flex,
StyledText,
Tag,
DIRECTION_COLUMN,
WRAP,
SPACING,
} from '@opentrons/components'

export interface PromptPreviewSectionProps {
title: string
items: string[]
}

const PromptPreviewSectionContainer = styled(Flex)`
flex-direction: ${DIRECTION_COLUMN};
margin-top: ${SPACING.spacing32};
`

const SectionHeading = styled(StyledText)`
margin-bottom: ${SPACING.spacing8};
`

const TagsContainer = styled(Flex)`
grid-gap: ${SPACING.spacing4};
flex-wrap: ${WRAP};
justify-content: flex-start;
width: 100%;
`

const TagItemWrapper = styled(Flex)`
width: auto;
white-space: nowrap;
overflow: hidden;
max-width: 35%;

& > div {
overflow: hidden;

> p {
overflow: hidden;
text-overflow: ellipsis;
}
}
`

export function PromptPreviewSection({
title,
items,
}: PromptPreviewSectionProps): JSX.Element {
return (
<PromptPreviewSectionContainer>
<SectionHeading desktopStyle="bodyLargeSemiBold">{title}</SectionHeading>
<TagsContainer>
{items.map(
(item: string, index: number) =>
item.trim() !== '' && (
<TagItemWrapper key={`item-tag-${index}`}>
<Tag text={item} type={'default'} />
</TagItemWrapper>
)
)}
</TagsContainer>
</PromptPreviewSectionContainer>
)
}
Loading