Skip to content

Commit

Permalink
feat(opentrons-ai-client): Created an update protocol page (#16569)
Browse files Browse the repository at this point in the history
…ol page basic layout

<!--
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 a new page that allows us to upload an existing protocol for
modification. It has error states for file size is zero and when
uploading a non-python file.

Drop down options are:
const updateOptions: DropdownOption[] = [
  {
    name: 'Adapt Python protocol from OT-2 to Flex',
    value: 'adapt_python_protocol',
  },
  { name: 'Change labware', value: 'change_labware' },
  { name: 'Change pipettes', value: 'change_pipettes' },
  { name: 'Other', value: 'other' },
]

START STATE:
<img width="755" alt="image"
src="https://github.com/user-attachments/assets/615d4a89-d998-4be5-95a2-0e792565f20c">

EMPTY FILE:
<img width="1503" alt="image"
src="https://github.com/user-attachments/assets/068ee486-6cde-4ca9-9b76-f9b401f46de5">

NON-PYTHON FILE:
<img width="1503" alt="image"
src="https://github.com/user-attachments/assets/daf2ba25-3a0f-469e-8683-5bc9b074108d">

ALL FIELDS FILLED OUT:
<img width="1508" alt="image"
src="https://github.com/user-attachments/assets/107e4d63-8243-4962-b1e6-6f345eef3471">


<!--
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

Tested non python files and empty python files. Made sure the prompt
text is correctly modified to include the fields filled out.
<!--
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]>
  • Loading branch information
connected-znaim and fbelginetw authored Nov 6, 2024
1 parent f66ffb9 commit 06cde70
Show file tree
Hide file tree
Showing 12 changed files with 732 additions and 15 deletions.
10 changes: 9 additions & 1 deletion opentrons-ai-client/src/OpentronsAIRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import { Route, Navigate, Routes } from 'react-router-dom'
import { Landing } from './pages/Landing'
import { UpdateProtocol } from './organisms/UpdateProtocol'

import type { RouteProps } from './resources/types'
import { Chat } from './pages/Chat'
import { CreateProtocol } from './pages/CreateProtocol'

