├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── docker-compose.yml ├── env.template ├── jsconfig.json ├── next.config.js ├── package-lock.json ├── package.json ├── prisma └── schema.prisma ├── public ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── apps.jpg ├── avatar.jpg ├── contact.png ├── hero.png ├── home.jpg ├── illustration.png ├── loading.png └── websites.jpg ├── src ├── app │ ├── about │ │ ├── page.jsx │ │ └── page.module.css │ ├── api │ │ ├── apps │ │ │ ├── [id] │ │ │ │ └── route.js │ │ │ └── route.js │ │ ├── auth │ │ │ ├── [...nextauth] │ │ │ │ └── route.js │ │ │ └── register │ │ │ │ └── route.js │ │ ├── comfyui │ │ │ ├── history │ │ │ │ └── [id] │ │ │ │ │ └── route.js │ │ │ ├── prompt │ │ │ │ └── route.js │ │ │ └── upload │ │ │ │ └── image │ │ │ │ └── route.js │ │ └── commands │ │ │ └── route.js │ ├── apps │ │ ├── [id] │ │ │ ├── page.jsx │ │ │ └── page.module.css │ │ ├── page.jsx │ │ └── page.module.css │ ├── dashboard │ │ ├── (auth) │ │ │ ├── login │ │ │ │ ├── page.jsx │ │ │ │ └── page.module.css │ │ │ └── register │ │ │ │ ├── page.jsx │ │ │ │ └── page.module.css │ │ ├── page.jsx │ │ └── page.module.css │ ├── favicon.ico │ ├── globals.css │ ├── layout.js │ ├── page.jsx │ ├── page.module.css │ ├── portfolio │ │ ├── [category] │ │ │ ├── data.js │ │ │ ├── page.jsx │ │ │ └── page.module.css │ │ ├── layout.jsx │ │ ├── page.jsx │ │ └── page.module.css │ └── settings │ │ ├── loading.jsx │ │ ├── page.jsx │ │ └── page.module.css ├── components │ ├── AppsLoader │ │ ├── AppsLoader.jsx │ │ └── AppsLoader.module.css │ ├── AuthProvider │ │ └── AuthProvider.jsx │ ├── Button │ │ ├── Button.jsx │ │ └── button.module.css │ ├── Config │ │ └── Config.jsx │ ├── DarkModeToggle │ │ ├── DarkModeToggle.jsx │ │ └── darkModeToggle.module.css │ ├── ImageLoader │ │ ├── AppDetails.jsx │ │ ├── ImageLoaderForm.module.css │ │ ├── MediaUploader.jsx │ │ └── PromptForm.jsx │ ├── Settings │ │ ├── Settings.jsx │ │ └── settings.module.css │ ├── Store │ │ └── SettingsStore.jsx │ ├── footer │ │ ├── Footer.jsx │ │ └── footer.module.css │ └── navbar │ │ ├── Navbar.jsx │ │ └── navbar.module.css ├── context │ ├── EnvContext.js │ └── ThemeContext.js ├── models │ ├── Post.js │ └── User.js └── utils │ ├── db.js │ └── env.js ├── vercel.Json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | .env 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | /prisma/migrations/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Dragon Lee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ComfyUI is the most powerful and modular stable diffusion GUI and backend. But it is not easy to use, and has a high learning curve. We build Apps 4 | based on the workflows, and provide several interfaces, like input prompt, upload image, upload video. You do not need 5 | to take care of the workflow details, what is your focus is only your content. We plan to 6 | provide follow functionalities, but we only support "Text to image" and "Video to video" so far: 7 | 8 | - Text to Image, Text to Video 9 | - Image to Image, Image to video 10 | - Video to video 11 | 12 | We also support upload workflow to customize your app now, you can specify the parameters in your app at the same time. 13 | 14 | ## Roadmap 15 | 16 | - [x] Base Text to image 17 | - [x] DB model 18 | - [x] Login and register 19 | - [x] Add App 20 | - [x] Video to video 21 | - [x] Customize App to support parameters 22 | - [x] Support vercel 23 | - [x] Start ComfyUI backend automatically 24 | - [x] Add settings in UI 25 | - [ ] Add limitation for free users 26 | - [ ] Adapt to mobile devices 27 | - [ ] Select model 28 | - [ ] Select styles 29 | - [ ] Text to video 30 | - [ ] Image to video 31 | 32 | # Screenshot 33 | Home Page: 34 | ![home](https://github.com/huanyingtianhe/EasyComfyUI/assets/5997003/aa4ec2a8-bd96-44d9-8819-c2f7b3337454) 35 | Apps Page: 36 | ![apps](https://github.com/huanyingtianhe/EasyComfyUI/assets/5997003/37bc33e9-96fb-436d-9c2f-da10fef03cb1) 37 | Text to Image App: 38 | ![Text2Image](https://github.com/user-attachments/assets/686138c4-a634-4e2f-b2f9-3449239350e1) 39 | Video to Video App 40 | ![Video2Video](https://github.com/user-attachments/assets/7220a1b0-3378-4aab-8895-e9ea8166c0a8) 41 | 42 | Live demo: 43 | [EasyComfyUI: 更容易上手的ComfyUI-哔哩哔哩](https://b23.tv/NTaFyoV) 44 | 45 | # Try For Fun 46 | You can go to the website https://easy-comfyui.vercel.app/apps to have a try first, but we do not provide ComfyUI backend by default. You need set your comfyUI address in the settings first. It may fail if your ComfyUI does not have installed the required nodes. If you do not have the ComfyUI, you can ask me to give you an url for testing, the url may only work an hour, not sure about it. 47 | 48 | # Install 49 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 50 | 51 | ## Prepare env file 52 | Rename the env.template to .env, and fill in the value with your own. The first part is related with Auth, you do not need to take care at first. What you need to notice is the second section. ComfyUI_BASE_ADDRESS is the url of your ComfyUI service, no matter it is local or remote. We can get the DATABASE_URL following the steps described below. 53 | 54 | ## Prepare DB 55 | 56 | If you already have a mysql instance, copy the mysql connection string, it is what DATABASE_URL variable need, and skip the section "Setup Mysql Instance". 57 | 58 | ### Setup Mysql Instance 59 | Install Mysql in your machine if you do not have a mysql instance. The question is how to setup the mysql instance? 60 | It is easy, just switch to the root folder of the project in command line tool, then run the command: 61 | 62 | ```bash 63 | sudo docker compose up -d 64 | ``` 65 | 66 | It requires you install the docker, docker-compose and mysql in your machine. After the execution, it will create a mysql instance and a database named "easy_comfyui", you can connect it with below command: 67 | 68 | ```bash 69 | mysql -h 127.0.0.1 -P 3306 -u root -p 70 | ``` 71 | 72 | The format of the connection string for a local mysql instance looks like this: 73 | 74 | ```bash 75 | "mysql://root:password@localhost:3306/easy_comfyui" 76 | ``` 77 | ## How to use Prisma 78 | Then we need to create the table in the db instance, we use Prisma to help us do that. Prisma ORM is an open-source next-generation ORM. For development, you only need to run following commands, it will create tables and keys in your database. 79 | 80 | ```bash 81 | yarn install 82 | npx prisma migrate dev --name init 83 | ``` 84 | 85 | You can find more in the following documents: 86 | 87 | [Getting started](https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch/relational-databases-node-mysql) 88 | 89 | [CRUD document](https://www.prisma.io/docs/orm/prisma-client/queries/crud#read) 90 | 91 | ## Start the Website 92 | Then, run the development server: 93 | 94 | ```bash 95 | npm run dev 96 | # or 97 | yarn dev 98 | # or 99 | pnpm dev 100 | ``` 101 | 102 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 103 | 104 | 105 | 106 | ## Learn More 107 | 108 | To learn more about Next.js, take a look at the following resources: 109 | 110 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 111 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 112 | 113 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 114 | 115 | ## Deploy on Vercel 116 | 117 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 118 | 119 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 120 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | mysql: 5 | image: mysql:8.0 6 | environment: 7 | MYSQL_DATABASE: 'easy_comfyui' 8 | # So you don't have to use root, but you can if you like 9 | MYSQL_USER: 'user' 10 | # You can use whatever password you like 11 | MYSQL_PASSWORD: 'password' 12 | # Password for root access 13 | MYSQL_ROOT_PASSWORD: 'password' 14 | volumes: 15 | - mysql:/var/lib/mysql 16 | ports: 17 | - 33060:3306 18 | 19 | # Names our volume 20 | volumes: 21 | mysql: -------------------------------------------------------------------------------- /env.template: -------------------------------------------------------------------------------- 1 | # variables related to auth 2 | GITHUB_ID= 3 | GITHUB_SECRET= 4 | AUTH_URL= 5 | AUTH_SECRET= 6 | 7 | #variables related to service 8 | DATABASE_URL= 9 | ComfyUI_BASE_ADDRESS= -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./src/*"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | "remotePatterns": [ 5 | { 6 | "protocol": "https", 7 | "hostname": "**.baidu.com", 8 | "port": "" 9 | }, 10 | { 11 | "protocol": "https", 12 | "hostname": "images.pexels.com", 13 | "port": "" 14 | }, 15 | { 16 | "protocol": "https", 17 | "hostname": "**.trycloudflare.com", 18 | "port": "" 19 | }, 20 | { 21 | "protocol": "https", 22 | "hostname": "github.com", 23 | "port": "" 24 | } 25 | ], 26 | }, 27 | }; 28 | 29 | module.exports = nextConfig; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nexttutorial", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@prisma/client": "^5.12.1", 13 | "azure-sdk-for-js": "^0.0.1", 14 | "bcryptjs": "^2.4.3", 15 | "eslint": "8.41.0", 16 | "eslint-config-next": "14.0.4", 17 | "jsonpath": "^1.1.1", 18 | "mongoose": "6.11.1", 19 | "next": "14.0.4", 20 | "next-auth": "^4.22.1", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0", 23 | "react-player": "^2.16.0", 24 | "reconnecting-websocket": "^4.4.0", 25 | "swr": "2.1.5", 26 | "uuid": "^9.0.1", 27 | "zustand": "^4.5.4" 28 | }, 29 | "devDependencies": { 30 | "prisma": "^5.12.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | } 10 | 11 | datasource db { 12 | provider = "mysql" 13 | url = env("DATABASE_URL") 14 | } 15 | 16 | model User { 17 | id Int @id @default(autoincrement()) 18 | email String @unique 19 | name String 20 | password String 21 | avatar String? 22 | role String 23 | createdAt DateTime @default(now()) 24 | Apps App[] 25 | } 26 | 27 | model Command { 28 | id Int @id @default(autoincrement()) 29 | jsonPath String 30 | desc String 31 | type String 32 | App App? @relation(fields: [appId], references: [id]) 33 | appId Int? 34 | } 35 | 36 | model App { 37 | id Int @id @default(autoincrement()) 38 | createdAt DateTime @default(now()) 39 | updatedAt DateTime @updatedAt 40 | img String 41 | title String 42 | desc String 43 | user User @relation(fields: [authorId], references: [id]) 44 | workflow Json 45 | authorId Int 46 | commands Command[] 47 | } 48 | -------------------------------------------------------------------------------- /public/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huanyingtianhe/EasyComfyUI/61260799ee767a8f1f0c6126cd95662ed8013664/public/1.png -------------------------------------------------------------------------------- /public/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huanyingtianhe/EasyComfyUI/61260799ee767a8f1f0c6126cd95662ed8013664/public/2.png -------------------------------------------------------------------------------- /public/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huanyingtianhe/EasyComfyUI/61260799ee767a8f1f0c6126cd95662ed8013664/public/3.png -------------------------------------------------------------------------------- /public/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huanyingtianhe/EasyComfyUI/61260799ee767a8f1f0c6126cd95662ed8013664/public/4.png -------------------------------------------------------------------------------- /public/apps.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huanyingtianhe/EasyComfyUI/61260799ee767a8f1f0c6126cd95662ed8013664/public/apps.jpg -------------------------------------------------------------------------------- /public/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huanyingtianhe/EasyComfyUI/61260799ee767a8f1f0c6126cd95662ed8013664/public/avatar.jpg -------------------------------------------------------------------------------- /public/contact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huanyingtianhe/EasyComfyUI/61260799ee767a8f1f0c6126cd95662ed8013664/public/contact.png -------------------------------------------------------------------------------- /public/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huanyingtianhe/EasyComfyUI/61260799ee767a8f1f0c6126cd95662ed8013664/public/hero.png -------------------------------------------------------------------------------- /public/home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huanyingtianhe/EasyComfyUI/61260799ee767a8f1f0c6126cd95662ed8013664/public/home.jpg -------------------------------------------------------------------------------- /public/illustration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huanyingtianhe/EasyComfyUI/61260799ee767a8f1f0c6126cd95662ed8013664/public/illustration.png -------------------------------------------------------------------------------- /public/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huanyingtianhe/EasyComfyUI/61260799ee767a8f1f0c6126cd95662ed8013664/public/loading.png -------------------------------------------------------------------------------- /public/websites.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huanyingtianhe/EasyComfyUI/61260799ee767a8f1f0c6126cd95662ed8013664/public/websites.jpg -------------------------------------------------------------------------------- /src/app/about/page.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./page.module.css"; 3 | import Image from "next/image"; 4 | import Button from "@/components/Button/Button"; 5 | 6 | const About = () => { 7 | return ( 8 |
9 |
10 | 16 |
17 |

