-
Notifications
You must be signed in to change notification settings - Fork 210
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
1 parent
352b440
commit ccbff24
Showing
14 changed files
with
405 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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="" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
}); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
Oops, something went wrong.
ccbff24
There was a problem hiding this comment.
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