yadongxie commited on
Commit
89682f8
1 Parent(s): 5818b4d

feat: add web

Browse files
.gitignore ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ .yarn/install-state.gz
8
+
9
+ # testing
10
+ /coverage
11
+
12
+ # next.js
13
+ /.next/
14
+ /out/
15
+
16
+ # production
17
+ /build
18
+
19
+ # misc
20
+ .idea
21
+ photon/__pycache__/
22
+ .DS_Store
23
+ *.pem
24
+
25
+ # debug
26
+ npm-debug.log*
27
+ yarn-debug.log*
28
+ yarn-error.log*
29
+
30
+ # local env files
31
+ .env*.local
32
+
33
+ # vercel
34
+ .vercel
35
+
36
+ # typescript
37
+ *.tsbuildinfo
38
+ next-env.d.ts
README.md CHANGED
@@ -7,5 +7,51 @@ sdk: static
7
  pinned: false
8
  license: mit
9
  ---
 
 
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  pinned: false
8
  license: mit
9
  ---
10
+ <div align="center">
11
+ <h1 align="center">🐱 tryEmoji</h1>
12
 
13
+ Turn emoji into amazing artwork via AI
14
+
15
+ <a href="https://tryemoji.com">
16
+ <img src="https://tryemoji.com/preview.png">
17
+ </a>
18
+ </div>
19
+
20
+ ## Features
21
+
22
+ - Includes complete front-end and back-end code.
23
+ - Support deployment both locally and in the cloud.
24
+ - Fully based on open source and can be used for commercial purposes.
25
+
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ # Install web dependencies
31
+ npm install
32
+
33
+ # Install server dependencies
34
+ pip install -r requirements.txt -U
35
+ ```
36
+
37
+ ## Development
38
+
39
+ ```bash
40
+ # Start server on localhost:8080
41
+
42
+ lep photon run -n tryemoji -m photon/main.py --local
43
+ ```
44
+
45
+ ```bash
46
+ # Start web server on localhost:3000
47
+
48
+ npm run dev
49
+ ```
50
+
51
+
52
+
53
+ ## Built with
54
+
55
+ - [Lepton AI](https://github.com/leptonai/leptonai)
56
+ - [emoji-mart](https://github.com/missive/emoji-mart)
57
+ - [Real-Time-Latent-Consistency-Model](https://huggingface.co/spaces/radames/Real-Time-Latent-Consistency-Model)
index.html CHANGED
@@ -3,17 +3,10 @@
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
  <link rel="stylesheet" href="style.css" />
8
  </head>
9
  <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
  </body>
19
  </html>
 
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width" />
6
+ <title>🐤 tryEmoji</title>
7
  <link rel="stylesheet" href="style.css" />
8
  </head>
9
  <body>
10
+ <iframe src="https://www.tryemoji.com/"></iframe>
 
 
 
 
 
 
 
11
  </body>
12
  </html>
next.config.js ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {};
3
+
4
+ module.exports = nextConfig;
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "useemoji",
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
+ "@emoji-mart/data": "^1.1.2",
13
+ "@emoji-mart/react": "^1.1.1",
14
+ "@radix-ui/react-select": "^2.0.0",
15
+ "@radix-ui/react-slider": "^1.1.2",
16
+ "@radix-ui/react-toast": "^1.1.5",
17
+ "@radix-ui/react-tooltip": "^1.0.7",
18
+ "@vercel/analytics": "^1.1.1",
19
+ "@vercel/kv": "^1.0.0",
20
+ "class-variance-authority": "^0.7.0",
21
+ "clsx": "^2.0.0",
22
+ "emoji-mart": "^5.5.2",
23
+ "lucide-react": "^0.294.0",
24
+ "lz-string": "^1.5.0",
25
+ "next": "14.0.3",
26
+ "react": "^18",
27
+ "react-dom": "^18",
28
+ "react-responsive": "^9.0.2",
29
+ "react-share": "^5.0.2",
30
+ "swr": "^2.2.4",
31
+ "tailwind-merge": "^2.0.0",
32
+ "use-debounce": "^10.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/lz-string": "^1.5.0",
36
+ "@types/node": "^20",
37
+ "@types/react": "^18",
38
+ "@types/react-dom": "^18",
39
+ "autoprefixer": "^10.0.1",
40
+ "eslint": "^8",
41
+ "eslint-config-next": "14.0.3",
42
+ "eslint-config-prettier": "^9.0.0",
43
+ "eslint-plugin-prettier": "^5.0.1",
44
+ "eslint-plugin-unused-imports": "^3.0.0",
45
+ "postcss": "^8",
46
+ "prettier": "^3.1.0",
47
+ "tailwindcss": "^3.3.0",
48
+ "typescript": "^5"
49
+ }
50
+ }
photon/main.py ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from contextlib import nullcontext
2
+ from io import BytesIO
3
+ import os
4
+ import threading
5
+ from typing import Optional, Union
6
+ import warnings
7
+
8
+ from compel import Compel
9
+ from fastapi.responses import StreamingResponse
10
+ from loguru import logger
11
+ from PIL import Image
12
+ import torch
13
+
14
+ from leptonai.photon import Photon, FileParam, get_file_content, HTTPException
15
+
16
+
17
+ EXAMPLE_IMAGE_BASE64 = "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"
18
+
19
+
20
+ class JPEGResponse(StreamingResponse):
21
+ media_type = "image/jpeg"
22
+
23
+
24
+ class ImgPilot(Photon):
25
+ requirement_dependency = [
26
+ "torch",
27
+ "diffusers",
28
+ "invisible-watermark",
29
+ "compel",
30
+ "Pillow",
31
+ ]
32
+
33
+ # In default, we will use gpu.a10 as the computation resource shape. This should
34
+ # be fast enough.
35
+ deployment_template = {
36
+ "resource_shape": "gpu.a10",
37
+ "env": {
38
+ "MODEL": "SimianLuo/LCM_Dreamshaper_v7",
39
+ "USE_TORCH_COMPILE": "false",
40
+ "WIDTH": "768",
41
+ "HEIGHT": "768",
42
+ "PRINT_PROMPT": "false",
43
+ },
44
+ }
45
+
46
+ # A10 should be able to support a maximum concurrency of 8 requests to interleave
47
+ # IO and compute. This is not tuned by the way.
48
+ handler_max_concurrency = 1
49
+
50
+ def init(self):
51
+ from diffusers import AutoPipelineForImage2Image # type: ignore
52
+
53
+ cuda_available = torch.cuda.is_available()
54
+
55
+ if cuda_available:
56
+ self.device = torch.device("cuda")
57
+ else:
58
+ self.device = torch.device("cpu")
59
+
60
+ self.base = AutoPipelineForImage2Image.from_pretrained(
61
+ os.environ["MODEL"],
62
+ torch_dtype=torch.float16 if cuda_available else torch.float32,
63
+ )
64
+ self.base.safety_checker = None
65
+ self.base.requires_safety_checker = False
66
+ if self.handler_max_concurrency > 1:
67
+ self.base_lock = threading.Lock()
68
+ else:
69
+ self.base_lock = nullcontext()
70
+ self.print_prompt = os.environ["PRINT_PROMPT"].lower() in [
71
+ "true",
72
+ "t",
73
+ "1",
74
+ "yes",
75
+ "y",
76
+ ]
77
+ logger.info(f"print_prompt: {self.print_prompt}")
78
+ if cuda_available:
79
+ self.base.to("cuda")
80
+ self.use_torch_compile = os.environ["USE_TORCH_COMPILE"].lower() in [
81
+ "true",
82
+ "t",
83
+ "1",
84
+ "yes",
85
+ "y",
86
+ ]
87
+ if self.use_torch_compile:
88
+ if self.handler_max_concurrency > 1:
89
+ warnings.warn(
90
+ "torch compile does not support multithreading, so we will"
91
+ " disable torch compile since handler_max_concurrency > 1."
92
+ )
93
+ else:
94
+ self.width = int(os.environ["WIDTH"])
95
+ self.height = int(os.environ["HEIGHT"])
96
+ logger.info(
97
+ "Compiling model with torch.compile. Note that with torch"
98
+ " compile, your first invocation will be slow, but subsequent"
99
+ " invocations will be faster."
100
+ )
101
+ self.base.unet = torch.compile(
102
+ self.base.unet, mode="reduce-overhead", fullgraph=True
103
+ )
104
+ else:
105
+ self.use_torch_compile = False
106
+
107
+ self.compel_proc = Compel(
108
+ tokenizer=self.base.tokenizer,
109
+ text_encoder=self.base.text_encoder,
110
+ truncate_long_prompts=False,
111
+ ) # type: ignore
112
+
113
+ logger.info(f"Initialized model {os.environ['MODEL']}. cuda: {cuda_available}.")
114
+
115
+ @Photon.handler(
116
+ "run",
117
+ example={
118
+ "prompt": (
119
+ "Portrait of The Terminator, glare pose, detailed, intricate, full of"
120
+ " colour, cinematic lighting, trending on artstation, 8k,"
121
+ " hyperrealistic, focused, extreme details, unreal engine 5, cinematic,"
122
+ " masterpiece"
123
+ ),
124
+ "seed": 2159232,
125
+ "strength": 0.5,
126
+ "steps": 4,
127
+ "guidance_scale": 8.0,
128
+ "width": 512,
129
+ "height": 512,
130
+ "lcm_steps": 50,
131
+ "input_image": EXAMPLE_IMAGE_BASE64,
132
+ },
133
+ )
134
+ def run(
135
+ self,
136
+ prompt: str,
137
+ seed: int,
138
+ strength: float,
139
+ steps: int,
140
+ guidance_scale: float,
141
+ width: int,
142
+ height: int,
143
+ lcm_steps: int,
144
+ input_image: Optional[Union[str, FileParam]],
145
+ ) -> JPEGResponse:
146
+ from diffusers.utils import load_image # type: ignore
147
+ import time
148
+
149
+ start = time.time()
150
+
151
+ if self.print_prompt:
152
+ logger.info(f"Prompt: {prompt}")
153
+
154
+ # diffusers truncates prompt to 77 tokens, in case prompt is too long, we will
155
+ # use compel to process the prompt (but compel is slower)
156
+ tokens = self.base.tokenizer(prompt, return_tensors="pt")
157
+ if tokens.input_ids.shape[1] > 77:
158
+ prompt_embeds = self.compel_proc(prompt)
159
+ prompt = None
160
+ else:
161
+ prompt_embeds = None
162
+
163
+ if input_image is not None:
164
+ image_file = get_file_content(input_image, return_file=True)
165
+ pil_image = Image.open(image_file, formats=["JPEG", "PNG", "GIF", "BMP"])
166
+ if self.use_torch_compile:
167
+ # checks width and height parameter, and return error if width and height are not correct
168
+ if width != self.width or height != self.height:
169
+ raise HTTPException(
170
+ status_code=400,
171
+ detail=(
172
+ f"width and height must be {self.width} and"
173
+ f" {self.height} when use_torch_compile is true."
174
+ ),
175
+ )
176
+ # checks input image height and width, and resize if necessary
177
+ if pil_image.height != self.height or pil_image.width != self.width:
178
+ pil_image = pil_image.resize(
179
+ (self.width, self.height), Image.BILINEAR
180
+ )
181
+ input_image = load_image(pil_image).convert("RGB")
182
+
183
+ with self.base_lock:
184
+ generator = torch.manual_seed(seed)
185
+ output_image = self.base(
186
+ prompt=prompt,
187
+ prompt_embeds=prompt_embeds,
188
+ generator=generator,
189
+ image=input_image,
190
+ strength=strength,
191
+ num_inference_steps=steps,
192
+ guidance_scale=guidance_scale,
193
+ width=width,
194
+ height=height,
195
+ lcm_origin_steps=lcm_steps,
196
+ output_type="pil",
197
+ ) # type: ignore
198
+
199
+ nsfw_content_detected = (
200
+ output_image.nsfw_content_detected[0]
201
+ if "nsfw_content_detected" in output_image
202
+ else False
203
+ ) # type: ignore
204
+ if nsfw_content_detected:
205
+ raise HTTPException(status_code=400, detail="nsfw content detected")
206
+ else:
207
+ img_io = BytesIO()
208
+ output_image.images[0].save(img_io, format="JPEG") # type: ignore
209
+ img_io.seek(0)
210
+ logger.info(f"Produced output in {time.time() - start} seconds.")
211
+ return JPEGResponse(img_io)
212
+
213
+
214
+ if __name__ == "__main__":
215
+ p = ImgPilot()
216
+ p.launch()
postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ leptonai
2
+ torch
3
+ diffusers
4
+ invisible-watermark
5
+ compel
6
+ Pillow
src/app/api/run/route.ts ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest } from "next/server";
2
+
3
+ const API_URL = process.env?.API_URL || "http://127.0.0.1:8080";
4
+ const API_TOKEN = process.env?.API_TOKEN || "";
5
+
6
+ export async function POST(req: NextRequest) {
7
+ const headers = new Headers();
8
+ headers.set("Accept", `image/jpeg`);
9
+ headers.set("Authorization", `Bearer ${API_TOKEN}`);
10
+ headers.set(
11
+ "Content-Type",
12
+ req.headers.get("Content-Type") || "application/json",
13
+ );
14
+ const url = new URL("/run", API_URL);
15
+
16
+ return fetch(url.toString(), {
17
+ body: req.body,
18
+ method: req.method,
19
+ headers,
20
+ duplex: "half",
21
+ } as unknown as RequestInit);
22
+ }
src/app/api/share/route.ts ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest } from "next/server";
2
+ import { createClient } from "@vercel/kv";
3
+
4
+ const kv =
5
+ process.env?.KV_REST_API_URL && process.env?.KV_REST_API_TOKEN
6
+ ? createClient({
7
+ url: process.env.KV_REST_API_URL,
8
+ token: process.env.KV_REST_API_TOKEN,
9
+ })
10
+ : null;
11
+
12
+ export async function POST(req: NextRequest) {
13
+ const { key, image } = await req.json();
14
+
15
+ if (!kv || !key || !image) {
16
+ return new Response("", {
17
+ status: 200,
18
+ });
19
+ }
20
+
21
+ const slug = key.replace(/[^a-zA-Z0-9]/g, "_");
22
+
23
+ await kv.set(slug, image);
24
+
25
+ return new Response("", {
26
+ status: 200,
27
+ });
28
+ }
29
+
30
+ export async function GET(req: NextRequest) {
31
+ const key = req.nextUrl.searchParams.get("share");
32
+
33
+ if (!kv || !key) {
34
+ return new Response("", {
35
+ status: 200,
36
+ });
37
+ }
38
+ const slug = key.replace(/[^a-zA-Z0-9]/g, "_");
39
+ const image = await kv.get<string>(slug);
40
+
41
+ if (!image) {
42
+ return new Response("", {
43
+ status: 200,
44
+ });
45
+ }
46
+
47
+ return new Response(image, {
48
+ status: 200,
49
+ });
50
+ }
src/app/globals.css ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+
6
+ [data-unified="1f5ff"],
7
+ [data-unified="1f997"],
8
+ [data-unified="1fab1"],
9
+ [data-unified="1f95c"],
10
+ [data-unified="1fa7b"],
11
+ [data-unified="1fa79"] {
12
+ display: none !important;
13
+ }
14
+
15
+
16
+ em-emoji-picker {
17
+ height: 512px;
18
+ width: 320px;
19
+ }
20
+
21
+ @media (max-width: 768px) {
22
+ em-emoji-picker {
23
+ height: 100px;
24
+ min-height: 100px;
25
+ width: 100%;
26
+ }
27
+ }
src/app/icon.svg ADDED
src/app/layout.tsx ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata, Viewport } from "next";
2
+ import { Sriracha } from "next/font/google";
3
+ import "./globals.css";
4
+ import { ReactNode } from "react";
5
+ import { Analytics } from "@vercel/analytics/react";
6
+
7
+ const inter = Sriracha({ weight: "400", subsets: ["latin"] });
8
+
9
+ const title = "tryEmoji";
10
+ const description = "Turn emoji into amazing artwork via AI";
11
+
12
+ export const metadata: Metadata = {
13
+ title,
14
+ description,
15
+ openGraph: {
16
+ title,
17
+ description,
18
+ type: "website",
19
+ url: "https://tryemoji.com",
20
+ images: [
21
+ {
22
+ url: "https://tryemoji.com/og.png",
23
+ width: 630,
24
+ height: 473,
25
+ alt: "tryEmoji",
26
+ },
27
+ ],
28
+ },
29
+ };
30
+
31
+ const viewport: Viewport = {
32
+ width: "device-width",
33
+ initialScale: 1,
34
+ userScalable: false,
35
+ maximumScale: 1,
36
+ minimumScale: 1,
37
+ };
38
+
39
+ export { viewport };
40
+
41
+ export default function RootLayout({ children }: { children: ReactNode }) {
42
+ return (
43
+ <html lang="en">
44
+ <body className={inter.className}>{children}</body>
45
+ <Analytics></Analytics>
46
+ </html>
47
+ );
48
+ }
src/app/og/image.png ADDED
src/app/og/route.tsx ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ImageResponse } from "next/og";
2
+ import { createClient } from "@vercel/kv";
3
+ import { shareString2Json } from "@/util/use-share";
4
+
5
+ const kv =
6
+ process.env?.KV_REST_API_URL && process.env?.KV_REST_API_TOKEN
7
+ ? createClient({
8
+ url: process.env.KV_REST_API_URL,
9
+ token: process.env.KV_REST_API_TOKEN,
10
+ })
11
+ : null;
12
+
13
+ export const runtime = "edge";
14
+
15
+ const siteUrl =
16
+ process.env.NODE_ENV === "production"
17
+ ? "https://www.tryemoji.com/"
18
+ : "http://localhost:3000/";
19
+
20
+ export async function GET(request: Request) {
21
+ const { searchParams } = new URL(request.url);
22
+ const image = await fetch(new URL("./image.png", import.meta.url)).then(
23
+ (res) => res.arrayBuffer(),
24
+ );
25
+ let base64URL = "";
26
+ let emoji = "";
27
+ const share = searchParams.get("share");
28
+ const option = share ? shareString2Json(share as string) : null;
29
+ const apiURL = new URL("api/share", siteUrl);
30
+ if (share) {
31
+ apiURL.searchParams.set("share", share);
32
+ const res = await fetch(apiURL.toString());
33
+ base64URL = await res.text();
34
+ emoji = option?.emoji || "👍";
35
+ }
36
+ return new ImageResponse(
37
+ (
38
+ <div
39
+ style={{
40
+ display: "flex",
41
+ background: "#f6f6f6",
42
+ width: "100%",
43
+ height: "100%",
44
+ flexDirection: "column",
45
+ justifyContent: "center",
46
+ alignItems: "center",
47
+ position: "relative",
48
+ }}
49
+ >
50
+ <img
51
+ style={{
52
+ position: "absolute",
53
+ top: 0,
54
+ left: 0,
55
+ width: "100%",
56
+ height: "100%",
57
+ objectFit: "cover",
58
+ objectPosition: "center",
59
+ }}
60
+ width="800"
61
+ height="471"
62
+ src={image as unknown as string}
63
+ />
64
+ {emoji && (
65
+ <span
66
+ style={{
67
+ width: 45,
68
+ height: 45,
69
+ textAlign: "center",
70
+ lineHeight: "40px",
71
+ background: "#0a0a0b",
72
+ position: "absolute",
73
+ top: 38,
74
+ left: 305,
75
+ fontSize: 40,
76
+ fontFamily: "sans-serif",
77
+ }}
78
+ >
79
+ {emoji}
80
+ </span>
81
+ )}
82
+ {base64URL && (
83
+ <img
84
+ src={base64URL}
85
+ width="302"
86
+ height="302"
87
+ style={{
88
+ position: "absolute",
89
+ borderRadius: 8,
90
+ top: 125,
91
+ right: 150,
92
+ width: 302,
93
+ height: 302,
94
+ objectFit: "cover",
95
+ objectPosition: "center",
96
+ }}
97
+ />
98
+ )}
99
+ </div>
100
+ ),
101
+ {
102
+ width: 800,
103
+ height: 471,
104
+ },
105
+ );
106
+ }
src/app/page.tsx ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Metadata } from "next";
2
+ import TryEmoji from "@/components/try-emoji";
3
+
4
+ type Props = {
5
+ params: { share?: string };
6
+ searchParams: { [key: string]: string | string[] | undefined };
7
+ };
8
+
9
+ export async function generateMetadata(po: Props): Promise<Metadata> {
10
+ // read route params
11
+ const share = po.searchParams?.share;
12
+
13
+ const siteUrl =
14
+ process.env.NODE_ENV === "production"
15
+ ? "https://www.tryemoji.com/"
16
+ : "http://localhost:3000/";
17
+ const ogUrl = new URL("og", siteUrl);
18
+ if (share) {
19
+ ogUrl.searchParams.set("share", share as string);
20
+ }
21
+ return {
22
+ openGraph: {
23
+ images: [
24
+ {
25
+ url: ogUrl.toString(),
26
+ width: 630,
27
+ height: 473,
28
+ alt: "tryEmoji",
29
+ },
30
+ ],
31
+ },
32
+ };
33
+ }
34
+
35
+ export default function Home() {
36
+ return <TryEmoji />;
37
+ }
src/components/dice.tsx ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Dice1, Dice2, Dice3, Dice4, Dice5, Dice6 } from "lucide-react";
2
+ import { FC, useState } from "react";
3
+
4
+ const dices = [
5
+ <Dice1 key="dice-1" />,
6
+ <Dice2 key="dice-2" />,
7
+ <Dice3 key="dice-3" />,
8
+ <Dice4 key="dice-4" />,
9
+ <Dice5 key="dice-5" />,
10
+ <Dice6 key="dice-6" />,
11
+ ];
12
+
13
+ export const Dice: FC = () => {
14
+ const [click, setClick] = useState(false);
15
+ const [currentNumber, setCurrentNumber] = useState(3);
16
+ const rollDice = () => {
17
+ let rollingTime = 0;
18
+ setClick(true);
19
+
20
+ const rollInterval = setInterval(
21
+ () => {
22
+ setCurrentNumber(Math.floor(Math.random() * 6));
23
+ rollingTime += 100;
24
+ // Slow down the rolling
25
+ if (rollingTime >= 1200) {
26
+ clearInterval(rollInterval);
27
+ setClick(false);
28
+ }
29
+ },
30
+ 100 - rollingTime / 20,
31
+ );
32
+ };
33
+ return (
34
+ <div
35
+ className={click ? "animate-[shake_1.2s_ease-in-out]" : ""}
36
+ onClick={(event) => {
37
+ event.preventDefault();
38
+ rollDice();
39
+ }}
40
+ >
41
+ {dices[currentNumber]}
42
+ </div>
43
+ );
44
+ };
src/components/emoji-selector.tsx ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Picker from "@emoji-mart/react";
2
+ import data from "@emoji-mart/data";
3
+ import { FC } from "react";
4
+ import { useMediaQuery } from "react-responsive";
5
+
6
+ const emojis = (data as unknown as any).emojis as { [key: string]: EmojiSkin };
7
+ export interface EmojiData {
8
+ id: string;
9
+ name: string;
10
+ native: string;
11
+ unified: string;
12
+ keywords: string[];
13
+ shortcodes: string;
14
+ skin: number;
15
+ aliases: string[];
16
+ }
17
+
18
+ const exceptEmojis = [
19
+ "bat",
20
+ "feet",
21
+ "coral",
22
+ "snail",
23
+ "bug",
24
+ "ant",
25
+ "bee",
26
+ "beetle",
27
+ "ladybug",
28
+ "cricket",
29
+ "cockroach",
30
+ "spider",
31
+ "scorpion",
32
+ "mosquito",
33
+ "fly",
34
+ "worm",
35
+ "microbe",
36
+ "gorilla",
37
+ "orangutan",
38
+ "tiger2",
39
+ "leopard",
40
+ "zebra_face",
41
+ "pig_nose",
42
+ "camel",
43
+ "black_cat",
44
+ "water_buffalo",
45
+ "rat",
46
+ "spider_web",
47
+ "service_dog",
48
+ "mammoth",
49
+ "frog",
50
+ "crocodile",
51
+ "lizard",
52
+ "snake",
53
+ "t-rex",
54
+ "dragon",
55
+ "empty_nest",
56
+ "octopus",
57
+ "ox",
58
+ "wolf",
59
+ "headstone",
60
+ "moyai",
61
+ "new_moon",
62
+ "new_moon_with_face",
63
+ "shrimp",
64
+ "lobster",
65
+ "fried_shrimp",
66
+ "coffin",
67
+ "drop_of_blood",
68
+ "pinata",
69
+ "performing_arts",
70
+ "rock",
71
+ "clubs",
72
+ "chess_pawn",
73
+ "spades",
74
+ "knot",
75
+ "bathtub",
76
+ "shower",
77
+ "white_flower",
78
+ "hammer",
79
+ "nazar_amulet",
80
+ "hamsa",
81
+ "hammer_and_wrench",
82
+ "squid",
83
+ "crab",
84
+ "smoking",
85
+ "dna",
86
+ "musical_score",
87
+ "musical_note",
88
+ "notes",
89
+ "dark_sunglasses",
90
+ "kaaba",
91
+ "old_key",
92
+ "bikini",
93
+ "one-piece_swimsuit",
94
+ "sari",
95
+ "sloth",
96
+ "x-ray",
97
+ ];
98
+
99
+ interface EmojiSkin {
100
+ id: string;
101
+ name: string;
102
+ keywords: string[];
103
+ skins: { native: string; shortcodes: string; unified: string }[];
104
+ }
105
+
106
+ const categoryIcons = {
107
+ categoryIcons: {
108
+ "new-people": {
109
+ svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M57.89 397.2c-6.262-8.616-16.02-13.19-25.92-13.19c-23.33 0-31.98 20.68-31.98 32.03c0 6.522 1.987 13.1 6.115 18.78l46.52 64C58.89 507.4 68.64 512 78.55 512c23.29 0 31.97-20.66 31.97-32.03c0-6.522-1.988-13.1-6.115-18.78L57.89 397.2zM496.1 352c-44.13 0-79.72 35.75-79.72 80s35.59 80 79.72 80s79.91-35.75 79.91-80S540.2 352 496.1 352zM640 99.38c0-13.61-4.133-27.34-12.72-39.2l-23.63-32.5c-13.44-18.5-33.77-27.68-54.12-27.68c-13.89 0-27.79 4.281-39.51 12.8L307.8 159.7C262.2 192.8 220.4 230.9 183.4 273.4c-24.22 27.88-59.18 63.99-103.5 99.63l56.34 77.52c53.79-35.39 99.15-55.3 127.1-67.27c51.88-22 101.3-49.87 146.9-82.1l202.3-146.7C630.5 140.4 640 120 640 99.38z"/></svg>',
110
+ },
111
+ },
112
+ };
113
+ const custom = [
114
+ {
115
+ id: "recommend",
116
+ name: "Recommend",
117
+ emojis: [
118
+ emojis["baby_chick"],
119
+ emojis["hatched_chick"],
120
+ emojis["dog"],
121
+ emojis["fox_face"],
122
+ emojis["lion_face"],
123
+ emojis["tiger"],
124
+ emojis["hamster"],
125
+ emojis["panda_face"],
126
+ emojis["rabbit"],
127
+ emojis["polar_bear"],
128
+ emojis["tangerine"],
129
+ emojis["watermelon"],
130
+ emojis["pineapple"],
131
+ emojis["beer"],
132
+ emojis["curry"],
133
+ emojis["cake"],
134
+ emojis["snow_capped_mountain"],
135
+ emojis["volcano"],
136
+ emojis["bridge_at_night"],
137
+ emojis["kiwifruit"],
138
+ emojis["stadium"],
139
+ emojis["foggy"],
140
+ emojis["night_with_stars"],
141
+ emojis["cityscape"],
142
+ emojis["sunrise_over_mountains"],
143
+ emojis["sunrise"],
144
+ emojis["city_sunset"],
145
+ emojis["city_sunrise"],
146
+ ],
147
+ },
148
+ {
149
+ id: "new-people",
150
+ name: "People",
151
+ emojis: [
152
+ emojis["child"],
153
+ emojis["boy"],
154
+ emojis["girl"],
155
+ emojis["adult"],
156
+ emojis["person_with_blond_hair"],
157
+ emojis["man"],
158
+ emojis["bearded_person"],
159
+ emojis["man_with_beard"],
160
+ emojis["woman_with_beard"],
161
+ emojis["red_haired_man"],
162
+ emojis["curly_haired_man"],
163
+ emojis["white_haired_man"],
164
+ emojis["bald_man"],
165
+ emojis["woman"],
166
+ emojis["red_haired_woman"],
167
+ emojis["red_haired_person"],
168
+ emojis["curly_haired_woman"],
169
+ emojis["curly_haired_person"],
170
+ emojis["white_haired_woman"],
171
+ emojis["white_haired_person"],
172
+ emojis["bald_woman"],
173
+ emojis["bald_person"],
174
+ emojis["blond-haired-woman"],
175
+ emojis["blond-haired-man"],
176
+ emojis["older_adult"],
177
+ emojis["older_man"],
178
+ emojis["older_woman"],
179
+ emojis["person_frowning"],
180
+ emojis["man-frowning"],
181
+ emojis["woman-frowning"],
182
+ emojis["person_with_pouting_face"],
183
+ emojis["man-pouting"],
184
+ emojis["woman-pouting"],
185
+ emojis["health_worker"],
186
+ emojis["male-doctor"],
187
+ emojis["female-doctor"],
188
+ emojis["student"],
189
+ emojis["male-student"],
190
+ emojis["female-student"],
191
+ emojis["teacher"],
192
+ emojis["male-teacher"],
193
+ emojis["female-teacher"],
194
+ emojis["judge"],
195
+ emojis["male-judge"],
196
+ emojis["female-judge"],
197
+ emojis["farmer"],
198
+ emojis["male-farmer"],
199
+ emojis["female-farmer"],
200
+ emojis["cook"],
201
+ emojis["male-cook"],
202
+ emojis["female-cook"],
203
+ emojis["mechanic"],
204
+ emojis["male-mechanic"],
205
+ emojis["female-mechanic"],
206
+ emojis["office_worker"],
207
+ emojis["male-office-worker"],
208
+ emojis["female-office-worker"],
209
+ emojis["scientist"],
210
+ emojis["male-scientist"],
211
+ emojis["female-scientist"],
212
+ emojis["technologist"],
213
+ emojis["male-technologist"],
214
+ emojis["female-technologist"],
215
+ emojis["artist"],
216
+ emojis["male-artist"],
217
+ emojis["female-artist"],
218
+ emojis["astronaut"],
219
+ emojis["male-astronaut"],
220
+ emojis["female-astronaut"],
221
+ emojis["sleuth_or_spy"],
222
+ emojis["male-detective"],
223
+ emojis["female-detective"],
224
+ emojis["construction_worker"],
225
+ emojis["male-construction-worker"],
226
+ emojis["female-construction-worker"],
227
+ emojis["person_with_crown"],
228
+ emojis["prince"],
229
+ emojis["princess"],
230
+ emojis["person_in_tuxedo"],
231
+ emojis["man_in_tuxedo"],
232
+ emojis["woman_in_tuxedo"],
233
+ emojis["bride_with_veil"],
234
+ emojis["man_with_veil"],
235
+ emojis["woman_with_veil"],
236
+ ],
237
+ },
238
+ ];
239
+ export const EmojiSelector: FC<{ onSelect: (e: EmojiData) => void }> = ({
240
+ onSelect,
241
+ }) => {
242
+ const isSmallScreen = useMediaQuery({ query: "(max-width: 768px)" });
243
+
244
+ return (
245
+ <Picker
246
+ exceptEmojis={exceptEmojis}
247
+ dynamicWidth={true}
248
+ custom={custom}
249
+ categories={[
250
+ "recommend",
251
+ "new-people",
252
+ "nature",
253
+ "foods",
254
+ "activity",
255
+ "places",
256
+ "objects",
257
+ ]}
258
+ theme="dark"
259
+ categoryIcons={categoryIcons}
260
+ searchPosition={isSmallScreen ? "none" : "bottom"}
261
+ navPosition={isSmallScreen ? "none" : "top"}
262
+ previewPosition={isSmallScreen ? "none" : "bottom"}
263
+ data={data}
264
+ onEmojiSelect={onSelect}
265
+ />
266
+ );
267
+ };
src/components/github.tsx ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+
3
+ export function GithubForkRibbon() {
4
+ return (
5
+ <div className="hidden md:block overflow-hidden w-[150px] h-[150px] absolute top-0 z-50 right-0">
6
+ <div className="bg-amber-600 text-zinc-100 shadow-2xl absolute p-1 z-50 top-[35px] right-[-45px] rotate-45">
7
+ <a
8
+ className="w-[200px] inline-block p-1 text-center"
9
+ href="https://github.com/leptonai/tryemoji"
10
+ target="_blank"
11
+ >
12
+ Fork me on GitHub
13
+ </a>
14
+ </div>
15
+ </div>
16
+ );
17
+ }
src/components/try-emoji.tsx ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { Dice } from "@/components/dice";
3
+ import { EmojiSelector } from "@/components/emoji-selector";
4
+ import { GithubForkRibbon } from "@/components/github";
5
+ import {
6
+ Select,
7
+ SelectContent,
8
+ SelectItem,
9
+ SelectTrigger,
10
+ SelectValue,
11
+ } from "@/components/ui/select";
12
+ import { Slider } from "@/components/ui/slider";
13
+ import { Toaster } from "@/components/ui/toaster";
14
+ import {
15
+ Tooltip,
16
+ TooltipContent,
17
+ TooltipProvider,
18
+ TooltipTrigger,
19
+ } from "@/components/ui/tooltip";
20
+ import { useToast } from "@/components/ui/use-toast";
21
+ import { presetImage, presetArtStyles } from "@/util/presets";
22
+ import { usePrevious } from "@/util/use-previous";
23
+ import { useResponse } from "@/util/use-response";
24
+ import { getShareUrl, Option, useShare } from "@/util/use-share";
25
+ import { clsx } from "clsx";
26
+ import { Check, Download, Share2 } from "lucide-react";
27
+ import { useEffect, useMemo, useState } from "react";
28
+ import {
29
+ FacebookIcon,
30
+ FacebookShareButton,
31
+ LinkedinIcon,
32
+ LinkedinShareButton,
33
+ TwitterShareButton,
34
+ XIcon,
35
+ } from "react-share";
36
+ import { setEmojiFavicon } from "@/util/set-emoji-favicon";
37
+
38
+ export default function TryEmoji() {
39
+ const { option: presetOption, hasShare } = useShare();
40
+ const { toast } = useToast();
41
+ const [emoji, setEmoji] = useState({
42
+ emoji: presetOption.emoji,
43
+ name: presetOption.name,
44
+ });
45
+ const [preset, setPreset] = useState(
46
+ presetArtStyles.find((p) => p.prompt === presetOption.prompt)!,
47
+ );
48
+ const [strength, setStrength] = useState(presetOption.strength);
49
+ const [seed, setSeed] = useState(presetOption.seed);
50
+
51
+ const shareOption: Option = useMemo(() => {
52
+ return {
53
+ emoji: emoji.emoji,
54
+ name: emoji.name,
55
+ prompt: preset.prompt,
56
+ seed: seed,
57
+ strength: strength,
58
+ };
59
+ }, [emoji.emoji, emoji.name, preset.prompt, seed, strength]);
60
+
61
+ const { image, loading } = useResponse(
62
+ hasShare,
63
+ emoji.emoji,
64
+ emoji.name,
65
+ preset.prompt,
66
+ strength,
67
+ seed,
68
+ );
69
+ const previousImage = usePrevious(image);
70
+
71
+ const mergedImage = useMemo(
72
+ () => image || previousImage || presetImage,
73
+ [image, previousImage],
74
+ );
75
+
76
+ useEffect(() => {
77
+ setEmojiFavicon(emoji.emoji);
78
+ }, [emoji.emoji]);
79
+
80
+ const shareKey = useMemo(() => {
81
+ return getShareUrl(shareOption);
82
+ }, [shareOption]);
83
+
84
+ const shareUrl = useMemo(() => {
85
+ return `https://tryemoji.com?share=${shareKey}`;
86
+ }, [shareKey]);
87
+
88
+ const warmOrg: Promise<void> = useMemo(() => {
89
+ if (image) {
90
+ return fetch("/api/share", {
91
+ method: "POST",
92
+ body: JSON.stringify({
93
+ image: image,
94
+ key: shareKey,
95
+ }),
96
+ }).then();
97
+ } else {
98
+ return new Promise((resolve) => resolve());
99
+ }
100
+ }, [image, shareKey]);
101
+
102
+ return (
103
+ <TooltipProvider delayDuration={50}>
104
+ <Toaster />
105
+ <div className="min-h-screen flex flex-col gap-4 bg-zinc-950 items-center justify-center py-4 md:py-12">
106
+ <GithubForkRibbon></GithubForkRibbon>
107
+ <div className="text-6xl text-zinc-100">
108
+ {emoji.emoji || "🐤"} tryEmoji{" "}
109
+ </div>
110
+ <div className="text-xl text-zinc-100">
111
+ Turn emoji into amazing artwork via AI
112
+ </div>
113
+ <div className="flex items-center justify-center flex-col md:flex-row gap-2 md:gap-4">
114
+ <div className="flex-0 w-full md:w-80">
115
+ <EmojiSelector
116
+ onSelect={(e) => {
117
+ const prefix =
118
+ e.keywords.indexOf("animal") > -1 ? "super cute" : "";
119
+ const keyword = e.keywords.join(", ");
120
+ const emoji = e.native;
121
+ const name = `${prefix} ${e.name}, ${keyword}`;
122
+ setEmoji({ emoji, name });
123
+ }}
124
+ ></EmojiSelector>
125
+ </div>
126
+ <div className="flex-1">
127
+ <div className="max-w-[100vw] h-auto md:h-[512px] w-[512px] rounded-lg overflow-hidden relative">
128
+ <img src={mergedImage} className="h-full w-full object-contain" />
129
+ <div
130
+ className={clsx("transition absolute inset-0", {
131
+ "backdrop-blur-xl": loading,
132
+ })}
133
+ ></div>
134
+ <div className="hidden absolute top-2 right-2 md:flex gap-2 items-center">
135
+ <FacebookShareButton
136
+ beforeOnClick={() => warmOrg}
137
+ url={shareUrl}
138
+ >
139
+ <FacebookIcon className="rounded" size={24}></FacebookIcon>
140
+ </FacebookShareButton>
141
+ <TwitterShareButton onClick={() => warmOrg} url={shareUrl}>
142
+ <XIcon className="rounded" size={24} />
143
+ </TwitterShareButton>
144
+ <LinkedinShareButton onClick={() => warmOrg} url={shareUrl}>
145
+ <LinkedinIcon className="rounded" size={24} />
146
+ </LinkedinShareButton>
147
+ <Tooltip>
148
+ <TooltipTrigger asChild>
149
+ <button
150
+ onClick={() => {
151
+ warmOrg.then(() => {
152
+ navigator.clipboard.writeText(shareUrl).then(() => {
153
+ toast({
154
+ description: (
155
+ <div className="flex gap-2 text-sm items-center">
156
+ <Check className="text-green-500"></Check>
157
+ Copied, paste to share
158
+ </div>
159
+ ),
160
+ });
161
+ });
162
+ });
163
+ }}
164
+ className="flex-0 rounded bg-amber-600 w-6 flex items-center justify-center h-6 text-sm font-semibold text-white shadow-sm hover:bg-amber-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600"
165
+ >
166
+ <Share2 size={16}></Share2>
167
+ </button>
168
+ </TooltipTrigger>
169
+ <TooltipContent>
170
+ <p>Share</p>
171
+ </TooltipContent>
172
+ </Tooltip>
173
+ </div>
174
+ <div className="absolute bottom-2 left-2 right-2 flex gap-2 flex-wrap">
175
+ <div className="flex flex-auto gap-2 w-full md:w-auto">
176
+ <div className="text-xl text-zinc-100">AI</div>
177
+ <Tooltip>
178
+ <TooltipTrigger asChild>
179
+ <Slider
180
+ className="flex-1"
181
+ defaultValue={[strength]}
182
+ onValueChange={(v) => setStrength(v[0])}
183
+ max={0.7}
184
+ min={0.5}
185
+ step={0.025}
186
+ />
187
+ </TooltipTrigger>
188
+ <TooltipContent>
189
+ <p>AI strength</p>
190
+ </TooltipContent>
191
+ </Tooltip>
192
+ </div>
193
+ <div className="flex flex-auto md:flex-grow-0 gap-2 w-full md:w-auto">
194
+ <Select
195
+ value={preset.artist}
196
+ onValueChange={(value) =>
197
+ setPreset(
198
+ presetArtStyles.find((p) => p.artist === value)!,
199
+ )
200
+ }
201
+ >
202
+ <Tooltip>
203
+ <TooltipTrigger asChild>
204
+ <SelectTrigger className="flex-1 w-56 border-0 rounded bg-amber-600 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-amber-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600">
205
+ <SelectValue placeholder="Select a fruit" />
206
+ </SelectTrigger>
207
+ </TooltipTrigger>
208
+ <TooltipContent>
209
+ <p>Art style</p>
210
+ </TooltipContent>
211
+ </Tooltip>
212
+
213
+ <SelectContent>
214
+ {presetArtStyles.map((p) => (
215
+ <SelectItem key={p.artist} value={p.artist}>
216
+ {p.artist}
217
+ </SelectItem>
218
+ ))}
219
+ </SelectContent>
220
+ </Select>
221
+ <Tooltip>
222
+ <TooltipTrigger asChild>
223
+ <button
224
+ onClick={() => {
225
+ setSeed(Math.floor(Math.random() * 2159232));
226
+ }}
227
+ className="flex-0 rounded bg-amber-600 px-0.5 py-0.5 text-sm font-semibold text-white shadow-sm hover:bg-amber-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600"
228
+ >
229
+ <Dice></Dice>
230
+ </button>
231
+ </TooltipTrigger>
232
+ <TooltipContent>
233
+ <p>Random</p>
234
+ </TooltipContent>
235
+ </Tooltip>
236
+ <Tooltip>
237
+ <TooltipTrigger asChild>
238
+ <a
239
+ href={image}
240
+ download
241
+ className="flex-0 block rounded bg-amber-600 px-0.5 py-0.5 text-sm font-semibold text-white shadow-sm hover:bg-amber-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600"
242
+ >
243
+ <Download />
244
+ </a>
245
+ </TooltipTrigger>
246
+ <TooltipContent>
247
+ <p>Download</p>
248
+ </TooltipContent>
249
+ </Tooltip>
250
+ </div>
251
+ </div>
252
+ </div>
253
+ </div>
254
+ </div>
255
+ <div className="text-xs text-zinc-500 font-sans mt-8 flex gap-2">
256
+ <a
257
+ className="hover:text-zinc-100"
258
+ href="https://lepton.ai"
259
+ target="_blank"
260
+ >
261
+ Powered by Lepton AI
262
+ </a>
263
+ |
264
+ <a
265
+ className="hover:text-zinc-100"
266
+ href="https://github.com/leptonai/tryemoji"
267
+ target="_blank"
268
+ >
269
+ Github
270
+ </a>
271
+ </div>
272
+ </div>
273
+ </TooltipProvider>
274
+ );
275
+ }
src/components/ui/select.tsx ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as SelectPrimitive from "@radix-ui/react-select";
5
+ import { Check, ChevronDown, ChevronUp } from "lucide-react";
6
+
7
+ import { cn } from "@/components/ui/utils";
8
+
9
+ const Select = SelectPrimitive.Root;
10
+
11
+ const SelectGroup = SelectPrimitive.Group;
12
+
13
+ const SelectValue = SelectPrimitive.Value;
14
+
15
+ const SelectTrigger = React.forwardRef<
16
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
17
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
18
+ >(({ className, children, ...props }, ref) => (
19
+ <SelectPrimitive.Trigger
20
+ ref={ref}
21
+ className={cn(
22
+ "flex w-full items-center justify-between rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
23
+ className,
24
+ )}
25
+ {...props}
26
+ >
27
+ {children}
28
+ <SelectPrimitive.Icon asChild>
29
+ <ChevronDown className="h-4 w-4 opacity-50" />
30
+ </SelectPrimitive.Icon>
31
+ </SelectPrimitive.Trigger>
32
+ ));
33
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
34
+
35
+ const SelectScrollUpButton = React.forwardRef<
36
+ React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
37
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
38
+ >(({ className, ...props }, ref) => (
39
+ <SelectPrimitive.ScrollUpButton
40
+ ref={ref}
41
+ className={cn(
42
+ "flex cursor-default items-center justify-center py-1",
43
+ className,
44
+ )}
45
+ {...props}
46
+ >
47
+ <ChevronUp className="h-4 w-4" />
48
+ </SelectPrimitive.ScrollUpButton>
49
+ ));
50
+ SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
51
+
52
+ const SelectScrollDownButton = React.forwardRef<
53
+ React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
54
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
55
+ >(({ className, ...props }, ref) => (
56
+ <SelectPrimitive.ScrollDownButton
57
+ ref={ref}
58
+ className={cn(
59
+ "flex cursor-default items-center justify-center py-1",
60
+ className,
61
+ )}
62
+ {...props}
63
+ >
64
+ <ChevronDown className="h-4 w-4" />
65
+ </SelectPrimitive.ScrollDownButton>
66
+ ));
67
+ SelectScrollDownButton.displayName =
68
+ SelectPrimitive.ScrollDownButton.displayName;
69
+
70
+ const SelectContent = React.forwardRef<
71
+ React.ElementRef<typeof SelectPrimitive.Content>,
72
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
73
+ >(({ className, children, position = "popper", ...props }, ref) => (
74
+ <SelectPrimitive.Portal>
75
+ <SelectPrimitive.Content
76
+ ref={ref}
77
+ className={cn(
78
+ "bg-zinc-900 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
79
+ position === "popper" &&
80
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
81
+ className,
82
+ )}
83
+ position={position}
84
+ {...props}
85
+ >
86
+ <SelectScrollUpButton />
87
+ <SelectPrimitive.Viewport
88
+ className={cn(
89
+ "p-0 bg-zinc-900",
90
+ position === "popper" &&
91
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
92
+ )}
93
+ >
94
+ {children}
95
+ </SelectPrimitive.Viewport>
96
+ <SelectScrollDownButton />
97
+ </SelectPrimitive.Content>
98
+ </SelectPrimitive.Portal>
99
+ ));
100
+ SelectContent.displayName = SelectPrimitive.Content.displayName;
101
+
102
+ const SelectLabel = React.forwardRef<
103
+ React.ElementRef<typeof SelectPrimitive.Label>,
104
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
105
+ >(({ className, ...props }, ref) => (
106
+ <SelectPrimitive.Label
107
+ ref={ref}
108
+ className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
109
+ {...props}
110
+ />
111
+ ));
112
+ SelectLabel.displayName = SelectPrimitive.Label.displayName;
113
+
114
+ const SelectItem = React.forwardRef<
115
+ React.ElementRef<typeof SelectPrimitive.Item>,
116
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
117
+ >(({ className, children, ...props }, ref) => (
118
+ <SelectPrimitive.Item
119
+ ref={ref}
120
+ className={cn(
121
+ "relative bg-zinc-900 text-zinc-100 flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
122
+ className,
123
+ )}
124
+ {...props}
125
+ >
126
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
127
+ <SelectPrimitive.ItemIndicator>
128
+ <Check className="h-4 w-4" />
129
+ </SelectPrimitive.ItemIndicator>
130
+ </span>
131
+
132
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
133
+ </SelectPrimitive.Item>
134
+ ));
135
+ SelectItem.displayName = SelectPrimitive.Item.displayName;
136
+
137
+ const SelectSeparator = React.forwardRef<
138
+ React.ElementRef<typeof SelectPrimitive.Separator>,
139
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
140
+ >(({ className, ...props }, ref) => (
141
+ <SelectPrimitive.Separator
142
+ ref={ref}
143
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
144
+ {...props}
145
+ />
146
+ ));
147
+ SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
148
+
149
+ export {
150
+ Select,
151
+ SelectGroup,
152
+ SelectValue,
153
+ SelectTrigger,
154
+ SelectContent,
155
+ SelectLabel,
156
+ SelectItem,
157
+ SelectSeparator,
158
+ SelectScrollUpButton,
159
+ SelectScrollDownButton,
160
+ };
src/components/ui/slider.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as SliderPrimitive from "@radix-ui/react-slider";
5
+
6
+ import { cn } from "@/components/ui/utils";
7
+
8
+ const Slider = React.forwardRef<
9
+ React.ElementRef<typeof SliderPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
11
+ >(({ className, ...props }, ref) => (
12
+ <SliderPrimitive.Root
13
+ ref={ref}
14
+ className={cn(
15
+ "relative flex w-full touch-none select-none items-center",
16
+ className,
17
+ )}
18
+ {...props}
19
+ >
20
+ <SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-amber-500">
21
+ <SliderPrimitive.Range className="absolute h-full bg-amber-600" />
22
+ </SliderPrimitive.Track>
23
+ <SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-amber-500 bg-amber-600 ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
24
+ </SliderPrimitive.Root>
25
+ ));
26
+ Slider.displayName = SliderPrimitive.Root.displayName;
27
+
28
+ export { Slider };
src/components/ui/toast.tsx ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as ToastPrimitives from "@radix-ui/react-toast";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { X } from "lucide-react";
5
+
6
+ import { cn } from "@/components/ui/utils";
7
+
8
+ const ToastProvider = ToastPrimitives.Provider;
9
+
10
+ const ToastViewport = React.forwardRef<
11
+ React.ElementRef<typeof ToastPrimitives.Viewport>,
12
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
13
+ >(({ className, ...props }, ref) => (
14
+ <ToastPrimitives.Viewport
15
+ ref={ref}
16
+ className={cn(
17
+ "fixed bottom-0 right-0 z-[100] flex max-h-screen w-fit flex-col-reverse p-4 md:max-w-[420px]",
18
+ className,
19
+ )}
20
+ {...props}
21
+ />
22
+ ));
23
+ ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
24
+
25
+ const toastVariants = cva(
26
+ "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md p-3 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
27
+ {
28
+ variants: {
29
+ variant: {
30
+ default: "bg-amber-600 text-zinc-100",
31
+ destructive:
32
+ "destructive group border-destructive bg-destructive text-destructive-zinc-100",
33
+ },
34
+ },
35
+ defaultVariants: {
36
+ variant: "default",
37
+ },
38
+ },
39
+ );
40
+
41
+ const Toast = React.forwardRef<
42
+ React.ElementRef<typeof ToastPrimitives.Root>,
43
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
44
+ VariantProps<typeof toastVariants>
45
+ >(({ className, variant, ...props }, ref) => {
46
+ return (
47
+ <ToastPrimitives.Root
48
+ ref={ref}
49
+ className={cn(toastVariants({ variant }), className)}
50
+ {...props}
51
+ />
52
+ );
53
+ });
54
+ Toast.displayName = ToastPrimitives.Root.displayName;
55
+
56
+ const ToastAction = React.forwardRef<
57
+ React.ElementRef<typeof ToastPrimitives.Action>,
58
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
59
+ >(({ className, ...props }, ref) => (
60
+ <ToastPrimitives.Action
61
+ ref={ref}
62
+ className={cn(
63
+ "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-amber-600 transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-zinc-100 group-[.destructive]:focus:ring-destructive",
64
+ className,
65
+ )}
66
+ {...props}
67
+ />
68
+ ));
69
+ ToastAction.displayName = ToastPrimitives.Action.displayName;
70
+
71
+ const ToastClose = React.forwardRef<
72
+ React.ElementRef<typeof ToastPrimitives.Close>,
73
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
74
+ >(({ className, ...props }, ref) => (
75
+ <ToastPrimitives.Close
76
+ ref={ref}
77
+ className={cn(
78
+ "absolute right-2 top-2 rounded-md p-1 text-zinc-100/50 opacity-0 transition-opacity hover:text-zinc-100 focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
79
+ className,
80
+ )}
81
+ toast-close=""
82
+ {...props}
83
+ >
84
+ <X className="h-4 w-4" />
85
+ </ToastPrimitives.Close>
86
+ ));
87
+ ToastClose.displayName = ToastPrimitives.Close.displayName;
88
+
89
+ const ToastTitle = React.forwardRef<
90
+ React.ElementRef<typeof ToastPrimitives.Title>,
91
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
92
+ >(({ className, ...props }, ref) => (
93
+ <ToastPrimitives.Title
94
+ ref={ref}
95
+ className={cn("text-sm font-semibold", className)}
96
+ {...props}
97
+ />
98
+ ));
99
+ ToastTitle.displayName = ToastPrimitives.Title.displayName;
100
+
101
+ const ToastDescription = React.forwardRef<
102
+ React.ElementRef<typeof ToastPrimitives.Description>,
103
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
104
+ >(({ className, ...props }, ref) => (
105
+ <ToastPrimitives.Description
106
+ ref={ref}
107
+ className={cn("text-sm opacity-90", className)}
108
+ {...props}
109
+ />
110
+ ));
111
+ ToastDescription.displayName = ToastPrimitives.Description.displayName;
112
+
113
+ type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
114
+
115
+ type ToastActionElement = React.ReactElement<typeof ToastAction>;
116
+
117
+ export {
118
+ type ToastProps,
119
+ type ToastActionElement,
120
+ ToastProvider,
121
+ ToastViewport,
122
+ Toast,
123
+ ToastTitle,
124
+ ToastDescription,
125
+ ToastClose,
126
+ ToastAction,
127
+ };
src/components/ui/toaster.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import {
4
+ Toast,
5
+ ToastClose,
6
+ ToastDescription,
7
+ ToastProvider,
8
+ ToastTitle,
9
+ ToastViewport,
10
+ } from "@/components/ui/toast";
11
+ import { useToast } from "@/components/ui/use-toast";
12
+
13
+ export function Toaster() {
14
+ const { toasts } = useToast();
15
+
16
+ return (
17
+ <ToastProvider>
18
+ {toasts.map(function ({ id, title, description, action, ...props }) {
19
+ return (
20
+ <Toast key={id} {...props}>
21
+ <div className="grid gap-1">
22
+ {title && <ToastTitle>{title}</ToastTitle>}
23
+ {description && (
24
+ <ToastDescription>{description}</ToastDescription>
25
+ )}
26
+ </div>
27
+ {action}
28
+ <ToastClose />
29
+ </Toast>
30
+ );
31
+ })}
32
+ <ToastViewport />
33
+ </ToastProvider>
34
+ );
35
+ }
src/components/ui/tooltip.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
5
+
6
+ import { cn } from "@/components/ui/utils";
7
+
8
+ const TooltipProvider = TooltipPrimitive.Provider;
9
+
10
+ const Tooltip = TooltipPrimitive.Root;
11
+
12
+ const TooltipTrigger = TooltipPrimitive.Trigger;
13
+
14
+ const TooltipContent = React.forwardRef<
15
+ React.ElementRef<typeof TooltipPrimitive.Content>,
16
+ React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
17
+ >(({ className, sideOffset = 4, ...props }, ref) => (
18
+ <TooltipPrimitive.Content
19
+ ref={ref}
20
+ sideOffset={sideOffset}
21
+ className={cn(
22
+ "z-50 overflow-hidden rounded-md px-3 py-1.5 text-sm text-zinc-100 bg-amber-800 shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
23
+ className,
24
+ )}
25
+ {...props}
26
+ />
27
+ ));
28
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName;
29
+
30
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
src/components/ui/use-toast.tsx ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Inspired by react-hot-toast library
2
+ import * as React from "react";
3
+
4
+ import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
5
+
6
+ const TOAST_LIMIT = 1;
7
+ const TOAST_REMOVE_DELAY = 1000000;
8
+
9
+ type ToasterToast = ToastProps & {
10
+ id: string;
11
+ title?: React.ReactNode;
12
+ description?: React.ReactNode;
13
+ action?: ToastActionElement;
14
+ };
15
+
16
+ const actionTypes = {
17
+ ADD_TOAST: "ADD_TOAST",
18
+ UPDATE_TOAST: "UPDATE_TOAST",
19
+ DISMISS_TOAST: "DISMISS_TOAST",
20
+ REMOVE_TOAST: "REMOVE_TOAST",
21
+ } as const;
22
+
23
+ let count = 0;
24
+
25
+ function genId() {
26
+ count = (count + 1) % Number.MAX_SAFE_INTEGER;
27
+ return count.toString();
28
+ }
29
+
30
+ type ActionType = typeof actionTypes;
31
+
32
+ type Action =
33
+ | {
34
+ type: ActionType["ADD_TOAST"];
35
+ toast: ToasterToast;
36
+ }
37
+ | {
38
+ type: ActionType["UPDATE_TOAST"];
39
+ toast: Partial<ToasterToast>;
40
+ }
41
+ | {
42
+ type: ActionType["DISMISS_TOAST"];
43
+ toastId?: ToasterToast["id"];
44
+ }
45
+ | {
46
+ type: ActionType["REMOVE_TOAST"];
47
+ toastId?: ToasterToast["id"];
48
+ };
49
+
50
+ interface State {
51
+ toasts: ToasterToast[];
52
+ }
53
+
54
+ const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
55
+
56
+ const addToRemoveQueue = (toastId: string) => {
57
+ if (toastTimeouts.has(toastId)) {
58
+ return;
59
+ }
60
+
61
+ const timeout = setTimeout(() => {
62
+ toastTimeouts.delete(toastId);
63
+ dispatch({
64
+ type: "REMOVE_TOAST",
65
+ toastId: toastId,
66
+ });
67
+ }, TOAST_REMOVE_DELAY);
68
+
69
+ toastTimeouts.set(toastId, timeout);
70
+ };
71
+
72
+ export const reducer = (state: State, action: Action): State => {
73
+ switch (action.type) {
74
+ case "ADD_TOAST":
75
+ return {
76
+ ...state,
77
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
78
+ };
79
+
80
+ case "UPDATE_TOAST":
81
+ return {
82
+ ...state,
83
+ toasts: state.toasts.map((t) =>
84
+ t.id === action.toast.id ? { ...t, ...action.toast } : t,
85
+ ),
86
+ };
87
+
88
+ case "DISMISS_TOAST": {
89
+ const { toastId } = action;
90
+
91
+ // ! Side effects ! - This could be extracted into a dismissToast() action,
92
+ // but I'll keep it here for simplicity
93
+ if (toastId) {
94
+ addToRemoveQueue(toastId);
95
+ } else {
96
+ state.toasts.forEach((toast) => {
97
+ addToRemoveQueue(toast.id);
98
+ });
99
+ }
100
+
101
+ return {
102
+ ...state,
103
+ toasts: state.toasts.map((t) =>
104
+ t.id === toastId || toastId === undefined
105
+ ? {
106
+ ...t,
107
+ open: false,
108
+ }
109
+ : t,
110
+ ),
111
+ };
112
+ }
113
+ case "REMOVE_TOAST":
114
+ if (action.toastId === undefined) {
115
+ return {
116
+ ...state,
117
+ toasts: [],
118
+ };
119
+ }
120
+ return {
121
+ ...state,
122
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
123
+ };
124
+ }
125
+ };
126
+
127
+ const listeners: Array<(state: State) => void> = [];
128
+
129
+ let memoryState: State = { toasts: [] };
130
+
131
+ function dispatch(action: Action) {
132
+ memoryState = reducer(memoryState, action);
133
+ listeners.forEach((listener) => {
134
+ listener(memoryState);
135
+ });
136
+ }
137
+
138
+ type Toast = Omit<ToasterToast, "id">;
139
+
140
+ function toast({ ...props }: Toast) {
141
+ const id = genId();
142
+
143
+ const update = (props: ToasterToast) =>
144
+ dispatch({
145
+ type: "UPDATE_TOAST",
146
+ toast: { ...props, id },
147
+ });
148
+ const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
149
+
150
+ dispatch({
151
+ type: "ADD_TOAST",
152
+ toast: {
153
+ ...props,
154
+ id,
155
+ open: true,
156
+ onOpenChange: (open) => {
157
+ if (!open) dismiss();
158
+ },
159
+ },
160
+ });
161
+
162
+ return {
163
+ id: id,
164
+ dismiss,
165
+ update,
166
+ };
167
+ }
168
+
169
+ function useToast() {
170
+ const [state, setState] = React.useState<State>(memoryState);
171
+
172
+ React.useEffect(() => {
173
+ listeners.push(setState);
174
+ return () => {
175
+ const index = listeners.indexOf(setState);
176
+ if (index > -1) {
177
+ listeners.splice(index, 1);
178
+ }
179
+ };
180
+ }, [state]);
181
+
182
+ return {
183
+ ...state,
184
+ toast,
185
+ dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
186
+ };
187
+ }
188
+
189
+ export { useToast, toast };
src/components/ui/utils.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
src/util/presets.ts ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const presetArtStyles = [
2
+ {
3
+ artist: "Pixar",
4
+ prompt:
5
+ "in the style of Pixar, cartoon-style characters, 4k, 8k, unreal engine, octane render photorealistic by cosmicwonder, hdr, photography by cosmicwonder, high definition, symmetrical face, volumetric lighting, dusty haze, photo, octane render, 24mm, 4k, 24mm, DSLR, high quality, 60 fps, ultra realistic",
6
+ },
7
+ {
8
+ artist: "Minecraft",
9
+ prompt:
10
+ "in the style of Minecraft Character, minecraft, ultra hd, realistic, vivid colors, highly detailed, UHD drawing, pen and ink, perfect composition",
11
+ },
12
+ {
13
+ artist: "8 Bit pixel",
14
+ prompt:
15
+ "pixel style, pixel style, 8 bit pixel art, golden ratio, fake detail, trending pixiv fanbox, acrylic palette knife, style of makoto shinkai studio ghibli genshin impact james gilleard greg rutkowski chiho aoshima",
16
+ },
17
+ {
18
+ artist: "Vincent van Gogh",
19
+ prompt:
20
+ "in the style of Vincent van Gogh, with bold, expressive brush strokes and vibrant colors",
21
+ },
22
+ {
23
+ artist: "Claude Monet",
24
+ prompt:
25
+ "in the style of Claude Monet, using impressionist techniques with a focus on light and color",
26
+ },
27
+ {
28
+ artist: "Salvador Dalí",
29
+ prompt:
30
+ "in the style of Salvador Dalí, with dream-like, bizarre elements and melting clocks",
31
+ },
32
+ {
33
+ artist: "Pablo Picasso",
34
+ prompt:
35
+ "in the style of Pablo Picasso's Cubism, with geometric shapes and multiple perspectives merged into one image.",
36
+ },
37
+ {
38
+ artist: "Edvard Munch",
39
+ prompt:
40
+ "in the style of Edvard Munch, using intense colors and bold lines to convey strong emotions, such as in 'The Scream'.",
41
+ },
42
+ ];
43
+
44
+ export const presetImage =
45
+ "data:image/jpeg;base64,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";
src/util/set-emoji-favicon.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ export const setEmojiFavicon = (emoji: string) => {
4
+ if (typeof document === "undefined") return;
5
+ const href = `data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>${emoji}</text></svg>`;
6
+ const link =
7
+ document.querySelector("link[rel*='icon']") ||
8
+ document.createElement("link");
9
+ link.setAttribute("rel", "icon");
10
+ link.setAttribute("href", href);
11
+ document.head.appendChild(link);
12
+ };
src/util/use-previous.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useRef } from "react";
2
+
3
+ export function usePrevious<T>(value: T) {
4
+ const ref = useRef<T>();
5
+ useEffect(() => {
6
+ if (value) {
7
+ ref.current = value;
8
+ }
9
+ }, [value]);
10
+ return ref.current;
11
+ }
src/util/use-response.ts ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import useSWR from "swr";
2
+
3
+ const dimension = 512;
4
+ function blobToBase64(blob: Blob): Promise<string> {
5
+ return new Promise((resolve, _) => {
6
+ const reader = new FileReader();
7
+ reader.onloadend = () => resolve(reader.result as string);
8
+ reader.readAsDataURL(blob);
9
+ });
10
+ }
11
+
12
+ function convertEmojiToDataToDataURL(emoji: string): string {
13
+ const element = document.createElement("canvas");
14
+ const ctx = element.getContext("2d")!;
15
+ element.height = dimension;
16
+ element.width = dimension;
17
+ ctx.fillStyle = "rgb(24 24 27)";
18
+ ctx.fillRect(0, 0, element.width, element.height);
19
+ ctx.textAlign = `center`;
20
+ ctx.font = `${dimension - 32}px serf`;
21
+ const textMetrics = ctx.measureText(emoji);
22
+
23
+ const textHeight =
24
+ textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent;
25
+ const y =
26
+ dimension / 2 + (textMetrics.actualBoundingBoxAscent - textHeight / 2);
27
+
28
+ ctx.fillText(emoji, dimension / 2, y);
29
+ return element.toDataURL("image/jpeg", 0.5);
30
+ }
31
+ export const useResponse = (
32
+ revalidateOnMount: boolean,
33
+ emoji: string,
34
+ name: string,
35
+ style: string,
36
+ strength: number,
37
+ seed: number,
38
+ ) => {
39
+ const { data, isLoading } = useSWR(
40
+ [emoji, name, style, strength, seed],
41
+ async ([base64, name, style, strength, seed]) => {
42
+ const response = await fetch("/api/run", {
43
+ headers: {
44
+ accept: "image/jpeg",
45
+ "content-type": "application/json",
46
+ },
47
+ body: JSON.stringify({
48
+ input_image: convertEmojiToDataToDataURL(emoji).replace(
49
+ /^data:image\/(png|jpeg);base64,/,
50
+ "",
51
+ ),
52
+ prompt: `${name}, emoji ${emoji}, ${style}`,
53
+ guidance_scale: 8,
54
+ lcm_steps: 50,
55
+ seed,
56
+ steps: 4,
57
+ strength,
58
+ width: dimension,
59
+ height: dimension,
60
+ }),
61
+ method: "POST",
62
+ });
63
+ if (response.status !== 200) return "";
64
+ const blob = await response.blob();
65
+ return await blobToBase64(blob);
66
+ },
67
+ {
68
+ revalidateOnFocus: false,
69
+ revalidateOnReconnect: false,
70
+ revalidateOnMount,
71
+ refreshWhenOffline: false,
72
+ refreshInterval: 0,
73
+ },
74
+ );
75
+ return { image: data as string, loading: isLoading };
76
+ };
src/util/use-share.ts ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { presetArtStyles } from "@/util/presets";
2
+ import {
3
+ decompressFromEncodedURIComponent,
4
+ compressToEncodedURIComponent,
5
+ } from "lz-string";
6
+ import { useSearchParams } from "next/navigation";
7
+
8
+ export interface Option {
9
+ emoji: string;
10
+ name: string;
11
+ prompt: string;
12
+ seed: number;
13
+ strength: number;
14
+ }
15
+
16
+ const fallbackOptions: Option = {
17
+ emoji: "🐤",
18
+ name: "cat",
19
+ prompt: presetArtStyles[0].prompt,
20
+ seed: 2159232,
21
+ strength: 0.7,
22
+ };
23
+
24
+ export const shareString2Json = (shareString: string): Option => {
25
+ try {
26
+ return JSON.parse(decompressFromEncodedURIComponent(shareString));
27
+ } catch (_) {
28
+ return fallbackOptions;
29
+ }
30
+ };
31
+
32
+ export const useShare = (): { option: Option; hasShare: boolean } => {
33
+ const searchParams = useSearchParams();
34
+ const shareParam = searchParams.get("share");
35
+ if (shareParam) {
36
+ try {
37
+ return {
38
+ option: JSON.parse(decompressFromEncodedURIComponent(shareParam)),
39
+ hasShare: true,
40
+ };
41
+ } catch (_) {
42
+ return { option: fallbackOptions, hasShare: false };
43
+ }
44
+ }
45
+ return { option: fallbackOptions, hasShare: false };
46
+ };
47
+
48
+ export const getShareUrl = (option: Option) => {
49
+ return compressToEncodedURIComponent(JSON.stringify(option));
50
+ };
style.css CHANGED
@@ -1,28 +1,16 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
 
 
4
  }
