Skip to content

Commit

Permalink
Add openai app example (#195)
Browse files Browse the repository at this point in the history
* first commit

* style changes

* Update examples/ai-image-generator/pages/index.jsx

Co-authored-by: Kirill Sudarushkin <[email protected]>

* working 3rd commit, clean spinner code

* add comments

* add README template

* adding title

* Update README.md with demo video

* remove unnecessary things from prompt component

* add same function style

* minimized css

* kirill feedback

* add spinner logic to be hidden by default

* mettin feedback

* try to reduce css

* added mirotone vars

* remove draggable=false

---------

Co-authored-by: Kirill Sudarushkin <[email protected]>
  • Loading branch information
horeaporutiu and kirillsud authored Sep 21, 2023
1 parent 352b440 commit ccbff24
Show file tree
Hide file tree
Showing 14 changed files with 405 additions and 0 deletions.
4 changes: 4 additions & 0 deletions examples/ai-image-generator/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
MIRO_CLIENT_ID=""
MIRO_CLIENT_SECRET=""
MIRO_REDIRECT_URL="http://localhost:3000/api/redirect"
OPENAI_API_KEY=""
24 changes: 24 additions & 0 deletions examples/ai-image-generator/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.next

# testing
/coverage

# misc
.DS_Store
*.pem
.idea

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env
dist
5 changes: 5 additions & 0 deletions examples/ai-image-generator/APP_SUBMISSION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## Submission to Miro Marketplace

Congrats! You have finished building your app & you'd like to publish it for
users. You can submit your app on the
[Miro Marketplace](https://developers.miro.com/docs/submit-your-app) for review.
105 changes: 105 additions & 0 deletions examples/ai-image-generator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# AI Image Generator

This app implements image generation using AI (OpenAI) and shows how to drag and drop those images to a Miro Board.

# 👨🏻‍💻 App Demo

https://github.com/miroapp/app-examples/assets/10428517/4660d1ab-80b6-4136-9cf6-0b64bed5c019

# 📒 Table of Contents

- [Included Features](#features)
- [Tools and Technologies](#tools)
- [Prerequisites](#prerequisites)
- [Run the app locally](#run)
- [Folder Structure](#folder)
- [Contributing](#contributing)
- [License](#license)

# ⚙️ Included Features <a name="features"></a>

- [Miro Web SDK](https://developers.miro.com/docs/web-sdk-reference)
- [drop event](https://developers.miro.com/docs/ui_boardui#drop-event)
- [openPanel(options)](https://developers.miro.com/docs/ui_boardui#openpanel)
- [draggable elements](https://developers.miro.com/docs/add-drag-and-drop-to-your-app#add-draggable-elements-to-the-app-panel)
- [zoomTo](https://developers.miro.com/docs/viewport_viewport#zoomto)

# 🛠️ Tools and Technologies <a name="tools"></a>

- [React](https://react.dev/)
- [Next.js](https://nextjs.org/)
- [OpenAI Node.js Library](https://platform.openai.com/docs/libraries/node-js-library)

# ✅ Prerequisites <a name="prerequisites"></a>

- You have a [Miro account](https://miro.com/signup/).
- You're [signed in to Miro](https://miro.com/login/).
- Your Miro account has a [Developer team](https://developers.miro.com/docs/create-a-developer-team).
- Your development environment includes [Node.js 14.13](https://nodejs.org/en/download) or a later version.
- All examples use `npm` as a package manager and `npx` as a package runner.
- OpenAI Account and [API Key](https://platform.openai.com/account/api-keys).

# 🏃🏽‍♂️ Run the app locally <a name="run"></a>

1. Rename the `.env.sample` file to `.env` and add in [OpenAI API key](https://platform.openai.com/account/api-keys),
client ID and client secret, and save the file.
2. Run `npm install` to install dependencies.

3. Open the [app manifest editor](https://developers.miro.com/docs/manually-create-an-app#step-2-configure-your-app-in-miro) by clicking **Edit in Manifest**. \
In the app manifest editor, configure the app as follows and then `click save`.

```yaml
appName: AI Image Generator
sdkVersion: SDK_V2
sdkUri: http://localhost:3000
redirectUris:
- http://localhost:3000/api/redirect
redirectUriForSdk: http://localhost:3000/api/redirect
scopes:
- boards:read
- boards:write
```
4. Run `npm start`.
Your URL should be similar to this example:

```
http://localhost:3000
```
Go to the URL and go through the OAuth flow and install the app on a dev team.
> ⚠️ We recommend to install your app on a [developer team](https://developers.miro.com/docs/create-a-developer-team) while you are developing or testing apps.⚠️
5. Go to your developer team, and open your boards.
6. Click on the plus icon from the bottom section of your left sidebar. If you hover over it, it will say `More apps`.
7. Search for your app `AI Image Generator` or whatever you chose to name it. Click on your app to use it, as shown in the video below.
https://github.com/horeaporutiu/app-examples-template/assets/10428517/b23d9c4c-e785-43f9-a72e-fa5d82c7b019
# 🗂️ Folder structure <a name="folder"></a>
```
.
├── package.json <-- The dependencies for the app.
└── .env <-- A file you create, where you store sensitive credentials (client ID, client secret).
└── .env.sample <-- A template file, where you store sensitive credentials (client ID, client secret).
└── index.js <-- Main index.js file for basic UI functions.
└── pages
└── api
└── openai.js <-- Handles API call to OpenAI using OpenAI library.
└── _app.jsx <-- Main _app.js file for Next.js app.
└── _document.jsx <-- Next.js import file.
└── index.jsx <-- UI for prompt and generate image button, with logic for handling prompt / spinner.
└── public
└── styles
└── node_modules <-- Node.js modules that are installed based on dependencies.
```
# 🫱🏻‍🫲🏽 Contributing <a name="contributing"></a>
If you want to contribute to this example, or any other Miro Open Source project, please review [Miro's contributing guide](https://github.com/miroapp/app-examples/blob/main/CONTRIBUTING.md).
# 🪪 License <a name="license"></a>
[MIT License](https://github.com/miroapp/app-examples/blob/main/LICENSE).
10 changes: 10 additions & 0 deletions examples/ai-image-generator/app-manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# See https://developers.miro.com/docs/app-manifest on how to use this
appName: AI Image Generator
sdkVersion: SDK_V2
sdkUri: http://localhost:3000
redirectUris:
- http://localhost:3000/api/redirect
redirectUriForSdk: http://localhost:3000/api/redirect
scopes:
- boards:read
- boards:write
13 changes: 13 additions & 0 deletions examples/ai-image-generator/components/Button.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const Button = ({ children, onClick }) => {
return (
<button
onClick={onClick}
className="cs1 ce12 button button-primary"
type="button"
>
{children}
</button>
);
};

export default Button;
15 changes: 15 additions & 0 deletions examples/ai-image-generator/components/PromptInput.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const TextInput = ({ placeholder, value, onChange }) => {
return (
<div className="form-group cs1 ce12">
<input
className="input"
type="text"
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</div>
);
};

export default TextInput;
7 changes: 7 additions & 0 deletions examples/ai-image-generator/jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./node_modules/@mirohq"]
},
"include": ["src"],
"exclude": ["node_modules"]
}
19 changes: 19 additions & 0 deletions examples/ai-image-generator/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "ai-image-generator",
"version": "0.1.0",
"license": "MIT",
"author": "[email protected]",
"scripts": {
"build": "next build",
"start": "next dev",
"lint": "next lint"
},
"dependencies": {
"dotenv": "^16.0.3",
"mirotone": "5",
"next": "^13.0.0",
"openai": "^4.8.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
7 changes: 7 additions & 0 deletions examples/ai-image-generator/pages/_app.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import "../src/assets/style.css";

const MyApp = ({ Component, pageProps }) => {
return <Component {...pageProps} />;
};

export default MyApp;
21 changes: 21 additions & 0 deletions examples/ai-image-generator/pages/_document.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Html, Head, Main, NextScript } from "next/document";

export default function Document() {
return (
<Html>
<Head>
<link
rel="stylesheet"
href="https://unpkg.com/mirotone/dist/styles.css"
></link>
<script src="https://miro.com/app/static/sdk/v2/miro.js" />
</Head>
<body>
<div id="root">
<Main />
</div>
<NextScript />
</body>
</Html>
);
}
29 changes: 29 additions & 0 deletions examples/ai-image-generator/pages/api/openai.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import OpenAI from "openai";

const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY, // This is also the default, can be omitted
});

export default async (req, res) => {
// grab prompt from the front end
let prompt = await req.body.prompt;

try {
const response = await openai.images.generate({
prompt: prompt,
});
let image_url = response.data[0].url;
// send url to front end to display the image
res.status(200).json({
success: true,
data: image_url,
});
} catch (error) {
console.log(error);
// send error to front end, so user can easily see that something went wrong
res.status(400).json({
success: false,
error: "The image could not be generated",
});
}
};
90 changes: 90 additions & 0 deletions examples/ai-image-generator/pages/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React, { useState, useEffect } from "react";
import PromptInput from "../components/PromptInput";
import Button from "../components/Button";

export default function Main() {
const [inputValue, setInputValue] = useState("");
const [image, setImage] = useState("");
const [loading, setLoading] = useState(false);

useEffect(() => {
// Opens the panel for our app UI when we click on icon in the left sidebar
if (new URLSearchParams(window.location.search).has("panel")) return;
window.miro.board.ui.on("icon:click", async () => {
window.miro.board.ui.openPanel({
url: `/?panel=1`,
});
});
}, []);

// Register the drop event handler once.
useEffect(() => {
window.miro.board.ui.on("drop", drop);
}, []);

//drag and drop logic
const drop = async ({ x, y, target }) => {
setLoading(true);

if (target instanceof HTMLImageElement) {
const image = await window.miro.board.createImage({
x,
y,
url: target.src,
});
await window.miro.board.viewport.zoomTo(image);
}
setLoading(false);
};

//handles the prompt input being typed in
const handleInputChange = (newValue) => {
setInputValue(newValue);
};

const handleButtonClick = async () => {
setImage("");
setLoading(true);

// post our prompt to our backend
try {
const response = await fetch("/api/openai", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ prompt: inputValue }),
});

//get the response back from backend, which has the URL which we are looking for
const { data: imageUrl } = await response.json();

//set the image src to the URL which is returned by OpenAI call
setImage(imageUrl);
} catch (err) {
console.log(err);
}
setLoading(false);
};

return (
<div className="grid">
{/* React component which takes the user input and uses that as a prompt for OpenAI image generation */}
<PromptInput
placeholder={"Van Gogh inspired portrait of a dog"}
value={inputValue}
onChange={handleInputChange}
/>

{/* Button which calls the OpenAI backend (pages/api/openai.js) with the prompt */}
<Button onClick={handleButtonClick}>Generate Image</Button>

<div className="image-container cs1 ce12">
{/* Spinner needs to be hidden by default, otherwise will spin when opening app first time */}
{Boolean(loading) && <div className="spinner" />}
{/* Img which needs to be draggable */}
{Boolean(image) && <img className="miro-draggable" src={image} />}
</div>
</div>
);
}
Loading

1 comment on commit ccbff24

@vercel
Copy link

@vercel vercel bot commented on ccbff24 Sep 21, 2023

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

app-examples-wordle – ./examples/wordle

app-examples-wordle-anthonyroux.vercel.app
app-examples-wordle.vercel.app
app-examples-wordle-git-main-anthonyroux.vercel.app

Please sign in to comment.