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

Add openai app example #195

Merged
merged 21 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from 12 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
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.
108 changes: 108 additions & 0 deletions examples/ai-image-generator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# 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.
└── initMiro.js <-- Module where we configure the Miro authorization helper.
└── pages
└── api
└── openai.js <-- Handles API call to OpenAI using OpenAI library.
└── redirect.js <-- Handles redirect after successful authorization to get access token.
└── _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
17 changes: 17 additions & 0 deletions examples/ai-image-generator/components/Button.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// components/Button.js
horeaporutiu marked this conversation as resolved.
Show resolved Hide resolved

import React from "react";

const Button = ({ label, onClick }) => {
return (
<button
onClick={onClick}
className="cs1 ce12 button button-primary"
type="button"
>
{label}
</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">
<input
className="input"
type="text"
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</div>
);
};

export default TextInput;
41 changes: 41 additions & 0 deletions examples/ai-image-generator/initMiro.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Miro } from "@mirohq/miro-api";
import { serialize } from "cookie";

function getSerializedCookie(name, value) {
return serialize(name, value, {
path: "/",
httpOnly: true,
sameSite: "none",
secure: true,
});
}

export default function initMiro(request, response) {
const tokensCookie = "miro_tokens";

// setup a Miro instance that loads tokens from cookies
return {
miro: new Miro({
storage: {
get: () => {
// Load state (tokens) from a cookie if it's set
try {
return JSON.parse(request.cookies[tokensCookie] || "null");
} catch (err) {
return null;
}
},
set: (_, state) => {
if (!response)
throw new Error(
"initMiro should be invoked with a response object",
);
// store state (tokens) in the cookie
response.setHeader("Set-Cookie", [
getSerializedCookie(tokensCookie, JSON.stringify(state)),
]);
},
},
}),
};
}
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"]
}
21 changes: 21 additions & 0 deletions examples/ai-image-generator/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "ai-image-generator",
"version": "0.1.0",
"license": "MIT",
"author": "[email protected]",
"scripts": {
"build": "next build",
"start": "next dev",
"lint": "next lint"
},
"dependencies": {
"@mirohq/miro-api": "^2.0.0",
"cookie": "^0.5.0",
"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;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
let image_url = response.data[0].url;
const 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",
});
}
};
16 changes: 16 additions & 0 deletions examples/ai-image-generator/pages/api/redirect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import initMiro from "../../initMiro";

// handle redirect with code and exchange it for the access token
export default async function handler(req, res) {
const { miro } = initMiro(req, res);

// Make sure the code is in query parameters
if (typeof req.query.code !== "string") {
res.status(400);
res.send("Missing code in the query");
return;
}

await miro.exchangeCodeForAccessToken("", req.query.code);
res.redirect("/");
}
Loading