Spaces:
Configuration error
Configuration error
Upload folder using huggingface_hub
Browse files- .dockerignore +21 -0
- .env.example +11 -0
- .gitignore +26 -0
- Dockerfile +22 -0
- README.md +145 -10
- eslint.config.js +28 -0
- index.html +29 -0
- module.d.ts +1 -0
- package-lock.json +0 -0
- package.json +45 -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 +46 -0
- services/groq.js +125 -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 +244 -0
- src/components/ask-ai/ask-ai.tsx +172 -0
- src/components/header/header.tsx +49 -0
- src/components/loading/loading.tsx +28 -0
- src/components/preview/preview.tsx +85 -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/consts.ts +42 -0
- utils/types.ts +5 -0
- vite.config.ts +11 -0
.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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
});
|