Spaces:
Running
Running
Commit
·
ea6c2a8
0
Parent(s):
Initial ✨
Browse files- .env.example +5 -0
- .gitignore +25 -0
- Dockerfile +22 -0
- README.md +12 -0
- eslint.config.js +28 -0
- index.html +27 -0
- middlewares/checkUser.js +10 -0
- package-lock.json +0 -0
- package.json +46 -0
- public/vite.svg +1 -0
- server.js +250 -0
- src/assets/deepseek-color.svg +1 -0
- src/assets/index.css +9 -0
- src/assets/space.svg +7 -0
- src/components/App.tsx +157 -0
- src/components/ask-ai/ask-ai.tsx +144 -0
- src/components/deploy-button/deploy-button.tsx +171 -0
- src/components/header/header.tsx +22 -0
- src/components/loading/loading.tsx +28 -0
- src/components/login/login.tsx +24 -0
- src/components/settings/settings.tsx +38 -0
- src/components/tabs/tabs.tsx +29 -0
- src/main.tsx +12 -0
- src/utils/consts.ts +24 -0
- src/utils/types.ts +5 -0
- src/vite-env.d.ts +1 -0
- tsconfig.app.json +26 -0
- tsconfig.json +13 -0
- tsconfig.node.json +24 -0
- vite.config.ts +11 -0
.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 |
+
});
|