5
 
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
 
9
  }
10
 
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
- }
17
-
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
- }
25
-
26
- .card p:last-child {
27
- margin-bottom: 0;
28
  }
 
1
+ html, body {
2
+ padding: 0;
3
+ margin: 0;
4
+ height: 100%;
5
+ width: 100%;
6
  }
7
 
8
+ iframe {
9
+ border: none;
10
+ height: 100%;
11
+ width: 100%;
12
  }
13
 
14
+ * {
15
+ outline: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  }
tailwind.config.ts ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Config } from "tailwindcss";
2
+
3
+ const config: Config = {
4
+ content: [
5
+ "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
6
+ "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
7
+ "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
8
+ ],
9
+ theme: {
10
+ extend: {
11
+ backgroundImage: {
12
+ "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
13
+ "gradient-conic":
14
+ "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
15
+ },
16
+ keyframes: {
17
+ shake: {
18
+ "10%, 90%": {
19
+ transform: "translate3d(0, 1px, 0)",
20
+ },
21
+ "20%, 80%": {
22
+ transform: "translate3d(0, -2px, 0)",
23
+ },
24
+ "30%, 50%, 70%": {
25
+ transform: "translate3d(0, 4px, 0)",
26
+ },
27
+ "40%, 60%": {
28
+ transform: "translate3d(0, -4px, 0)",
29
+ },
30
+ },
31
+ },
32
+ },
33
+ },
34
+ plugins: [],
35
+ };
36
+ export default config;
tsconfig.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es5",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./src/*"]
23
+ }
24
+ },
25
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26
+ "exclude": ["node_modules"]
27
+ }