Skip to content

Commit

Permalink
feat(opentrons-ai-client, opentrons-ai-server): add folders for opent…
Browse files Browse the repository at this point in the history
…rons-ai (#14788)

* feat(opentrons-ai-client, opentrons-ai-server): add folders for opentrons-ai
  • Loading branch information
koji authored Apr 10, 2024
1 parent 7544175 commit e423319
Show file tree
Hide file tree
Showing 24 changed files with 527 additions and 0 deletions.
59 changes: 59 additions & 0 deletions opentrons-ai-client/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# opentrons ai client makefile

# using bash instead of /bin/bash in SHELL prevents macOS optimizing away our PATH update
SHELL := bash

# add node_modules/.bin to PATH
PATH := $(shell cd .. && yarn bin):$(PATH)

benchmark_output := $(shell node -e 'console.log(new Date());')

# These variables can be overriden when make is invoked to customize the
# behavior of jest
tests ?=
cov_opts ?= --coverage=true
test_opts ?=

# standard targets
#####################################################################

.PHONY: all
all: clean build

.PHONY: setup
setup:
yarn

.PHONY: clean
clean:
shx rm -rf dist

# artifacts
#####################################################################

.PHONY: build
build: export NODE_ENV := production
build:
vite build
git rev-parse HEAD > dist/.commit

# development
#####################################################################

.PHONY: dev
dev: export NODE_ENV := development
dev:
vite serve

# production assets server
.PHONY: serve
serve: all
node ../scripts/serve-static dist

.PHONY: test
test:
$(MAKE) -C .. test-js-ai-client tests="$(tests)" test_opts="$(test_opts)"

.PHONY: test-cov
test-cov:
make -C .. test-js-ai-client tests=$(tests) test_opts="$(test_opts)" cov_opts="$(cov_opts)"
64 changes: 64 additions & 0 deletions opentrons-ai-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Opentrons AI Frontend

[![JavaScript Style Guide][style-guide-badge]][style-guide]

[Download][] | [Support][]

## Overview

The Opentrons AI application helps you to create a protocol with natural language.

## Developing

To get started: clone the `Opentrons/opentrons` repository, set up your computer for development as specified in the [contributing guide][contributing-guide-setup], and then:

```shell
# change into the cloned directory
cd opentrons
# prerequisite: install dependencies as specified in project setup
make setup
# launch the dev server
make -C opentrons-ai-client dev
```

## Stack and structure

The UI stack is built using:

- [React][]
- [Babel][]
- [Vite][]

Some important directories:

- `opentrons-ai-server` — Opentrons AI application's server

## Copy management

We use [i18next](https://www.i18next.com) for copy management and internationalization.

## Testing

Tests for the Opentrons App are run from the top level along with all other JS project tests.

- `make test-js` - Run all JavaScript tests

Test tasks can also be run with the following arguments:

| Argument | Default | Description | Example |
| -------- | -------- | ----------------------- | --------------------------------- |
| watch | `false` | Run tests in watch mode | `make test-unit watch=true` |
| cover | `!watch` | Calculate code coverage | `make test watch=true cover=true` |

## Building

TBD

[style-guide]: https://standardjs.com
[style-guide-badge]: https://img.shields.io/badge/code_style-standard-brightgreen.svg?style=flat-square&maxAge=3600
[contributing-guide-setup]: ../CONTRIBUTING.md#development-setup
[contributing-guide-running-the-api]: ../CONTRIBUTING.md#opentrons-api
[react]: https://react.dev/
[babel]: https://babeljs.io/
[vite]: https://vitejs.dev/
[bundle-analyzer]: https://github.com/webpack-contrib/webpack-bundle-analyzer
21 changes: 21 additions & 0 deletions opentrons-ai-client/babel.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use strict'

module.exports = {
env: {
// Must have babel-plugin-styled-components in each env,
// see here for further details: s https://styled-components.com/docs/tooling#babel-plugin
production: {
plugins: ['babel-plugin-styled-components', 'babel-plugin-unassert'],
},
development: {
plugins: ['babel-plugin-styled-components'],
},
test: {
plugins: [
// disable ssr, displayName to fix toHaveStyleRule
// https://github.com/styled-components/jest-styled-components/issues/294
['babel-plugin-styled-components', { ssr: false, displayName: false }],
],
},
},
}
13 changes: 13 additions & 0 deletions opentrons-ai-client/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Opentrons AI</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
38 changes: 38 additions & 0 deletions opentrons-ai-client/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "opentrons-ai-client",
"type": "module",
"version": "0.0.0-dev",
"description": "Opentrons AI application UI",
"source": "src/index.tsx",
"types": "lib/index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/Opentrons/opentrons.git"
},
"author": {
"name": "Opentrons Labworks",
"email": "[email protected]"
},
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/Opentrons/opentrons/issues"
},
"homepage": "https://github.com/Opentrons/opentrons",
"dependencies": {
"@fontsource/dejavu-sans": "5.0.3",
"@fontsource/public-sans": "5.0.3",
"@opentrons/components": "link:../components",
"i18next": "^19.8.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-error-boundary": "^4.0.10",
"react-i18next": "13.5.0",
"styled-components": "5.3.6"
},
"engines": {
"node": ">=18.19.0"
},
"devDependencies": {
"@types/styled-components": "^5.1.26"
}
}
18 changes: 18 additions & 0 deletions opentrons-ai-client/src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react'
import { screen } from '@testing-library/react'
import { describe, it } from 'vitest'

