diff --git a/opentrons-ai-client/README.md b/opentrons-ai-client/README.md index 5296a41b882..c2a15875311 100644 --- a/opentrons-ai-client/README.md +++ b/opentrons-ai-client/README.md @@ -13,8 +13,13 @@ To get started: clone the `Opentrons/opentrons` repository, set up your computer ```shell # change into the cloned directory cd opentrons + # prerequisite: install dependencies as specified in project setup make setup + +# if you have done the setup already, you can run the following instead of make setup +make teardown-js && make setup-js + # launch the dev server make -C opentrons-ai-client dev ``` @@ -27,6 +32,7 @@ The UI stack is built using: - [Babel][] - [Vite][] - [Jotai][] +- [styled-components][] Some important directories: @@ -61,5 +67,6 @@ TBD [babel]: https://babeljs.io/ [vite]: https://vitejs.dev/ [jotai]: https://jotai.org/ +[styled-components]: https://styled-components.com/ [bundle-analyzer]: https://github.com/webpack-contrib/webpack-bundle-analyzer [opentrons-ai-server]: https://github.com/Opentrons/opentrons/tree/edge/opentrons-ai-server diff --git a/opentrons-ai-client/package.json b/opentrons-ai-client/package.json index dfd8069c7b1..3742fbe70c7 100644 --- a/opentrons-ai-client/package.json +++ b/opentrons-ai-client/package.json @@ -19,6 +19,7 @@ }, "homepage": "https://github.com/Opentrons/opentrons", "dependencies": { + "@auth0/auth0-react": "2.2.4", "@fontsource/public-sans": "5.0.3", "@opentrons/components": "link:../components", "axios": "^0.21.1", diff --git a/opentrons-ai-client/src/App.test.tsx b/opentrons-ai-client/src/App.test.tsx index 4ae3494a53c..cbc13739b45 100644 --- a/opentrons-ai-client/src/App.test.tsx +++ b/opentrons-ai-client/src/App.test.tsx @@ -1,29 +1,66 @@ import React from 'react' -import { screen } from '@testing-library/react' -import { describe, it, vi, beforeEach } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, vi, beforeEach, expect } from 'vitest' +import * as auth0 from '@auth0/auth0-react' import { renderWithProviders } from './__testing-utils__' +import { i18n } from './i18n' import { SidePanel } from './molecules/SidePanel' import { ChatContainer } from './organisms/ChatContainer' +import { Loading } from './molecules/Loading' import { App } from './App' +vi.mock('@auth0/auth0-react') + +const mockLogout = vi.fn() + vi.mock('./molecules/SidePanel') vi.mock('./organisms/ChatContainer') +vi.mock('./molecules/Loading') const render = (): ReturnType => { - return renderWithProviders() + return renderWithProviders(, { + i18nInstance: i18n, + }) } describe('App', () => { beforeEach(() => { vi.mocked(SidePanel).mockReturnValue(
mock SidePanel
) vi.mocked(ChatContainer).mockReturnValue(
mock ChatContainer
) + vi.mocked(Loading).mockReturnValue(
mock Loading
) + }) + + it('should render loading screen when isLoading is true', () => { + ;(auth0 as any).useAuth0 = vi.fn().mockReturnValue({ + isAuthenticated: false, + isLoading: true, + }) + render() + screen.getByText('mock Loading') }) it('should render text', () => { + ;(auth0 as any).useAuth0 = vi.fn().mockReturnValue({ + isAuthenticated: true, + isLoading: false, + }) render() screen.getByText('mock SidePanel') screen.getByText('mock ChatContainer') + screen.getByText('Logout') + }) + + it('should call a mock function when clicking logout button', () => { + ;(auth0 as any).useAuth0 = vi.fn().mockReturnValue({ + isAuthenticated: true, + isLoading: false, + logout: mockLogout, + }) + render() + const logoutButton = screen.getByText('Logout') + fireEvent.click(logoutButton) + expect(mockLogout).toHaveBeenCalled() }) }) diff --git a/opentrons-ai-client/src/App.tsx b/opentrons-ai-client/src/App.tsx index 268a61b2e7f..6879985dc71 100644 --- a/opentrons-ai-client/src/App.tsx +++ b/opentrons-ai-client/src/App.tsx @@ -1,12 +1,48 @@ import React from 'react' -import { DIRECTION_ROW, Flex } from '@opentrons/components' +import { useAuth0 } from '@auth0/auth0-react' +import { useTranslation } from 'react-i18next' + +import { + DIRECTION_ROW, + Flex, + Link as LinkButton, + POSITION_ABSOLUTE, + POSITION_RELATIVE, + TYPOGRAPHY, +} from '@opentrons/components' import { SidePanel } from './molecules/SidePanel' import { ChatContainer } from './organisms/ChatContainer' +import { Loading } from './molecules/Loading' + +export function App(): JSX.Element | null { + const { t } = useTranslation('protocol_generator') + const { isAuthenticated, logout, isLoading, loginWithRedirect } = useAuth0() + + React.useEffect(() => { + if (!isAuthenticated && !isLoading) { + loginWithRedirect() + } + }, [isAuthenticated, isLoading]) + + if (isLoading) { + return + } + + if (!isAuthenticated) { + return null + } -export function App(): JSX.Element { return ( - + + + logout()} + textDecoration={TYPOGRAPHY.textDecorationUnderline} + > + {t('logout')} + + 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 739611e853e..a2cdfb12256 100644 --- a/opentrons-ai-client/src/assets/localization/en/protocol_generator.json +++ b/opentrons-ai-client/src/assets/localization/en/protocol_generator.json @@ -5,6 +5,9 @@ "copy_code": "Copy code", "disclaimer": "OpentronsAI can make mistakes. Review your protocol before running it on an Opentrons robot.", "got_feedback": "Got feedback? We love to hear it.", + "loading": "Loading...", + "login": "Login", + "logout": "Logout", "make_sure_your_prompt": "Make sure your prompt includes the following:", "metadata": "Metadata: Three pieces of information.", "modules": "Modules: Thermocycler or Temperature Module.", diff --git a/opentrons-ai-client/src/main.tsx b/opentrons-ai-client/src/main.tsx index b7a003ddeca..f36640d349a 100644 --- a/opentrons-ai-client/src/main.tsx +++ b/opentrons-ai-client/src/main.tsx @@ -1,6 +1,7 @@ import React from 'react' import ReactDOM from 'react-dom/client' import { I18nextProvider } from 'react-i18next' +import { Auth0Provider } from '@auth0/auth0-react' import { GlobalStyle } from './atoms/GlobalStyle' import { i18n } from './i18n' @@ -10,10 +11,18 @@ const rootElement = document.getElementById('root') if (rootElement != null) { ReactDOM.createRoot(rootElement).render( - - - - + + + + + + ) } else { diff --git a/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx index a8b844af08c..7559fea0a0a 100644 --- a/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx +++ b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx @@ -27,7 +27,7 @@ interface ChatDisplayProps { export function ChatDisplay({ chat, chatId }: ChatDisplayProps): JSX.Element { const { t } = useTranslation('protocol_generator') const [isCopied, setIsCopied] = React.useState(false) - const { role, content } = chat + const { role, reply } = chat const isUser = role === 'user' const handleClickCopy = async (): Promise => { @@ -71,7 +71,7 @@ export function ChatDisplay({ chat, chatId }: ChatDisplayProps): JSX.Element { code: CodeText, }} > - {content} + {reply} {role === 'assistant' ? ( (null) const [loading, setLoading] = React.useState(false) - const [error, setError] = React.useState('') + // ToDo (kk:05/15/2024) this will be used in the future + // const [error, setError] = React.useState('') + + const { getAccessTokenSilently } = useAuth0() const userPrompt = watch('userPrompt') ?? '' @@ -53,15 +57,24 @@ export function InputPrompt(): JSX.Element { if (prompt !== '') { setLoading(true) try { - const response = await axios.post(url, { - headers: { - 'Content-Type': 'application/json', + const accessToken = await getAccessTokenSilently({ + authorizationParams: { + audience: 'sandbox-ai-api', }, - query: prompt, }) + const postData = { + message: prompt, + fake: false, + } + const headers = { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + } + const response = await axios.post(url, postData, { headers }) setData(response.data) } catch (err) { - setError('Error fetching data from the API.') + // setError('Error fetching data from the API.') + console.error(`error: ${err}`) } finally { setLoading(false) } @@ -71,7 +84,7 @@ export function InputPrompt(): JSX.Element { const handleClick = (): void => { const userInput: ChatData = { role: 'user', - content: userPrompt, + reply: userPrompt, } setChatData(chatData => [...chatData, userInput]) void fetchData(userPrompt) @@ -85,10 +98,10 @@ export function InputPrompt(): JSX.Element { React.useEffect(() => { if (submitted && data && !loading) { - const { role, content } = data.data + const { role, reply } = data const assistantResponse: ChatData = { role, - content, + reply, } setChatData(chatData => [...chatData, assistantResponse]) setSubmitted(false) @@ -96,7 +109,7 @@ export function InputPrompt(): JSX.Element { }, [data, loading, submitted]) // ToDo (kk:05/02/2024) This is also temp. Asking the design about error. - console.error('error', error) + // console.error('error', error) return ( diff --git a/opentrons-ai-client/src/molecules/Loading/index.tsx b/opentrons-ai-client/src/molecules/Loading/index.tsx new file mode 100644 index 00000000000..c715812b749 --- /dev/null +++ b/opentrons-ai-client/src/molecules/Loading/index.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { + ALIGN_CENTER, + DIRECTION_COLUMN, + Flex, + Icon, + JUSTIFY_CENTER, + SPACING, + StyledText, +} from '@opentrons/components' + +export function Loading(): JSX.Element { + const { t } = useTranslation('protocol_generator') + return ( + + {t('loading')} + + + ) +} diff --git a/opentrons-ai-client/src/organisms/ChatContainer/index.tsx b/opentrons-ai-client/src/organisms/ChatContainer/index.tsx index 5eaa44888a0..1e81e312bcd 100644 --- a/opentrons-ai-client/src/organisms/ChatContainer/index.tsx +++ b/opentrons-ai-client/src/organisms/ChatContainer/index.tsx @@ -7,8 +7,6 @@ import { COLORS, DIRECTION_COLUMN, Flex, - POSITION_ABSOLUTE, - POSITION_RELATIVE, SPACING, StyledText, TYPOGRAPHY, @@ -27,14 +25,13 @@ export function ChatContainer(): JSX.Element { padding={`${SPACING.spacing40} ${SPACING.spacing40} ${SPACING.spacing24}`} backgroundColor={COLORS.grey10} width="100%" + id="ChatContainer" + flexDirection={DIRECTION_COLUMN} + gridGap={SPACING.spacing40} > {/* This will be updated when input textbox and function are implemented */} - + {t('opentronsai')} {/* Prompt Guide remain as a reference for users. */} @@ -49,24 +46,21 @@ export function ChatContainer(): JSX.Element { )) : null} - - - {t('disclaimer')} - + + + + {t('disclaimer')} ) } const ChatDataContainer = styled(Flex)` - max-height: calc(100vh); - overflow-y: auto; flex-direction: ${DIRECTION_COLUMN}; grid-gap: ${SPACING.spacing12}; width: 100%; diff --git a/opentrons-ai-client/src/resources/types.ts b/opentrons-ai-client/src/resources/types.ts index a0f5ebca959..f71100aeb67 100644 --- a/opentrons-ai-client/src/resources/types.ts +++ b/opentrons-ai-client/src/resources/types.ts @@ -2,5 +2,7 @@ export interface ChatData { /** assistant: ChatGPT API, user: user */ role: 'assistant' | 'user' /** content ChatGPT API return or user prompt */ - content: string + // content: string + reply: string + fake?: boolean } diff --git a/package.json b/package.json index ef12e013721..4e70d03e212 100755 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "step-generation", "api-client", "react-api-client", - "usb-bridge/node-client" + "usb-bridge/node-client", + "opentrons-ai-client" ] }, "config": { diff --git a/yarn.lock b/yarn.lock index 8427f656e8d..7c490e3db6d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30,6 +30,18 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" +"@auth0/auth0-react@2.2.4": + version "2.2.4" + resolved "https://registry.yarnpkg.com/@auth0/auth0-react/-/auth0-react-2.2.4.tgz#7f21751a219d4e0e019141819f00e76e436176dd" + integrity sha512-l29PQC0WdgkCoOc6WeMAY26gsy/yXJICW0jHfj0nz8rZZphYKrLNqTRWFFCMJY+sagza9tSgB1kG/UvQYgGh9A== + dependencies: + "@auth0/auth0-spa-js" "^2.1.3" + +"@auth0/auth0-spa-js@^2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@auth0/auth0-spa-js/-/auth0-spa-js-2.1.3.tgz#aabf6f439e41edbeef0cf4766ad754e5b47616e5" + integrity sha512-NMTBNuuG4g3rame1aCnNS5qFYIzsTUV5qTFPRfTyYFS1feS6jsCBR+eTq9YkxCp1yuoM2UIcjunPaoPl77U9xQ== + "@aw-web-design/x-default-browser@1.4.126": version "1.4.126" resolved "https://registry.yarnpkg.com/@aw-web-design/x-default-browser/-/x-default-browser-1.4.126.tgz#43e4bd8f0314ed907a8718d7e862a203af79bc16"