abdullahalivv commited on
Commit
664c219
·
verified ·
1 Parent(s): fc7c3d7

Upload 45 files

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,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
26
+ .aider*
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 5173
20
+
21
+ # Start the application
22
+ CMD ["npm", "start"]
README.md CHANGED
@@ -1,13 +1,15 @@
1
- ---
2
- title: Deepseek
3
- emoji: 🚀
4
- colorFrom: purple
5
- colorTo: gray
6
- sdk: gradio
7
- sdk_version: 5.23.3
8
- app_file: app.py
9
- pinned: false
10
- license: mit
11
- ---
12
-
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
1
+ ---
2
+ title: DeepSite
3
+ emoji: 🐳
4
+ colorFrom: blue
5
+ colorTo: blue
6
+ sdk: docker
7
+ pinned: true
8
+ app_port: 5173
9
+ license: mit
10
+ short_description: Generate any application with DeepSeek
11
+ models:
12
+ - deepseek-ai/DeepSeek-V3-0324
13
+ ---
14
+
15
+ 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,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/logo.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>DeepSite | Build with AI ✨</title>
8
+ <meta
9
+ name="description"
10
+ content="DeepSite is a web development tool that
11
+ helps you build websites with AI, no code required. Let's deploy your
12
+ website with DeepSite and enjoy the magic of AI."
13
+ />
14
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
15
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
16
+ <link
17
+ href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap"
18
+ rel="stylesheet"
19
+ />
20
+ <link
21
+ 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"
22
+ rel="stylesheet"
23
+ />
24
+ </head>
25
+ <body>
26
+ <div id="root"></div>
27
+ <script type="module" src="/src/main.tsx"></script>
28
+ </body>
29
+ </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
+ }
module.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ declare module "react-speech-recognition";
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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-speech-recognition": "^4.0.0",
29
+ "react-toastify": "^11.0.5",
30
+ "react-use": "^17.6.0",
31
+ "tailwindcss": "^4.0.15"
32
+ },
33
+ "devDependencies": {
34
+ "@eslint/js": "^9.21.0",
35
+ "@types/express": "^5.0.1",
36
+ "@types/react": "^19.0.10",
37
+ "@types/react-dom": "^19.0.4",
38
+ "@types/react-speech-recognition": "^3.9.6",
39
+ "@vitejs/plugin-react": "^4.3.4",
40
+ "eslint": "^9.21.0",
41
+ "eslint-plugin-react-hooks": "^5.1.0",
42
+ "eslint-plugin-react-refresh": "^0.4.19",
43
+ "globals": "^15.15.0",
44
+ "typescript": "~5.7.2",
45
+ "typescript-eslint": "^8.24.1",
46
+ "vite": "^6.2.0"
47
+ }
48
+ }
public/arrow.svg ADDED
public/logo.svg ADDED
public/providers/fireworks-ai.svg ADDED
public/providers/hyperbolic.svg ADDED
public/providers/nebius.svg ADDED
public/providers/novita.svg ADDED
public/providers/sambanova.svg ADDED
server.js ADDED
@@ -0,0 +1,399 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 {
7
+ createRepo,
8
+ uploadFiles,
9
+ whoAmI,
10
+ spaceInfo,
11
+ fileExists,
12
+ } from "@huggingface/hub";
13
+ import { InferenceClient } from "@huggingface/inference";
14
+ import bodyParser from "body-parser";
15
+
16
+ import checkUser from "./middlewares/checkUser.js";
17
+ import { PROVIDERS } from "./utils/providers.js";
18
+ import { COLORS } from "./utils/colors.js";
19
+
20
+ // Load environment variables from .env file
21
+ dotenv.config();
22
+
23
+ const app = express();
24
+
25
+ const ipAddresses = new Map();
26
+
27
+ const __filename = fileURLToPath(import.meta.url);
28
+ const __dirname = path.dirname(__filename);
29
+
30
+ const PORT = process.env.APP_PORT || 3000;
31
+ const REDIRECT_URI =
32
+ process.env.REDIRECT_URI || `http://localhost:${PORT}/auth/login`;
33
+ const MODEL_ID = "deepseek-ai/DeepSeek-V3-0324";
34
+ const MAX_REQUESTS_PER_IP = 2;
35
+
36
+ app.use(cookieParser());
37
+ app.use(bodyParser.json());
38
+ app.use(express.static(path.join(__dirname, "dist")));
39
+
40
+ const getPTag = (repoId) => {
41
+ return `<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=${repoId}" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p>`;
42
+ };
43
+
44
+ app.get("/api/login", (_req, res) => {
45
+ res.redirect(
46
+ 302,
47
+ `https://huggingface.co/oauth/authorize?client_id=${process.env.OAUTH_CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=openid%20profile%20write-repos%20manage-repos%20inference-api&prompt=consent&state=1234567890`
48
+ );
49
+ });
50
+ app.get("/auth/login", async (req, res) => {
51
+ const { code } = req.query;
52
+
53
+ if (!code) {
54
+ return res.redirect(302, "/");
55
+ }
56
+ const Authorization = `Basic ${Buffer.from(
57
+ `${process.env.OAUTH_CLIENT_ID}:${process.env.OAUTH_CLIENT_SECRET}`
58
+ ).toString("base64")}`;
59
+
60
+ const request_auth = await fetch("https://huggingface.co/oauth/token", {
61
+ method: "POST",
62
+ headers: {
63
+ "Content-Type": "application/x-www-form-urlencoded",
64
+ Authorization,
65
+ },
66
+ body: new URLSearchParams({
67
+ grant_type: "authorization_code",
68
+ code: code,
69
+ redirect_uri: REDIRECT_URI,
70
+ }),
71
+ });
72
+
73
+ const response = await request_auth.json();
74
+
75
+ if (!response.access_token) {
76
+ return res.redirect(302, "/");
77
+ }
78
+
79
+ res.cookie("hf_token", response.access_token, {
80
+ httpOnly: false,
81
+ secure: true,
82
+ sameSite: "none",
83
+ maxAge: 30 * 24 * 60 * 60 * 1000,
84
+ });
85
+
86
+ return res.redirect(302, "/");
87
+ });
88
+ app.get("/auth/logout", (req, res) => {
89
+ res.clearCookie("hf_token", {
90
+ httpOnly: false,
91
+ secure: true,
92
+ sameSite: "none",
93
+ });
94
+ return res.redirect(302, "/");
95
+ });
96
+
97
+ app.get("/api/@me", checkUser, async (req, res) => {
98
+ const { hf_token } = req.cookies;
99
+ try {
100
+ const request_user = await fetch("https://huggingface.co/oauth/userinfo", {
101
+ headers: {
102
+ Authorization: `Bearer ${hf_token}`,
103
+ },
104
+ });
105
+
106
+ const user = await request_user.json();
107
+ res.send(user);
108
+ } catch (err) {
109
+ res.clearCookie("hf_token", {
110
+ httpOnly: false,
111
+ secure: true,
112
+ sameSite: "none",
113
+ });
114
+ res.status(401).send({
115
+ ok: false,
116
+ message: err.message,
117
+ });
118
+ }
119
+ });
120
+
121
+ app.post("/api/deploy", checkUser, async (req, res) => {
122
+ const { html, title, path } = req.body;
123
+ if (!html || !title) {
124
+ return res.status(400).send({
125
+ ok: false,
126
+ message: "Missing required fields",
127
+ });
128
+ }
129
+
130
+ const { hf_token } = req.cookies;
131
+ try {
132
+ const repo = {
133
+ type: "space",
134
+ name: path ?? "",
135
+ };
136
+
137
+ let readme;
138
+ let newHtml = html;
139
+
140
+ if (!path || path === "") {
141
+ const { name: username } = await whoAmI({ accessToken: hf_token });
142
+ const newTitle = title
143
+ .toLowerCase()
144
+ .replace(/[^a-z0-9]+/g, "-")
145
+ .split("-")
146
+ .filter(Boolean)
147
+ .join("-")
148
+ .slice(0, 96);
149
+
150
+ const repoId = `${username}/${newTitle}`;
151
+ repo.name = repoId;
152
+
153
+ await createRepo({
154
+ repo,
155
+ accessToken: hf_token,
156
+ });
157
+ const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
158
+ const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
159
+ readme = `---
160
+ title: ${newTitle}
161
+ emoji: 🐳
162
+ colorFrom: ${colorFrom}
163
+ colorTo: ${colorTo}
164
+ sdk: static
165
+ pinned: false
166
+ tags:
167
+ - deepsite
168
+ ---
169
+
170
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference`;
171
+ }
172
+
173
+ newHtml = html.replace(/<\/body>/, `${getPTag(repo.name)}</body>`);
174
+ const file = new Blob([newHtml], { type: "text/html" });
175
+ file.name = "index.html"; // Add name property to the Blob
176
+
177
+ const files = [file];
178
+ if (readme) {
179
+ const readmeFile = new Blob([readme], { type: "text/markdown" });
180
+ readmeFile.name = "README.md"; // Add name property to the Blob
181
+ files.push(readmeFile);
182
+ }
183
+ await uploadFiles({
184
+ repo,
185
+ files,
186
+ accessToken: hf_token,
187
+ });
188
+ return res.status(200).send({ ok: true, path: repo.name });
189
+ } catch (err) {
190
+ return res.status(500).send({
191
+ ok: false,
192
+ message: err.message,
193
+ });
194
+ }
195
+ });
196
+
197
+ app.post("/api/ask-ai", async (req, res) => {
198
+ const { prompt, html, previousPrompt, provider } = req.body;
199
+ if (!prompt) {
200
+ return res.status(400).send({
201
+ ok: false,
202
+ message: "Missing required fields",
203
+ });
204
+ }
205
+
206
+ const { hf_token } = req.cookies;
207
+ let token = hf_token;
208
+ const ip =
209
+ req.headers["x-forwarded-for"]?.split(",")[0].trim() ||
210
+ req.headers["x-real-ip"] ||
211
+ req.socket.remoteAddress ||
212
+ req.ip ||
213
+ "0.0.0.0";
214
+
215
+ if (!hf_token) {
216
+ ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
217
+ if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
218
+ return res.status(429).send({
219
+ ok: false,
220
+ openLogin: true,
221
+ message: "Log In to continue using the service",
222
+ });
223
+ }
224
+
225
+ token = process.env.DEFAULT_HF_TOKEN;
226
+ }
227
+
228
+ // Set up response headers for streaming
229
+ res.setHeader("Content-Type", "text/plain");
230
+ res.setHeader("Cache-Control", "no-cache");
231
+ res.setHeader("Connection", "keep-alive");
232
+
233
+ const client = new InferenceClient(token);
234
+ let completeResponse = "";
235
+
236
+ let TOKENS_USED = prompt?.length;
237
+ if (previousPrompt) TOKENS_USED += previousPrompt.length;
238
+ if (html) TOKENS_USED += html.length;
239
+
240
+ const DEFAULT_PROVIDER = PROVIDERS.novita;
241
+ // const selectedProvider =
242
+ // provider === "auto"
243
+ // ? TOKENS_USED < PROVIDERS.sambanova.max_tokens
244
+ // ? PROVIDERS.sambanova
245
+ // : DEFAULT_PROVIDER
246
+ // : PROVIDERS[provider] ?? DEFAULT_PROVIDER;
247
+ const selectedProvider =
248
+ provider === "auto"
249
+ ? DEFAULT_PROVIDER
250
+ : PROVIDERS[provider] ?? DEFAULT_PROVIDER;
251
+
252
+ if (provider !== "auto" && TOKENS_USED >= selectedProvider.max_tokens) {
253
+ return res.status(400).send({
254
+ ok: false,
255
+ openSelectProvider: true,
256
+ message: `Context is too long. ${selectedProvider.name} allow ${selectedProvider.max_tokens} max tokens.`,
257
+ });
258
+ }
259
+
260
+ try {
261
+ const chatCompletion = client.chatCompletionStream({
262
+ model: MODEL_ID,
263
+ provider: selectedProvider.id,
264
+ messages: [
265
+ {
266
+ role: "system",
267
+ content: `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. Use as much as you can TailwindCSS for the CSS, if you can't do something with TailwindCSS, then use custom CSS (make sure to import <script src="https://cdn.tailwindcss.com"></script> in the head). Also, try to ellaborate as much as you can, to create something unique. ALWAYS GIVE THE RESPONSE INTO A SINGLE HTML FILE`,
268
+ },
269
+ ...(previousPrompt
270
+ ? [
271
+ {
272
+ role: "user",
273
+ content: previousPrompt,
274
+ },
275
+ ]
276
+ : []),
277
+ ...(html
278
+ ? [
279
+ {
280
+ role: "assistant",
281
+ content: `The current code is: ${html}.`,
282
+ },
283
+ ]
284
+ : []),
285
+ {
286
+ role: "user",
287
+ content: prompt,
288
+ },
289
+ ],
290
+ ...(selectedProvider.id !== "sambanova"
291
+ ? {
292
+ max_tokens: selectedProvider.max_tokens,
293
+ }
294
+ : {}),
295
+ });
296
+
297
+ while (true) {
298
+ const { done, value } = await chatCompletion.next();
299
+ if (done) {
300
+ break;
301
+ }
302
+ const chunk = value.choices[0]?.delta?.content;
303
+ if (chunk) {
304
+ if (provider !== "sambanova") {
305
+ res.write(chunk);
306
+ completeResponse += chunk;
307
+
308
+ if (completeResponse.includes("</html>")) {
309
+ break;
310
+ }
311
+ } else {
312
+ let newChunk = chunk;
313
+ if (chunk.includes("</html>")) {
314
+ // Replace everything after the last </html> tag with an empty string
315
+ newChunk = newChunk.replace(/<\/html>[\s\S]*/, "</html>");
316
+ }
317
+ completeResponse += newChunk;
318
+ res.write(newChunk);
319
+ if (newChunk.includes("</html>")) {
320
+ break;
321
+ }
322
+ }
323
+ }
324
+ }
325
+ // End the response stream
326
+ res.end();
327
+ } catch (error) {
328
+ if (error.message.includes("exceeded your monthly included credits")) {
329
+ return res.status(402).send({
330
+ ok: false,
331
+ openProModal: true,
332
+ message: error.message,
333
+ });
334
+ }
335
+ if (!res.headersSent) {
336
+ res.status(500).send({
337
+ ok: false,
338
+ message:
339
+ error.message || "An error occurred while processing your request.",
340
+ });
341
+ } else {
342
+ // Otherwise end the stream
343
+ res.end();
344
+ }
345
+ }
346
+ });
347
+
348
+ app.get("/api/remix/:username/:repo", async (req, res) => {
349
+ const { username, repo } = req.params;
350
+ const { hf_token } = req.cookies;
351
+
352
+ const token = hf_token || process.env.DEFAULT_HF_TOKEN;
353
+
354
+ const repoId = `${username}/${repo}`;
355
+
356
+ const url = `https://huggingface.co/spaces/${repoId}/raw/main/index.html`;
357
+ try {
358
+ const space = await spaceInfo({
359
+ name: repoId,
360
+ accessToken: token,
361
+ });
362
+
363
+ if (!space || space.sdk !== "static" || space.private) {
364
+ return res.status(404).send({
365
+ ok: false,
366
+ message: "Space not found",
367
+ });
368
+ }
369
+
370
+ const response = await fetch(url);
371
+ if (!response.ok) {
372
+ return res.status(404).send({
373
+ ok: false,
374
+ message: "Space not found",
375
+ });
376
+ }
377
+ let html = await response.text();
378
+ // remove the last p tag including this url https://enzostvs-deepsite.hf.space
379
+ html = html.replace(getPTag(repoId), "");
380
+
381
+ res.status(200).send({
382
+ ok: true,
383
+ html,
384
+ });
385
+ } catch (error) {
386
+ return res.status(500).send({
387
+ ok: false,
388
+ message: error.message,
389
+ });
390
+ }
391
+ });
392
+
393
+ app.get("*", (_req, res) => {
394
+ res.sendFile(path.join(__dirname, "dist", "index.html"));
395
+ });
396
+
397
+ app.listen(PORT, () => {
398
+ console.log(`Server is running on port ${PORT}`);
399
+ });
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/logo.svg ADDED
src/assets/space.svg ADDED
src/assets/success.mp3 ADDED
Binary file (49.3 kB). View file
 
