R-Kentaren commited on
Commit
d5df1b1
·
verified ·
1 Parent(s): ca9bad9

Upload folder using huggingface_hub

Browse files
.dockerignore ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Exclude node_modules
2
+ node_modules
3
+
4
+ # Exclude logs
5
+ *.log
6
+
7
+ # Exclude temporary files
8
+ *.tmp
9
+ *.swp
10
+
11
+ # Exclude build artifacts
12
+ dist
13
+ build
14
+
15
+ # Exclude environment files
16
+ .env
17
+ .env.local
18
+
19
+ # Exclude Docker-related files
20
+ Dockerfile
21
+ docker-compose.yml
.env.example ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # openai base url
3
+ OPENAI_BASE_URL=https://xxxxxxx/v1
4
+ # openai model
5
+ OPENAI_MODEL=deepseek-v3
6
+ #openai api key
7
+ OPENAI_API_KEY=your_openai_api_key
8
+ # PORT
9
+ APP_PORT=5173
10
+
11
+
.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,10 +1,145 @@
1
- ---
2
- title: Deepsitegroq
3
- emoji: 🏆
4
- colorFrom: yellow
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -
2
+
3
+ # DeepSite 🚀
4
+
5
+ DeepSite 是一个基于 React + TypeScript + Vite 构建的智能应用生成器,集成了 Monaco Editor 和 Groq,提供强大的代码编辑和 AI 辅助功能,现在支持构建全栈网站,并使用 Groq 的 `openai/gpt-oss-20b` 模型。
6
+
7
+ ## 技术栈 💻
8
+
9
+ - **前端框架**: React 19
10
+ - **开发语言**: TypeScript 5.7
11
+ - **构建工具**: Vite 6
12
+ - **UI 框架**: Tailwind CSS 4
13
+ - **代码编辑器**: Monaco Editor
14
+ - **AI 集成**: Groq API (使用 `openai/gpt-oss-20b` 模型)
15
+ - **其他特性**:
16
+ - React Speech Recognition
17
+ - React Markdown
18
+ - React Toastify
19
+
20
+ ## 快速开始 🚀
21
+
22
+ ### 环境要求
23
+
24
+ - Node.js >= 16
25
+ - npm 或 yarn
26
+ - Docker(可选,用于容器化部署)
27
+
28
+ ### 本地开发
29
+
30
+ 1. 克隆仓库:
31
+
32
+ ```bash
33
+ git clone https://github.com/BF667/DeepSite-groq deepsite
34
+ cd deepsite
35
+ ```
36
+
37
+ 2. 安装依赖:
38
+
39
+ ```bash
40
+ npm install
41
+ ```
42
+
43
+ 3. 配置环境变量:
44
+
45
+ ```bash
46
+ cp .env.example .env
47
+ # 编辑 .env 文件,填入必要的配置信息
48
+ ```
49
+
50
+ 4. 启动开发服务器:
51
+
52
+ ```bash
53
+ npm run dev
54
+ ```
55
+
56
+ 5. 构建生产版本:
57
+
58
+ ```bash
59
+ npm run build
60
+ ```
61
+
62
+ ## Docker 启动 🐳
63
+
64
+ ### 构建镜像
65
+
66
+ ```bash
67
+ docker build -t my-deepsite .
68
+ ```
69
+
70
+ ### 启动容器
71
+
72
+ ```bash
73
+ docker run -d -p 5173:5173 \
74
+
75
+ my-deepsite
76
+ ```
77
+
78
+ ### 使用示例
79
+
80
+ 如果您想使用不同的端口(例如 8080),可以这样配置:
81
+
82
+ ```bash
83
+ docker run -d -p 8080:8080 \
84
+ -e APP_PORT=8080 \
85
+ -e GROQ_BASE_URL=https://api.groq.com/openai/v1/chat/completions \
86
+ -e GTOQ_API_KEY=ghp_xxxxxxxx \
87
+ -e GROQ_MODEL=openai/gpt-oss-20b \
88
+ my-deepsite
89
+ ```
90
+
91
+ ### 注意事项
92
+
93
+ - 确保 Docker 已正确安装并运行。
94
+ - 构建镜像前,确保当前目录包含有效的 Dockerfile。
95
+ - 请替换 `sk-or-v1-xxxxx` 为您的实际 API 密钥。
96
+ - 可根据需要调整端口映射和环境变量。
97
+
98
+ ## 环境变量可选参数 ⚙️
99
+
100
+ - **`GROQ_BASE_URL`**: API 的基础 URL(必填)
101
+ - **`GROQ_API_KEY`**: API 密钥(必填)
102
+ - **`GROQ_MODEL`**: 模型名称(必填)
103
+ - **`APP_PORT`**: 应用端口,默认为 `5173`(可选)
104
+
105
+ ## 项目结构 📁
106
+
107
+ ```
108
+ deepsite/
109
+ ├── src/
110
+ │ ├── components/ # React 组件
111
+ │ ├── config/ # 配置文件
112
+ │ ├── assets/ # 静态资源
113
+ │ └── main.tsx # 应用入口
114
+ ├── services/ # 后端服务
115
+ ├── middlewares/ # Express 中间件
116
+ ├── utils/ # 工具函数
117
+ ├── public/ # 公共资源
118
+ └── dist/ # 构建输出目录
119
+ ```
120
+
121
+ ## 开发命令 ⌨️
122
+
123
+ - `npm run dev` - 启动开发服务器
124
+ - `npm run build` - 构建生产版本
125
+ - `npm run preview` - 预览生产构建
126
+ - `npm run lint` - 运行 ESLint 检查
127
+ - `npm start` - 启动生产服务器
128
+
129
+ ## 环境变量配置 ⚙️
130
+
131
+ 在 `.env` 文件中配置以下环境变量:
132
+
133
+ ```env
134
+ VITE_APP_TITLE=DeepSite
135
+ GRPQ_API_KEY=your_api_key
136
+ PORT=5173
137
+ ```
138
+
139
+ ## 贡献指南 🤝
140
+
141
+ 1. Fork 本仓库
142
+ 2. 创建特性分支 (`git checkout -b feature/amazing-feature`)
143
+ 3. 提交更改 (`git commit -m 'Add some amazing feature'`)
144
+ 4. 推送到分支 (`git push origin feature/amazing-feature`)
145
+ 5. 创建 Pull Request
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>
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,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ "@monaco-editor/react": "^4.7.0",
15
+ "@tailwindcss/vite": "^4.0.15",
16
+ "@xenova/transformers": "^2.17.2",
17
+ "body-parser": "^1.20.3",
18
+ "classnames": "^2.5.1",
19
+ "dotenv": "^16.4.7",
20
+ "express": "^4.21.2",
21
+ "react": "^19.0.0",
22
+ "react-dom": "^19.0.0",
23
+ "react-icons": "^5.5.0",
24
+ "react-markdown": "^10.1.0",
25
+ "react-speech-recognition": "^4.0.0",
26
+ "react-toastify": "^11.0.5",
27
+ "react-use": "^17.6.0",
28
+ "tailwindcss": "^4.0.15"
29
+ },
30
+ "devDependencies": {
31
+ "@eslint/js": "^9.21.0",
32
+ "@types/express": "^5.0.1",
33
+ "@types/react": "^19.0.10",
34
+ "@types/react-dom": "^19.0.4",
35
+ "@types/react-speech-recognition": "^3.9.6",
36
+ "@vitejs/plugin-react": "^4.3.4",
37
+ "eslint": "^9.21.0",
38
+ "eslint-plugin-react-hooks": "^5.1.0",
39
+ "eslint-plugin-react-refresh": "^0.4.19",
40
+ "globals": "^15.15.0",
41
+ "typescript": "~5.7.2",
42
+ "typescript-eslint": "^8.24.1",
43
+ "vite": "^6.2.0"
44
+ }
45
+ }
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,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from "express";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import dotenv from "dotenv";
5
+ import bodyParser from "body-parser";
6
+
7
+ import { createChatCompletion } from "./services/groq.js";
8
+
9
+ // Load environment variables from .env file
10
+ dotenv.config();
11
+
12
+ const app = express();
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+
17
+ const PORT = process.env.APP_PORT || 5173;
18
+
19
+ app.use(bodyParser.json());
20
+ app.use(express.static(path.join(__dirname, "dist")));
21
+
22
+ app.post("/api/ask-ai", async (req, res) => {
23
+ const { prompt, html, previousPrompt } = req.body;
24
+ if (!prompt) {
25
+ return res.status(400).send({
26
+ ok: false,
27
+ message: "Missing required fields",
28
+ });
29
+ }
30
+
31
+ // Set up response headers for streaming
32
+ res.setHeader("Content-Type", "text/plain");
33
+ res.setHeader("Cache-Control", "no-cache");
34
+ res.setHeader("Connection", "keep-alive");
35
+
36
+ // 始终使用 OpenAI
37
+ await createChatCompletion({ prompt, previousPrompt, html, res });
38
+ });
39
+
40
+ app.get("*", (_req, res) => {
41
+ res.sendFile(path.join(__dirname, "dist", "index.html"));
42
+ });
43
+
44
+ app.listen(PORT, () => {
45
+ console.log(`Server is running on port ${PORT}`);
46
+ });
services/groq.js ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Groq from "groq-sdk";
2
+ import dotenv from 'dotenv';
3
+
4
+ dotenv.config();
5
+
6
+ const groq = new Groq({ apiKey: process.env.GROQ_API_KEY });
7
+
8
+ // System prompt information
9
+ const SYSTEM_PROMPT = `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 elaborate as much as you can, to create something unique. ALWAYS GIVE THE RESPONSE INTO A SINGLE HTML FILE otherwise you can build full backend frontend typescript website if user want! If the user asks for a full-stack website, provide the necessary files (e.g., HTML, CSS, JavaScript, and backend code) in separate code blocks.`;
10
+
11
+ /**
12
+ * Create chat messages array
13
+ * @param {string} prompt - User prompt
14
+ * @param {string} previousPrompt - Previous prompt (optional)
15
+ * @param {string} html - Current HTML code (optional)
16
+ * @returns {Array} Messages array
17
+ */
18
+ const createChatMessages = (prompt, previousPrompt, html) => {
19
+ const messages = [
20
+ {
21
+ role: "system",
22
+ content: SYSTEM_PROMPT,
23
+ }
24
+ ];
25
+
26
+ if (previousPrompt) {
27
+ messages.push({
28
+ role: "user",
29
+ content: previousPrompt,
30
+ });
31
+ }
32
+
33
+ if (html) {
34
+ messages.push({
35
+ role: "assistant",
36
+ content: `The current code is: ${html}.`,
37
+ });
38
+ }
39
+
40
+ messages.push({
41
+ role: "user",
42
+ content: prompt,
43
+ });
44
+
45
+ return messages;
46
+ };
47
+
48
+ /**
49
+ * Handle stream response
50
+ * @param {Response} res - Express response object
51
+ * @param {AsyncGenerator} stream - Groq stream response
52
+ * @returns {Promise<void>}
53
+ */
54
+ const handleStream = async (res, stream) => {
55
+ let completeResponse = "";
56
+ res.setHeader('Content-Type', 'text/html');
57
+ res.setHeader('Transfer-Encoding', 'chunked');
58
+
59
+ try {
60
+ for await (const chunk of stream) {
61
+ const content = chunk.choices[0]?.delta?.content;
62
+ if (content) {
63
+ res.write(content);
64
+ completeResponse += content;
65
+
66
+ if (completeResponse.includes("</html>")) {
67
+ break;
68
+ }
69
+ }
70
+ }
71
+ res.end();
72
+ } catch (error) {
73
+ console.error("Stream error:", error);
74
+ res.status(500).end("Error processing stream");
75
+ }
76
+ };
77
+
78
+ /**
79
+ * Generate HTML based on prompt
80
+ * @param {string} prompt - User prompt
81
+ * @param {string} previousPrompt - Previous prompt (optional)
82
+ * @param {string} html - Current HTML code (optional)
83
+ * @returns {AsyncGenerator} Stream response
84
+ */
85
+ export const generateHTML = async (prompt, previousPrompt, html) => {
86
+ try {
87
+ const messages = createChatMessages(prompt, previousPrompt, html);
88
+
89
+ const stream = await groq.chat.completions.create({
90
+ messages: messages,
91
+ "model": "openai/gpt-oss-20b",
92
+ "temperature": 1,
93
+ "max_completion_tokens": 8192,
94
+ "top_p": 1,
95
+ "stream": true,
96
+ "reasoning_effort": "medium",
97
+ "stop": null,
98
+ "tools": [
99
+ {
100
+ "type": "browser_search"
101
+ },
102
+ {
103
+ "type": "code_interpreter"
104
+ }
105
+ ]
106
+ });
107
+
108
+ return stream;
109
+ } catch (error) {
110
+ console.error("API error:", error);
111
+ throw error;
112
+ }
113
+ };
114
+
115
+ // Example usage
116
+ export const processPrompt = async (req, res) => {
117
+ try {
118
+ const { prompt, previousPrompt, html } = req.body;
119
+ const stream = await generateHTML(prompt, previousPrompt, html);
120
+ await handleStream(res, stream);
121
+ } catch (error) {
122
+ console.error("Processing error:", error);
123
+ res.status(500).json({ error: "Failed to process prompt" });
124
+ }
125
+ };
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,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ } from "react-use";
11
+ import { toast } from "react-toastify";
12
+
13
+ import Header from "./header/header";
14
+ import { defaultHTML } from "./../../utils/consts";
15
+ import Tabs from "./tabs/tabs";
16
+ import AskAI from "./ask-ai/ask-ai";
17
+ import Preview from "./preview/preview";
18
+
19
+ function App() {
20
+ const [htmlStorage, , removeHtmlStorage] = useLocalStorage("html_content");
21
+
22
+ const preview = useRef<HTMLDivElement>(null);
23
+ const editor = useRef<HTMLDivElement>(null);
24
+ const resizer = useRef<HTMLDivElement>(null);
25
+ const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
26
+
27
+ const [isResizing, setIsResizing] = useState(false);
28
+ const [html, setHtml] = useState((htmlStorage as string) ?? defaultHTML);
29
+ const [isAiWorking, setisAiWorking] = useState(false);
30
+ const [currentView, setCurrentView] = useState<"editor" | "preview">(
31
+ "editor"
32
+ );
33
+
34
+ /**
35
+ * Resets the layout based on screen size
36
+ * - For desktop: Sets editor to 1/3 width and preview to 2/3
37
+ * - For mobile: Removes inline styles to let CSS handle it
38
+ */
39
+ const resetLayout = () => {
40
+ if (!editor.current || !preview.current) return;
41
+
42
+ // lg breakpoint is 1024px based on useBreakpoint definition and Tailwind defaults
43
+ if (window.innerWidth >= 1024) {
44
+ // Set initial 1/3 - 2/3 sizes for large screens, accounting for resizer width
45
+ const resizerWidth = resizer.current?.offsetWidth ?? 8; // w-2 = 0.5rem = 8px
46
+ const availableWidth = window.innerWidth - resizerWidth;
47
+ const initialEditorWidth = availableWidth / 3; // Editor takes 1/3 of space
48
+ const initialPreviewWidth = availableWidth - initialEditorWidth; // Preview takes 2/3
49
+ editor.current.style.width = `${initialEditorWidth}px`;
50
+ preview.current.style.width = `${initialPreviewWidth}px`;
51
+ } else {
52
+ // Remove inline styles for smaller screens, let CSS flex-col handle it
53
+ editor.current.style.width = "";
54
+ preview.current.style.width = "";
55
+ }
56
+ };
57
+
58
+ /**
59
+ * Handles resizing when the user drags the resizer
60
+ * Ensures minimum widths are maintained for both panels
61
+ */
62
+ const handleResize = (e: MouseEvent) => {
63
+ if (!editor.current || !preview.current || !resizer.current) return;
64
+
65
+ const resizerWidth = resizer.current.offsetWidth;
66
+ const minWidth = 100; // Minimum width for editor/preview
67
+ const maxWidth = window.innerWidth - resizerWidth - minWidth;
68
+
69
+ const editorWidth = e.clientX;
70
+ const clampedEditorWidth = Math.max(
71
+ minWidth,
72
+ Math.min(editorWidth, maxWidth)
73
+ );
74
+ const calculatedPreviewWidth =
75
+ window.innerWidth - clampedEditorWidth - resizerWidth;
76
+
77
+ editor.current.style.width = `${clampedEditorWidth}px`;
78
+ preview.current.style.width = `${calculatedPreviewWidth}px`;
79
+ };
80
+
81
+ const handleMouseDown = () => {
82
+ setIsResizing(true);
83
+ document.addEventListener("mousemove", handleResize);
84
+ document.addEventListener("mouseup", handleMouseUp);
85
+ };
86
+
87
+ const handleMouseUp = () => {
88
+ setIsResizing(false);
89
+ document.removeEventListener("mousemove", handleResize);
90
+ document.removeEventListener("mouseup", handleMouseUp);
91
+ };
92
+
93
+ const handleDownloadHtml = () => {
94
+ if (html === defaultHTML) {
95
+ toast.info("Nothing to download yet.");
96
+ return;
97
+ }
98
+
99
+ try {
100
+ const blob = new Blob([html], { type: "text/html" });
101
+ const url = URL.createObjectURL(blob);
102
+ const a = document.createElement("a");
103
+ a.href = url;
104
+ a.download = "index.html"; // Or a more dynamic name if needed
105
+ document.body.appendChild(a); // Append anchor to body
106
+ a.click(); // Simulate click to trigger download
107
+ document.body.removeChild(a); // Remove anchor from body
108
+ URL.revokeObjectURL(url); // Clean up the object URL
109
+ toast.success("HTML file download started.");
110
+ } catch (error) {
111
+ console.error("Error downloading HTML:", error);
112
+ toast.error("Failed to download HTML file.");
113
+ }
114
+ };
115
+
116
+ // Prevent accidental navigation away when AI is working or content has changed
117
+ useEvent("beforeunload", (e) => {
118
+ if (isAiWorking || html !== defaultHTML) {
119
+ e.preventDefault();
120
+ return "";
121
+ }
122
+ });
123
+
124
+ // Initialize component on mount
125
+ useMount(() => {
126
+ // Restore content from storage if available
127
+ if (htmlStorage) {
128
+ removeHtmlStorage();
129
+ toast.warn("Previous HTML content restored from local storage.");
130
+ }
131
+
132
+ // Set initial layout based on window size
133
+ resetLayout();
134
+
135
+ // Attach event listeners
136
+ if (!resizer.current) return;
137
+ resizer.current.addEventListener("mousedown", handleMouseDown);
138
+ window.addEventListener("resize", resetLayout);
139
+ });
140
+
141
+ // Clean up event listeners on unmount
142
+ useUnmount(() => {
143
+ document.removeEventListener("mousemove", handleResize);
144
+ document.removeEventListener("mouseup", handleMouseUp);
145
+ if (resizer.current) {
146
+ resizer.current.removeEventListener("mousedown", handleMouseDown);
147
+ }
148
+ window.removeEventListener("resize", resetLayout);
149
+ });
150
+
151
+ return (
152
+ <div className="h-screen bg-gray-950 font-sans overflow-hidden">
153
+ <Header
154
+ onReset={() => {
155
+ if (isAiWorking) {
156
+ toast.warn("Please wait for the AI to finish working.");
157
+ return;
158
+ }
159
+ if (
160
+ window.confirm("You're about to reset the editor. Are you sure?")
161
+ ) {
162
+ setHtml(defaultHTML);
163
+ removeHtmlStorage();
164
+ editorRef.current?.revealLine(
165
+ editorRef.current?.getModel()?.getLineCount() ?? 0
166
+ );
167
+ }
168
+ }}
169
+ onDownload={handleDownloadHtml}
170
+ >
171
+ </Header>
172
+ <main className="max-lg:flex-col flex w-full">
173
+ <div
174
+ ref={editor}
175
+ className={classNames(
176
+ "w-full h-[calc(100dvh-49px)] lg:h-[calc(100dvh-54px)] relative overflow-hidden max-lg:transition-all max-lg:duration-200 select-none",
177
+ {
178
+ "max-lg:h-0": currentView === "preview",
179
+ }
180
+ )}
181
+ >
182
+ <Tabs />
183
+ <div
184
+ onClick={(e) => {
185
+ if (isAiWorking) {
186
+ e.preventDefault();
187
+ e.stopPropagation();
188
+ toast.warn("Please wait for the AI to finish working.");
189
+ }
190
+ }}
191
+ >
192
+ <Editor
193
+ language="html"
194
+ theme="vs-dark"
195
+ className={classNames(
196
+ "h-[calc(100dvh-90px)] lg:h-[calc(100dvh-96px)]",
197
+ {
198
+ "pointer-events-none": isAiWorking,
199
+ }
200
+ )}
201
+ value={html}
202
+ onValidate={(markers) => {
203
+ if (markers?.length > 0) {
204
+ // setError(true);
205
+ }
206
+ }}
207
+ onChange={(value) => {
208
+ const newValue = value ?? "";
209
+ setHtml(newValue);
210
+ // setError(false);
211
+ }}
212
+ onMount={(editor) => (editorRef.current = editor)}
213
+ />
214
+ </div>
215
+ <AskAI
216
+ html={html}
217
+ setHtml={setHtml}
218
+ isAiWorking={isAiWorking}
219
+ setisAiWorking={setisAiWorking}
220
+ setView={setCurrentView}
221
+ onScrollToBottom={() => {
222
+ editorRef.current?.revealLine(
223
+ editorRef.current?.getModel()?.getLineCount() ?? 0
224
+ );
225
+ }}
226
+ />
227
+ </div>
228
+ <div
229
+ ref={resizer}
230
+ className="bg-gray-700 hover:bg-blue-500 w-2 cursor-col-resize h-[calc(100dvh-53px)] max-lg:hidden"
231
+ />
232
+ <Preview
233
+ html={html}
234
+ isResizing={isResizing}
235
+ isAiWorking={isAiWorking}
236
+ ref={preview}
237
+ setView={setCurrentView}
238
+ />
239
+ </main>
240
+ </div>
241
+ );
242
+ }
243
+
244
+ export default App;
src/components/ask-ai/ask-ai.tsx ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 { toast } from "react-toastify";
6
+ import { MdPreview } from "react-icons/md";
7
+
8
+ // import Login from "../login/login"; // 移除
9
+ import { defaultHTML } from "./../../../utils/consts";
10
+ import SuccessSound from "./../../assets/success.mp3";
11
+ // import ProModal from "../pro-modal/pro-modal"; // 移除
12
+ // import SpeechPrompt from "../speech-prompt/speech-prompt";
13
+
14
+ function AskAI({
15
+ html,
16
+ setHtml,
17
+ onScrollToBottom,
18
+ isAiWorking,
19
+ setisAiWorking,
20
+ setView,
21
+ }: {
22
+ html: string;
23
+ setHtml: (html: string) => void;
24
+ onScrollToBottom: () => void;
25
+ isAiWorking: boolean;
26
+ setView: React.Dispatch<React.SetStateAction<"editor" | "preview">>;
27
+ setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
28
+ }) {
29
+ // const [open, setOpen] = useState(false); // 移除
30
+ const [prompt, setPrompt] = useState("");
31
+ const [hasAsked, setHasAsked] = useState(false);
32
+ const [previousPrompt, setPreviousPrompt] = useState("");
33
+ // const [openProModal, setOpenProModal] = useState(false); // 移除
34
+
35
+ const audio = new Audio(SuccessSound);
36
+ audio.volume = 0.5;
37
+
38
+ const callAi = async () => {
39
+ if (isAiWorking || !prompt.trim()) return;
40
+ setisAiWorking(true);
41
+
42
+ let contentResponse = "";
43
+ let lastRenderTime = 0;
44
+ try {
45
+ const request = await fetch("/api/ask-ai", {
46
+ method: "POST",
47
+ body: JSON.stringify({
48
+ prompt,
49
+ ...(html === defaultHTML ? {} : { html }),
50
+ ...(previousPrompt ? { previousPrompt } : {}),
51
+ }),
52
+ headers: {
53
+ "Content-Type": "application/json",
54
+ },
55
+ });
56
+ if (request && request.body) {
57
+ if (!request.ok) {
58
+ const res = await request.json();
59
+ // 移除与 openLogin 和 openProModal 相关的处理
60
+ toast.error(res.message);
61
+ setisAiWorking(false);
62
+ return;
63
+ }
64
+ const reader = request.body.getReader();
65
+ const decoder = new TextDecoder("utf-8");
66
+
67
+ const read = async () => {
68
+ const { done, value } = await reader.read();
69
+ if (done) {
70
+ toast.success("AI responded successfully");
71
+ setPrompt("");
72
+ setPreviousPrompt(prompt);
73
+ setisAiWorking(false);
74
+ setHasAsked(true);
75
+ audio.play();
76
+ setView("preview");
77
+
78
+ // Now we have the complete HTML including </html>, so set it to be sure
79
+ const finalDoc = contentResponse.match(
80
+ /<!DOCTYPE html>[\s\S]*<\/html>/
81
+ )?.[0];
82
+ if (finalDoc) {
83
+ setHtml(finalDoc);
84
+ }
85
+
86
+ return;
87
+ }
88
+
89
+ const chunk = decoder.decode(value, { stream: true });
90
+ contentResponse += chunk;
91
+ const newHtml = contentResponse.match(/<!DOCTYPE html>[\s\S]*/)?.[0];
92
+ if (newHtml) {
93
+ // Force-close the HTML tag so the iframe doesn't render half-finished markup
94
+ let partialDoc = newHtml;
95
+ if (!partialDoc.includes("</html>")) {
96
+ partialDoc += "\n</html>";
97
+ }
98
+
99
+ // Throttle the re-renders to avoid flashing/flicker
100
+ const now = Date.now();
101
+ if (now - lastRenderTime > 300) {
102
+ setHtml(partialDoc);
103
+ lastRenderTime = now;
104
+ }
105
+
106
+ if (partialDoc.length > 200) {
107
+ onScrollToBottom();
108
+ }
109
+ }
110
+ read();
111
+ };
112
+
113
+ read();
114
+ }
115
+
116
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
117
+ } catch (error: any) {
118
+ setisAiWorking(false);
119
+ toast.error(error.message);
120
+ // 移除与 openLogin 相关的处理
121
+ }
122
+ };
123
+
124
+ return (
125
+ <div
126
+ 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 ${
127
+ isAiWorking ? "animate-pulse" : ""
128
+ }`}
129
+ >
130
+ {defaultHTML !== html && (
131
+ <button
132
+ 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"
133
+ onClick={() => setView("preview")}
134
+ >
135
+ <MdPreview className="text-sm" />
136
+ View Preview
137
+ </button>
138
+ )}
139
+ <div className="w-full relative flex items-center justify-between">
140
+ <RiSparkling2Fill className="text-lg lg:text-xl text-gray-500 group-focus-within:text-pink-500" />
141
+ <input
142
+ type="text"
143
+ disabled={isAiWorking}
144
+ className="w-full bg-transparent max-lg:text-sm outline-none px-3 text-white placeholder:text-gray-500 font-code"
145
+ placeholder={
146
+ hasAsked ? "What do you want to ask AI next?" : "Ask AI anything..."
147
+ }
148
+ value={prompt}
149
+ onChange={(e) => setPrompt(e.target.value)}
150
+ onKeyDown={(e) => {
151
+ if (e.key === "Enter") {
152
+ callAi();
153
+ }
154
+ }}
155
+ />
156
+ <div className="flex items-center justify-end gap-2">
157
+ {/* <SpeechPrompt setPrompt={setPrompt} /> */}
158
+ <button
159
+ disabled={isAiWorking}
160
+ 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"
161
+ onClick={callAi}
162
+ >
163
+ <GrSend className="-translate-x-[1px]" />
164
+ </button>
165
+ </div>
166
+ </div>
167
+ {/* 移除 Login 和 ProModal 的渲染 */}
168
+ </div>
169
+ );
170
+ }
171
+
172
+ export default AskAI;
src/components/header/header.tsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { MdRefresh } from "react-icons/md";
3
+ import { FaDownload } from "react-icons/fa6";
4
+
5
+ // import Logo from "@/assets/logo.svg";
6
+
7
+ function Header({
8
+ onReset,
9
+ onDownload,
10
+ children,
11
+ }: {
12
+ onReset: () => void;
13
+ onDownload: () => void;
14
+ children?: React.ReactNode;
15
+ }) {
16
+ return (
17
+ <header className="flex h-[50px] lg:h-[54px] items-center justify-between px-3 lg:px-4 border-b border-gray-800 select-none">
18
+ <div className="flex items-center gap-2 lg:gap-3">
19
+ <img
20
+ src="/logo.svg"
21
+ alt="logo"
22
+ className="size-6 lg:size-7 filter invert"
23
+ />
24
+ <h1 className="text-white font-medium text-sm lg:text-base">
25
+ DeepSite
26
+ </h1>
27
+ </div>
28
+ <div className="flex items-center justify-end gap-2 lg:gap-3">
29
+ {children}
30
+ <button
31
+ title="Download HTML"
32
+ className="flex-none flex items-center justify-center text-gray-400 hover:text-white cursor-pointer transition-colors duration-100"
33
+ onClick={onDownload}
34
+ >
35
+ <FaDownload className="text-base lg:text-lg" />
36
+ </button>
37
+ <button
38
+ title="Reset Editor"
39
+ className="flex-none flex items-center justify-center text-gray-400 hover:text-red-500 cursor-pointer transition-colors duration-100"
40
+ onClick={onReset}
41
+ >
42
+ <MdRefresh className="text-lg lg:text-xl" />
43
+ </button>
44
+ </div>
45
+ </header>
46
+ );
47
+ }
48
+
49
+ export default Header;
src/components/loading/loading.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function Loading() {
2
+ return (
3
+ <div className="absolute left-0 top-0 h-full w-full flex items-center justify-center bg-white/30 z-20">
4
+ <svg
5
+ className="size-5 animate-spin text-white"
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ fill="none"
8
+ viewBox="0 0 24 24"
9
+ >
10
+ <circle
11
+ className="opacity-25"
12
+ cx="12"
13
+ cy="12"
14
+ r="10"
15
+ stroke="currentColor"
16
+ strokeWidth="4"
17
+ ></circle>
18
+ <path
19
+ className="opacity-75"
20
+ fill="currentColor"
21
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
22
+ ></path>
23
+ </svg>
24
+ </div>
25
+ );
26
+ }
27
+
28
+ export default Loading;
src/components/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/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/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/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
+ });