EasyComfyUI

18 |

19 | We Simplify ComfyUI usage 20 |

21 |
22 |
23 |
24 |
25 |

Who Are We?

26 |

27 | We are a dynamic team of developers and designers passionate about creating user-friendly interfaces. Our mission is to make software interactions comfortable and efficient for users. 28 |
29 |
30 | Our core focus is on enhancing user experience. 31 |

32 |
33 |
34 |

What We Do?

35 |

36 | ComfyUI is the most powerful and modular stable diffusion GUI and backend. But it is not easy to use, and has a high learning curve. We build Apps 37 | based on the workflows, and provide several interfaces, like input prompt, upload image, upload video. You do not need 38 | to take care of the workflow details, what is your focus is only your content. Here are the Apps we want to provide: 39 |
40 |
- Text to Image, Text to Video 41 |
42 |
- Image to Image, Image to video 43 |
44 |
- Video to video 45 |

46 |
48 |
49 |
50 | ); 51 | }; 52 | 53 | export default About; 54 | -------------------------------------------------------------------------------- /src/app/about/page.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | } 3 | 4 | .imgContainer { 5 | width: 100%; 6 | height: 300px; 7 | position: relative; 8 | } 9 | 10 | .img { 11 | object-fit: cover; 12 | filter: grayscale(100%); 13 | } 14 | 15 | .imgText { 16 | position: absolute; 17 | bottom: 20px; 18 | left: 20px; 19 | background-color: #53c28b; 20 | padding: 5px; 21 | color: white; 22 | } 23 | 24 | .textContainer { 25 | display: flex; 26 | gap: 100px; 27 | } 28 | 29 | .item { 30 | flex: 1; 31 | margin-top: 50px; 32 | display: flex; 33 | flex-direction: column; 34 | gap: 30px; 35 | } 36 | 37 | .desc { 38 | font-size: 18px; 39 | font-weight: 300; 40 | text-align: justify; 41 | } 42 | -------------------------------------------------------------------------------- /src/app/api/apps/[id]/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import connect from "@/utils/db"; 3 | import Post from "@/models/Post"; 4 | 5 | export const GET = async (request, { params }) => { 6 | const { id } = params; 7 | 8 | try { 9 | const dbClient = await connect(); 10 | 11 | const app = await dbClient.app.findUnique({ 12 | where: { 13 | id: Number(id), 14 | }, 15 | include:{ 16 | user: true, 17 | commands: true, 18 | }, 19 | }); 20 | 21 | return new NextResponse(JSON.stringify(app), { status: 200 }); 22 | } catch (err) { 23 | console.log("Got app due to error: ", err); 24 | return new NextResponse("Database Error", { status: 500 }); 25 | } 26 | }; 27 | 28 | export const DELETE = async (request, { params }) => { 29 | const { id } = params; 30 | 31 | try { 32 | const dbClient = await connect(); 33 | 34 | await dbClient.app.delete({ 35 | where: { 36 | id: Number(id), 37 | }, 38 | }); 39 | 40 | return new NextResponse("App has been deleted", { status: 200 }); 41 | } catch (err) { 42 | console.log("Database Error: ", err); 43 | return new NextResponse("Database Error", { status: 500 }); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/app/api/apps/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import connect from "@/utils/db"; 3 | 4 | export const GET = async (request) => { 5 | var userName = request.nextUrl.searchParams.get("username") 6 | console.log("Try to get apps, user name: ", userName) 7 | 8 | try { 9 | const dbClient = await connect(); 10 | const user = userName ? await dbClient.user.findFirst({ 11 | where: { 12 | name: userName, 13 | }, 14 | }) : null; 15 | 16 | const apps = userName ? await dbClient.app.findMany({ 17 | where: { 18 | authorId: user.id 19 | }, 20 | include: { 21 | user: true, 22 | }, 23 | }) : 24 | await dbClient.app.findMany({ 25 | include: { 26 | user: true, 27 | }, 28 | }); 29 | 30 | console.log("Got apps number: ", apps.length) 31 | return new NextResponse(JSON.stringify(apps), { status: 200 }); 32 | } catch (err) { 33 | return new NextResponse("Database Error", { status: 500 }); 34 | } 35 | }; 36 | 37 | export const POST = async (request) => { 38 | const body = await request.json(); 39 | console.log("add new app: ", body) 40 | 41 | try { 42 | const dbClient = await connect(); 43 | 44 | await dbClient.app.create( 45 | { 46 | data: { 47 | img: body.img, 48 | title: body.title, 49 | desc: body.desc, 50 | workflow: JSON.parse(body.workflow), 51 | user: { 52 | connect: { 53 | email: body.email 54 | }, 55 | }, 56 | }, 57 | include: { 58 | user: true, 59 | }, 60 | } 61 | ) 62 | 63 | return new NextResponse("App has been created", { status: 201 }); 64 | } catch (err) { 65 | console.log("Got error when add app: ", err); 66 | return new NextResponse("Got error when add app", { status: 500 }); 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.js: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import GithubProvider from "next-auth/providers/github"; 3 | import GoogleProvider from "next-auth/providers/google"; 4 | import CredentialsProvider from "next-auth/providers/credentials"; 5 | import User from "@/models/User"; 6 | import connect from "@/utils/db"; 7 | import bcrypt from "bcryptjs"; 8 | 9 | const handler = NextAuth({ 10 | providers: [ 11 | CredentialsProvider({ 12 | id: "credentials", 13 | name: "Credentials", 14 | async authorize(credentials) { 15 | //Check if the user exists. 16 | var dbClient = await connect(); 17 | 18 | try { 19 | const user = await dbClient.user.findUnique({ 20 | where: { 21 | email: credentials.email, 22 | } 23 | }); 24 | 25 | if (user) { 26 | const isPasswordCorrect = await bcrypt.compare( 27 | credentials.password, 28 | user.password 29 | ); 30 | 31 | if (isPasswordCorrect) { 32 | return user; 33 | } else { 34 | throw new Error("Wrong Credentials!"); 35 | } 36 | } else { 37 | throw new Error("User not found!"); 38 | } 39 | } catch (err) { 40 | throw new Error(err); 41 | } 42 | }, 43 | }), 44 | GithubProvider({ 45 | clientId: process.env.GITHUB_ID, 46 | clientSecret: process.env.GITHUB_SECRET, 47 | }), 48 | GoogleProvider({ 49 | clientId: process.env.GOOGLE_CLIENT_ID, 50 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 51 | }), 52 | ], 53 | pages: { 54 | error: "/dashboard/login", 55 | }, 56 | 57 | }); 58 | 59 | export { handler as GET, handler as POST }; 60 | -------------------------------------------------------------------------------- /src/app/api/auth/register/route.js: -------------------------------------------------------------------------------- 1 | import connect from "@/utils/db"; 2 | import bcrypt from "bcryptjs"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export const POST = async (request) => { 6 | const { name, email, password } = await request.json(); 7 | console.log(`register, get name: ${name}, email: ${email}`) 8 | 9 | const dbClient = await connect(); 10 | 11 | const hashedPassword = await bcrypt.hash(password, 5); 12 | 13 | try { 14 | await dbClient.user.create({ 15 | data: { 16 | name: name, 17 | email: email, 18 | password: hashedPassword, 19 | role: "normal", 20 | } 21 | }) 22 | return new NextResponse("User has been created", { 23 | status: 201, 24 | }); 25 | } catch (err) { 26 | console.log("register, got error: ", err) 27 | return new NextResponse(err.message, { 28 | status: 500, 29 | }); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/app/api/comfyui/history/[id]/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | export const POST = async (request, { params }) => { 4 | console.log(params); 5 | const { id } = params; 6 | const data = await request.formData(); 7 | const comfyUIBaseAddress = data.get("comfyUIBaseAddress"); 8 | // upload image 9 | console.log("The url of getting history info: ", `${comfyUIBaseAddress}/history/${id}`) 10 | try{ 11 | 12 | const response = await fetch(`${comfyUIBaseAddress}/history/${id}`, { 13 | method: 'GET', 14 | }); 15 | 16 | console.log(response); 17 | const res = await response.json(); 18 | console.log(res); 19 | return new NextResponse(JSON.stringify(res), { status: 202 }); 20 | }catch(error){ 21 | console.log("Failed to get history, error", error) 22 | return new NextResponse(error, { status: 500 }); 23 | } 24 | } -------------------------------------------------------------------------------- /src/app/api/comfyui/prompt/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | export const POST = async (request) => { 4 | //const body = await request.json(); 5 | const data = await request.formData(); 6 | const comfyUIBaseAddress = data.get("comfyUIBaseAddress"); 7 | const body = data.get("prompt"); 8 | console.log("queue a new prompt in comfyui, body: ", body); 9 | try{ 10 | // queue prompt 11 | console.log("request to url: ", `${comfyUIBaseAddress}/prompt`) 12 | const response = await fetch(`${comfyUIBaseAddress}/prompt`, { 13 | method: 'POST', 14 | cache: 'no-cache', 15 | headers: { 16 | 'Content-Type': 'application/json' 17 | }, 18 | body: body 19 | }); 20 | 21 | var responseJson = await response.json(); 22 | console.log(responseJson); 23 | return new NextResponse(JSON.stringify(responseJson), { status: 202 }); 24 | }catch (error) { 25 | console.log("Failed to queue prompt", error); 26 | return new NextResponse("Failed to queue prompt", { status: 500 }); 27 | } 28 | } -------------------------------------------------------------------------------- /src/app/api/comfyui/upload/image/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | export const POST = async (request) => { 4 | const data = await request.formData(); 5 | const file = data.get('file'); 6 | const comfyUIBaseAddress = data.get("comfyUIBaseAddress"); 7 | console.log("Start to upload image to comfyui, file: ", file); 8 | 9 | try { 10 | const body = new FormData(); 11 | body.append("image", file); 12 | // upload image 13 | console.log("request to url: ", `${comfyUIBaseAddress}/upload/image`) 14 | const response = await fetch(`${comfyUIBaseAddress}/upload/image`, { 15 | method: 'POST', 16 | body, 17 | }); 18 | 19 | console.log(await response.status); 20 | if (!response.ok) { 21 | throw new Error(await response.text()); 22 | } 23 | return new NextResponse("Image has been uploaded", { status: 202 }); 24 | } catch (error) { 25 | console.log("Failed to upload image", error); 26 | return new NextResponse("Failed to upload image", { status: 500 }); 27 | } 28 | } -------------------------------------------------------------------------------- /src/app/api/commands/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import connect from "@/utils/db"; 3 | 4 | export const POST = async (request) => { 5 | const body = await request.json(); 6 | console.log("add new command: ", body) 7 | 8 | try { 9 | if (body.type != "text" && body.type != "image" && body.type != "video") { 10 | return new NextResponse("App not exist", { status: 400 }); 11 | } 12 | 13 | const dbClient = await connect(); 14 | 15 | const app = await dbClient.app.findFirst({ 16 | where: { 17 | title: body.appName, 18 | }, 19 | }) 20 | 21 | if (app == null) { 22 | return new NextResponse("App not exist", { status: 404 }); 23 | } 24 | 25 | await dbClient.command.create( 26 | { 27 | data: { 28 | jsonPath: body.jsonPath, 29 | desc: body.desc, 30 | type: body.type, 31 | App: { 32 | connect: { 33 | id: app.id 34 | }, 35 | }, 36 | }, 37 | } 38 | ) 39 | 40 | return new NextResponse("Command has been created", { status: 201 }); 41 | } catch (err) { 42 | console.log("Got error when add new command: ", err); 43 | return new NextResponse("Got error when add new command", { status: 500 }); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/app/apps/[id]/page.jsx: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | import AppDetails from "@/components/ImageLoader/AppDetails" 3 | 4 | async function AppPage({ params }) { 5 | const client_id = uuidv4(); 6 | //return APPLogic(data, client_id) 7 | return 8 | }; 9 | 10 | export default AppPage; -------------------------------------------------------------------------------- /src/app/apps/[id]/page.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | } 3 | 4 | .top { 5 | display: flex; 6 | } 7 | 8 | .info { 9 | flex: 1; 10 | display: flex; 11 | flex-direction: column; 12 | justify-content: space-between; 13 | } 14 | 15 | .title{ 16 | font-size: 40px; 17 | } 18 | 19 | .desc{ 20 | font-size: 18px; 21 | font-weight: 300; 22 | } 23 | 24 | .author { 25 | display: flex; 26 | align-items: center; 27 | gap: 10px; 28 | } 29 | 30 | .avatar { 31 | object-fit: cover; 32 | border-radius: 50%; 33 | } 34 | 35 | .imageContainer { 36 | flex: 1; 37 | height: 300px; 38 | position: relative; 39 | } 40 | 41 | .image { 42 | object-fit: cover; 43 | } 44 | 45 | .content { 46 | margin-top: 50px; 47 | font-size: 20px; 48 | font-weight: 300; 49 | color: #999; 50 | text-align: justify; 51 | } 52 | 53 | .generate { 54 | margin-top: 10px; 55 | font-size: 20px; 56 | font-weight: 300; 57 | color: #999; 58 | text-align: justify; 59 | } 60 | -------------------------------------------------------------------------------- /src/app/apps/page.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import AppsLoader from "@/components/AppsLoader/AppsLoader"; 3 | 4 | const Apps = async () => { 5 | return (); 6 | }; 7 | 8 | export default Apps; -------------------------------------------------------------------------------- /src/app/apps/page.module.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huanyingtianhe/EasyComfyUI/61260799ee767a8f1f0c6126cd95662ed8013664/src/app/apps/page.module.css -------------------------------------------------------------------------------- /src/app/dashboard/(auth)/login/page.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useEffect, useState } from "react"; 3 | import styles from "./page.module.css"; 4 | import { getProviders, signIn, useSession } from "next-auth/react"; 5 | import { useRouter, useSearchParams } from "next/navigation"; 6 | import Link from "next/link"; 7 | 8 | const Login = ({ url }) => { 9 | const session = useSession(); 10 | const router = useRouter(); 11 | const params = useSearchParams(); 12 | const [error, setError] = useState(""); 13 | const [success, setSuccess] = useState(""); 14 | 15 | useEffect(() => { 16 | setError(params.get("error")); 17 | setSuccess(params.get("success")); 18 | }, [params]); 19 | 20 | if (session.status === "loading") { 21 | return

Loading...

; 22 | } 23 | 24 | if (session.status === "authenticated") { 25 | router?.push("/dashboard"); 26 | } 27 | 28 | const handleSubmit = (e) => { 29 | e.preventDefault(); 30 | const email = e.target[0].value; 31 | const password = e.target[1].value; 32 | 33 | console.log(`got email: ${email}, password: {password}`) 34 | signIn("credentials", { 35 | email, 36 | password, 37 | }); 38 | }; 39 | 40 | return ( 41 |
42 |

{success ? success : "Welcome Back"}

43 |

Please sign in to see the dashboard.

44 | 45 |
46 | 52 | 58 | 59 | {error && error} 60 |
61 | 69 | 77 | - OR - 78 | 79 | Create new account 80 | 81 |
82 | ); 83 | }; 84 | 85 | export default Login; 86 | -------------------------------------------------------------------------------- /src/app/dashboard/(auth)/login/page.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | align-items: center; 4 | flex-direction: column; 5 | gap: 20px; 6 | } 7 | 8 | .title{ 9 | color: #bbb; 10 | } 11 | 12 | .subtitle{ 13 | font-size: 20px; 14 | margin-bottom: 30px; 15 | color: #bbb; 16 | } 17 | 18 | .form { 19 | width: 300px; 20 | display: flex; 21 | flex-direction: column; 22 | gap: 20px; 23 | } 24 | 25 | .input { 26 | padding: 20px; 27 | background-color: transparent; 28 | border: 2px solid #bbb; 29 | border-radius: 5px; 30 | outline: none; 31 | color: #bbb; 32 | font-size: 20px; 33 | font-weight: bold; 34 | } 35 | 36 | .button { 37 | width: 300px; 38 | padding: 20px; 39 | cursor: pointer; 40 | background-color: #53c28b; 41 | border: none; 42 | border-radius: 5px; 43 | color: #eee; 44 | font-weight: bold; 45 | } 46 | 47 | .button:hover { 48 | background-color: #49b07d; 49 | } 50 | 51 | .or { 52 | color: #bbb; 53 | } 54 | 55 | .link { 56 | text-decoration: underline; 57 | color: #7d7c7c; 58 | } 59 | 60 | .link:hover { 61 | color: rgb(61, 61, 61); 62 | } 63 | 64 | .google { 65 | background-color: rgba(228, 97, 50, 0.72); 66 | } 67 | 68 | .google:hover { 69 | background-color: rgba(228, 97, 50, 0.803); 70 | } -------------------------------------------------------------------------------- /src/app/dashboard/(auth)/register/page.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState } from "react"; 3 | import styles from "./page.module.css"; 4 | import Link from "next/link"; 5 | import { useRouter } from "next/navigation"; 6 | 7 | const Register = () => { 8 | const [error, setError] = useState(null); 9 | 10 | const router = useRouter(); 11 | 12 | const handleSubmit = async (e) => { 13 | e.preventDefault(); 14 | const name = e.target[0].value; 15 | const email = e.target[1].value; 16 | const password = e.target[2].value; 17 | 18 | try { 19 | const res = await fetch("/api/auth/register", { 20 | method: "POST", 21 | headers: { 22 | "Content-Type": "application/json", 23 | }, 24 | body: JSON.stringify({ 25 | name, 26 | email, 27 | password, 28 | }), 29 | }); 30 | res.status === 201 && router.push("/dashboard/login?success=Account has been created"); 31 | } catch (err) { 32 | setError(err); 33 | console.log(err); 34 | } 35 | }; 36 | 37 | return ( 38 |
39 |

Create an Account

40 |

Please sign up to see the dashboard.

41 |
42 | 48 | 54 | 60 | 61 | {error && "Something went wrong!"} 62 |
63 | - OR - 64 | 65 | Login with an existing account 66 | 67 |
68 | ); 69 | }; 70 | 71 | export default Register; 72 | -------------------------------------------------------------------------------- /src/app/dashboard/(auth)/register/page.module.css: -------------------------------------------------------------------------------- 1 | .container{ 2 | display: flex; 3 | flex-direction: column; 4 | gap: 20px; 5 | align-items: center; 6 | justify-content: center; 7 | } 8 | 9 | .form{ 10 | width: 300px; 11 | display: flex; 12 | flex-direction: column; 13 | gap: 20px; 14 | } 15 | 16 | .input{ 17 | padding: 20px; 18 | background-color: transparent; 19 | border: 2px solid #bbb; 20 | color:#bbb; 21 | border-radius: 5px; 22 | font-size: 20px; 23 | font-weight: bold; 24 | } 25 | 26 | .button { 27 | width: 300px; 28 | padding: 20px; 29 | cursor: pointer; 30 | background-color: #53c28b; 31 | border: none; 32 | border-radius: 5px; 33 | color: #eee; 34 | font-weight: bold; 35 | } -------------------------------------------------------------------------------- /src/app/dashboard/page.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useEffect, useState } from "react"; 3 | import styles from "./page.module.css"; 4 | import useSWR from "swr"; 5 | import { useSession } from "next-auth/react"; 6 | import { useRouter } from "next/navigation"; 7 | import Image from "next/image"; 8 | 9 | const Dashboard = () => { 10 | 11 | //OLD WAY TO FETCH DATA 12 | 13 | // const [data, setData] = useState([]); 14 | // const [err, setErr] = useState(false); 15 | // const [isLoading, setIsLoading] = useState(false); 16 | 17 | // useEffect(() => { 18 | // const getData = async () => { 19 | // setIsLoading(true); 20 | // const res = await fetch("https://jsonplaceholder.typicode.com/posts", { 21 | // cache: "no-store", 22 | // }); 23 | 24 | // if (!res.ok) { 25 | // setErr(true); 26 | // } 27 | 28 | // const data = await res.json() 29 | 30 | // setData(data); 31 | // setIsLoading(false); 32 | // }; 33 | // getData() 34 | // }, []); 35 | 36 | const session = useSession(); 37 | 38 | const router = useRouter(); 39 | 40 | //NEW WAY TO FETCH DATA 41 | const fetcher = (...args) => fetch(...args).then((res) => res.json()); 42 | 43 | const { data, mutate, error, isLoading } = useSWR( 44 | `/api/apps?username=${session?.data?.user.name}`, 45 | fetcher 46 | ); 47 | 48 | if (session.status === "loading") { 49 | return

Loading...

; 50 | } 51 | 52 | if (session.status === "unauthenticated") { 53 | router?.push("/dashboard/login"); 54 | } 55 | 56 | // Regular Expression 57 | function remove_linebreaks(str) { 58 | return str.replace(/[\r\n]+/gm, " "); 59 | } 60 | 61 | const handleAppSubmit = async (e) => { 62 | e.preventDefault(); 63 | const title = e.target[0].value; 64 | const desc = e.target[1].value; 65 | const img = e.target[2].value; 66 | const workflow = remove_linebreaks(e.target[3].value); 67 | 68 | // parse the json 69 | const workflowStr = JSON.stringify(JSON.parse(workflow)); 70 | 71 | try { 72 | await fetch("/api/apps", { 73 | method: "POST", 74 | body: JSON.stringify({ 75 | title, 76 | desc, 77 | img, 78 | workflow: workflowStr, 79 | email: session.data.user.email, 80 | }), 81 | }); 82 | mutate(); 83 | e.target.reset() 84 | } catch (err) { 85 | console.log(err); 86 | } 87 | }; 88 | 89 | const handleCommandSubmit = async (e) => { 90 | e.preventDefault(); 91 | const appName = e.target[0].value; 92 | const jsonPath = e.target[1].value; 93 | const desc = e.target[2].value; 94 | const type = e.target[3].value; 95 | 96 | try { 97 | await fetch("/api/commands", { 98 | method: "POST", 99 | body: JSON.stringify({ 100 | appName, 101 | jsonPath, 102 | desc, 103 | type, 104 | }), 105 | }); 106 | mutate(); 107 | e.target.reset() 108 | } catch (err) { 109 | console.log(err); 110 | } 111 | }; 112 | 113 | const handleDelete = async (id) => { 114 | try { 115 | await fetch(`/api/apps/${id}`, { 116 | method: "DELETE", 117 | }); 118 | mutate(); 119 | } catch (err) { 120 | console.log(err); 121 | } 122 | }; 123 | 124 | if (session.status === "authenticated") { 125 | return ( 126 |
127 |
128 | {isLoading 129 | ? "loading" 130 | : data?.map((app) => ( 131 |
132 |
133 | 134 |
135 |

{app.title}

136 | handleDelete(app.id)} 139 | > 140 | X 141 | 142 |
143 | ))} 144 |
145 |
146 |