const opentronsAIRoutes: RouteProps[] = [
{
Component: Chat,
name: 'Chat',
navLinkTo: '/chat',
path: '/chat',
},
{
Component: CreateProtocol,
name: 'Create A New Protocol',
navLinkTo: '/new-protocol',
path: '/new-protocol',
},
{
Component: Landing,
Component: UpdateProtocol,
name: 'Update An Existing Protocol',
navLinkTo: '/update-protocol',
path: '/update-protocol',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
"commands": "Commands: List the protocol's steps, specifying quantities in microliters (uL) and giving exact source and destination locations.",
"copyright": "Copyright © 2024 Opentrons",
"copy_code": "Copy code",
"choose_file": "Choose file",
"disclaimer": "OpentronsAI can make mistakes. Review your protocol before running it on an Opentrons robot.",
"drag_and_drop": "Drag and drop or <a>browse</a> your files",
"example": "For example prompts, click the buttons in the left panel.",
"file_length_error": "The length of the file contents is 0. Please upload a file with content.",
"exit": "Exit",
"exit_confirmation_title": "Are you sure you want to exit?",
"exit_confirmation_body": "Exiting now will discard your progress.",
Expand All @@ -26,6 +29,10 @@
"login": "Login",
"logout": "Logout",
"make_sure_your_prompt": "Write a prompt in a natural language for OpentronsAI to generate a protocol using the Opentrons Python Protocol API v2. The better the prompt, the better the quality of the protocol produced by OpentronsAI.",
"modify_intro": "Modify the following Python code using the Opentrons Python Protocol API v2. Ensure that the new labware and pipettes are compatible with the Flex robot. Ensure that you perform the correct Type of Update use the Details of Changes.\n\n",
"modify_python_code": "Original Python Code:\n",
"modify_type_of_update": "Type of update:\n- ",
"modify_details_of_change": "Details of Changes:\n- ",
"modules_and_adapters": "Modules and adapters: Specify the modules and labware adapters required by your protocol.",
"notes": "A few important things to note:",
"opentrons": "Opentrons",
Expand All @@ -35,6 +42,9 @@
"pcr": "PCR",
"pipettes": "Pipettes: Specify your pipettes, including the volume, number of channels, and whether they’re mounted on the left or right.",
"privacy_policy": "By continuing, you agree to the Opentrons <privacyPolicyLink>Privacy Policy</privacyPolicyLink> and <EULALink>End user license agreement</EULALink>",
"protocol_file": "Protocol file",
"provide_details_of_changes": "Provide details of changes you want to make",
"python_file_type_error": "Python file type required",
"reagent_transfer_flex": "Reagent Transfer (Flex)",
"reagent_transfer": "Reagent Transfer",
"reload_page": "To start over and create a new protocol, simply reload the page.",
Expand All @@ -44,8 +54,11 @@
"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.",
"side_panel_header": "Use natural language to generate protocols with OpentronsAI powered by OpenAI",
"simulate_description": "Once OpentronsAI has written your protocol, type `simulate` in the prompt box to try it out.",
"submit_prompt": "Submit prompt",
"try_example_prompts": "Stuck? Try these example prompts to get started.",
"type_of_update": "Type of update",
"type_your_prompt": "Type your prompt...",
"update_existing_protocol": "Update an existing protocol",
"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?",
Expand Down
2 changes: 2 additions & 0 deletions opentrons-ai-client/src/molecules/ChatDisplay/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
SPACING,
LegacyStyledText,
TYPOGRAPHY,
OVERFLOW_AUTO,
} from '@opentrons/components'

import type { ChatData } from '../../resources/types'
Expand Down Expand Up @@ -63,6 +64,7 @@ export function ChatDisplay({ chat, chatId }: ChatDisplayProps): JSX.Element {
data-testid={`ChatDisplay_from_${isUser ? 'user' : 'backend'}`}
borderRadius={BORDERS.borderRadius12}
width="100%"
overflowY={OVERFLOW_AUTO}
flexDirection={DIRECTION_COLUMN}
gridGap={SPACING.spacing16}
position={POSITION_RELATIVE}
Expand Down
74 changes: 74 additions & 0 deletions opentrons-ai-client/src/molecules/FileUpload/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { css } from 'styled-components'

import {
ALIGN_CENTER,
BORDERS,
Btn,
COLORS,
DIRECTION_COLUMN,
Flex,
Icon,
JUSTIFY_SPACE_BETWEEN,
SPACING,
LegacyStyledText,
truncateString,
} from '@opentrons/components'

const FILE_UPLOAD_STYLE = css`
&:hover > svg {
background: ${COLORS.black90}${COLORS.opacity20HexCode};
}
&:active > svg {
background: ${COLORS.black90}${COLORS.opacity20HexCode}};
}
`

const FILE_UPLOAD_FOCUS_VISIBLE = css`
&:focus-visible {
border-radius: ${BORDERS.borderRadius4};
box-shadow: 0 0 0 ${SPACING.spacing2} ${COLORS.blue50};
}
`

interface FileUploadProps {
file: File
fileError: string | null
handleClick: () => unknown
}

export function FileUpload({
file,
fileError,
handleClick,
}: FileUploadProps): JSX.Element {
return (
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing4}>
<Btn
onClick={handleClick}
aria-label="remove_file"
css={FILE_UPLOAD_FOCUS_VISIBLE}
>
<Flex
alignItems={ALIGN_CENTER}
backgroundColor={fileError == null ? COLORS.grey20 : COLORS.red30}
borderRadius={BORDERS.borderRadius4}
height={SPACING.spacing44}
justifyContent={JUSTIFY_SPACE_BETWEEN}
padding={SPACING.spacing8}
css={FILE_UPLOAD_STYLE}
>
<LegacyStyledText as="p">
{truncateString(file.name, 34, 19)}
</LegacyStyledText>
<Icon name="close" size="1.5rem" borderRadius="50%" />
</Flex>
</Btn>
{fileError != null ? (
<LegacyStyledText as="label" color={COLORS.red50}>
{fileError}
</LegacyStyledText>
) : null}
</Flex>
)
}
14 changes: 12 additions & 2 deletions opentrons-ai-client/src/molecules/InputPrompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ import {
TYPOGRAPHY,
} from '@opentrons/components'
import { SendButton } from '../../atoms/SendButton'
import { chatDataAtom, chatHistoryAtom, tokenAtom } from '../../resources/atoms'
import {
chatDataAtom,
chatHistoryAtom,
chatPromptAtom,
tokenAtom,
} from '../../resources/atoms'
import { useApiCall } from '../../resources/hooks'
import { calcTextAreaHeight } from '../../resources/utils/utils'
import {
Expand All @@ -29,14 +34,19 @@ import type { ChatData } from '../../resources/types'

export function InputPrompt(): JSX.Element {
const { t } = useTranslation('protocol_generator')
const { register, watch, reset } = useFormContext()
const { register, watch, reset, setValue } = useFormContext()
const [chatPromptAtomValue] = useAtom(chatPromptAtom)
const [, setChatData] = useAtom(chatDataAtom)
const [chatHistory, setChatHistory] = useAtom(chatHistoryAtom)
const [token] = useAtom(tokenAtom)
const [submitted, setSubmitted] = useState<boolean>(false)
const userPrompt = watch('userPrompt') ?? ''
const { data, isLoading, callApi } = useApiCall()

useEffect(() => {
setValue('userPrompt', chatPromptAtomValue)
}, [chatPromptAtomValue, setValue])

const handleClick = async (): Promise<void> => {
const userInput: ChatData = {
role: 'user',
Expand Down
168 changes: 168 additions & 0 deletions opentrons-ai-client/src/molecules/UploadInput/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import * as React from 'react'
import styled, { css } from 'styled-components'
import { useTranslation } from 'react-i18next'
import {
ALIGN_CENTER,
BORDERS,
COLORS,
CURSOR_POINTER,
DIRECTION_COLUMN,
DISPLAY_FLEX,
Flex,
Icon,
JUSTIFY_CENTER,
LegacyStyledText,
POSITION_FIXED,
PrimaryButton,
SPACING,
TYPOGRAPHY,
} from '@opentrons/components'

const StyledLabel = styled.label`
display: ${DISPLAY_FLEX};
cursor: ${CURSOR_POINTER};
flex-direction: ${DIRECTION_COLUMN};
align-items: ${ALIGN_CENTER};
width: 100%;
padding: ${SPACING.spacing32};
border: 2px dashed ${COLORS.grey30};
border-radius: ${BORDERS.borderRadius4};
text-align: ${TYPOGRAPHY.textAlignCenter};
background-color: ${COLORS.white};
&:hover {
border: 2px dashed ${COLORS.blue50};
}
`
const DRAG_OVER_STYLES = css`
border: 2px dashed ${COLORS.blue50};
`

const StyledInput = styled.input`
position: ${POSITION_FIXED};
clip: rect(1px 1px 1px 1px);
`

export interface UploadInputProps {
/** Callback function that is called when a file is uploaded. */
onUpload: (file: File) => unknown
/** Optional callback function that is called when the upload button is clicked. */
onClick?: () => void
/** Optional text for the upload button. If undefined, the button displays Upload */
uploadButtonText?: string
/** Optional text or JSX element that is displayed above the upload button. */
uploadText?: string | JSX.Element
/** Optional text or JSX element that is displayed in the drag and drop area. */
dragAndDropText?: string | JSX.Element
}

export function UploadInput(props: UploadInputProps): JSX.Element | null {
const {
dragAndDropText,
onClick,
onUpload,
uploadButtonText,
uploadText,
} = props
const { t } = useTranslation('protocol_info')

const fileInput = React.useRef<HTMLInputElement>(null)
const [isFileOverDropZone, setIsFileOverDropZone] = React.useState<boolean>(
false
)
const [isHover, setIsHover] = React.useState<boolean>(false)
const handleDrop: React.DragEventHandler<HTMLLabelElement> = e => {
e.preventDefault()
e.stopPropagation()
Array.from(e.dataTransfer.files).forEach(f => onUpload(f))
setIsFileOverDropZone(false)
}
const handleDragEnter: React.DragEventHandler<HTMLLabelElement> = e => {
e.preventDefault()
e.stopPropagation()
}
const handleDragLeave: React.DragEventHandler<HTMLLabelElement> = e => {
e.preventDefault()
e.stopPropagation()
setIsFileOverDropZone(false)
setIsHover(false)
}
const handleDragOver: React.DragEventHandler<HTMLLabelElement> = e => {
e.preventDefault()
e.stopPropagation()
setIsFileOverDropZone(true)
setIsHover(true)
}

const handleClick: React.MouseEventHandler<HTMLButtonElement> = _event => {
onClick != null ? onClick() : fileInput.current?.click()
}

const onChange: React.ChangeEventHandler<HTMLInputElement> = event => {
;[...(event.target.files ?? [])].forEach(f => onUpload(f))
if ('value' in event.currentTarget) event.currentTarget.value = ''
}

return (
<Flex
height="100%"
flexDirection={DIRECTION_COLUMN}
justifyContent={JUSTIFY_CENTER}
alignItems={ALIGN_CENTER}
gridGap={SPACING.spacing24}
>
{uploadText != null ? (
<>
{typeof uploadText === 'string' ? (
<LegacyStyledText
as="p"
textAlign={TYPOGRAPHY.textAlignCenter}
marginTop={SPACING.spacing16}
>
{uploadText}
</LegacyStyledText>
) : (
<>{uploadText}</>
)}
</>
) : null}
<PrimaryButton
onClick={handleClick}
id="UploadInput_protocolUploadButton"
>
{uploadButtonText ?? t('upload')}
</PrimaryButton>

<StyledLabel
data-testid="file_drop_zone"
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onMouseEnter={() => {
setIsHover(true)
}}
onMouseLeave={() => {
setIsHover(false)
}}
css={isFileOverDropZone ? DRAG_OVER_STYLES : undefined}
>
<Icon
width="4rem"
color={isHover ? COLORS.blue50 : COLORS.grey60}
name="upload"
marginBottom={SPACING.spacing24}
/>
{dragAndDropText}
<StyledInput
id="file_input"
data-testid="file_input"
ref={fileInput}
type="file"
onChange={onChange}
multiple
/>
</StyledLabel>
</Flex>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ describe('MainContentContainer', () => {

it('should render prompt guide and text', () => {
render()
screen.getByText('OpentronsAI')
screen.getByText('mock PromptGuide')
screen.getByText('mock ChatFooter')
})
Expand Down
Loading

0 comments on commit 06cde70

Please sign in to comment.