Skip to content

Commit 69c8cc0

Browse files
feat(opentrons-ai-client): Chat page to interact with LLM (#16703)
<!-- Thanks for taking the time to open a Pull Request (PR)! Please make sure you've read the "Opening Pull Requests" section of our Contributing Guide: https://github.com/Opentrons/opentrons/blob/edge/CONTRIBUTING.md#opening-pull-requests GitHub provides robust markdown to format your PR. Links, diagrams, pictures, and videos along with text formatting make it possible to create a rich and informative PR. For more information on GitHub markdown, see: https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax To ensure your code is reviewed quickly and thoroughly, please fill out the sections below to the best of your ability! --> # Overview Added chat page with four new buttons to regenerate, send feedback (not fully functional), copy and download the python code. Regenerate: Finds the corresponding request to that LLM response and fills in the inputfield so that it can be resubmitted. It also scrolls down to the input field to show the filled out prompt Send Feedback: Shows a send feedback modal that can in the future send feedback to the server. This API will be implemented in a subsequent PR. Copy: Copies the code to the clipboard Download: Converts the python code in the LLM response into a python file and downloads it. <img width="1895" alt="image" src="https://github.com/user-attachments/assets/5825ea5b-7796-4f74-ae68-1e9a73d027d8"> <img width="551" alt="image" src="https://github.com/user-attachments/assets/4c298768-d3a3-4731-ba6b-6007c4649736"> <!-- Describe your PR at a high level. State acceptance criteria and how this PR fits into other work. Link issues, PRs, and other relevant resources. --> ## Test Plan and Hands on Testing Manually tested that regenerate picks the right prompt by adding a few in the chat. Manually tested that clicking feedback shows the feedback modal Manually tested that copy works Manually tested that download creates the file, downloads it and checked the contents to make sure the file was created properly. <!-- Describe your testing of the PR. Emphasize testing not reflected in the code. Attach protocols, logs, screenshots and any other assets that support your testing. --> ## Changelog <!-- List changes introduced by this PR considering future developers and the end user. Give careful thought and clear documentation to breaking changes. --> ## Review requests <!-- - What do you need from reviewers to feel confident this PR is ready to merge? - Ask questions. --> ## Risk assessment <!-- - Indicate the level of attention this PR needs. - Provide context to guide reviewers. - Discuss trade-offs, coupling, and side effects. - Look for the possibility, even if you think it's small, that your change may affect some other part of the system. - For instance, changing return tip behavior may also change the behavior of labware calibration. - How do your unit tests and on hands on testing mitigate this PR's risks and the risk of future regressions? - Especially in high risk PRs, explain how you know your testing is enough. --> --------- Co-authored-by: FELIPE BELGINE <[email protected]>
1 parent ac00351 commit 69c8cc0

File tree

14 files changed

+335
-129
lines changed

14 files changed

+335
-129
lines changed

components/src/icons/icon-data.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,11 @@ export const ICON_DATA_BY_NAME: Record<
685685
'M18.793 34.9163C15.3763 34.6386 12.5013 33.2358 10.168 30.708C7.83464 28.1802 6.66797 25.1802 6.66797 21.708C6.66797 19.5691 7.16102 17.5552 8.14714 15.6663C9.13325 13.7775 10.5152 12.2358 12.293 11.0413L14.0846 12.833C12.5291 13.7497 11.3207 15.0066 10.4596 16.6038C9.59852 18.2011 9.16797 19.9025 9.16797 21.708C9.16797 24.4858 10.0846 26.8886 11.918 28.9163C13.7513 30.9441 16.043 32.1108 18.793 32.4163V34.9163ZM21.293 34.9163V32.4163C24.0707 32.083 26.3624 30.9094 28.168 28.8955C29.9735 26.8816 30.8763 24.4858 30.8763 21.708C30.8763 18.6802 29.8277 16.1177 27.7305 14.0205C25.6332 11.9233 23.0707 10.8747 20.043 10.8747H19.2096L21.7096 13.3747L19.918 15.1663L14.3763 9.62467L19.918 4.08301L21.7096 5.87467L19.2096 8.37467H20.043C23.7652 8.37467 26.918 9.67329 29.5013 12.2705C32.0846 14.8677 33.3763 18.0136 33.3763 21.708C33.3763 25.1802 32.2166 28.1802 29.8971 30.708C27.5777 33.2358 24.7096 34.6386 21.293 34.9163Z',
686686
viewBox: '0 0 40 40',
687687
},
688+
reload: {
689+
path:
690+
'M15.1406 23.6501C11.9581 23.6501 9.25062 22.5457 7.01813 20.337C4.78562 18.1282 3.66937 15.4326 3.66937 12.2501V12.0007L2.38687 13.2832C2.12562 13.5445 1.79312 13.6751 1.38937 13.6751C0.985625 13.6751 0.653125 13.5445 0.391875 13.2832C0.130625 13.022 0 12.6895 0 12.2857C0 11.882 0.130625 11.5495 0.391875 11.2882L4.09687 7.58322C4.38188 7.29822 4.71438 7.15572 5.09438 7.15572C5.47437 7.15572 5.80687 7.29822 6.09188 7.58322L9.79688 11.2882C10.0581 11.5495 10.1888 11.882 10.1888 12.2857C10.1888 12.6895 10.0581 13.022 9.79688 13.2832C9.53562 13.5445 9.20312 13.6751 8.79937 13.6751C8.39563 13.6751 8.06312 13.5445 7.80188 13.2832L6.51937 12.0007V12.2501C6.51937 14.6251 7.35656 16.6438 9.03094 18.3063C10.7053 19.9688 12.7419 20.8001 15.1406 20.8001C15.5206 20.8001 15.8947 20.7764 16.2628 20.7288C16.6309 20.6813 16.9931 20.5982 17.3494 20.4795C17.7531 20.3607 18.1331 20.3726 18.4894 20.5151C18.8456 20.6576 19.1187 20.907 19.3088 21.2632C19.4988 21.6432 19.5166 22.0173 19.3622 22.3854C19.2078 22.7535 18.9287 22.997 18.525 23.1157C17.9788 23.3057 17.4206 23.4423 16.8506 23.5254C16.2806 23.6085 15.7106 23.6501 15.1406 23.6501ZM14.9981 3.7001C14.6181 3.7001 14.2441 3.72385 13.8759 3.77135C13.5078 3.81885 13.1456 3.90197 12.7894 4.02072C12.3856 4.13947 11.9997 4.1276 11.6316 3.9851C11.2634 3.8426 10.9844 3.59322 10.7944 3.23697C10.6044 2.88072 10.5866 2.51854 10.7409 2.15041C10.8953 1.78229 11.1625 1.53885 11.5425 1.4201C12.1125 1.2301 12.6825 1.0876 13.2525 0.992598C13.8225 0.897598 14.4044 0.850098 14.9981 0.850098C18.1806 0.850098 20.8881 1.95447 23.1206 4.16322C25.3531 6.37197 26.4694 9.0676 26.4694 12.2501V12.4995L27.7519 11.217C28.0131 10.9557 28.3456 10.8251 28.7494 10.8251C29.1531 10.8251 29.4856 10.9557 29.7469 11.217C30.0081 11.4782 30.1388 11.8107 30.1388 12.2145C30.1388 12.6182 30.0081 12.9507 29.7469 13.212L26.0419 16.917C25.7569 17.202 25.4244 17.3445 25.0444 17.3445C24.6644 17.3445 24.3319 17.202 24.0469 16.917L20.3419 13.212C20.0806 12.9507 19.95 12.6182 19.95 12.2145C19.95 11.8107 20.0806 11.4782 20.3419 11.217C20.6031 10.9557 20.9356 10.8251 21.3394 10.8251C21.7431 10.8251 22.0756 10.9557 22.3369 11.217L23.6194 12.4995V12.2501C23.6194 9.8751 22.7822 7.85635 21.1078 6.19385C19.4334 4.53135 17.3969 3.7001 14.9981 3.7001Z',
691+
viewBox: '0 0 31 24',
692+
},
688693
reticle: {
689694
path:
690695
'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',
@@ -729,6 +734,11 @@ export const ICON_DATA_BY_NAME: Record<
729734
'M10.8307 8.3335L1.66406 31.6668H4.78906L7.16406 25.4168H17.8307L20.2057 31.6668H23.3307L14.1641 8.3335H10.8307ZM16.8307 22.7502H8.16406L12.4141 11.4585H12.5807L16.8307 22.7502ZM30.1577 16.6668L24.1641 31.6668H26.2073L27.7602 27.649H34.7346L36.2875 31.6668H38.3307L32.3371 16.6668H30.1577ZM34.0807 25.9347H28.4141L31.1929 18.6758H31.3019L34.0807 25.9347Z',
730735
viewBox: '0 0 40 40',
731736
},
737+
'thumbs-down': {
738+
path:
739+
'M2.99062 18.9525C2.23062 18.9525 1.56562 18.6675 0.995625 18.0975C0.425625 17.5275 0.140625 16.8625 0.140625 16.1025V13.2525C0.140625 13.0862 0.164375 12.9081 0.211875 12.7181C0.259375 12.5281 0.306875 12.35 0.354375 12.1837L4.62937 2.13749C4.84312 1.66249 5.19938 1.25874 5.69812 0.92624C6.19688 0.59374 6.71938 0.42749 7.26562 0.42749H22.9406V18.9525L14.3906 27.4312C14.0344 27.7875 13.6128 27.9953 13.1259 28.0547C12.6391 28.1141 12.17 28.025 11.7188 27.7875C11.2675 27.55 10.935 27.2175 10.7212 26.79C10.5075 26.3625 10.46 25.9231 10.5787 25.4719L12.1819 18.9525H2.99062ZM20.0906 17.7412V3.27749H7.26562L2.99062 13.2525V16.1025H15.8156L13.8919 23.94L20.0906 17.7412ZM27.2156 0.42749C27.9994 0.42749 28.6703 0.706553 29.2284 1.26468C29.7866 1.8228 30.0656 2.49374 30.0656 3.27749V16.1025C30.0656 16.8862 29.7866 17.5572 29.2284 18.1153C28.6703 18.6734 27.9994 18.9525 27.2156 18.9525H22.9406V16.1025H27.2156V3.27749H22.9406V0.42749H27.2156Z',
740+
viewBox: '0 0 31 29',
741+
},
732742
'tip-position': {
733743
path:
734744
'M10.75 2H9.25V4.75H10.75V2ZM10.75 9.25V7.25H9.25V9.25H7.25V10.75H9.25V12.75H10.75V10.75H12.75V9.25H10.75ZM10.75 18V15.25H9.25V18H10.75ZM2 9.25V10.75H4.75V9.25H2ZM18 9.25H15.25V10.75H18V9.25Z',

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"api": "API: An API level is 2.15",
44
"application": "Application: Your protocol's name, describing what it does.",
55
"commands": "Commands: List the protocol's steps, specifying quantities in microliters (uL) and giving exact source and destination locations.",
6+
"cancel": "Cancel",
67
"copyright": "Copyright © 2024 Opentrons",
78
"copy_code": "Copy code",
89
"choose_file": "Choose file",
@@ -51,6 +52,9 @@
5152
"robot_type": "Robot type: Choose the OT-2 or Opentrons Flex.",
5253
"robot": "Robot: OT-2.",
5354
"share_your_thoughts": "Share your thoughts here",
55+
"send_feedback": "Send feedback",
56+
"send_feedback_input_title": "Share why the response was not helpful",
57+
"send_feedback_to_opentrons": "Send feedback to Opentrons",
5458
"side_panel_body": "Write a prompt in natural language to generate a Reagent Transfer or a PCR protocol for the OT-2 or Opentrons Flex using the Opentrons Python Protocol API.",
5559
"side_panel_header": "Use natural language to generate protocols with OpentronsAI powered by OpenAI",
5660
"simulate_description": "Once OpentronsAI has written your protocol, type `simulate` in the prompt box to try it out.",

opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,24 @@ import { renderWithProviders } from '../../../__testing-utils__'
55
import { i18n } from '../../../i18n'
66

77
import { ChatDisplay } from '../index'
8+
import { useForm, FormProvider } from 'react-hook-form'
9+
10+
const RenderChatDisplay = (props: React.ComponentProps<typeof ChatDisplay>) => {
11+
const methods = useForm({
12+
defaultValues: {},
13+
})
14+
15+
return (
16+
<FormProvider {...methods}>
17+
<ChatDisplay {...props} />
18+
</FormProvider>
19+
)
20+
}
821

922
const render = (props: React.ComponentProps<typeof ChatDisplay>) => {
10-
return renderWithProviders(<ChatDisplay {...props} />, { i18nInstance: i18n })
23+
return renderWithProviders(<RenderChatDisplay {...props} />, {
24+
i18nInstance: i18n,
25+
})
1126
}
1227

1328
describe('ChatDisplay', () => {
@@ -18,6 +33,7 @@ describe('ChatDisplay', () => {
1833
chat: {
1934
role: 'assistant',
2035
reply: 'mock text from the backend',
36+
requestId: '12351234',
2137
},
2238
chatId: 'mockId',
2339
}
@@ -35,6 +51,7 @@ describe('ChatDisplay', () => {
3551
chat: {
3652
role: 'user',
3753
reply: 'mock text from user input',
54+
requestId: '12351234',
3855
},
3956
chatId: 'mockId',
4057
}

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

Lines changed: 107 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from 'react'
1+
import { useEffect, useState } from 'react'
22
import styled from 'styled-components'
33
import { useTranslation } from 'react-i18next'
44
import Markdown from 'react-markdown'
@@ -12,57 +12,114 @@ import {
1212
JUSTIFY_CENTER,
1313
JUSTIFY_FLEX_END,
1414
JUSTIFY_FLEX_START,
15-
POSITION_ABSOLUTE,
1615
POSITION_RELATIVE,
17-
PrimaryButton,
1816
SPACING,
1917
LegacyStyledText,
2018
TYPOGRAPHY,
19+
StyledText,
20+
DIRECTION_ROW,
2121
OVERFLOW_AUTO,
2222
} from '@opentrons/components'
2323

2424
import type { ChatData } from '../../resources/types'
25+
import { useAtom } from 'jotai'
26+
import {
27+
chatDataAtom,
28+
feedbackModalAtom,
29+
scrollToBottomAtom,
30+
} from '../../resources/atoms'
31+
import { delay } from 'lodash'
32+
import { useFormContext } from 'react-hook-form'
2533

2634
interface ChatDisplayProps {
2735
chat: ChatData
2836
chatId: string
2937
}
3038

39+
const HoverShadow = styled(Flex)`
40+
alignitems: ${ALIGN_CENTER};
41+
justifycontent: ${JUSTIFY_CENTER};
42+
padding: ${SPACING.spacing8};
43+
transition: box-shadow 0.3s ease;
44+
border-radius: ${BORDERS.borderRadius8};
45+
46+
&:hover {
47+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
48+
border-radius: ${BORDERS.borderRadius8};
49+
}
50+
`
51+
52+
const StyledIcon = styled(Icon)`
53+
color: ${COLORS.blue50};
54+
`
55+
3156
export function ChatDisplay({ chat, chatId }: ChatDisplayProps): JSX.Element {
3257
const { t } = useTranslation('protocol_generator')
3358
const [isCopied, setIsCopied] = useState<boolean>(false)
34-
const { role, reply } = chat
59+
const [, setShowFeedbackModal] = useAtom(feedbackModalAtom)
60+
const { setValue } = useFormContext()
61+
const [chatdata] = useAtom(chatDataAtom)
62+
const [scrollToBottom, setScrollToBottom] = useAtom(scrollToBottomAtom)
63+
const { role, reply, requestId } = chat
3564
const isUser = role === 'user'
3665

66+
const setInputFieldToCorrespondingRequest = (): void => {
67+
const prompt = chatdata.find(
68+
chat => chat.role === 'user' && chat.requestId === requestId
69+
)?.reply
70+
setScrollToBottom(!scrollToBottom)
71+
setValue('userPrompt', prompt)
72+
}
73+
74+
const handleFileDownload = (): void => {
75+
const lastCodeBlock = document.querySelector(`#${chatId}`)
76+
const code = lastCodeBlock?.textContent ?? ''
77+
const blobParts: BlobPart[] = [code]
78+
79+
const file = new File(blobParts, 'OpentronsAI.py', { type: 'text/python' })
80+
const url = URL.createObjectURL(file)
81+
const a = document.createElement('a')
82+
83+
document.body.appendChild(a)
84+
a.href = url
85+
a.download = 'OpentronsAI.py'
86+
a.click()
87+
window.URL.revokeObjectURL(url)
88+
}
89+
3790
const handleClickCopy = async (): Promise<void> => {
3891
const lastCodeBlock = document.querySelector(`#${chatId}`)
3992
const code = lastCodeBlock?.textContent ?? ''
4093
await navigator.clipboard.writeText(code)
4194
setIsCopied(true)
4295
}
4396

97+
useEffect(() => {
98+
if (isCopied)
99+
delay(() => {
100+
setIsCopied(false)
101+
}, 2000)
102+
}, [isCopied])
103+
44104
function CodeText(props: JSX.IntrinsicAttributes): JSX.Element {
45105
return <CodeWrapper {...props} id={chatId} />
46106
}
47107

48108
return (
49-
<Flex
50-
flexDirection={DIRECTION_COLUMN}
51-
gridGap={SPACING.spacing12}
52-
paddingLeft={isUser ? SPACING.spacing40 : undefined}
53-
paddingRight={isUser ? undefined : SPACING.spacing40}
54-
>
109+
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing12}>
55110
<Flex justifyContent={isUser ? JUSTIFY_FLEX_END : JUSTIFY_FLEX_START}>
56-
<LegacyStyledText>
111+
<StyledText paddingTop={SPACING.spacing12}>
57112
{isUser ? t('you') : t('opentronsai')}
58-
</LegacyStyledText>
113+
</StyledText>
59114
</Flex>
60115
{/* text should be markdown so this component will have a package or function to parse markdown */}
61116
<Flex
62-
padding={SPACING.spacing32}
117+
padding={`${SPACING.spacing40} ${SPACING.spacing40} ${
118+
isUser ? SPACING.spacing40 : SPACING.spacing12
119+
} ${SPACING.spacing40}`}
63120
backgroundColor={isUser ? COLORS.blue30 : COLORS.grey30}
64121
data-testid={`ChatDisplay_from_${isUser ? 'user' : 'backend'}`}
65-
borderRadius={BORDERS.borderRadius12}
122+
borderRadius={SPACING.spacing12}
66123
width="100%"
67124
overflowY={OVERFLOW_AUTO}
68125
flexDirection={DIRECTION_COLUMN}
@@ -84,21 +141,44 @@ export function ChatDisplay({ chat, chatId }: ChatDisplayProps): JSX.Element {
84141
</Markdown>
85142

86143
{!isUser ? (
87-
<PrimaryButton
88-
position={POSITION_ABSOLUTE}
89-
right={SPACING.spacing16}
90-
bottom={`-${SPACING.spacing24}`}
91-
borderRadius={BORDERS.borderRadiusFull}
92-
onClick={handleClickCopy}
144+
<Flex
145+
flexDirection={DIRECTION_ROW}
146+
justifyContent={JUSTIFY_FLEX_END}
147+
gridGap={SPACING.spacing20}
148+
paddingTop={SPACING.spacing12}
93149
>
94-
<Flex alignItems={ALIGN_CENTER} justifyContent={JUSTIFY_CENTER}>
95-
<Icon
96-
size="2rem"
97-
name={isCopied ? 'check' : 'copy-text'}
98-
color={COLORS.white}
150+
<HoverShadow
151+
onClick={() => {
152+
setInputFieldToCorrespondingRequest()
153+
}}
154+
>
155+
<StyledIcon size={SPACING.spacing20} name={'reload'} />
156+
</HoverShadow>
157+
<HoverShadow
158+
onClick={() => {
159+
setShowFeedbackModal(true)
160+
}}
161+
>
162+
<StyledIcon size={SPACING.spacing20} name={'thumbs-down'} />
163+
</HoverShadow>
164+
<HoverShadow
165+
onClick={async () => {
166+
await handleClickCopy()
167+
}}
168+
>
169+
<StyledIcon
170+
size={SPACING.spacing20}
171+
name={isCopied ? 'check' : 'content-copy'}
99172
/>
100-
</Flex>
101-
</PrimaryButton>
173+
</HoverShadow>
174+
<HoverShadow
175+
onClick={() => {
176+
handleFileDownload()
177+
}}
178+
>
179+
<StyledIcon size={SPACING.spacing20} name={'download'} />
180+
</HoverShadow>
181+
</Flex>
102182
) : null}
103183
</Flex>
104184
</Flex>

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ export function ChatFooter(): JSX.Element {
1515

1616
return (
1717
<Flex
18+
paddingTop={SPACING.spacing24}
1819
gridGap={SPACING.spacing24}
1920
flexDirection={DIRECTION_COLUMN}
20-
paddingBottom={SPACING.spacing24}
2121
>
2222
<InputPrompt />
2323
<LegacyStyledText css={DISCLAIMER_TEXT_STYLE}>
@@ -32,5 +32,4 @@ const DISCLAIMER_TEXT_STYLE = css`
3232
font-size: ${TYPOGRAPHY.fontSize20};
3333
line-height: ${TYPOGRAPHY.lineHeight24};
3434
text-align: ${TYPOGRAPHY.textAlignCenter};
35-
padding-bottom: ${SPACING.spacing24};
3635
`
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { FeedbackModal } from '..'
2+
import { renderWithProviders } from '../../../__testing-utils__'
3+
import { screen } from '@testing-library/react'
4+
import { describe, it, expect } from 'vitest'
5+
import { i18n } from '../../../i18n'
6+
import { feedbackModalAtom } from '../../../resources/atoms'
7+
8+
const initialValues: Array<[any, any]> = [[feedbackModalAtom, true]]
9+
10+
const render = (): ReturnType<typeof renderWithProviders> => {
11+
return renderWithProviders(<FeedbackModal />, {
12+
i18nInstance: i18n,
13+
initialValues,
14+
})
15+
}
16+
17+
describe('FeedbackModal', () => {
18+
it('should render Feedback modal', () => {
19+
render()
20+
screen.getByText('Send feedback to Opentrons')
21+
screen.getByText('Share why the response was not helpful')
22+
screen.getByText('Cancel')
23+
screen.getByText('Send feedback')
24+
})
25+
26+
// should move this test to the chat page
27+
it.skip('should set the showFeedbackModel atom to be false when cancel button is clicked', () => {
28+
render()
29+
expect(feedbackModalAtom.init).toBe(true)
30+
31+
const cancelButton = screen.getByText('Cancel')
32+
cancelButton.click()
33+
// check if the feedbackModalAtom is set to false
34+
expect(feedbackModalAtom.read).toBe(false)
35+
})
36+
})
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {
2+
Modal,
3+
Flex,
4+
SPACING,
5+
ALIGN_FLEX_END,
6+
SecondaryButton,
7+
StyledText,
8+
PrimaryButton,
9+
InputField,
10+
} from '@opentrons/components'
11+
import { useAtom } from 'jotai'
12+
import { useTranslation } from 'react-i18next'
13+
import { feedbackModalAtom } from '../../resources/atoms'
14+
import { useState } from 'react'
15+
16+
export function FeedbackModal(): JSX.Element {
17+
const { t } = useTranslation('protocol_generator')
18+
19+
const [feedbackValue, setFeedbackValue] = useState<string>('')
20+
const [, setShowFeedbackModal] = useAtom(feedbackModalAtom)
21+
22+
return (
23+
<Modal
24+
title={t(`send_feedback_to_opentrons`)}
25+
onClose={() => {
26+
setShowFeedbackModal(false)
27+
}}
28+
footer={
29+
<Flex
30+
padding={`0 ${SPACING.spacing24} ${SPACING.spacing24}`}
31+
gridGap={SPACING.spacing8}
32+
justifyContent={ALIGN_FLEX_END}
33+
>
34+
<SecondaryButton
35+
onClick={() => {
36+
setShowFeedbackModal(false)
37+
}}
38+
>
39+
<StyledText desktopStyle="bodyDefaultSemiBold">
40+
{t(`cancel`)}
41+
</StyledText>
42+
</SecondaryButton>
43+
<PrimaryButton
44+
onClick={() => {
45+
setShowFeedbackModal(false)
46+
}}
47+
>
48+
<StyledText desktopStyle="bodyDefaultSemiBold">
49+
{t(`send_feedback`)}
50+
</StyledText>
51+
</PrimaryButton>
52+
</Flex>
53+
}
54+
>
55+
<InputField
56+
title={t(`send_feedback_input_title`)}
57+
size="medium"
58+
value={feedbackValue}
59+
onChange={event => {
60+
setFeedbackValue(event.target.value as string)
61+
}}
62+
></InputField>
63+
</Modal>
64+
)
65+
}

0 commit comments

Comments
 (0)