Add New App

147 | 148 | 149 | 150 | 156 | 157 |
158 |
159 |

Add New Parameter for the App

160 | 161 | 162 | 163 | 164 | 165 |
166 |
167 | ); 168 | } 169 | }; 170 | 171 | export default Dashboard; 172 | -------------------------------------------------------------------------------- /src/app/dashboard/page.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | gap: 100px; 4 | } 5 | 6 | .posts { 7 | flex: 1; 8 | } 9 | 10 | .post { 11 | display: flex; 12 | align-items: center; 13 | justify-content: space-between; 14 | margin: 50px 0px; 15 | } 16 | 17 | .imgContainer { 18 | width: 200px; 19 | height: 100px; 20 | position: Ū; 21 | } 22 | 23 | .img { 24 | object-fit: cover; 25 | } 26 | 27 | .delete { 28 | cursor: pointer; 29 | color: red; 30 | } 31 | 32 | .new { 33 | flex: 1; 34 | display: flex; 35 | flex-direction: column; 36 | gap: 20px; 37 | } 38 | 39 | .input, 40 | .textArea { 41 | padding: 10px; 42 | background-color: transparent; 43 | border: 2px solid #bbb; 44 | border-radius: 3px; 45 | color: #bbb; 46 | font-size: 20px; 47 | font-weight: bold; 48 | } 49 | 50 | .button { 51 | padding: 20px; 52 | cursor: pointer; 53 | background-color: #53c28b; 54 | border: none; 55 | border-radius: 5px; 56 | color: #eee; 57 | font-weight: bold; 58 | } -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huanyingtianhe/EasyComfyUI/61260799ee767a8f1f0c6126cd95662ed8013664/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | html, 8 | body { 9 | max-width: 100vw; 10 | overflow-x: hidden; 11 | } 12 | 13 | a { 14 | text-decoration: none; 15 | color: inherit; 16 | } 17 | 18 | .theme{ 19 | transition: 1s all ease; 20 | } 21 | 22 | .light { 23 | background-color: white; 24 | color: #111; 25 | } 26 | 27 | .dark { 28 | background-color: #111; 29 | color: #bbb; 30 | } 31 | 32 | .container { 33 | max-width: 1366px; 34 | min-height: 100vh; 35 | margin: 0 auto; 36 | padding: 0 60px; 37 | display: flex; 38 | flex-direction: column; 39 | justify-content: space-between; 40 | } 41 | -------------------------------------------------------------------------------- /src/app/layout.js: -------------------------------------------------------------------------------- 1 | import Navbar from "@/components/navbar/Navbar"; 2 | import "./globals.css"; 3 | import { Inter, Roboto, Poppins } from "next/font/google"; 4 | import Footer from "@/components/footer/Footer"; 5 | import { ThemeProvider } from "@/context/ThemeContext"; 6 | import AuthProvider from "@/components/AuthProvider/AuthProvider"; 7 | import { EnvProvider } from "@/context/EnvContext"; 8 | 9 | const inter = Inter({ subsets: ["latin"] }); 10 | 11 | export const metadata = { 12 | title: "EasyComfyUI", 13 | description: "This is the description", 14 | }; 15 | 16 | export default function RootLayout({ children }) { 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | {children} 26 |
27 |
28 |
29 |
30 |
31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/app/page.jsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import styles from "./page.module.css"; 3 | import HomeImage from "/public/hero.png"; 4 | import Button from "@/components/Button/Button"; 5 | 6 | export default function Home() { 7 | return ( 8 |
9 |
10 |

11 | Different Apps help you use ComfyUI easily 12 |

13 |

14 | Turning your Idea into Reality. Generate images and videos using EasyComfyUI 15 |

16 |
18 |
19 | 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/app/page.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | align-items: center; 4 | gap: 100px; 5 | } 6 | 7 | .item { 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | gap: 50px; 12 | } 13 | 14 | .title { 15 | font-size: 72px; 16 | background: linear-gradient(to bottom, #194c33, #bbb); 17 | -webkit-background-clip: text; 18 | -webkit-text-fill-color: transparent; 19 | } 20 | 21 | .img { 22 | width: 100%; 23 | height: 500px; 24 | object-fit: contain; 25 | /* animation: move 3s infinite ease alternate; */ 26 | } 27 | 28 | @keyframes move { 29 | from { 30 | transform: translateY(-10px); 31 | } 32 | to { 33 | transform: translateY(10px); 34 | } 35 | } 36 | 37 | 38 | .desc { 39 | font-size: 24px; 40 | font-weight: 300; 41 | } 42 | 43 | -------------------------------------------------------------------------------- /src/app/portfolio/[category]/data.js: -------------------------------------------------------------------------------- 1 | export const items = { 2 | illustrations: [ 3 | { 4 | id: 1, 5 | title: "Creative Portfolio", 6 | desc: "Thank you very much. He is spared or bound by the necessities from which he was born, we can easily lead him with desire, and from that he gives us pain. Nor will it happen that he is bound to flee?", 7 | image: 8 | "https://images.pexels.com/photos/3130810/pexels-photo-3130810.jpeg", 9 | }, 10 | { 11 | id: 2, 12 | title: "Minimal Single Product", 13 | desc: "Thank you very much. He is spared or bound by the necessities from which he was born, we can easily lead him with desire, and from that he gives us pain. Nor will it happen that he is bound to flee?", 14 | image: 15 | "https://images.pexels.com/photos/2103127/pexels-photo-2103127.jpeg", 16 | }, 17 | { 18 | id: 3, 19 | title: "Strong Together Charity", 20 | desc: "Thank you very much. He is spared or bound by the necessities from which he was born, we can easily lead him with desire, and from that he gives us pain. Nor will it happen that he is bound to flee?", 21 | image: 22 | "https://images.pexels.com/photos/2916450/pexels-photo-2916450.jpeg", 23 | } 24 | ] 25 | }; 26 | -------------------------------------------------------------------------------- /src/app/portfolio/[category]/page.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./page.module.css"; 3 | import Button from "@/components/Button/Button"; 4 | import Image from "next/image"; 5 | import { items } from "./data.js"; 6 | import { notFound } from "next/navigation"; 7 | 8 | const getData = (cat) => { 9 | const data = items[cat]; 10 | 11 | if (data) { 12 | return data; 13 | } 14 | 15 | return notFound(); 16 | }; 17 | 18 | const Category = ({ params }) => { 19 | const data = getData(params.category); 20 | return ( 21 |
22 |

{params.category}

23 | 24 | {data.map((item) => ( 25 |
26 |
27 |

{item.title}

28 |

{item.desc}

29 |
31 |
32 | 38 |
39 |
40 | ))} 41 |
42 | ); 43 | }; 44 | 45 | export default Category; 46 | -------------------------------------------------------------------------------- /src/app/portfolio/[category]/page.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | } 3 | .catTitle { 4 | color: #53c28b; 5 | } 6 | 7 | .item{ 8 | display: flex; 9 | gap: 50px; 10 | margin-top: 50px; 11 | margin-bottom: 100px; 12 | } 13 | 14 | .item:nth-child(2n+1){ 15 | flex-direction: row-reverse; 16 | } 17 | 18 | .imgContainer{ 19 | flex:1; 20 | height: 500px; 21 | position: relative; 22 | } 23 | 24 | .img{ 25 | object-fit: cover; 26 | } 27 | 28 | .content{ 29 | flex:1; 30 | display: flex; 31 | flex-direction: column; 32 | gap: 20px; 33 | justify-content: center; 34 | } 35 | 36 | .title{ 37 | font-size: 50px; 38 | } 39 | 40 | .desc{ 41 | font-size: 20px; 42 | } -------------------------------------------------------------------------------- /src/app/portfolio/layout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from './page.module.css' 3 | 4 | const Layout = ({children}) => { 5 | return ( 6 |
7 |

Our Works

8 | {children} 9 |
10 | ) 11 | } 12 | 13 | export default Layout -------------------------------------------------------------------------------- /src/app/portfolio/page.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./page.module.css"; 3 | import Link from "next/link"; 4 | 5 | const Portfolio = () => { 6 | return ( 7 |
8 |