import { renderWithProviders } from './__testing-utils__'

import { App } from './App'

const render = (): ReturnType<typeof renderWithProviders> => {
return renderWithProviders(<App />)
}

describe('App', () => {
it('should render text', () => {
render()
screen.getByText('Opentrons AI')
})
})
9 changes: 9 additions & 0 deletions opentrons-ai-client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react'
import { Flex, StyledText } from '@opentrons/components'
export function App(): JSX.Element {
return (
<Flex>
<StyledText as="h1">Opentrons AI</StyledText>
</Flex>
)
}
2 changes: 2 additions & 0 deletions opentrons-ai-client/src/__testing-utils__/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './renderWithProviders'
export * from './matchers'
24 changes: 24 additions & 0 deletions opentrons-ai-client/src/__testing-utils__/matchers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Matcher } from '@testing-library/react'

// Match things like <p>Some <strong>nested</strong> text</p>
// Use with either string match: getByText(nestedTextMatcher("Some nested text"))
// or regexp: getByText(nestedTextMatcher(/Some nested text/))
export const nestedTextMatcher = (textMatch: string | RegExp): Matcher => (
content,
node
) => {
const hasText = (n: typeof node): boolean => {
if (n == null || n.textContent === null) return false
return typeof textMatch === 'string'
? Boolean(n?.textContent.match(textMatch))
: textMatch.test(n.textContent)
}
const nodeHasText = hasText(node)
const childrenDontHaveText =
node != null && Array.from(node.children).every(child => !hasText(child))

return nodeHasText && childrenDontHaveText
}

// need componentPropsMatcher
// need partialComponentPropsMatcher
53 changes: 53 additions & 0 deletions opentrons-ai-client/src/__testing-utils__/renderWithProviders.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// render using targetted component using @testing-library/react
// with wrapping providers for i18next and redux
import * as React from 'react'
import { QueryClient, QueryClientProvider } from 'react-query'
import { I18nextProvider } from 'react-i18next'
import { Provider } from 'react-redux'
import { vi } from 'vitest'
import { render } from '@testing-library/react'
import { createStore } from 'redux'

import type { PreloadedState, Store } from 'redux'
import type { RenderOptions, RenderResult } from '@testing-library/react'

export interface RenderWithProvidersOptions<State> extends RenderOptions {
initialState?: State
i18nInstance: React.ComponentProps<typeof I18nextProvider>['i18n']
}

