Skip to content

Commit

Permalink
feat(opentrons-ai-client): add auth0 package (#15148)
Browse files Browse the repository at this point in the history
* feat(opentrons-ai-client): add auth0 package
  • Loading branch information
koji authored May 16, 2024
1 parent 9c739b6 commit cfdcdf6
Show file tree
Hide file tree
Showing 12 changed files with 173 additions and 29 deletions.
1 change: 1 addition & 0 deletions opentrons-ai-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
43 changes: 40 additions & 3 deletions opentrons-ai-client/src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof renderWithProviders> => {
return renderWithProviders(<App />)
return renderWithProviders(<App />, {
i18nInstance: i18n,
})
}

describe('App', () => {
beforeEach(() => {
vi.mocked(SidePanel).mockReturnValue(<div>mock SidePanel</div>)
vi.mocked(ChatContainer).mockReturnValue(<div>mock ChatContainer</div>)
vi.mocked(Loading).mockReturnValue(<div>mock Loading</div>)
})

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()
})
})
42 changes: 39 additions & 3 deletions opentrons-ai-client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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 <Loading />
}

if (!isAuthenticated) {
return null
}

export function App(): JSX.Element {
return (
<Flex flexDirection={DIRECTION_ROW}>
<Flex flexDirection={DIRECTION_ROW} position={POSITION_RELATIVE}>
<Flex position={POSITION_ABSOLUTE} top="1rem" right="1rem">
<LinkButton
onClick={() => logout()}
textDecoration={TYPOGRAPHY.textDecorationUnderline}
>
{t('logout')}
</LinkButton>
</Flex>
<SidePanel />
<ChatContainer />
</Flex>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
17 changes: 13 additions & 4 deletions opentrons-ai-client/src/main.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -10,10 +11,18 @@ const rootElement = document.getElementById('root')
if (rootElement != null) {
ReactDOM.createRoot(rootElement).render(
<React.StrictMode>
<GlobalStyle />
<I18nextProvider i18n={i18n}>
<App />
</I18nextProvider>
<Auth0Provider
domain="identity.auth-dev.opentrons.com"
clientId="PcuD1wEutfijyglNeRBi41oxsKJ1HtKw"
authorizationParams={{
redirect_uri: window.location.origin,
}}
>
<GlobalStyle />
<I18nextProvider i18n={i18n}>
<App />
</I18nextProvider>
</Auth0Provider>
</React.StrictMode>
)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ describe('ChatDisplay', () => {
props = {
chat: {
role: 'assistant',
content: 'mock text from the backend',
reply: 'mock text from the backend',
},
chatId: 'mockId',
}
})
it('should display response from the backend and label', () => {
Expand All @@ -33,8 +34,9 @@ describe('ChatDisplay', () => {
props = {
chat: {
role: 'user',
content: 'mock text from user input',
reply: 'mock text from user input',
},
chatId: 'mockId',
}
render(props)
screen.getByText('You')
Expand Down
4 changes: 2 additions & 2 deletions opentrons-ai-client/src/molecules/ChatDisplay/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ interface ChatDisplayProps {
export function ChatDisplay({ chat, chatId }: ChatDisplayProps): JSX.Element {
const { t } = useTranslation('protocol_generator')
const [isCopied, setIsCopied] = React.useState<boolean>(false)
const { role, content } = chat
const { role, reply } = chat
const isUser = role === 'user'

const handleClickCopy = async (): Promise<void> => {
Expand Down Expand Up @@ -71,7 +71,7 @@ export function ChatDisplay({ chat, chatId }: ChatDisplayProps): JSX.Element {
code: CodeText,
}}
>
{content}
{reply}
</Markdown>
{role === 'assistant' ? (
<PrimaryButton
Expand Down
40 changes: 27 additions & 13 deletions opentrons-ai-client/src/molecules/InputPrompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import styled, { css } from 'styled-components'
import { useForm } from 'react-hook-form'
import { useAtom } from 'jotai'
import axios from 'axios'
import { useAuth0 } from '@auth0/auth0-react'

import {
ALIGN_CENTER,
Expand All @@ -20,8 +21,8 @@ import { preparedPromptAtom, chatDataAtom } from '../../resources/atoms'

import type { ChatData } from '../../resources/types'

// ToDo (kk:05/02/2024) This url is temporary
const url = 'http://localhost:8000/streaming/ask'
const url =
'https://fk0py9eu3e.execute-api.us-east-2.amazonaws.com/sandbox/chat/completion'

interface InputType {
userPrompt: string
Expand All @@ -40,7 +41,10 @@ export function InputPrompt(): JSX.Element {

const [data, setData] = React.useState<any>(null)
const [loading, setLoading] = React.useState<boolean>(false)
const [error, setError] = React.useState<string>('')
// ToDo (kk:05/15/2024) this will be used in the future
// const [error, setError] = React.useState<string>('')

const { getAccessTokenSilently } = useAuth0()

const userPrompt = watch('userPrompt') ?? ''

Expand All @@ -49,19 +53,29 @@ export function InputPrompt(): JSX.Element {
return rowsNum
}

// ToDo (kk:05/15/2024) This will be moved to a better place
const fetchData = async (prompt: string): Promise<void> => {
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)
}
Expand All @@ -71,7 +85,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)
Expand All @@ -84,19 +98,19 @@ export function InputPrompt(): JSX.Element {
}, [preparedPrompt, setValue])

React.useEffect(() => {
if (submitted && data && !loading) {
const { role, content } = data.data
if (submitted && data != null && !loading) {
const { role, reply } = data
const assistantResponse: ChatData = {
role,
content,
reply,
}
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)
// console.error('error', error)

return (
<StyledForm id="User_Prompt">
Expand Down
27 changes: 27 additions & 0 deletions opentrons-ai-client/src/molecules/Loading/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Flex
flexDirection={DIRECTION_COLUMN}
justifyContent={JUSTIFY_CENTER}
alignItems={ALIGN_CENTER}
gridGap={SPACING.spacing16}
height="100vh"
>
<StyledText as="h3">{t('loading')}</StyledText>
<Icon name="ot-spinner" size="3rem" spin />
</Flex>
)
}
4 changes: 3 additions & 1 deletion opentrons-ai-client/src/resources/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@ export interface ChatData {
/** assistant: ChatGPT API, user: user */
role: 'assistant' | 'user'
/** content ChatGPT API return or user prompt */
content: string
reply: string
/** for testing purpose will be removed and this is not used in the app */
fake?: boolean
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"step-generation",
"api-client",
"react-api-client",
"usb-bridge/node-client"
"usb-bridge/node-client",
"opentrons-ai-client"
]
},
"config": {
Expand Down
12 changes: 12 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.24"

"@auth0/[email protected]":
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/[email protected]":
version "1.4.126"
resolved "https://registry.yarnpkg.com/@aw-web-design/x-default-browser/-/x-default-browser-1.4.126.tgz#43e4bd8f0314ed907a8718d7e862a203af79bc16"
Expand Down

0 comments on commit cfdcdf6

Please sign in to comment.