diff --git a/opentrons-ai-client/src/assets/localization/en/protocol_generator.json b/opentrons-ai-client/src/assets/localization/en/protocol_generator.json index 65fa6653667..21db34105db 100644 --- a/opentrons-ai-client/src/assets/localization/en/protocol_generator.json +++ b/opentrons-ai-client/src/assets/localization/en/protocol_generator.json @@ -4,9 +4,11 @@ "application": "Application: Your protocol's name, describing what it does.", "commands": "Commands: List the protocol's steps, specifying quantities in microliters (uL) and giving exact source and destination locations.", "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 browse 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.", "got_feedback": "Got feedback? We love to hear it.", "key_info": "Here are some key pieces of information to provide in your prompt:", "labware_and_tipracks": "Labware and tip racks: Use names from the Opentrons Labware Library.", @@ -25,6 +27,7 @@ "pipettes": "Pipettes: Specify your pipettes, including the volume, number of channels, and whether they’re mounted on the left or right.", "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.", diff --git a/opentrons-ai-client/src/molecules/FileUpload/index.tsx b/opentrons-ai-client/src/molecules/FileUpload/index.tsx new file mode 100644 index 00000000000..551c3d0bd05 --- /dev/null +++ b/opentrons-ai-client/src/molecules/FileUpload/index.tsx @@ -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 ( + + + + + {truncateString(file.name, 34, 19)} + + + + + {fileError != null ? ( + + {fileError} + + ) : null} + + ) +} diff --git a/opentrons-ai-client/src/organisms/UpdateProtocol/index.tsx b/opentrons-ai-client/src/organisms/UpdateProtocol/index.tsx index dd19fd73722..2c4baa5fd41 100644 --- a/opentrons-ai-client/src/organisms/UpdateProtocol/index.tsx +++ b/opentrons-ai-client/src/organisms/UpdateProtocol/index.tsx @@ -3,7 +3,6 @@ import { COLORS, DIRECTION_COLUMN, DIRECTION_ROW, - DropdownField, Flex, InputField, JUSTIFY_CENTER, @@ -11,14 +10,17 @@ import { LargeButton, StyledText, Link as LinkComponent, + DropdownMenu, } from '@opentrons/components' +import type { DropdownOption } from '@opentrons/components' import { UploadInput } from '../../molecules/UploadInput' import { HeaderWithMeter } from '../../molecules/HeaderWithMeter' import { useEffect, useState } from 'react' import type { ChangeEvent } from 'react' import { Trans, useTranslation } from 'react-i18next' +import { FileUpload } from '../../molecules/FileUpload' -const updateOptions = [ +const updateOptions: DropdownOption[] = [ { name: 'Adapt Python protocol from OT-2 to Flex', value: 'adapt_python_protocol', @@ -47,11 +49,6 @@ const ContentBox = styled(Flex)` width: 60%; ` -const ContentFlex = styled(Flex)` - flex-direction: ${DIRECTION_COLUMN}; - justify-content: ${JUSTIFY_CENTER}; -` - const HeadingText = styled(StyledText).attrs({ desktopStyle: 'headingSmallBold', })`` @@ -67,15 +64,19 @@ const isValidProtocolFileName = (protocolFileName: string): boolean => { } export function UpdateProtocol(): JSX.Element { - const { t } = useTranslation('protocol_generator') + const { t }: { t: (key: string) => string } = useTranslation( + 'protocol_generator' + ) const [progressPercentage, setProgressPercentage] = useState(0.0) - const [updateType, setUpdateType] = useState('') + const [updateType, setUpdateType] = useState(null) const [detailsValue, setDetailsValue] = useState('') + const [fileValue, setFile] = useState(null) const [pythonText, setPythonTextValue] = useState('') + const [errorText, setErrorText] = useState(null) useEffect(() => { let progress = 0.0 - if (updateType !== '') { + if (updateType !== null) { progress += 0.33 } @@ -83,45 +84,38 @@ export function UpdateProtocol(): JSX.Element { progress += 0.33 } - if (pythonText !== '') { + if (pythonText !== '' && fileValue !== null && errorText === null) { progress += 0.34 } setProgressPercentage(progress) - }, [updateType, detailsValue, pythonText]) + }, [updateType, detailsValue, pythonText, errorText, fileValue]) const handleInputChange = (event: ChangeEvent): void => { setDetailsValue(event.target.value) } - const handleUpload = async ( - file: File2 & { name: string } + const handleFileUpload = async ( + file: File & { name: string } ): Promise => { - if (file.path === null) { - // logger.warn('Failed to upload file, path not found') - } if (isValidProtocolFileName(file.name)) { - // todo convert to text - - console.log('compatible_file_type') - console.log(file.name) - // const text = await new Response(new Blob([await file.text()])).text() const text = await file.text().catch(error => { console.error('Error reading file:', error) + setErrorText(t('python_file_read_error')) }) - console.log(text) - if (text) { + if (typeof text === 'string' && text !== '') { + setErrorText(null) setPythonTextValue(text) + } else { + setErrorText(t('file_length_error')) } + + setFile(file) } else { - console.log('incompatible_file_type') + setErrorText(t('python_file_type_error')) + setFile(file) } - // props.onUpload?.() - // trackEvent({ - // name: ANALYTICS_IMPORT_PROTOCOL_TO_APP, - // properties: { protocolFileName: file.name }, - // }) } return ( @@ -131,17 +125,28 @@ export function UpdateProtocol(): JSX.Element { > - - {t('update_existing_protocol')} - {t('protocol_file')} - + {t('update_existing_protocol')} + {t('protocol_file')} + + {fileValue !== null ? ( + + + + ) : ( - - - {t('type_of_update')} - ): void { - setUpdateType(event.target.value) + /> + )} + + + {t('type_of_update')} + + { + const selectedOption = updateOptions.find(v => v.value === value) + if (selectedOption != null) { + setUpdateType(selectedOption) + } }} /> - {t('provide_details_of_changes')} - + {t('provide_details_of_changes')} + + + - - - - + ) } - -interface File2 { - /** - * The real path to the file on the users filesystem - */ - path?: string - /** - * The name of the file - */ - name: string - - /** - * The contents of the file - */ - // contents: string - text: () => Promise -}