export function renderWithProviders<State>(
Component: React.ReactElement,
options?: RenderWithProvidersOptions<State>
): [RenderResult, Store<State>] {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const { initialState = {}, i18nInstance = null } = options || {}

const store: Store<State> = createStore(
vi.fn(),
initialState as PreloadedState<State>
)
store.dispatch = vi.fn()
store.getState = vi.fn(() => initialState) as () => State

const queryClient = new QueryClient()

const ProviderWrapper: React.ComponentType<React.PropsWithChildren<{}>> = ({
children,
}) => {
const BaseWrapper = (
<QueryClientProvider client={queryClient}>
<Provider store={store}>{children}</Provider>
</QueryClientProvider>
)
if (i18nInstance != null) {
return (
<I18nextProvider i18n={i18nInstance}>{BaseWrapper}</I18nextProvider>
)
} else {
return BaseWrapper
}
}

return [render(Component, { wrapper: ProviderWrapper }), store]
}
7 changes: 7 additions & 0 deletions opentrons-ai-client/src/assets/localization/en/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import shared from './shared.json'
import protocol_generator from './protocol_generator.json'

export const en = {
shared,
protocol_generator,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"api": "API: An API level is 2.15",
"application": "Application: Your protocol's name, describing what it does.",
"commands": "Commands: List the protocol's steps, specifying quantities in microliters and giving exact source and destination locations.",
"make_sure_your_prompt": "Make sure your prompt includes the following:",
"metadata": "Metadata: Three pieces of information.",
"modules": "Modules: Thermocycler or Temperature Module.",
"opentronsai_asks_you": "OpentronsAI asks you to provide it!",
"ot2_pipettes": "OT-2 pipettes: Include volume, number of channels, and generation.",
"prc_flex": "PRC (Flex)",
"prc": "PCR",
"reagent_transfer_flex": "Reagent Transfer (Flex)",
"reagent_transfer": "Reagent Transfer",
"robot": "Robot: OT-2.",
"sidebar_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.",
"sidebar_header": "Use natural language to generate protocols with OpentronsAI powered by OpenAI",
"stuck": "Stuck? Try these example prompts to get started.",
"tipracks_and_labware": "Tip racks and labware: Use names from the Opentrons Labware Library.",
"type_your_prompt": "Type your prompt...",
"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?",
"what_typeof_protocol": "What type of protocol do you need?"
}
3 changes: 3 additions & 0 deletions opentrons-ai-client/src/assets/localization/en/shared.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"send": "Send"
}
5 changes: 5 additions & 0 deletions opentrons-ai-client/src/assets/localization/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { en } from './en'

export const resources = {
en,
}
45 changes: 45 additions & 0 deletions opentrons-ai-client/src/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import i18n from 'i18next'
import capitalize from 'lodash/capitalize'
import startCase from 'lodash/startCase'
import { initReactI18next } from 'react-i18next'
import { resources } from './assets/localization'
import { titleCase } from '@opentrons/shared-data'

i18n.use(initReactI18next).init(
{
resources,
lng: 'en',
fallbackLng: 'en',
debug: process.env.NODE_ENV === 'development',
ns: ['shared'],
defaultNS: 'shared',
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
format: function (value, format, lng) {
if (format === 'upperCase') return value.toUpperCase()
if (format === 'lowerCase') return value.toLowerCase()
if (format === 'capitalize') return capitalize(value)
if (format === 'sentenceCase') return startCase(value)
if (format === 'titleCase') return titleCase(value)
return value
},
},
keySeparator: false, // use namespaces and context instead
saveMissing: true,
missingKeyHandler: (lng, ns, key) => {
process.env.NODE_ENV === 'test'
? console.error(`Missing ${lng} Translation: key={${key}} ns={${ns}}`)
: console.warn(`Missing ${lng} Translation: key={${key}} ns={${ns}}`)
},
},
err => {
if (err) {
console.error(
'Internationalization was not initialized properly. error: ',
err
)
}
}
)

export { i18n }
Loading

0 comments on commit e423319

Please sign in to comment.