diff --git a/opentrons-ai-client/Makefile b/opentrons-ai-client/Makefile
index 9c15fa32e41..8afd804af4c 100644
--- a/opentrons-ai-client/Makefile
+++ b/opentrons-ai-client/Makefile
@@ -6,6 +6,9 @@ SHELL := bash
# add node_modules/.bin to PATH
PATH := $(shell cd .. && yarn bin):$(PATH)
+# dev server port
+PORT ?= 5173
+
benchmark_output := $(shell node -e 'console.log(new Date());')
# These variables can be overriden when make is invoked to customize the
@@ -42,6 +45,7 @@ build:
.PHONY: dev
dev: export NODE_ENV := development
+dev: export PORT := $(PORT)
dev:
vite serve
diff --git a/opentrons-ai-client/index.html b/opentrons-ai-client/index.html
index 57e7f83f591..4e8e60a4121 100644
--- a/opentrons-ai-client/index.html
+++ b/opentrons-ai-client/index.html
@@ -2,7 +2,8 @@
-
+
+
Opentrons AI
diff --git a/opentrons-ai-client/package.json b/opentrons-ai-client/package.json
index d8ea50136ff..dfd8069c7b1 100644
--- a/opentrons-ai-client/package.json
+++ b/opentrons-ai-client/package.json
@@ -21,7 +21,9 @@
"dependencies": {
"@fontsource/public-sans": "5.0.3",
"@opentrons/components": "link:../components",
+ "axios": "^0.21.1",
"i18next": "^19.8.3",
+ "jotai": "2.8.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-error-boundary": "^4.0.10",
diff --git a/opentrons-ai-client/src/assets/images/favicon/android-chrome-192x192.png b/opentrons-ai-client/src/assets/images/favicon/android-chrome-192x192.png
new file mode 100644
index 00000000000..468479c76e7
Binary files /dev/null and b/opentrons-ai-client/src/assets/images/favicon/android-chrome-192x192.png differ
diff --git a/opentrons-ai-client/src/assets/images/favicon/android-chrome-512x512.png b/opentrons-ai-client/src/assets/images/favicon/android-chrome-512x512.png
new file mode 100644
index 00000000000..bab2df65fdb
Binary files /dev/null and b/opentrons-ai-client/src/assets/images/favicon/android-chrome-512x512.png differ
diff --git a/opentrons-ai-client/src/assets/images/favicon/apple-touch-icon.png b/opentrons-ai-client/src/assets/images/favicon/apple-touch-icon.png
new file mode 100644
index 00000000000..ccbd74a497b
Binary files /dev/null and b/opentrons-ai-client/src/assets/images/favicon/apple-touch-icon.png differ
diff --git a/opentrons-ai-client/src/assets/images/favicon/favicon-16x16.png b/opentrons-ai-client/src/assets/images/favicon/favicon-16x16.png
new file mode 100644
index 00000000000..4edd2e8b352
Binary files /dev/null and b/opentrons-ai-client/src/assets/images/favicon/favicon-16x16.png differ
diff --git a/opentrons-ai-client/src/assets/images/favicon/favicon-32x32.png b/opentrons-ai-client/src/assets/images/favicon/favicon-32x32.png
new file mode 100644
index 00000000000..eed71c70949
Binary files /dev/null and b/opentrons-ai-client/src/assets/images/favicon/favicon-32x32.png differ
diff --git a/opentrons-ai-client/src/assets/images/favicon/favicon.ico b/opentrons-ai-client/src/assets/images/favicon/favicon.ico
new file mode 100644
index 00000000000..d1266c550bf
Binary files /dev/null and b/opentrons-ai-client/src/assets/images/favicon/favicon.ico differ
diff --git a/opentrons-ai-client/src/assets/images/favicon/site.webmanifest b/opentrons-ai-client/src/assets/images/favicon/site.webmanifest
new file mode 100644
index 00000000000..fe3af17b5d1
--- /dev/null
+++ b/opentrons-ai-client/src/assets/images/favicon/site.webmanifest
@@ -0,0 +1,19 @@
+{
+ "name": "opentrons_favicon",
+ "short_name": "favicon",
+ "icons": [
+ {
+ "src": "/android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/android-chrome-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ],
+ "theme_color": "#ffffff",
+ "background_color": "#ffffff",
+ "display": "standalone"
+}
diff --git a/opentrons-ai-client/src/atoms/SendButton/__tests__/SendButton.test.tsx b/opentrons-ai-client/src/atoms/SendButton/__tests__/SendButton.test.tsx
new file mode 100644
index 00000000000..dcf90ec1022
--- /dev/null
+++ b/opentrons-ai-client/src/atoms/SendButton/__tests__/SendButton.test.tsx
@@ -0,0 +1,53 @@
+import React from 'react'
+import { describe, it, vi, beforeEach, expect } from 'vitest'
+import { fireEvent, screen } from '@testing-library/react'
+import { renderWithProviders } from '../../../__testing-utils__'
+
+import { SendButton } from '../index'
+
+const mockHandleClick = vi.fn()
+const render = (props: React.ComponentProps) => {
+ return renderWithProviders()
+}
+
+describe('SendButton', () => {
+ let props: React.ComponentProps
+
+ beforeEach(() => {
+ props = {
+ handleClick: mockHandleClick,
+ disabled: true,
+ isLoading: false,
+ }
+ })
+ it('should render button with send icon and its initially disabled', () => {
+ render(props)
+ const button = screen.getByRole('button')
+ expect(button).toBeDisabled()
+ screen.getByTestId('SendButton_icon_send')
+ })
+
+ it('should render button and its not disabled when disabled false', () => {
+ props = { ...props, disabled: false }
+ render(props)
+ const button = screen.getByRole('button')
+ expect(button).not.toBeDisabled()
+ screen.getByTestId('SendButton_icon_send')
+ })
+
+ it('should render button with spinner icon when isLoading', () => {
+ props = { ...props, isLoading: true }
+ render(props)
+ const button = screen.getByRole('button')
+ expect(button).toBeDisabled()
+ screen.getByTestId('SendButton_icon_ot-spinner')
+ })
+
+ it('should call a mock function when clicking the button', () => {
+ props = { ...props, disabled: false }
+ render(props)
+ const button = screen.getByRole('button')
+ fireEvent.click(button)
+ expect(mockHandleClick).toHaveBeenCalled()
+ })
+})
diff --git a/opentrons-ai-client/src/atoms/SendButton/index.tsx b/opentrons-ai-client/src/atoms/SendButton/index.tsx
new file mode 100644
index 00000000000..e165762b2ab
--- /dev/null
+++ b/opentrons-ai-client/src/atoms/SendButton/index.tsx
@@ -0,0 +1,74 @@
+import React from 'react'
+import { css } from 'styled-components'
+
+import {
+ ALIGN_CENTER,
+ BORDERS,
+ Btn,
+ COLORS,
+ DISPLAY_FLEX,
+ Icon,
+ JUSTIFY_CENTER,
+} from '@opentrons/components'
+
+interface SendButtonProps {
+ handleClick: () => void
+ disabled?: boolean
+ isLoading?: boolean
+}
+
+export function SendButton({
+ handleClick,
+ disabled = false,
+ isLoading = false,
+}: SendButtonProps): JSX.Element {
+ const playButtonStyle = css`
+ -webkit-tap-highlight-color: transparent;
+ &:focus {
+ background-color: ${COLORS.blue60};
+ color: ${COLORS.white};
+ }
+
+ &:hover {
+ background-color: ${COLORS.blue50};
+ color: ${COLORS.white};
+ }
+
+ &:focus-visible {
+ background-color: ${COLORS.blue50};
+ }
+
+ &:active {
+ background-color: ${COLORS.blue60};
+ color: ${COLORS.white};
+ }
+
+ &:disabled {
+ background-color: ${COLORS.grey35};
+ color: ${COLORS.grey50};
+ }
+ `
+ return (
+
+
+
+ )
+}
diff --git a/opentrons-ai-client/src/main.tsx b/opentrons-ai-client/src/main.tsx
index a5719bc94d8..a2f1338bd7b 100644
--- a/opentrons-ai-client/src/main.tsx
+++ b/opentrons-ai-client/src/main.tsx
@@ -2,7 +2,6 @@ import React from 'react'
import ReactDOM from 'react-dom/client'
import { I18nextProvider } from 'react-i18next'
import { GlobalStyle } from './atoms/GlobalStyle'
-import { PromptProvider } from './organisms/PromptButton/PromptProvider'
import { i18n } from './i18n'
import { App } from './App'
@@ -13,9 +12,7 @@ if (rootElement != null) {
-
-
-
+
)
diff --git a/opentrons-ai-client/src/molecules/ChatDisplay/ChatDisplay.stories.tsx b/opentrons-ai-client/src/molecules/ChatDisplay/ChatDisplay.stories.tsx
index ae03a25f754..e3e0a1a6f36 100644
--- a/opentrons-ai-client/src/molecules/ChatDisplay/ChatDisplay.stories.tsx
+++ b/opentrons-ai-client/src/molecules/ChatDisplay/ChatDisplay.stories.tsx
@@ -24,7 +24,9 @@ type Story = StoryObj
export const OpentronsAI: Story = {
args: {
- content: `
+ chat: {
+ role: 'assistant',
+ content: `
## sample output from OpentronsAI
\`\`\`py
@@ -50,13 +52,15 @@ def run(protocol: protocol_api.ProtocolContext):
TEMP_DECK_WAIT_TIME = 50 # seconds
\`\`\`
`,
- isUserInput: false,
+ },
},
}
export const User: Story = {
args: {
- content: `
+ chat: {
+ role: 'user',
+ content: `
- Application: Reagent transfer
- Robot: OT-2
- API: 2.13
@@ -76,6 +80,6 @@ export const User: Story = {
to first well in the destination labware.
Use new tip for each transfer.
`,
- isUserInput: true,
+ },
},
}
diff --git a/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx b/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx
index 75b99717abb..98fd30274ee 100644
--- a/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx
+++ b/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx
@@ -15,8 +15,10 @@ describe('ChatDisplay', () => {
beforeEach(() => {
props = {
- content: 'mock text from the backend',
- isUserInput: false,
+ chat: {
+ role: 'assistant',
+ content: 'mock text from the backend',
+ },
}
})
it('should display response from the backend and label', () => {
@@ -29,8 +31,10 @@ describe('ChatDisplay', () => {
})
it('should display input from use and label', () => {
props = {
- content: 'mock text from user input',
- isUserInput: true,
+ chat: {
+ role: 'user',
+ content: 'mock text from user input',
+ },
}
render(props)
screen.getByText('You')
diff --git a/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx
index c2d52e6a593..3b5c54ebbc6 100644
--- a/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx
+++ b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx
@@ -1,4 +1,5 @@
import React from 'react'
+// import { css } from 'styled-components'
import { useTranslation } from 'react-i18next'
import Markdown from 'react-markdown'
import {
@@ -10,37 +11,82 @@ import {
StyledText,
} from '@opentrons/components'
+import type { ChatData } from '../../resources/types'
+
interface ChatDisplayProps {
- content: string
- isUserInput: boolean
+ chat: ChatData
}
-export function ChatDisplay({
- content,
- isUserInput,
-}: ChatDisplayProps): JSX.Element {
+export function ChatDisplay({ chat }: ChatDisplayProps): JSX.Element {
const { t } = useTranslation('protocol_generator')
+ const { role, content } = chat
+ const isUser = role === 'user'
return (
- {isUserInput ? t('you') : t('opentronsai')}
+ {isUser ? t('you') : t('opentronsai')}
{/* text should be markdown so this component will have a package or function to parse markdown */}
- {/* ToDo (kk:04/19/2024) I will get feedback for additional styling from the design team. */}
+ {/* ToDo (kk:05/02/2024) This part is waiting for Mel's design */}
+ {/*
+ {content}
+ */}
{content}
)
}
+
+// ToDo (kk:05/02/2024) This part is waiting for Mel's design
+// function ExternalLink(props: JSX.IntrinsicAttributes): JSX.Element {
+// return
+// }
+
+// function ParagraphText(props: JSX.IntrinsicAttributes): JSX.Element {
+// return
+// }
+
+// function HeaderText(props: JSX.IntrinsicAttributes): JSX.Element {
+// return
+// }
+
+// function ListItemText(props: JSX.IntrinsicAttributes): JSX.Element {
+// return
+// }
+
+// function UnnumberedListText(props: JSX.IntrinsicAttributes): JSX.Element {
+// return
+// }
+
+// const CODE_TEXT_STYLE = css`
+// padding: ${SPACING.spacing16};
+// font-family: monospace;
+// color: ${COLORS.white};
+// background-color: ${COLORS.black90};
+// `
+
+// function CodeText(props: JSX.IntrinsicAttributes): JSX.Element {
+// return
+// }
diff --git a/opentrons-ai-client/src/molecules/InputPrompt/index.tsx b/opentrons-ai-client/src/molecules/InputPrompt/index.tsx
index cdff4e7c44b..e8a8dda0c20 100644
--- a/opentrons-ai-client/src/molecules/InputPrompt/index.tsx
+++ b/opentrons-ai-client/src/molecules/InputPrompt/index.tsx
@@ -2,65 +2,115 @@ import React from 'react'
import { useTranslation } from 'react-i18next'
import styled, { css } from 'styled-components'
import { useForm } from 'react-hook-form'
+import { useAtom } from 'jotai'
+import axios from 'axios'
import {
ALIGN_CENTER,
BORDERS,
- Btn,
COLORS,
DIRECTION_ROW,
- DISPLAY_FLEX,
Flex,
- Icon,
JUSTIFY_CENTER,
SPACING,
TYPOGRAPHY,
} from '@opentrons/components'
-import { promptContext } from '../../organisms/PromptButton/PromptProvider'
-import type { SubmitHandler } from 'react-hook-form'
+import { SendButton } from '../../atoms/SendButton'
+import { preparedPromptAtom, chatDataAtom } from '../../resources/atoms'
-// ToDo (kk:04/19/2024) Note this interface will be used by prompt buttons in SidePanel
-// interface InputPromptProps {}
+import type { ChatData } from '../../resources/types'
+
+// ToDo (kk:05/02/2024) This url is temporary
+const url = 'http://localhost:8000/streaming/ask'
interface InputType {
userPrompt: string
}
-export function InputPrompt(/* props: InputPromptProps */): JSX.Element {
+export function InputPrompt(): JSX.Element {
const { t } = useTranslation('protocol_generator')
- const { register, handleSubmit, watch, setValue } = useForm({
+ const { register, watch, setValue, reset } = useForm({
defaultValues: {
userPrompt: '',
},
})
- const usePromptValue = (): string => React.useContext(promptContext)
- const promptFromButton = usePromptValue()
- const userPrompt = watch('userPrompt') ?? ''
+ const [preparedPrompt] = useAtom(preparedPromptAtom)
+ const [, setChatData] = useAtom(chatDataAtom)
+ const [submitted, setSubmitted] = React.useState(false)
- const onSubmit: SubmitHandler = async data => {
- // ToDo (kk: 04/19/2024) call api
- const { userPrompt } = data
- console.log('user prompt', userPrompt)
- }
+ const [data, setData] = React.useState(null)
+ const [loading, setLoading] = React.useState(false)
+ const [error, setError] = React.useState('')
+
+ const userPrompt = watch('userPrompt') ?? ''
const calcTextAreaHeight = (): number => {
const rowsNum = userPrompt.split('\n').length
return rowsNum
}
+ const fetchData = async (prompt: string): Promise => {
+ if (prompt !== '') {
+ setLoading(true)
+ try {
+ const response = await axios.post(url, {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ query: prompt,
+ })
+ setData(response.data)
+ } catch (err) {
+ setError('Error fetching data from the API.')
+ } finally {
+ setLoading(false)
+ }
+ }
+ }
+
+ const handleClick = (): void => {
+ const userInput: ChatData = {
+ role: 'user',
+ content: userPrompt,
+ }
+ setChatData(chatData => [...chatData, userInput])
+ void fetchData(userPrompt)
+ setSubmitted(true)
+ reset()
+ }
+
+ React.useEffect(() => {
+ if (preparedPrompt !== '') setValue('userPrompt', preparedPrompt as string)
+ }, [preparedPrompt, setValue])
+
React.useEffect(() => {
- if (promptFromButton !== '') setValue('userPrompt', promptFromButton)
- }, [promptFromButton, setValue])
+ if (submitted && data && !loading) {
+ const { role, content } = data.data
+ const assistantResponse: ChatData = {
+ role,
+ content,
+ }
+ setChatData(chatData => [...chatData, assistantResponse])
+ setSubmitted(false)
+ }
+ }, [data, loading, submitted])
+
+ // ToDo (kk:05/02/2024) This is also temp. Asking the design about error.
+ console.error('error', error)
return (
- handleSubmit(onSubmit)}>
+
-
+
)
@@ -106,65 +156,3 @@ const StyledTextarea = styled.textarea`
transform: translateY(-50%);
}
`
-
-interface PlayButtonProps {
- onPlay?: () => void
- disabled?: boolean
- isLoading?: boolean
-}
-
-function PlayButton({
- onPlay,
- disabled = false,
- isLoading = false,
-}: PlayButtonProps): JSX.Element {
- const playButtonStyle = css`
- -webkit-tap-highlight-color: transparent;
- &:focus {
- background-color: ${COLORS.blue60};
- color: ${COLORS.white};
- }
-
- &:hover {
- background-color: ${COLORS.blue50};
- color: ${COLORS.white};
- }
-
- &:focus-visible {
- background-color: ${COLORS.blue50};
- }
-
- &:active {
- background-color: ${COLORS.blue60};
- color: ${COLORS.white};
- }
-
- &:disabled {
- background-color: ${COLORS.grey35};
- color: ${COLORS.grey50};
- }
- `
- return (
-
-
-
- )
-}
diff --git a/opentrons-ai-client/src/organisms/ChatContainer/index.tsx b/opentrons-ai-client/src/organisms/ChatContainer/index.tsx
index be6c4d619da..8a120c65112 100644
--- a/opentrons-ai-client/src/organisms/ChatContainer/index.tsx
+++ b/opentrons-ai-client/src/organisms/ChatContainer/index.tsx
@@ -1,6 +1,8 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
-import { css } from 'styled-components'
+import styled, { css } from 'styled-components'
+import { useAtom } from 'jotai'
+
import {
COLORS,
DIRECTION_COLUMN,
@@ -13,10 +15,13 @@ import {
} from '@opentrons/components'
import { PromptGuide } from '../../molecules/PromptGuide'
import { InputPrompt } from '../../molecules/InputPrompt'
+import { ChatDisplay } from '../../molecules/ChatDisplay'
+import { chatDataAtom } from '../../resources/atoms'
export function ChatContainer(): JSX.Element {
const { t } = useTranslation('protocol_generator')
- const isDummyInitial = true
+ const [chatData] = useAtom(chatDataAtom)
+
return (
{/* This will be updated when input textbox and function are implemented */}
- {isDummyInitial ? (
+
+
+
+ {t('opentronsai')}
+ {/* Prompt Guide remain as a reference for users. */}
+
+ {chatData.length > 0
+ ? chatData.map((chat, index) => (
+
+ ))
+ : null}
+
-
- {t('opentronsai')}
-
-
-
-
-
- {t('disclaimer')}
-
-
+
+ {t('disclaimer')}
- ) : null}
+
)
}
+const ChatDataContainer = styled(Flex)`
+ max-height: calc(100vh);
+ overflow-y: auto;
+ flex-direction: ${DIRECTION_COLUMN};
+ grid-gap: ${SPACING.spacing12};
+ width: 100%;
+`
+
const DISCLAIMER_TEXT_STYLE = css`
color: ${COLORS.grey55};
font-size: ${TYPOGRAPHY.fontSize20};
diff --git a/opentrons-ai-client/src/organisms/PromptButton/PromptProvider.tsx b/opentrons-ai-client/src/organisms/PromptButton/PromptProvider.tsx
deleted file mode 100644
index f148e4fdd94..00000000000
--- a/opentrons-ai-client/src/organisms/PromptButton/PromptProvider.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react'
-
-export const promptContext = React.createContext('')
-export const setPromptContext = React.createContext<
- React.Dispatch>
->(() => undefined)
-
-interface PromptProviderProps {
- children: React.ReactNode
-}
-
-export function PromptProvider({
- children,
-}: PromptProviderProps): React.ReactElement {
- const [prompt, setPrompt] = React.useState('')
-
- return (
-
-
- {children}
-
-
- )
-}
diff --git a/opentrons-ai-client/src/organisms/PromptButton/__tests__/PromptButton.test.tsx b/opentrons-ai-client/src/organisms/PromptButton/__tests__/PromptButton.test.tsx
index b4dadfcc931..d4659cc3c84 100644
--- a/opentrons-ai-client/src/organisms/PromptButton/__tests__/PromptButton.test.tsx
+++ b/opentrons-ai-client/src/organisms/PromptButton/__tests__/PromptButton.test.tsx
@@ -1,20 +1,15 @@
import React from 'react'
-import { fireEvent, screen } from '@testing-library/react'
-import { describe, it, vi, beforeEach, expect } from 'vitest'
+import { useAtom } from 'jotai'
+import { fireEvent, screen, renderHook } from '@testing-library/react'
+import { describe, it, beforeEach, expect } from 'vitest'
import { renderWithProviders } from '../../../__testing-utils__'
-import { setPromptContext } from '../PromptProvider'
import { reagentTransfer } from '../../../assets/prompts'
+import { preparedPromptAtom } from '../../../resources/atoms'
import { PromptButton } from '../index'
-const mockSetPrompt = vi.fn()
-
const render = (props: React.ComponentProps) => {
- return renderWithProviders(
-
- s
-
- )
+ return renderWithProviders()
}
describe('PromptButton', () => {
@@ -34,6 +29,8 @@ describe('PromptButton', () => {
render(props)
const button = screen.getByRole('button', { name: 'Reagent Transfer' })
fireEvent.click(button)
- expect(mockSetPrompt).toHaveBeenCalledWith(reagentTransfer)
+ const { result } = renderHook(() => useAtom(preparedPromptAtom))
+ fireEvent.click(button)
+ expect(result.current[0]).toBe(reagentTransfer)
})
})
diff --git a/opentrons-ai-client/src/organisms/PromptButton/__tests__/PromptProvider.test.tsx b/opentrons-ai-client/src/organisms/PromptButton/__tests__/PromptProvider.test.tsx
deleted file mode 100644
index 5caedf2c3ad..00000000000
--- a/opentrons-ai-client/src/organisms/PromptButton/__tests__/PromptProvider.test.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import React from 'react'
-import { describe, it, expect } from 'vitest'
-import { fireEvent, screen } from '@testing-library/react'
-
-import { renderWithProviders } from '../../../__testing-utils__'
-import {
- PromptProvider,
- promptContext,
- setPromptContext,
-} from '../PromptProvider'
-
-const TestComponent = () => {
- const usePromptValue = (): string => React.useContext(promptContext)
- const prompt = usePromptValue()
-
- const usePromptSetValue = (): React.Dispatch> =>
- React.useContext(setPromptContext)
- const setPrompt = usePromptSetValue()
-
- return (
-