enzostvs HF Staff commited on
Commit
ea6c2a8
·
0 Parent(s):

Initial ✨

Browse files
.env.example ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ OAUTH_CLIENT_ID=
2
+ OAUTH_CLIENT_SECRET=
3
+ APP_PORT=5173
4
+ REDIRECT_URI=http://localhost:5173/auth/login
5
+ DEFAULT_HF_TOKEN=
.gitignore ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
25
+ .env
Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dockerfile
2
+ # Use an official Node.js runtime as the base image
3
+ FROM node:22.1.0
4
+ USER root
5
+
6
+ RUN apt-get update
7
+ USER 1000
8
+ WORKDIR /usr/src/app
9
+ # Copy package.json and package-lock.json to the container
10
+ COPY --chown=1000 package.json package-lock.json ./
11
+
12
+ # Copy the rest of the application files to the container
13
+ COPY --chown=1000 . .
14
+
15
+ RUN npm install
16
+ RUN npm run build
17
+
18
+ # Expose the application port (assuming your app runs on port 3000)
19
+ EXPOSE 3000
20
+
21
+ # Start the application
22
+ CMD ["npm", "start"]
README.md ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Space Generator
3
+ emoji: 🚀
4
+ colorFrom: green
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ short_description: Develop and deploy your static space in few-seconds
10
+ ---
11
+
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
eslint.config.js ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+
7
+ export default tseslint.config(
8
+ { ignores: ['dist'] },
9
+ {
10
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
11
+ files: ['**/*.{ts,tsx}'],
12
+ languageOptions: {
13
+ ecmaVersion: 2020,
14
+ globals: globals.browser,
15
+ },
16
+ plugins: {
17
+ 'react-hooks': reactHooks,
18
+ 'react-refresh': reactRefresh,
19
+ },
20
+ rules: {
21
+ ...reactHooks.configs.recommended.rules,
22
+ 'react-refresh/only-export-components': [
23
+ 'warn',
24
+ { allowConstantExport: true },
25
+ ],
26
+ },
27
+ },
28
+ )
index.html ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Space Generator</title>
8
+ <meta
9
+ name="description"
10
+ content="Code and Deploy your Hugging Face Static App in 1-Click 🚀"
11
+ />
12
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
13
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
14
+ <link
15
+ href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap"
16
+ rel="stylesheet"
17
+ />
18
+ <link
19
+ href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap"
20
+ rel="stylesheet"
21
+ />
22
+ </head>
23
+ <body>
24
+ <div id="root"></div>
25
+ <script type="module" src="/src/main.tsx"></script>
26
+ </body>
27
+ </html>
middlewares/checkUser.js ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ export default async function checkUser(req, res, next) {
2
+ const { hf_token } = req.cookies;
3
+ if (!hf_token) {
4
+ return res.status(401).send({
5
+ ok: false,
6
+ message: "Unauthorized",
7
+ });
8
+ }
9
+ next();
10
+ }
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "html-space-editor",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview",
11
+ "start": "node server.js"
12
+ },
13
+ "dependencies": {
14
+ "@huggingface/hub": "^1.1.1",
15
+ "@huggingface/inference": "^3.6.1",
16
+ "@monaco-editor/react": "^4.7.0",
17
+ "@tailwindcss/vite": "^4.0.15",
18
+ "@xenova/transformers": "^2.17.2",
19
+ "body-parser": "^1.20.3",
20
+ "classnames": "^2.5.1",
21
+ "cookie-parser": "^1.4.7",
22
+ "dotenv": "^16.4.7",
23
+ "express": "^4.21.2",
24
+ "react": "^19.0.0",
25
+ "react-dom": "^19.0.0",
26
+ "react-icons": "^5.5.0",
27
+ "react-markdown": "^10.1.0",
28
+ "react-toastify": "^11.0.5",
29
+ "react-use": "^17.6.0",
30
+ "tailwindcss": "^4.0.15"
31
+ },
32
+ "devDependencies": {
33
+ "@eslint/js": "^9.21.0",
34
+ "@types/express": "^5.0.1",
35
+ "@types/react": "^19.0.10",
36
+ "@types/react-dom": "^19.0.4",
37
+ "@vitejs/plugin-react": "^4.3.4",
38
+ "eslint": "^9.21.0",
39
+ "eslint-plugin-react-hooks": "^5.1.0",
40
+ "eslint-plugin-react-refresh": "^0.4.19",
41
+ "globals": "^15.15.0",
42
+ "typescript": "~5.7.2",
43
+ "typescript-eslint": "^8.24.1",
44
+ "vite": "^6.2.0"
45
+ }
46
+ }
public/vite.svg ADDED
server.js ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from "express";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import dotenv from "dotenv";
5
+ import cookieParser from "cookie-parser";
6
+ import { createRepo, uploadFile, whoAmI } from "@huggingface/hub";
7
+ import { InferenceClient } from "@huggingface/inference";
8
+ import bodyParser from "body-parser";
9
+
10
+ import checkUser from "./middlewares/checkUser.js";
11
+
12
+ // Load environment variables from .env file
13
+ dotenv.config();
14
+
15
+ const app = express();
16
+
17
+ const ipAddresses = new Map();
18
+
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = path.dirname(__filename);
21
+
22
+ const PORT = process.env.APP_PORT || 3000;
23
+ const REDIRECT_URI = `http://localhost:${PORT}/auth/login`;
24
+ const MODEL_ID = "deepseek-ai/DeepSeek-V3-0324";
25
+
26
+ app.use(cookieParser());
27
+ app.use(bodyParser.json());
28
+ app.use(express.static(path.join(__dirname, "dist")));
29
+
30
+ app.get("/api/login", (_req, res) => {
31
+ res.redirect(
32
+ 302,
33
+ `https://huggingface.co/oauth/authorize?client_id=${process.env.OAUTH_CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=openid%20profile%20read-repos%20write-repos%20manage-repos%20inference-api&prompt=consent&state=1234567890`
34
+ );
35
+ });
36
+ app.get("/auth/login", async (req, res) => {
37
+ const { code } = req.query;
38
+
39
+ if (!code) {
40
+ return res.redirect(302, "/");
41
+ }
42
+ const Authorization = `Basic ${Buffer.from(
43
+ `${process.env.OAUTH_CLIENT_ID}:${process.env.OAUTH_CLIENT_SECRET}`
44
+ ).toString("base64")}`;
45
+
46
+ const request_auth = await fetch("https://huggingface.co/oauth/token", {
47
+ method: "POST",
48
+ headers: {
49
+ "Content-Type": "application/x-www-form-urlencoded",
50
+ Authorization,
51
+ },
52
+ body: new URLSearchParams({
53
+ grant_type: "authorization_code",
54
+ code: code,
55
+ redirect_uri: REDIRECT_URI,
56
+ }),
57
+ });
58
+
59
+ const response = await request_auth.json();
60
+
61
+ if (!response.access_token) {
62
+ return res.redirect(302, "/");
63
+ }
64
+
65
+ res.cookie("hf_token", response.access_token, {
66
+ httpOnly: false,
67
+ secure: true,
68
+ sameSite: true,
69
+ maxAge: 30 * 24 * 60 * 60 * 1000,
70
+ });
71
+
72
+ return res.redirect(302, "/");
73
+ });
74
+ app.get("/api/@me", checkUser, async (req, res) => {
75
+ const { hf_token } = req.cookies;
76
+ try {
77
+ const request_user = await fetch("https://huggingface.co/oauth/userinfo", {
78
+ headers: {
79
+ Authorization: `Bearer ${hf_token}`,
80
+ },
81
+ });
82
+
83
+ const user = await request_user.json();
84
+ res.send(user);
85
+ } catch (err) {
86
+ res.clearCookie("hf_token");
87
+ res.status(401).send({
88
+ ok: false,
89
+ message: err.message,
90
+ });
91
+ }
92
+ });
93
+
94
+ app.post("/api/deploy", checkUser, async (req, res) => {
95
+ const { html, title, path } = req.body;
96
+ if (!html || !title) {
97
+ return res.status(400).send({
98
+ ok: false,
99
+ message: "Missing required fields",
100
+ });
101
+ }
102
+
103
+ const file = new Blob([html], { type: "text/html" });
104
+ file.name = "index.html"; // Add name property to the Blob
105
+
106
+ const { hf_token } = req.cookies;
107
+ try {
108
+ const repo = {
109
+ type: "space",
110
+ name: path ?? "",
111
+ };
112
+ if (!path || path === "") {
113
+ const { name: username } = await whoAmI({ accessToken: hf_token });
114
+ const newTitle = title
115
+ .toLowerCase()
116
+ .replace(/[^a-z0-9]+/g, "-")
117
+ .split("-")
118
+ .filter(Boolean)
119
+ .join("-")
120
+ .slice(0, 96);
121
+
122
+ const repoId = `${username}/${newTitle}`;
123
+ repo.name = repoId;
124
+ await createRepo({
125
+ repo,
126
+ accessToken: hf_token,
127
+ });
128
+ }
129
+ await uploadFile({
130
+ repo,
131
+ file,
132
+ accessToken: hf_token,
133
+ });
134
+ return res.status(200).send({ ok: true, path: repo.name });
135
+ } catch (err) {
136
+ return res.status(500).send({
137
+ ok: false,
138
+ message: err.message,
139
+ });
140
+ }
141
+ });
142
+
143
+ app.post("/api/ask-ai", async (req, res) => {
144
+ const { prompt, html } = req.body;
145
+ if (!prompt) {
146
+ return res.status(400).send({
147
+ ok: false,
148
+ message: "Missing required fields",
149
+ });
150
+ }
151
+
152
+ const { hf_token } = req.cookies;
153
+ let token = hf_token;
154
+ const ip =
155
+ req.headers["x-forwarded-for"]?.split(",")[0].trim() ||
156
+ req.headers["x-real-ip"] ||
157
+ req.socket.remoteAddress ||
158
+ req.ip ||
159
+ "0.0.0.0";
160
+
161
+ if (!hf_token) {
162
+ // Rate limit requests from the same IP address, to prevent abuse, free is limited to 2 requests per IP
163
+ ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
164
+ if (ipAddresses.get(ip) > 2) {
165
+ return res.status(429).send({
166
+ ok: false,
167
+ openLogin: true,
168
+ message: "Log In to continue using the service",
169
+ });
170
+ }
171
+
172
+ token = process.env.DEFAULT_HF_TOKEN;
173
+ }
174
+
175
+ // Set up response headers for streaming
176
+ res.setHeader("Content-Type", "text/plain");
177
+ res.setHeader("Cache-Control", "no-cache");
178
+ res.setHeader("Connection", "keep-alive");
179
+
180
+ const client = new InferenceClient(token);
181
+ let completeResponse = "";
182
+
183
+ try {
184
+ const chatCompletion = client.chatCompletionStream({
185
+ model: MODEL_ID,
186
+ provider: "fireworks-ai",
187
+ messages: [
188
+ {
189
+ role: "system",
190
+ content:
191
+ "ONLY USE HTML, CSS AND JAVASCRIPT. If you want to use ICON make sure to import the library first. Try to create the best UI possible by using only HTML, CSS and JAVASCRIPT. Also, try to ellaborate as much as you can, to create something unique. ALWAYS GIVE THE RESPONSE INTO A SINGLE HTML FILE",
192
+ },
193
+ ...(html
194
+ ? [
195
+ {
196
+ role: "user",
197
+ content: `My current code is: ${html}.`,
198
+ },
199
+ ]
200
+ : []),
201
+ {
202
+ role: "user",
203
+ content: prompt,
204
+ },
205
+ ],
206
+ max_tokens: 12_000,
207
+ });
208
+
209
+ while (true) {
210
+ const { done, value } = await chatCompletion.next();
211
+ if (done) {
212
+ break;
213
+ }
214
+ const chunk = value.choices[0]?.delta?.content;
215
+ if (chunk) {
216
+ // Stream chunk to client
217
+ res.write(chunk);
218
+ completeResponse += chunk;
219
+
220
+ // Break when HTML is complete
221
+ if (completeResponse.includes("</html>")) {
222
+ break;
223
+ }
224
+ }
225
+ }
226
+
227
+ // End the response stream
228
+ res.end();
229
+ } catch (error) {
230
+ console.error("Error:", error);
231
+ // If we haven't sent a response yet, send an error
232
+ if (!res.headersSent) {
233
+ res.status(500).send({
234
+ ok: false,
235
+ message: "Error generating response",
236
+ });
237
+ } else {
238
+ // Otherwise end the stream
239
+ res.end();
240
+ }
241
+ }
242
+ });
243
+
244
+ app.get("*", (_req, res) => {
245
+ res.sendFile(path.join(__dirname, "dist", "index.html"));
246
+ });
247
+
248
+ app.listen(PORT, () => {
249
+ console.log(`Server is running on port ${PORT}`);
250
+ });
src/assets/deepseek-color.svg ADDED
src/assets/index.css ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ * {
4
+ font-family: "Noto Sans";
5
+ }
6
+
7
+ .font-code {
8
+ font-family: "Source Code Pro";
9
+ }
src/assets/space.svg ADDED
src/components/App.tsx ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { useRef, useState } from "react";
3
+ import Editor from "@monaco-editor/react";
4
+ import classNames from "classnames";
5
+ import { editor } from "monaco-editor";
6
+ import { useMount, useUnmount } from "react-use";
7
+ import { toast } from "react-toastify";
8
+
9
+ import Header from "./header/header";
10
+ import DeployButton from "./deploy-button/deploy-button";
11
+ import { defaultHTML } from "../utils/consts";
12
+ import Tabs from "./tabs/tabs";
13
+ import AskAI from "./ask-ai/ask-ai";
14
+ import { Auth } from "../utils/types";
15
+
16
+ function App() {
17
+ const preview = useRef<HTMLDivElement>(null);
18
+ const editor = useRef<HTMLDivElement>(null);
19
+ const resizer = useRef<HTMLDivElement>(null);
20
+ const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
21
+
22
+ const [isResizing, setIsResizing] = useState(false);
23
+ const [error, setError] = useState(false);
24
+ const [html, setHtml] = useState(defaultHTML);
25
+ const [isAiWorking, setisAiWorking] = useState(false);
26
+ const [auth, setAuth] = useState<Auth | undefined>(undefined);
27
+
28
+ const fetchMe = async () => {
29
+ const res = await fetch("/api/@me");
30
+ if (res.ok) {
31
+ const data = await res.json();
32
+ setAuth(data);
33
+ } else {
34
+ setAuth(undefined);
35
+ }
36
+ };
37
+
38
+ const handleResize = (e: MouseEvent) => {
39
+ if (!editor.current || !preview.current || !resizer.current) return;
40
+ const editorWidth = e.clientX;
41
+ const previewWidth = window.innerWidth - editorWidth - 4;
42
+ editor.current.style.width = `${editorWidth}px`;
43
+ preview.current.style.width = `${previewWidth}px`;
44
+ };
45
+
46
+ const handleMouseDown = () => {
47
+ setIsResizing(true);
48
+ document.addEventListener("mousemove", handleResize);
49
+ document.addEventListener("mouseup", handleMouseUp);
50
+ };
51
+
52
+ const handleMouseUp = () => {
53
+ setIsResizing(false);
54
+ document.removeEventListener("mousemove", handleResize);
55
+ document.removeEventListener("mouseup", handleMouseUp);
56
+ };
57
+
58
+ useMount(() => {
59
+ fetchMe();
60
+ if (!editor.current || !preview.current) return;
61
+ // Set initial sizes
62
+ const initialEditorWidth = window.innerWidth / 2;
63
+ const initialPreviewWidth = window.innerWidth - initialEditorWidth - 4;
64
+ editor.current.style.width = `${initialEditorWidth}px`;
65
+ preview.current.style.width = `${initialPreviewWidth}px`;
66
+
67
+ if (!resizer.current) return;
68
+ resizer.current.addEventListener("mousedown", handleMouseDown);
69
+ window.addEventListener("resize", () => handleMouseDown);
70
+ });
71
+
72
+ useUnmount(() => {
73
+ document.removeEventListener("mousemove", handleResize);
74
+ document.removeEventListener("mouseup", handleMouseUp);
75
+ if (resizer.current) {
76
+ resizer.current.removeEventListener("mousedown", handleMouseDown);
77
+ }
78
+ window.removeEventListener("resize", () => handleMouseDown);
79
+ });
80
+
81
+ return (
82
+ <div className="h-screen bg-gray-950 font-sans overflow-hidden">
83
+ <Header>
84
+ <DeployButton html={html} error={error} auth={auth} />
85
+ </Header>
86
+ <main className="lg:flex w-full hidden">
87
+ <div
88
+ ref={editor}
89
+ className="w-full h-full relative"
90
+ onClick={(e) => {
91
+ if (isAiWorking) {
92
+ e.preventDefault();
93
+ e.stopPropagation();
94
+ toast.warn("Please wait for the AI to finish working.");
95
+ }
96
+ }}
97
+ >
98
+ <Tabs>{/* <Settings /> */}</Tabs>
99
+ <Editor
100
+ language="html"
101
+ theme="vs-dark"
102
+ className={classNames("h-[calc(100dvh-96px)]", {
103
+ "pointer-events-none": isAiWorking,
104
+ })}
105
+ value={html}
106
+ onValidate={(markers) => {
107
+ if (markers?.length > 0) {
108
+ setError(true);
109
+ }
110
+ }}
111
+ onChange={(value) => {
112
+ const newValue = value ?? "";
113
+ setHtml(newValue);
114
+ setError(false);
115
+ }}
116
+ onMount={(editor) => (editorRef.current = editor)}
117
+ />
118
+ <AskAI
119
+ html={html}
120
+ setHtml={setHtml}
121
+ isAiWorking={isAiWorking}
122
+ setisAiWorking={setisAiWorking}
123
+ onScrollToBottom={() => {
124
+ editorRef.current?.revealLine(
125
+ editorRef.current?.getModel()?.getLineCount() ?? 0
126
+ );
127
+ }}
128
+ />
129
+ </div>
130
+ <div
131
+ ref={resizer}
132
+ className="bg-gray-700 hover:bg-blue-500 w-2 cursor-col-resize h-[calc(100dvh-54px)]"
133
+ />
134
+ <div
135
+ ref={preview}
136
+ className="w-full border-l border-gray-900 bg-white h-[calc(100dvh-54px)]"
137
+ >
138
+ <iframe
139
+ title="output"
140
+ className={classNames("w-full h-full select-none", {
141
+ "pointer-events-none": isResizing || isAiWorking,
142
+ })}
143
+ srcDoc={html}
144
+ />
145
+ </div>
146
+ </main>
147
+ <main className="lg:hidden p-5">
148
+ <p className="p-5 bg-red-500/10 text-red-500 rounded-md text-base text-pretty">
149
+ This app is not yet optimized for mobile. Please use a desktop browser
150
+ for the best experience.
151
+ </p>
152
+ </main>
153
+ </div>
154
+ );
155
+ }
156
+
157
+ export default App;
src/components/ask-ai/ask-ai.tsx ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { RiSparkling2Fill } from "react-icons/ri";
3
+ import { GrSend } from "react-icons/gr";
4
+ import classNames from "classnames";
5
+ import { toast } from "react-toastify";
6
+ import Login from "../login/login";
7
+ import { defaultHTML } from "../../utils/consts";
8
+
9
+ function AskAI({
10
+ html,
11
+ setHtml,
12
+ onScrollToBottom,
13
+ isAiWorking,
14
+ setisAiWorking,
15
+ }: {
16
+ html: string;
17
+ setHtml: (html: string) => void;
18
+ onScrollToBottom: () => void;
19
+ isAiWorking: boolean;
20
+ setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
21
+ }) {
22
+ const [open, setOpen] = useState(false);
23
+ const [prompt, setPrompt] = useState("");
24
+
25
+ const callAi = async () => {
26
+ if (isAiWorking) return;
27
+ setisAiWorking(true);
28
+
29
+ let contentResponse = "";
30
+ try {
31
+ const request = await fetch("/api/ask-ai", {
32
+ method: "POST",
33
+ body: JSON.stringify({
34
+ prompt,
35
+ ...(html === defaultHTML ? {} : { html }),
36
+ }),
37
+ headers: {
38
+ "Content-Type": "application/json",
39
+ },
40
+ });
41
+ if (request && request.body) {
42
+ if (!request.ok) {
43
+ const res = await request.json();
44
+ toast.error(res.message);
45
+ if (res.openLogin) {
46
+ setOpen(true);
47
+ }
48
+ setisAiWorking(false);
49
+ return;
50
+ }
51
+ const reader = request.body.getReader();
52
+ const decoder = new TextDecoder("utf-8");
53
+
54
+ const read = async () => {
55
+ const { done, value } = await reader.read();
56
+ if (done) {
57
+ toast.success("AI responded successfully");
58
+ setPrompt("");
59
+ setisAiWorking(false);
60
+ return;
61
+ }
62
+
63
+ const chunk = decoder.decode(value, { stream: true });
64
+ contentResponse += chunk;
65
+ const newHtml = contentResponse.match(/<!DOCTYPE html>[\s\S]*/)?.[0];
66
+ if (newHtml) {
67
+ setHtml(newHtml);
68
+ if (newHtml?.length > 200) {
69
+ onScrollToBottom();
70
+ }
71
+ }
72
+ read();
73
+ };
74
+
75
+ read();
76
+ }
77
+
78
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
+ } catch (error: any) {
80
+ setisAiWorking(false);
81
+ toast.error(error.message);
82
+ if (error.openLogin) {
83
+ setOpen(true);
84
+ }
85
+ }
86
+ };
87
+
88
+ return (
89
+ <div
90
+ className={`bg-gray-950 rounded-xl py-2.5 pl-4 pr-2.5 sticky bottom-4 left-4 w-[calc(100%-2rem)] z-10 group ${
91
+ isAiWorking ? "animate-pulse" : ""
92
+ }`}
93
+ >
94
+ <div className="w-full relative flex items-center justify-between">
95
+ <RiSparkling2Fill className="text-xl text-gray-500 group-focus-within:text-pink-500" />
96
+ <input
97
+ type="text"
98
+ disabled={isAiWorking}
99
+ className="w-full bg-transparent outline-none pl-3 text-white placeholder:text-gray-500 font-code"
100
+ placeholder="Ask AI anything..."
101
+ value={prompt}
102
+ onChange={(e) => setPrompt(e.target.value)}
103
+ onKeyDown={(e) => {
104
+ if (e.key === "Enter") {
105
+ callAi();
106
+ }
107
+ }}
108
+ />
109
+ <button
110
+ disabled={isAiWorking}
111
+ className="relative overflow-hidden cursor-pointer flex-none flex items-center justify-center rounded-full text-sm font-semibold size-8 text-center bg-pink-500 hover:bg-pink-400 text-white shadow-sm dark:shadow-highlight/20 disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed disabled:hover:bg-gray-300"
112
+ onClick={callAi}
113
+ >
114
+ <GrSend className="-translate-x-[1px]" />
115
+ </button>
116
+ </div>
117
+ <div
118
+ className={classNames(
119
+ "h-screen w-screen bg-black/20 fixed left-0 top-0 z-10",
120
+ {
121
+ "opacity-0 pointer-events-none": !open,
122
+ }
123
+ )}
124
+ onClick={() => setOpen(false)}
125
+ ></div>
126
+ <div
127
+ className={classNames(
128
+ "absolute top-0 -translate-y-[calc(100%+8px)] right-0 z-10 w-80 bg-white border border-gray-200 rounded-lg shadow-lg transition-all duration-75 overflow-hidden",
129
+ {
130
+ "opacity-0 pointer-events-none": !open,
131
+ }
132
+ )}
133
+ >
134
+ <Login>
135
+ <p className="text-gray-500 text-sm mb-3">
136
+ You reached the limit of free AI usage. Please login to continue.
137
+ </p>
138
+ </Login>
139
+ </div>
140
+ </div>
141
+ );
142
+ }
143
+
144
+ export default AskAI;
src/components/deploy-button/deploy-button.tsx ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { useState } from "react";
3
+ import classNames from "classnames";
4
+ import { toast } from "react-toastify";
5
+
6
+ import SpaceIcon from "@/assets/space.svg";
7
+ import Loading from "../loading/loading";
8
+ import Login from "../login/login";
9
+ import { Auth } from "../../utils/types";
10
+
11
+ function DeployButton({
12
+ html,
13
+ error = false,
14
+ auth,
15
+ }: {
16
+ html: string;
17
+ error: boolean;
18
+ auth?: Auth;
19
+ }) {
20
+ const [open, setOpen] = useState(false);
21
+ const [loading, setLoading] = useState(false);
22
+ const [path, setPath] = useState<string | undefined>(undefined);
23
+
24
+ const [config, setConfig] = useState({
25
+ title: "",
26
+ });
27
+
28
+ const createSpace = async () => {
29
+ setLoading(true);
30
+
31
+ try {
32
+ const request = await fetch("/api/deploy", {
33
+ method: "POST",
34
+ body: JSON.stringify({
35
+ title: config.title,
36
+ path,
37
+ html,
38
+ }),
39
+ headers: {
40
+ "Content-Type": "application/json",
41
+ },
42
+ });
43
+ const response = await request.json();
44
+ if (response.ok) {
45
+ toast.success(
46
+ path ? `Space updated successfully!` : `Space created successfully!`
47
+ );
48
+ setPath(response.path);
49
+ } else {
50
+ toast.error(response.message);
51
+ }
52
+ } catch (err: any) {
53
+ toast.error(err.message);
54
+ } finally {
55
+ setLoading(false);
56
+ setOpen(false);
57
+ }
58
+ };
59
+
60
+ return (
61
+ <div className="relative max-lg:hidden flex items-center justify-end">
62
+ {auth && (
63
+ <p className="mr-3 text-sm text-gray-300">
64
+ Connected as{" "}
65
+ <a
66
+ href={`https://huggingface/${auth.preferred_username}`}
67
+ target="_blank"
68
+ className="underline hover:text-white"
69
+ >
70
+ {auth.preferred_username}
71
+ </a>
72
+ </p>
73
+ )}
74
+ <button
75
+ className={classNames(
76
+ "relative cursor-pointer flex-none flex items-center justify-center rounded-md text-sm font-semibold leading-6 py-1.5 px-5 hover:bg-pink-400 text-white shadow-sm dark:shadow-highlight/20",
77
+ {
78
+ "bg-pink-400": open,
79
+ "bg-pink-500": !open,
80
+ }
81
+ )}
82
+ onClick={() => setOpen(!open)}
83
+ >
84
+ {path ? "Update Space" : "Deploy to Space"}
85
+ </button>
86
+ <div
87
+ className={classNames(
88
+ "h-screen w-screen bg-black/20 fixed left-0 top-0 z-10",
89
+ {
90
+ "opacity-0 pointer-events-none": !open,
91
+ }
92
+ )}
93
+ onClick={() => setOpen(false)}
94
+ ></div>
95
+ <div
96
+ className={classNames(
97
+ "absolute top-[calc(100%+8px)] right-0 z-10 w-80 bg-white border border-gray-200 rounded-lg shadow-lg transition-all duration-75 overflow-hidden",
98
+ {
99
+ "opacity-0 pointer-events-none": !open,
100
+ }
101
+ )}
102
+ >
103
+ {!auth ? (
104
+ <Login />
105
+ ) : (
106
+ <>
107
+ <header className="flex items-center text-sm px-4 py-2 border-b border-gray-200 gap-2 bg-gray-100 font-semibold text-gray-700">
108
+ <span className="text-xs bg-pink-500/10 text-pink-500 rounded-full pl-1.5 pr-2.5 py-0.5 flex items-center justify-start gap-1.5">
109
+ <img src={SpaceIcon} alt="Space Icon" className="size-4" />
110
+ Space
111
+ </span>
112
+ Configure Deployment
113
+ </header>
114
+ <main className="px-4 pt-3 pb-4 space-y-3">
115
+ <p className="text-xs text-amber-600 bg-amber-500/10 rounded-md p-2">
116
+ {path ? (
117
+ <span>
118
+ Your space is live at{" "}
119
+ <a
120
+ href={`https://huggingface.co/spaces/${path}`}
121
+ target="_blank"
122
+ className="underline hover:text-amber-700"
123
+ >
124
+ huggingface.co/{path}
125
+ </a>
126
+ . You can update it by deploying again.
127
+ </span>
128
+ ) : (
129
+ "Deploy your project to a space on the Hub. Spaces are a way to share your project with the world."
130
+ )}
131
+ </p>
132
+ {!path && (
133
+ <label className="block">
134
+ <p className="text-gray-600 text-sm font-medium mb-1.5">
135
+ Space Title
136
+ </p>
137
+ <input
138
+ type="text"
139
+ value={config.title}
140
+ className="mr-2 border rounded-md px-3 py-1.5 border-gray-300 w-full text-sm"
141
+ placeholder="My Awesome Space"
142
+ onChange={(e) =>
143
+ setConfig({ ...config, title: e.target.value })
144
+ }
145
+ />
146
+ </label>
147
+ )}
148
+ {error && (
149
+ <p className="text-red-500 text-xs bg-red-500/10 rounded-md p-2">
150
+ Your code has errors. Fix them before deploying.
151
+ </p>
152
+ )}
153
+ <div className="pt-2 text-right">
154
+ <button
155
+ disabled={error || loading || !config.title}
156
+ className="relative rounded-full bg-black px-5 py-2 text-white font-semibold text-xs hover:bg-black/90 transition-all duration-100 disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed disabled:hover:bg-gray-300"
157
+ onClick={createSpace}
158
+ >
159
+ {path ? "Update Space" : "Create Space"}
160
+ {loading && <Loading />}
161
+ </button>
162
+ </div>
163
+ </main>
164
+ </>
165
+ )}
166
+ </div>
167
+ </div>
168
+ );
169
+ }
170
+
171
+ export default DeployButton;
src/components/header/header.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import SpaceIcon from "@/assets/space.svg";
2
+ import { ReactNode } from "react";
3
+
4
+ function Header({ children }: { children?: ReactNode }) {
5
+ return (
6
+ <header className="border-b border-gray-900 px-6 py-2 flex justify-center md:justify-between items-center">
7
+ <div className="flex items-center justify-start gap-3">
8
+ <h1 className="text-white text-xl font-bold flex items-center justify-start">
9
+ <img src={SpaceIcon} alt="Space Icon" className="size-8 mr-2" />
10
+ Space Generator
11
+ </h1>
12
+ <p className="text-gray-700 max-md:hidden">|</p>
13
+ <p className="text-gray-500 text-sm max-md:hidden">
14
+ Code and Deploy in 1-Click
15
+ </p>
16
+ </div>
17
+ {children}
18
+ </header>
19
+ );
20
+ }
21
+
22
+ export default Header;
src/components/loading/loading.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function Loading() {
2
+ return (
3
+ <div className="absolute left-0 top-0 h-full w-full flex items-center justify-center bg-white/30 z-20">
4
+ <svg
5
+ className="size-5 animate-spin text-white"
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ fill="none"
8
+ viewBox="0 0 24 24"
9
+ >
10
+ <circle
11
+ className="opacity-25"
12
+ cx="12"
13
+ cy="12"
14
+ r="10"
15
+ stroke="currentColor"
16
+ strokeWidth="4"
17
+ ></circle>
18
+ <path
19
+ className="opacity-75"
20
+ fill="currentColor"
21
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
22
+ ></path>
23
+ </svg>
24
+ </div>
25
+ );
26
+ }
27
+
28
+ export default Loading;
src/components/login/login.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function Login({ children }: { children?: React.ReactNode }) {
2
+ return (
3
+ <>
4
+ <header className="flex items-center text-sm px-4 py-2 border-b border-gray-200 gap-2 bg-gray-100 font-semibold text-gray-700">
5
+ <span className="text-xs bg-red-500/10 text-red-500 rounded-full pl-1.5 pr-2.5 py-0.5 flex items-center justify-start gap-1.5">
6
+ REQUIRED
7
+ </span>
8
+ Login with Hugging Face
9
+ </header>
10
+ <main className="px-4 py-4 space-y-3">
11
+ {children}
12
+ <a href="/api/login">
13
+ <img
14
+ src="https://huggingface.co/datasets/huggingface/badges/resolve/main/sign-in-with-huggingface-md-dark.svg"
15
+ alt="Sign in with Hugging Face"
16
+ className="mx-auto"
17
+ />
18
+ </a>
19
+ </main>
20
+ </>
21
+ );
22
+ }
23
+
24
+ export default Login;
src/components/settings/settings.tsx ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import classNames from "classnames";
3
+ import Login from "../login/login";
4
+
5
+ function Settings() {
6
+ const [open, setOpen] = useState(false);
7
+
8
+ return (
9
+ <div className="relative">
10
+ <button
11
+ className="bg-gray-800/70 rounded-md text-xs text-gray-300 hover:brightness-125 px-3 py-1.5 font-medium cursor-pointer"
12
+ onClick={() => setOpen(!open)}
13
+ >
14
+ Settings
15
+ </button>
16
+ <div
17
+ className={classNames(
18
+ "h-screen w-screen bg-black/20 fixed left-0 top-0 z-10",
19
+ {
20
+ "opacity-0 pointer-events-none": !open,
21
+ }
22
+ )}
23
+ onClick={() => setOpen(false)}
24
+ ></div>
25
+ <div
26
+ className={classNames(
27
+ "absolute top-[calc(100%+8px)] right-0 z-10 w-80 bg-white border border-gray-200 rounded-lg shadow-lg transition-all duration-75 overflow-hidden",
28
+ {
29
+ "opacity-0 pointer-events-none": !open,
30
+ }
31
+ )}
32
+ >
33
+ <Login />
34
+ </div>
35
+ </div>
36
+ );
37
+ }
38
+ export default Settings;
src/components/tabs/tabs.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Deepseek from "./../../assets/deepseek-color.svg";
2
+
3
+ function Tabs({ children }: { children?: React.ReactNode }) {
4
+ return (
5
+ <div className="border-b border-gray-800 pl-7 pr-3 flex items-center justify-between">
6
+ <div
7
+ className="
8
+ space-x-6"
9
+ >
10
+ <button className="rounded-md text-sm cursor-pointer transition-all duration-100 font-medium relative py-2.5 text-white">
11
+ index.html
12
+ <span className="absolute bottom-0 left-0 h-0.5 w-full transition-all duration-100 bg-white" />
13
+ </button>
14
+ </div>
15
+ <div className="flex items-center justify-end gap-3">
16
+ <a
17
+ href="https://huggingface.co/deepseek-ai/DeepSeek-V3-0324"
18
+ target="_blank"
19
+ className="text-[12px] text-gray-300 hover:brightness-120 flex items-center gap-1 font-code"
20
+ >
21
+ Powered by <img src={Deepseek} className="size-5" /> Deepseek
22
+ </a>
23
+ {children}
24
+ </div>
25
+ </div>
26
+ );
27
+ }
28
+
29
+ export default Tabs;
src/main.tsx ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { ToastContainer } from "react-toastify";
4
+ import "./assets/index.css";
5
+ import App from "./components/App.tsx";
6
+
7
+ createRoot(document.getElementById("root")!).render(
8
+ <StrictMode>
9
+ <App />
10
+ <ToastContainer />
11
+ </StrictMode>
12
+ );
src/utils/consts.ts ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const defaultHTML = `<!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>My app</title>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta charset="utf-8">
7
+ <style>
8
+ body {
9
+ display: flex;
10
+ justify-content: center;
11
+ align-items: center;
12
+ height: 100dvh;
13
+ font-family: "Arial", sans-serif;
14
+ }
15
+ </style>
16
+ </head>
17
+ <body>
18
+ <h1>
19
+ Start editing to see some magic happen!
20
+ </h1>
21
+ <script></script>
22
+ </body>
23
+ </html>
24
+ `;
src/utils/types.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export interface Auth {
2
+ preferred_username: string;
3
+ picture: string;
4
+ name: string;
5
+ }
src/vite-env.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ /// <reference types="vite/client" />
tsconfig.app.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2020",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "isolatedModules": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+ "jsx": "react-jsx",
17
+
18
+ /* Linting */
19
+ "strict": true,
20
+ "noUnusedLocals": true,
21
+ "noUnusedParameters": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["src", "middleware"]
26
+ }
tsconfig.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ],
7
+ "compilerOptions": {
8
+ "baseUrl": ".",
9
+ "paths": {
10
+ "@/*": ["src/*"]
11
+ }
12
+ }
13
+ }
tsconfig.node.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "isolatedModules": true,
13
+ "moduleDetection": "force",
14
+ "noEmit": true,
15
+
16
+ /* Linting */
17
+ "strict": true,
18
+ "noUnusedLocals": true,
19
+ "noUnusedParameters": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedSideEffectImports": true
22
+ },
23
+ "include": ["vite.config.ts"]
24
+ }
vite.config.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+ import tailwindcss from "@tailwindcss/vite";
4
+
5
+ // https://vite.dev/config/
6
+ export default defineConfig({
7
+ plugins: [react(), tailwindcss()],
8
+ resolve: {
9
+ alias: [{ find: "@", replacement: "/src" }],
10
+ },
11
+ });