Choose a gallery

9 |
10 | 11 | Illustrations 12 | 13 |
14 |
15 | ); 16 | }; 17 | 18 | export default Portfolio; 19 | -------------------------------------------------------------------------------- /src/app/portfolio/page.module.css: -------------------------------------------------------------------------------- 1 | .mainTitle { 2 | font-size: 100px; 3 | } 4 | 5 | .container { 6 | } 7 | 8 | .selectTitle { 9 | margin: 20px 0px; 10 | } 11 | 12 | .items { 13 | display: flex; 14 | gap: 50px; 15 | } 16 | 17 | .item { 18 | border: 5px solid #bbb; 19 | border-radius: 5px; 20 | width: 300px; 21 | height: 400px; 22 | position: relative; 23 | background-size: cover !important; 24 | } 25 | 26 | .item:nth-child(1) { 27 | background: url("/illustration.png"); 28 | } 29 | .item:nth-child(2) { 30 | background: url("/websites.jpg"); 31 | } 32 | .item:nth-child(3) { 33 | background: url("/apps.jpg"); 34 | } 35 | 36 | .title { 37 | position: absolute; 38 | right: 10px; 39 | bottom: 10px; 40 | font-size: 40px; 41 | font-weight: bold; 42 | } 43 | 44 | .item:hover .title { 45 | color: #53c28b; 46 | } 47 | -------------------------------------------------------------------------------- /src/app/settings/loading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Loading = () => { 4 | return ( 5 |
Loading
6 | ) 7 | } 8 | 9 | export default Loading -------------------------------------------------------------------------------- /src/app/settings/page.jsx: -------------------------------------------------------------------------------- 1 | import { SettingBox } from "@/components/Settings/Settings"; 2 | 3 | const Contact = () => { 4 | return ( 5 | 6 | ); 7 | }; 8 | 9 | export default Contact; 10 | -------------------------------------------------------------------------------- /src/app/settings/page.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 10px; 3 | } 4 | 5 | .title { 6 | font-size: 60px; 7 | margin-bottom: 100px; 8 | text-align: center; 9 | } 10 | 11 | .content { 12 | display: flex; 13 | align-items: center; 14 | gap: 100px; 15 | } 16 | 17 | .imgContainer { 18 | flex: 1; 19 | height: 500px; 20 | position: relative; 21 | } 22 | 23 | .image { 24 | object-fit: contain; 25 | /* animation: move 3s infinite ease alternate; */ 26 | } 27 | 28 | @keyframes move { 29 | from { 30 | transform: translateY(-15px); 31 | } 32 | to { 33 | transform: translateY(0px) scale(1.03); 34 | } 35 | } 36 | 37 | .form { 38 | flex: 1; 39 | display: flex; 40 | flex-direction: column; 41 | gap: 20px; 42 | } 43 | 44 | .input, 45 | .textArea { 46 | padding: 20px; 47 | background-color: transparent; 48 | border: none; 49 | outline: none; 50 | color: #bbb; 51 | border: 3px solid #bbb; 52 | font-size: 16px; 53 | font-weight: bold; 54 | } 55 | 56 | .button { 57 | padding: 20px; 58 | cursor: pointer; 59 | background-color: #53c28b; 60 | border: none; 61 | border-radius: 5px; 62 | width: max-content; 63 | color: white; 64 | } 65 | -------------------------------------------------------------------------------- /src/components/AppsLoader/AppsLoader.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React, { useEffect, useState } from "react"; 4 | import styles from "./AppsLoader.module.css"; 5 | import Link from "next/link"; 6 | import Image from "next/image"; 7 | 8 | const AppsLoader = () => { 9 | const [data, setData] = useState(); 10 | useEffect(() =>{ 11 | const fetchData = async() => { 12 | const res = await fetch("/api/apps", { 13 | cache: "no-store", 14 | }); 15 | 16 | if (!res.ok) { 17 | throw new Error("Failed to fetch data"); 18 | } 19 | setData(await res.json()); 20 | } 21 | 22 | fetchData().catch((e) => { 23 | console.error("An error occurred while fetching data: ", e); 24 | }) 25 | }, []) 26 | 27 | console.log("Got apps: ", data); 28 | return ( 29 |
30 | { data != undefined && data != null && data.map((item) => ( 31 | 32 |
33 | 40 |
41 |
42 |

{item.title}

43 |

{item.desc}

44 |
45 | 46 | ))} 47 |
48 | ); 49 | }; 50 | 51 | export default AppsLoader; 52 | -------------------------------------------------------------------------------- /src/components/AppsLoader/AppsLoader.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | align-items: center; 4 | gap: 50px; 5 | margin-bottom: 50px; 6 | } 7 | 8 | .image{ 9 | object-fit: cover; 10 | border: 5px solid #6be3a9; 11 | border-radius: 5px; 12 | } 13 | 14 | .content { 15 | } 16 | 17 | .title{ 18 | margin-bottom: 10px; 19 | } 20 | 21 | .desc{ 22 | font-size: 18px; 23 | color: #999; 24 | } -------------------------------------------------------------------------------- /src/components/AuthProvider/AuthProvider.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { SessionProvider } from "next-auth/react"; 4 | import React from "react"; 5 | 6 | const AuthProvider = ({ children }) => { 7 | return {children}; 8 | }; 9 | 10 | export default AuthProvider; 11 | -------------------------------------------------------------------------------- /src/components/Button/Button.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./button.module.css"; 3 | import Link from "next/link"; 4 | 5 | const Button = ({ text, url }) => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default Button; 14 | -------------------------------------------------------------------------------- /src/components/Button/button.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 20px; 3 | cursor: pointer; 4 | background-color: #53c28b; 5 | border: none; 6 | border-radius: 5px; 7 | width: max-content; 8 | color: white; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Config/Config.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { useEnv } from '@/context/EnvContext'; 3 | import { useSettingsStore } from '../Store/SettingsStore'; 4 | 5 | export default function Config(){ 6 | const env = useEnv(); 7 | const addressSettings = useSettingsStore(state => state.comfyUIAddress); 8 | 9 | return { comfyUIBaseAddress: addressSettings == null ? env.ComfyUI_BASE_ADDRESS : addressSettings}; 10 | } -------------------------------------------------------------------------------- /src/components/DarkModeToggle/DarkModeToggle.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React, { useContext } from "react"; 4 | import styles from "./darkModeToggle.module.css"; 5 | import { ThemeContext } from "../../context/ThemeContext"; 6 | 7 | const DarkModeToggle = () => { 8 | const { toggle,mode } = useContext(ThemeContext); 9 | return ( 10 |
11 |
🌙
12 |
🔆
13 |
17 |
18 | ); 19 | }; 20 | 21 | export default DarkModeToggle; 22 | -------------------------------------------------------------------------------- /src/components/DarkModeToggle/darkModeToggle.module.css: -------------------------------------------------------------------------------- 1 | .container{ 2 | width: 42px; 3 | height: 24px; 4 | border: 1.5px solid #53c28b70; 5 | border-radius: 30px; 6 | display: flex; 7 | align-items: center; 8 | justify-content: space-between; 9 | padding: 2px; 10 | position: relative; 11 | cursor: pointer; 12 | } 13 | 14 | .icon{ 15 | font-size: 12px; 16 | } 17 | 18 | .ball{ 19 | width: 15px; 20 | height: 15px; 21 | background-color: #53c28b; 22 | border-radius: 50%; 23 | position: absolute; 24 | } -------------------------------------------------------------------------------- /src/components/ImageLoader/AppDetails.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React, { useState, useEffect } from 'react'; 4 | import styles from "./ImageLoaderForm.module.css"; 5 | import Image from "next/image"; 6 | import PromptForm from "@/components/ImageLoader/PromptForm" 7 | import MediaUploader from './MediaUploader'; 8 | import LoadingIcon from "/public/loading.png"; 9 | import Avatar from "/public/avatar.jpg" 10 | import ReconnectingWebSocket from 'reconnecting-websocket'; 11 | import ReactPlayer from 'react-player'; 12 | import Config from '@/components/Config/Config'; 13 | 14 | export default function AppDetails({appId, client_id}) { 15 | console.log(`Start to generate image, id: ${appId} client_id: ${client_id}`) 16 | const [app, setApp] = useState(null); 17 | const [loading, setLoading] = useState(false); 18 | const [srcImage, setSrcImage] = useState("https://github.com/huanyingtianhe/EasyComfyUI/assets/5997003/d6df6d6b-510f-48eb-bc8e-ee165fd5c55b"); 19 | const [resultVideo, setResultVideo] = useState(null); 20 | const [workflow, setWorkflow] = useState(null); 21 | const [lastNodeId, setLastNodeId] = useState(null); 22 | const [promptId, setPromptId] = useState(null); 23 | const [loadingHistory, setLoadingHistory] = useState(false) 24 | const config = Config(); 25 | 26 | useEffect(()=> { 27 | const fetchData = async (id) =>{ 28 | const res = await fetch(`/api/apps/${id}`, { 29 | cache: "no-store", 30 | }); 31 | 32 | if (!res.ok) { 33 | return notFound() 34 | } 35 | 36 | const appData = await res.json() 37 | setApp(appData); 38 | setWorkflow(appData.workflow); 39 | } 40 | 41 | fetchData(appId).catch((e) => { 42 | console.error("An error occurred while fetching data: ", e); 43 | }) 44 | }, []); 45 | 46 | useEffect( ()=> { 47 | console.log("Start to call web socket to get image, client_id: ", client_id) 48 | // create webSocket 49 | const address = config.comfyUIBaseAddress.replace("https", "wss"); 50 | console.log(`socket address: ${address}`); 51 | const options = { 52 | maxReconnectionDelay: 20000, 53 | minReconnectionDelay: 10000 + Math.random() * 1000, 54 | connectionTimeout: 20000, 55 | reconnectionDelayGrowFactor: 2, 56 | }; 57 | const socket = new ReconnectingWebSocket(address + '/ws?clientId=' + client_id, [], options); 58 | socket.addEventListener('open', (event) => { 59 | console.log('Connected to the server'); 60 | }); 61 | socket.addEventListener('error', console.error); 62 | socket.addEventListener('message', (event) => { 63 | console.log("got event from web socket, event data: ", event.data) 64 | const data = JSON.parse(event.data); 65 | 66 | if (data.type === 'progress') { 67 | //IS_GENERATING = true; 68 | console.log("Generating..."); 69 | console.log("is loading: ", loading); 70 | } else if (data.type === "executing"){ 71 | //setLoading(true); 72 | console.log("executing..."); 73 | console.log("is loading: ", loading); 74 | if("node" in data['data'] && data['data']['node'] != null){ 75 | const nodeId = parseInt(data['data']['node']); 76 | console.log("last node id: ", nodeId) 77 | setLastNodeId(nodeId) 78 | } 79 | } else if (data.type === 'executed') { 80 | if('gifs' in data['data']['output']){ 81 | const videos = data['data']['output']['gifs']; 82 | for (let i = 0; i < videos.length; i++) { 83 | const filename = videos[i]['filename'] 84 | const subfolder = videos[i]['subfolder'] 85 | const rand = Math.random(); 86 | const path = `/viewvideo?filename=${filename}&type=output&subfolder=${subfolder}&rand=${rand}`; 87 | console.log("Got video path: ", config.comfyUIBaseAddress + path); 88 | setResultVideo(config.comfyUIBaseAddress + path); 89 | setSrcImage(null); 90 | } 91 | }else if ('images' in data['data']['output']) { 92 | const images = data['data']['output']['images']; 93 | for (let i = 0; i < images.length; i++) { 94 | const filename = images[i]['filename'] 95 | const subfolder = images[i]['subfolder'] 96 | const rand = Math.random(); 97 | const path = `/view?filename=${filename}&type=output&subfolder=${subfolder}&rand=${rand}`; 98 | console.log("image path: ", config.comfyUIBaseAddress + path); 99 | setSrcImage(config.comfyUIBaseAddress + path); 100 | setResultVideo(null) 101 | } 102 | } 103 | 104 | setLoading(false); 105 | } else if (data.type === 'execution_interrupted') { 106 | console.log('Execution Interrupted'); 107 | } else if (data.type === 'status') { 108 | const IS_GENERATING = (data['data']['status']['exec_info']['queue_remaining'] > 0) ? true : false; 109 | console.log("is generationg: ", IS_GENERATING); 110 | console.log("is loading: ", loading); 111 | if (!IS_GENERATING && promptId != null && loading){ 112 | setLoadingHistory(true) 113 | setLoading(false); 114 | } 115 | } 116 | }); 117 | 118 | // clean up function 119 | return () => socket.close(); 120 | }, []); 121 | const data = new FormData(); 122 | data.set("comfyUIBaseAddress", config.comfyUIBaseAddress); 123 | useEffect(() => { 124 | const fetchHistory = async() => { 125 | if(loadingHistory){ 126 | try{ 127 | console.log("Start to get history info"); 128 | const response = await fetch('/api/comfyui/history/' + promptId, 129 | { 130 | method: 'POST', 131 | body: data, 132 | }) 133 | 134 | const res = await response.json(); 135 | console.log("Get history info success: ", res); 136 | // Handle response if necessary 137 | if(!response.ok) throw new Error(); 138 | }catch(error){ 139 | console.log("Failed to get history info, error: ", error); 140 | } 141 | setLoadingHistory(false); 142 | } 143 | } 144 | 145 | fetchHistory().catch(console.error); 146 | }, [loadingHistory]); 147 | 148 | return ( 149 | app &&
150 |
151 |
152 |

{app.title}

153 |
154 | 161 | {app.user.name} 162 |
163 |
164 |
165 |
166 |
167 | {app.commands.map((command) => ( 168 | command.type === "text" ? 169 | (
170 |

171 | {command.desc} 172 |

173 | 174 |
) : 175 | (
176 |

177 | {command.desc} 178 |

179 | 180 |
) 181 | ))} 182 |
183 |
184 |

Result

185 |
186 | { loading && () 193 | } 194 | { !loading && srcImage && () 201 | } 202 | { 203 | !loading && resultVideo && () 204 | } 205 |
206 |
207 |
208 |
209 | ); 210 | } -------------------------------------------------------------------------------- /src/components/ImageLoader/ImageLoaderForm.module.css: -------------------------------------------------------------------------------- 1 | .top { 2 | display: flex; 3 | } 4 | 5 | .info { 6 | flex: 1; 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: space-between; 10 | } 11 | 12 | .title{ 13 | flex: 1; 14 | font-size: 20px; 15 | } 16 | 17 | .desc{ 18 | font-size: 18px; 19 | font-weight: 300; 20 | padding-bottom: 1%; 21 | } 22 | 23 | .author { 24 | display: flex; 25 | align-items: center; 26 | gap: 10px; 27 | } 28 | 29 | .avatar { 30 | object-fit: cover; 31 | border-radius: 60%; 32 | } 33 | 34 | .content { 35 | margin-top: 50px; 36 | font-size: 20px; 37 | font-weight: 300; 38 | text-align: justify; 39 | display: grid; 40 | grid-template-columns: repeat(2, 1fr); 41 | gap: 30px; 42 | } 43 | 44 | .app { 45 | display: flex; 46 | flex-direction: column; 47 | align-items: center; 48 | border: 2px solid #fbf7f7; 49 | border-radius: 5px; 50 | } 51 | 52 | .result { 53 | display: flex; 54 | flex-direction: column; 55 | align-items: center; 56 | border: 1px solid #e8e2e0; 57 | border-radius: 5px; 58 | } 59 | 60 | .item{ 61 | font-size: 18px; 62 | font-weight: 300; 63 | margin-top: 10px; 64 | margin-bottom: 5px; 65 | width: 95%; 66 | } 67 | 68 | .promptForm { 69 | display: grid; 70 | grid-template-columns: 1fr; 71 | margin-top: 5px; 72 | width: 100%; 73 | justify-content: center; 74 | } 75 | 76 | .prompt{ 77 | padding: 20px; 78 | background-color: transparent; 79 | border: 1px solid #e7e7e7; 80 | color:#e4eae6; 81 | border-radius: 5px; 82 | font-size: 18px; 83 | width: 100%; 84 | } 85 | 86 | .generate { 87 | margin-top: 5px; 88 | border: 1px solid #fbf7f7; 89 | border-radius: 5px; 90 | font-size: 16px; 91 | font-weight: normal; 92 | color: #fbf7f7; 93 | background-color: #cbdbd328; 94 | width: 100%; 95 | } 96 | 97 | .image { 98 | margin-top: 1%; 99 | object-fit: cover; 100 | } 101 | 102 | .loading{ 103 | margin-top: 1%; 104 | object-fit: none; 105 | } 106 | 107 | .chooseform { 108 | display: flex; 109 | justify-content: space-between; 110 | align-items: stretch; 111 | } 112 | 113 | .choosemedia { 114 | display: flex; 115 | justify-content: center; 116 | align-items: center; 117 | width: 45%; 118 | } 119 | 120 | .chooseinput{ 121 | border: 1px solid #e7e7e7; 122 | border-radius: 3px; 123 | color: #fbf7f7; 124 | background-color: #2f121a; 125 | flex: 6; 126 | } 127 | 128 | .chooseupload { 129 | border: 1px solid #e7e7e7; 130 | border-radius: 3px; 131 | font-size: 12px; 132 | color: #fbf7f7; 133 | background-color: #e7e7e7; 134 | flex: 1; 135 | } 136 | 137 | .uploadblock{ 138 | border: 1px solid #e7e7e7; 139 | border-radius: 3px; 140 | display: flex; 141 | flex-direction: column; 142 | justify-content: center; 143 | align-items: center; 144 | width: 100%; 145 | } -------------------------------------------------------------------------------- /src/components/ImageLoader/MediaUploader.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React, { useState, useEffect } from 'react'; 4 | import Image from 'next/image'; 5 | import ReactPlayer from 'react-player'; 6 | import styles from "./ImageLoaderForm.module.css"; 7 | import Background from "/public/illustration.png" 8 | import Error from 'next/error'; 9 | import Config from '@/components/Config/Config'; 10 | 11 | const MediaUploader = ({ app, command, workflow, setWorkflow }) => { 12 | const [getFile, setFile] = useState(); 13 | const config = Config(); 14 | 15 | async function onChange(event) { 16 | event.preventDefault() 17 | console.log("start to submit media file: ", event) 18 | const file = event.target.files[0] 19 | setFile(file) 20 | if(!file) return; 21 | 22 | console.log("Got file:", file) 23 | 24 | //Update workflow with media file name 25 | const jp = require("jsonpath"); 26 | console.log("The json path is: ", command.jsonPath); 27 | console.log("The app is: ", app) 28 | jp.apply(workflow, command.jsonPath, function(value) { return file.name }); 29 | setWorkflow(workflow) 30 | console.log("The updated workflow is : ", workflow); 31 | 32 | const data = new FormData(); 33 | data.set('file', file); 34 | data.set("comfyUIBaseAddress", config.comfyUIBaseAddress); 35 | try{ 36 | 37 | const response = await fetch('/api/comfyui/upload/image', 38 | { 39 | method: 'POST', 40 | body: data, 41 | }) 42 | // Handle response if necessary 43 | if(!response.ok) throw new Error(await response.text()); 44 | console.log("Upload media file success!") 45 | }catch(error){ 46 | console.log("Failed to upload media file, error: ", error) 47 | } 48 | } 49 | 50 | const isImage = (file) => file.type.startsWith("image") 51 | const isVideo = (file) => file.type.startsWith("video") 52 | 53 | return ( 54 |
55 |
56 | onChange(e)} 61 | > 62 |
63 |
64 | { 65 | !getFile && ( 66 | Uploading image 72 | ) 73 | } 74 | { 75 | getFile && isImage(getFile) && ( 76 | Uploaded image 82 | ) 83 | } 84 | { 85 | getFile && isVideo(getFile) && ( 86 | 87 | ) 88 | } 89 |
90 |
91 | ); 92 | } 93 | 94 | export default MediaUploader; -------------------------------------------------------------------------------- /src/components/ImageLoader/PromptForm.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./ImageLoaderForm.module.css"; 3 | import Config from "@/components/Config/Config"; 4 | 5 | const PromptForm = ({ data, command, client_id, workflow, setWorkflow, setPromptId, setLoading }) => { 6 | const config = Config() 7 | async function onSubmit(event) { 8 | event.preventDefault() 9 | const formData = new FormData(event.target) 10 | const prompt = formData.get("prompt"); 11 | console.log("Get formData on submit: ", prompt); 12 | const jp = require("jsonpath"); 13 | console.log("The json path is: ", command.jsonPath); 14 | jp.apply(workflow, command.jsonPath, function(value) { return prompt }); 15 | setWorkflow(workflow) 16 | console.log("The updated workflow is:", workflow); 17 | const body = { 'prompt': workflow, 'client_id': client_id }; 18 | const data = new FormData(); 19 | data.set("comfyUIBaseAddress", config.comfyUIBaseAddress); 20 | data.set("prompt", JSON.stringify(body)); 21 | 22 | const response = await fetch('/api/comfyui/prompt', 23 | { 24 | method: 'POST', 25 | body: data, 26 | }) 27 | // Handle response if necessary 28 | const res = await response.json(); 29 | console.log("Got response from comfyUI api: ", res); 30 | setPromptId(res['prompt_id']); 31 | setLoading(true); 32 | } 33 | 34 | console.log(`received parameters in image loader form, data: ${data}, client_id: ${client_id}`); 35 | //const GenerateImagWithWorkflow = GenerateImag.bind(null, client_id, data.workflow); 36 | //const [state, formAction] = useFormState(GenerateImagWithWorkflow, data.img) 37 | 38 | return ( 39 |
40 |
41 | 47 |
48 |
49 | 52 |
53 |
54 | ); 55 | }; 56 | 57 | export default PromptForm; 58 | -------------------------------------------------------------------------------- /src/components/Settings/Settings.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useSettingsStore } from "@/components/Store/SettingsStore"; 4 | import styles from "./settings.module.css"; 5 | import { useState } from "react"; 6 | 7 | export const SettingBox = () => { 8 | const setSettingsStore = useSettingsStore(state => state.setComfyUIAddress); 9 | const comfyUIAddress = useSettingsStore(state => state.comfyUIAddress); 10 | //const [comfyUIAddress, setSettingsStore] = useState(""); 11 | const handleCommandSubmit = async (e) => { 12 | e.preventDefault(); 13 | const comfyUIAddress = e.target[0].value; 14 | console.log("Get comfyUI address: ", comfyUIAddress) 15 | try { 16 | setSettingsStore(comfyUIAddress); 17 | // await fetch("/api/commands", { 18 | // method: "POST", 19 | // body: JSON.stringify({ 20 | // appName, 21 | // jsonPath, 22 | // desc, 23 | // type, 24 | // }), 25 | // }); 26 | e.target.reset() 27 | } catch (err) { 28 | console.log(err); 29 | } 30 | }; 31 | 32 | return ( 33 |
34 |

Settings

35 |
36 |
37 |

ComfyUI URL

38 | 39 |

ComfyUI URL: {comfyUIAddress}

40 | 41 |
42 |
43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/Settings/settings.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 10px; 3 | } 4 | 5 | .title { 6 | font-size: 60px; 7 | margin-bottom: 100px; 8 | text-align: center; 9 | } 10 | 11 | .content { 12 | display: flex; 13 | align-items: center; 14 | gap: 100px; 15 | } 16 | 17 | .imgContainer { 18 | flex: 1; 19 | height: 500px; 20 | position: relative; 21 | } 22 | 23 | .image { 24 | object-fit: contain; 25 | /* animation: move 3s infinite ease alternate; */ 26 | } 27 | 28 | @keyframes move { 29 | from { 30 | transform: translateY(-15px); 31 | } 32 | to { 33 | transform: translateY(0px) scale(1.03); 34 | } 35 | } 36 | 37 | .form { 38 | flex: 1; 39 | display: flex; 40 | flex-direction: column; 41 | gap: 20px; 42 | } 43 | 44 | .input, 45 | .textArea { 46 | padding: 20px; 47 | background-color: transparent; 48 | border: none; 49 | outline: none; 50 | color: #bbb; 51 | border: 3px solid #bbb; 52 | font-size: 16px; 53 | font-weight: bold; 54 | } 55 | 56 | .button { 57 | padding: 20px; 58 | cursor: pointer; 59 | background-color: #53c28b; 60 | border: none; 61 | border-radius: 5px; 62 | width: max-content; 63 | color: white; 64 | } 65 | -------------------------------------------------------------------------------- /src/components/Store/SettingsStore.jsx: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import { createJSONStorage, persist } from "zustand/middleware"; 3 | 4 | export const useSettingsStore = create( 5 | persist( 6 | (set) => ({ 7 | comfyUIAddress: null, 8 | setComfyUIAddress: (address) => set((state) => ({ comfyUIAddress: address })), 9 | }), 10 | { 11 | name: 'ComfyUIAddress-storage', // unique name 12 | storage: createJSONStorage(() => localStorage), 13 | }, 14 | ), 15 | ) -------------------------------------------------------------------------------- /src/components/footer/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./footer.module.css"; 3 | import Image from "next/image"; 4 | 5 | const Footer = () => { 6 | return ( 7 |
8 |
©2024 huanyingtianhe. All rights reserved.
9 |
10 | huanyingtianhe Facebook Account 11 | huanyingtianhe 12 | huanyingtianhe 13 | huanyingtianhe 14 |
15 |
16 | ); 17 | }; 18 | 19 | export default Footer; 20 | -------------------------------------------------------------------------------- /src/components/footer/footer.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 50px; 3 | font-size: 14px; 4 | display: flex; 5 | align-items: center; 6 | justify-content: space-between; 7 | } 8 | 9 | .social { 10 | display: flex; 11 | align-items: center; 12 | gap: 10px; 13 | } 14 | 15 | .icon { 16 | opacity: 0.6; 17 | cursor: pointer; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/navbar/Navbar.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import React from "react"; 5 | import styles from "./navbar.module.css"; 6 | import DarkModeToggle from "../DarkModeToggle/DarkModeToggle"; 7 | import { signOut, useSession } from "next-auth/react"; 8 | 9 | const links = [ 10 | { 11 | id: 1, 12 | title: "Home", 13 | url: "/", 14 | }, 15 | { 16 | id: 2, 17 | title: "Portfolio", 18 | url: "/portfolio", 19 | }, 20 | { 21 | id: 3, 22 | title: "Apps", 23 | url: "/apps", 24 | }, 25 | { 26 | id: 4, 27 | title: "About", 28 | url: "/about", 29 | }, 30 | { 31 | id: 5, 32 | title: "Settings", 33 | url: "/settings", 34 | }, 35 | { 36 | id: 6, 37 | title: "Dashboard", 38 | url: "/dashboard", 39 | }, 40 | ]; 41 | 42 | const Navbar = () => { 43 | const session = useSession(); 44 | 45 | return ( 46 |
47 | 48 | EasyComfyUI 49 | 50 |
51 | 52 | {links.map((link) => ( 53 | 54 | {link.title} 55 | 56 | ))} 57 | {session.status === "authenticated" && ( 58 | 61 | )} 62 |
63 |
64 | ); 65 | }; 66 | 67 | export default Navbar; 68 | -------------------------------------------------------------------------------- /src/components/navbar/navbar.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100px; 3 | display: flex; 4 | justify-content: space-between; 5 | align-items: center; 6 | } 7 | 8 | .logo { 9 | font-weight: bold; 10 | font-size: 22px; 11 | } 12 | 13 | .links { 14 | display: flex; 15 | align-items: center; 16 | gap: 20px; 17 | } 18 | 19 | .logout { 20 | padding: 5px; 21 | border: none; 22 | background-color: #53c28b; 23 | color: white; 24 | cursor: pointer; 25 | border-radius: 3px; 26 | } 27 | -------------------------------------------------------------------------------- /src/context/EnvContext.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { getEnvs } from "@/utils/env"; 4 | import { createContext, useContext, useEffect, useState } from "react"; 5 | 6 | export const EnvContext = createContext(); 7 | 8 | export const EnvProvider = ({ children }) => { 9 | const [env, setEnv] = useState(); 10 | 11 | useEffect( () => { 12 | getEnvs().then((env) => { 13 | setEnv(env); 14 | }); 15 | }, []); 16 | 17 | return {children} 18 | } 19 | 20 | export const useEnv = () => { 21 | return useContext(EnvContext); 22 | } -------------------------------------------------------------------------------- /src/context/ThemeContext.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { createContext, useState } from "react"; 4 | 5 | export const ThemeContext = createContext(); 6 | 7 | export const ThemeProvider = ({ children }) => { 8 | const [mode, setMode] = useState("dark"); 9 | 10 | const toggle = () => { 11 | setMode((prev) => (prev === "dark" ? "light" : "dark")); 12 | }; 13 | 14 | return ( 15 | 16 |
{children}
17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/models/Post.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const { Schema } = mongoose; 4 | 5 | const postSchema = new Schema( 6 | { 7 | title: { 8 | type: String, 9 | required: true, 10 | }, 11 | desc: { 12 | type: String, 13 | required: true, 14 | }, 15 | img: { 16 | type: String, 17 | required: true, 18 | }, 19 | content: { 20 | type: String, 21 | required: true, 22 | }, 23 | username: { 24 | type: String, 25 | required: true, 26 | }, 27 | }, 28 | { timestamps: true } 29 | ); 30 | 31 | //If the Post collection does not exist create a new one. 32 | export default mongoose.models.Post || mongoose.model("Post", postSchema); 33 | -------------------------------------------------------------------------------- /src/models/User.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const { Schema } = mongoose; 4 | 5 | const userSchema = new Schema( 6 | { 7 | name: { 8 | type: String, 9 | unique: true, 10 | required: true, 11 | }, 12 | email: { 13 | type: String, 14 | unique: true, 15 | required: true, 16 | }, 17 | password: { 18 | type: String, 19 | required: true, 20 | }, 21 | }, 22 | { timestamps: true } 23 | ); 24 | 25 | //If the User collection does not exist create a new one. 26 | export default mongoose.models.User || mongoose.model("User", userSchema); 27 | -------------------------------------------------------------------------------- /src/utils/db.js: -------------------------------------------------------------------------------- 1 | const { PrismaClient } = require('@prisma/client') 2 | 3 | var connection = null; 4 | 5 | const connect = async () => { 6 | try { 7 | if (connection) { 8 | console.log("use existing db connection") 9 | return connection 10 | } 11 | else { 12 | connection = new PrismaClient() 13 | return connection; 14 | } 15 | } catch (error) { 16 | throw new Error("Connection failed!"); 17 | } 18 | }; 19 | 20 | export default connect; 21 | -------------------------------------------------------------------------------- /src/utils/env.js: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | export const getEnvs = async () => { 4 | return { 5 | // Feature Flags 6 | //NEW_FEATURE_ENABLED: process.env.NEW_FEATURE_ENABLED, 7 | // Environment Variables 8 | ComfyUI_BASE_ADDRESS: process.env.ComfyUI_BASE_ADDRESS, 9 | // Values 10 | //HAS_SUBSCRIPTION: process.env.HAS_SUBSCRIPTION, 11 | }; 12 | }; -------------------------------------------------------------------------------- /vercel.Json: -------------------------------------------------------------------------------- 1 | { 2 | "images": { 3 | "remotePatterns": [ 4 | { 5 | "protocol": "https", 6 | "hostname": "**.baidu.com", 7 | "port": "" 8 | }, 9 | { 10 | "protocol": "https", 11 | "hostname": "images.pexels.com", 12 | "port": "" 13 | }, 14 | { 15 | "protocol": "https", 16 | "hostname": "**.trycloudflare.com", 17 | "port": "" 18 | }, 19 | { 20 | "protocol": "https", 21 | "hostname": "github.com", 22 | "port": "" 23 | } 24 | ], 25 | "minimumCacheTTL": 60, 26 | "formats": ["image/webp"], 27 | "dangerouslyAllowSVG": false, 28 | "contentSecurityPolicy": "script-src 'none'; frame-src 'none'; sandbox;", 29 | "contentDispositionType": "inline" 30 | } 31 | } --------------------------------------------------------------------------------