Skip to content

Commit 4794f55

Browse files
authored
feat(opentrons-ai-client) add input textbox to container (#14968)
* feat(opentrons-ai-client) add input textbox to container
1 parent 3b7058e commit 4794f55

File tree

9 files changed

+227
-8
lines changed

9 files changed

+227
-8
lines changed

components/src/icons/icon-data.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,11 @@ export const ICON_DATA_BY_NAME = {
632632
'M8.01487 8.84912C8.47511 8.84912 8.84821 8.47603 8.84821 8.01579C8.84821 7.55555 8.47511 7.18245 8.01487 7.18245C7.55464 7.18245 7.18154 7.55555 7.18154 8.01579C7.18154 8.47603 7.55464 8.84912 8.01487 8.84912Z M8.66654 0.928711V2.36089C11.27 2.66533 13.3354 4.73075 13.6398 7.33418H15.072V8.66751H13.6398C13.3354 11.2709 11.27 13.3363 8.66654 13.6408V15.073H7.3332V13.6408C4.72979 13.3363 2.66437 11.2709 2.35992 8.66751H0.927734V7.33418H2.35992C2.66436 4.73075 4.72978 2.66533 7.3332 2.36089V0.928711H8.66654ZM12.2944 7.33418H11.6184C11.2502 7.33418 10.9518 7.63266 10.9518 8.00085C10.9518 8.36904 11.2502 8.66751 11.6184 8.66751H12.2944C12.0071 10.5336 10.5326 12.008 8.66654 12.2953V11.6194C8.66654 11.2512 8.36806 10.9527 7.99987 10.9527C7.63168 10.9527 7.3332 11.2512 7.3332 11.6194V12.2953C5.46716 12.008 3.99268 10.5336 3.70536 8.66751H4.38132C4.74951 8.66751 5.04798 8.36904 5.04798 8.00085C5.04798 7.63266 4.74951 7.33418 4.38132 7.33418H3.70536C3.99267 5.46812 5.46715 3.99364 7.3332 3.70632V4.38229C7.3332 4.75048 7.63168 5.04896 7.99987 5.04896C8.36806 5.04896 8.66654 4.75048 8.66654 4.38229V3.70632C10.5326 3.99364 12.0071 5.46812 12.2944 7.33418Z',
633633
viewBox: '0 0 16 16',
634634
},
635+
send: {
636+
path:
637+
'M6.96216 26.6667V5.33337L32.2955 16L6.96216 26.6667ZM9.62882 22.6667L25.4288 16L9.62882 9.33337V14L17.6288 16L9.62882 18V22.6667Z',
638+
viewBox: '0 0 32 32',
639+
},
635640
settings: {
636641
path:
637642
'M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z',

opentrons-ai-client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"react": "18.2.0",
2626
"react-dom": "18.2.0",
2727
"react-error-boundary": "^4.0.10",
28+
"react-hook-form": "7.50.1",
2829
"react-i18next": "13.5.0",
2930
"react-markdown": "9.0.1",
3031
"styled-components": "5.3.6"

opentrons-ai-client/src/assets/localization/en/protocol_generator.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"api": "API: An API level is 2.15",
33
"application": "Application: Your protocol's name, describing what it does.",
44
"commands": "Commands: List the protocol's steps, specifying quantities in microliters and giving exact source and destination locations.",
5+
"disclaimer": "OpentronsAI can make mistakes. Review your protocol before running it on an Opentrons robot.",
56
"got_feedback": "Got feedback? We love to hear it.",
67
"make_sure_your_prompt": "Make sure your prompt includes the following:",
78
"metadata": "Metadata: Three pieces of information.",
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from 'react'
2+
import { describe, it, expect } from 'vitest'
3+
import { fireEvent, screen } from '@testing-library/react'
4+
import { renderWithProviders } from '../../../__testing-utils__'
5+
import { i18n } from '../../../i18n'
6+
import { InputPrompt } from '../index'
7+
8+
const render = () => {
9+
return renderWithProviders(<InputPrompt />, { i18nInstance: i18n })
10+
}
11+
12+
describe('InputPrompt', () => {
13+
it('should render textarea and disabled button', () => {
14+
render()
15+
screen.getByRole('textbox')
16+
screen.queryByPlaceholderText('Type your prompt...')
17+
screen.getByRole('button')
18+
expect(screen.getByRole('button')).toBeDisabled()
19+
})
20+
21+
it('should make send button not disabled when a user inputs something in textarea', () => {
22+
render()
23+
const textbox = screen.getByRole('textbox')
24+
fireEvent.change(textbox, { target: { value: ['test'] } })
25+
expect(screen.getByRole('button')).not.toBeDisabled()
26+
})
27+
28+
// ToDo (kk:04/19/2024) add more test cases
29+
})
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import React from 'react'
2+
import { useTranslation } from 'react-i18next'
3+
import styled, { css } from 'styled-components'
4+
import { useForm } from 'react-hook-form'
5+
6+
import {
7+
ALIGN_CENTER,
8+
BORDERS,
9+
Btn,
10+
COLORS,
11+
DIRECTION_ROW,
12+
DISPLAY_FLEX,
13+
Flex,
14+
Icon,
15+
JUSTIFY_CENTER,
16+
SPACING,
17+
TYPOGRAPHY,
18+
} from '@opentrons/components'
19+
20+
import type { SubmitHandler } from 'react-hook-form'
21+
22+
// ToDo (kk:04/19/2024) Note this interface will be used by prompt buttons in SidePanel
23+
// interface InputPromptProps {}
24+
25+
interface InputType {
26+
userPrompt: string
27+
}
28+
29+
export function InputPrompt(/* props: InputPromptProps */): JSX.Element {
30+
const { t } = useTranslation('protocol_generator')
31+
const { register, handleSubmit, watch } = useForm<InputType>({
32+
defaultValues: {
33+
userPrompt: '',
34+
},
35+
})
36+
const userPrompt = watch('userPrompt') ?? ''
37+
38+
const onSubmit: SubmitHandler<InputType> = async data => {
39+
// ToDo (kk: 04/19/2024) call api
40+
const { userPrompt } = data
41+
console.log('user prompt', userPrompt)
42+
}
43+
44+
return (
45+
<StyledForm id="User_Prompt" onSubmit={() => handleSubmit(onSubmit)}>
46+
<Flex
47+
padding={SPACING.spacing40}
48+
gridGap={SPACING.spacing40}
49+
flexDirection={DIRECTION_ROW}
50+
backgroundColor={COLORS.white}
51+
borderRadius={BORDERS.borderRadius4}
52+
justifyContent={JUSTIFY_CENTER}
53+
alignItems={ALIGN_CENTER}
54+
>
55+
<StyledTextarea
56+
rows={1}
57+
placeholder={t('type_your_prompt')}
58+
{...register('userPrompt')}
59+
/>
60+
<PlayButton disabled={userPrompt.length === 0} />
61+
</Flex>
62+
</StyledForm>
63+
)
64+
}
65+
66+
const StyledForm = styled.form`
67+
width: 100%;
68+
`
69+
70+
const StyledTextarea = styled.textarea`
71+
resize: none;
72+
min-height: 3.75rem;
73+
background-color: ${COLORS.white};
74+
border: none;
75+
outline: none;
76+
padding: 0;
77+
box-shadow: none;
78+
color: ${COLORS.black90};
79+
width: 100%;
80+
font-size: ${TYPOGRAPHY.fontSize20};
81+
line-height: ${TYPOGRAPHY.lineHeight24};
82+
::placeholder {
83+
position: absolute;
84+
top: 50%;
85+
transform: translateY(-50%);
86+
}
87+
`
88+
89+
interface PlayButtonProps {
90+
onPlay?: () => void
91+
disabled?: boolean
92+
isLoading?: boolean
93+
}
94+
95+
function PlayButton({
96+
onPlay,
97+
disabled = false,
98+
isLoading = false,
99+
}: PlayButtonProps): JSX.Element {
100+
const playButtonStyle = css`
101+
-webkit-tap-highlight-color: transparent;
102+
&:focus {
103+
background-color: ${COLORS.blue60};
104+
color: ${COLORS.white};
105+
}
106+
107+
&:hover {
108+
background-color: ${COLORS.blue50};
109+
color: ${COLORS.white};
110+
}
111+
112+
&:focus-visible {
113+
background-color: ${COLORS.blue50};
114+
}
115+
116+
&:active {
117+
background-color: ${COLORS.blue60};
118+
color: ${COLORS.white};
119+
}
120+
121+
&:disabled {
122+
background-color: ${COLORS.grey35};
123+
color: ${COLORS.grey50};
124+
}
125+
`
126+
return (
127+
<Btn
128+
alignItems={ALIGN_CENTER}
129+
backgroundColor={disabled ? COLORS.grey35 : COLORS.blue50}
130+
borderRadius={BORDERS.borderRadiusFull}
131+
display={DISPLAY_FLEX}
132+
justifyContent={JUSTIFY_CENTER}
133+
width="4.25rem"
134+
height="3.75rem"
135+
disabled={disabled || isLoading}
136+
onClick={onPlay}
137+
aria-label="play"
138+
css={playButtonStyle}
139+
type="submit"
140+
>
141+
<Icon
142+
color={disabled ? COLORS.grey50 : COLORS.white}
143+
name={isLoading ? 'ot-spinner' : 'send'}
144+
spin={isLoading}
145+
size="2rem"
146+
/>
147+
</Btn>
148+
)
149+
}

opentrons-ai-client/src/molecules/PromptGuide/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ export function PromptGuide(): JSX.Element {
2424
backgroundColor={COLORS.grey30}
2525
borderRadius={BORDERS.borderRadius12}
2626
gridGap={SPACING.spacing32}
27-
width="58.125rem"
2827
>
2928
<StyledText css={HEADER_TEXT_STYLE}>
3029
{t('what_typeof_protocol')}

opentrons-ai-client/src/molecules/SidePanel/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ export function SidePanel(): JSX.Element {
2626
flexDirection={DIRECTION_COLUMN}
2727
backgroundColor={COLORS.black90}
2828
width="24.375rem"
29-
height="64rem"
3029
>
3130
{/* logo */}
3231
<Flex>

opentrons-ai-client/src/organisms/ChatContainer/__tests__/ChatContainer.test.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import { describe, it, vi, beforeEach } from 'vitest'
44
import { renderWithProviders } from '../../../__testing-utils__'
55
import { i18n } from '../../../i18n'
66
import { PromptGuide } from '../../../molecules/PromptGuide'
7+
import { InputPrompt } from '../../../molecules/InputPrompt'
78
import { ChatContainer } from '../index'
89

910
vi.mock('../../../molecules/PromptGuide')
11+
vi.mock('../../../molecules/InputPrompt')
1012

1113
const render = (): ReturnType<typeof renderWithProviders> => {
1214
return renderWithProviders(<ChatContainer />, {
@@ -17,11 +19,16 @@ const render = (): ReturnType<typeof renderWithProviders> => {
1719
describe('ChatContainer', () => {
1820
beforeEach(() => {
1921
vi.mocked(PromptGuide).mockReturnValue(<div>mock PromptGuide</div>)
22+
vi.mocked(InputPrompt).mockReturnValue(<div>mock InputPrompt</div>)
2023
})
2124
it('should render prompt guide and text', () => {
2225
render()
2326
screen.getByText('OpentronsAI')
2427
screen.getByText('mock PromptGuide')
28+
screen.getByText('mock InputPrompt')
29+
screen.getByText(
30+
'OpentronsAI can make mistakes. Review your protocol before running it on an Opentrons robot.'
31+
)
2532
})
2633

2734
// ToDo (kk:04/16/2024) Add more test cases
Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,64 @@
11
import React from 'react'
22
import { useTranslation } from 'react-i18next'
3+
import { css } from 'styled-components'
34
import {
45
COLORS,
56
DIRECTION_COLUMN,
6-
FLEX_MAX_CONTENT,
77
Flex,
8+
POSITION_ABSOLUTE,
9+
POSITION_RELATIVE,
810
SPACING,
911
StyledText,
12+
TYPOGRAPHY,
1013
} from '@opentrons/components'
1114
import { PromptGuide } from '../../molecules/PromptGuide'
15+
import { InputPrompt } from '../../molecules/InputPrompt'
1216

1317
export function ChatContainer(): JSX.Element {
1418
const { t } = useTranslation('protocol_generator')
1519
const isDummyInitial = true
1620
return (
1721
<Flex
18-
padding={SPACING.spacing40}
22+
padding={`${SPACING.spacing40} ${SPACING.spacing40} ${SPACING.spacing24}`}
1923
backgroundColor={COLORS.grey10}
20-
width={FLEX_MAX_CONTENT}
24+
width="100%"
2125
>
2226
{/* This will be updated when input textbox and function are implemented */}
2327
{isDummyInitial ? (
2428
<Flex
2529
flexDirection={DIRECTION_COLUMN}
26-
gridGap={SPACING.spacing12}
30+
position={POSITION_RELATIVE}
2731
width="100%"
2832
>
29-
<StyledText>{t('opentronsai')}</StyledText>
30-
<PromptGuide />
33+
<Flex
34+
flexDirection={DIRECTION_COLUMN}
35+
gridGap={SPACING.spacing12}
36+
width="100%"
37+
>
38+
<StyledText>{t('opentronsai')}</StyledText>
39+
<PromptGuide />
40+
</Flex>
41+
<Flex
42+
position={POSITION_ABSOLUTE}
43+
bottom="0"
44+
width="100%"
45+
gridGap={SPACING.spacing24}
46+
flexDirection={DIRECTION_COLUMN}
47+
>
48+
<InputPrompt />
49+
<StyledText css={DISCLAIMER_TEXT_STYLE}>
50+
{t('disclaimer')}
51+
</StyledText>
52+
</Flex>
3153
</Flex>
3254
) : null}
3355
</Flex>
3456
)
3557
}
58+
59+
const DISCLAIMER_TEXT_STYLE = css`
60+
color: ${COLORS.grey55};
61+
font-size: ${TYPOGRAPHY.fontSize20};
62+
line-height: ${TYPOGRAPHY.lineHeight24};
63+
text-align: ${TYPOGRAPHY.textAlignCenter};
64+
`

0 commit comments

Comments
 (0)