src/components/App.tsx ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useRef, useState } from "react";
2
+ import Editor from "@monaco-editor/react";
3
+ import classNames from "classnames";
4
+ import { editor } from "monaco-editor";
5
+ import {
6
+ useMount,
7
+ useUnmount,
8
+ useEvent,
9
+ useLocalStorage,
10
+ useSearchParam,
11
+ } from "react-use";
12
+ import { toast } from "react-toastify";
13
+
14
+ import Header from "./header/header";
15
+ import DeployButton from "./deploy-button/deploy-button";
16
+ import { defaultHTML } from "./../../utils/consts";
17
+ import Tabs from "./tabs/tabs";
18
+ import AskAI from "./ask-ai/ask-ai";
19
+ import { Auth } from "./../../utils/types";
20
+ import Preview from "./preview/preview";
21
+ import LoadButton from "./load-button/load-button";
22
+
23
+ function App() {
24
+ const [htmlStorage, , removeHtmlStorage] = useLocalStorage("html_content");
25
+ const remix = useSearchParam("remix");
26
+
27
+ const preview = useRef<HTMLDivElement>(null);
28
+ const editor = useRef<HTMLDivElement>(null);
29
+ const resizer = useRef<HTMLDivElement>(null);
30
+ const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
31
+
32
+ const [isResizing, setIsResizing] = useState(false);
33
+ const [error, setError] = useState(false);
34
+ const [html, setHtml] = useState((htmlStorage as string) ?? defaultHTML);
35
+ const [isAiWorking, setisAiWorking] = useState(false);
36
+ const [auth, setAuth] = useState<Auth | undefined>(undefined);
37
+ const [currentView, setCurrentView] = useState<"editor" | "preview">(
38
+ "editor"
39
+ );
40
+
41
+ const fetchMe = async () => {
42
+ const res = await fetch("/api/@me");
43
+ if (res.ok) {
44
+ const data = await res.json();
45
+ setAuth(data);
46
+ } else {
47
+ setAuth(undefined);
48
+ }
49
+ };
50
+
51
+ const fetchRemix = async () => {
52
+ if (!remix) return;
53
+ const res = await fetch(`/api/remix/${remix}`);
54
+ if (res.ok) {
55
+ const data = await res.json();
56
+ if (data.html) {
57
+ setHtml(data.html);
58
+ toast.success("Remix content loaded successfully.");
59
+ }
60
+ } else {
61
+ toast.error("Failed to load remix content.");
62
+ }
63
+ const url = new URL(window.location.href);
64
+ url.searchParams.delete("remix");
65
+ window.history.replaceState({}, document.title, url.toString());
66
+ };
67
+
68
+ /**
69
+ * Resets the layout based on screen size
70
+ * - For desktop: Sets editor to 1/3 width and preview to 2/3
71
+ * - For mobile: Removes inline styles to let CSS handle it
72
+ */
73
+ const resetLayout = () => {
74
+ if (!editor.current || !preview.current) return;
75
+
76
+ // lg breakpoint is 1024px based on useBreakpoint definition and Tailwind defaults
77
+ if (window.innerWidth >= 1024) {
78
+ // Set initial 1/3 - 2/3 sizes for large screens, accounting for resizer width
79
+ const resizerWidth = resizer.current?.offsetWidth ?? 8; // w-2 = 0.5rem = 8px
80
+ const availableWidth = window.innerWidth - resizerWidth;
81
+ const initialEditorWidth = availableWidth / 3; // Editor takes 1/3 of space
82
+ const initialPreviewWidth = availableWidth - initialEditorWidth; // Preview takes 2/3
83
+ editor.current.style.width = `${initialEditorWidth}px`;
84
+ preview.current.style.width = `${initialPreviewWidth}px`;
85
+ } else {
86
+ // Remove inline styles for smaller screens, let CSS flex-col handle it
87
+ editor.current.style.width = "";
88
+ preview.current.style.width = "";
89
+ }
90
+ };
91
+
92
+ /**
93
+ * Handles resizing when the user drags the resizer
94
+ * Ensures minimum widths are maintained for both panels
95
+ */
96
+ const handleResize = (e: MouseEvent) => {
97
+ if (!editor.current || !preview.current || !resizer.current) return;
98
+
99
+ const resizerWidth = resizer.current.offsetWidth;
100
+ const minWidth = 100; // Minimum width for editor/preview
101
+ const maxWidth = window.innerWidth - resizerWidth - minWidth;
102
+
103
+ const editorWidth = e.clientX;
104
+ const clampedEditorWidth = Math.max(
105
+ minWidth,
106
+ Math.min(editorWidth, maxWidth)
107
+ );
108
+ const calculatedPreviewWidth =
109
+ window.innerWidth - clampedEditorWidth - resizerWidth;
110
+
111
+ editor.current.style.width = `${clampedEditorWidth}px`;
112
+ preview.current.style.width = `${calculatedPreviewWidth}px`;
113
+ };
114
+
115
+ const handleMouseDown = () => {
116
+ setIsResizing(true);
117
+ document.addEventListener("mousemove", handleResize);
118
+ document.addEventListener("mouseup", handleMouseUp);
119
+ };
120
+
121
+ const handleMouseUp = () => {
122
+ setIsResizing(false);
123
+ document.removeEventListener("mousemove", handleResize);
124
+ document.removeEventListener("mouseup", handleMouseUp);
125
+ };
126
+
127
+ // Prevent accidental navigation away when AI is working or content has changed
128
+ useEvent("beforeunload", (e) => {
129
+ if (isAiWorking || html !== defaultHTML) {
130
+ e.preventDefault();
131
+ return "";
132
+ }
133
+ });
134
+
135
+ // Initialize component on mount
136
+ useMount(() => {
137
+ // Fetch user data
138
+ fetchMe();
139
+ fetchRemix();
140
+
141
+ // Restore content from storage if available
142
+ if (htmlStorage) {
143
+ removeHtmlStorage();
144
+ toast.warn("Previous HTML content restored from local storage.");
145
+ }
146
+
147
+ // Set initial layout based on window size
148
+ resetLayout();
149
+
150
+ // Attach event listeners
151
+ if (!resizer.current) return;
152
+ resizer.current.addEventListener("mousedown", handleMouseDown);
153
+ window.addEventListener("resize", resetLayout);
154
+ });
155
+
156
+ // Clean up event listeners on unmount
157
+ useUnmount(() => {
158
+ document.removeEventListener("mousemove", handleResize);
159
+ document.removeEventListener("mouseup", handleMouseUp);
160
+ if (resizer.current) {
161
+ resizer.current.removeEventListener("mousedown", handleMouseDown);
162
+ }
163
+ window.removeEventListener("resize", resetLayout);
164
+ });
165
+
166
+ return (
167
+ <div className="h-screen bg-gray-950 font-sans overflow-hidden">
168
+ <Header
169
+ onReset={() => {
170
+ if (isAiWorking) {
171
+ toast.warn("Please wait for the AI to finish working.");
172
+ return;
173
+ }
174
+ if (
175
+ window.confirm("You're about to reset the editor. Are you sure?")
176
+ ) {
177
+ setHtml(defaultHTML);
178
+ setError(false);
179
+ removeHtmlStorage();
180
+ editorRef.current?.revealLine(
181
+ editorRef.current?.getModel()?.getLineCount() ?? 0
182
+ );
183
+ }
184
+ }}
185
+ >
186
+ <div className="flex items-center justify-end gap-5">
187
+ <LoadButton auth={auth} setHtml={setHtml} />
188
+ <DeployButton html={html} error={error} auth={auth} />
189
+ </div>
190
+ </Header>
191
+ <main className="max-lg:flex-col flex w-full">
192
+ <div
193
+ ref={editor}
194
+ className={classNames(
195
+ "w-full h-[calc(100dvh-49px)] lg:h-[calc(100dvh-54px)] relative overflow-hidden max-lg:transition-all max-lg:duration-200 select-none",
196
+ {
197
+ "max-lg:h-0": currentView === "preview",
198
+ }
199
+ )}
200
+ >
201
+ <Tabs />
202
+ <div
203
+ onClick={(e) => {
204
+ if (isAiWorking) {
205
+ e.preventDefault();
206
+ e.stopPropagation();
207
+ toast.warn("Please wait for the AI to finish working.");
208
+ }
209
+ }}
210
+ >
211
+ <Editor
212
+ language="html"
213
+ theme="vs-dark"
214
+ className={classNames(
215
+ "h-[calc(100dvh-90px)] lg:h-[calc(100dvh-96px)]",
216
+ {
217
+ "pointer-events-none": isAiWorking,
218
+ }
219
+ )}
220
+ value={html}
221
+ onValidate={(markers) => {
222
+ if (markers?.length > 0) {
223
+ setError(true);
224
+ }
225
+ }}
226
+ onChange={(value) => {
227
+ const newValue = value ?? "";
228
+ setHtml(newValue);
229
+ setError(false);
230
+ }}
231
+ onMount={(editor) => (editorRef.current = editor)}
232
+ />
233
+ </div>
234
+ <AskAI
235
+ html={html}
236
+ setHtml={setHtml}
237
+ isAiWorking={isAiWorking}
238
+ setisAiWorking={setisAiWorking}
239
+ setView={setCurrentView}
240
+ onScrollToBottom={() => {
241
+ editorRef.current?.revealLine(
242
+ editorRef.current?.getModel()?.getLineCount() ?? 0
243
+ );
244
+ }}
245
+ />
246
+ </div>
247
+ <div
248
+ ref={resizer}
249
+ className="bg-gray-700 hover:bg-blue-500 w-2 cursor-col-resize h-[calc(100dvh-53px)] max-lg:hidden"
250
+ />
251
+ <Preview
252
+ html={html}
253
+ isResizing={isResizing}
254
+ isAiWorking={isAiWorking}
255
+ ref={preview}
256
+ setView={setCurrentView}
257
+ />
258
+ </main>
259
+ </div>
260
+ );
261
+ }
262
+
263
+ export default App;
src/components/ask-ai/ask-ai.tsx ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { useState } from "react";
3
+ import { RiSparkling2Fill } from "react-icons/ri";
4
+ import { GrSend } from "react-icons/gr";
5
+ import classNames from "classnames";
6
+ import { toast } from "react-toastify";
7
+ import { useLocalStorage } from "react-use";
8
+ import { MdPreview } from "react-icons/md";
9
+
10
+ import Login from "../login/login";
11
+ import { defaultHTML } from "./../../../utils/consts";
12
+ import SuccessSound from "./../../assets/success.mp3";
13
+ import Settings from "../settings/settings";
14
+ import ProModal from "../pro-modal/pro-modal";
15
+ // import SpeechPrompt from "../speech-prompt/speech-prompt";
16
+
17
+ function AskAI({
18
+ html,
19
+ setHtml,
20
+ onScrollToBottom,
21
+ isAiWorking,
22
+ setisAiWorking,
23
+ setView,
24
+ }: {
25
+ html: string;
26
+ setHtml: (html: string) => void;
27
+ onScrollToBottom: () => void;
28
+ isAiWorking: boolean;
29
+ setView: React.Dispatch<React.SetStateAction<"editor" | "preview">>;
30
+ setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
31
+ }) {
32
+ const [open, setOpen] = useState(false);
33
+ const [prompt, setPrompt] = useState("");
34
+ const [hasAsked, setHasAsked] = useState(false);
35
+ const [previousPrompt, setPreviousPrompt] = useState("");
36
+ const [provider, setProvider] = useLocalStorage("provider", "auto");
37
+ const [openProvider, setOpenProvider] = useState(false);
38
+ const [providerError, setProviderError] = useState("");
39
+ const [openProModal, setOpenProModal] = useState(false);
40
+
41
+ const audio = new Audio(SuccessSound);
42
+ audio.volume = 0.5;
43
+
44
+ const callAi = async () => {
45
+ if (isAiWorking || !prompt.trim()) return;
46
+ setisAiWorking(true);
47
+ setProviderError("");
48
+
49
+ let contentResponse = "";
50
+ let lastRenderTime = 0;
51
+ try {
52
+ const request = await fetch("/api/ask-ai", {
53
+ method: "POST",
54
+ body: JSON.stringify({
55
+ prompt,
56
+ provider,
57
+ ...(html === defaultHTML ? {} : { html }),
58
+ ...(previousPrompt ? { previousPrompt } : {}),
59
+ }),
60
+ headers: {
61
+ "Content-Type": "application/json",
62
+ },
63
+ });
64
+ if (request && request.body) {
65
+ if (!request.ok) {
66
+ const res = await request.json();
67
+ if (res.openLogin) {
68
+ setOpen(true);
69
+ } else if (res.openSelectProvider) {
70
+ setOpenProvider(true);
71
+ setProviderError(res.message);
72
+ } else if (res.openProModal) {
73
+ setOpenProModal(true);
74
+ } else {
75
+ toast.error(res.message);
76
+ }
77
+ setisAiWorking(false);
78
+ return;
79
+ }
80
+ const reader = request.body.getReader();
81
+ const decoder = new TextDecoder("utf-8");
82
+
83
+ const read = async () => {
84
+ const { done, value } = await reader.read();
85
+ if (done) {
86
+ toast.success("AI responded successfully");
87
+ setPrompt("");
88
+ setPreviousPrompt(prompt);
89
+ setisAiWorking(false);
90
+ setHasAsked(true);
91
+ audio.play();
92
+ setView("preview");
93
+
94
+ // Now we have the complete HTML including </html>, so set it to be sure
95
+ const finalDoc = contentResponse.match(
96
+ /<!DOCTYPE html>[\s\S]*<\/html>/
97
+ )?.[0];
98
+ if (finalDoc) {
99
+ setHtml(finalDoc);
100
+ }
101
+
102
+ return;
103
+ }
104
+
105
+ const chunk = decoder.decode(value, { stream: true });
106
+ contentResponse += chunk;
107
+ const newHtml = contentResponse.match(/<!DOCTYPE html>[\s\S]*/)?.[0];
108
+ if (newHtml) {
109
+ // Force-close the HTML tag so the iframe doesn't render half-finished markup
110
+ let partialDoc = newHtml;
111
+ if (!partialDoc.includes("</html>")) {
112
+ partialDoc += "\n</html>";
113
+ }
114
+
115
+ // Throttle the re-renders to avoid flashing/flicker
116
+ const now = Date.now();
117
+ if (now - lastRenderTime > 300) {
118
+ setHtml(partialDoc);
119
+ lastRenderTime = now;
120
+ }
121
+
122
+ if (partialDoc.length > 200) {
123
+ onScrollToBottom();
124
+ }
125
+ }
126
+ read();
127
+ };
128
+
129
+ read();
130
+ }
131
+
132
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
133
+ } catch (error: any) {
134
+ setisAiWorking(false);
135
+ toast.error(error.message);
136
+ if (error.openLogin) {
137
+ setOpen(true);
138
+ }
139
+ }
140
+ };
141
+
142
+ return (
143
+ <div
144
+ className={`bg-gray-950 rounded-xl py-2 lg:py-2.5 pl-3.5 lg:pl-4 pr-2 lg:pr-2.5 absolute lg:sticky bottom-3 left-3 lg:bottom-4 lg:left-4 w-[calc(100%-1.5rem)] lg:w-[calc(100%-2rem)] z-10 group ${
145
+ isAiWorking ? "animate-pulse" : ""
146
+ }`}
147
+ >
148
+ {defaultHTML !== html && (
149
+ <button
150
+ className="bg-white lg:hidden -translate-y-[calc(100%+8px)] absolute left-0 top-0 shadow-md text-gray-950 text-xs font-medium py-2 px-3 lg:px-4 rounded-lg flex items-center gap-2 border border-gray-100 hover:brightness-150 transition-all duration-100 cursor-pointer"
151
+ onClick={() => setView("preview")}
152
+ >
153
+ <MdPreview className="text-sm" />
154
+ View Preview
155
+ </button>
156
+ )}
157
+ <div className="w-full relative flex items-center justify-between">
158
+ <RiSparkling2Fill className="text-lg lg:text-xl text-gray-500 group-focus-within:text-pink-500" />
159
+ <input
160
+ type="text"
161
+ disabled={isAiWorking}
162
+ className="w-full bg-transparent max-lg:text-sm outline-none px-3 text-white placeholder:text-gray-500 font-code"
163
+ placeholder={
164
+ hasAsked ? "What do you want to ask AI next?" : "Ask AI anything..."
165
+ }
166
+ value={prompt}
167
+ onChange={(e) => setPrompt(e.target.value)}
168
+ onKeyDown={(e) => {
169
+ if (e.key === "Enter") {
170
+ callAi();
171
+ }
172
+ }}
173
+ />
174
+ <div className="flex items-center justify-end gap-2">
175
+ {/* <SpeechPrompt setPrompt={setPrompt} /> */}
176
+ <Settings
177
+ provider={provider as string}
178
+ onChange={setProvider}
179
+ open={openProvider}
180
+ error={providerError}
181
+ onClose={setOpenProvider}
182
+ />
183
+ <button
184
+ disabled={isAiWorking}
185
+ 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"
186
+ onClick={callAi}
187
+ >
188
+ <GrSend className="-translate-x-[1px]" />
189
+ </button>
190
+ </div>
191
+ </div>
192
+ <div
193
+ className={classNames(
194
+ "h-screen w-screen bg-black/20 fixed left-0 top-0 z-10",
195
+ {
196
+ "opacity-0 pointer-events-none": !open,
197
+ }
198
+ )}
199
+ onClick={() => setOpen(false)}
200
+ ></div>
201
+ <div
202
+ className={classNames(
203
+ "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",
204
+ {
205
+ "opacity-0 pointer-events-none": !open,
206
+ }
207
+ )}
208
+ >
209
+ <Login html={html}>
210
+ <p className="text-gray-500 text-sm mb-3">
211
+ You reached the limit of free AI usage. Please login to continue.
212
+ </p>
213
+ </Login>
214
+ </div>
215
+ <ProModal
216
+ html={html}
217
+ open={openProModal}
218
+ onClose={() => setOpenProModal(false)}
219
+ />
220
+ </div>
221
+ );
222
+ }
223
+
224
+ export default AskAI;
src/components/deploy-button/deploy-button.tsx ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import { FaPowerOff } from "react-icons/fa6";
6
+
7
+ import SpaceIcon from "@/assets/space.svg";
8
+ import Loading from "../loading/loading";
9
+ import Login from "../login/login";
10
+ import { Auth } from "./../../../utils/types";
11
+
12
+ const MsgToast = ({ url }: { url: string }) => (
13
+ <div className="w-full flex items-center justify-center gap-3">
14
+ Your space is live!
15
+ <button
16
+ className="bg-black text-sm block text-white rounded-md px-3 py-1.5 hover:bg-gray-900 cursor-pointer"
17
+ onClick={() => {
18
+ window.open(url, "_blank");
19
+ }}
20
+ >
21
+ See Space
22
+ </button>
23
+ </div>
24
+ );
25
+
26
+ function DeployButton({
27
+ html,
28
+ error = false,
29
+ auth,
30
+ }: {
31
+ html: string;
32
+ error: boolean;
33
+ auth?: Auth;
34
+ }) {
35
+ const [open, setOpen] = useState(false);
36
+ const [loading, setLoading] = useState(false);
37
+ const [path, setPath] = useState<string | undefined>(undefined);
38
+
39
+ const [config, setConfig] = useState({
40
+ title: "",
41
+ });
42
+
43
+ const createSpace = async () => {
44
+ setLoading(true);
45
+
46
+ try {
47
+ const request = await fetch("/api/deploy", {
48
+ method: "POST",
49
+ body: JSON.stringify({
50
+ title: config.title,
51
+ path,
52
+ html,
53
+ }),
54
+ headers: {
55
+ "Content-Type": "application/json",
56
+ },
57
+ });
58
+ const response = await request.json();
59
+ if (response.ok) {
60
+ toast.success(
61
+ <MsgToast
62
+ url={`https://huggingface.co/spaces/${response.path ?? path}`}
63
+ />,
64
+ {
65
+ autoClose: 10000,
66
+ }
67
+ );
68
+ setPath(response.path);
69
+ } else {
70
+ toast.error(response.message);
71
+ }
72
+ } catch (err: any) {
73
+ toast.error(err.message);
74
+ } finally {
75
+ setLoading(false);
76
+ setOpen(false);
77
+ }
78
+ };
79
+
80
+ return (
81
+ <div className="relative flex items-center justify-end">
82
+ {auth && (
83
+ <>
84
+ <button
85
+ className="mr-2 cursor-pointer"
86
+ onClick={() => {
87
+ if (confirm("Are you sure you want to log out?")) {
88
+ // go to /auth/logout page
89
+ window.location.href = "/auth/logout";
90
+ }
91
+ }}
92
+ >
93
+ <FaPowerOff className="text-lg text-red-500" />
94
+ </button>
95
+ <p className="mr-3 text-xs lg:text-sm text-gray-300">
96
+ <span className="max-lg:hidden">Connected as </span>
97
+ <a
98
+ href={`https://huggingface.co/${auth.preferred_username}`}
99
+ target="_blank"
100
+ className="underline hover:text-white"
101
+ >
102
+ {auth.preferred_username}
103
+ </a>
104
+ </p>
105
+ </>
106
+ )}
107
+ <button
108
+ className={classNames(
109
+ "relative cursor-pointer flex-none flex items-center justify-center rounded-md text-xs lg:text-sm font-semibold leading-5 lg:leading-6 py-1.5 px-5 hover:bg-pink-400 text-white shadow-sm dark:shadow-highlight/20",
110
+ {
111
+ "bg-pink-400": open,
112
+ "bg-pink-500": !open,
113
+ }
114
+ )}
115
+ onClick={() => setOpen(!open)}
116
+ >
117
+ {path ? "Update Space" : "Deploy to Space"}
118
+ </button>
119
+ <div
120
+ className={classNames(
121
+ "h-screen w-screen bg-black/20 fixed left-0 top-0 z-10",
122
+ {
123
+ "opacity-0 pointer-events-none": !open,
124
+ }
125
+ )}
126
+ onClick={() => setOpen(false)}
127
+ ></div>
128
+ <div
129
+ className={classNames(
130
+ "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",
131
+ {
132
+ "opacity-0 pointer-events-none": !open,
133
+ }
134
+ )}
135
+ >
136
+ {!auth ? (
137
+ <Login html={html}>
138
+ <p className="text-gray-500 text-sm mb-3">
139
+ Host this project for free and share it with your friends.
140
+ </p>
141
+ </Login>
142
+ ) : (
143
+ <>
144
+ <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">
145
+ <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">
146
+ <img src={SpaceIcon} alt="Space Icon" className="size-4" />
147
+ Space
148
+ </span>
149
+ Configure Deployment
150
+ </header>
151
+ <main className="px-4 pt-3 pb-4 space-y-3">
152
+ <p className="text-xs text-amber-600 bg-amber-500/10 rounded-md p-2">
153
+ {path ? (
154
+ <span>
155
+ Your space is live at{" "}
156
+ <a
157
+ href={`https://huggingface.co/spaces/${path}`}
158
+ target="_blank"
159
+ className="underline hover:text-amber-700"
160
+ >
161
+ huggingface.co/{path}
162
+ </a>
163
+ . You can update it by deploying again.
164
+ </span>
165
+ ) : (
166
+ "Deploy your project to a space on the Hub. Spaces are a way to share your project with the world."
167
+ )}
168
+ </p>
169
+ {!path && (
170
+ <label className="block">
171
+ <p className="text-gray-600 text-sm font-medium mb-1.5">
172
+ Space Title
173
+ </p>
174
+ <input
175
+ type="text"
176
+ value={config.title}
177
+ className="mr-2 border rounded-md px-3 py-1.5 border-gray-300 w-full text-sm"
178
+ placeholder="My Awesome Space"
179
+ onChange={(e) =>
180
+ setConfig({ ...config, title: e.target.value })
181
+ }
182
+ />
183
+ </label>
184
+ )}
185
+ {error && (
186
+ <p className="text-red-500 text-xs bg-red-500/10 rounded-md p-2">
187
+ Your code has errors. Fix them before deploying.
188
+ </p>
189
+ )}
190
+ <div className="pt-2 text-right">
191
+ <button
192
+ disabled={error || loading || !config.title}
193
+ 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"
194
+ onClick={createSpace}
195
+ >
196
+ {path ? "Update Space" : "Create Space"}
197
+ {loading && <Loading />}
198
+ </button>
199
+ </div>
200
+ </main>
201
+ </>
202
+ )}
203
+ </div>
204
+ </div>
205
+ );
206
+ }
207
+
208
+ export default DeployButton;
src/components/header/header.tsx ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ReactNode } from "react";
2
+ import { MdAdd } from "react-icons/md";
3
+
4
+ import Logo from "@/assets/logo.svg";
5
+
6
+ function Header({
7
+ onReset,
8
+ children,
9
+ }: {
10
+ onReset: () => void;
11
+ children?: ReactNode;
12
+ }) {
13
+ return (
14
+ <header className="border-b border-gray-900 bg-gray-950 px-3 lg:px-6 py-2 flex justify-between items-center sticky top-0 z-20">
15
+ <div className="flex items-center justify-start gap-3">
16
+ <h1 className="text-white text-lg lg:text-xl font-bold flex items-center justify-start">
17
+ <img
18
+ src={Logo}
19
+ alt="DeepSite Logo"
20
+ className="size-6 lg:size-8 mr-2"
21
+ />
22
+ DeepSite
23
+ </h1>
24
+ <p className="text-gray-700 max-md:hidden">|</p>
25
+ <button
26
+ className="max-md:hidden relative cursor-pointer flex-none flex items-center justify-center rounded-md text-xs font-semibold leading-4 py-1.5 px-3 hover:bg-gray-700 text-gray-100 shadow-sm dark:shadow-highlight/20 bg-gray-800"
27
+ onClick={onReset}
28
+ >
29
+ <MdAdd className="mr-1 text-base" />
30
+ New
31
+ </button>
32
+ <p className="text-gray-500 text-sm max-md:hidden">
33
+ Imagine and Share in 1-Click
34
+ </p>
35
+ </div>
36
+ {children}
37
+ </header>
38
+ );
39
+ }
40
+
41
+ export default Header;
src/components/load-button/load-button.tsx ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import classNames from "classnames";
2
+ import { useState } from "react";
3
+ import { toast } from "react-toastify";
4
+
5
+ import SpaceIcon from "@/assets/space.svg";
6
+ import Loading from "../loading/loading";
7
+ import { Auth } from "../../../utils/types";
8
+
9
+ function LoadButton({
10
+ auth,
11
+ setHtml,
12
+ }: {
13
+ auth?: Auth;
14
+ setHtml: (html: string) => void;
15
+ }) {
16
+ const [open, setOpen] = useState(false);
17
+ const [loading, setLoading] = useState(false);
18
+ const [error, setError] = useState(false);
19
+ const [path, setPath] = useState<string | undefined>(undefined);
20
+
21
+ const loadSpace = async () => {
22
+ setLoading(true);
23
+ try {
24
+ const res = await fetch(`/api/remix/${path}`);
25
+ const data = await res.json();
26
+ if (res.ok) {
27
+ if (data.html) {
28
+ setHtml(data.html);
29
+ toast.success("Project loaded successfully.");
30
+ }
31
+ setOpen(false);
32
+ } else {
33
+ toast.error(data.message);
34
+ setError(data.message);
35
+ }
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ } catch (error: any) {
38
+ toast.error(error.message);
39
+ setError(error.message);
40
+ }
41
+ setLoading(false);
42
+ };
43
+
44
+ return (
45
+ <div
46
+ className={classNames("max-md:hidden", {
47
+ "border-r border-gray-700 pr-5": auth,
48
+ })}
49
+ >
50
+ <p
51
+ className="underline hover:text-white cursor-pointer text-xs lg:text-sm text-gray-300"
52
+ onClick={() => setOpen(!open)}
53
+ >
54
+ Load project
55
+ </p>
56
+ <div
57
+ className={classNames(
58
+ "h-screen w-screen bg-black/20 fixed left-0 top-0 z-10",
59
+ {
60
+ "opacity-0 pointer-events-none": !open,
61
+ }
62
+ )}
63
+ onClick={() => setOpen(false)}
64
+ ></div>
65
+ <div
66
+ className={classNames(
67
+ "absolute top-[calc(100%+8px)] right-2 z-10 w-80 bg-white border border-gray-200 rounded-lg shadow-lg transition-all duration-75 overflow-hidden",
68
+ {
69
+ "opacity-0 pointer-events-none": !open,
70
+ }
71
+ )}
72
+ >
73
+ <>
74
+ <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">
75
+ <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">
76
+ <img src={SpaceIcon} alt="Space Icon" className="size-4" />
77
+ Space
78
+ </span>
79
+ Load Project
80
+ </header>
81
+ <main className="px-4 pt-3 pb-4 space-y-3">
82
+ <label className="block">
83
+ <p className="text-gray-600 text-sm font-medium mb-1.5">
84
+ Space URL
85
+ </p>
86
+ <input
87
+ type="text"
88
+ value={path}
89
+ className="mr-2 border rounded-md px-3 py-1.5 border-gray-300 w-full text-sm"
90
+ placeholder="https://huggingface.co/spaces/username/space-name"
91
+ onChange={(e) => setPath(e.target.value)}
92
+ onFocus={() => setError(false)}
93
+ onBlur={(e) => {
94
+ const pathParts = e.target.value.split("/");
95
+ setPath(
96
+ `${pathParts[pathParts.length - 2]}/${
97
+ pathParts[pathParts.length - 1]
98
+ }`
99
+ );
100
+ setError(false);
101
+ }}
102
+ />
103
+ </label>
104
+ {error && (
105
+ <p className="text-red-500 text-xs bg-red-500/10 rounded-md p-2 break-all">
106
+ {error}
107
+ </p>
108
+ )}
109
+ <div className="pt-2 text-right">
110
+ <button
111
+ disabled={error || loading || !path}
112
+ 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"
113
+ onClick={loadSpace}
114
+ >
115
+ Load Project
116
+ {loading && <Loading />}
117
+ </button>
118
+ </div>
119
+ </main>
120
+ </>
121
+ </div>
122
+ </div>
123
+ );
124
+ }
125
+ export default LoadButton;
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,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useLocalStorage } from "react-use";
2
+ import { defaultHTML } from "./../../../utils/consts";
3
+
4
+ function Login({
5
+ html,
6
+ children,
7
+ }: {
8
+ html?: string;
9
+ children?: React.ReactNode;
10
+ }) {
11
+ const [, setStorage] = useLocalStorage("html_content");
12
+
13
+ const handleClick = () => {
14
+ if (html !== defaultHTML) {
15
+ setStorage(html);
16
+ }
17
+ };
18
+
19
+ return (
20
+ <>
21
+ <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">
22
+ <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">
23
+ REQUIRED
24
+ </span>
25
+ Login with Hugging Face
26
+ </header>
27
+ <main className="px-4 py-4 space-y-3">
28
+ {children}
29
+ <a href="/api/login" onClick={handleClick}>
30
+ <img
31
+ src="https://huggingface.co/datasets/huggingface/badges/resolve/main/sign-in-with-huggingface-lg-dark.svg"
32
+ alt="Sign in with Hugging Face"
33
+ className="mx-auto"
34
+ />
35
+ </a>
36
+ </main>
37
+ </>
38
+ );
39
+ }
40
+
41
+ export default Login;
src/components/preview/preview.tsx ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import classNames from "classnames";
2
+ import { useRef } from "react";
3
+ import { TbReload } from "react-icons/tb";
4
+ import { toast } from "react-toastify";
5
+ import { FaLaptopCode } from "react-icons/fa6";
6
+ import { defaultHTML } from "../../../utils/consts";
7
+
8
+ function Preview({
9
+ html,
10
+ isResizing,
11
+ isAiWorking,
12
+ setView,
13
+ ref,
14
+ }: {
15
+ html: string;
16
+ isResizing: boolean;
17
+ isAiWorking: boolean;
18
+ setView: React.Dispatch<React.SetStateAction<"editor" | "preview">>;
19
+ ref: React.RefObject<HTMLDivElement | null>;
20
+ }) {
21
+ const iframeRef = useRef<HTMLIFrameElement | null>(null);
22
+
23
+ const handleRefreshIframe = () => {
24
+ if (iframeRef.current) {
25
+ const iframe = iframeRef.current;
26
+ const content = iframe.srcdoc;
27
+ iframe.srcdoc = "";
28
+ setTimeout(() => {
29
+ iframe.srcdoc = content;
30
+ }, 10);
31
+ }
32
+ };
33
+
34
+ return (
35
+ <div
36
+ ref={ref}
37
+ className="w-full border-l border-gray-900 bg-white h-[calc(100dvh-49px)] lg:h-[calc(100dvh-53px)] relative"
38
+ onClick={(e) => {
39
+ if (isAiWorking) {
40
+ e.preventDefault();
41
+ e.stopPropagation();
42
+ toast.warn("Please wait for the AI to finish working.");
43
+ }
44
+ }}
45
+ >
46
+ <iframe
47
+ ref={iframeRef}
48
+ title="output"
49
+ className={classNames("w-full h-full select-none", {
50
+ "pointer-events-none": isResizing || isAiWorking,
51
+ })}
52
+ srcDoc={html}
53
+ />
54
+ <div className="flex items-center justify-start gap-3 absolute bottom-3 lg:bottom-5 max-lg:left-3 lg:right-5">
55
+ <button
56
+ className="lg:hidden bg-gray-950 shadow-md text-white text-xs lg:text-sm font-medium py-2 px-3 lg:px-4 rounded-lg flex items-center gap-2 border border-gray-900 hover:brightness-150 transition-all duration-100 cursor-pointer"
57
+ onClick={() => setView("editor")}
58
+ >
59
+ <FaLaptopCode className="text-sm" />
60
+ Hide preview
61
+ </button>
62
+ {html === defaultHTML && (
63
+ <a
64
+ href="https://huggingface.co/spaces/victor/deepsite-gallery"
65
+ target="_blank"
66
+ className="bg-gray-200 text-gray-950 text-xs lg:text-sm font-medium py-2 px-3 lg:px-4 rounded-lg flex items-center gap-2 border border-gray-200 hover:bg-gray-300 transition-all duration-100 cursor-pointer"
67
+ >
68
+ 🖼️ <span>DeepSite Gallery</span>
69
+ </a>
70
+ )}
71
+ {!isAiWorking && (
72
+ <button
73
+ className="bg-white lg:bg-gray-950 shadow-md text-gray-950 lg:text-white text-xs lg:text-sm font-medium py-2 px-3 lg:px-4 rounded-lg flex items-center gap-2 border border-gray-100 lg:border-gray-900 hover:brightness-150 transition-all duration-100 cursor-pointer"
74
+ onClick={handleRefreshIframe}
75
+ >
76
+ <TbReload className="text-sm" />
77
+ Refresh Preview
78
+ </button>
79
+ )}
80
+ </div>
81
+ </div>
82
+ );
83
+ }
84
+
85
+ export default Preview;
src/components/pro-modal/pro-modal.tsx ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import classNames from "classnames";
2
+ import { useLocalStorage } from "react-use";
3
+ import { defaultHTML } from "../../../utils/consts";
4
+
5
+ function ProModal({
6
+ open,
7
+ html,
8
+ onClose,
9
+ }: {
10
+ open: boolean;
11
+ html: string;
12
+ onClose: React.Dispatch<React.SetStateAction<boolean>>;
13
+ }) {
14
+ const [, setStorage] = useLocalStorage("html_content");
15
+
16
+ const handleProClick = () => {
17
+ if (html !== defaultHTML) {
18
+ setStorage(html);
19
+ }
20
+ };
21
+
22
+ return (
23
+ <>
24
+ <div
25
+ className={classNames(
26
+ "h-screen w-screen bg-black/20 fixed left-0 top-0 z-40",
27
+ {
28
+ "opacity-0 pointer-events-none": !open,
29
+ }
30
+ )}
31
+ onClick={() => onClose(false)}
32
+ ></div>
33
+ <div
34
+ className={classNames(
35
+ "absolute top-0 -translate-y-[calc(100%+16px)] right-0 z-40 w-96 bg-white border border-gray-200 rounded-lg shadow-lg transition-all duration-75 overflow-hidden",
36
+ {
37
+ "opacity-0 pointer-events-none": !open,
38
+ }
39
+ )}
40
+ >
41
+ <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">
42
+ <span className="bg-linear-to-br shadow-green-500/10 dark:shadow-green-500/20 inline-block -skew-x-12 border border-gray-200 from-pink-300 via-green-200 to-yellow-200 text-xs font-bold text-black shadow-lg dark:from-pink-500 dark:via-green-500 dark:to-yellow-500 dark:text-black rounded-lg px-2.5 py-0.5 ">
43
+ PRO
44
+ </span>
45
+ Your free inference quota is exhausted
46
+ </header>
47
+ <main className="px-4 pt-3 pb-4">
48
+ <p className="text-gray-950 text-sm font-semibold flex items-center justify-between">
49
+ Upgrade to a PRO account to activate Inference Providers and
50
+ continue using DeepSite.
51
+ </p>
52
+ <p className="text-sm text-pretty text-gray-500 mt-2">
53
+ It also unlocks thousands of Space apps powered by ZeroGPU for 3d,
54
+ audio, music, video and more!
55
+ </p>
56
+ <a
57
+ href="https://huggingface.co/subscribe/pro"
58
+ target="_blank"
59
+ className="mt-4 bg-black text-white cursor-pointer rounded-full py-2 px-3 text-sm font-medium w-full block text-center hover:bg-gray-800 transition duration-200 ease-in-out"
60
+ onClick={handleProClick}
61
+ >
62
+ Subscribe to PRO ($9/month)
63
+ </a>
64
+ </main>
65
+ </div>
66
+ </>
67
+ );
68
+ }
69
+
70
+ export default ProModal;
src/components/settings/settings.tsx ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import classNames from "classnames";
3
+
4
+ import { PiGearSixFill } from "react-icons/pi";
5
+ // @ts-expect-error not needed
6
+ import { PROVIDERS } from "./../../../utils/providers";
7
+
8
+ function Settings({
9
+ open,
10
+ onClose,
11
+ provider,
12
+ error,
13
+ onChange,
14
+ }: {
15
+ open: boolean;
16
+ provider: string;
17
+ error?: string;
18
+ onClose: React.Dispatch<React.SetStateAction<boolean>>;
19
+ onChange: (provider: string) => void;
20
+ }) {
21
+ return (
22
+ <div className="">
23
+ <button
24
+ className="relative overflow-hidden cursor-pointer flex-none flex items-center justify-center rounded-full text-base font-semibold size-8 text-center bg-gray-800 hover:bg-gray-700 text-gray-100 shadow-sm dark:shadow-highlight/20"
25
+ onClick={() => {
26
+ onClose((prev) => !prev);
27
+ }}
28
+ >
29
+ <PiGearSixFill />
30
+ </button>
31
+ <div
32
+ className={classNames(
33
+ "h-screen w-screen bg-black/20 fixed left-0 top-0 z-40",
34
+ {
35
+ "opacity-0 pointer-events-none": !open,
36
+ }
37
+ )}
38
+ onClick={() => onClose(false)}
39
+ ></div>
40
+ <div
41
+ className={classNames(
42
+ "absolute top-0 -translate-y-[calc(100%+16px)] right-0 z-40 w-96 bg-white border border-gray-200 rounded-lg shadow-lg transition-all duration-75 overflow-hidden",
43
+ {
44
+ "opacity-0 pointer-events-none": !open,
45
+ }
46
+ )}
47
+ >
48
+ <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">
49
+ <span className="text-xs bg-blue-500/10 text-blue-500 rounded-full pl-1.5 pr-2.5 py-0.5 flex items-center justify-start gap-1.5">
50
+ Provider
51
+ </span>
52
+ Customize Settings
53
+ </header>
54
+ <main className="px-4 pt-3 pb-4 space-y-4">
55
+ {/* toggle using tailwind css */}
56
+ <div>
57
+ <div className="flex items-center justify-between">
58
+ <p className="text-gray-800 text-sm font-medium flex items-center justify-between">
59
+ Use auto-provider
60
+ </p>
61
+ <div
62
+ className={classNames(
63
+ "bg-gray-200 rounded-full w-10 h-6 flex items-center justify-between p-1 cursor-pointer transition-all duration-200",
64
+ {
65
+ "!bg-blue-500": provider === "auto",
66
+ }
67
+ )}
68
+ onClick={() => {
69
+ onChange(provider === "auto" ? "fireworks-ai" : "auto");
70
+ }}
71
+ >
72
+ <div
73
+ className={classNames(
74
+ "w-4 h-4 rounded-full shadow-md transition-all duration-200 bg-white",
75
+ {
76
+ "translate-x-4": provider === "auto",
77
+ }
78
+ )}
79
+ />
80
+ </div>
81
+ </div>
82
+ <p className="text-xs text-gray-500 mt-2">
83
+ We'll automatically select the best provider for you based on your
84
+ prompt.
85
+ </p>
86
+ </div>
87
+ {error !== "" && (
88
+ <p className="text-red-500 text-sm font-medium mb-2 flex items-center justify-between bg-red-500/10 p-2 rounded-md">
89
+ {error}
90
+ </p>
91
+ )}
92
+ <label className="block">
93
+ <p className="text-gray-800 text-sm font-medium mb-2 flex items-center justify-between">
94
+ Inference Provider
95
+ </p>
96
+ <div className="grid grid-cols-2 gap-1.5">
97
+ {Object.keys(PROVIDERS).map((id: string) => (
98
+ <div
99
+ key={id}
100
+ className={classNames(
101
+ "text-gray-600 text-sm font-medium cursor-pointer border p-2 rounded-md flex items-center justify-start gap-2",
102
+ {
103
+ "bg-blue-500/10 border-blue-500/15 text-blue-500":
104
+ id === provider,
105
+ "hover:bg-gray-100 border-gray-100": id !== provider,
106
+ }
107
+ )}
108
+ onClick={() => {
109
+ onChange(id);
110
+ }}
111
+ >
112
+ <img
113
+ src={`/providers/${id}.svg`}
114
+ alt={PROVIDERS[id].name}
115
+ className="size-5"
116
+ />
117
+ {PROVIDERS[id].name}
118
+ </div>
119
+ ))}
120
+ </div>
121
+ </label>
122
+ </main>
123
+ </div>
124
+ </div>
125
+ );
126
+ }
127
+ export default Settings;
src/components/speech-prompt/speech-prompt.tsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import classNames from "classnames";
2
+ import { FaMicrophone } from "react-icons/fa";
3
+ import SpeechRecognition, {
4
+ useSpeechRecognition,
5
+ } from "react-speech-recognition";
6
+ import { useUpdateEffect } from "react-use";
7
+
8
+ function SpeechPrompt({
9
+ setPrompt,
10
+ }: {
11
+ setPrompt: React.Dispatch<React.SetStateAction<string>>;
12
+ }) {
13
+ const {
14
+ transcript,
15
+ listening,
16
+ browserSupportsSpeechRecognition,
17
+ resetTranscript,
18
+ } = useSpeechRecognition();
19
+
20
+ const startListening = () =>
21
+ SpeechRecognition.startListening({ continuous: true });
22
+
23
+ useUpdateEffect(() => {
24
+ if (transcript) setPrompt(transcript);
25
+ }, [transcript]);
26
+
27
+ useUpdateEffect(() => {
28
+ if (!listening) resetTranscript();
29
+ }, [listening]);
30
+
31
+ if (!browserSupportsSpeechRecognition) {
32
+ return null;
33
+ }
34
+
35
+ return (
36
+ <button
37
+ className={classNames(
38
+ "flex cursor-pointer flex-none items-center justify-center rounded-full text-sm font-semibold size-8 text-center bg-gray-800 hover:bg-gray-700 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",
39
+ {
40
+ "animate-pulse !bg-orange-500": listening,
41
+ }
42
+ )}
43
+ onTouchStart={startListening}
44
+ onMouseDown={startListening}
45
+ onTouchEnd={SpeechRecognition.stopListening}
46
+ onMouseUp={SpeechRecognition.stopListening}
47
+ >
48
+ <FaMicrophone className="text-base" />
49
+ </button>
50
+ );
51
+ }
52
+
53
+ export default SpeechPrompt;
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-4 lg: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/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", "utils/consts.ts", "utils/types.ts"]
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
+ }
utils/colors.js ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ export const COLORS = [
2
+ "red",
3
+ "blue",
4
+ "green",
5
+ "yellow",
6
+ "purple",
7
+ "pink",
8
+ "gray",
9
+ ];
utils/consts.ts ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ overflow: hidden;
13
+ height: 100dvh;
14
+ font-family: "Arial", sans-serif;
15
+ text-align: center;
16
+ }
17
+ .arrow {
18
+ position: absolute;
19
+ bottom: 32px;
20
+ left: 0px;
21
+ width: 100px;
22
+ transform: rotate(30deg);
23
+ }
24
+ h1 {
25
+ font-size: 50px;
26
+ }
27
+ h1 span {
28
+ color: #acacac;
29
+ font-size: 32px;
30
+ }
31
+ </style>
32
+ </head>
33
+ <body>
34
+ <h1>
35
+ <span>I'm ready to work,</span><br />
36
+ Ask me anything.
37
+ </h1>
38
+ <img src="https://enzostvs-deepsite.hf.space/arrow.svg" class="arrow" />
39
+ <script></script>
40
+ </body>
41
+ </html>
42
+ `;
utils/providers.js ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const PROVIDERS = {
2
+ "fireworks-ai": {
3
+ name: "Fireworks AI",
4
+ max_tokens: 131_000,
5
+ id: "fireworks-ai",
6
+ },
7
+ nebius: {
8
+ name: "Nebius AI Studio",
9
+ max_tokens: 131_000,
10
+ id: "nebius",
11
+ },
12
+ sambanova: {
13
+ name: "SambaNova",
14
+ max_tokens: 8_000,
15
+ id: "sambanova",
16
+ },
17
+ novita: {
18
+ name: "NovitaAI",
19
+ max_tokens: 16_000,
20
+ id: "novita",
21
+ },
22
+ // hyperbolic: {
23
+ // name: "Hyperbolic",
24
+ // max_tokens: 131_000,
25
+ // id: "hyperbolic",
26
+ // },
27
+ };
utils/types.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export interface Auth {
2
+ preferred_username: string;
3
+ picture: string;
4
+ name: string;
5
+ }
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
+ });