Spaces:
Runtime error
Runtime error
Upload 45 files
Browse files- .env.example +5 -0
- .gitignore +26 -0
- Dockerfile +22 -0
- README.md +15 -13
- eslint.config.js +28 -0
- index.html +29 -0
- middlewares/checkUser.js +10 -0
- module.d.ts +1 -0
- package-lock.json +0 -0
- package.json +48 -0
- public/arrow.svg +10 -0
- public/logo.svg +330 -0
- public/providers/fireworks-ai.svg +4 -0
- public/providers/hyperbolic.svg +7 -0
- public/providers/nebius.svg +4 -0
- public/providers/novita.svg +4 -0
- public/providers/sambanova.svg +5 -0
- server.js +399 -0
- src/assets/deepseek-color.svg +1 -0
- src/assets/index.css +9 -0
- src/assets/logo.svg +330 -0
- src/assets/space.svg +7 -0
- src/assets/success.mp3 +0 -0
- src/components/App.tsx +263 -0
- src/components/ask-ai/ask-ai.tsx +224 -0
- src/components/deploy-button/deploy-button.tsx +208 -0
- src/components/header/header.tsx +41 -0
- src/components/load-button/load-button.tsx +125 -0
- src/components/loading/loading.tsx +28 -0
- src/components/login/login.tsx +41 -0
- src/components/preview/preview.tsx +85 -0
- src/components/pro-modal/pro-modal.tsx +70 -0
- src/components/settings/settings.tsx +127 -0
- src/components/speech-prompt/speech-prompt.tsx +53 -0
- src/components/tabs/tabs.tsx +29 -0
- src/main.tsx +12 -0
- src/vite-env.d.ts +1 -0
- tsconfig.app.json +26 -0
- tsconfig.json +13 -0
- tsconfig.node.json +24 -0
- utils/colors.js +9 -0
- utils/consts.ts +42 -0
- utils/providers.js +27 -0
- utils/types.ts +5 -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,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:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
-
sdk:
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
|
|
|
|
|
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 |
+
});
|