Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(opentrons-ai-client, opentrons-ai-server): add folders for opentrons-ai #14788

Merged
merged 12 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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][]
Comment on lines +28 to +30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a single sentence for each line to know why they are there?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, the reason is monorepo


Some important directories:

- `opentrons-ai-server` — Opentrons AI application's server
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: Could we provide hyperlink here so that when we click it we go directly there.


## Copy management

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does internationalisation mean here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be translated into other languages and that is the reason why we use i18next.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean English to Japan for example?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah that is one example.
I think we can gat help from Ed to make readme better before the externa beta testing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i18next will work like this when we add simple ui for language change and json files for languages.


## 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",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea to add i18next right off the bat instead of what we did with PD haha

"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
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?",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since OpenAI cannot follow instruction consistently, this may not happen all the time. So I am happy to remove this line.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why everything line by line, why dont we do something like triplet quote in python?
''' '''

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is json file and we use eslint.

"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
Loading