diff --git a/opentrons-ai-client/src/OpentronsAIRoutes.tsx b/opentrons-ai-client/src/OpentronsAIRoutes.tsx index b790b262ebe..32d09f351cf 100644 --- a/opentrons-ai-client/src/OpentronsAIRoutes.tsx +++ b/opentrons-ai-client/src/OpentronsAIRoutes.tsx @@ -1,10 +1,18 @@ 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', @@ -12,7 +20,7 @@ const opentronsAIRoutes: RouteProps[] = [ path: '/new-protocol', }, { - Component: Landing, + Component: UpdateProtocol, name: 'Update An Existing Protocol', navLinkTo: '/update-protocol', path: '/update-protocol', 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 0f38061ff47..e3e23b4f0d3 100644 --- a/opentrons-ai-client/src/assets/localization/en/protocol_generator.json +++ b/opentrons-ai-client/src/assets/localization/en/protocol_generator.json @@ -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 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.", "exit": "Exit", "exit_confirmation_title": "Are you sure you want to exit?", "exit_confirmation_body": "Exiting now will discard your progress.", @@ -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", @@ -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 Privacy Policy and End user license agreement", + "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.", @@ -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": "What if you don’t provide all of those pieces of information? OpentronsAI asks you to provide it!", "what_typeof_protocol": "What type of protocol do you need?", diff --git a/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx index 4eaa840dbcb..dd39d415acc 100644 --- a/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx +++ b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx @@ -18,6 +18,7 @@ import { SPACING, LegacyStyledText, TYPOGRAPHY, + OVERFLOW_AUTO, } from '@opentrons/components' import type { ChatData } from '../../resources/types' @@ -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} 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/molecules/InputPrompt/index.tsx b/opentrons-ai-client/src/molecules/InputPrompt/index.tsx index d4c4cdf5f8d..70ee01560f4 100644 --- a/opentrons-ai-client/src/molecules/InputPrompt/index.tsx +++ b/opentrons-ai-client/src/molecules/InputPrompt/index.tsx @@ -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 { @@ -29,7 +34,8 @@ 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) @@ -37,6 +43,10 @@ export function InputPrompt(): JSX.Element { const userPrompt = watch('userPrompt') ?? '' const { data, isLoading, callApi } = useApiCall() + useEffect(() => { + setValue('userPrompt', chatPromptAtomValue) + }, [chatPromptAtomValue, setValue]) + const handleClick = async (): Promise => { const userInput: ChatData = { role: 'user', diff --git a/opentrons-ai-client/src/molecules/UploadInput/index.tsx b/opentrons-ai-client/src/molecules/UploadInput/index.tsx new file mode 100644 index 00000000000..77dc5a2616d --- /dev/null +++ b/opentrons-ai-client/src/molecules/UploadInput/index.tsx @@ -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(null) + const [isFileOverDropZone, setIsFileOverDropZone] = React.useState( + false + ) + const [isHover, setIsHover] = React.useState(false) + const handleDrop: React.DragEventHandler = e => { + e.preventDefault() + e.stopPropagation() + Array.from(e.dataTransfer.files).forEach(f => onUpload(f)) + setIsFileOverDropZone(false) + } + const handleDragEnter: React.DragEventHandler = e => { + e.preventDefault() + e.stopPropagation() + } + const handleDragLeave: React.DragEventHandler = e => { + e.preventDefault() + e.stopPropagation() + setIsFileOverDropZone(false) + setIsHover(false) + } + const handleDragOver: React.DragEventHandler = e => { + e.preventDefault() + e.stopPropagation() + setIsFileOverDropZone(true) + setIsHover(true) + } + + const handleClick: React.MouseEventHandler = _event => { + onClick != null ? onClick() : fileInput.current?.click() + } + + const onChange: React.ChangeEventHandler = event => { + ;[...(event.target.files ?? [])].forEach(f => onUpload(f)) + if ('value' in event.currentTarget) event.currentTarget.value = '' + } + + return ( + + {uploadText != null ? ( + <> + {typeof uploadText === 'string' ? ( + + {uploadText} + + ) : ( + <>{uploadText} + )} + + ) : null} + + {uploadButtonText ?? t('upload')} + + + { + setIsHover(true) + }} + onMouseLeave={() => { + setIsHover(false) + }} + css={isFileOverDropZone ? DRAG_OVER_STYLES : undefined} + > + + {dragAndDropText} + + + + ) +} diff --git a/opentrons-ai-client/src/organisms/MainContentContainer/__tests__/MainContentContainer.test.tsx b/opentrons-ai-client/src/organisms/MainContentContainer/__tests__/MainContentContainer.test.tsx index 4598eddc49e..d3014a5895b 100644 --- a/opentrons-ai-client/src/organisms/MainContentContainer/__tests__/MainContentContainer.test.tsx +++ b/opentrons-ai-client/src/organisms/MainContentContainer/__tests__/MainContentContainer.test.tsx @@ -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') }) diff --git a/opentrons-ai-client/src/organisms/MainContentContainer/index.tsx b/opentrons-ai-client/src/organisms/MainContentContainer/index.tsx index b5b495a691e..cc2ad54bc39 100644 --- a/opentrons-ai-client/src/organisms/MainContentContainer/index.tsx +++ b/opentrons-ai-client/src/organisms/MainContentContainer/index.tsx @@ -1,15 +1,12 @@ import { useRef, useEffect } from 'react' -import { useTranslation } from 'react-i18next' import styled from 'styled-components' import { useAtom } from 'jotai' import { - COLORS, DIRECTION_COLUMN, Flex, OVERFLOW_AUTO, SPACING, - LegacyStyledText, } from '@opentrons/components' import { PromptGuide } from '../../molecules/PromptGuide' import { ChatDisplay } from '../../molecules/ChatDisplay' @@ -17,7 +14,6 @@ import { ChatFooter } from '../../molecules/ChatFooter' import { chatDataAtom } from '../../resources/atoms' export function MainContentContainer(): JSX.Element { - const { t } = useTranslation('protocol_generator') const [chatData] = useAtom(chatDataAtom) const scrollRef = useRef(null) @@ -32,14 +28,11 @@ export function MainContentContainer(): JSX.Element { return ( - {/* Prompt Guide remain as a reference for users. */} - {t('opentronsai')} @@ -74,6 +65,5 @@ export function MainContentContainer(): JSX.Element { const ChatDataContainer = styled(Flex)` flex-direction: ${DIRECTION_COLUMN}; - grid-gap: ${SPACING.spacing40}; width: 100%; ` diff --git a/opentrons-ai-client/src/organisms/UpdateProtocol/__tests__/UpdateProtocol.test.tsx b/opentrons-ai-client/src/organisms/UpdateProtocol/__tests__/UpdateProtocol.test.tsx new file mode 100644 index 00000000000..04c3ad3b167 --- /dev/null +++ b/opentrons-ai-client/src/organisms/UpdateProtocol/__tests__/UpdateProtocol.test.tsx @@ -0,0 +1,125 @@ +import { fireEvent, screen, waitFor } from '@testing-library/react' +import { describe, it, vi, beforeEach, expect } from 'vitest' +import { renderWithProviders } from '../../../__testing-utils__' +import type { NavigateFunction } from 'react-router-dom' + +import { UpdateProtocol } from '../index' +import { i18n } from '../../../i18n' + +// global.Blob = BlobPolyfill as any +global.Blob = require('node:buffer').Blob + +const mockNavigate = vi.fn() +const mockUseTrackEvent = vi.fn() +const mockUseChatData = vi.fn() + +vi.mock('../../../resources/hooks/useTrackEvent', () => ({ + useTrackEvent: () => mockUseTrackEvent, +})) + +File.prototype.text = vi.fn().mockResolvedValue('test file content') + +vi.mock('../../../resources/chatDataAtom', () => ({ + chatDataAtom: () => mockUseChatData, +})) + +vi.mock('react-router-dom', async importOriginal => { + const reactRouterDom = await importOriginal() + return { + ...reactRouterDom, + useNavigate: () => mockNavigate, + } +}) + +const render = () => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('Update Protocol', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should render', () => { + render() + expect(screen.getByText('Update an existing protocol')).toBeInTheDocument() + expect(screen.getByText('Choose file')).toBeInTheDocument() + expect(screen.getByText('Protocol file')).toBeInTheDocument() + expect(screen.getByText('Choose file')).toBeInTheDocument() + expect(screen.getByText('Type of update')).toBeInTheDocument() + expect(screen.getByText('Select an option')).toBeInTheDocument() + expect( + screen.getByText('Provide details of changes you want to make') + ).toBeInTheDocument() + }) + + it('should update the file value when the file is uploaded', async () => { + render() + + const blobParts: BlobPart[] = [ + 'x = 1\n', + 'x = 2\n', + 'x = 3\n', + 'x = 4\n', + 'print("x is 1.")\n', + ] + + const file = new File(blobParts, 'test-file.py', { type: 'text/python' }) + + fireEvent.drop(screen.getByTestId('file_drop_zone'), { + dataTransfer: { + files: [file], + }, + }) + + await waitFor(() => { + expect(screen.getByText('test-file.py')).toBeInTheDocument() + }) + }) + + it('should not proceed when you click the submit prompt when the progress percentage is not 1.0', () => { + render() + expect(mockNavigate).not.toHaveBeenCalled() + }) + + it.skip('should call navigate to the chat page when the submit prompt button is clicked when progress is 1.0', async () => { + render() + + // upload file + const blobParts: BlobPart[] = [ + 'x = 1\n', + 'x = 2\n', + 'x = 3\n', + 'x = 4\n', + 'print("x is 1.")\n', + ] + const file = new File(blobParts, 'test-file.py', { type: 'text/python' }) + fireEvent.drop(screen.getByTestId('file_drop_zone'), { + dataTransfer: { + files: [file], + }, + }) + + // input description + const describeInput = screen.getByRole('textbox') + fireEvent.change(describeInput, { target: { value: 'Test description' } }) + + expect(screen.getByDisplayValue('Test description')).toBeInTheDocument() + + // select update type + const applicationDropdown = screen.getByText('Select an option') + fireEvent.click(applicationDropdown) + + const basicOtherOption = screen.getByText('Other') + fireEvent.click(basicOtherOption) + + const submitPromptButton = screen.getByText('Submit prompt') + await waitFor(() => { + expect(submitPromptButton).toBeEnabled() + submitPromptButton.click() + }) + expect(mockNavigate).toHaveBeenCalledWith('/chat') + }) +}) diff --git a/opentrons-ai-client/src/organisms/UpdateProtocol/index.tsx b/opentrons-ai-client/src/organisms/UpdateProtocol/index.tsx new file mode 100644 index 00000000000..f0f8f4c7e12 --- /dev/null +++ b/opentrons-ai-client/src/organisms/UpdateProtocol/index.tsx @@ -0,0 +1,287 @@ +import styled from 'styled-components' +import { + COLORS, + DIRECTION_COLUMN, + DIRECTION_ROW, + Flex, + InputField, + JUSTIFY_CENTER, + JUSTIFY_END, + LargeButton, + StyledText, + Link as LinkComponent, + DropdownMenu, +} from '@opentrons/components' +import type { DropdownOption } from '@opentrons/components' +import { UploadInput } from '../../molecules/UploadInput' +import { useEffect, useState } from 'react' +import type { ChangeEvent } from 'react' +import { Trans, useTranslation } from 'react-i18next' +import { FileUpload } from '../../molecules/FileUpload' +import { useNavigate } from 'react-router-dom' +import { chatPromptAtom, headerWithMeterAtom } from '../../resources/atoms' +import { CSSTransition } from 'react-transition-group' +import { useAtom } from 'jotai' + +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' }, +] + +const FadeWrapper = styled.div` + &.fade-enter { + opacity: 0; + } + &.fade-enter-active { + opacity: 1; + transition: opacity 1000ms; + } + &.fade-exit { + height: 100%; + opacity: 1; + } + &.fade-exit-active { + opacity: 0; + height: 0%; + transition: opacity 1000ms; + } +` + +const Container = styled(Flex)` + width: 100%; + flex-direction: ${DIRECTION_COLUMN}; + align-items: ${JUSTIFY_CENTER}; +` + +const Spacer = styled(Flex)` + height: 16px; +` + +const ContentBox = styled(Flex)` + background-color: white; + border-radius: 16px; + flex-direction: ${DIRECTION_COLUMN}; + justify-content: ${JUSTIFY_CENTER}; + padding: 32px 24px; + width: 60%; +` + +const HeadingText = styled(StyledText).attrs({ + desktopStyle: 'headingSmallBold', +})`` + +const BodyText = styled(StyledText).attrs({ + color: COLORS.grey60, + desktopStyle: 'bodyDefaultRegular', + paddingBottom: '8px', + paddingTop: '16px', +})`` + +const isValidProtocolFileName = (protocolFileName: string): boolean => { + return protocolFileName.endsWith('.py') +} + +export function UpdateProtocol(): JSX.Element { + const navigate = useNavigate() + const { t }: { t: (key: string) => string } = useTranslation( + 'protocol_generator' + ) + const [, setChatPrompt] = useAtom(chatPromptAtom) + const [headerState, setHeaderWithMeterAtom] = useAtom(headerWithMeterAtom) + 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 !== null) { + progress += 0.33 + } + + if (detailsValue !== '') { + progress += 0.33 + } + + if (pythonText !== '' && fileValue !== null && errorText === null) { + progress += 0.34 + } + + setHeaderWithMeterAtom({ + displayHeaderWithMeter: true, + progress, + }) + }, [ + updateType, + detailsValue, + pythonText, + errorText, + fileValue, + setHeaderWithMeterAtom, + ]) + + const handleInputChange = (event: ChangeEvent): void => { + setDetailsValue(event.target.value) + } + + const handleFileUpload = async ( + file: File & { name: string } + ): Promise => { + if (isValidProtocolFileName(file.name)) { + const text = await file.text().catch(error => { + console.error('Error reading file:', error) + setErrorText(t('python_file_read_error')) + }) + + if (typeof text === 'string' && text !== '') { + setErrorText(null) + console.log('File read successfully:\n', text) + setPythonTextValue(text) + } else { + setErrorText(t('file_length_error')) + } + + setFile(file) + } else { + setErrorText(t('python_file_type_error')) + setFile(file) + } + } + + function processDataAndNavigateToChat(): void { + const introText = t('modify_intro') + const originalCodeText = + t('modify_python_code') + `\`\`\`python\n` + pythonText + `\n\`\`\`\n\n` + const updateTypeText = + t('modify_type_of_update') + updateType?.value + `\n\n` + const detailsText = t('modify_details_of_change') + detailsValue + '\n' + + const chatPrompt = `${introText}${originalCodeText}${updateTypeText}${detailsText}` + + console.log(chatPrompt) + + setChatPrompt(chatData => chatPrompt) + navigate('/chat') + } + + return ( + + + + {t('update_existing_protocol')} + {t('protocol_file')} + + + + {fileValue !== null ? ( + + + + ) : null} + + + + + + + + ), + }} + /> + + } + onUpload={async function (file: File) { + try { + await handleFileUpload(file) + } catch (error) { + // todo perhaps make this a toast? + console.error('Error uploading file:', error) + } + }} + /> + + + + + + { + const selectedOption = updateOptions.find(v => v.value === value) + if (selectedOption != null) { + setUpdateType(selectedOption) + } + }} + /> + + {t('provide_details_of_changes')} + + + + + + + ) +} diff --git a/opentrons-ai-client/src/pages/Chat/index.tsx b/opentrons-ai-client/src/pages/Chat/index.tsx new file mode 100644 index 00000000000..6d9492038b8 --- /dev/null +++ b/opentrons-ai-client/src/pages/Chat/index.tsx @@ -0,0 +1,38 @@ +import { useForm, FormProvider } from 'react-hook-form' +import { + DIRECTION_COLUMN, + DIRECTION_ROW, + Flex, + JUSTIFY_CENTER, + POSITION_RELATIVE, +} from '@opentrons/components' + +import { MainContentContainer } from '../../organisms/MainContentContainer' + +export interface InputType { + userPrompt: string +} + +export function Chat(): JSX.Element | null { + const methods = useForm({ + defaultValues: { + userPrompt: '', + }, + }) + + return ( + + + + {/* */} + + + + + ) +} diff --git a/opentrons-ai-client/src/resources/atoms.ts b/opentrons-ai-client/src/resources/atoms.ts index 229e9fbe739..b92f3e848f5 100644 --- a/opentrons-ai-client/src/resources/atoms.ts +++ b/opentrons-ai-client/src/resources/atoms.ts @@ -11,6 +11,9 @@ import type { /** ChatDataAtom is for chat data (user prompt and response from OpenAI API) */ export const chatDataAtom = atom([]) +/** ChatPromptAtom is for the prefilled userprompt when landing on the chat page */ +export const chatPromptAtom = atom('') + export const chatHistoryAtom = atom([]) export const tokenAtom = atom(null)