diff --git a/.storybook/main.js b/.storybook/main.js
index e9fc91cdf48..985486d5d4e 100644
--- a/.storybook/main.js
+++ b/.storybook/main.js
@@ -2,6 +2,7 @@ module.exports = {
stories: [
'../components/**/*.stories.@(js|jsx|ts|tsx)',
'../app/**/*.stories.@(js|jsx|ts|tsx)',
+ '../opentrons-ai-client/**/*.stories.@(js|jsx|ts|tsx)',
],
addons: [
diff --git a/.storybook/preview.jsx b/.storybook/preview.jsx
index d8537e57827..32864c9abcb 100644
--- a/.storybook/preview.jsx
+++ b/.storybook/preview.jsx
@@ -20,7 +20,7 @@ export const parameters = {
options: {
storySort: {
method: 'alphabetical',
- order: ['Design Tokens', 'Library', 'App', 'ODD'],
+ order: ['Design Tokens', 'Library', 'App', 'ODD', 'AI'],
},
},
}
diff --git a/components/src/styles/flexbox.ts b/components/src/styles/flexbox.ts
index bc588372e96..2c36936b200 100644
--- a/components/src/styles/flexbox.ts
+++ b/components/src/styles/flexbox.ts
@@ -1,6 +1,7 @@
export const FLEX_NONE = 'none'
export const FLEX_AUTO = 'auto'
export const FLEX_MIN_CONTENT = 'min-content'
+export const FLEX_MAX_CONTENT = 'max-content'
export const ALIGN_NORMAL = 'normal'
export const ALIGN_START = 'start'
diff --git a/opentrons-ai-client/README.md b/opentrons-ai-client/README.md
index c2ff2908418..d4c80c2bb23 100644
--- a/opentrons-ai-client/README.md
+++ b/opentrons-ai-client/README.md
@@ -2,8 +2,6 @@
[![JavaScript Style Guide][style-guide-badge]][style-guide]
-[Download][] | [Support][]
-
## Overview
The Opentrons AI application helps you to create a protocol with natural language.
@@ -31,7 +29,7 @@ The UI stack is built using:
Some important directories:
-- `opentrons-ai-server` — Opentrons AI application's server
+- [opentrons-ai-server][] — Opentrons AI application's server
## Copy management
@@ -62,3 +60,4 @@ TBD
[babel]: https://babeljs.io/
[vite]: https://vitejs.dev/
[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/src/assets/images/opentrons_logo.svg b/opentrons-ai-client/src/assets/images/opentrons_logo.svg
new file mode 100644
index 00000000000..b183d161e81
--- /dev/null
+++ b/opentrons-ai-client/src/assets/images/opentrons_logo.svg
@@ -0,0 +1,51 @@
+
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 c8ac35504bb..f19455ad47e 100644
--- a/opentrons-ai-client/src/assets/localization/en/protocol_generator.json
+++ b/opentrons-ai-client/src/assets/localization/en/protocol_generator.json
@@ -2,20 +2,22 @@
"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.",
+ "got_feedback": "Got feedback? We love to hear it.",
"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_flex": "PCR (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.",
+ "share_your_thoughts": "Share your thoughts here",
+ "side_panel_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.",
+ "side_panel_header": "Use natural language to generate protocols with OpentronsAI powered by OpenAI",
"tipracks_and_labware": "Tip racks and labware: Use names from the Opentrons Labware Library.",
+ "try_example_prompts": "Stuck? Try these example prompts to get started.",
"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?",
diff --git a/opentrons-ai-client/src/main.tsx b/opentrons-ai-client/src/main.tsx
index 466bd35e081..bf46623695e 100644
--- a/opentrons-ai-client/src/main.tsx
+++ b/opentrons-ai-client/src/main.tsx
@@ -1,12 +1,17 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
+import { I18nextProvider } from 'react-i18next'
+
+import { i18n } from './i18n'
import { App } from './App'
const rootElement = document.getElementById('root')
-if (rootElement) {
+if (rootElement != null) {
ReactDOM.createRoot(rootElement).render(
-
+
+
+
)
} else {
diff --git a/opentrons-ai-client/src/molecules/SidePanel/SidePanel.stories.tsx b/opentrons-ai-client/src/molecules/SidePanel/SidePanel.stories.tsx
new file mode 100644
index 00000000000..1c1d30b7548
--- /dev/null
+++ b/opentrons-ai-client/src/molecules/SidePanel/SidePanel.stories.tsx
@@ -0,0 +1,21 @@
+import React from 'react'
+import { I18nextProvider } from 'react-i18next'
+import { i18n } from '../../i18n'
+import { SidePanel as SidePanelComponent } from './index'
+
+import type { Meta, StoryObj } from '@storybook/react'
+
+const meta: Meta = {
+ title: 'AI/molecules/SidePanel',
+ component: SidePanelComponent,
+ decorators: [
+ Story => (
+
+
+
+ ),
+ ],
+}
+export default meta
+type Story = StoryObj
+export const SidePanel: Story = {}
diff --git a/opentrons-ai-client/src/molecules/SidePanel/__tests__/SidePanel.test.tsx b/opentrons-ai-client/src/molecules/SidePanel/__tests__/SidePanel.test.tsx
new file mode 100644
index 00000000000..56cb50f73fc
--- /dev/null
+++ b/opentrons-ai-client/src/molecules/SidePanel/__tests__/SidePanel.test.tsx
@@ -0,0 +1,48 @@
+import React from 'react'
+import { screen } from '@testing-library/react'
+import { describe, it, expect } from 'vitest'
+
+import { renderWithProviders } from '../../../__testing-utils__'
+import { i18n } from '../../../i18n'
+
+import { SidePanel } from '../index'
+
+const LOGO_FILE_NAME =
+ '/opentrons-ai-client/src/assets/images/opentrons_logo.svg'
+
+const FEEDBACK_FORM_LINK = 'https://opentrons-ai-beta.paperform.co/'
+
+const render = (): ReturnType => {
+ return renderWithProviders(, {
+ i18nInstance: i18n,
+ })
+}
+
+describe('SidePanel', () => {
+ it('should render logo and text', () => {
+ render()
+ const image = screen.getByRole('img')
+ expect(image.getAttribute('src')).toEqual(LOGO_FILE_NAME)
+ screen.getByText(
+ 'Use natural language to generate protocols with OpentronsAI powered by OpenAI'
+ )
+ screen.getByText(
+ '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.'
+ )
+ screen.getByText('Stuck? Try these example prompts to get started.')
+ screen.getByText('Got feedback? We love to hear it.')
+ const link = screen.getByRole('link', {
+ name: 'Share your thoughts here',
+ })
+ expect(link).toHaveAttribute('href', FEEDBACK_FORM_LINK)
+ })
+
+ it('should render buttons', () => {
+ render()
+ screen.getByRole('button', { name: 'PCR' })
+ screen.getByRole('button', { name: 'PCR (Flex)' })
+ screen.getByRole('button', { name: 'Reagent Transfer' })
+ screen.getByRole('button', { name: 'Reagent Transfer (Flex)' })
+ })
+ it.todo('should call a mock function when clicking a button')
+})
diff --git a/opentrons-ai-client/src/molecules/SidePanel/index.tsx b/opentrons-ai-client/src/molecules/SidePanel/index.tsx
new file mode 100644
index 00000000000..536c0709a8b
--- /dev/null
+++ b/opentrons-ai-client/src/molecules/SidePanel/index.tsx
@@ -0,0 +1,103 @@
+import React from 'react'
+import styled, { css } from 'styled-components'
+import { useTranslation } from 'react-i18next'
+import {
+ COLORS,
+ DIRECTION_COLUMN,
+ Flex,
+ Link,
+ PrimaryButton,
+ SPACING,
+ StyledText,
+ TYPOGRAPHY,
+ WRAP,
+} from '@opentrons/components'
+import LOGO_PATH from '../../assets/images/opentrons_logo.svg'
+
+const IMAGE_ALT = 'Opentrons logo'
+const FEEDBACK_FORM_LINK = 'https://opentrons-ai-beta.paperform.co/'
+export function SidePanel(): JSX.Element {
+ const { t } = useTranslation('protocol_generator')
+ return (
+
+ {/* logo */}
+
+
+
+
+ {/* body text */}
+
+
+ {t('side_panel_header')}
+
+ {t('side_panel_body')}
+
+
+ {/* buttons */}
+
+
+ {t('try_example_prompts')}
+
+
+
+ {/* ToDo(kk:04/11/2024) add a button component */}
+ {t('reagent_transfer')}
+ {t('reagent_transfer_flex')}
+ {t('prc')}
+ {t('prc_flex')}
+
+
+
+
+ {t('got_feedback')}
+
+
+ {t('share_your_thoughts')}
+
+
+
+ )
+}
+
+const HEADER_TEXT_STYLE = css`
+ font-size: ${TYPOGRAPHY.fontSize32};
+ line-height: ${TYPOGRAPHY.lineHeight42};
+ font-weight: ${TYPOGRAPHY.fontWeightBold};
+ color: ${COLORS.white};
+`
+const BODY_TEXT_STYLE = css`
+ font-size: ${TYPOGRAPHY.fontSize20};
+ line-height: ${TYPOGRAPHY.lineHeight24};
+ font-weight: ${TYPOGRAPHY.fontWeightRegular};
+ color: ${COLORS.white};
+`
+const BUTTON_GUIDE_TEXT_STYLE = css`
+ font-size: ${TYPOGRAPHY.fontSize20};
+ line-height: ${TYPOGRAPHY.lineHeight24};
+ font-weight: ${TYPOGRAPHY.fontWeightSemiBold};
+ color: ${COLORS.white};
+`
+
+const PromptButton = styled(PrimaryButton)`
+ border-radius: 2rem;
+ white-space: nowrap;
+`
+
+const FeedbackLink = styled(Link)`
+ font-size: ${TYPOGRAPHY.fontSize20};
+ line-height: ${TYPOGRAPHY.lineHeight24};
+ font-weight: ${TYPOGRAPHY.fontWeightBold};
+ color: ${COLORS.white};
+ text-decoration: ${TYPOGRAPHY.textDecorationUnderline};
+`
diff --git a/opentrons-ai-client/src/molecules/index.ts b/opentrons-ai-client/src/molecules/index.ts
new file mode 100644
index 00000000000..80fcd68f91a
--- /dev/null
+++ b/opentrons-ai-client/src/molecules/index.ts
@@ -0,0 +1 @@
+export * from './SidePanel'