Spaces:
Running
Running
Create Index.html
#356
by
HoundeglassoD
- opened
This view is limited to 50 files because it contains too many changes.
See the raw diff here.
- Index.html +27 -0
- MCP-SERVER.md +0 -428
- README.md +4 -8
- app/(public)/layout.tsx +1 -1
- app/(public)/page.tsx +43 -4
- app/(public)/projects/page.tsx +13 -0
- app/[namespace]/[repoId]/page.tsx +0 -28
- app/actions/auth.ts +1 -1
- app/actions/projects.ts +40 -24
- app/api/{ask → ask-ai}/route.ts +186 -171
- app/api/auth/login-url/route.ts +0 -21
- app/api/auth/logout/route.ts +0 -25
- app/api/auth/route.ts +2 -2
- app/api/mcp/route.ts +0 -435
- app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts +0 -190
- app/api/me/projects/[namespace]/[repoId]/images/route.ts +0 -125
- app/api/me/projects/[namespace]/[repoId]/route.ts +162 -122
- app/api/me/projects/[namespace]/[repoId]/save/route.ts +0 -72
- app/api/me/projects/[namespace]/[repoId]/update/route.ts +0 -141
- app/api/me/projects/route.ts +92 -87
- app/api/me/route.ts +1 -22
- app/api/re-design/route.ts +16 -48
- app/auth/callback/page.tsx +42 -67
- app/layout.tsx +48 -70
- app/new/page.tsx +0 -14
- app/projects/[namespace]/[repoId]/page.tsx +40 -0
- app/projects/new/page.tsx +5 -0
- app/sitemap.ts +0 -28
- assets/deepseek.svg +0 -1
- assets/globals.css +1 -226
- assets/kimi.svg +0 -1
- assets/qwen.svg +0 -1
- assets/zai.svg +0 -13
- components.json +1 -1
- components/animated-blobs/index.tsx +0 -34
- components/animated-text/index.tsx +0 -123
- components/contexts/app-context.tsx +31 -26
- components/contexts/login-context.tsx +0 -62
- components/contexts/pro-context.tsx +0 -48
- components/domain-redirect/index.tsx +0 -23
- components/editor/ask-ai/context.tsx +0 -124
- components/editor/ask-ai/fake-ask.tsx +0 -97
- components/editor/ask-ai/follow-up-tooltip.tsx +36 -0
- components/editor/ask-ai/index.tsx +347 -198
- components/editor/ask-ai/loading.tsx +0 -68
- components/editor/ask-ai/prompt-builder/content-modal.tsx +0 -196
- components/editor/ask-ai/prompt-builder/index.tsx +0 -68
- components/editor/ask-ai/prompt-builder/tailwind-colors.tsx +0 -58
- components/editor/ask-ai/prompt-builder/themes.tsx +0 -48
- components/editor/ask-ai/re-imagine.tsx +4 -10
Index.html
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="fr">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<title>Mon site</title>
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<style>
|
| 8 |
+
body {
|
| 9 |
+
font-family: Arial, sans-serif;
|
| 10 |
+
background: #f4f4f4;
|
| 11 |
+
text-align: center;
|
| 12 |
+
padding: 50px;
|
| 13 |
+
}
|
| 14 |
+
h1 {
|
| 15 |
+
color: #333;
|
| 16 |
+
}
|
| 17 |
+
p {
|
| 18 |
+
font-size: 18px;
|
| 19 |
+
color: #555;
|
| 20 |
+
}
|
| 21 |
+
</style>
|
| 22 |
+
</head>
|
| 23 |
+
<body>
|
| 24 |
+
<h1>Bienvenue sur mon site 🚀</h1>
|
| 25 |
+
<p>Ce site a été déployé avec Hugging Face Spaces.</p>
|
| 26 |
+
</body>
|
| 27 |
+
</html>
|
MCP-SERVER.md
DELETED
|
@@ -1,428 +0,0 @@
|
|
| 1 |
-
# DeepSite MCP Server
|
| 2 |
-
|
| 3 |
-
DeepSite is now available as an MCP (Model Context Protocol) server, enabling AI assistants like Claude to create websites directly using natural language.
|
| 4 |
-
|
| 5 |
-
## Two Ways to Use DeepSite MCP
|
| 6 |
-
|
| 7 |
-
**Quick Comparison:**
|
| 8 |
-
|
| 9 |
-
| Feature | Option 1: HTTP Server | Option 2: Local Server |
|
| 10 |
-
|---------|----------------------|------------------------|
|
| 11 |
-
| **Setup Difficulty** | ✅ Easy (just config) | ⚠️ Requires installation |
|
| 12 |
-
| **Authentication** | HF Token in config header | HF Token or session cookie in env |
|
| 13 |
-
| **Best For** | Most users | Developers, custom modifications |
|
| 14 |
-
| **Maintenance** | ✅ Always up-to-date | Need to rebuild for updates |
|
| 15 |
-
|
| 16 |
-
**Recommendation:** Use Option 1 (HTTP Server) unless you need to modify the MCP server code.
|
| 17 |
-
|
| 18 |
-
---
|
| 19 |
-
|
| 20 |
-
### 🌐 Option 1: HTTP Server (Recommended)
|
| 21 |
-
|
| 22 |
-
**No installation required!** Use DeepSite's hosted MCP server.
|
| 23 |
-
|
| 24 |
-
#### Setup for Claude Desktop
|
| 25 |
-
|
| 26 |
-
Add to your Claude Desktop configuration file:
|
| 27 |
-
|
| 28 |
-
**MacOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
| 29 |
-
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
| 30 |
-
|
| 31 |
-
```json
|
| 32 |
-
{
|
| 33 |
-
"mcpServers": {
|
| 34 |
-
"deepsite": {
|
| 35 |
-
"url": "https://huggingface.co/deepsite/api/mcp",
|
| 36 |
-
"transport": {
|
| 37 |
-
"type": "sse"
|
| 38 |
-
},
|
| 39 |
-
"headers": {
|
| 40 |
-
"Authorization": "Bearer hf_your_token_here"
|
| 41 |
-
}
|
| 42 |
-
}
|
| 43 |
-
}
|
| 44 |
-
}
|
| 45 |
-
```
|
| 46 |
-
|
| 47 |
-
**Getting Your Hugging Face Token:**
|
| 48 |
-
|
| 49 |
-
1. Go to https://huggingface.co/settings/tokens
|
| 50 |
-
2. Create a new token with `write` access
|
| 51 |
-
3. Copy the token
|
| 52 |
-
4. Add it to the `Authorization` header in your config (recommended for security)
|
| 53 |
-
5. Alternatively, you can pass it as the `hf_token` parameter when using the tool
|
| 54 |
-
|
| 55 |
-
**⚠️ Security Recommendation:** Use the `Authorization` header in your config instead of passing the token in chat. This keeps your token secure and out of conversation history.
|
| 56 |
-
|
| 57 |
-
#### Example Usage with Claude
|
| 58 |
-
|
| 59 |
-
> "Create a portfolio website using DeepSite. Include a hero section, about section, and contact form."
|
| 60 |
-
|
| 61 |
-
Claude will automatically:
|
| 62 |
-
1. Use the `create_project` tool
|
| 63 |
-
2. Authenticate using the token from your config
|
| 64 |
-
3. Create the website on Hugging Face Spaces
|
| 65 |
-
4. Return the URLs to access your new site
|
| 66 |
-
|
| 67 |
-
---
|
| 68 |
-
|
| 69 |
-
### 💻 Option 2: Local Server
|
| 70 |
-
|
| 71 |
-
Run the MCP server locally for more control or offline use.
|
| 72 |
-
|
| 73 |
-
> **Note:** Most users should use Option 1 (HTTP Server) instead. Option 2 is only needed if you want to run the MCP server locally or modify its behavior.
|
| 74 |
-
|
| 75 |
-
#### Installation
|
| 76 |
-
|
| 77 |
-
```bash
|
| 78 |
-
cd mcp-server
|
| 79 |
-
npm install
|
| 80 |
-
npm run build
|
| 81 |
-
```
|
| 82 |
-
|
| 83 |
-
#### Setup for Claude Desktop
|
| 84 |
-
|
| 85 |
-
**Method A: Using HF Token (Recommended)**
|
| 86 |
-
|
| 87 |
-
```json
|
| 88 |
-
{
|
| 89 |
-
"mcpServers": {
|
| 90 |
-
"deepsite-local": {
|
| 91 |
-
"command": "node",
|
| 92 |
-
"args": ["/absolute/path/to/deepsite-v3/mcp-server/dist/index.js"],
|
| 93 |
-
"env": {
|
| 94 |
-
"HF_TOKEN": "hf_your_token_here",
|
| 95 |
-
"DEEPSITE_API_URL": "https://huggingface.co/deepsite"
|
| 96 |
-
}
|
| 97 |
-
}
|
| 98 |
-
}
|
| 99 |
-
}
|
| 100 |
-
```
|
| 101 |
-
|
| 102 |
-
**Method B: Using Session Cookie (Alternative)**
|
| 103 |
-
|
| 104 |
-
```json
|
| 105 |
-
{
|
| 106 |
-
"mcpServers": {
|
| 107 |
-
"deepsite-local": {
|
| 108 |
-
"command": "node",
|
| 109 |
-
"args": ["/absolute/path/to/deepsite-v3/mcp-server/dist/index.js"],
|
| 110 |
-
"env": {
|
| 111 |
-
"DEEPSITE_AUTH_COOKIE": "your-session-cookie",
|
| 112 |
-
"DEEPSITE_API_URL": "https://huggingface.co/deepsite"
|
| 113 |
-
}
|
| 114 |
-
}
|
| 115 |
-
}
|
| 116 |
-
}
|
| 117 |
-
```
|
| 118 |
-
|
| 119 |
-
**Getting Your Session Cookie (Method B only):**
|
| 120 |
-
|
| 121 |
-
1. Log in to https://huggingface.co/deepsite
|
| 122 |
-
2. Open Developer Tools (F12)
|
| 123 |
-
3. Go to Application → Cookies
|
| 124 |
-
4. Copy the session cookie value
|
| 125 |
-
5. Set as `DEEPSITE_AUTH_COOKIE` in the config
|
| 126 |
-
|
| 127 |
-
---
|
| 128 |
-
|
| 129 |
-
## Available Tools
|
| 130 |
-
|
| 131 |
-
### `create_project`
|
| 132 |
-
|
| 133 |
-
Creates a new DeepSite project with HTML/CSS/JS files.
|
| 134 |
-
|
| 135 |
-
**Parameters:**
|
| 136 |
-
|
| 137 |
-
| Parameter | Type | Required | Description |
|
| 138 |
-
|-----------|------|----------|-------------|
|
| 139 |
-
| `title` | string | No | Project title (defaults to "DeepSite Project") |
|
| 140 |
-
| `pages` | array | Yes | Array of file objects with `path` and `html` |
|
| 141 |
-
| `prompt` | string | No | Commit message/description |
|
| 142 |
-
| `hf_token` | string | No* | Hugging Face API token (*optional if provided via Authorization header in config) |
|
| 143 |
-
|
| 144 |
-
**Page Object:**
|
| 145 |
-
```typescript
|
| 146 |
-
{
|
| 147 |
-
path: string; // e.g., "index.html", "styles.css", "script.js"
|
| 148 |
-
html: string; // File content
|
| 149 |
-
}
|
| 150 |
-
```
|
| 151 |
-
|
| 152 |
-
**Returns:**
|
| 153 |
-
```json
|
| 154 |
-
{
|
| 155 |
-
"success": true,
|
| 156 |
-
"message": "Project created successfully!",
|
| 157 |
-
"projectUrl": "https://huggingface.co/deepsite/username/project-name",
|
| 158 |
-
"spaceUrl": "https://huggingface.co/spaces/username/project-name",
|
| 159 |
-
"liveUrl": "https://username-project-name.hf.space",
|
| 160 |
-
"spaceId": "username/project-name",
|
| 161 |
-
"projectId": "space-id",
|
| 162 |
-
"files": ["index.html", "styles.css"]
|
| 163 |
-
}
|
| 164 |
-
```
|
| 165 |
-
|
| 166 |
-
---
|
| 167 |
-
|
| 168 |
-
## Example Prompts for Claude
|
| 169 |
-
|
| 170 |
-
### Simple Landing Page
|
| 171 |
-
> "Create a modern landing page for my SaaS product using DeepSite. Include a hero section with CTA, features grid, and footer. Use gradient background."
|
| 172 |
-
|
| 173 |
-
### Portfolio Website
|
| 174 |
-
> "Build a portfolio website with DeepSite. I need:
|
| 175 |
-
> - Hero section with my name and photo
|
| 176 |
-
> - Projects gallery with 3 sample projects
|
| 177 |
-
> - Skills section with tech stack
|
| 178 |
-
> - Contact form
|
| 179 |
-
> Use dark mode with accent colors."
|
| 180 |
-
|
| 181 |
-
### Blog Homepage
|
| 182 |
-
> "Create a blog homepage using DeepSite. Include:
|
| 183 |
-
> - Header with navigation
|
| 184 |
-
> - Featured post section
|
| 185 |
-
> - Grid of recent posts (3 cards)
|
| 186 |
-
> - Sidebar with categories
|
| 187 |
-
> - Footer with social links
|
| 188 |
-
> Clean, minimal design."
|
| 189 |
-
|
| 190 |
-
### Interactive Dashboard
|
| 191 |
-
> "Make an analytics dashboard with DeepSite:
|
| 192 |
-
> - Sidebar navigation
|
| 193 |
-
> - 4 metric cards at top
|
| 194 |
-
> - 2 chart placeholders
|
| 195 |
-
> - Data table
|
| 196 |
-
> - Modern, professional UI with charts.css"
|
| 197 |
-
|
| 198 |
-
---
|
| 199 |
-
|
| 200 |
-
## Direct API Usage
|
| 201 |
-
|
| 202 |
-
You can also call the HTTP endpoint directly:
|
| 203 |
-
|
| 204 |
-
### Using Authorization Header (Recommended)
|
| 205 |
-
|
| 206 |
-
```bash
|
| 207 |
-
curl -X POST https://huggingface.co/deepsite/api/mcp \
|
| 208 |
-
-H "Content-Type: application/json" \
|
| 209 |
-
-H "Authorization: Bearer hf_your_token_here" \
|
| 210 |
-
-d '{
|
| 211 |
-
"jsonrpc": "2.0",
|
| 212 |
-
"id": 1,
|
| 213 |
-
"method": "tools/call",
|
| 214 |
-
"params": {
|
| 215 |
-
"name": "create_project",
|
| 216 |
-
"arguments": {
|
| 217 |
-
"title": "My Website",
|
| 218 |
-
"pages": [
|
| 219 |
-
{
|
| 220 |
-
"path": "index.html",
|
| 221 |
-
"html": "<!DOCTYPE html><html><head><title>Hello</title></head><body><h1>Hello World!</h1></body></html>"
|
| 222 |
-
}
|
| 223 |
-
]
|
| 224 |
-
}
|
| 225 |
-
}
|
| 226 |
-
}'
|
| 227 |
-
```
|
| 228 |
-
|
| 229 |
-
### Using Token Parameter (Fallback)
|
| 230 |
-
|
| 231 |
-
```bash
|
| 232 |
-
curl -X POST https://huggingface.co/deepsite/api/mcp \
|
| 233 |
-
-H "Content-Type: application/json" \
|
| 234 |
-
-d '{
|
| 235 |
-
"jsonrpc": "2.0",
|
| 236 |
-
"id": 1,
|
| 237 |
-
"method": "tools/call",
|
| 238 |
-
"params": {
|
| 239 |
-
"name": "create_project",
|
| 240 |
-
"arguments": {
|
| 241 |
-
"title": "My Website",
|
| 242 |
-
"pages": [
|
| 243 |
-
{
|
| 244 |
-
"path": "index.html",
|
| 245 |
-
"html": "<!DOCTYPE html><html><head><title>Hello</title></head><body><h1>Hello World!</h1></body></html>"
|
| 246 |
-
}
|
| 247 |
-
],
|
| 248 |
-
"hf_token": "hf_xxxxx"
|
| 249 |
-
}
|
| 250 |
-
}
|
| 251 |
-
}'
|
| 252 |
-
```
|
| 253 |
-
|
| 254 |
-
### List Available Tools
|
| 255 |
-
|
| 256 |
-
```bash
|
| 257 |
-
curl -X POST https://huggingface.co/deepsite/api/mcp \
|
| 258 |
-
-H "Content-Type: application/json" \
|
| 259 |
-
-d '{
|
| 260 |
-
"jsonrpc": "2.0",
|
| 261 |
-
"id": 1,
|
| 262 |
-
"method": "tools/list",
|
| 263 |
-
"params": {}
|
| 264 |
-
}'
|
| 265 |
-
```
|
| 266 |
-
|
| 267 |
-
---
|
| 268 |
-
|
| 269 |
-
## Testing
|
| 270 |
-
|
| 271 |
-
### Test Local Server
|
| 272 |
-
|
| 273 |
-
```bash
|
| 274 |
-
cd mcp-server
|
| 275 |
-
./test.sh
|
| 276 |
-
```
|
| 277 |
-
|
| 278 |
-
### Test HTTP Server
|
| 279 |
-
|
| 280 |
-
```bash
|
| 281 |
-
curl -X POST https://huggingface.co/deepsite/api/mcp \
|
| 282 |
-
-H "Content-Type: application/json" \
|
| 283 |
-
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
|
| 284 |
-
```
|
| 285 |
-
|
| 286 |
-
---
|
| 287 |
-
|
| 288 |
-
## Migration Guide: From Parameter to Header Auth
|
| 289 |
-
|
| 290 |
-
If you're currently passing the token as a parameter in your prompts, here's how to migrate to the more secure header-based authentication:
|
| 291 |
-
|
| 292 |
-
### Step 1: Update Your Config
|
| 293 |
-
|
| 294 |
-
Edit your Claude Desktop config file and add the `headers` section:
|
| 295 |
-
|
| 296 |
-
```json
|
| 297 |
-
{
|
| 298 |
-
"mcpServers": {
|
| 299 |
-
"deepsite": {
|
| 300 |
-
"url": "https://huggingface.co/deepsite/api/mcp",
|
| 301 |
-
"transport": {
|
| 302 |
-
"type": "sse"
|
| 303 |
-
},
|
| 304 |
-
"headers": {
|
| 305 |
-
"Authorization": "Bearer hf_your_actual_token_here"
|
| 306 |
-
}
|
| 307 |
-
}
|
| 308 |
-
}
|
| 309 |
-
}
|
| 310 |
-
```
|
| 311 |
-
|
| 312 |
-
### Step 2: Restart Claude Desktop
|
| 313 |
-
|
| 314 |
-
Completely quit and restart Claude Desktop for the changes to take effect.
|
| 315 |
-
|
| 316 |
-
### Step 3: Use Simpler Prompts
|
| 317 |
-
|
| 318 |
-
Now you can simply say:
|
| 319 |
-
> "Create a portfolio website with DeepSite"
|
| 320 |
-
|
| 321 |
-
Instead of:
|
| 322 |
-
> "Create a portfolio website with DeepSite using token `hf_xxxxx`"
|
| 323 |
-
|
| 324 |
-
Your token is automatically included in all requests via the header!
|
| 325 |
-
|
| 326 |
-
---
|
| 327 |
-
|
| 328 |
-
## Security Notes
|
| 329 |
-
|
| 330 |
-
### HTTP Server (Option 1)
|
| 331 |
-
- **✅ Recommended:** Store your HF token in the `Authorization` header in your Claude Desktop config
|
| 332 |
-
- The token is stored locally on your machine and never exposed in chat
|
| 333 |
-
- The token is sent with each request but only used to authenticate with Hugging Face API
|
| 334 |
-
- DeepSite does not store your token
|
| 335 |
-
- Use tokens with minimal required permissions (write access to spaces)
|
| 336 |
-
- You can revoke tokens anytime at https://huggingface.co/settings/tokens
|
| 337 |
-
- **⚠️ Fallback:** You can still pass the token as a parameter, but this is less secure as it appears in conversation history
|
| 338 |
-
|
| 339 |
-
### Local Server (Option 2)
|
| 340 |
-
- Use `HF_TOKEN` environment variable (same security as Option 1)
|
| 341 |
-
- Or use `DEEPSITE_AUTH_COOKIE` if you prefer session-based auth
|
| 342 |
-
- All authentication data stays on your local machine
|
| 343 |
-
- Better for development and testing
|
| 344 |
-
- No need for both HTTP Server and Local Server - choose one!
|
| 345 |
-
|
| 346 |
-
---
|
| 347 |
-
|
| 348 |
-
## Troubleshooting
|
| 349 |
-
|
| 350 |
-
### "Invalid Hugging Face token"
|
| 351 |
-
- Verify your token at https://huggingface.co/settings/tokens
|
| 352 |
-
- Ensure the token has write permissions
|
| 353 |
-
- Check that you copied the full token (starts with `hf_`)
|
| 354 |
-
|
| 355 |
-
### "At least one page is required"
|
| 356 |
-
- Make sure you're providing the `pages` array
|
| 357 |
-
- Each page must have both `path` and `html` properties
|
| 358 |
-
|
| 359 |
-
### "Failed to create project"
|
| 360 |
-
- Check your token permissions
|
| 361 |
-
- Ensure the project title doesn't conflict with existing spaces
|
| 362 |
-
- Verify your Hugging Face account is in good standing
|
| 363 |
-
|
| 364 |
-
### Claude doesn't see the tool
|
| 365 |
-
- Restart Claude Desktop after modifying the config
|
| 366 |
-
- Check that the JSON config is valid (no trailing commas)
|
| 367 |
-
- For HTTP: verify the URL is correct
|
| 368 |
-
- For local: check the absolute path to index.js
|
| 369 |
-
|
| 370 |
-
---
|
| 371 |
-
|
| 372 |
-
## Architecture
|
| 373 |
-
|
| 374 |
-
### HTTP Server Flow
|
| 375 |
-
```
|
| 376 |
-
Claude Desktop
|
| 377 |
-
↓
|
| 378 |
-
(HTTP Request)
|
| 379 |
-
↓
|
| 380 |
-
huggingface.co/deepsite/api/mcp
|
| 381 |
-
↓
|
| 382 |
-
Hugging Face API (with user's token)
|
| 383 |
-
↓
|
| 384 |
-
New Space Created
|
| 385 |
-
↓
|
| 386 |
-
URLs returned to Claude
|
| 387 |
-
```
|
| 388 |
-
|
| 389 |
-
### Local Server Flow
|
| 390 |
-
```
|
| 391 |
-
Claude Desktop
|
| 392 |
-
↓
|
| 393 |
-
(stdio transport)
|
| 394 |
-
↓
|
| 395 |
-
Local MCP Server
|
| 396 |
-
↓
|
| 397 |
-
(HTTP to DeepSite API)
|
| 398 |
-
↓
|
| 399 |
-
huggingface.co/deepsite/api/me/projects
|
| 400 |
-
↓
|
| 401 |
-
New Space Created
|
| 402 |
-
```
|
| 403 |
-
|
| 404 |
-
---
|
| 405 |
-
|
| 406 |
-
## Contributing
|
| 407 |
-
|
| 408 |
-
The MCP server implementation lives in:
|
| 409 |
-
- HTTP Server: `/app/api/mcp/route.ts`
|
| 410 |
-
- Local Server: `/mcp-server/index.ts`
|
| 411 |
-
|
| 412 |
-
Both use the same core DeepSite logic for creating projects - no duplication!
|
| 413 |
-
|
| 414 |
-
---
|
| 415 |
-
|
| 416 |
-
## License
|
| 417 |
-
|
| 418 |
-
MIT
|
| 419 |
-
|
| 420 |
-
---
|
| 421 |
-
|
| 422 |
-
## Resources
|
| 423 |
-
|
| 424 |
-
- [Model Context Protocol Spec](https://modelcontextprotocol.io/)
|
| 425 |
-
- [DeepSite Documentation](https://huggingface.co/deepsite)
|
| 426 |
-
- [Hugging Face Spaces](https://huggingface.co/docs/hub/spaces)
|
| 427 |
-
- [Claude Desktop](https://claude.ai/desktop)
|
| 428 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title: DeepSite
|
| 3 |
emoji: 🐳
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: blue
|
|
@@ -7,23 +7,19 @@ sdk: docker
|
|
| 7 |
pinned: true
|
| 8 |
app_port: 3000
|
| 9 |
license: mit
|
| 10 |
-
short_description: Generate any application
|
| 11 |
models:
|
| 12 |
- deepseek-ai/DeepSeek-V3-0324
|
| 13 |
- deepseek-ai/DeepSeek-R1-0528
|
| 14 |
- deepseek-ai/DeepSeek-V3.1
|
| 15 |
-
- deepseek-ai/DeepSeek-V3.1-Terminus
|
| 16 |
-
- deepseek-ai/DeepSeek-V3.2-Exp
|
| 17 |
- Qwen/Qwen3-Coder-480B-A35B-Instruct
|
| 18 |
- moonshotai/Kimi-K2-Instruct
|
| 19 |
-
- moonshotai/Kimi-K2-Instruct-0905
|
| 20 |
-
- zai-org/GLM-4.6
|
| 21 |
---
|
| 22 |
|
| 23 |
# DeepSite 🐳
|
| 24 |
|
| 25 |
-
DeepSite is a
|
| 26 |
|
| 27 |
## How to use it locally
|
| 28 |
|
| 29 |
-
Follow [this discussion](https://huggingface.co/spaces/enzostvs/deepsite/discussions/74)
|
|
|
|
| 1 |
---
|
| 2 |
+
title: DeepSite v2
|
| 3 |
emoji: 🐳
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: blue
|
|
|
|
| 7 |
pinned: true
|
| 8 |
app_port: 3000
|
| 9 |
license: mit
|
| 10 |
+
short_description: Generate any application with DeepSeek
|
| 11 |
models:
|
| 12 |
- deepseek-ai/DeepSeek-V3-0324
|
| 13 |
- deepseek-ai/DeepSeek-R1-0528
|
| 14 |
- deepseek-ai/DeepSeek-V3.1
|
|
|
|
|
|
|
| 15 |
- Qwen/Qwen3-Coder-480B-A35B-Instruct
|
| 16 |
- moonshotai/Kimi-K2-Instruct
|
|
|
|
|
|
|
| 17 |
---
|
| 18 |
|
| 19 |
# DeepSite 🐳
|
| 20 |
|
| 21 |
+
DeepSite is a coding platform powered by DeepSeek AI, designed to make coding smarter and more efficient. Tailored for developers, data scientists, and AI engineers, it integrates generative AI into your coding projects to enhance creativity and productivity.
|
| 22 |
|
| 23 |
## How to use it locally
|
| 24 |
|
| 25 |
+
Follow [this discussion](https://huggingface.co/spaces/enzostvs/deepsite/discussions/74)
|
app/(public)/layout.tsx
CHANGED
|
@@ -6,7 +6,7 @@ export default async function PublicLayout({
|
|
| 6 |
children: React.ReactNode;
|
| 7 |
}>) {
|
| 8 |
return (
|
| 9 |
-
<div className="h-screen bg-
|
| 10 |
<div className="background__noisy" />
|
| 11 |
<Navigation />
|
| 12 |
{children}
|
|
|
|
| 6 |
children: React.ReactNode;
|
| 7 |
}>) {
|
| 8 |
return (
|
| 9 |
+
<div className="min-h-screen bg-black z-1 relative">
|
| 10 |
<div className="background__noisy" />
|
| 11 |
<Navigation />
|
| 12 |
{children}
|
app/(public)/page.tsx
CHANGED
|
@@ -1,5 +1,44 @@
|
|
| 1 |
-
import {
|
| 2 |
-
|
| 3 |
-
export default
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
}
|
|
|
|
| 1 |
+
import { AskAi } from "@/components/space/ask-ai";
|
| 2 |
+
import { redirect } from "next/navigation";
|
| 3 |
+
export default function Home() {
|
| 4 |
+
redirect("/projects/new");
|
| 5 |
+
return (
|
| 6 |
+
<>
|
| 7 |
+
<header className="container mx-auto pt-20 px-6 relative flex flex-col items-center justify-center text-center">
|
| 8 |
+
<div className="rounded-full border border-neutral-100/10 bg-neutral-100/5 text-xs text-neutral-300 px-3 py-1 max-w-max mx-auto mb-2">
|
| 9 |
+
✨ DeepSite Public Beta
|
| 10 |
+
</div>
|
| 11 |
+
<h1 className="text-8xl font-semibold text-white font-mono max-w-4xl">
|
| 12 |
+
Code your website with AI in seconds
|
| 13 |
+
</h1>
|
| 14 |
+
<p className="text-2xl text-neutral-300/80 mt-4 text-center max-w-2xl">
|
| 15 |
+
Vibe Coding has never been so easy.
|
| 16 |
+
</p>
|
| 17 |
+
<div className="mt-14 max-w-2xl w-full mx-auto">
|
| 18 |
+
<AskAi />
|
| 19 |
+
</div>
|
| 20 |
+
<div className="absolute inset-0 pointer-events-none -z-[1]">
|
| 21 |
+
<div className="w-full h-full bg-gradient-to-r from-purple-500 to-pink-500 opacity-10 blur-3xl rounded-full" />
|
| 22 |
+
<div className="w-2/3 h-3/4 bg-gradient-to-r from-blue-500 to-teal-500 opacity-24 blur-3xl absolute -top-20 right-10 transform rotate-12" />
|
| 23 |
+
<div className="w-1/2 h-1/2 bg-gradient-to-r from-amber-500 to-rose-500 opacity-20 blur-3xl absolute bottom-0 left-10 rounded-3xl" />
|
| 24 |
+
<div className="w-48 h-48 bg-gradient-to-r from-cyan-500 to-indigo-500 opacity-20 blur-3xl absolute top-1/3 right-1/3 rounded-lg transform -rotate-15" />
|
| 25 |
+
</div>
|
| 26 |
+
</header>
|
| 27 |
+
<div id="community" className="h-screen flex items-center justify-center">
|
| 28 |
+
<h1 className="text-7xl font-extrabold text-white font-mono">
|
| 29 |
+
Community Driven
|
| 30 |
+
</h1>
|
| 31 |
+
</div>
|
| 32 |
+
<div id="deploy" className="h-screen flex items-center justify-center">
|
| 33 |
+
<h1 className="text-7xl font-extrabold text-white font-mono">
|
| 34 |
+
Deploy your website in seconds
|
| 35 |
+
</h1>
|
| 36 |
+
</div>
|
| 37 |
+
<div id="features" className="h-screen flex items-center justify-center">
|
| 38 |
+
<h1 className="text-7xl font-extrabold text-white font-mono">
|
| 39 |
+
Features that make you smile
|
| 40 |
+
</h1>
|
| 41 |
+
</div>
|
| 42 |
+
</>
|
| 43 |
+
);
|
| 44 |
}
|
app/(public)/projects/page.tsx
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { redirect } from "next/navigation";
|
| 2 |
+
|
| 3 |
+
import { MyProjects } from "@/components/my-projects";
|
| 4 |
+
import { getProjects } from "@/app/actions/projects";
|
| 5 |
+
|
| 6 |
+
export default async function ProjectsPage() {
|
| 7 |
+
const { ok, projects } = await getProjects();
|
| 8 |
+
if (!ok) {
|
| 9 |
+
redirect("/");
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
return <MyProjects projects={projects} />;
|
| 13 |
+
}
|
app/[namespace]/[repoId]/page.tsx
DELETED
|
@@ -1,28 +0,0 @@
|
|
| 1 |
-
import { AppEditor } from "@/components/editor";
|
| 2 |
-
import { generateSEO } from "@/lib/seo";
|
| 3 |
-
import { Metadata } from "next";
|
| 4 |
-
|
| 5 |
-
export async function generateMetadata({
|
| 6 |
-
params,
|
| 7 |
-
}: {
|
| 8 |
-
params: Promise<{ namespace: string; repoId: string }>;
|
| 9 |
-
}): Promise<Metadata> {
|
| 10 |
-
const { namespace, repoId } = await params;
|
| 11 |
-
|
| 12 |
-
return generateSEO({
|
| 13 |
-
title: `${namespace}/${repoId} - DeepSite Editor`,
|
| 14 |
-
description: `Edit and build ${namespace}/${repoId} with AI-powered tools on DeepSite. Create stunning websites with no code required.`,
|
| 15 |
-
path: `/${namespace}/${repoId}`,
|
| 16 |
-
// Prevent indexing of individual project editor pages if they contain sensitive content
|
| 17 |
-
noIndex: false, // Set to true if you want to keep project pages private
|
| 18 |
-
});
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
export default async function ProjectNamespacePage({
|
| 22 |
-
params,
|
| 23 |
-
}: {
|
| 24 |
-
params: Promise<{ namespace: string; repoId: string }>;
|
| 25 |
-
}) {
|
| 26 |
-
const { namespace, repoId } = await params;
|
| 27 |
-
return <AppEditor namespace={namespace} repoId={repoId} />;
|
| 28 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/actions/auth.ts
CHANGED
|
@@ -11,7 +11,7 @@ export async function getAuth() {
|
|
| 11 |
const redirect_uri =
|
| 12 |
`${host.includes("localhost") ? "http://" : "https://"}` +
|
| 13 |
url +
|
| 14 |
-
"/
|
| 15 |
|
| 16 |
const loginRedirectUrl = `https://huggingface.co/oauth/authorize?client_id=${process.env.OAUTH_CLIENT_ID}&redirect_uri=${redirect_uri}&response_type=code&scope=openid%20profile%20write-repos%20manage-repos%20inference-api&prompt=consent&state=1234567890`;
|
| 17 |
return loginRedirectUrl;
|
|
|
|
| 11 |
const redirect_uri =
|
| 12 |
`${host.includes("localhost") ? "http://" : "https://"}` +
|
| 13 |
url +
|
| 14 |
+
"/auth/callback";
|
| 15 |
|
| 16 |
const loginRedirectUrl = `https://huggingface.co/oauth/authorize?client_id=${process.env.OAUTH_CLIENT_ID}&redirect_uri=${redirect_uri}&response_type=code&scope=openid%20profile%20write-repos%20manage-repos%20inference-api&prompt=consent&state=1234567890`;
|
| 17 |
return loginRedirectUrl;
|
app/actions/projects.ts
CHANGED
|
@@ -2,13 +2,13 @@
|
|
| 2 |
|
| 3 |
import { isAuthenticated } from "@/lib/auth";
|
| 4 |
import { NextResponse } from "next/server";
|
| 5 |
-
import
|
| 6 |
-
import
|
|
|
|
| 7 |
|
| 8 |
export async function getProjects(): Promise<{
|
| 9 |
ok: boolean;
|
| 10 |
projects: ProjectType[];
|
| 11 |
-
isEmpty?: boolean;
|
| 12 |
}> {
|
| 13 |
const user = await isAuthenticated();
|
| 14 |
|
|
@@ -19,29 +19,45 @@ export async function getProjects(): Promise<{
|
|
| 19 |
};
|
| 20 |
}
|
| 21 |
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
(
|
| 35 |
-
((space.cardData as { tags?: string[] })?.tags?.includes("deepsite-v3")) ||
|
| 36 |
-
((space.cardData as { tags?: string[] })?.tags?.includes("deepsite"))
|
| 37 |
-
)
|
| 38 |
-
) {
|
| 39 |
-
projects.push(space);
|
| 40 |
-
}
|
| 41 |
}
|
| 42 |
-
|
| 43 |
return {
|
| 44 |
ok: true,
|
| 45 |
-
projects,
|
| 46 |
};
|
| 47 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
import { isAuthenticated } from "@/lib/auth";
|
| 4 |
import { NextResponse } from "next/server";
|
| 5 |
+
import dbConnect from "@/lib/mongodb";
|
| 6 |
+
import Project from "@/models/Project";
|
| 7 |
+
import { Project as ProjectType } from "@/types";
|
| 8 |
|
| 9 |
export async function getProjects(): Promise<{
|
| 10 |
ok: boolean;
|
| 11 |
projects: ProjectType[];
|
|
|
|
| 12 |
}> {
|
| 13 |
const user = await isAuthenticated();
|
| 14 |
|
|
|
|
| 19 |
};
|
| 20 |
}
|
| 21 |
|
| 22 |
+
await dbConnect();
|
| 23 |
+
const projects = await Project.find({
|
| 24 |
+
user_id: user?.id,
|
| 25 |
+
})
|
| 26 |
+
.sort({ _createdAt: -1 })
|
| 27 |
+
.limit(100)
|
| 28 |
+
.lean();
|
| 29 |
+
if (!projects) {
|
| 30 |
+
return {
|
| 31 |
+
ok: false,
|
| 32 |
+
projects: [],
|
| 33 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
}
|
|
|
|
| 35 |
return {
|
| 36 |
ok: true,
|
| 37 |
+
projects: JSON.parse(JSON.stringify(projects)) as ProjectType[],
|
| 38 |
};
|
| 39 |
}
|
| 40 |
+
|
| 41 |
+
export async function getProject(
|
| 42 |
+
namespace: string,
|
| 43 |
+
repoId: string
|
| 44 |
+
): Promise<ProjectType | null> {
|
| 45 |
+
const user = await isAuthenticated();
|
| 46 |
+
|
| 47 |
+
if (user instanceof NextResponse || !user) {
|
| 48 |
+
return null;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
await dbConnect();
|
| 52 |
+
const project = await Project.findOne({
|
| 53 |
+
user_id: user.id,
|
| 54 |
+
namespace,
|
| 55 |
+
repoId,
|
| 56 |
+
}).lean();
|
| 57 |
+
|
| 58 |
+
if (!project) {
|
| 59 |
+
return null;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
return JSON.parse(JSON.stringify(project)) as ProjectType;
|
| 63 |
+
}
|
app/api/{ask → ask-ai}/route.ts
RENAMED
|
@@ -4,18 +4,16 @@ import { NextResponse } from "next/server";
|
|
| 4 |
import { headers } from "next/headers";
|
| 5 |
import { InferenceClient } from "@huggingface/inference";
|
| 6 |
|
| 7 |
-
import { MODELS } from "@/lib/providers";
|
| 8 |
import {
|
|
|
|
| 9 |
FOLLOW_UP_SYSTEM_PROMPT,
|
| 10 |
INITIAL_SYSTEM_PROMPT,
|
| 11 |
MAX_REQUESTS_PER_IP,
|
| 12 |
-
|
|
|
|
| 13 |
} from "@/lib/prompts";
|
| 14 |
-
import { calculateMaxTokens, estimateInputTokens, getProviderSpecificConfig } from "@/lib/max-tokens";
|
| 15 |
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
| 16 |
-
import { Page } from "@/types";
|
| 17 |
-
import { isAuthenticated } from "@/lib/auth";
|
| 18 |
-
import { getBestProvider } from "@/lib/best-provider";
|
| 19 |
|
| 20 |
const ipAddresses = new Map();
|
| 21 |
|
|
@@ -24,7 +22,7 @@ export async function POST(request: NextRequest) {
|
|
| 24 |
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
| 25 |
|
| 26 |
const body = await request.json();
|
| 27 |
-
const { prompt, provider, model, redesignMarkdown,
|
| 28 |
|
| 29 |
if (!model || (!prompt && !redesignMarkdown)) {
|
| 30 |
return NextResponse.json(
|
|
@@ -36,7 +34,6 @@ export async function POST(request: NextRequest) {
|
|
| 36 |
const selectedModel = MODELS.find(
|
| 37 |
(m) => m.value === model || m.label === model
|
| 38 |
);
|
| 39 |
-
|
| 40 |
if (!selectedModel) {
|
| 41 |
return NextResponse.json(
|
| 42 |
{ ok: false, error: "Invalid model selected" },
|
|
@@ -44,8 +41,18 @@ export async function POST(request: NextRequest) {
|
|
| 44 |
);
|
| 45 |
}
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
let billTo: string | null = null;
|
| 50 |
|
| 51 |
/**
|
|
@@ -78,19 +85,19 @@ export async function POST(request: NextRequest) {
|
|
| 78 |
billTo = "huggingface";
|
| 79 |
}
|
| 80 |
|
| 81 |
-
const
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
// rewrittenPrompt = await rewritePrompt(rewrittenPrompt, enhancedSettings, { token, billTo }, selectedModel.value, selectedProvider.provider);
|
| 87 |
-
}
|
| 88 |
|
| 89 |
try {
|
|
|
|
| 90 |
const encoder = new TextEncoder();
|
| 91 |
const stream = new TransformStream();
|
| 92 |
const writer = stream.writable.getWriter();
|
| 93 |
|
|
|
|
| 94 |
const response = new NextResponse(stream.readable, {
|
| 95 |
headers: {
|
| 96 |
"Content-Type": "text/plain; charset=utf-8",
|
|
@@ -100,51 +107,75 @@ export async function POST(request: NextRequest) {
|
|
| 100 |
});
|
| 101 |
|
| 102 |
(async () => {
|
| 103 |
-
|
| 104 |
try {
|
| 105 |
const client = new InferenceClient(token);
|
| 106 |
-
|
| 107 |
-
const systemPrompt = INITIAL_SYSTEM_PROMPT;
|
| 108 |
-
|
| 109 |
-
const userPrompt = rewrittenPrompt;
|
| 110 |
-
const estimatedInputTokens = estimateInputTokens(systemPrompt, userPrompt);
|
| 111 |
-
const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, true);
|
| 112 |
-
const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens);
|
| 113 |
-
|
| 114 |
const chatCompletion = client.chatCompletionStream(
|
| 115 |
{
|
| 116 |
model: selectedModel.value,
|
| 117 |
-
provider: selectedProvider.
|
| 118 |
messages: [
|
| 119 |
{
|
| 120 |
role: "system",
|
| 121 |
-
content:
|
| 122 |
},
|
| 123 |
{
|
| 124 |
role: "user",
|
| 125 |
-
content:
|
| 126 |
-
|
| 127 |
-
|
|
|
|
|
|
|
| 128 |
},
|
| 129 |
],
|
| 130 |
-
|
| 131 |
},
|
| 132 |
billTo ? { billTo } : {}
|
| 133 |
);
|
| 134 |
|
| 135 |
while (true) {
|
| 136 |
-
const { done, value } = await chatCompletion.next()
|
| 137 |
if (done) {
|
| 138 |
break;
|
| 139 |
}
|
| 140 |
|
| 141 |
const chunk = value.choices[0]?.delta?.content;
|
| 142 |
if (chunk) {
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
}
|
| 145 |
}
|
| 146 |
-
|
| 147 |
-
await writer.close();
|
| 148 |
} catch (error: any) {
|
| 149 |
if (error.message?.includes("exceeded your monthly included credits")) {
|
| 150 |
await writer.write(
|
|
@@ -156,18 +187,7 @@ export async function POST(request: NextRequest) {
|
|
| 156 |
})
|
| 157 |
)
|
| 158 |
);
|
| 159 |
-
} else
|
| 160 |
-
await writer.write(
|
| 161 |
-
encoder.encode(
|
| 162 |
-
JSON.stringify({
|
| 163 |
-
ok: false,
|
| 164 |
-
openSelectProvider: true,
|
| 165 |
-
message: error.message,
|
| 166 |
-
})
|
| 167 |
-
)
|
| 168 |
-
);
|
| 169 |
-
}
|
| 170 |
-
else {
|
| 171 |
await writer.write(
|
| 172 |
encoder.encode(
|
| 173 |
JSON.stringify({
|
|
@@ -180,10 +200,7 @@ export async function POST(request: NextRequest) {
|
|
| 180 |
);
|
| 181 |
}
|
| 182 |
} finally {
|
| 183 |
-
|
| 184 |
-
await writer?.close();
|
| 185 |
-
} catch {
|
| 186 |
-
}
|
| 187 |
}
|
| 188 |
})();
|
| 189 |
|
|
@@ -202,18 +219,14 @@ export async function POST(request: NextRequest) {
|
|
| 202 |
}
|
| 203 |
|
| 204 |
export async function PUT(request: NextRequest) {
|
| 205 |
-
const user = await isAuthenticated();
|
| 206 |
-
if (user instanceof NextResponse || !user) {
|
| 207 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 208 |
-
}
|
| 209 |
-
|
| 210 |
const authHeaders = await headers();
|
|
|
|
| 211 |
|
| 212 |
const body = await request.json();
|
| 213 |
-
const { prompt,
|
| 214 |
body;
|
| 215 |
|
| 216 |
-
if (!prompt ||
|
| 217 |
return NextResponse.json(
|
| 218 |
{ ok: false, error: "Missing required fields" },
|
| 219 |
{ status: 400 }
|
|
@@ -230,7 +243,7 @@ export async function PUT(request: NextRequest) {
|
|
| 230 |
);
|
| 231 |
}
|
| 232 |
|
| 233 |
-
let token =
|
| 234 |
let billTo: string | null = null;
|
| 235 |
|
| 236 |
/**
|
|
@@ -263,133 +276,136 @@ export async function PUT(request: NextRequest) {
|
|
| 263 |
billTo = "huggingface";
|
| 264 |
}
|
| 265 |
|
| 266 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
|
| 268 |
try {
|
| 269 |
-
const
|
| 270 |
-
|
| 271 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
},
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
(async () => {
|
| 282 |
-
try {
|
| 283 |
-
const client = new InferenceClient(token);
|
| 284 |
|
| 285 |
-
|
| 286 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
|
| 293 |
-
const
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
|
|
|
| 297 |
|
| 298 |
-
const
|
| 299 |
-
|
| 300 |
-
|
|
|
|
|
|
|
| 301 |
|
| 302 |
-
const
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
content: systemPrompt,
|
| 310 |
-
},
|
| 311 |
-
{
|
| 312 |
-
role: "user",
|
| 313 |
-
content: userContext,
|
| 314 |
-
},
|
| 315 |
-
{
|
| 316 |
-
role: "assistant",
|
| 317 |
-
content: assistantContext,
|
| 318 |
-
},
|
| 319 |
-
{
|
| 320 |
-
role: "user",
|
| 321 |
-
content: prompt,
|
| 322 |
-
},
|
| 323 |
-
],
|
| 324 |
-
...providerConfig,
|
| 325 |
-
},
|
| 326 |
-
billTo ? { billTo } : {}
|
| 327 |
);
|
| 328 |
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
|
|
|
|
|
|
|
|
|
| 339 |
}
|
| 340 |
}
|
| 341 |
|
| 342 |
-
|
| 343 |
-
repoId,
|
| 344 |
-
isNew,
|
| 345 |
-
userName: user.name,
|
| 346 |
-
})}\n___METADATA_END___\n`));
|
| 347 |
-
|
| 348 |
-
await writer.close();
|
| 349 |
-
} catch (error: any) {
|
| 350 |
-
if (error.message?.includes("exceeded your monthly included credits")) {
|
| 351 |
-
await writer.write(
|
| 352 |
-
encoder.encode(
|
| 353 |
-
JSON.stringify({
|
| 354 |
-
ok: false,
|
| 355 |
-
openProModal: true,
|
| 356 |
-
message: error.message,
|
| 357 |
-
})
|
| 358 |
-
)
|
| 359 |
-
);
|
| 360 |
-
} else if (error?.message?.includes("inference provider information")) {
|
| 361 |
-
await writer.write(
|
| 362 |
-
encoder.encode(
|
| 363 |
-
JSON.stringify({
|
| 364 |
-
ok: false,
|
| 365 |
-
openSelectProvider: true,
|
| 366 |
-
message: error.message,
|
| 367 |
-
})
|
| 368 |
-
)
|
| 369 |
-
);
|
| 370 |
-
} else {
|
| 371 |
-
await writer.write(
|
| 372 |
-
encoder.encode(
|
| 373 |
-
JSON.stringify({
|
| 374 |
-
ok: false,
|
| 375 |
-
message:
|
| 376 |
-
error.message ||
|
| 377 |
-
"An error occurred while processing your request.",
|
| 378 |
-
})
|
| 379 |
-
)
|
| 380 |
-
);
|
| 381 |
-
}
|
| 382 |
-
} finally {
|
| 383 |
-
try {
|
| 384 |
-
await writer?.close();
|
| 385 |
-
} catch {
|
| 386 |
-
// ignore
|
| 387 |
-
}
|
| 388 |
}
|
| 389 |
-
})();
|
| 390 |
|
| 391 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
} catch (error: any) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
return NextResponse.json(
|
| 394 |
{
|
| 395 |
ok: false,
|
|
@@ -401,4 +417,3 @@ export async function PUT(request: NextRequest) {
|
|
| 401 |
);
|
| 402 |
}
|
| 403 |
}
|
| 404 |
-
|
|
|
|
| 4 |
import { headers } from "next/headers";
|
| 5 |
import { InferenceClient } from "@huggingface/inference";
|
| 6 |
|
| 7 |
+
import { MODELS, PROVIDERS } from "@/lib/providers";
|
| 8 |
import {
|
| 9 |
+
DIVIDER,
|
| 10 |
FOLLOW_UP_SYSTEM_PROMPT,
|
| 11 |
INITIAL_SYSTEM_PROMPT,
|
| 12 |
MAX_REQUESTS_PER_IP,
|
| 13 |
+
REPLACE_END,
|
| 14 |
+
SEARCH_START,
|
| 15 |
} from "@/lib/prompts";
|
|
|
|
| 16 |
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
const ipAddresses = new Map();
|
| 19 |
|
|
|
|
| 22 |
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
| 23 |
|
| 24 |
const body = await request.json();
|
| 25 |
+
const { prompt, provider, model, redesignMarkdown, html } = body;
|
| 26 |
|
| 27 |
if (!model || (!prompt && !redesignMarkdown)) {
|
| 28 |
return NextResponse.json(
|
|
|
|
| 34 |
const selectedModel = MODELS.find(
|
| 35 |
(m) => m.value === model || m.label === model
|
| 36 |
);
|
|
|
|
| 37 |
if (!selectedModel) {
|
| 38 |
return NextResponse.json(
|
| 39 |
{ ok: false, error: "Invalid model selected" },
|
|
|
|
| 41 |
);
|
| 42 |
}
|
| 43 |
|
| 44 |
+
if (!selectedModel.providers.includes(provider) && provider !== "auto") {
|
| 45 |
+
return NextResponse.json(
|
| 46 |
+
{
|
| 47 |
+
ok: false,
|
| 48 |
+
error: `The selected model does not support the ${provider} provider.`,
|
| 49 |
+
openSelectProvider: true,
|
| 50 |
+
},
|
| 51 |
+
{ status: 400 }
|
| 52 |
+
);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
let token = userToken;
|
| 56 |
let billTo: string | null = null;
|
| 57 |
|
| 58 |
/**
|
|
|
|
| 85 |
billTo = "huggingface";
|
| 86 |
}
|
| 87 |
|
| 88 |
+
const DEFAULT_PROVIDER = PROVIDERS.novita;
|
| 89 |
+
const selectedProvider =
|
| 90 |
+
provider === "auto"
|
| 91 |
+
? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS]
|
| 92 |
+
: PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
|
|
|
|
|
|
|
| 93 |
|
| 94 |
try {
|
| 95 |
+
// Create a stream response
|
| 96 |
const encoder = new TextEncoder();
|
| 97 |
const stream = new TransformStream();
|
| 98 |
const writer = stream.writable.getWriter();
|
| 99 |
|
| 100 |
+
// Start the response
|
| 101 |
const response = new NextResponse(stream.readable, {
|
| 102 |
headers: {
|
| 103 |
"Content-Type": "text/plain; charset=utf-8",
|
|
|
|
| 107 |
});
|
| 108 |
|
| 109 |
(async () => {
|
| 110 |
+
let completeResponse = "";
|
| 111 |
try {
|
| 112 |
const client = new InferenceClient(token);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
const chatCompletion = client.chatCompletionStream(
|
| 114 |
{
|
| 115 |
model: selectedModel.value,
|
| 116 |
+
provider: selectedProvider.id as any,
|
| 117 |
messages: [
|
| 118 |
{
|
| 119 |
role: "system",
|
| 120 |
+
content: INITIAL_SYSTEM_PROMPT,
|
| 121 |
},
|
| 122 |
{
|
| 123 |
role: "user",
|
| 124 |
+
content: redesignMarkdown
|
| 125 |
+
? `Here is my current design as a markdown:\n\n${redesignMarkdown}\n\nNow, please create a new design based on this markdown.`
|
| 126 |
+
: html
|
| 127 |
+
? `Here is my current HTML code:\n\n\`\`\`html\n${html}\n\`\`\`\n\nNow, please create a new design based on this HTML.`
|
| 128 |
+
: prompt,
|
| 129 |
},
|
| 130 |
],
|
| 131 |
+
max_tokens: selectedProvider.max_tokens,
|
| 132 |
},
|
| 133 |
billTo ? { billTo } : {}
|
| 134 |
);
|
| 135 |
|
| 136 |
while (true) {
|
| 137 |
+
const { done, value } = await chatCompletion.next();
|
| 138 |
if (done) {
|
| 139 |
break;
|
| 140 |
}
|
| 141 |
|
| 142 |
const chunk = value.choices[0]?.delta?.content;
|
| 143 |
if (chunk) {
|
| 144 |
+
let newChunk = chunk;
|
| 145 |
+
if (!selectedModel?.isThinker) {
|
| 146 |
+
if (provider !== "sambanova") {
|
| 147 |
+
await writer.write(encoder.encode(chunk));
|
| 148 |
+
completeResponse += chunk;
|
| 149 |
+
|
| 150 |
+
if (completeResponse.includes("</html>")) {
|
| 151 |
+
break;
|
| 152 |
+
}
|
| 153 |
+
} else {
|
| 154 |
+
if (chunk.includes("</html>")) {
|
| 155 |
+
newChunk = newChunk.replace(/<\/html>[\s\S]*/, "</html>");
|
| 156 |
+
}
|
| 157 |
+
completeResponse += newChunk;
|
| 158 |
+
await writer.write(encoder.encode(newChunk));
|
| 159 |
+
if (newChunk.includes("</html>")) {
|
| 160 |
+
break;
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
} else {
|
| 164 |
+
const lastThinkTagIndex =
|
| 165 |
+
completeResponse.lastIndexOf("</think>");
|
| 166 |
+
completeResponse += newChunk;
|
| 167 |
+
await writer.write(encoder.encode(newChunk));
|
| 168 |
+
if (lastThinkTagIndex !== -1) {
|
| 169 |
+
const afterLastThinkTag = completeResponse.slice(
|
| 170 |
+
lastThinkTagIndex + "</think>".length
|
| 171 |
+
);
|
| 172 |
+
if (afterLastThinkTag.includes("</html>")) {
|
| 173 |
+
break;
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
}
|
| 178 |
}
|
|
|
|
|
|
|
| 179 |
} catch (error: any) {
|
| 180 |
if (error.message?.includes("exceeded your monthly included credits")) {
|
| 181 |
await writer.write(
|
|
|
|
| 187 |
})
|
| 188 |
)
|
| 189 |
);
|
| 190 |
+
} else {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
await writer.write(
|
| 192 |
encoder.encode(
|
| 193 |
JSON.stringify({
|
|
|
|
| 200 |
);
|
| 201 |
}
|
| 202 |
} finally {
|
| 203 |
+
await writer?.close();
|
|
|
|
|
|
|
|
|
|
| 204 |
}
|
| 205 |
})();
|
| 206 |
|
|
|
|
| 219 |
}
|
| 220 |
|
| 221 |
export async function PUT(request: NextRequest) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
const authHeaders = await headers();
|
| 223 |
+
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
| 224 |
|
| 225 |
const body = await request.json();
|
| 226 |
+
const { prompt, html, previousPrompt, provider, selectedElementHtml, model } =
|
| 227 |
body;
|
| 228 |
|
| 229 |
+
if (!prompt || !html) {
|
| 230 |
return NextResponse.json(
|
| 231 |
{ ok: false, error: "Missing required fields" },
|
| 232 |
{ status: 400 }
|
|
|
|
| 243 |
);
|
| 244 |
}
|
| 245 |
|
| 246 |
+
let token = userToken;
|
| 247 |
let billTo: string | null = null;
|
| 248 |
|
| 249 |
/**
|
|
|
|
| 276 |
billTo = "huggingface";
|
| 277 |
}
|
| 278 |
|
| 279 |
+
const client = new InferenceClient(token);
|
| 280 |
+
|
| 281 |
+
const DEFAULT_PROVIDER = PROVIDERS.novita;
|
| 282 |
+
const selectedProvider =
|
| 283 |
+
provider === "auto"
|
| 284 |
+
? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS]
|
| 285 |
+
: PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
|
| 286 |
|
| 287 |
try {
|
| 288 |
+
const response = await client.chatCompletion(
|
| 289 |
+
{
|
| 290 |
+
model: selectedModel.value,
|
| 291 |
+
provider: selectedProvider.id as any,
|
| 292 |
+
messages: [
|
| 293 |
+
{
|
| 294 |
+
role: "system",
|
| 295 |
+
content: FOLLOW_UP_SYSTEM_PROMPT,
|
| 296 |
+
},
|
| 297 |
+
{
|
| 298 |
+
role: "user",
|
| 299 |
+
content: previousPrompt
|
| 300 |
+
? previousPrompt
|
| 301 |
+
: "You are modifying the HTML file based on the user's request.",
|
| 302 |
+
},
|
| 303 |
+
{
|
| 304 |
+
role: "assistant",
|
| 305 |
|
| 306 |
+
content: `The current code is: \n\`\`\`html\n${html}\n\`\`\` ${
|
| 307 |
+
selectedElementHtml
|
| 308 |
+
? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\``
|
| 309 |
+
: ""
|
| 310 |
+
}`,
|
| 311 |
+
},
|
| 312 |
+
{
|
| 313 |
+
role: "user",
|
| 314 |
+
content: prompt,
|
| 315 |
+
},
|
| 316 |
+
],
|
| 317 |
+
...(selectedProvider.id !== "sambanova"
|
| 318 |
+
? {
|
| 319 |
+
max_tokens: selectedProvider.max_tokens,
|
| 320 |
+
}
|
| 321 |
+
: {}),
|
| 322 |
},
|
| 323 |
+
billTo ? { billTo } : {}
|
| 324 |
+
);
|
|
|
|
|
|
|
|
|
|
| 325 |
|
| 326 |
+
const chunk = response.choices[0]?.message?.content;
|
| 327 |
+
if (!chunk) {
|
| 328 |
+
return NextResponse.json(
|
| 329 |
+
{ ok: false, message: "No content returned from the model" },
|
| 330 |
+
{ status: 400 }
|
| 331 |
+
);
|
| 332 |
+
}
|
| 333 |
|
| 334 |
+
if (chunk) {
|
| 335 |
+
const updatedLines: number[][] = [];
|
| 336 |
+
let newHtml = html;
|
| 337 |
+
let position = 0;
|
| 338 |
+
let moreBlocks = true;
|
| 339 |
+
|
| 340 |
+
while (moreBlocks) {
|
| 341 |
+
const searchStartIndex = chunk.indexOf(SEARCH_START, position);
|
| 342 |
+
if (searchStartIndex === -1) {
|
| 343 |
+
moreBlocks = false;
|
| 344 |
+
continue;
|
| 345 |
+
}
|
| 346 |
|
| 347 |
+
const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
|
| 348 |
+
if (dividerIndex === -1) {
|
| 349 |
+
moreBlocks = false;
|
| 350 |
+
continue;
|
| 351 |
+
}
|
| 352 |
|
| 353 |
+
const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
|
| 354 |
+
if (replaceEndIndex === -1) {
|
| 355 |
+
moreBlocks = false;
|
| 356 |
+
continue;
|
| 357 |
+
}
|
| 358 |
|
| 359 |
+
const searchBlock = chunk.substring(
|
| 360 |
+
searchStartIndex + SEARCH_START.length,
|
| 361 |
+
dividerIndex
|
| 362 |
+
);
|
| 363 |
+
const replaceBlock = chunk.substring(
|
| 364 |
+
dividerIndex + DIVIDER.length,
|
| 365 |
+
replaceEndIndex
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
);
|
| 367 |
|
| 368 |
+
if (searchBlock.trim() === "") {
|
| 369 |
+
newHtml = `${replaceBlock}\n${newHtml}`;
|
| 370 |
+
updatedLines.push([1, replaceBlock.split("\n").length]);
|
| 371 |
+
} else {
|
| 372 |
+
const blockPosition = newHtml.indexOf(searchBlock);
|
| 373 |
+
if (blockPosition !== -1) {
|
| 374 |
+
const beforeText = newHtml.substring(0, blockPosition);
|
| 375 |
+
const startLineNumber = beforeText.split("\n").length;
|
| 376 |
+
const replaceLines = replaceBlock.split("\n").length;
|
| 377 |
+
const endLineNumber = startLineNumber + replaceLines - 1;
|
| 378 |
+
|
| 379 |
+
updatedLines.push([startLineNumber, endLineNumber]);
|
| 380 |
+
newHtml = newHtml.replace(searchBlock, replaceBlock);
|
| 381 |
}
|
| 382 |
}
|
| 383 |
|
| 384 |
+
position = replaceEndIndex + REPLACE_END.length;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
}
|
|
|
|
| 386 |
|
| 387 |
+
return NextResponse.json({
|
| 388 |
+
ok: true,
|
| 389 |
+
html: newHtml,
|
| 390 |
+
updatedLines,
|
| 391 |
+
});
|
| 392 |
+
} else {
|
| 393 |
+
return NextResponse.json(
|
| 394 |
+
{ ok: false, message: "No content returned from the model" },
|
| 395 |
+
{ status: 400 }
|
| 396 |
+
);
|
| 397 |
+
}
|
| 398 |
} catch (error: any) {
|
| 399 |
+
if (error.message?.includes("exceeded your monthly included credits")) {
|
| 400 |
+
return NextResponse.json(
|
| 401 |
+
{
|
| 402 |
+
ok: false,
|
| 403 |
+
openProModal: true,
|
| 404 |
+
message: error.message,
|
| 405 |
+
},
|
| 406 |
+
{ status: 402 }
|
| 407 |
+
);
|
| 408 |
+
}
|
| 409 |
return NextResponse.json(
|
| 410 |
{
|
| 411 |
ok: false,
|
|
|
|
| 417 |
);
|
| 418 |
}
|
| 419 |
}
|
|
|
app/api/auth/login-url/route.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
| 1 |
-
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
-
|
| 3 |
-
export async function GET(req: NextRequest) {
|
| 4 |
-
const host = req.headers.get("host") ?? "localhost:3000";
|
| 5 |
-
|
| 6 |
-
let url: string;
|
| 7 |
-
if (host.includes("localhost")) {
|
| 8 |
-
url = host;
|
| 9 |
-
} else {
|
| 10 |
-
url = "huggingface.co";
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
const redirect_uri =
|
| 14 |
-
`${host.includes("localhost") ? "http://" : "https://"}` +
|
| 15 |
-
url +
|
| 16 |
-
"/deepsite/auth/callback";
|
| 17 |
-
|
| 18 |
-
const loginRedirectUrl = `https://huggingface.co/oauth/authorize?client_id=${process.env.OAUTH_CLIENT_ID}&redirect_uri=${redirect_uri}&response_type=code&scope=openid%20profile%20write-repos%20manage-repos%20inference-api&prompt=consent&state=1234567890`;
|
| 19 |
-
|
| 20 |
-
return NextResponse.json({ loginUrl: loginRedirectUrl });
|
| 21 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/auth/logout/route.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
| 1 |
-
import { NextResponse } from "next/server";
|
| 2 |
-
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
| 3 |
-
|
| 4 |
-
export async function POST() {
|
| 5 |
-
const cookieName = MY_TOKEN_KEY();
|
| 6 |
-
const isProduction = process.env.NODE_ENV === "production";
|
| 7 |
-
|
| 8 |
-
const response = NextResponse.json(
|
| 9 |
-
{ message: "Logged out successfully" },
|
| 10 |
-
{ status: 200 }
|
| 11 |
-
);
|
| 12 |
-
|
| 13 |
-
// Clear the HTTP-only cookie
|
| 14 |
-
const cookieOptions = [
|
| 15 |
-
`${cookieName}=`,
|
| 16 |
-
"Max-Age=0",
|
| 17 |
-
"Path=/",
|
| 18 |
-
"HttpOnly",
|
| 19 |
-
...(isProduction ? ["Secure", "SameSite=None"] : ["SameSite=Lax"])
|
| 20 |
-
].join("; ");
|
| 21 |
-
|
| 22 |
-
response.headers.set("Set-Cookie", cookieOptions);
|
| 23 |
-
|
| 24 |
-
return response;
|
| 25 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/auth/route.ts
CHANGED
|
@@ -24,12 +24,12 @@ export async function POST(req: NextRequest) {
|
|
| 24 |
req.headers.get("host") ?? req.headers.get("origin") ?? "localhost:3000";
|
| 25 |
|
| 26 |
const url = host.includes("/spaces/enzostvs")
|
| 27 |
-
? "
|
| 28 |
: host;
|
| 29 |
const redirect_uri =
|
| 30 |
`${host.includes("localhost") ? "http://" : "https://"}` +
|
| 31 |
url +
|
| 32 |
-
"/
|
| 33 |
const request_auth = await fetch("https://huggingface.co/oauth/token", {
|
| 34 |
method: "POST",
|
| 35 |
headers: {
|
|
|
|
| 24 |
req.headers.get("host") ?? req.headers.get("origin") ?? "localhost:3000";
|
| 25 |
|
| 26 |
const url = host.includes("/spaces/enzostvs")
|
| 27 |
+
? "enzostvs-deepsite.hf.space"
|
| 28 |
: host;
|
| 29 |
const redirect_uri =
|
| 30 |
`${host.includes("localhost") ? "http://" : "https://"}` +
|
| 31 |
url +
|
| 32 |
+
"/auth/callback";
|
| 33 |
const request_auth = await fetch("https://huggingface.co/oauth/token", {
|
| 34 |
method: "POST",
|
| 35 |
headers: {
|
app/api/mcp/route.ts
DELETED
|
@@ -1,435 +0,0 @@
|
|
| 1 |
-
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
-
import { RepoDesignation, createRepo, uploadFiles, spaceInfo, listCommits } from "@huggingface/hub";
|
| 3 |
-
import { COLORS } from "@/lib/utils";
|
| 4 |
-
import { injectDeepSiteBadge, isIndexPage } from "@/lib/inject-badge";
|
| 5 |
-
import { Commit, Page } from "@/types";
|
| 6 |
-
|
| 7 |
-
// Timeout configuration (in milliseconds)
|
| 8 |
-
const OPERATION_TIMEOUT = 120000; // 2 minutes for HF operations
|
| 9 |
-
|
| 10 |
-
// Extend the maximum execution time for this route
|
| 11 |
-
export const maxDuration = 180; // 3 minutes
|
| 12 |
-
|
| 13 |
-
// Utility function to wrap promises with timeout
|
| 14 |
-
async function withTimeout<T>(
|
| 15 |
-
promise: Promise<T>,
|
| 16 |
-
timeoutMs: number,
|
| 17 |
-
errorMessage: string = "Operation timed out"
|
| 18 |
-
): Promise<T> {
|
| 19 |
-
let timeoutId: NodeJS.Timeout;
|
| 20 |
-
|
| 21 |
-
const timeoutPromise = new Promise<never>((_, reject) => {
|
| 22 |
-
timeoutId = setTimeout(() => {
|
| 23 |
-
reject(new Error(errorMessage));
|
| 24 |
-
}, timeoutMs);
|
| 25 |
-
});
|
| 26 |
-
|
| 27 |
-
try {
|
| 28 |
-
const result = await Promise.race([promise, timeoutPromise]);
|
| 29 |
-
clearTimeout(timeoutId!);
|
| 30 |
-
return result;
|
| 31 |
-
} catch (error) {
|
| 32 |
-
clearTimeout(timeoutId!);
|
| 33 |
-
throw error;
|
| 34 |
-
}
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
interface MCPRequest {
|
| 38 |
-
jsonrpc: "2.0";
|
| 39 |
-
id: number | string;
|
| 40 |
-
method: string;
|
| 41 |
-
params?: any;
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
interface MCPResponse {
|
| 45 |
-
jsonrpc: "2.0";
|
| 46 |
-
id: number | string;
|
| 47 |
-
result?: any;
|
| 48 |
-
error?: {
|
| 49 |
-
code: number;
|
| 50 |
-
message: string;
|
| 51 |
-
data?: any;
|
| 52 |
-
};
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
interface CreateProjectParams {
|
| 56 |
-
title?: string;
|
| 57 |
-
pages: Page[];
|
| 58 |
-
prompt?: string;
|
| 59 |
-
hf_token?: string; // Optional - can come from header instead
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
// MCP Server over HTTP
|
| 63 |
-
export async function POST(req: NextRequest) {
|
| 64 |
-
try {
|
| 65 |
-
const body: MCPRequest = await req.json();
|
| 66 |
-
const { jsonrpc, id, method, params } = body;
|
| 67 |
-
|
| 68 |
-
// Validate JSON-RPC 2.0 format
|
| 69 |
-
if (jsonrpc !== "2.0") {
|
| 70 |
-
return NextResponse.json({
|
| 71 |
-
jsonrpc: "2.0",
|
| 72 |
-
id: id || null,
|
| 73 |
-
error: {
|
| 74 |
-
code: -32600,
|
| 75 |
-
message: "Invalid Request: jsonrpc must be '2.0'",
|
| 76 |
-
},
|
| 77 |
-
});
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
let response: MCPResponse;
|
| 81 |
-
|
| 82 |
-
switch (method) {
|
| 83 |
-
case "initialize":
|
| 84 |
-
response = {
|
| 85 |
-
jsonrpc: "2.0",
|
| 86 |
-
id,
|
| 87 |
-
result: {
|
| 88 |
-
protocolVersion: "2024-11-05",
|
| 89 |
-
capabilities: {
|
| 90 |
-
tools: {},
|
| 91 |
-
},
|
| 92 |
-
serverInfo: {
|
| 93 |
-
name: "deepsite-mcp-server",
|
| 94 |
-
version: "1.0.0",
|
| 95 |
-
},
|
| 96 |
-
},
|
| 97 |
-
};
|
| 98 |
-
break;
|
| 99 |
-
|
| 100 |
-
case "tools/list":
|
| 101 |
-
response = {
|
| 102 |
-
jsonrpc: "2.0",
|
| 103 |
-
id,
|
| 104 |
-
result: {
|
| 105 |
-
tools: [
|
| 106 |
-
{
|
| 107 |
-
name: "create_project",
|
| 108 |
-
description: `Create a new DeepSite project. This will create a new Hugging Face Space with your HTML/CSS/JS files.
|
| 109 |
-
|
| 110 |
-
Example usage:
|
| 111 |
-
- Create a simple website with HTML, CSS, and JavaScript files
|
| 112 |
-
- Each page needs a 'path' (filename like "index.html", "styles.css", "script.js") and 'html' (the actual content)
|
| 113 |
-
- The title will be formatted to a valid repository name
|
| 114 |
-
- Returns the project URL and metadata`,
|
| 115 |
-
inputSchema: {
|
| 116 |
-
type: "object",
|
| 117 |
-
properties: {
|
| 118 |
-
title: {
|
| 119 |
-
type: "string",
|
| 120 |
-
description: "Project title (optional, defaults to 'DeepSite Project'). Will be formatted to a valid repo name.",
|
| 121 |
-
},
|
| 122 |
-
pages: {
|
| 123 |
-
type: "array",
|
| 124 |
-
description: "Array of files to include in the project",
|
| 125 |
-
items: {
|
| 126 |
-
type: "object",
|
| 127 |
-
properties: {
|
| 128 |
-
path: {
|
| 129 |
-
type: "string",
|
| 130 |
-
description: "File path (e.g., 'index.html', 'styles.css', 'script.js')",
|
| 131 |
-
},
|
| 132 |
-
html: {
|
| 133 |
-
type: "string",
|
| 134 |
-
description: "File content",
|
| 135 |
-
},
|
| 136 |
-
},
|
| 137 |
-
required: ["path", "html"],
|
| 138 |
-
},
|
| 139 |
-
},
|
| 140 |
-
prompt: {
|
| 141 |
-
type: "string",
|
| 142 |
-
description: "Optional prompt/description for the commit message",
|
| 143 |
-
},
|
| 144 |
-
hf_token: {
|
| 145 |
-
type: "string",
|
| 146 |
-
description: "Hugging Face API token (optional if provided via Authorization header)",
|
| 147 |
-
},
|
| 148 |
-
},
|
| 149 |
-
required: ["pages"],
|
| 150 |
-
},
|
| 151 |
-
},
|
| 152 |
-
],
|
| 153 |
-
},
|
| 154 |
-
};
|
| 155 |
-
break;
|
| 156 |
-
|
| 157 |
-
case "tools/call":
|
| 158 |
-
const { name, arguments: toolArgs } = params;
|
| 159 |
-
|
| 160 |
-
if (name === "create_project") {
|
| 161 |
-
try {
|
| 162 |
-
// Extract token from Authorization header if present
|
| 163 |
-
const authHeader = req.headers.get("authorization");
|
| 164 |
-
let hf_token = toolArgs.hf_token;
|
| 165 |
-
|
| 166 |
-
if (authHeader && authHeader.startsWith("Bearer ")) {
|
| 167 |
-
hf_token = authHeader.substring(7); // Remove "Bearer " prefix
|
| 168 |
-
}
|
| 169 |
-
|
| 170 |
-
const result = await handleCreateProject({
|
| 171 |
-
...toolArgs,
|
| 172 |
-
hf_token,
|
| 173 |
-
} as CreateProjectParams);
|
| 174 |
-
response = {
|
| 175 |
-
jsonrpc: "2.0",
|
| 176 |
-
id,
|
| 177 |
-
result,
|
| 178 |
-
};
|
| 179 |
-
} catch (error: any) {
|
| 180 |
-
response = {
|
| 181 |
-
jsonrpc: "2.0",
|
| 182 |
-
id,
|
| 183 |
-
error: {
|
| 184 |
-
code: -32000,
|
| 185 |
-
message: error.message || "Failed to create project",
|
| 186 |
-
data: error.data,
|
| 187 |
-
},
|
| 188 |
-
};
|
| 189 |
-
}
|
| 190 |
-
} else {
|
| 191 |
-
response = {
|
| 192 |
-
jsonrpc: "2.0",
|
| 193 |
-
id,
|
| 194 |
-
error: {
|
| 195 |
-
code: -32601,
|
| 196 |
-
message: `Unknown tool: ${name}`,
|
| 197 |
-
},
|
| 198 |
-
};
|
| 199 |
-
}
|
| 200 |
-
break;
|
| 201 |
-
|
| 202 |
-
default:
|
| 203 |
-
response = {
|
| 204 |
-
jsonrpc: "2.0",
|
| 205 |
-
id,
|
| 206 |
-
error: {
|
| 207 |
-
code: -32601,
|
| 208 |
-
message: `Method not found: ${method}`,
|
| 209 |
-
},
|
| 210 |
-
};
|
| 211 |
-
}
|
| 212 |
-
|
| 213 |
-
return NextResponse.json(response);
|
| 214 |
-
} catch (error: any) {
|
| 215 |
-
return NextResponse.json({
|
| 216 |
-
jsonrpc: "2.0",
|
| 217 |
-
id: null,
|
| 218 |
-
error: {
|
| 219 |
-
code: -32700,
|
| 220 |
-
message: "Parse error",
|
| 221 |
-
data: error.message,
|
| 222 |
-
},
|
| 223 |
-
});
|
| 224 |
-
}
|
| 225 |
-
}
|
| 226 |
-
|
| 227 |
-
// Handle OPTIONS for CORS
|
| 228 |
-
export async function OPTIONS() {
|
| 229 |
-
return new NextResponse(null, {
|
| 230 |
-
status: 200,
|
| 231 |
-
headers: {
|
| 232 |
-
"Access-Control-Allow-Origin": "*",
|
| 233 |
-
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
| 234 |
-
"Access-Control-Allow-Headers": "Content-Type",
|
| 235 |
-
},
|
| 236 |
-
});
|
| 237 |
-
}
|
| 238 |
-
|
| 239 |
-
async function handleCreateProject(params: CreateProjectParams) {
|
| 240 |
-
const { title: titleFromRequest, pages, prompt, hf_token } = params;
|
| 241 |
-
|
| 242 |
-
// Validate required parameters
|
| 243 |
-
if (!hf_token || typeof hf_token !== "string") {
|
| 244 |
-
throw new Error("hf_token is required and must be a string");
|
| 245 |
-
}
|
| 246 |
-
|
| 247 |
-
if (!pages || !Array.isArray(pages) || pages.length === 0) {
|
| 248 |
-
throw new Error("At least one page is required");
|
| 249 |
-
}
|
| 250 |
-
|
| 251 |
-
// Validate that each page has required fields
|
| 252 |
-
for (const page of pages) {
|
| 253 |
-
if (!page.path || !page.html) {
|
| 254 |
-
throw new Error("Each page must have 'path' and 'html' properties");
|
| 255 |
-
}
|
| 256 |
-
}
|
| 257 |
-
|
| 258 |
-
// Get user info from HF token
|
| 259 |
-
let username: string;
|
| 260 |
-
try {
|
| 261 |
-
const userResponse = await withTimeout(
|
| 262 |
-
fetch("https://huggingface.co/api/whoami-v2", {
|
| 263 |
-
headers: {
|
| 264 |
-
Authorization: `Bearer ${hf_token}`,
|
| 265 |
-
},
|
| 266 |
-
}),
|
| 267 |
-
30000, // 30 seconds for authentication
|
| 268 |
-
"Authentication timeout: Unable to verify Hugging Face token"
|
| 269 |
-
);
|
| 270 |
-
|
| 271 |
-
if (!userResponse.ok) {
|
| 272 |
-
throw new Error("Invalid Hugging Face token");
|
| 273 |
-
}
|
| 274 |
-
|
| 275 |
-
const userData = await userResponse.json();
|
| 276 |
-
username = userData.name;
|
| 277 |
-
} catch (error: any) {
|
| 278 |
-
if (error.message?.includes('timeout')) {
|
| 279 |
-
throw new Error(`Authentication timeout: ${error.message}`);
|
| 280 |
-
}
|
| 281 |
-
throw new Error(`Authentication failed: ${error.message}`);
|
| 282 |
-
}
|
| 283 |
-
|
| 284 |
-
const title = titleFromRequest ?? "DeepSite Project";
|
| 285 |
-
|
| 286 |
-
const formattedTitle = title
|
| 287 |
-
.toLowerCase()
|
| 288 |
-
.replace(/[^a-z0-9]+/g, "-")
|
| 289 |
-
.split("-")
|
| 290 |
-
.filter(Boolean)
|
| 291 |
-
.join("-")
|
| 292 |
-
.slice(0, 96);
|
| 293 |
-
|
| 294 |
-
const repo: RepoDesignation = {
|
| 295 |
-
type: "space",
|
| 296 |
-
name: `${username}/${formattedTitle}`,
|
| 297 |
-
};
|
| 298 |
-
|
| 299 |
-
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 300 |
-
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 301 |
-
const README = `---
|
| 302 |
-
title: ${title}
|
| 303 |
-
colorFrom: ${colorFrom}
|
| 304 |
-
colorTo: ${colorTo}
|
| 305 |
-
emoji: 🐳
|
| 306 |
-
sdk: static
|
| 307 |
-
pinned: false
|
| 308 |
-
tags:
|
| 309 |
-
- deepsite-v3
|
| 310 |
-
---
|
| 311 |
-
|
| 312 |
-
# Welcome to your new DeepSite project!
|
| 313 |
-
This project was created with [DeepSite](https://huggingface.co/deepsite).
|
| 314 |
-
`;
|
| 315 |
-
|
| 316 |
-
const files: File[] = [];
|
| 317 |
-
const readmeFile = new File([README], "README.md", { type: "text/markdown" });
|
| 318 |
-
files.push(readmeFile);
|
| 319 |
-
|
| 320 |
-
pages.forEach((page: Page) => {
|
| 321 |
-
// Determine MIME type based on file extension
|
| 322 |
-
let mimeType = "text/html";
|
| 323 |
-
if (page.path.endsWith(".css")) {
|
| 324 |
-
mimeType = "text/css";
|
| 325 |
-
} else if (page.path.endsWith(".js")) {
|
| 326 |
-
mimeType = "text/javascript";
|
| 327 |
-
} else if (page.path.endsWith(".json")) {
|
| 328 |
-
mimeType = "application/json";
|
| 329 |
-
}
|
| 330 |
-
|
| 331 |
-
// Inject the DeepSite badge script into index pages only
|
| 332 |
-
const content = mimeType === "text/html" && isIndexPage(page.path)
|
| 333 |
-
? injectDeepSiteBadge(page.html)
|
| 334 |
-
: page.html;
|
| 335 |
-
const file = new File([content], page.path, { type: mimeType });
|
| 336 |
-
files.push(file);
|
| 337 |
-
});
|
| 338 |
-
|
| 339 |
-
try {
|
| 340 |
-
const { repoUrl } = await withTimeout(
|
| 341 |
-
createRepo({
|
| 342 |
-
repo,
|
| 343 |
-
accessToken: hf_token,
|
| 344 |
-
}),
|
| 345 |
-
60000, // 1 minute for repo creation
|
| 346 |
-
"Timeout creating repository. Please try again."
|
| 347 |
-
);
|
| 348 |
-
|
| 349 |
-
const commitTitle = !prompt || prompt.trim() === "" ? "Initial project creation via MCP" : prompt;
|
| 350 |
-
|
| 351 |
-
await withTimeout(
|
| 352 |
-
uploadFiles({
|
| 353 |
-
repo,
|
| 354 |
-
files,
|
| 355 |
-
accessToken: hf_token,
|
| 356 |
-
commitTitle,
|
| 357 |
-
}),
|
| 358 |
-
OPERATION_TIMEOUT,
|
| 359 |
-
"Timeout uploading files. The repository was created but files may not have been uploaded."
|
| 360 |
-
);
|
| 361 |
-
|
| 362 |
-
const path = repoUrl.split("/").slice(-2).join("/");
|
| 363 |
-
|
| 364 |
-
const commits: Commit[] = [];
|
| 365 |
-
const commitIterator = listCommits({ repo, accessToken: hf_token });
|
| 366 |
-
|
| 367 |
-
// Wrap the commit listing with a timeout
|
| 368 |
-
const commitTimeout = new Promise<void>((_, reject) => {
|
| 369 |
-
setTimeout(() => reject(new Error("Timeout listing commits")), 30000);
|
| 370 |
-
});
|
| 371 |
-
|
| 372 |
-
try {
|
| 373 |
-
await Promise.race([
|
| 374 |
-
(async () => {
|
| 375 |
-
for await (const commit of commitIterator) {
|
| 376 |
-
if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Promote version")) {
|
| 377 |
-
continue;
|
| 378 |
-
}
|
| 379 |
-
commits.push({
|
| 380 |
-
title: commit.title,
|
| 381 |
-
oid: commit.oid,
|
| 382 |
-
date: commit.date,
|
| 383 |
-
});
|
| 384 |
-
}
|
| 385 |
-
})(),
|
| 386 |
-
commitTimeout
|
| 387 |
-
]);
|
| 388 |
-
} catch (error: any) {
|
| 389 |
-
// If listing commits times out, continue with empty commits array
|
| 390 |
-
console.error("Failed to list commits:", error.message);
|
| 391 |
-
}
|
| 392 |
-
|
| 393 |
-
const space = await withTimeout(
|
| 394 |
-
spaceInfo({
|
| 395 |
-
name: repo.name,
|
| 396 |
-
accessToken: hf_token,
|
| 397 |
-
}),
|
| 398 |
-
30000, // 30 seconds for space info
|
| 399 |
-
"Timeout fetching space information"
|
| 400 |
-
);
|
| 401 |
-
|
| 402 |
-
const projectUrl = `https://huggingface.co/deepsite/${path}`;
|
| 403 |
-
const spaceUrl = `https://huggingface.co/spaces/${path}`;
|
| 404 |
-
const liveUrl = `https://${username}-${formattedTitle}.hf.space`;
|
| 405 |
-
|
| 406 |
-
return {
|
| 407 |
-
content: [
|
| 408 |
-
{
|
| 409 |
-
type: "text",
|
| 410 |
-
text: JSON.stringify(
|
| 411 |
-
{
|
| 412 |
-
success: true,
|
| 413 |
-
message: "Project created successfully!",
|
| 414 |
-
projectUrl,
|
| 415 |
-
spaceUrl,
|
| 416 |
-
liveUrl,
|
| 417 |
-
spaceId: space.name,
|
| 418 |
-
projectId: space.id,
|
| 419 |
-
files: pages.map((p) => p.path),
|
| 420 |
-
updatedAt: space.updatedAt,
|
| 421 |
-
},
|
| 422 |
-
null,
|
| 423 |
-
2
|
| 424 |
-
),
|
| 425 |
-
},
|
| 426 |
-
],
|
| 427 |
-
};
|
| 428 |
-
} catch (err: any) {
|
| 429 |
-
if (err.message?.includes('timeout') || err.message?.includes('Timeout')) {
|
| 430 |
-
throw new Error(err.message || "Operation timed out. Please try again.");
|
| 431 |
-
}
|
| 432 |
-
throw new Error(err.message || "Failed to create project");
|
| 433 |
-
}
|
| 434 |
-
}
|
| 435 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts
DELETED
|
@@ -1,190 +0,0 @@
|
|
| 1 |
-
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
-
import { RepoDesignation, listFiles, spaceInfo, uploadFiles, deleteFiles } from "@huggingface/hub";
|
| 3 |
-
|
| 4 |
-
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
-
import { Page } from "@/types";
|
| 6 |
-
|
| 7 |
-
export async function POST(
|
| 8 |
-
req: NextRequest,
|
| 9 |
-
{ params }: {
|
| 10 |
-
params: Promise<{
|
| 11 |
-
namespace: string;
|
| 12 |
-
repoId: string;
|
| 13 |
-
commitId: string;
|
| 14 |
-
}>
|
| 15 |
-
}
|
| 16 |
-
) {
|
| 17 |
-
const user = await isAuthenticated();
|
| 18 |
-
|
| 19 |
-
if (user instanceof NextResponse || !user) {
|
| 20 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
const param = await params;
|
| 24 |
-
const { namespace, repoId, commitId } = param;
|
| 25 |
-
|
| 26 |
-
try {
|
| 27 |
-
const repo: RepoDesignation = {
|
| 28 |
-
type: "space",
|
| 29 |
-
name: `${namespace}/${repoId}`,
|
| 30 |
-
};
|
| 31 |
-
|
| 32 |
-
const space = await spaceInfo({
|
| 33 |
-
name: `${namespace}/${repoId}`,
|
| 34 |
-
accessToken: user.token as string,
|
| 35 |
-
additionalFields: ["author"],
|
| 36 |
-
});
|
| 37 |
-
|
| 38 |
-
if (!space || space.sdk !== "static") {
|
| 39 |
-
return NextResponse.json(
|
| 40 |
-
{ ok: false, error: "Space is not a static space." },
|
| 41 |
-
{ status: 404 }
|
| 42 |
-
);
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
if (space.author !== user.name) {
|
| 46 |
-
return NextResponse.json(
|
| 47 |
-
{ ok: false, error: "Space does not belong to the authenticated user." },
|
| 48 |
-
{ status: 403 }
|
| 49 |
-
);
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
// Fetch files from the specific commit
|
| 53 |
-
const files: File[] = [];
|
| 54 |
-
const pages: Page[] = [];
|
| 55 |
-
const allowedExtensions = ["html", "md", "css", "js", "json", "txt"];
|
| 56 |
-
const commitFilePaths: Set<string> = new Set();
|
| 57 |
-
|
| 58 |
-
// Get all files from the specific commit
|
| 59 |
-
for await (const fileInfo of listFiles({
|
| 60 |
-
repo,
|
| 61 |
-
accessToken: user.token as string,
|
| 62 |
-
revision: commitId,
|
| 63 |
-
})) {
|
| 64 |
-
const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
|
| 65 |
-
|
| 66 |
-
if (allowedExtensions.includes(fileExtension || "")) {
|
| 67 |
-
commitFilePaths.add(fileInfo.path);
|
| 68 |
-
|
| 69 |
-
// Fetch the file content from the specific commit
|
| 70 |
-
const response = await fetch(
|
| 71 |
-
`https://huggingface.co/spaces/${namespace}/${repoId}/raw/${commitId}/${fileInfo.path}`
|
| 72 |
-
);
|
| 73 |
-
|
| 74 |
-
if (response.ok) {
|
| 75 |
-
const content = await response.text();
|
| 76 |
-
let mimeType = "text/plain";
|
| 77 |
-
|
| 78 |
-
switch (fileExtension) {
|
| 79 |
-
case "html":
|
| 80 |
-
mimeType = "text/html";
|
| 81 |
-
// Add HTML files to pages array for client-side setPages
|
| 82 |
-
pages.push({
|
| 83 |
-
path: fileInfo.path,
|
| 84 |
-
html: content,
|
| 85 |
-
});
|
| 86 |
-
break;
|
| 87 |
-
case "css":
|
| 88 |
-
mimeType = "text/css";
|
| 89 |
-
break;
|
| 90 |
-
case "js":
|
| 91 |
-
mimeType = "application/javascript";
|
| 92 |
-
break;
|
| 93 |
-
case "json":
|
| 94 |
-
mimeType = "application/json";
|
| 95 |
-
break;
|
| 96 |
-
case "md":
|
| 97 |
-
mimeType = "text/markdown";
|
| 98 |
-
break;
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
const file = new File([content], fileInfo.path, { type: mimeType });
|
| 102 |
-
files.push(file);
|
| 103 |
-
}
|
| 104 |
-
}
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
// Get files currently in main branch to identify files to delete
|
| 108 |
-
const mainBranchFilePaths: Set<string> = new Set();
|
| 109 |
-
for await (const fileInfo of listFiles({
|
| 110 |
-
repo,
|
| 111 |
-
accessToken: user.token as string,
|
| 112 |
-
revision: "main",
|
| 113 |
-
})) {
|
| 114 |
-
const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
|
| 115 |
-
|
| 116 |
-
if (allowedExtensions.includes(fileExtension || "")) {
|
| 117 |
-
mainBranchFilePaths.add(fileInfo.path);
|
| 118 |
-
}
|
| 119 |
-
}
|
| 120 |
-
|
| 121 |
-
// Identify files to delete (exist in main but not in commit)
|
| 122 |
-
const filesToDelete: string[] = [];
|
| 123 |
-
for (const mainFilePath of mainBranchFilePaths) {
|
| 124 |
-
if (!commitFilePaths.has(mainFilePath)) {
|
| 125 |
-
filesToDelete.push(mainFilePath);
|
| 126 |
-
}
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
-
if (files.length === 0 && filesToDelete.length === 0) {
|
| 130 |
-
return NextResponse.json(
|
| 131 |
-
{ ok: false, error: "No files found in the specified commit and no files to delete" },
|
| 132 |
-
{ status: 404 }
|
| 133 |
-
);
|
| 134 |
-
}
|
| 135 |
-
|
| 136 |
-
// Delete files that exist in main but not in the commit being promoted
|
| 137 |
-
if (filesToDelete.length > 0) {
|
| 138 |
-
await deleteFiles({
|
| 139 |
-
repo,
|
| 140 |
-
paths: filesToDelete,
|
| 141 |
-
accessToken: user.token as string,
|
| 142 |
-
commitTitle: `Removed files from promoting ${commitId.slice(0, 7)}`,
|
| 143 |
-
commitDescription: `Removed files that don't exist in commit ${commitId}:\n${filesToDelete.map(path => `- ${path}`).join('\n')}`,
|
| 144 |
-
});
|
| 145 |
-
}
|
| 146 |
-
|
| 147 |
-
// Upload the files to the main branch with a promotion commit message
|
| 148 |
-
if (files.length > 0) {
|
| 149 |
-
await uploadFiles({
|
| 150 |
-
repo,
|
| 151 |
-
files,
|
| 152 |
-
accessToken: user.token as string,
|
| 153 |
-
commitTitle: `Promote version ${commitId.slice(0, 7)} to main`,
|
| 154 |
-
commitDescription: `Promoted commit ${commitId} to main branch`,
|
| 155 |
-
});
|
| 156 |
-
}
|
| 157 |
-
|
| 158 |
-
return NextResponse.json(
|
| 159 |
-
{
|
| 160 |
-
ok: true,
|
| 161 |
-
message: "Version promoted successfully",
|
| 162 |
-
promotedCommit: commitId,
|
| 163 |
-
pages: pages,
|
| 164 |
-
},
|
| 165 |
-
{ status: 200 }
|
| 166 |
-
);
|
| 167 |
-
|
| 168 |
-
} catch (error: any) {
|
| 169 |
-
|
| 170 |
-
// Handle specific HuggingFace API errors
|
| 171 |
-
if (error.statusCode === 404) {
|
| 172 |
-
return NextResponse.json(
|
| 173 |
-
{ ok: false, error: "Commit not found" },
|
| 174 |
-
{ status: 404 }
|
| 175 |
-
);
|
| 176 |
-
}
|
| 177 |
-
|
| 178 |
-
if (error.statusCode === 403) {
|
| 179 |
-
return NextResponse.json(
|
| 180 |
-
{ ok: false, error: "Access denied to repository" },
|
| 181 |
-
{ status: 403 }
|
| 182 |
-
);
|
| 183 |
-
}
|
| 184 |
-
|
| 185 |
-
return NextResponse.json(
|
| 186 |
-
{ ok: false, error: error.message || "Failed to promote version" },
|
| 187 |
-
{ status: 500 }
|
| 188 |
-
);
|
| 189 |
-
}
|
| 190 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/me/projects/[namespace]/[repoId]/images/route.ts
DELETED
|
@@ -1,125 +0,0 @@
|
|
| 1 |
-
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
-
import { RepoDesignation, spaceInfo, uploadFiles } from "@huggingface/hub";
|
| 3 |
-
|
| 4 |
-
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
-
import Project from "@/models/Project";
|
| 6 |
-
import dbConnect from "@/lib/mongodb";
|
| 7 |
-
|
| 8 |
-
export async function POST(
|
| 9 |
-
req: NextRequest,
|
| 10 |
-
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
| 11 |
-
) {
|
| 12 |
-
try {
|
| 13 |
-
const user = await isAuthenticated();
|
| 14 |
-
|
| 15 |
-
if (user instanceof NextResponse || !user) {
|
| 16 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
const param = await params;
|
| 20 |
-
const { namespace, repoId } = param;
|
| 21 |
-
|
| 22 |
-
const space = await spaceInfo({
|
| 23 |
-
name: `${namespace}/${repoId}`,
|
| 24 |
-
accessToken: user.token as string,
|
| 25 |
-
additionalFields: ["author"],
|
| 26 |
-
});
|
| 27 |
-
|
| 28 |
-
if (!space || space.sdk !== "static") {
|
| 29 |
-
return NextResponse.json(
|
| 30 |
-
{ ok: false, error: "Space is not a static space." },
|
| 31 |
-
{ status: 404 }
|
| 32 |
-
);
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
if (space.author !== user.name) {
|
| 36 |
-
return NextResponse.json(
|
| 37 |
-
{ ok: false, error: "Space does not belong to the authenticated user." },
|
| 38 |
-
{ status: 403 }
|
| 39 |
-
);
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
// Parse the FormData to get the media files
|
| 43 |
-
const formData = await req.formData();
|
| 44 |
-
const mediaFiles = formData.getAll("images") as File[];
|
| 45 |
-
|
| 46 |
-
if (!mediaFiles || mediaFiles.length === 0) {
|
| 47 |
-
return NextResponse.json(
|
| 48 |
-
{
|
| 49 |
-
ok: false,
|
| 50 |
-
error: "At least one media file is required under the 'images' key",
|
| 51 |
-
},
|
| 52 |
-
{ status: 400 }
|
| 53 |
-
);
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
const files: File[] = [];
|
| 57 |
-
for (const file of mediaFiles) {
|
| 58 |
-
if (!(file instanceof File)) {
|
| 59 |
-
return NextResponse.json(
|
| 60 |
-
{
|
| 61 |
-
ok: false,
|
| 62 |
-
error: "Invalid file format - all items under 'images' key must be files",
|
| 63 |
-
},
|
| 64 |
-
{ status: 400 }
|
| 65 |
-
);
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
// Check if file is a supported media type
|
| 69 |
-
const isImage = file.type.startsWith('image/');
|
| 70 |
-
const isVideo = file.type.startsWith('video/');
|
| 71 |
-
const isAudio = file.type.startsWith('audio/');
|
| 72 |
-
|
| 73 |
-
if (!isImage && !isVideo && !isAudio) {
|
| 74 |
-
return NextResponse.json(
|
| 75 |
-
{
|
| 76 |
-
ok: false,
|
| 77 |
-
error: `File ${file.name} is not a supported media type (image, video, or audio)`,
|
| 78 |
-
},
|
| 79 |
-
{ status: 400 }
|
| 80 |
-
);
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
// Create File object with appropriate folder prefix
|
| 84 |
-
let folderPrefix = 'images/';
|
| 85 |
-
if (isVideo) {
|
| 86 |
-
folderPrefix = 'videos/';
|
| 87 |
-
} else if (isAudio) {
|
| 88 |
-
folderPrefix = 'audio/';
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
const fileName = `${folderPrefix}${file.name}`;
|
| 92 |
-
const processedFile = new File([file], fileName, { type: file.type });
|
| 93 |
-
files.push(processedFile);
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
// Upload files to HuggingFace space
|
| 97 |
-
const repo: RepoDesignation = {
|
| 98 |
-
type: "space",
|
| 99 |
-
name: `${namespace}/${repoId}`,
|
| 100 |
-
};
|
| 101 |
-
|
| 102 |
-
await uploadFiles({
|
| 103 |
-
repo,
|
| 104 |
-
files,
|
| 105 |
-
accessToken: user.token as string,
|
| 106 |
-
commitTitle: `Upload ${files.length} media file(s)`,
|
| 107 |
-
});
|
| 108 |
-
|
| 109 |
-
return NextResponse.json({
|
| 110 |
-
ok: true,
|
| 111 |
-
message: `Successfully uploaded ${files.length} media file(s) to ${namespace}/${repoId}/`,
|
| 112 |
-
uploadedFiles: files.map((file) => `https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${file.name}`),
|
| 113 |
-
}, { status: 200 });
|
| 114 |
-
|
| 115 |
-
} catch (error) {
|
| 116 |
-
console.error('Error uploading media files:', error);
|
| 117 |
-
return NextResponse.json(
|
| 118 |
-
{
|
| 119 |
-
ok: false,
|
| 120 |
-
error: "Failed to upload media files",
|
| 121 |
-
},
|
| 122 |
-
{ status: 500 }
|
| 123 |
-
);
|
| 124 |
-
}
|
| 125 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/me/projects/[namespace]/[repoId]/route.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
-
import { RepoDesignation, spaceInfo,
|
| 3 |
|
| 4 |
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
-
import
|
|
|
|
|
|
|
| 6 |
|
| 7 |
-
export async function
|
| 8 |
req: NextRequest,
|
| 9 |
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
| 10 |
) {
|
|
@@ -14,63 +16,24 @@ export async function DELETE(
|
|
| 14 |
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 15 |
}
|
| 16 |
|
|
|
|
| 17 |
const param = await params;
|
| 18 |
const { namespace, repoId } = param;
|
| 19 |
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
});
|
| 26 |
-
|
| 27 |
-
if (!space || space.sdk !== "static") {
|
| 28 |
-
return NextResponse.json(
|
| 29 |
-
{ ok: false, error: "Space is not a static space." },
|
| 30 |
-
{ status: 404 }
|
| 31 |
-
);
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
if (space.author !== user.name) {
|
| 35 |
-
return NextResponse.json(
|
| 36 |
-
{ ok: false, error: "Space does not belong to the authenticated user." },
|
| 37 |
-
{ status: 403 }
|
| 38 |
-
);
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
const repo: RepoDesignation = {
|
| 42 |
-
type: "space",
|
| 43 |
-
name: `${namespace}/${repoId}`,
|
| 44 |
-
};
|
| 45 |
-
|
| 46 |
-
await deleteRepo({
|
| 47 |
-
repo,
|
| 48 |
-
accessToken: user.token as string,
|
| 49 |
-
});
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
return NextResponse.json({ ok: true }, { status: 200 });
|
| 53 |
-
} catch (error: any) {
|
| 54 |
return NextResponse.json(
|
| 55 |
-
{
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
| 57 |
);
|
| 58 |
}
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
export async function GET(
|
| 62 |
-
req: NextRequest,
|
| 63 |
-
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
| 64 |
-
) {
|
| 65 |
-
const user = await isAuthenticated();
|
| 66 |
-
|
| 67 |
-
if (user instanceof NextResponse || !user) {
|
| 68 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 69 |
-
}
|
| 70 |
-
|
| 71 |
-
const param = await params;
|
| 72 |
-
const { namespace, repoId } = param;
|
| 73 |
-
|
| 74 |
try {
|
| 75 |
const space = await spaceInfo({
|
| 76 |
name: namespace + "/" + repoId,
|
|
@@ -97,85 +60,26 @@ export async function GET(
|
|
| 97 |
);
|
| 98 |
}
|
| 99 |
|
| 100 |
-
const
|
| 101 |
-
|
| 102 |
-
name: `${namespace}/${repoId}`,
|
| 103 |
-
};
|
| 104 |
-
|
| 105 |
-
const htmlFiles: Page[] = [];
|
| 106 |
-
const files: string[] = [];
|
| 107 |
-
|
| 108 |
-
const allowedFilesExtensions = ["jpg", "jpeg", "png", "gif", "svg", "webp", "avif", "heic", "heif", "ico", "bmp", "tiff", "tif", "mp4", "webm", "ogg", "avi", "mov", "mp3", "wav", "ogg", "aac", "m4a"];
|
| 109 |
-
|
| 110 |
-
for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
|
| 111 |
-
if (fileInfo.path.endsWith(".html") || fileInfo.path.endsWith(".css") || fileInfo.path.endsWith(".js")) {
|
| 112 |
-
const blob = await downloadFile({ repo, accessToken: user.token as string, path: fileInfo.path, raw: true });
|
| 113 |
-
const html = await blob?.text();
|
| 114 |
-
if (!html) {
|
| 115 |
-
continue;
|
| 116 |
-
}
|
| 117 |
-
if (fileInfo.path === "index.html") {
|
| 118 |
-
htmlFiles.unshift({
|
| 119 |
-
path: fileInfo.path,
|
| 120 |
-
html,
|
| 121 |
-
});
|
| 122 |
-
} else {
|
| 123 |
-
htmlFiles.push({
|
| 124 |
-
path: fileInfo.path,
|
| 125 |
-
html,
|
| 126 |
-
});
|
| 127 |
-
}
|
| 128 |
-
}
|
| 129 |
-
if (fileInfo.type === "directory" && (["videos", "images", "audio"].includes(fileInfo.path) || fileInfo.path === "components")) {
|
| 130 |
-
for await (const subFileInfo of listFiles({repo, accessToken: user.token as string, path: fileInfo.path})) {
|
| 131 |
-
if (subFileInfo.path.includes("components")) {
|
| 132 |
-
const blob = await downloadFile({ repo, accessToken: user.token as string, path: subFileInfo.path, raw: true });
|
| 133 |
-
const html = await blob?.text();
|
| 134 |
-
if (!html) {
|
| 135 |
-
continue;
|
| 136 |
-
}
|
| 137 |
-
htmlFiles.push({
|
| 138 |
-
path: subFileInfo.path,
|
| 139 |
-
html,
|
| 140 |
-
});
|
| 141 |
-
} else if (allowedFilesExtensions.includes(subFileInfo.path.split(".").pop() || "")) {
|
| 142 |
-
files.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${subFileInfo.path}`);
|
| 143 |
-
}
|
| 144 |
-
}
|
| 145 |
-
}
|
| 146 |
-
}
|
| 147 |
-
const commits: Commit[] = [];
|
| 148 |
-
for await (const commit of listCommits({ repo, accessToken: user.token as string })) {
|
| 149 |
-
if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Removed files from promoting")) {
|
| 150 |
-
continue;
|
| 151 |
-
}
|
| 152 |
-
commits.push({
|
| 153 |
-
title: commit.title,
|
| 154 |
-
oid: commit.oid,
|
| 155 |
-
date: commit.date,
|
| 156 |
-
});
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
if (htmlFiles.length === 0) {
|
| 160 |
return NextResponse.json(
|
| 161 |
{
|
| 162 |
ok: false,
|
| 163 |
-
error: "
|
| 164 |
},
|
| 165 |
{ status: 404 }
|
| 166 |
);
|
| 167 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
return NextResponse.json(
|
| 169 |
{
|
| 170 |
project: {
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
private: space.private,
|
| 174 |
-
_updatedAt: space.updatedAt,
|
| 175 |
},
|
| 176 |
-
pages: htmlFiles,
|
| 177 |
-
files,
|
| 178 |
-
commits,
|
| 179 |
ok: true,
|
| 180 |
},
|
| 181 |
{ status: 200 }
|
|
@@ -184,6 +88,10 @@ export async function GET(
|
|
| 184 |
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 185 |
} catch (error: any) {
|
| 186 |
if (error.statusCode === 404) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
return NextResponse.json(
|
| 188 |
{ error: "Space not found", ok: false },
|
| 189 |
{ status: 404 }
|
|
@@ -195,3 +103,135 @@ export async function GET(
|
|
| 195 |
);
|
| 196 |
}
|
| 197 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { RepoDesignation, spaceInfo, uploadFile } from "@huggingface/hub";
|
| 3 |
|
| 4 |
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
+
import Project from "@/models/Project";
|
| 6 |
+
import dbConnect from "@/lib/mongodb";
|
| 7 |
+
import { getPTag } from "@/lib/utils";
|
| 8 |
|
| 9 |
+
export async function GET(
|
| 10 |
req: NextRequest,
|
| 11 |
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
| 12 |
) {
|
|
|
|
| 16 |
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 17 |
}
|
| 18 |
|
| 19 |
+
await dbConnect();
|
| 20 |
const param = await params;
|
| 21 |
const { namespace, repoId } = param;
|
| 22 |
|
| 23 |
+
const project = await Project.findOne({
|
| 24 |
+
user_id: user.id,
|
| 25 |
+
space_id: `${namespace}/${repoId}`,
|
| 26 |
+
}).lean();
|
| 27 |
+
if (!project) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
return NextResponse.json(
|
| 29 |
+
{
|
| 30 |
+
ok: false,
|
| 31 |
+
error: "Project not found",
|
| 32 |
+
},
|
| 33 |
+
{ status: 404 }
|
| 34 |
);
|
| 35 |
}
|
| 36 |
+
const space_url = `https://huggingface.co/spaces/${namespace}/${repoId}/raw/main/index.html`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
try {
|
| 38 |
const space = await spaceInfo({
|
| 39 |
name: namespace + "/" + repoId,
|
|
|
|
| 60 |
);
|
| 61 |
}
|
| 62 |
|
| 63 |
+
const response = await fetch(space_url);
|
| 64 |
+
if (!response.ok) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
return NextResponse.json(
|
| 66 |
{
|
| 67 |
ok: false,
|
| 68 |
+
error: "Failed to fetch space HTML",
|
| 69 |
},
|
| 70 |
{ status: 404 }
|
| 71 |
);
|
| 72 |
}
|
| 73 |
+
let html = await response.text();
|
| 74 |
+
// remove the last p tag including this url https://enzostvs-deepsite.hf.space
|
| 75 |
+
html = html.replace(getPTag(namespace + "/" + repoId), "");
|
| 76 |
+
|
| 77 |
return NextResponse.json(
|
| 78 |
{
|
| 79 |
project: {
|
| 80 |
+
...project,
|
| 81 |
+
html,
|
|
|
|
|
|
|
| 82 |
},
|
|
|
|
|
|
|
|
|
|
| 83 |
ok: true,
|
| 84 |
},
|
| 85 |
{ status: 200 }
|
|
|
|
| 88 |
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 89 |
} catch (error: any) {
|
| 90 |
if (error.statusCode === 404) {
|
| 91 |
+
await Project.deleteOne({
|
| 92 |
+
user_id: user.id,
|
| 93 |
+
space_id: `${namespace}/${repoId}`,
|
| 94 |
+
});
|
| 95 |
return NextResponse.json(
|
| 96 |
{ error: "Space not found", ok: false },
|
| 97 |
{ status: 404 }
|
|
|
|
| 103 |
);
|
| 104 |
}
|
| 105 |
}
|
| 106 |
+
|
| 107 |
+
export async function PUT(
|
| 108 |
+
req: NextRequest,
|
| 109 |
+
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
| 110 |
+
) {
|
| 111 |
+
const user = await isAuthenticated();
|
| 112 |
+
|
| 113 |
+
if (user instanceof NextResponse || !user) {
|
| 114 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
await dbConnect();
|
| 118 |
+
const param = await params;
|
| 119 |
+
const { namespace, repoId } = param;
|
| 120 |
+
const { html, prompts } = await req.json();
|
| 121 |
+
|
| 122 |
+
const project = await Project.findOne({
|
| 123 |
+
user_id: user.id,
|
| 124 |
+
space_id: `${namespace}/${repoId}`,
|
| 125 |
+
}).lean();
|
| 126 |
+
if (!project) {
|
| 127 |
+
return NextResponse.json(
|
| 128 |
+
{
|
| 129 |
+
ok: false,
|
| 130 |
+
error: "Project not found",
|
| 131 |
+
},
|
| 132 |
+
{ status: 404 }
|
| 133 |
+
);
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
const repo: RepoDesignation = {
|
| 137 |
+
type: "space",
|
| 138 |
+
name: `${namespace}/${repoId}`,
|
| 139 |
+
};
|
| 140 |
+
|
| 141 |
+
const newHtml = html.replace(/<\/body>/, `${getPTag(repo.name)}</body>`);
|
| 142 |
+
const file = new File([newHtml], "index.html", { type: "text/html" });
|
| 143 |
+
await uploadFile({
|
| 144 |
+
repo,
|
| 145 |
+
file,
|
| 146 |
+
accessToken: user.token as string,
|
| 147 |
+
commitTitle: `${prompts[prompts.length - 1]} - Follow Up Deployment`,
|
| 148 |
+
});
|
| 149 |
+
|
| 150 |
+
await Project.updateOne(
|
| 151 |
+
{ user_id: user.id, space_id: `${namespace}/${repoId}` },
|
| 152 |
+
{
|
| 153 |
+
$set: {
|
| 154 |
+
prompts: [
|
| 155 |
+
...(project && "prompts" in project ? project.prompts : []),
|
| 156 |
+
...prompts,
|
| 157 |
+
],
|
| 158 |
+
},
|
| 159 |
+
}
|
| 160 |
+
);
|
| 161 |
+
return NextResponse.json({ ok: true }, { status: 200 });
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
export async function POST(
|
| 165 |
+
req: NextRequest,
|
| 166 |
+
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
| 167 |
+
) {
|
| 168 |
+
const user = await isAuthenticated();
|
| 169 |
+
|
| 170 |
+
if (user instanceof NextResponse || !user) {
|
| 171 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
await dbConnect();
|
| 175 |
+
const param = await params;
|
| 176 |
+
const { namespace, repoId } = param;
|
| 177 |
+
|
| 178 |
+
const space = await spaceInfo({
|
| 179 |
+
name: namespace + "/" + repoId,
|
| 180 |
+
accessToken: user.token as string,
|
| 181 |
+
additionalFields: ["author"],
|
| 182 |
+
});
|
| 183 |
+
|
| 184 |
+
if (!space || space.sdk !== "static") {
|
| 185 |
+
return NextResponse.json(
|
| 186 |
+
{
|
| 187 |
+
ok: false,
|
| 188 |
+
error: "Space is not a static space",
|
| 189 |
+
},
|
| 190 |
+
{ status: 404 }
|
| 191 |
+
);
|
| 192 |
+
}
|
| 193 |
+
if (space.author !== user.name) {
|
| 194 |
+
return NextResponse.json(
|
| 195 |
+
{
|
| 196 |
+
ok: false,
|
| 197 |
+
error: "Space does not belong to the authenticated user",
|
| 198 |
+
},
|
| 199 |
+
{ status: 403 }
|
| 200 |
+
);
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
const project = await Project.findOne({
|
| 204 |
+
user_id: user.id,
|
| 205 |
+
space_id: `${namespace}/${repoId}`,
|
| 206 |
+
}).lean();
|
| 207 |
+
if (project) {
|
| 208 |
+
// redirect to the project page if it already exists
|
| 209 |
+
return NextResponse.json(
|
| 210 |
+
{
|
| 211 |
+
ok: false,
|
| 212 |
+
error: "Project already exists",
|
| 213 |
+
redirect: `/projects/${namespace}/${repoId}`,
|
| 214 |
+
},
|
| 215 |
+
{ status: 400 }
|
| 216 |
+
);
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
const newProject = new Project({
|
| 220 |
+
user_id: user.id,
|
| 221 |
+
space_id: `${namespace}/${repoId}`,
|
| 222 |
+
prompts: [],
|
| 223 |
+
});
|
| 224 |
+
|
| 225 |
+
await newProject.save();
|
| 226 |
+
return NextResponse.json(
|
| 227 |
+
{
|
| 228 |
+
ok: true,
|
| 229 |
+
project: {
|
| 230 |
+
id: newProject._id,
|
| 231 |
+
space_id: newProject.space_id,
|
| 232 |
+
prompts: newProject.prompts,
|
| 233 |
+
},
|
| 234 |
+
},
|
| 235 |
+
{ status: 201 }
|
| 236 |
+
);
|
| 237 |
+
}
|
app/api/me/projects/[namespace]/[repoId]/save/route.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
| 1 |
-
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
-
import { uploadFiles } from "@huggingface/hub";
|
| 3 |
-
|
| 4 |
-
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
-
import { Page } from "@/types";
|
| 6 |
-
|
| 7 |
-
export async function PUT(
|
| 8 |
-
req: NextRequest,
|
| 9 |
-
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
| 10 |
-
) {
|
| 11 |
-
const user = await isAuthenticated();
|
| 12 |
-
if (user instanceof NextResponse || !user) {
|
| 13 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
const param = await params;
|
| 17 |
-
const { namespace, repoId } = param;
|
| 18 |
-
const { pages, commitTitle = "Manual changes saved" } = await req.json();
|
| 19 |
-
|
| 20 |
-
if (!pages || !Array.isArray(pages) || pages.length === 0) {
|
| 21 |
-
return NextResponse.json(
|
| 22 |
-
{ ok: false, error: "Pages are required" },
|
| 23 |
-
{ status: 400 }
|
| 24 |
-
);
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
try {
|
| 28 |
-
// Prepare files for upload
|
| 29 |
-
const files: File[] = [];
|
| 30 |
-
pages.forEach((page: Page) => {
|
| 31 |
-
// Determine MIME type based on file extension
|
| 32 |
-
let mimeType = "text/html";
|
| 33 |
-
if (page.path.endsWith(".css")) {
|
| 34 |
-
mimeType = "text/css";
|
| 35 |
-
} else if (page.path.endsWith(".js")) {
|
| 36 |
-
mimeType = "text/javascript";
|
| 37 |
-
} else if (page.path.endsWith(".json")) {
|
| 38 |
-
mimeType = "application/json";
|
| 39 |
-
}
|
| 40 |
-
const file = new File([page.html], page.path, { type: mimeType });
|
| 41 |
-
files.push(file);
|
| 42 |
-
});
|
| 43 |
-
|
| 44 |
-
const response = await uploadFiles({
|
| 45 |
-
repo: {
|
| 46 |
-
type: "space",
|
| 47 |
-
name: `${namespace}/${repoId}`,
|
| 48 |
-
},
|
| 49 |
-
files,
|
| 50 |
-
commitTitle,
|
| 51 |
-
accessToken: user.token as string,
|
| 52 |
-
});
|
| 53 |
-
|
| 54 |
-
return NextResponse.json({
|
| 55 |
-
ok: true,
|
| 56 |
-
pages,
|
| 57 |
-
commit: {
|
| 58 |
-
...response.commit,
|
| 59 |
-
title: commitTitle,
|
| 60 |
-
}
|
| 61 |
-
});
|
| 62 |
-
} catch (error: any) {
|
| 63 |
-
console.error("Error saving manual changes:", error);
|
| 64 |
-
return NextResponse.json(
|
| 65 |
-
{
|
| 66 |
-
ok: false,
|
| 67 |
-
error: error.message || "Failed to save changes",
|
| 68 |
-
},
|
| 69 |
-
{ status: 500 }
|
| 70 |
-
);
|
| 71 |
-
}
|
| 72 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/me/projects/[namespace]/[repoId]/update/route.ts
DELETED
|
@@ -1,141 +0,0 @@
|
|
| 1 |
-
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
-
import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
|
| 3 |
-
|
| 4 |
-
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
-
import { Page } from "@/types";
|
| 6 |
-
import { COLORS } from "@/lib/utils";
|
| 7 |
-
import { injectDeepSiteBadge, isIndexPage } from "@/lib/inject-badge";
|
| 8 |
-
import { pagesToFiles } from "@/lib/format-ai-response";
|
| 9 |
-
|
| 10 |
-
/**
|
| 11 |
-
* UPDATE route - for updating existing projects or creating new ones after AI streaming
|
| 12 |
-
* This route handles the HuggingFace upload after client-side AI response processing
|
| 13 |
-
*/
|
| 14 |
-
export async function PUT(
|
| 15 |
-
req: NextRequest,
|
| 16 |
-
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
| 17 |
-
) {
|
| 18 |
-
const user = await isAuthenticated();
|
| 19 |
-
if (user instanceof NextResponse || !user) {
|
| 20 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
const param = await params;
|
| 24 |
-
let { namespace, repoId } = param;
|
| 25 |
-
const { pages, commitTitle = "AI-generated changes", isNew, projectName } = await req.json();
|
| 26 |
-
|
| 27 |
-
if (!pages || !Array.isArray(pages) || pages.length === 0) {
|
| 28 |
-
return NextResponse.json(
|
| 29 |
-
{ ok: false, error: "Pages are required" },
|
| 30 |
-
{ status: 400 }
|
| 31 |
-
);
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
try {
|
| 35 |
-
let files: File[];
|
| 36 |
-
|
| 37 |
-
if (isNew) {
|
| 38 |
-
// Creating a new project
|
| 39 |
-
const title = projectName || "DeepSite Project";
|
| 40 |
-
const formattedTitle = title
|
| 41 |
-
.toLowerCase()
|
| 42 |
-
.replace(/[^a-z0-9]+/g, "-")
|
| 43 |
-
.split("-")
|
| 44 |
-
.filter(Boolean)
|
| 45 |
-
.join("-")
|
| 46 |
-
.slice(0, 96);
|
| 47 |
-
|
| 48 |
-
const repo: RepoDesignation = {
|
| 49 |
-
type: "space",
|
| 50 |
-
name: `${user.name}/${formattedTitle}`,
|
| 51 |
-
};
|
| 52 |
-
|
| 53 |
-
try {
|
| 54 |
-
const { repoUrl } = await createRepo({
|
| 55 |
-
repo,
|
| 56 |
-
accessToken: user.token as string,
|
| 57 |
-
});
|
| 58 |
-
namespace = user.name;
|
| 59 |
-
repoId = repoUrl.split("/").slice(-2).join("/").split("/")[1];
|
| 60 |
-
} catch (createRepoError: any) {
|
| 61 |
-
return NextResponse.json(
|
| 62 |
-
{
|
| 63 |
-
ok: false,
|
| 64 |
-
error: `Failed to create repository: ${createRepoError.message || 'Unknown error'}`,
|
| 65 |
-
},
|
| 66 |
-
{ status: 500 }
|
| 67 |
-
);
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
// Prepare files with badge injection for new projects
|
| 71 |
-
files = [];
|
| 72 |
-
pages.forEach((page: Page) => {
|
| 73 |
-
let mimeType = "text/html";
|
| 74 |
-
if (page.path.endsWith(".css")) {
|
| 75 |
-
mimeType = "text/css";
|
| 76 |
-
} else if (page.path.endsWith(".js")) {
|
| 77 |
-
mimeType = "text/javascript";
|
| 78 |
-
} else if (page.path.endsWith(".json")) {
|
| 79 |
-
mimeType = "application/json";
|
| 80 |
-
}
|
| 81 |
-
const content = (mimeType === "text/html" && isIndexPage(page.path))
|
| 82 |
-
? injectDeepSiteBadge(page.html)
|
| 83 |
-
: page.html;
|
| 84 |
-
const file = new File([content], page.path, { type: mimeType });
|
| 85 |
-
files.push(file);
|
| 86 |
-
});
|
| 87 |
-
|
| 88 |
-
// Add README.md for new projects
|
| 89 |
-
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 90 |
-
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 91 |
-
const README = `---
|
| 92 |
-
title: ${title}
|
| 93 |
-
colorFrom: ${colorFrom}
|
| 94 |
-
colorTo: ${colorTo}
|
| 95 |
-
emoji: 🐳
|
| 96 |
-
sdk: static
|
| 97 |
-
pinned: false
|
| 98 |
-
tags:
|
| 99 |
-
- deepsite-v3
|
| 100 |
-
---
|
| 101 |
-
|
| 102 |
-
# Welcome to your new DeepSite project!
|
| 103 |
-
This project was created with [DeepSite](https://huggingface.co/deepsite).
|
| 104 |
-
`;
|
| 105 |
-
files.push(new File([README], "README.md", { type: "text/markdown" }));
|
| 106 |
-
} else {
|
| 107 |
-
// Updating existing project - no badge injection
|
| 108 |
-
files = pagesToFiles(pages);
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
const response = await uploadFiles({
|
| 112 |
-
repo: {
|
| 113 |
-
type: "space",
|
| 114 |
-
name: `${namespace}/${repoId}`,
|
| 115 |
-
},
|
| 116 |
-
files,
|
| 117 |
-
commitTitle,
|
| 118 |
-
accessToken: user.token as string,
|
| 119 |
-
});
|
| 120 |
-
|
| 121 |
-
return NextResponse.json({
|
| 122 |
-
ok: true,
|
| 123 |
-
pages,
|
| 124 |
-
repoId: `${namespace}/${repoId}`,
|
| 125 |
-
commit: {
|
| 126 |
-
...response.commit,
|
| 127 |
-
title: commitTitle,
|
| 128 |
-
}
|
| 129 |
-
});
|
| 130 |
-
} catch (error: any) {
|
| 131 |
-
console.error("Error updating project:", error);
|
| 132 |
-
return NextResponse.json(
|
| 133 |
-
{
|
| 134 |
-
ok: false,
|
| 135 |
-
error: error.message || "Failed to update project",
|
| 136 |
-
},
|
| 137 |
-
{ status: 500 }
|
| 138 |
-
);
|
| 139 |
-
}
|
| 140 |
-
}
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/me/projects/route.ts
CHANGED
|
@@ -1,121 +1,126 @@
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
-
import {
|
| 3 |
|
| 4 |
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
-
import
|
| 6 |
-
import
|
| 7 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
const user = await isAuthenticated();
|
|
|
|
| 13 |
if (user instanceof NextResponse || !user) {
|
| 14 |
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 15 |
}
|
| 16 |
|
| 17 |
-
const { title
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
colorFrom: ${colorFrom}
|
| 38 |
colorTo: ${colorTo}
|
| 39 |
-
emoji: 🐳
|
| 40 |
sdk: static
|
| 41 |
pinned: false
|
| 42 |
tags:
|
| 43 |
-
- deepsite
|
| 44 |
---
|
| 45 |
|
| 46 |
-
|
| 47 |
-
This project was created with [DeepSite](https://huggingface.co/deepsite).
|
| 48 |
-
`;
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
// Determine MIME type based on file extension
|
| 55 |
-
let mimeType = "text/html";
|
| 56 |
-
if (page.path.endsWith(".css")) {
|
| 57 |
-
mimeType = "text/css";
|
| 58 |
-
} else if (page.path.endsWith(".js")) {
|
| 59 |
-
mimeType = "text/javascript";
|
| 60 |
-
} else if (page.path.endsWith(".json")) {
|
| 61 |
-
mimeType = "application/json";
|
| 62 |
-
}
|
| 63 |
-
// Inject the DeepSite badge script into index pages only (not components or other HTML files)
|
| 64 |
-
const content = (mimeType === "text/html" && isIndexPage(page.path))
|
| 65 |
-
? injectDeepSiteBadge(page.html)
|
| 66 |
-
: page.html;
|
| 67 |
-
const file = new File([content], page.path, { type: mimeType });
|
| 68 |
-
files.push(file);
|
| 69 |
-
});
|
| 70 |
-
|
| 71 |
-
try {
|
| 72 |
-
const { repoUrl} = await createRepo({
|
| 73 |
-
repo,
|
| 74 |
-
accessToken: user.token as string,
|
| 75 |
});
|
| 76 |
-
const
|
| 77 |
await uploadFiles({
|
| 78 |
repo,
|
| 79 |
files,
|
| 80 |
accessToken: user.token as string,
|
| 81 |
-
commitTitle
|
| 82 |
});
|
| 83 |
-
|
| 84 |
const path = repoUrl.split("/").slice(-2).join("/");
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
continue;
|
| 90 |
-
}
|
| 91 |
-
commits.push({
|
| 92 |
-
title: commit.title,
|
| 93 |
-
oid: commit.oid,
|
| 94 |
-
date: commit.date,
|
| 95 |
-
});
|
| 96 |
-
}
|
| 97 |
-
|
| 98 |
-
const space = await spaceInfo({
|
| 99 |
-
name: repo.name,
|
| 100 |
-
accessToken: user.token as string,
|
| 101 |
});
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
files,
|
| 105 |
-
pages,
|
| 106 |
-
commits,
|
| 107 |
-
project: {
|
| 108 |
-
id: space.id,
|
| 109 |
-
space_id: space.name,
|
| 110 |
-
_updatedAt: space.updatedAt,
|
| 111 |
-
}
|
| 112 |
-
}
|
| 113 |
-
|
| 114 |
-
return NextResponse.json({ space: newProject, path, ok: true }, { status: 201 });
|
| 115 |
} catch (err: any) {
|
| 116 |
return NextResponse.json(
|
| 117 |
{ error: err.message, ok: false },
|
| 118 |
{ status: 500 }
|
| 119 |
);
|
| 120 |
}
|
| 121 |
-
}
|
|
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
|
| 3 |
|
| 4 |
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
+
import Project from "@/models/Project";
|
| 6 |
+
import dbConnect from "@/lib/mongodb";
|
| 7 |
+
import { COLORS, getPTag } from "@/lib/utils";
|
| 8 |
+
// import type user
|
| 9 |
+
export async function GET() {
|
| 10 |
+
const user = await isAuthenticated();
|
| 11 |
+
|
| 12 |
+
if (user instanceof NextResponse || !user) {
|
| 13 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 14 |
+
}
|
| 15 |
|
| 16 |
+
await dbConnect();
|
| 17 |
+
|
| 18 |
+
const projects = await Project.find({
|
| 19 |
+
user_id: user?.id,
|
| 20 |
+
})
|
| 21 |
+
.sort({ _createdAt: -1 })
|
| 22 |
+
.limit(100)
|
| 23 |
+
.lean();
|
| 24 |
+
if (!projects) {
|
| 25 |
+
return NextResponse.json(
|
| 26 |
+
{
|
| 27 |
+
ok: false,
|
| 28 |
+
projects: [],
|
| 29 |
+
},
|
| 30 |
+
{ status: 404 }
|
| 31 |
+
);
|
| 32 |
+
}
|
| 33 |
+
return NextResponse.json(
|
| 34 |
+
{
|
| 35 |
+
ok: true,
|
| 36 |
+
projects,
|
| 37 |
+
},
|
| 38 |
+
{ status: 200 }
|
| 39 |
+
);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/**
|
| 43 |
+
* This API route creates a new project in Hugging Face Spaces.
|
| 44 |
+
* It requires an Authorization header with a valid token and a JSON body with the project details.
|
| 45 |
+
*/
|
| 46 |
+
export async function POST(request: NextRequest) {
|
| 47 |
const user = await isAuthenticated();
|
| 48 |
+
|
| 49 |
if (user instanceof NextResponse || !user) {
|
| 50 |
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 51 |
}
|
| 52 |
|
| 53 |
+
const { title, html, prompts } = await request.json();
|
| 54 |
+
|
| 55 |
+
if (!title || !html) {
|
| 56 |
+
return NextResponse.json(
|
| 57 |
+
{ message: "Title and HTML content are required.", ok: false },
|
| 58 |
+
{ status: 400 }
|
| 59 |
+
);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
await dbConnect();
|
| 63 |
|
| 64 |
+
try {
|
| 65 |
+
let readme = "";
|
| 66 |
+
let newHtml = html;
|
| 67 |
|
| 68 |
+
const newTitle = title
|
| 69 |
+
.toLowerCase()
|
| 70 |
+
.replace(/[^a-z0-9]+/g, "-")
|
| 71 |
+
.split("-")
|
| 72 |
+
.filter(Boolean)
|
| 73 |
+
.join("-")
|
| 74 |
+
.slice(0, 96);
|
| 75 |
|
| 76 |
+
const repo: RepoDesignation = {
|
| 77 |
+
type: "space",
|
| 78 |
+
name: `${user.name}/${newTitle}`,
|
| 79 |
+
};
|
| 80 |
+
|
| 81 |
+
const { repoUrl } = await createRepo({
|
| 82 |
+
repo,
|
| 83 |
+
accessToken: user.token as string,
|
| 84 |
+
});
|
| 85 |
+
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 86 |
+
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 87 |
+
readme = `---
|
| 88 |
+
title: ${newTitle}
|
| 89 |
+
emoji: 🐳
|
| 90 |
colorFrom: ${colorFrom}
|
| 91 |
colorTo: ${colorTo}
|
|
|
|
| 92 |
sdk: static
|
| 93 |
pinned: false
|
| 94 |
tags:
|
| 95 |
+
- deepsite
|
| 96 |
---
|
| 97 |
|
| 98 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference`;
|
|
|
|
|
|
|
| 99 |
|
| 100 |
+
newHtml = html.replace(/<\/body>/, `${getPTag(repo.name)}</body>`);
|
| 101 |
+
const file = new File([newHtml], "index.html", { type: "text/html" });
|
| 102 |
+
const readmeFile = new File([readme], "README.md", {
|
| 103 |
+
type: "text/markdown",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
});
|
| 105 |
+
const files = [file, readmeFile];
|
| 106 |
await uploadFiles({
|
| 107 |
repo,
|
| 108 |
files,
|
| 109 |
accessToken: user.token as string,
|
| 110 |
+
commitTitle: `${prompts[prompts.length - 1]} - Initial Deployment`,
|
| 111 |
});
|
|
|
|
| 112 |
const path = repoUrl.split("/").slice(-2).join("/");
|
| 113 |
+
const project = await Project.create({
|
| 114 |
+
user_id: user.id,
|
| 115 |
+
space_id: path,
|
| 116 |
+
prompts,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
});
|
| 118 |
+
return NextResponse.json({ project, path, ok: true }, { status: 201 });
|
| 119 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
} catch (err: any) {
|
| 121 |
return NextResponse.json(
|
| 122 |
{ error: err.message, ok: false },
|
| 123 |
{ status: 500 }
|
| 124 |
);
|
| 125 |
}
|
| 126 |
+
}
|
app/api/me/route.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
import { listSpaces } from "@huggingface/hub";
|
| 2 |
import { headers } from "next/headers";
|
| 3 |
import { NextResponse } from "next/server";
|
| 4 |
|
|
@@ -22,25 +21,5 @@ export async function GET() {
|
|
| 22 |
);
|
| 23 |
}
|
| 24 |
const user = await userResponse.json();
|
| 25 |
-
|
| 26 |
-
for await (const space of listSpaces({
|
| 27 |
-
accessToken: token.replace("Bearer ", "") as string,
|
| 28 |
-
additionalFields: ["author", "cardData"],
|
| 29 |
-
search: {
|
| 30 |
-
owner: user.name,
|
| 31 |
-
}
|
| 32 |
-
})) {
|
| 33 |
-
if (
|
| 34 |
-
space.sdk === "static" &&
|
| 35 |
-
Array.isArray((space.cardData as { tags?: string[] })?.tags) &&
|
| 36 |
-
(
|
| 37 |
-
((space.cardData as { tags?: string[] })?.tags?.includes("deepsite-v3")) ||
|
| 38 |
-
((space.cardData as { tags?: string[] })?.tags?.includes("deepsite"))
|
| 39 |
-
)
|
| 40 |
-
) {
|
| 41 |
-
projects.push(space);
|
| 42 |
-
}
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
return NextResponse.json({ user, projects, errCode: null }, { status: 200 });
|
| 46 |
}
|
|
|
|
|
|
|
| 1 |
import { headers } from "next/headers";
|
| 2 |
import { NextResponse } from "next/server";
|
| 3 |
|
|
|
|
| 21 |
);
|
| 22 |
}
|
| 23 |
const user = await userResponse.json();
|
| 24 |
+
return NextResponse.json({ user, errCode: null }, { status: 200 });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
}
|
app/api/re-design/route.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
|
| 3 |
-
// Timeout configuration (in milliseconds)
|
| 4 |
-
const FETCH_TIMEOUT = 30000; // 30 seconds for external fetch
|
| 5 |
-
|
| 6 |
-
// Extend the maximum execution time for this route
|
| 7 |
-
export const maxDuration = 60; // 1 minute
|
| 8 |
-
|
| 9 |
export async function PUT(request: NextRequest) {
|
| 10 |
const body = await request.json();
|
| 11 |
const { url } = body;
|
|
@@ -15,54 +9,28 @@ export async function PUT(request: NextRequest) {
|
|
| 15 |
}
|
| 16 |
|
| 17 |
try {
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
try {
|
| 23 |
-
const response = await fetch(
|
| 24 |
-
`https://r.jina.ai/${encodeURIComponent(url)}`,
|
| 25 |
-
{
|
| 26 |
-
method: "POST",
|
| 27 |
-
signal: controller.signal,
|
| 28 |
-
}
|
| 29 |
-
);
|
| 30 |
-
|
| 31 |
-
clearTimeout(timeoutId);
|
| 32 |
-
|
| 33 |
-
if (!response.ok) {
|
| 34 |
-
return NextResponse.json(
|
| 35 |
-
{ error: "Failed to fetch redesign" },
|
| 36 |
-
{ status: 500 }
|
| 37 |
-
);
|
| 38 |
}
|
| 39 |
-
|
|
|
|
| 40 |
return NextResponse.json(
|
| 41 |
-
{
|
| 42 |
-
|
| 43 |
-
markdown,
|
| 44 |
-
},
|
| 45 |
-
{ status: 200 }
|
| 46 |
);
|
| 47 |
-
} catch (fetchError: any) {
|
| 48 |
-
clearTimeout(timeoutId);
|
| 49 |
-
|
| 50 |
-
if (fetchError.name === 'AbortError') {
|
| 51 |
-
return NextResponse.json(
|
| 52 |
-
{ error: "Request timeout: The external service took too long to respond. Please try again." },
|
| 53 |
-
{ status: 504 }
|
| 54 |
-
);
|
| 55 |
-
}
|
| 56 |
-
throw fetchError;
|
| 57 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 59 |
} catch (error: any) {
|
| 60 |
-
if (error.name === 'AbortError' || error.message?.includes('timeout')) {
|
| 61 |
-
return NextResponse.json(
|
| 62 |
-
{ error: "Request timeout: The external service took too long to respond. Please try again." },
|
| 63 |
-
{ status: 504 }
|
| 64 |
-
);
|
| 65 |
-
}
|
| 66 |
return NextResponse.json(
|
| 67 |
{ error: error.message || "An error occurred" },
|
| 68 |
{ status: 500 }
|
|
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
export async function PUT(request: NextRequest) {
|
| 4 |
const body = await request.json();
|
| 5 |
const { url } = body;
|
|
|
|
| 9 |
}
|
| 10 |
|
| 11 |
try {
|
| 12 |
+
const response = await fetch(
|
| 13 |
+
`https://r.jina.ai/${encodeURIComponent(url)}`,
|
| 14 |
+
{
|
| 15 |
+
method: "POST",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
}
|
| 17 |
+
);
|
| 18 |
+
if (!response.ok) {
|
| 19 |
return NextResponse.json(
|
| 20 |
+
{ error: "Failed to fetch redesign" },
|
| 21 |
+
{ status: 500 }
|
|
|
|
|
|
|
|
|
|
| 22 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
}
|
| 24 |
+
const markdown = await response.text();
|
| 25 |
+
return NextResponse.json(
|
| 26 |
+
{
|
| 27 |
+
ok: true,
|
| 28 |
+
markdown,
|
| 29 |
+
},
|
| 30 |
+
{ status: 200 }
|
| 31 |
+
);
|
| 32 |
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 33 |
} catch (error: any) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
return NextResponse.json(
|
| 35 |
{ error: error.message || "An error occurred" },
|
| 36 |
{ status: 500 }
|
app/auth/callback/page.tsx
CHANGED
|
@@ -5,92 +5,67 @@ import { use, useState } from "react";
|
|
| 5 |
import { useMount, useTimeoutFn } from "react-use";
|
| 6 |
|
| 7 |
import { Button } from "@/components/ui/button";
|
| 8 |
-
import { AnimatedBlobs } from "@/components/animated-blobs";
|
| 9 |
-
import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
|
| 10 |
export default function AuthCallback({
|
| 11 |
searchParams,
|
| 12 |
}: {
|
| 13 |
searchParams: Promise<{ code: string }>;
|
| 14 |
}) {
|
| 15 |
const [showButton, setShowButton] = useState(false);
|
| 16 |
-
const [isPopupAuth, setIsPopupAuth] = useState(false);
|
| 17 |
const { code } = use(searchParams);
|
| 18 |
const { loginFromCode } = useUser();
|
| 19 |
-
const { postMessage } = useBroadcastChannel("auth", () => {});
|
| 20 |
|
| 21 |
useMount(async () => {
|
| 22 |
if (code) {
|
| 23 |
-
|
| 24 |
-
setIsPopupAuth(isPopup);
|
| 25 |
-
|
| 26 |
-
if (isPopup) {
|
| 27 |
-
postMessage({
|
| 28 |
-
type: "user-oauth",
|
| 29 |
-
code: code,
|
| 30 |
-
});
|
| 31 |
-
|
| 32 |
-
setTimeout(() => {
|
| 33 |
-
if (window.opener) {
|
| 34 |
-
window.close();
|
| 35 |
-
}
|
| 36 |
-
}, 1000);
|
| 37 |
-
} else {
|
| 38 |
-
await loginFromCode(code);
|
| 39 |
-
}
|
| 40 |
}
|
| 41 |
});
|
| 42 |
|
| 43 |
-
useTimeoutFn(
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
return (
|
| 46 |
-
<div className="h-screen flex flex-col justify-center items-center
|
| 47 |
-
<div className="
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
<div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
| 53 |
-
🚀
|
| 54 |
-
</div>
|
| 55 |
-
<div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
|
| 56 |
-
👋
|
| 57 |
-
</div>
|
| 58 |
-
<div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
| 59 |
-
🙌
|
| 60 |
-
</div>
|
| 61 |
</div>
|
| 62 |
-
<
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
</p>
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
| 78 |
</p>
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
Go to Home
|
| 83 |
-
</Button>
|
| 84 |
-
</Link>
|
| 85 |
-
) : (
|
| 86 |
-
<p className="text-xs text-neutral-500">
|
| 87 |
-
Please wait, we are logging you in...
|
| 88 |
-
</p>
|
| 89 |
-
)}
|
| 90 |
-
</div>
|
| 91 |
-
</main>
|
| 92 |
-
</div>
|
| 93 |
-
<AnimatedBlobs />
|
| 94 |
</div>
|
| 95 |
</div>
|
| 96 |
);
|
|
|
|
| 5 |
import { useMount, useTimeoutFn } from "react-use";
|
| 6 |
|
| 7 |
import { Button } from "@/components/ui/button";
|
|
|
|
|
|
|
| 8 |
export default function AuthCallback({
|
| 9 |
searchParams,
|
| 10 |
}: {
|
| 11 |
searchParams: Promise<{ code: string }>;
|
| 12 |
}) {
|
| 13 |
const [showButton, setShowButton] = useState(false);
|
|
|
|
| 14 |
const { code } = use(searchParams);
|
| 15 |
const { loginFromCode } = useUser();
|
|
|
|
| 16 |
|
| 17 |
useMount(async () => {
|
| 18 |
if (code) {
|
| 19 |
+
await loginFromCode(code);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
}
|
| 21 |
});
|
| 22 |
|
| 23 |
+
useTimeoutFn(
|
| 24 |
+
() => setShowButton(true),
|
| 25 |
+
7000 // Show button after 5 seconds
|
| 26 |
+
);
|
| 27 |
|
| 28 |
return (
|
| 29 |
+
<div className="h-screen flex flex-col justify-center items-center">
|
| 30 |
+
<div className="!rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden ring-[8px] ring-white/20">
|
| 31 |
+
<header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
|
| 32 |
+
<div className="flex items-center justify-center -space-x-4 mb-3">
|
| 33 |
+
<div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
| 34 |
+
🚀
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
</div>
|
| 36 |
+
<div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
|
| 37 |
+
👋
|
| 38 |
+
</div>
|
| 39 |
+
<div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
| 40 |
+
🙌
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
<p className="text-xl font-semibold text-neutral-950">
|
| 44 |
+
Login In Progress...
|
| 45 |
+
</p>
|
| 46 |
+
<p className="text-sm text-neutral-500 mt-1.5">
|
| 47 |
+
Wait a moment while we log you in with your code.
|
| 48 |
+
</p>
|
| 49 |
+
</header>
|
| 50 |
+
<main className="space-y-4 p-6">
|
| 51 |
+
<div>
|
| 52 |
+
<p className="text-sm text-neutral-700 mb-4 max-w-xs">
|
| 53 |
+
If you are not redirected automatically in the next 5 seconds,
|
| 54 |
+
please click the button below
|
| 55 |
</p>
|
| 56 |
+
{showButton ? (
|
| 57 |
+
<Link href="/">
|
| 58 |
+
<Button variant="black" className="relative">
|
| 59 |
+
Go to Home
|
| 60 |
+
</Button>
|
| 61 |
+
</Link>
|
| 62 |
+
) : (
|
| 63 |
+
<p className="text-xs text-neutral-500">
|
| 64 |
+
Please wait, we are logging you in...
|
| 65 |
</p>
|
| 66 |
+
)}
|
| 67 |
+
</div>
|
| 68 |
+
</main>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
</div>
|
| 70 |
</div>
|
| 71 |
);
|
app/layout.tsx
CHANGED
|
@@ -2,19 +2,14 @@
|
|
| 2 |
import type { Metadata, Viewport } from "next";
|
| 3 |
import { Inter, PT_Sans } from "next/font/google";
|
| 4 |
import { cookies } from "next/headers";
|
| 5 |
-
import Script from "next/script";
|
| 6 |
|
|
|
|
| 7 |
import "@/assets/globals.css";
|
| 8 |
import { Toaster } from "@/components/ui/sonner";
|
| 9 |
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
| 10 |
import { apiServer } from "@/lib/api";
|
| 11 |
-
import IframeDetector from "@/components/iframe-detector";
|
| 12 |
import AppContext from "@/components/contexts/app-context";
|
| 13 |
-
import
|
| 14 |
-
import { LoginProvider } from "@/components/contexts/login-context";
|
| 15 |
-
import { ProProvider } from "@/components/contexts/pro-context";
|
| 16 |
-
import { generateSEO, generateStructuredData } from "@/lib/seo";
|
| 17 |
-
import DomainRedirect from "@/components/domain-redirect";
|
| 18 |
|
| 19 |
const inter = Inter({
|
| 20 |
variable: "--font-inter-sans",
|
|
@@ -28,12 +23,31 @@ const ptSans = PT_Sans({
|
|
| 28 |
});
|
| 29 |
|
| 30 |
export const metadata: Metadata = {
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
| 32 |
title: "DeepSite | Build with AI ✨",
|
| 33 |
description:
|
| 34 |
"DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.",
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
appleWebApp: {
|
| 38 |
capable: true,
|
| 39 |
title: "DeepSite",
|
|
@@ -44,9 +58,6 @@ export const metadata: Metadata = {
|
|
| 44 |
shortcut: "/logo.svg",
|
| 45 |
apple: "/logo.svg",
|
| 46 |
},
|
| 47 |
-
verification: {
|
| 48 |
-
google: process.env.GOOGLE_SITE_VERIFICATION,
|
| 49 |
-
},
|
| 50 |
};
|
| 51 |
|
| 52 |
export const viewport: Viewport = {
|
|
@@ -55,75 +66,42 @@ export const viewport: Viewport = {
|
|
| 55 |
themeColor: "#000000",
|
| 56 |
};
|
| 57 |
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
// }
|
| 74 |
-
// }
|
| 75 |
|
| 76 |
export default async function RootLayout({
|
| 77 |
children,
|
| 78 |
}: Readonly<{
|
| 79 |
children: React.ReactNode;
|
| 80 |
}>) {
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
// Generate structured data
|
| 84 |
-
const structuredData = generateStructuredData("WebApplication", {
|
| 85 |
-
name: "DeepSite",
|
| 86 |
-
description: "Build websites with AI, no code required",
|
| 87 |
-
url: "https://huggingface.co/deepsite",
|
| 88 |
-
});
|
| 89 |
-
|
| 90 |
-
const organizationData = generateStructuredData("Organization", {
|
| 91 |
-
name: "DeepSite",
|
| 92 |
-
url: "https://huggingface.co/deepsite",
|
| 93 |
-
});
|
| 94 |
-
|
| 95 |
return (
|
| 96 |
<html lang="en">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
<body
|
| 98 |
className={`${inter.variable} ${ptSans.variable} antialiased bg-black dark h-[100dvh] overflow-hidden`}
|
| 99 |
>
|
| 100 |
-
<script
|
| 101 |
-
type="application/ld+json"
|
| 102 |
-
dangerouslySetInnerHTML={{
|
| 103 |
-
__html: JSON.stringify(structuredData),
|
| 104 |
-
}}
|
| 105 |
-
/>
|
| 106 |
-
<script
|
| 107 |
-
type="application/ld+json"
|
| 108 |
-
dangerouslySetInnerHTML={{
|
| 109 |
-
__html: JSON.stringify(organizationData),
|
| 110 |
-
}}
|
| 111 |
-
/>
|
| 112 |
-
<Script
|
| 113 |
-
defer
|
| 114 |
-
data-domain="deepsite.hf.co"
|
| 115 |
-
src="https://plausible.io/js/script.js"
|
| 116 |
-
/>
|
| 117 |
-
<DomainRedirect />
|
| 118 |
-
<IframeDetector />
|
| 119 |
<Toaster richColors position="bottom-center" />
|
| 120 |
-
<
|
| 121 |
-
<AppContext>
|
| 122 |
-
|
| 123 |
-
<ProProvider>{children}</ProProvider>
|
| 124 |
-
</LoginProvider>
|
| 125 |
-
</AppContext>
|
| 126 |
-
</TanstackContext>
|
| 127 |
</body>
|
| 128 |
</html>
|
| 129 |
);
|
|
|
|
| 2 |
import type { Metadata, Viewport } from "next";
|
| 3 |
import { Inter, PT_Sans } from "next/font/google";
|
| 4 |
import { cookies } from "next/headers";
|
|
|
|
| 5 |
|
| 6 |
+
import TanstackProvider from "@/components/providers/tanstack-query-provider";
|
| 7 |
import "@/assets/globals.css";
|
| 8 |
import { Toaster } from "@/components/ui/sonner";
|
| 9 |
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
| 10 |
import { apiServer } from "@/lib/api";
|
|
|
|
| 11 |
import AppContext from "@/components/contexts/app-context";
|
| 12 |
+
import Script from "next/script";
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
const inter = Inter({
|
| 15 |
variable: "--font-inter-sans",
|
|
|
|
| 23 |
});
|
| 24 |
|
| 25 |
export const metadata: Metadata = {
|
| 26 |
+
title: "DeepSite | Build with AI ✨",
|
| 27 |
+
description:
|
| 28 |
+
"DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.",
|
| 29 |
+
openGraph: {
|
| 30 |
title: "DeepSite | Build with AI ✨",
|
| 31 |
description:
|
| 32 |
"DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.",
|
| 33 |
+
url: "https://deepsite.hf.co",
|
| 34 |
+
siteName: "DeepSite",
|
| 35 |
+
images: [
|
| 36 |
+
{
|
| 37 |
+
url: "https://deepsite.hf.co/banner.png",
|
| 38 |
+
width: 1200,
|
| 39 |
+
height: 630,
|
| 40 |
+
alt: "DeepSite Open Graph Image",
|
| 41 |
+
},
|
| 42 |
+
],
|
| 43 |
+
},
|
| 44 |
+
twitter: {
|
| 45 |
+
card: "summary_large_image",
|
| 46 |
+
title: "DeepSite | Build with AI ✨",
|
| 47 |
+
description:
|
| 48 |
+
"DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.",
|
| 49 |
+
images: ["https://deepsite.hf.co/banner.png"],
|
| 50 |
+
},
|
| 51 |
appleWebApp: {
|
| 52 |
capable: true,
|
| 53 |
title: "DeepSite",
|
|
|
|
| 58 |
shortcut: "/logo.svg",
|
| 59 |
apple: "/logo.svg",
|
| 60 |
},
|
|
|
|
|
|
|
|
|
|
| 61 |
};
|
| 62 |
|
| 63 |
export const viewport: Viewport = {
|
|
|
|
| 66 |
themeColor: "#000000",
|
| 67 |
};
|
| 68 |
|
| 69 |
+
async function getMe() {
|
| 70 |
+
const cookieStore = await cookies();
|
| 71 |
+
const token = cookieStore.get(MY_TOKEN_KEY())?.value;
|
| 72 |
+
if (!token) return { user: null, errCode: null };
|
| 73 |
+
try {
|
| 74 |
+
const res = await apiServer.get("/me", {
|
| 75 |
+
headers: {
|
| 76 |
+
Authorization: `Bearer ${token}`,
|
| 77 |
+
},
|
| 78 |
+
});
|
| 79 |
+
return { user: res.data.user, errCode: null };
|
| 80 |
+
} catch (err: any) {
|
| 81 |
+
return { user: null, errCode: err.status };
|
| 82 |
+
}
|
| 83 |
+
}
|
|
|
|
|
|
|
| 84 |
|
| 85 |
export default async function RootLayout({
|
| 86 |
children,
|
| 87 |
}: Readonly<{
|
| 88 |
children: React.ReactNode;
|
| 89 |
}>) {
|
| 90 |
+
const data = await getMe();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
return (
|
| 92 |
<html lang="en">
|
| 93 |
+
<Script
|
| 94 |
+
defer
|
| 95 |
+
data-domain="deepsite.hf.co"
|
| 96 |
+
src="https://plausible.io/js/script.js"
|
| 97 |
+
></Script>
|
| 98 |
<body
|
| 99 |
className={`${inter.variable} ${ptSans.variable} antialiased bg-black dark h-[100dvh] overflow-hidden`}
|
| 100 |
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
<Toaster richColors position="bottom-center" />
|
| 102 |
+
<TanstackProvider>
|
| 103 |
+
<AppContext me={data}>{children}</AppContext>
|
| 104 |
+
</TanstackProvider>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
</body>
|
| 106 |
</html>
|
| 107 |
);
|
app/new/page.tsx
DELETED
|
@@ -1,14 +0,0 @@
|
|
| 1 |
-
import { AppEditor } from "@/components/editor";
|
| 2 |
-
import { Metadata } from "next";
|
| 3 |
-
import { generateSEO } from "@/lib/seo";
|
| 4 |
-
|
| 5 |
-
export const metadata: Metadata = generateSEO({
|
| 6 |
-
title: "Create New Project - DeepSite",
|
| 7 |
-
description:
|
| 8 |
-
"Start building your next website with AI. Create a new project on DeepSite and experience the power of AI-driven web development.",
|
| 9 |
-
path: "/new",
|
| 10 |
-
});
|
| 11 |
-
|
| 12 |
-
export default function NewProjectPage() {
|
| 13 |
-
return <AppEditor isNew />;
|
| 14 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/projects/[namespace]/[repoId]/page.tsx
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { cookies } from "next/headers";
|
| 2 |
+
import { redirect } from "next/navigation";
|
| 3 |
+
|
| 4 |
+
import { apiServer } from "@/lib/api";
|
| 5 |
+
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
| 6 |
+
import { AppEditor } from "@/components/editor";
|
| 7 |
+
|
| 8 |
+
async function getProject(namespace: string, repoId: string) {
|
| 9 |
+
// TODO replace with a server action
|
| 10 |
+
const cookieStore = await cookies();
|
| 11 |
+
const token = cookieStore.get(MY_TOKEN_KEY())?.value;
|
| 12 |
+
if (!token) return {};
|
| 13 |
+
try {
|
| 14 |
+
const { data } = await apiServer.get(
|
| 15 |
+
`/me/projects/${namespace}/${repoId}`,
|
| 16 |
+
{
|
| 17 |
+
headers: {
|
| 18 |
+
Authorization: `Bearer ${token}`,
|
| 19 |
+
},
|
| 20 |
+
}
|
| 21 |
+
);
|
| 22 |
+
|
| 23 |
+
return data.project;
|
| 24 |
+
} catch {
|
| 25 |
+
return {};
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export default async function ProjectNamespacePage({
|
| 30 |
+
params,
|
| 31 |
+
}: {
|
| 32 |
+
params: Promise<{ namespace: string; repoId: string }>;
|
| 33 |
+
}) {
|
| 34 |
+
const { namespace, repoId } = await params;
|
| 35 |
+
const project = await getProject(namespace, repoId);
|
| 36 |
+
if (!project?.html) {
|
| 37 |
+
redirect("/projects");
|
| 38 |
+
}
|
| 39 |
+
return <AppEditor project={project} />;
|
| 40 |
+
}
|
app/projects/new/page.tsx
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { AppEditor } from "@/components/editor";
|
| 2 |
+
|
| 3 |
+
export default function ProjectsNewPage() {
|
| 4 |
+
return <AppEditor />;
|
| 5 |
+
}
|
app/sitemap.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
| 1 |
-
import { MetadataRoute } from 'next';
|
| 2 |
-
|
| 3 |
-
export default function sitemap(): MetadataRoute.Sitemap {
|
| 4 |
-
const baseUrl = 'https://huggingface.co/deepsite';
|
| 5 |
-
|
| 6 |
-
return [
|
| 7 |
-
{
|
| 8 |
-
url: baseUrl,
|
| 9 |
-
lastModified: new Date(),
|
| 10 |
-
changeFrequency: 'daily',
|
| 11 |
-
priority: 1,
|
| 12 |
-
},
|
| 13 |
-
{
|
| 14 |
-
url: `${baseUrl}/new`,
|
| 15 |
-
lastModified: new Date(),
|
| 16 |
-
changeFrequency: 'weekly',
|
| 17 |
-
priority: 0.8,
|
| 18 |
-
},
|
| 19 |
-
{
|
| 20 |
-
url: `${baseUrl}/auth`,
|
| 21 |
-
lastModified: new Date(),
|
| 22 |
-
changeFrequency: 'monthly',
|
| 23 |
-
priority: 0.5,
|
| 24 |
-
},
|
| 25 |
-
// Note: Dynamic project routes will be handled by Next.js automatically
|
| 26 |
-
// but you can add specific high-priority project pages here if needed
|
| 27 |
-
];
|
| 28 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assets/deepseek.svg
DELETED
assets/globals.css
CHANGED
|
@@ -112,10 +112,6 @@
|
|
| 112 |
--sidebar-ring: oklch(0.556 0 0);
|
| 113 |
}
|
| 114 |
|
| 115 |
-
body {
|
| 116 |
-
@apply scroll-smooth
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
@layer base {
|
| 120 |
* {
|
| 121 |
@apply border-border outline-ring/50;
|
|
@@ -131,7 +127,7 @@ body {
|
|
| 131 |
.background__noisy {
|
| 132 |
@apply bg-blend-normal pointer-events-none opacity-90;
|
| 133 |
background-size: 25ww auto;
|
| 134 |
-
background-image: url("/
|
| 135 |
@apply fixed w-screen h-screen -z-1 top-0 left-0;
|
| 136 |
}
|
| 137 |
|
|
@@ -148,224 +144,3 @@ body {
|
|
| 148 |
.matched-line {
|
| 149 |
@apply bg-sky-500/30;
|
| 150 |
}
|
| 151 |
-
|
| 152 |
-
/* Fast liquid deformation animations */
|
| 153 |
-
@keyframes liquidBlob1 {
|
| 154 |
-
0%, 100% {
|
| 155 |
-
border-radius: 40% 60% 50% 50%;
|
| 156 |
-
transform: scaleX(1) scaleY(1) rotate(0deg);
|
| 157 |
-
}
|
| 158 |
-
12.5% {
|
| 159 |
-
border-radius: 20% 80% 70% 30%;
|
| 160 |
-
transform: scaleX(1.6) scaleY(0.4) rotate(25deg);
|
| 161 |
-
}
|
| 162 |
-
25% {
|
| 163 |
-
border-radius: 80% 20% 30% 70%;
|
| 164 |
-
transform: scaleX(0.5) scaleY(2.1) rotate(-15deg);
|
| 165 |
-
}
|
| 166 |
-
37.5% {
|
| 167 |
-
border-radius: 30% 70% 80% 20%;
|
| 168 |
-
transform: scaleX(1.8) scaleY(0.6) rotate(40deg);
|
| 169 |
-
}
|
| 170 |
-
50% {
|
| 171 |
-
border-radius: 70% 30% 20% 80%;
|
| 172 |
-
transform: scaleX(0.4) scaleY(1.9) rotate(-30deg);
|
| 173 |
-
}
|
| 174 |
-
62.5% {
|
| 175 |
-
border-radius: 25% 75% 60% 40%;
|
| 176 |
-
transform: scaleX(1.5) scaleY(0.7) rotate(55deg);
|
| 177 |
-
}
|
| 178 |
-
75% {
|
| 179 |
-
border-radius: 75% 25% 40% 60%;
|
| 180 |
-
transform: scaleX(0.6) scaleY(1.7) rotate(-10deg);
|
| 181 |
-
}
|
| 182 |
-
87.5% {
|
| 183 |
-
border-radius: 50% 50% 75% 25%;
|
| 184 |
-
transform: scaleX(1.3) scaleY(0.8) rotate(35deg);
|
| 185 |
-
}
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
@keyframes liquidBlob2 {
|
| 189 |
-
0%, 100% {
|
| 190 |
-
border-radius: 60% 40% 50% 50%;
|
| 191 |
-
transform: scaleX(1) scaleY(1) rotate(12deg);
|
| 192 |
-
}
|
| 193 |
-
16% {
|
| 194 |
-
border-radius: 15% 85% 60% 40%;
|
| 195 |
-
transform: scaleX(0.3) scaleY(2.3) rotate(50deg);
|
| 196 |
-
}
|
| 197 |
-
32% {
|
| 198 |
-
border-radius: 85% 15% 25% 75%;
|
| 199 |
-
transform: scaleX(2.0) scaleY(0.5) rotate(-20deg);
|
| 200 |
-
}
|
| 201 |
-
48% {
|
| 202 |
-
border-radius: 30% 70% 85% 15%;
|
| 203 |
-
transform: scaleX(0.4) scaleY(1.8) rotate(70deg);
|
| 204 |
-
}
|
| 205 |
-
64% {
|
| 206 |
-
border-radius: 70% 30% 15% 85%;
|
| 207 |
-
transform: scaleX(1.9) scaleY(0.6) rotate(-35deg);
|
| 208 |
-
}
|
| 209 |
-
80% {
|
| 210 |
-
border-radius: 40% 60% 70% 30%;
|
| 211 |
-
transform: scaleX(0.7) scaleY(1.6) rotate(45deg);
|
| 212 |
-
}
|
| 213 |
-
}
|
| 214 |
-
|
| 215 |
-
@keyframes liquidBlob3 {
|
| 216 |
-
0%, 100% {
|
| 217 |
-
border-radius: 50% 50% 40% 60%;
|
| 218 |
-
transform: scaleX(1) scaleY(1) rotate(0deg);
|
| 219 |
-
}
|
| 220 |
-
20% {
|
| 221 |
-
border-radius: 10% 90% 75% 25%;
|
| 222 |
-
transform: scaleX(2.2) scaleY(0.3) rotate(-45deg);
|
| 223 |
-
}
|
| 224 |
-
40% {
|
| 225 |
-
border-radius: 90% 10% 20% 80%;
|
| 226 |
-
transform: scaleX(0.4) scaleY(2.5) rotate(60deg);
|
| 227 |
-
}
|
| 228 |
-
60% {
|
| 229 |
-
border-radius: 25% 75% 90% 10%;
|
| 230 |
-
transform: scaleX(1.7) scaleY(0.5) rotate(-25deg);
|
| 231 |
-
}
|
| 232 |
-
80% {
|
| 233 |
-
border-radius: 75% 25% 10% 90%;
|
| 234 |
-
transform: scaleX(0.6) scaleY(2.0) rotate(80deg);
|
| 235 |
-
}
|
| 236 |
-
}
|
| 237 |
-
|
| 238 |
-
@keyframes liquidBlob4 {
|
| 239 |
-
0%, 100% {
|
| 240 |
-
border-radius: 45% 55% 50% 50%;
|
| 241 |
-
transform: scaleX(1) scaleY(1) rotate(-15deg);
|
| 242 |
-
}
|
| 243 |
-
14% {
|
| 244 |
-
border-radius: 90% 10% 65% 35%;
|
| 245 |
-
transform: scaleX(0.2) scaleY(2.8) rotate(35deg);
|
| 246 |
-
}
|
| 247 |
-
28% {
|
| 248 |
-
border-radius: 10% 90% 20% 80%;
|
| 249 |
-
transform: scaleX(2.4) scaleY(0.4) rotate(-50deg);
|
| 250 |
-
}
|
| 251 |
-
42% {
|
| 252 |
-
border-radius: 35% 65% 90% 10%;
|
| 253 |
-
transform: scaleX(0.3) scaleY(2.1) rotate(70deg);
|
| 254 |
-
}
|
| 255 |
-
56% {
|
| 256 |
-
border-radius: 80% 20% 10% 90%;
|
| 257 |
-
transform: scaleX(2.0) scaleY(0.5) rotate(-40deg);
|
| 258 |
-
}
|
| 259 |
-
70% {
|
| 260 |
-
border-radius: 20% 80% 55% 45%;
|
| 261 |
-
transform: scaleX(0.5) scaleY(1.9) rotate(55deg);
|
| 262 |
-
}
|
| 263 |
-
84% {
|
| 264 |
-
border-radius: 65% 35% 80% 20%;
|
| 265 |
-
transform: scaleX(1.6) scaleY(0.6) rotate(-25deg);
|
| 266 |
-
}
|
| 267 |
-
}
|
| 268 |
-
|
| 269 |
-
/* Fast flowing movement animations */
|
| 270 |
-
@keyframes liquidFlow1 {
|
| 271 |
-
0%, 100% { transform: translate(0, 0); }
|
| 272 |
-
16% { transform: translate(60px, -40px); }
|
| 273 |
-
32% { transform: translate(-45px, -70px); }
|
| 274 |
-
48% { transform: translate(80px, 25px); }
|
| 275 |
-
64% { transform: translate(-30px, 60px); }
|
| 276 |
-
80% { transform: translate(50px, -20px); }
|
| 277 |
-
}
|
| 278 |
-
|
| 279 |
-
@keyframes liquidFlow2 {
|
| 280 |
-
0%, 100% { transform: translate(0, 0); }
|
| 281 |
-
20% { transform: translate(-70px, 50px); }
|
| 282 |
-
40% { transform: translate(90px, -30px); }
|
| 283 |
-
60% { transform: translate(-40px, -55px); }
|
| 284 |
-
80% { transform: translate(65px, 35px); }
|
| 285 |
-
}
|
| 286 |
-
|
| 287 |
-
@keyframes liquidFlow3 {
|
| 288 |
-
0%, 100% { transform: translate(0, 0); }
|
| 289 |
-
12% { transform: translate(-50px, -60px); }
|
| 290 |
-
24% { transform: translate(40px, -20px); }
|
| 291 |
-
36% { transform: translate(-30px, 70px); }
|
| 292 |
-
48% { transform: translate(70px, 20px); }
|
| 293 |
-
60% { transform: translate(-60px, -35px); }
|
| 294 |
-
72% { transform: translate(35px, 55px); }
|
| 295 |
-
84% { transform: translate(-25px, -45px); }
|
| 296 |
-
}
|
| 297 |
-
|
| 298 |
-
@keyframes liquidFlow4 {
|
| 299 |
-
0%, 100% { transform: translate(0, 0); }
|
| 300 |
-
14% { transform: translate(50px, 60px); }
|
| 301 |
-
28% { transform: translate(-80px, -40px); }
|
| 302 |
-
42% { transform: translate(30px, -90px); }
|
| 303 |
-
56% { transform: translate(-55px, 45px); }
|
| 304 |
-
70% { transform: translate(75px, -25px); }
|
| 305 |
-
84% { transform: translate(-35px, 65px); }
|
| 306 |
-
}
|
| 307 |
-
|
| 308 |
-
/* Light sweep animation for buttons */
|
| 309 |
-
@keyframes lightSweep {
|
| 310 |
-
0% {
|
| 311 |
-
transform: translateX(-150%);
|
| 312 |
-
opacity: 0;
|
| 313 |
-
}
|
| 314 |
-
8% {
|
| 315 |
-
opacity: 0.3;
|
| 316 |
-
}
|
| 317 |
-
25% {
|
| 318 |
-
opacity: 0.8;
|
| 319 |
-
}
|
| 320 |
-
42% {
|
| 321 |
-
opacity: 0.3;
|
| 322 |
-
}
|
| 323 |
-
50% {
|
| 324 |
-
transform: translateX(150%);
|
| 325 |
-
opacity: 0;
|
| 326 |
-
}
|
| 327 |
-
58% {
|
| 328 |
-
opacity: 0.3;
|
| 329 |
-
}
|
| 330 |
-
75% {
|
| 331 |
-
opacity: 0.8;
|
| 332 |
-
}
|
| 333 |
-
92% {
|
| 334 |
-
opacity: 0.3;
|
| 335 |
-
}
|
| 336 |
-
100% {
|
| 337 |
-
transform: translateX(-150%);
|
| 338 |
-
opacity: 0;
|
| 339 |
-
}
|
| 340 |
-
}
|
| 341 |
-
|
| 342 |
-
.light-sweep {
|
| 343 |
-
position: relative;
|
| 344 |
-
overflow: hidden;
|
| 345 |
-
}
|
| 346 |
-
|
| 347 |
-
.light-sweep::before {
|
| 348 |
-
content: '';
|
| 349 |
-
position: absolute;
|
| 350 |
-
top: 0;
|
| 351 |
-
left: 0;
|
| 352 |
-
right: 0;
|
| 353 |
-
bottom: 0;
|
| 354 |
-
width: 300%;
|
| 355 |
-
background: linear-gradient(
|
| 356 |
-
90deg,
|
| 357 |
-
transparent 0%,
|
| 358 |
-
transparent 20%,
|
| 359 |
-
rgba(56, 189, 248, 0.1) 35%,
|
| 360 |
-
rgba(56, 189, 248, 0.2) 45%,
|
| 361 |
-
rgba(255, 255, 255, 0.2) 50%,
|
| 362 |
-
rgba(168, 85, 247, 0.2) 55%,
|
| 363 |
-
rgba(168, 85, 247, 0.1) 65%,
|
| 364 |
-
transparent 80%,
|
| 365 |
-
transparent 100%
|
| 366 |
-
);
|
| 367 |
-
animation: lightSweep 7s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
| 368 |
-
pointer-events: none;
|
| 369 |
-
z-index: 1;
|
| 370 |
-
filter: blur(1px);
|
| 371 |
-
}
|
|
|
|
| 112 |
--sidebar-ring: oklch(0.556 0 0);
|
| 113 |
}
|
| 114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
@layer base {
|
| 116 |
* {
|
| 117 |
@apply border-border outline-ring/50;
|
|
|
|
| 127 |
.background__noisy {
|
| 128 |
@apply bg-blend-normal pointer-events-none opacity-90;
|
| 129 |
background-size: 25ww auto;
|
| 130 |
+
background-image: url("/background_noisy.webp");
|
| 131 |
@apply fixed w-screen h-screen -z-1 top-0 left-0;
|
| 132 |
}
|
| 133 |
|
|
|
|
| 144 |
.matched-line {
|
| 145 |
@apply bg-sky-500/30;
|
| 146 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assets/kimi.svg
DELETED
assets/qwen.svg
DELETED
assets/zai.svg
DELETED
components.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
| 5 |
"tsx": true,
|
| 6 |
"tailwind": {
|
| 7 |
"config": "",
|
| 8 |
-
"css": "
|
| 9 |
"baseColor": "neutral",
|
| 10 |
"cssVariables": true,
|
| 11 |
"prefix": ""
|
|
|
|
| 5 |
"tsx": true,
|
| 6 |
"tailwind": {
|
| 7 |
"config": "",
|
| 8 |
+
"css": "app/globals.css",
|
| 9 |
"baseColor": "neutral",
|
| 10 |
"cssVariables": true,
|
| 11 |
"prefix": ""
|
components/animated-blobs/index.tsx
DELETED
|
@@ -1,34 +0,0 @@
|
|
| 1 |
-
export function AnimatedBlobs() {
|
| 2 |
-
return (
|
| 3 |
-
<div className="absolute inset-0 pointer-events-none -z-[1]">
|
| 4 |
-
<div
|
| 5 |
-
className="w-full h-full bg-gradient-to-r from-purple-500 to-pink-500 opacity-10 blur-3xl"
|
| 6 |
-
style={{
|
| 7 |
-
animation:
|
| 8 |
-
"liquidBlob1 4s ease-in-out infinite, liquidFlow1 6s ease-in-out infinite",
|
| 9 |
-
}}
|
| 10 |
-
/>
|
| 11 |
-
<div
|
| 12 |
-
className="w-2/3 h-3/4 bg-gradient-to-r from-blue-500 to-teal-500 opacity-24 blur-3xl absolute -top-20 right-10"
|
| 13 |
-
style={{
|
| 14 |
-
animation:
|
| 15 |
-
"liquidBlob2 5s ease-in-out infinite, liquidFlow2 7s ease-in-out infinite",
|
| 16 |
-
}}
|
| 17 |
-
/>
|
| 18 |
-
<div
|
| 19 |
-
className="w-1/2 h-1/2 bg-gradient-to-r from-amber-500 to-rose-500 opacity-20 blur-3xl absolute bottom-0 left-10"
|
| 20 |
-
style={{
|
| 21 |
-
animation:
|
| 22 |
-
"liquidBlob3 3.5s ease-in-out infinite, liquidFlow3 8s ease-in-out infinite",
|
| 23 |
-
}}
|
| 24 |
-
/>
|
| 25 |
-
<div
|
| 26 |
-
className="w-48 h-48 bg-gradient-to-r from-cyan-500 to-indigo-500 opacity-20 blur-3xl absolute top-1/3 right-1/3"
|
| 27 |
-
style={{
|
| 28 |
-
animation:
|
| 29 |
-
"liquidBlob4 4.5s ease-in-out infinite, liquidFlow4 6.5s ease-in-out infinite",
|
| 30 |
-
}}
|
| 31 |
-
/>
|
| 32 |
-
</div>
|
| 33 |
-
);
|
| 34 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/animated-text/index.tsx
DELETED
|
@@ -1,123 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { useState, useEffect } from "react";
|
| 4 |
-
|
| 5 |
-
interface AnimatedTextProps {
|
| 6 |
-
className?: string;
|
| 7 |
-
}
|
| 8 |
-
|
| 9 |
-
export function AnimatedText({ className = "" }: AnimatedTextProps) {
|
| 10 |
-
const [displayText, setDisplayText] = useState("");
|
| 11 |
-
const [currentSuggestionIndex, setCurrentSuggestionIndex] = useState(0);
|
| 12 |
-
const [isTyping, setIsTyping] = useState(true);
|
| 13 |
-
const [showCursor, setShowCursor] = useState(true);
|
| 14 |
-
const [lastTypedIndex, setLastTypedIndex] = useState(-1);
|
| 15 |
-
const [animationComplete, setAnimationComplete] = useState(false);
|
| 16 |
-
|
| 17 |
-
// Randomize suggestions on each component mount
|
| 18 |
-
const [suggestions] = useState(() => {
|
| 19 |
-
const baseSuggestions = [
|
| 20 |
-
"create a stunning portfolio!",
|
| 21 |
-
"build a tic tac toe game!",
|
| 22 |
-
"design a website for my restaurant!",
|
| 23 |
-
"make a sleek landing page!",
|
| 24 |
-
"build an e-commerce store!",
|
| 25 |
-
"create a personal blog!",
|
| 26 |
-
"develop a modern dashboard!",
|
| 27 |
-
"design a company website!",
|
| 28 |
-
"build a todo app!",
|
| 29 |
-
"create an online gallery!",
|
| 30 |
-
"make a contact form!",
|
| 31 |
-
"build a weather app!",
|
| 32 |
-
];
|
| 33 |
-
|
| 34 |
-
// Fisher-Yates shuffle algorithm
|
| 35 |
-
const shuffled = [...baseSuggestions];
|
| 36 |
-
for (let i = shuffled.length - 1; i > 0; i--) {
|
| 37 |
-
const j = Math.floor(Math.random() * (i + 1));
|
| 38 |
-
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
return shuffled;
|
| 42 |
-
});
|
| 43 |
-
|
| 44 |
-
useEffect(() => {
|
| 45 |
-
if (animationComplete) return;
|
| 46 |
-
|
| 47 |
-
let timeout: NodeJS.Timeout;
|
| 48 |
-
|
| 49 |
-
const typeText = () => {
|
| 50 |
-
const currentSuggestion = suggestions[currentSuggestionIndex];
|
| 51 |
-
|
| 52 |
-
if (isTyping) {
|
| 53 |
-
if (displayText.length < currentSuggestion.length) {
|
| 54 |
-
setDisplayText(currentSuggestion.slice(0, displayText.length + 1));
|
| 55 |
-
setLastTypedIndex(displayText.length);
|
| 56 |
-
timeout = setTimeout(typeText, 80);
|
| 57 |
-
} else {
|
| 58 |
-
// Finished typing, wait then start erasing
|
| 59 |
-
setLastTypedIndex(-1);
|
| 60 |
-
timeout = setTimeout(() => {
|
| 61 |
-
setIsTyping(false);
|
| 62 |
-
}, 2000);
|
| 63 |
-
}
|
| 64 |
-
}
|
| 65 |
-
};
|
| 66 |
-
|
| 67 |
-
timeout = setTimeout(typeText, 100);
|
| 68 |
-
return () => clearTimeout(timeout);
|
| 69 |
-
}, [
|
| 70 |
-
displayText,
|
| 71 |
-
currentSuggestionIndex,
|
| 72 |
-
isTyping,
|
| 73 |
-
suggestions,
|
| 74 |
-
animationComplete,
|
| 75 |
-
]);
|
| 76 |
-
|
| 77 |
-
// Cursor blinking effect
|
| 78 |
-
useEffect(() => {
|
| 79 |
-
if (animationComplete) {
|
| 80 |
-
setShowCursor(false);
|
| 81 |
-
return;
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
const cursorInterval = setInterval(() => {
|
| 85 |
-
setShowCursor((prev) => !prev);
|
| 86 |
-
}, 600);
|
| 87 |
-
|
| 88 |
-
return () => clearInterval(cursorInterval);
|
| 89 |
-
}, [animationComplete]);
|
| 90 |
-
|
| 91 |
-
useEffect(() => {
|
| 92 |
-
if (lastTypedIndex >= 0) {
|
| 93 |
-
const timeout = setTimeout(() => {
|
| 94 |
-
setLastTypedIndex(-1);
|
| 95 |
-
}, 400);
|
| 96 |
-
|
| 97 |
-
return () => clearTimeout(timeout);
|
| 98 |
-
}
|
| 99 |
-
}, [lastTypedIndex]);
|
| 100 |
-
|
| 101 |
-
return (
|
| 102 |
-
<p className={`font-mono ${className}`}>
|
| 103 |
-
Hey DeepSite,
|
| 104 |
-
{displayText.split("").map((char, index) => (
|
| 105 |
-
<span
|
| 106 |
-
key={`${currentSuggestionIndex}-${index}`}
|
| 107 |
-
className={`transition-colors duration-300 ${
|
| 108 |
-
index === lastTypedIndex ? "text-neutral-100" : ""
|
| 109 |
-
}`}
|
| 110 |
-
>
|
| 111 |
-
{char}
|
| 112 |
-
</span>
|
| 113 |
-
))}
|
| 114 |
-
<span
|
| 115 |
-
className={`${
|
| 116 |
-
showCursor ? "opacity-100" : "opacity-0"
|
| 117 |
-
} transition-opacity`}
|
| 118 |
-
>
|
| 119 |
-
|
|
| 120 |
-
</span>
|
| 121 |
-
</p>
|
| 122 |
-
);
|
| 123 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/contexts/app-context.tsx
CHANGED
|
@@ -1,41 +1,42 @@
|
|
| 1 |
/* eslint-disable @typescript-eslint/no-explicit-any */
|
| 2 |
"use client";
|
| 3 |
-
import { useMount } from "react-use";
|
| 4 |
-
import { toast } from "sonner";
|
| 5 |
-
import { usePathname, useRouter } from "next/navigation";
|
| 6 |
|
| 7 |
import { useUser } from "@/hooks/useUser";
|
| 8 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
|
| 10 |
|
| 11 |
export default function AppContext({
|
| 12 |
children,
|
| 13 |
-
|
| 14 |
-
{
|
| 15 |
children: React.ReactNode;
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
// };
|
| 21 |
}) {
|
| 22 |
-
const { loginFromCode, user, logout, loading, errCode } =
|
|
|
|
| 23 |
const pathname = usePathname();
|
| 24 |
const router = useRouter();
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
|
| 40 |
const events: any = {};
|
| 41 |
|
|
@@ -48,5 +49,9 @@ export default function AppContext({
|
|
| 48 |
}
|
| 49 |
});
|
| 50 |
|
| 51 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
}
|
|
|
|
| 1 |
/* eslint-disable @typescript-eslint/no-explicit-any */
|
| 2 |
"use client";
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
import { useUser } from "@/hooks/useUser";
|
| 5 |
+
import { usePathname, useRouter } from "next/navigation";
|
| 6 |
+
import { useMount } from "react-use";
|
| 7 |
+
import { UserContext } from "@/components/contexts/user-context";
|
| 8 |
+
import { User } from "@/types";
|
| 9 |
+
import { toast } from "sonner";
|
| 10 |
import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
|
| 11 |
|
| 12 |
export default function AppContext({
|
| 13 |
children,
|
| 14 |
+
me: initialData,
|
| 15 |
+
}: {
|
| 16 |
children: React.ReactNode;
|
| 17 |
+
me?: {
|
| 18 |
+
user: User | null;
|
| 19 |
+
errCode: number | null;
|
| 20 |
+
};
|
|
|
|
| 21 |
}) {
|
| 22 |
+
const { loginFromCode, user, logout, loading, errCode } =
|
| 23 |
+
useUser(initialData);
|
| 24 |
const pathname = usePathname();
|
| 25 |
const router = useRouter();
|
| 26 |
|
| 27 |
+
useMount(() => {
|
| 28 |
+
if (!initialData?.user && !user) {
|
| 29 |
+
if ([401, 403].includes(errCode as number)) {
|
| 30 |
+
logout();
|
| 31 |
+
} else if (pathname.includes("/spaces")) {
|
| 32 |
+
if (errCode) {
|
| 33 |
+
toast.error("An error occured while trying to log in");
|
| 34 |
+
}
|
| 35 |
+
// If we did not manage to log in (probs because api is down), we simply redirect to the home page
|
| 36 |
+
router.push("/");
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
});
|
| 40 |
|
| 41 |
const events: any = {};
|
| 42 |
|
|
|
|
| 49 |
}
|
| 50 |
});
|
| 51 |
|
| 52 |
+
return (
|
| 53 |
+
<UserContext value={{ user, loading, logout } as any}>
|
| 54 |
+
{children}
|
| 55 |
+
</UserContext>
|
| 56 |
+
);
|
| 57 |
}
|
components/contexts/login-context.tsx
DELETED
|
@@ -1,62 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import React, { createContext, useContext, useState, ReactNode } from "react";
|
| 4 |
-
import { LoginModal } from "@/components/login-modal";
|
| 5 |
-
import { Page } from "@/types";
|
| 6 |
-
|
| 7 |
-
interface LoginContextType {
|
| 8 |
-
isOpen: boolean;
|
| 9 |
-
openLoginModal: (options?: LoginModalOptions) => void;
|
| 10 |
-
closeLoginModal: () => void;
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
interface LoginModalOptions {
|
| 14 |
-
pages?: Page[];
|
| 15 |
-
title?: string;
|
| 16 |
-
prompt?: string;
|
| 17 |
-
description?: string;
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
const LoginContext = createContext<LoginContextType | undefined>(undefined);
|
| 21 |
-
|
| 22 |
-
export function LoginProvider({ children }: { children: ReactNode }) {
|
| 23 |
-
const [isOpen, setIsOpen] = useState(false);
|
| 24 |
-
const [modalOptions, setModalOptions] = useState<LoginModalOptions>({});
|
| 25 |
-
|
| 26 |
-
const openLoginModal = (options: LoginModalOptions = {}) => {
|
| 27 |
-
setModalOptions(options);
|
| 28 |
-
setIsOpen(true);
|
| 29 |
-
};
|
| 30 |
-
|
| 31 |
-
const closeLoginModal = () => {
|
| 32 |
-
setIsOpen(false);
|
| 33 |
-
setModalOptions({});
|
| 34 |
-
};
|
| 35 |
-
|
| 36 |
-
const value = {
|
| 37 |
-
isOpen,
|
| 38 |
-
openLoginModal,
|
| 39 |
-
closeLoginModal,
|
| 40 |
-
};
|
| 41 |
-
|
| 42 |
-
return (
|
| 43 |
-
<LoginContext.Provider value={value}>
|
| 44 |
-
{children}
|
| 45 |
-
<LoginModal
|
| 46 |
-
open={isOpen}
|
| 47 |
-
onClose={setIsOpen}
|
| 48 |
-
title={modalOptions.title}
|
| 49 |
-
prompt={modalOptions.prompt}
|
| 50 |
-
description={modalOptions.description}
|
| 51 |
-
/>
|
| 52 |
-
</LoginContext.Provider>
|
| 53 |
-
);
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
export function useLoginModal() {
|
| 57 |
-
const context = useContext(LoginContext);
|
| 58 |
-
if (context === undefined) {
|
| 59 |
-
throw new Error("useLoginModal must be used within a LoginProvider");
|
| 60 |
-
}
|
| 61 |
-
return context;
|
| 62 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/contexts/pro-context.tsx
DELETED
|
@@ -1,48 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import React, { createContext, useContext, useState, ReactNode } from "react";
|
| 4 |
-
import { ProModal } from "@/components/pro-modal";
|
| 5 |
-
import { Page } from "@/types";
|
| 6 |
-
import { useEditor } from "@/hooks/useEditor";
|
| 7 |
-
|
| 8 |
-
interface ProContextType {
|
| 9 |
-
isOpen: boolean;
|
| 10 |
-
openProModal: (pages: Page[]) => void;
|
| 11 |
-
closeProModal: () => void;
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
const ProContext = createContext<ProContextType | undefined>(undefined);
|
| 15 |
-
|
| 16 |
-
export function ProProvider({ children }: { children: ReactNode }) {
|
| 17 |
-
const [isOpen, setIsOpen] = useState(false);
|
| 18 |
-
const { pages } = useEditor();
|
| 19 |
-
|
| 20 |
-
const openProModal = () => {
|
| 21 |
-
setIsOpen(true);
|
| 22 |
-
};
|
| 23 |
-
|
| 24 |
-
const closeProModal = () => {
|
| 25 |
-
setIsOpen(false);
|
| 26 |
-
};
|
| 27 |
-
|
| 28 |
-
const value = {
|
| 29 |
-
isOpen,
|
| 30 |
-
openProModal,
|
| 31 |
-
closeProModal,
|
| 32 |
-
};
|
| 33 |
-
|
| 34 |
-
return (
|
| 35 |
-
<ProContext.Provider value={value}>
|
| 36 |
-
{children}
|
| 37 |
-
<ProModal open={isOpen} onClose={setIsOpen} pages={pages} />
|
| 38 |
-
</ProContext.Provider>
|
| 39 |
-
);
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
export function useProModal() {
|
| 43 |
-
const context = useContext(ProContext);
|
| 44 |
-
if (context === undefined) {
|
| 45 |
-
throw new Error("useProModal must be used within a ProProvider");
|
| 46 |
-
}
|
| 47 |
-
return context;
|
| 48 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/domain-redirect/index.tsx
DELETED
|
@@ -1,23 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { useEffect } from "react";
|
| 4 |
-
|
| 5 |
-
export default function DomainRedirect() {
|
| 6 |
-
useEffect(() => {
|
| 7 |
-
if (typeof window === "undefined") return;
|
| 8 |
-
|
| 9 |
-
const host = window.location.host;
|
| 10 |
-
|
| 11 |
-
// Check if we're not on hf.co or huggingface.co
|
| 12 |
-
const isHfCo = host === "hf.co" || host.startsWith("hf.co:");
|
| 13 |
-
const isHuggingFaceCo =
|
| 14 |
-
host === "huggingface.co" || host.startsWith("huggingface.co:");
|
| 15 |
-
|
| 16 |
-
if (!isHfCo && !isHuggingFaceCo) {
|
| 17 |
-
// Redirect to the correct URL
|
| 18 |
-
window.location.replace("https://huggingface.co/deepsite");
|
| 19 |
-
}
|
| 20 |
-
}, []);
|
| 21 |
-
|
| 22 |
-
return null;
|
| 23 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/context.tsx
DELETED
|
@@ -1,124 +0,0 @@
|
|
| 1 |
-
import { useState, useMemo } from "react";
|
| 2 |
-
import { FileCode, FileText, Braces, AtSign } from "lucide-react";
|
| 3 |
-
|
| 4 |
-
import { Button } from "@/components/ui/button";
|
| 5 |
-
import { useEditor } from "@/hooks/useEditor";
|
| 6 |
-
import { useAi } from "@/hooks/useAi";
|
| 7 |
-
import {
|
| 8 |
-
Popover,
|
| 9 |
-
PopoverContent,
|
| 10 |
-
PopoverTrigger,
|
| 11 |
-
} from "@/components/ui/popover";
|
| 12 |
-
import classNames from "classnames";
|
| 13 |
-
|
| 14 |
-
export const Context = () => {
|
| 15 |
-
const { pages, currentPage, globalEditorLoading } = useEditor();
|
| 16 |
-
const { contextFile, setContextFile, globalAiLoading } = useAi();
|
| 17 |
-
const [open, setOpen] = useState(false);
|
| 18 |
-
|
| 19 |
-
const selectedFile = contextFile || null;
|
| 20 |
-
|
| 21 |
-
const getFileIcon = (filePath: string, size = "size-3.5") => {
|
| 22 |
-
if (filePath.endsWith(".css")) {
|
| 23 |
-
return <Braces className={size} />;
|
| 24 |
-
} else if (filePath.endsWith(".js")) {
|
| 25 |
-
return <FileCode className={size} />;
|
| 26 |
-
} else {
|
| 27 |
-
return <FileText className={size} />;
|
| 28 |
-
}
|
| 29 |
-
};
|
| 30 |
-
|
| 31 |
-
const buttonContent = useMemo(() => {
|
| 32 |
-
if (selectedFile) {
|
| 33 |
-
return (
|
| 34 |
-
<>
|
| 35 |
-
<span className="truncate max-w-[120px]">{selectedFile}</span>
|
| 36 |
-
</>
|
| 37 |
-
);
|
| 38 |
-
}
|
| 39 |
-
return <>Add Context</>;
|
| 40 |
-
}, [selectedFile]);
|
| 41 |
-
|
| 42 |
-
return (
|
| 43 |
-
<Popover open={open} onOpenChange={setOpen}>
|
| 44 |
-
<PopoverTrigger asChild>
|
| 45 |
-
<Button
|
| 46 |
-
size="xs"
|
| 47 |
-
variant={open ? "default" : "outline"}
|
| 48 |
-
className={classNames("!rounded-md", {
|
| 49 |
-
"!bg-blue-500/10 !border-blue-500/30 !text-blue-400":
|
| 50 |
-
selectedFile && selectedFile.endsWith(".css"),
|
| 51 |
-
"!bg-orange-500/10 !border-orange-500/30 !text-orange-400":
|
| 52 |
-
selectedFile && selectedFile.endsWith(".html"),
|
| 53 |
-
"!bg-amber-500/10 !border-amber-500/30 !text-amber-400":
|
| 54 |
-
selectedFile && selectedFile.endsWith(".js"),
|
| 55 |
-
})}
|
| 56 |
-
disabled={
|
| 57 |
-
globalAiLoading || globalEditorLoading || pages.length === 0
|
| 58 |
-
}
|
| 59 |
-
>
|
| 60 |
-
<AtSign className="size-3.5" />
|
| 61 |
-
|
| 62 |
-
{buttonContent}
|
| 63 |
-
</Button>
|
| 64 |
-
</PopoverTrigger>
|
| 65 |
-
<PopoverContent
|
| 66 |
-
align="start"
|
| 67 |
-
className="w-64 !bg-neutral-900 !border-neutral-800 !p-0 !rounded-2xl overflow-hidden"
|
| 68 |
-
>
|
| 69 |
-
<header className="flex items-center justify-center text-xs px-2 py-2.5 border-b gap-2 bg-neutral-950 border-neutral-800 font-semibold text-neutral-200">
|
| 70 |
-
Select a file to send as context
|
| 71 |
-
</header>
|
| 72 |
-
<main className="space-y-1 p-2">
|
| 73 |
-
<div className="max-h-[200px] overflow-y-auto space-y-0.5">
|
| 74 |
-
{pages.length === 0 ? (
|
| 75 |
-
<div className="px-2 py-2 text-xs text-neutral-500">
|
| 76 |
-
No files available
|
| 77 |
-
</div>
|
| 78 |
-
) : (
|
| 79 |
-
<>
|
| 80 |
-
<button
|
| 81 |
-
onClick={() => {
|
| 82 |
-
setContextFile(null);
|
| 83 |
-
setOpen(false);
|
| 84 |
-
}}
|
| 85 |
-
className={`cursor-pointer w-full px-2 py-1.5 text-xs text-left rounded-md hover:bg-neutral-800 transition-colors ${
|
| 86 |
-
!selectedFile
|
| 87 |
-
? "bg-neutral-800 text-neutral-200 font-medium"
|
| 88 |
-
: "text-neutral-400 hover:text-neutral-200"
|
| 89 |
-
}`}
|
| 90 |
-
>
|
| 91 |
-
All files (default)
|
| 92 |
-
</button>
|
| 93 |
-
{pages.map((page) => (
|
| 94 |
-
<button
|
| 95 |
-
key={page.path}
|
| 96 |
-
onClick={() => {
|
| 97 |
-
setContextFile(page.path);
|
| 98 |
-
setOpen(false);
|
| 99 |
-
}}
|
| 100 |
-
className={`cursor-pointer w-full px-2 py-1.5 text-xs text-left rounded-md hover:bg-neutral-800 transition-colors flex items-center gap-1.5 ${
|
| 101 |
-
selectedFile === page.path
|
| 102 |
-
? "bg-neutral-800 text-neutral-200 font-medium"
|
| 103 |
-
: "text-neutral-400 hover:text-neutral-200"
|
| 104 |
-
}`}
|
| 105 |
-
>
|
| 106 |
-
<span className="shrink-0">
|
| 107 |
-
{getFileIcon(page.path, "size-3")}
|
| 108 |
-
</span>
|
| 109 |
-
<span className="truncate flex-1">{page.path}</span>
|
| 110 |
-
{page.path === currentPage && (
|
| 111 |
-
<span className="text-[10px] text-neutral-500 shrink-0">
|
| 112 |
-
(current)
|
| 113 |
-
</span>
|
| 114 |
-
)}
|
| 115 |
-
</button>
|
| 116 |
-
))}
|
| 117 |
-
</>
|
| 118 |
-
)}
|
| 119 |
-
</div>
|
| 120 |
-
</main>
|
| 121 |
-
</PopoverContent>
|
| 122 |
-
</Popover>
|
| 123 |
-
);
|
| 124 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/fake-ask.tsx
DELETED
|
@@ -1,97 +0,0 @@
|
|
| 1 |
-
import { useState } from "react";
|
| 2 |
-
import { useLocalStorage } from "react-use";
|
| 3 |
-
import { ArrowUp, Dice6 } from "lucide-react";
|
| 4 |
-
import { useRouter } from "next/navigation";
|
| 5 |
-
|
| 6 |
-
import { Button } from "@/components/ui/button";
|
| 7 |
-
import { PromptBuilder } from "./prompt-builder";
|
| 8 |
-
import { EnhancedSettings } from "@/types";
|
| 9 |
-
import { Settings } from "./settings";
|
| 10 |
-
import classNames from "classnames";
|
| 11 |
-
import { PROMPTS_FOR_AI } from "@/lib/prompts";
|
| 12 |
-
|
| 13 |
-
export const FakeAskAi = () => {
|
| 14 |
-
const router = useRouter();
|
| 15 |
-
const [prompt, setPrompt] = useState("");
|
| 16 |
-
const [openProvider, setOpenProvider] = useState(false);
|
| 17 |
-
const [enhancedSettings, setEnhancedSettings, removeEnhancedSettings] =
|
| 18 |
-
useLocalStorage<EnhancedSettings>("deepsite-enhancedSettings", {
|
| 19 |
-
isActive: true,
|
| 20 |
-
primaryColor: undefined,
|
| 21 |
-
secondaryColor: undefined,
|
| 22 |
-
theme: undefined,
|
| 23 |
-
});
|
| 24 |
-
const [, setPromptStorage] = useLocalStorage("prompt", "");
|
| 25 |
-
const [randomPromptLoading, setRandomPromptLoading] = useState(false);
|
| 26 |
-
|
| 27 |
-
const callAi = async () => {
|
| 28 |
-
setPromptStorage(prompt);
|
| 29 |
-
router.push("/new");
|
| 30 |
-
};
|
| 31 |
-
|
| 32 |
-
const randomPrompt = () => {
|
| 33 |
-
setRandomPromptLoading(true);
|
| 34 |
-
setTimeout(() => {
|
| 35 |
-
setPrompt(
|
| 36 |
-
PROMPTS_FOR_AI[Math.floor(Math.random() * PROMPTS_FOR_AI.length)]
|
| 37 |
-
);
|
| 38 |
-
setRandomPromptLoading(false);
|
| 39 |
-
}, 400);
|
| 40 |
-
};
|
| 41 |
-
|
| 42 |
-
return (
|
| 43 |
-
<div className="p-3 w-full max-w-xl mx-auto">
|
| 44 |
-
<div className="relative bg-neutral-800 border border-neutral-700 rounded-2xl ring-[4px] focus-within:ring-neutral-500/30 focus-within:border-neutral-600 ring-transparent z-20 w-full group">
|
| 45 |
-
<div className="w-full relative flex items-start justify-between pr-4 pt-4">
|
| 46 |
-
<textarea
|
| 47 |
-
className="w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 px-4 pb-4 resize-none"
|
| 48 |
-
placeholder="Ask DeepSite anything..."
|
| 49 |
-
value={prompt}
|
| 50 |
-
onChange={(e) => setPrompt(e.target.value)}
|
| 51 |
-
onKeyDown={(e) => {
|
| 52 |
-
if (e.key === "Enter" && !e.shiftKey) {
|
| 53 |
-
callAi();
|
| 54 |
-
}
|
| 55 |
-
}}
|
| 56 |
-
/>
|
| 57 |
-
<Button
|
| 58 |
-
size="iconXs"
|
| 59 |
-
variant="outline"
|
| 60 |
-
className="!rounded-md"
|
| 61 |
-
onClick={() => randomPrompt()}
|
| 62 |
-
>
|
| 63 |
-
<Dice6
|
| 64 |
-
className={classNames("size-4", {
|
| 65 |
-
"animate-spin animation-duration-500": randomPromptLoading,
|
| 66 |
-
})}
|
| 67 |
-
/>
|
| 68 |
-
</Button>
|
| 69 |
-
</div>
|
| 70 |
-
<div className="flex items-center justify-between gap-2 px-4 pb-3 mt-2">
|
| 71 |
-
<div className="flex-1 flex items-center justify-start gap-1.5 flex-wrap">
|
| 72 |
-
<PromptBuilder
|
| 73 |
-
enhancedSettings={enhancedSettings!}
|
| 74 |
-
setEnhancedSettings={setEnhancedSettings}
|
| 75 |
-
/>
|
| 76 |
-
<Settings
|
| 77 |
-
open={openProvider}
|
| 78 |
-
isFollowUp={false}
|
| 79 |
-
error=""
|
| 80 |
-
onClose={setOpenProvider}
|
| 81 |
-
/>
|
| 82 |
-
</div>
|
| 83 |
-
<div className="flex items-center justify-end gap-2">
|
| 84 |
-
<Button
|
| 85 |
-
size="iconXs"
|
| 86 |
-
variant="outline"
|
| 87 |
-
className="!rounded-md"
|
| 88 |
-
onClick={() => callAi()}
|
| 89 |
-
>
|
| 90 |
-
<ArrowUp className="size-4" />
|
| 91 |
-
</Button>
|
| 92 |
-
</div>
|
| 93 |
-
</div>
|
| 94 |
-
</div>
|
| 95 |
-
</div>
|
| 96 |
-
);
|
| 97 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/follow-up-tooltip.tsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
Popover,
|
| 3 |
+
PopoverContent,
|
| 4 |
+
PopoverTrigger,
|
| 5 |
+
} from "@/components/ui/popover";
|
| 6 |
+
import { Info } from "lucide-react";
|
| 7 |
+
|
| 8 |
+
export const FollowUpTooltip = () => {
|
| 9 |
+
return (
|
| 10 |
+
<Popover>
|
| 11 |
+
<PopoverTrigger asChild>
|
| 12 |
+
<Info className="size-3 text-neutral-300 cursor-pointer" />
|
| 13 |
+
</PopoverTrigger>
|
| 14 |
+
<PopoverContent
|
| 15 |
+
align="start"
|
| 16 |
+
className="!rounded-2xl !p-0 min-w-xs text-center overflow-hidden"
|
| 17 |
+
>
|
| 18 |
+
<header className="bg-neutral-950 px-4 py-3 border-b border-neutral-700/70">
|
| 19 |
+
<p className="text-base text-neutral-200 font-semibold">
|
| 20 |
+
⚡ Faster, Smarter Updates
|
| 21 |
+
</p>
|
| 22 |
+
</header>
|
| 23 |
+
<main className="p-4">
|
| 24 |
+
<p className="text-neutral-300 text-sm">
|
| 25 |
+
Using the Diff-Patch system, allow DeepSite to intelligently update
|
| 26 |
+
your project without rewritting the entire codebase.
|
| 27 |
+
</p>
|
| 28 |
+
<p className="text-neutral-500 text-sm mt-2">
|
| 29 |
+
This means faster updates, less data usage, and a more efficient
|
| 30 |
+
development process.
|
| 31 |
+
</p>
|
| 32 |
+
</main>
|
| 33 |
+
</PopoverContent>
|
| 34 |
+
</Popover>
|
| 35 |
+
);
|
| 36 |
+
};
|
components/editor/ask-ai/index.tsx
CHANGED
|
@@ -1,140 +1,276 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
| 2 |
import classNames from "classnames";
|
| 3 |
-
import { ArrowUp, ChevronDown, CircleStop, Dice6 } from "lucide-react";
|
| 4 |
-
import { useLocalStorage, useUpdateEffect, useMount } from "react-use";
|
| 5 |
import { toast } from "sonner";
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
-
import
|
| 8 |
-
import { useEditor } from "@/hooks/useEditor";
|
| 9 |
-
import { EnhancedSettings, Project } from "@/types";
|
| 10 |
-
import { SelectedFiles } from "@/components/editor/ask-ai/selected-files";
|
| 11 |
-
import { SelectedHtmlElement } from "@/components/editor/ask-ai/selected-html-element";
|
| 12 |
-
import { AiLoading } from "@/components/editor/ask-ai/loading";
|
| 13 |
import { Button } from "@/components/ui/button";
|
| 14 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
|
| 16 |
-
import
|
| 17 |
-
import {
|
| 18 |
-
import {
|
| 19 |
-
import {
|
| 20 |
-
import {
|
| 21 |
-
import {
|
| 22 |
-
import {
|
| 23 |
-
import { MAX_FREE_PROJECTS } from "@/lib/utils";
|
| 24 |
-
import { PROMPTS_FOR_AI } from "@/lib/prompts";
|
| 25 |
|
| 26 |
-
export
|
| 27 |
-
|
| 28 |
-
|
| 29 |
onScrollToBottom,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
}: {
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
const
|
| 51 |
-
const
|
|
|
|
| 52 |
const [openProvider, setOpenProvider] = useState(false);
|
| 53 |
const [providerError, setProviderError] = useState("");
|
| 54 |
-
const
|
| 55 |
-
|
| 56 |
-
const [enhancedSettings, setEnhancedSettings, removeEnhancedSettings] =
|
| 57 |
-
useLocalStorage<EnhancedSettings>("deepsite-enhancedSettings", {
|
| 58 |
-
isActive: false,
|
| 59 |
-
primaryColor: undefined,
|
| 60 |
-
secondaryColor: undefined,
|
| 61 |
-
theme: undefined,
|
| 62 |
-
});
|
| 63 |
-
const [promptStorage, , removePromptStorage] = useLocalStorage("prompt", "");
|
| 64 |
-
|
| 65 |
-
const [isFollowUp, setIsFollowUp] = useState(true);
|
| 66 |
-
const [prompt, setPrompt] = useState(
|
| 67 |
-
promptStorage && promptStorage.trim() !== "" ? promptStorage : ""
|
| 68 |
-
);
|
| 69 |
-
const [think, setThink] = useState("");
|
| 70 |
const [openThink, setOpenThink] = useState(false);
|
| 71 |
-
const [
|
|
|
|
|
|
|
| 72 |
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
}
|
| 77 |
-
});
|
| 78 |
|
| 79 |
const callAi = async (redesignMarkdown?: string) => {
|
| 80 |
-
removePromptStorage();
|
| 81 |
-
if (user && !user.isPro && projects.length >= MAX_FREE_PROJECTS)
|
| 82 |
-
return openProModal([]);
|
| 83 |
if (isAiWorking) return;
|
| 84 |
if (!redesignMarkdown && !prompt.trim()) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
user?.name
|
| 105 |
-
);
|
| 106 |
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
}
|
| 115 |
}
|
| 116 |
};
|
| 117 |
|
| 118 |
-
const
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
break;
|
| 127 |
-
case "pro_required":
|
| 128 |
-
openProModal([]);
|
| 129 |
-
break;
|
| 130 |
-
case "api_error":
|
| 131 |
-
toast.error(message || "An error occurred");
|
| 132 |
-
break;
|
| 133 |
-
case "network_error":
|
| 134 |
-
toast.error(message || "Network error occurred");
|
| 135 |
-
break;
|
| 136 |
-
default:
|
| 137 |
-
toast.error("An unexpected error occurred");
|
| 138 |
}
|
| 139 |
};
|
| 140 |
|
|
@@ -144,19 +280,19 @@ export const AskAi = ({
|
|
| 144 |
}
|
| 145 |
}, [think]);
|
| 146 |
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
};
|
| 156 |
|
| 157 |
return (
|
| 158 |
-
<div className="
|
| 159 |
-
<div className="relative bg-neutral-800 border border-neutral-700 rounded-2xl ring-[4px] focus-within:ring-neutral-500/30 focus-within:border-neutral-600 ring-transparent z-
|
| 160 |
{think && (
|
| 161 |
<div className="w-full border-b border-neutral-700 relative overflow-hidden">
|
| 162 |
<header
|
|
@@ -194,13 +330,6 @@ export const AskAi = ({
|
|
| 194 |
</main>
|
| 195 |
</div>
|
| 196 |
)}
|
| 197 |
-
<SelectedFiles
|
| 198 |
-
files={selectedFiles}
|
| 199 |
-
isAiWorking={isAiWorking}
|
| 200 |
-
onDelete={(file) =>
|
| 201 |
-
setSelectedFiles(selectedFiles.filter((f) => f !== file))
|
| 202 |
-
}
|
| 203 |
-
/>
|
| 204 |
{selectedElement && (
|
| 205 |
<div className="px-4 pt-3">
|
| 206 |
<SelectedHtmlElement
|
|
@@ -211,47 +340,36 @@ export const AskAi = ({
|
|
| 211 |
</div>
|
| 212 |
)}
|
| 213 |
<div className="w-full relative flex items-center justify-between">
|
| 214 |
-
{
|
| 215 |
-
<div className="absolute bg-neutral-800
|
| 216 |
-
<
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
size="iconXs"
|
| 230 |
-
variant="outline"
|
| 231 |
-
className="!rounded-md mr-0.5"
|
| 232 |
-
onClick={cancelRequest}
|
| 233 |
-
>
|
| 234 |
-
<CircleStop className="size-4" />
|
| 235 |
-
</Button>
|
| 236 |
-
)}
|
| 237 |
</div>
|
| 238 |
)}
|
| 239 |
-
<
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
}
|
| 243 |
className={classNames(
|
| 244 |
-
"w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4
|
| 245 |
{
|
| 246 |
-
"!pt-2.5":
|
| 247 |
-
selectedElement &&
|
| 248 |
-
!(isAiWorking || isUploading || isThinking),
|
| 249 |
}
|
| 250 |
)}
|
| 251 |
placeholder={
|
| 252 |
selectedElement
|
| 253 |
? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
|
| 254 |
-
:
|
| 255 |
? "Ask DeepSite for edits"
|
| 256 |
: "Ask DeepSite anything..."
|
| 257 |
}
|
|
@@ -263,60 +381,91 @@ export const AskAi = ({
|
|
| 263 |
}
|
| 264 |
}}
|
| 265 |
/>
|
| 266 |
-
{isNew && !isAiWorking && isSameHtml && (
|
| 267 |
-
<Button
|
| 268 |
-
size="iconXs"
|
| 269 |
-
variant="outline"
|
| 270 |
-
className="!rounded-md -translate-y-2 -translate-x-4"
|
| 271 |
-
onClick={() => randomPrompt()}
|
| 272 |
-
>
|
| 273 |
-
<Dice6
|
| 274 |
-
className={classNames("size-4", {
|
| 275 |
-
"animate-spin animation-duration-500": randomPromptLoading,
|
| 276 |
-
})}
|
| 277 |
-
/>
|
| 278 |
-
</Button>
|
| 279 |
-
)}
|
| 280 |
</div>
|
| 281 |
-
<div className="flex items-center justify-between gap-2 px-4 pb-3
|
| 282 |
-
<div className="flex-1 flex items-center justify-start gap-1.5
|
| 283 |
-
{
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
)}
|
|
|
|
|
|
|
|
|
|
| 291 |
<Settings
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
open={openProvider}
|
| 293 |
error={providerError}
|
| 294 |
isFollowUp={!isSameHtml && isFollowUp}
|
| 295 |
onClose={setOpenProvider}
|
| 296 |
/>
|
| 297 |
-
{!isNew && <Uploader project={project} />}
|
| 298 |
-
{isNew && <ReImagine onRedesign={(md) => callAi(md)} />}
|
| 299 |
-
{!isNew && !isSameHtml && <Selector />}
|
| 300 |
-
</div>
|
| 301 |
-
<div className="flex items-center justify-end gap-2">
|
| 302 |
<Button
|
| 303 |
size="iconXs"
|
| 304 |
-
|
| 305 |
-
className="!rounded-md"
|
| 306 |
-
disabled={
|
| 307 |
-
isAiWorking || isUploading || isThinking || !prompt.trim()
|
| 308 |
-
}
|
| 309 |
onClick={() => callAi()}
|
| 310 |
>
|
| 311 |
<ArrowUp className="size-4" />
|
| 312 |
</Button>
|
| 313 |
</div>
|
| 314 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
</div>
|
| 316 |
-
<audio ref={
|
| 317 |
-
<source src="/
|
| 318 |
Your browser does not support the audio element.
|
| 319 |
</audio>
|
| 320 |
</div>
|
| 321 |
);
|
| 322 |
-
}
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
| 3 |
+
import { useState, useRef, useMemo } from "react";
|
| 4 |
import classNames from "classnames";
|
|
|
|
|
|
|
| 5 |
import { toast } from "sonner";
|
| 6 |
+
import { useLocalStorage, useUpdateEffect } from "react-use";
|
| 7 |
+
import { ArrowUp, ChevronDown, Crosshair } from "lucide-react";
|
| 8 |
+
import { FaStopCircle } from "react-icons/fa";
|
| 9 |
|
| 10 |
+
import ProModal from "@/components/pro-modal";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
import { Button } from "@/components/ui/button";
|
| 12 |
+
import { MODELS } from "@/lib/providers";
|
| 13 |
+
import { HtmlHistory } from "@/types";
|
| 14 |
+
import { InviteFriends } from "@/components/invite-friends";
|
| 15 |
+
import { Settings } from "@/components/editor/ask-ai/settings";
|
| 16 |
+
import { LoginModal } from "@/components/login-modal";
|
| 17 |
import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
|
| 18 |
+
import Loading from "@/components/loading";
|
| 19 |
+
import { Checkbox } from "@/components/ui/checkbox";
|
| 20 |
+
import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip";
|
| 21 |
+
import { TooltipContent } from "@radix-ui/react-tooltip";
|
| 22 |
+
import { SelectedHtmlElement } from "./selected-html-element";
|
| 23 |
+
import { FollowUpTooltip } from "./follow-up-tooltip";
|
| 24 |
+
import { isTheSameHtml } from "@/lib/compare-html-diff";
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
export function AskAI({
|
| 27 |
+
html,
|
| 28 |
+
setHtml,
|
| 29 |
onScrollToBottom,
|
| 30 |
+
isAiWorking,
|
| 31 |
+
setisAiWorking,
|
| 32 |
+
isEditableModeEnabled = false,
|
| 33 |
+
selectedElement,
|
| 34 |
+
setSelectedElement,
|
| 35 |
+
setIsEditableModeEnabled,
|
| 36 |
+
onNewPrompt,
|
| 37 |
+
onSuccess,
|
| 38 |
}: {
|
| 39 |
+
html: string;
|
| 40 |
+
setHtml: (html: string) => void;
|
| 41 |
+
onScrollToBottom: () => void;
|
| 42 |
+
isAiWorking: boolean;
|
| 43 |
+
onNewPrompt: (prompt: string) => void;
|
| 44 |
+
htmlHistory?: HtmlHistory[];
|
| 45 |
+
setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
|
| 46 |
+
onSuccess: (h: string, p: string, n?: number[][]) => void;
|
| 47 |
+
isEditableModeEnabled: boolean;
|
| 48 |
+
setIsEditableModeEnabled: React.Dispatch<React.SetStateAction<boolean>>;
|
| 49 |
+
selectedElement?: HTMLElement | null;
|
| 50 |
+
setSelectedElement: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
|
| 51 |
+
}) {
|
| 52 |
+
const refThink = useRef<HTMLDivElement | null>(null);
|
| 53 |
+
const audio = useRef<HTMLAudioElement | null>(null);
|
| 54 |
+
|
| 55 |
+
const [open, setOpen] = useState(false);
|
| 56 |
+
const [prompt, setPrompt] = useState("");
|
| 57 |
+
const [hasAsked, setHasAsked] = useState(false);
|
| 58 |
+
const [previousPrompt, setPreviousPrompt] = useState("");
|
| 59 |
+
const [provider, setProvider] = useLocalStorage("provider", "auto");
|
| 60 |
+
const [model, setModel] = useLocalStorage("model", MODELS[0].value);
|
| 61 |
const [openProvider, setOpenProvider] = useState(false);
|
| 62 |
const [providerError, setProviderError] = useState("");
|
| 63 |
+
const [openProModal, setOpenProModal] = useState(false);
|
| 64 |
+
const [think, setThink] = useState<string | undefined>(undefined);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
const [openThink, setOpenThink] = useState(false);
|
| 66 |
+
const [isThinking, setIsThinking] = useState(true);
|
| 67 |
+
const [controller, setController] = useState<AbortController | null>(null);
|
| 68 |
+
const [isFollowUp, setIsFollowUp] = useState(true);
|
| 69 |
|
| 70 |
+
const selectedModel = useMemo(() => {
|
| 71 |
+
return MODELS.find((m: { value: string }) => m.value === model);
|
| 72 |
+
}, [model]);
|
|
|
|
|
|
|
| 73 |
|
| 74 |
const callAi = async (redesignMarkdown?: string) => {
|
|
|
|
|
|
|
|
|
|
| 75 |
if (isAiWorking) return;
|
| 76 |
if (!redesignMarkdown && !prompt.trim()) return;
|
| 77 |
+
setisAiWorking(true);
|
| 78 |
+
setProviderError("");
|
| 79 |
+
setThink("");
|
| 80 |
+
setOpenThink(false);
|
| 81 |
+
setIsThinking(true);
|
| 82 |
|
| 83 |
+
let contentResponse = "";
|
| 84 |
+
let thinkResponse = "";
|
| 85 |
+
let lastRenderTime = 0;
|
| 86 |
|
| 87 |
+
const abortController = new AbortController();
|
| 88 |
+
setController(abortController);
|
| 89 |
+
try {
|
| 90 |
+
onNewPrompt(prompt);
|
| 91 |
+
if (isFollowUp && !redesignMarkdown && !isSameHtml) {
|
| 92 |
+
const selectedElementHtml = selectedElement
|
| 93 |
+
? selectedElement.outerHTML
|
| 94 |
+
: "";
|
| 95 |
+
const request = await fetch("/api/ask-ai", {
|
| 96 |
+
method: "PUT",
|
| 97 |
+
body: JSON.stringify({
|
| 98 |
+
prompt,
|
| 99 |
+
provider,
|
| 100 |
+
previousPrompt,
|
| 101 |
+
model,
|
| 102 |
+
html,
|
| 103 |
+
selectedElementHtml,
|
| 104 |
+
}),
|
| 105 |
+
headers: {
|
| 106 |
+
"Content-Type": "application/json",
|
| 107 |
+
"x-forwarded-for": window.location.hostname,
|
| 108 |
+
},
|
| 109 |
+
signal: abortController.signal,
|
| 110 |
+
});
|
| 111 |
+
if (request && request.body) {
|
| 112 |
+
const res = await request.json();
|
| 113 |
+
if (!request.ok) {
|
| 114 |
+
if (res.openLogin) {
|
| 115 |
+
setOpen(true);
|
| 116 |
+
} else if (res.openSelectProvider) {
|
| 117 |
+
setOpenProvider(true);
|
| 118 |
+
setProviderError(res.message);
|
| 119 |
+
} else if (res.openProModal) {
|
| 120 |
+
setOpenProModal(true);
|
| 121 |
+
} else {
|
| 122 |
+
toast.error(res.message);
|
| 123 |
+
}
|
| 124 |
+
setisAiWorking(false);
|
| 125 |
+
return;
|
| 126 |
+
}
|
| 127 |
+
setHtml(res.html);
|
| 128 |
+
toast.success("AI responded successfully");
|
| 129 |
+
setPreviousPrompt(prompt);
|
| 130 |
+
setPrompt("");
|
| 131 |
+
setisAiWorking(false);
|
| 132 |
+
onSuccess(res.html, prompt, res.updatedLines);
|
| 133 |
+
if (audio.current) audio.current.play();
|
| 134 |
+
}
|
| 135 |
+
} else {
|
| 136 |
+
const request = await fetch("/api/ask-ai", {
|
| 137 |
+
method: "POST",
|
| 138 |
+
body: JSON.stringify({
|
| 139 |
+
prompt,
|
| 140 |
+
provider,
|
| 141 |
+
model,
|
| 142 |
+
html: isSameHtml ? "" : html,
|
| 143 |
+
redesignMarkdown,
|
| 144 |
+
}),
|
| 145 |
+
headers: {
|
| 146 |
+
"Content-Type": "application/json",
|
| 147 |
+
"x-forwarded-for": window.location.hostname,
|
| 148 |
+
},
|
| 149 |
+
signal: abortController.signal,
|
| 150 |
+
});
|
| 151 |
+
if (request && request.body) {
|
| 152 |
+
const reader = request.body.getReader();
|
| 153 |
+
const decoder = new TextDecoder("utf-8");
|
| 154 |
+
const selectedModel = MODELS.find(
|
| 155 |
+
(m: { value: string }) => m.value === model
|
| 156 |
+
);
|
| 157 |
+
let contentThink: string | undefined = undefined;
|
| 158 |
+
const read = async () => {
|
| 159 |
+
const { done, value } = await reader.read();
|
| 160 |
+
if (done) {
|
| 161 |
+
const isJson =
|
| 162 |
+
contentResponse.trim().startsWith("{") &&
|
| 163 |
+
contentResponse.trim().endsWith("}");
|
| 164 |
+
const jsonResponse = isJson ? JSON.parse(contentResponse) : null;
|
| 165 |
+
if (jsonResponse && !jsonResponse.ok) {
|
| 166 |
+
if (jsonResponse.openLogin) {
|
| 167 |
+
setOpen(true);
|
| 168 |
+
} else if (jsonResponse.openSelectProvider) {
|
| 169 |
+
setOpenProvider(true);
|
| 170 |
+
setProviderError(jsonResponse.message);
|
| 171 |
+
} else if (jsonResponse.openProModal) {
|
| 172 |
+
setOpenProModal(true);
|
| 173 |
+
} else {
|
| 174 |
+
toast.error(jsonResponse.message);
|
| 175 |
+
}
|
| 176 |
+
setisAiWorking(false);
|
| 177 |
+
return;
|
| 178 |
+
}
|
| 179 |
|
| 180 |
+
toast.success("AI responded successfully");
|
| 181 |
+
setPreviousPrompt(prompt);
|
| 182 |
+
setPrompt("");
|
| 183 |
+
setisAiWorking(false);
|
| 184 |
+
setHasAsked(true);
|
| 185 |
+
if (selectedModel?.isThinker) {
|
| 186 |
+
setModel(MODELS[0].value);
|
| 187 |
+
}
|
| 188 |
+
if (audio.current) audio.current.play();
|
|
|
|
|
|
|
| 189 |
|
| 190 |
+
// Now we have the complete HTML including </html>, so set it to be sure
|
| 191 |
+
const finalDoc = contentResponse.match(
|
| 192 |
+
/<!DOCTYPE html>[\s\S]*<\/html>/
|
| 193 |
+
)?.[0];
|
| 194 |
+
if (finalDoc) {
|
| 195 |
+
setHtml(finalDoc);
|
| 196 |
+
}
|
| 197 |
+
onSuccess(finalDoc ?? contentResponse, prompt);
|
| 198 |
+
|
| 199 |
+
return;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
const chunk = decoder.decode(value, { stream: true });
|
| 203 |
+
thinkResponse += chunk;
|
| 204 |
+
if (selectedModel?.isThinker) {
|
| 205 |
+
const thinkMatch = thinkResponse.match(/<think>[\s\S]*/)?.[0];
|
| 206 |
+
if (thinkMatch && !thinkResponse?.includes("</think>")) {
|
| 207 |
+
if ((contentThink?.length ?? 0) < 3) {
|
| 208 |
+
setOpenThink(true);
|
| 209 |
+
}
|
| 210 |
+
setThink(thinkMatch.replace("<think>", "").trim());
|
| 211 |
+
contentThink += chunk;
|
| 212 |
+
return read();
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
contentResponse += chunk;
|
| 217 |
+
|
| 218 |
+
const newHtml = contentResponse.match(
|
| 219 |
+
/<!DOCTYPE html>[\s\S]*/
|
| 220 |
+
)?.[0];
|
| 221 |
+
if (newHtml) {
|
| 222 |
+
setIsThinking(false);
|
| 223 |
+
let partialDoc = newHtml;
|
| 224 |
+
if (
|
| 225 |
+
partialDoc.includes("<head>") &&
|
| 226 |
+
!partialDoc.includes("</head>")
|
| 227 |
+
) {
|
| 228 |
+
partialDoc += "\n</head>";
|
| 229 |
+
}
|
| 230 |
+
if (
|
| 231 |
+
partialDoc.includes("<body") &&
|
| 232 |
+
!partialDoc.includes("</body>")
|
| 233 |
+
) {
|
| 234 |
+
partialDoc += "\n</body>";
|
| 235 |
+
}
|
| 236 |
+
if (!partialDoc.includes("</html>")) {
|
| 237 |
+
partialDoc += "\n</html>";
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
// Throttle the re-renders to avoid flashing/flicker
|
| 241 |
+
const now = Date.now();
|
| 242 |
+
if (now - lastRenderTime > 300) {
|
| 243 |
+
setHtml(partialDoc);
|
| 244 |
+
lastRenderTime = now;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
if (partialDoc.length > 200) {
|
| 248 |
+
onScrollToBottom();
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
read();
|
| 252 |
+
};
|
| 253 |
|
| 254 |
+
read();
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
} catch (error: any) {
|
| 258 |
+
setisAiWorking(false);
|
| 259 |
+
toast.error(error.message);
|
| 260 |
+
if (error.openLogin) {
|
| 261 |
+
setOpen(true);
|
| 262 |
}
|
| 263 |
}
|
| 264 |
};
|
| 265 |
|
| 266 |
+
const stopController = () => {
|
| 267 |
+
if (controller) {
|
| 268 |
+
controller.abort();
|
| 269 |
+
setController(null);
|
| 270 |
+
setisAiWorking(false);
|
| 271 |
+
setThink("");
|
| 272 |
+
setOpenThink(false);
|
| 273 |
+
setIsThinking(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
}
|
| 275 |
};
|
| 276 |
|
|
|
|
| 280 |
}
|
| 281 |
}, [think]);
|
| 282 |
|
| 283 |
+
useUpdateEffect(() => {
|
| 284 |
+
if (!isThinking) {
|
| 285 |
+
setOpenThink(false);
|
| 286 |
+
}
|
| 287 |
+
}, [isThinking]);
|
| 288 |
+
|
| 289 |
+
const isSameHtml = useMemo(() => {
|
| 290 |
+
return isTheSameHtml(html);
|
| 291 |
+
}, [html]);
|
| 292 |
|
| 293 |
return (
|
| 294 |
+
<div className="px-3">
|
| 295 |
+
<div className="relative bg-neutral-800 border border-neutral-700 rounded-2xl ring-[4px] focus-within:ring-neutral-500/30 focus-within:border-neutral-600 ring-transparent z-10 w-full group">
|
| 296 |
{think && (
|
| 297 |
<div className="w-full border-b border-neutral-700 relative overflow-hidden">
|
| 298 |
<header
|
|
|
|
| 330 |
</main>
|
| 331 |
</div>
|
| 332 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
{selectedElement && (
|
| 334 |
<div className="px-4 pt-3">
|
| 335 |
<SelectedHtmlElement
|
|
|
|
| 340 |
</div>
|
| 341 |
)}
|
| 342 |
<div className="w-full relative flex items-center justify-between">
|
| 343 |
+
{isAiWorking && (
|
| 344 |
+
<div className="absolute bg-neutral-800 rounded-lg bottom-0 left-4 w-[calc(100%-30px)] h-full z-1 flex items-center justify-between max-lg:text-sm">
|
| 345 |
+
<div className="flex items-center justify-start gap-2">
|
| 346 |
+
<Loading overlay={false} className="!size-4" />
|
| 347 |
+
<p className="text-neutral-400 text-sm">
|
| 348 |
+
AI is {isThinking ? "thinking" : "coding"}...{" "}
|
| 349 |
+
</p>
|
| 350 |
+
</div>
|
| 351 |
+
<div
|
| 352 |
+
className="text-xs text-neutral-400 px-1 py-0.5 rounded-md border border-neutral-600 flex items-center justify-center gap-1.5 bg-neutral-800 hover:brightness-110 transition-all duration-200 cursor-pointer"
|
| 353 |
+
onClick={stopController}
|
| 354 |
+
>
|
| 355 |
+
<FaStopCircle />
|
| 356 |
+
Stop generation
|
| 357 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
</div>
|
| 359 |
)}
|
| 360 |
+
<input
|
| 361 |
+
type="text"
|
| 362 |
+
disabled={isAiWorking}
|
|
|
|
| 363 |
className={classNames(
|
| 364 |
+
"w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4",
|
| 365 |
{
|
| 366 |
+
"!pt-2.5": selectedElement && !isAiWorking,
|
|
|
|
|
|
|
| 367 |
}
|
| 368 |
)}
|
| 369 |
placeholder={
|
| 370 |
selectedElement
|
| 371 |
? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
|
| 372 |
+
: hasAsked
|
| 373 |
? "Ask DeepSite for edits"
|
| 374 |
: "Ask DeepSite anything..."
|
| 375 |
}
|
|
|
|
| 381 |
}
|
| 382 |
}}
|
| 383 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
</div>
|
| 385 |
+
<div className="flex items-center justify-between gap-2 px-4 pb-3">
|
| 386 |
+
<div className="flex-1 flex items-center justify-start gap-1.5">
|
| 387 |
+
<ReImagine onRedesign={(md) => callAi(md)} />
|
| 388 |
+
{!isSameHtml && (
|
| 389 |
+
<Tooltip>
|
| 390 |
+
<TooltipTrigger asChild>
|
| 391 |
+
<Button
|
| 392 |
+
size="xs"
|
| 393 |
+
variant={isEditableModeEnabled ? "default" : "outline"}
|
| 394 |
+
onClick={() => {
|
| 395 |
+
setIsEditableModeEnabled?.(!isEditableModeEnabled);
|
| 396 |
+
}}
|
| 397 |
+
className={classNames("h-[28px]", {
|
| 398 |
+
"!text-neutral-400 hover:!text-neutral-200 !border-neutral-600 !hover:!border-neutral-500":
|
| 399 |
+
!isEditableModeEnabled,
|
| 400 |
+
})}
|
| 401 |
+
>
|
| 402 |
+
<Crosshair className="size-4" />
|
| 403 |
+
Edit
|
| 404 |
+
</Button>
|
| 405 |
+
</TooltipTrigger>
|
| 406 |
+
<TooltipContent
|
| 407 |
+
align="start"
|
| 408 |
+
className="bg-neutral-950 text-xs text-neutral-200 py-1 px-2 rounded-md -translate-y-0.5"
|
| 409 |
+
>
|
| 410 |
+
Select an element on the page to ask DeepSite edit it
|
| 411 |
+
directly.
|
| 412 |
+
</TooltipContent>
|
| 413 |
+
</Tooltip>
|
| 414 |
)}
|
| 415 |
+
<InviteFriends />
|
| 416 |
+
</div>
|
| 417 |
+
<div className="flex items-center justify-end gap-2">
|
| 418 |
<Settings
|
| 419 |
+
provider={provider as string}
|
| 420 |
+
model={model as string}
|
| 421 |
+
onChange={setProvider}
|
| 422 |
+
onModelChange={setModel}
|
| 423 |
open={openProvider}
|
| 424 |
error={providerError}
|
| 425 |
isFollowUp={!isSameHtml && isFollowUp}
|
| 426 |
onClose={setOpenProvider}
|
| 427 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
<Button
|
| 429 |
size="iconXs"
|
| 430 |
+
disabled={isAiWorking || !prompt.trim()}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
onClick={() => callAi()}
|
| 432 |
>
|
| 433 |
<ArrowUp className="size-4" />
|
| 434 |
</Button>
|
| 435 |
</div>
|
| 436 |
</div>
|
| 437 |
+
<LoginModal open={open} onClose={() => setOpen(false)} html={html} />
|
| 438 |
+
<ProModal
|
| 439 |
+
html={html}
|
| 440 |
+
open={openProModal}
|
| 441 |
+
onClose={() => setOpenProModal(false)}
|
| 442 |
+
/>
|
| 443 |
+
{!isSameHtml && (
|
| 444 |
+
<div className="absolute top-0 right-0 -translate-y-[calc(100%+8px)] select-none text-xs text-neutral-400 flex items-center justify-center gap-2 bg-neutral-800 border border-neutral-700 rounded-md p-1 pr-2.5">
|
| 445 |
+
<label
|
| 446 |
+
htmlFor="diff-patch-checkbox"
|
| 447 |
+
className="flex items-center gap-1.5 cursor-pointer"
|
| 448 |
+
>
|
| 449 |
+
<Checkbox
|
| 450 |
+
id="diff-patch-checkbox"
|
| 451 |
+
checked={isFollowUp}
|
| 452 |
+
onCheckedChange={(e) => {
|
| 453 |
+
if (e === true && !isSameHtml && selectedModel?.isThinker) {
|
| 454 |
+
setModel(MODELS[0].value);
|
| 455 |
+
}
|
| 456 |
+
setIsFollowUp(e === true);
|
| 457 |
+
}}
|
| 458 |
+
/>
|
| 459 |
+
Diff-Patch Update
|
| 460 |
+
</label>
|
| 461 |
+
<FollowUpTooltip />
|
| 462 |
+
</div>
|
| 463 |
+
)}
|
| 464 |
</div>
|
| 465 |
+
<audio ref={audio} id="audio" className="hidden">
|
| 466 |
+
<source src="/success.mp3" type="audio/mpeg" />
|
| 467 |
Your browser does not support the audio element.
|
| 468 |
</audio>
|
| 469 |
</div>
|
| 470 |
);
|
| 471 |
+
}
|
components/editor/ask-ai/loading.tsx
DELETED
|
@@ -1,68 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
import Loading from "@/components/loading";
|
| 3 |
-
import { useState, useEffect } from "react";
|
| 4 |
-
import { useInterval } from "react-use";
|
| 5 |
-
|
| 6 |
-
const TEXTS = [
|
| 7 |
-
"Teaching pixels to dance with style...",
|
| 8 |
-
"AI is having a creative breakthrough...",
|
| 9 |
-
"Channeling digital vibes into pure code...",
|
| 10 |
-
"Summoning the website spirits...",
|
| 11 |
-
"Brewing some algorithmic magic...",
|
| 12 |
-
"Composing a symphony of divs and spans...",
|
| 13 |
-
"Riding the wave of computational creativity...",
|
| 14 |
-
"Aligning the stars for perfect design...",
|
| 15 |
-
"Training circus animals to write CSS...",
|
| 16 |
-
"Launching ideas into the digital stratosphere...",
|
| 17 |
-
];
|
| 18 |
-
|
| 19 |
-
export const AiLoading = ({
|
| 20 |
-
text,
|
| 21 |
-
className,
|
| 22 |
-
}: {
|
| 23 |
-
text?: string;
|
| 24 |
-
className?: string;
|
| 25 |
-
}) => {
|
| 26 |
-
const [selectedText, setSelectedText] = useState(
|
| 27 |
-
text ?? TEXTS[0] // Start with first text to avoid hydration issues
|
| 28 |
-
);
|
| 29 |
-
|
| 30 |
-
// Set random text on client-side only to avoid hydration mismatch
|
| 31 |
-
useEffect(() => {
|
| 32 |
-
if (!text) {
|
| 33 |
-
setSelectedText(TEXTS[Math.floor(Math.random() * TEXTS.length)]);
|
| 34 |
-
}
|
| 35 |
-
}, [text]);
|
| 36 |
-
|
| 37 |
-
useInterval(() => {
|
| 38 |
-
if (!text) {
|
| 39 |
-
if (selectedText === TEXTS[TEXTS.length - 1]) {
|
| 40 |
-
setSelectedText(TEXTS[0]);
|
| 41 |
-
} else {
|
| 42 |
-
setSelectedText(TEXTS[TEXTS.indexOf(selectedText) + 1]);
|
| 43 |
-
}
|
| 44 |
-
}
|
| 45 |
-
}, 12000);
|
| 46 |
-
return (
|
| 47 |
-
<div className={`flex items-center justify-start gap-2 ${className}`}>
|
| 48 |
-
<Loading overlay={false} className="!size-5 opacity-50" />
|
| 49 |
-
<p className="text-neutral-400 text-sm">
|
| 50 |
-
<span className="inline-flex">
|
| 51 |
-
{selectedText.split("").map((char, index) => (
|
| 52 |
-
<span
|
| 53 |
-
key={index}
|
| 54 |
-
className="bg-gradient-to-r from-neutral-100 to-neutral-300 bg-clip-text text-transparent animate-pulse"
|
| 55 |
-
style={{
|
| 56 |
-
animationDelay: `${index * 0.1}s`,
|
| 57 |
-
animationDuration: "1.3s",
|
| 58 |
-
animationIterationCount: "infinite",
|
| 59 |
-
}}
|
| 60 |
-
>
|
| 61 |
-
{char === " " ? "\u00A0" : char}
|
| 62 |
-
</span>
|
| 63 |
-
))}
|
| 64 |
-
</span>
|
| 65 |
-
</p>
|
| 66 |
-
</div>
|
| 67 |
-
);
|
| 68 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/prompt-builder/content-modal.tsx
DELETED
|
@@ -1,196 +0,0 @@
|
|
| 1 |
-
import classNames from "classnames";
|
| 2 |
-
import { ChevronRight, RefreshCcw } from "lucide-react";
|
| 3 |
-
import { useState } from "react";
|
| 4 |
-
import { TailwindColors } from "./tailwind-colors";
|
| 5 |
-
import { Switch } from "@/components/ui/switch";
|
| 6 |
-
import { Button } from "@/components/ui/button";
|
| 7 |
-
import { Themes } from "./themes";
|
| 8 |
-
import { EnhancedSettings } from "@/types";
|
| 9 |
-
|
| 10 |
-
export const ContentModal = ({
|
| 11 |
-
enhancedSettings,
|
| 12 |
-
setEnhancedSettings,
|
| 13 |
-
}: {
|
| 14 |
-
enhancedSettings: EnhancedSettings;
|
| 15 |
-
setEnhancedSettings: (settings: EnhancedSettings) => void;
|
| 16 |
-
}) => {
|
| 17 |
-
const [collapsed, setCollapsed] = useState(["colors", "theme"]);
|
| 18 |
-
return (
|
| 19 |
-
<main className="overflow-x-hidden max-h-[50dvh] overflow-y-auto">
|
| 20 |
-
<section className="w-full border-b border-neutral-800/80 px-6 py-3.5 sticky top-0 bg-neutral-900 z-10">
|
| 21 |
-
<div className="flex items-center justify-between gap-3">
|
| 22 |
-
<p className="text-base font-semibold text-neutral-200">
|
| 23 |
-
Allow DeepSite to enhance your prompt
|
| 24 |
-
</p>
|
| 25 |
-
<Switch
|
| 26 |
-
checked={enhancedSettings.isActive}
|
| 27 |
-
onCheckedChange={() =>
|
| 28 |
-
setEnhancedSettings({
|
| 29 |
-
...enhancedSettings,
|
| 30 |
-
isActive: !enhancedSettings.isActive,
|
| 31 |
-
})
|
| 32 |
-
}
|
| 33 |
-
/>
|
| 34 |
-
</div>
|
| 35 |
-
<p className="text-sm text-neutral-500 mt-2">
|
| 36 |
-
While using DeepSite enhanced prompt, you'll get better results. We'll
|
| 37 |
-
add more details and features to your request.
|
| 38 |
-
</p>
|
| 39 |
-
<div className="text-sm text-sky-500 mt-3 bg-gradient-to-r from-sky-400/15 to-purple-400/15 rounded-md px-3 py-2 border border-white/10">
|
| 40 |
-
<p className="text-transparent bg-gradient-to-r from-sky-400 to-purple-400 bg-clip-text">
|
| 41 |
-
You can also use the custom properties below to set specific
|
| 42 |
-
information.
|
| 43 |
-
</p>
|
| 44 |
-
</div>
|
| 45 |
-
</section>
|
| 46 |
-
<section className="py-3.5 border-b border-neutral-800/80">
|
| 47 |
-
<div
|
| 48 |
-
className={classNames(
|
| 49 |
-
"flex items-center justify-start gap-3 px-4 cursor-pointer text-neutral-400 hover:text-neutral-200",
|
| 50 |
-
{
|
| 51 |
-
"!text-neutral-200": collapsed.includes("colors"),
|
| 52 |
-
}
|
| 53 |
-
)}
|
| 54 |
-
onClick={() =>
|
| 55 |
-
setCollapsed((prev) => {
|
| 56 |
-
if (prev.includes("colors")) {
|
| 57 |
-
return prev.filter((item) => item !== "colors");
|
| 58 |
-
}
|
| 59 |
-
return [...prev, "colors"];
|
| 60 |
-
})
|
| 61 |
-
}
|
| 62 |
-
>
|
| 63 |
-
<ChevronRight className="size-4" />
|
| 64 |
-
<p className="text-base font-semibold">Colors</p>
|
| 65 |
-
</div>
|
| 66 |
-
{collapsed.includes("colors") && (
|
| 67 |
-
<div className="mt-4 space-y-4">
|
| 68 |
-
<article className="w-full">
|
| 69 |
-
<div className="flex items-center justify-start gap-2 px-5">
|
| 70 |
-
<p className="text-xs font-medium uppercase text-neutral-400">
|
| 71 |
-
Primary Color
|
| 72 |
-
</p>
|
| 73 |
-
<Button
|
| 74 |
-
variant="bordered"
|
| 75 |
-
size="xss"
|
| 76 |
-
className={`${
|
| 77 |
-
enhancedSettings.primaryColor ? "" : "opacity-0"
|
| 78 |
-
}`}
|
| 79 |
-
onClick={() =>
|
| 80 |
-
setEnhancedSettings({
|
| 81 |
-
...enhancedSettings,
|
| 82 |
-
primaryColor: undefined,
|
| 83 |
-
})
|
| 84 |
-
}
|
| 85 |
-
>
|
| 86 |
-
<RefreshCcw className="size-2.5" />
|
| 87 |
-
Reset
|
| 88 |
-
</Button>
|
| 89 |
-
</div>
|
| 90 |
-
<div className="text-muted-foreground text-sm mt-4">
|
| 91 |
-
<TailwindColors
|
| 92 |
-
value={enhancedSettings.primaryColor}
|
| 93 |
-
onChange={(value) =>
|
| 94 |
-
setEnhancedSettings({
|
| 95 |
-
...enhancedSettings,
|
| 96 |
-
primaryColor: value,
|
| 97 |
-
})
|
| 98 |
-
}
|
| 99 |
-
/>
|
| 100 |
-
</div>
|
| 101 |
-
</article>
|
| 102 |
-
<article className="w-full">
|
| 103 |
-
<div className="flex items-center justify-start gap-2 px-5">
|
| 104 |
-
<p className="text-xs font-medium uppercase text-neutral-400">
|
| 105 |
-
Secondary Color
|
| 106 |
-
</p>
|
| 107 |
-
<Button
|
| 108 |
-
variant="bordered"
|
| 109 |
-
size="xss"
|
| 110 |
-
className={`${
|
| 111 |
-
enhancedSettings.secondaryColor ? "" : "opacity-0"
|
| 112 |
-
}`}
|
| 113 |
-
onClick={() =>
|
| 114 |
-
setEnhancedSettings({
|
| 115 |
-
...enhancedSettings,
|
| 116 |
-
secondaryColor: undefined,
|
| 117 |
-
})
|
| 118 |
-
}
|
| 119 |
-
>
|
| 120 |
-
<RefreshCcw className="size-2.5" />
|
| 121 |
-
Reset
|
| 122 |
-
</Button>
|
| 123 |
-
</div>
|
| 124 |
-
<div className="text-muted-foreground text-sm mt-4">
|
| 125 |
-
<TailwindColors
|
| 126 |
-
value={enhancedSettings.secondaryColor}
|
| 127 |
-
onChange={(value) =>
|
| 128 |
-
setEnhancedSettings({
|
| 129 |
-
...enhancedSettings,
|
| 130 |
-
secondaryColor: value,
|
| 131 |
-
})
|
| 132 |
-
}
|
| 133 |
-
/>
|
| 134 |
-
</div>
|
| 135 |
-
</article>
|
| 136 |
-
</div>
|
| 137 |
-
)}
|
| 138 |
-
</section>
|
| 139 |
-
<section className="py-3.5 border-b border-neutral-800/80">
|
| 140 |
-
<div
|
| 141 |
-
className={classNames(
|
| 142 |
-
"flex items-center justify-start gap-3 px-4 cursor-pointer text-neutral-400 hover:text-neutral-200",
|
| 143 |
-
{
|
| 144 |
-
"!text-neutral-200": collapsed.includes("theme"),
|
| 145 |
-
}
|
| 146 |
-
)}
|
| 147 |
-
onClick={() =>
|
| 148 |
-
setCollapsed((prev) => {
|
| 149 |
-
if (prev.includes("theme")) {
|
| 150 |
-
return prev.filter((item) => item !== "theme");
|
| 151 |
-
}
|
| 152 |
-
return [...prev, "theme"];
|
| 153 |
-
})
|
| 154 |
-
}
|
| 155 |
-
>
|
| 156 |
-
<ChevronRight className="size-4" />
|
| 157 |
-
<p className="text-base font-semibold">Theme</p>
|
| 158 |
-
</div>
|
| 159 |
-
{collapsed.includes("theme") && (
|
| 160 |
-
<article className="w-full mt-4">
|
| 161 |
-
<div className="flex items-center justify-start gap-2 px-5">
|
| 162 |
-
<p className="text-xs font-medium uppercase text-neutral-400">
|
| 163 |
-
Theme
|
| 164 |
-
</p>
|
| 165 |
-
<Button
|
| 166 |
-
variant="bordered"
|
| 167 |
-
size="xss"
|
| 168 |
-
className={`${enhancedSettings.theme ? "" : "opacity-0"}`}
|
| 169 |
-
onClick={() =>
|
| 170 |
-
setEnhancedSettings({
|
| 171 |
-
...enhancedSettings,
|
| 172 |
-
theme: undefined,
|
| 173 |
-
})
|
| 174 |
-
}
|
| 175 |
-
>
|
| 176 |
-
<RefreshCcw className="size-2.5" />
|
| 177 |
-
Reset
|
| 178 |
-
</Button>
|
| 179 |
-
</div>
|
| 180 |
-
<div className="text-muted-foreground text-sm mt-4">
|
| 181 |
-
<Themes
|
| 182 |
-
value={enhancedSettings.theme}
|
| 183 |
-
onChange={(value) =>
|
| 184 |
-
setEnhancedSettings({
|
| 185 |
-
...enhancedSettings,
|
| 186 |
-
theme: value,
|
| 187 |
-
})
|
| 188 |
-
}
|
| 189 |
-
/>
|
| 190 |
-
</div>
|
| 191 |
-
</article>
|
| 192 |
-
)}
|
| 193 |
-
</section>
|
| 194 |
-
</main>
|
| 195 |
-
);
|
| 196 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/prompt-builder/index.tsx
DELETED
|
@@ -1,68 +0,0 @@
|
|
| 1 |
-
import { useState } from "react";
|
| 2 |
-
import { WandSparkles } from "lucide-react";
|
| 3 |
-
|
| 4 |
-
import { Button } from "@/components/ui/button";
|
| 5 |
-
import { useEditor } from "@/hooks/useEditor";
|
| 6 |
-
import { useAi } from "@/hooks/useAi";
|
| 7 |
-
import {
|
| 8 |
-
Dialog,
|
| 9 |
-
DialogContent,
|
| 10 |
-
DialogFooter,
|
| 11 |
-
DialogTitle,
|
| 12 |
-
} from "@/components/ui/dialog";
|
| 13 |
-
import { ContentModal } from "./content-modal";
|
| 14 |
-
import { EnhancedSettings } from "@/types";
|
| 15 |
-
|
| 16 |
-
export const PromptBuilder = ({
|
| 17 |
-
enhancedSettings,
|
| 18 |
-
setEnhancedSettings,
|
| 19 |
-
}: {
|
| 20 |
-
enhancedSettings: EnhancedSettings;
|
| 21 |
-
setEnhancedSettings: (settings: EnhancedSettings) => void;
|
| 22 |
-
}) => {
|
| 23 |
-
const { globalAiLoading } = useAi();
|
| 24 |
-
const { globalEditorLoading } = useEditor();
|
| 25 |
-
|
| 26 |
-
const [open, setOpen] = useState(false);
|
| 27 |
-
return (
|
| 28 |
-
<>
|
| 29 |
-
<Button
|
| 30 |
-
size="xs"
|
| 31 |
-
variant="outline"
|
| 32 |
-
className="!rounded-md !border-white/10 !bg-gradient-to-r from-sky-400/15 to-purple-400/15 light-sweep hover:brightness-110"
|
| 33 |
-
disabled={globalAiLoading || globalEditorLoading}
|
| 34 |
-
onClick={() => {
|
| 35 |
-
setOpen(true);
|
| 36 |
-
}}
|
| 37 |
-
>
|
| 38 |
-
<WandSparkles className="size-3.5 text-sky-500 relative z-10" />
|
| 39 |
-
<span className="text-transparent bg-gradient-to-r from-sky-400 to-purple-400 bg-clip-text relative z-10">
|
| 40 |
-
Enhance
|
| 41 |
-
</span>
|
| 42 |
-
</Button>
|
| 43 |
-
<Dialog open={open} onOpenChange={() => setOpen(false)}>
|
| 44 |
-
<DialogContent className="sm:max-w-xl !p-0 !rounded-3xl !bg-neutral-900 !border-neutral-800/80 !gap-0">
|
| 45 |
-
<DialogTitle className="px-6 py-3.5 border-b border-neutral-800">
|
| 46 |
-
<div className="flex items-center justify-start gap-2 text-neutral-200 text-base font-medium">
|
| 47 |
-
<WandSparkles className="size-3.5" />
|
| 48 |
-
<p>Enhance Prompt</p>
|
| 49 |
-
</div>
|
| 50 |
-
</DialogTitle>
|
| 51 |
-
<ContentModal
|
| 52 |
-
enhancedSettings={enhancedSettings}
|
| 53 |
-
setEnhancedSettings={setEnhancedSettings}
|
| 54 |
-
/>
|
| 55 |
-
<DialogFooter className="px-6 py-3.5 border-t border-neutral-800">
|
| 56 |
-
<Button
|
| 57 |
-
variant="bordered"
|
| 58 |
-
size="default"
|
| 59 |
-
onClick={() => setOpen(false)}
|
| 60 |
-
>
|
| 61 |
-
Close
|
| 62 |
-
</Button>
|
| 63 |
-
</DialogFooter>
|
| 64 |
-
</DialogContent>
|
| 65 |
-
</Dialog>
|
| 66 |
-
</>
|
| 67 |
-
);
|
| 68 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/prompt-builder/tailwind-colors.tsx
DELETED
|
@@ -1,58 +0,0 @@
|
|
| 1 |
-
import classNames from "classnames";
|
| 2 |
-
import { useRef } from "react";
|
| 3 |
-
|
| 4 |
-
import { TAILWIND_COLORS } from "@/lib/prompt-builder";
|
| 5 |
-
import { useMount } from "react-use";
|
| 6 |
-
|
| 7 |
-
export const TailwindColors = ({
|
| 8 |
-
value,
|
| 9 |
-
onChange,
|
| 10 |
-
}: {
|
| 11 |
-
value: string | undefined;
|
| 12 |
-
onChange: (value: string) => void;
|
| 13 |
-
}) => {
|
| 14 |
-
const ref = useRef<HTMLDivElement>(null);
|
| 15 |
-
|
| 16 |
-
useMount(() => {
|
| 17 |
-
if (ref.current) {
|
| 18 |
-
if (value) {
|
| 19 |
-
const color = ref.current.querySelector(`[data-color="${value}"]`);
|
| 20 |
-
if (color) {
|
| 21 |
-
color.scrollIntoView({ inline: "center" });
|
| 22 |
-
}
|
| 23 |
-
}
|
| 24 |
-
}
|
| 25 |
-
});
|
| 26 |
-
return (
|
| 27 |
-
<div
|
| 28 |
-
ref={ref}
|
| 29 |
-
className="flex items-center justify-start gap-3 overflow-x-auto px-5 scrollbar-hide"
|
| 30 |
-
>
|
| 31 |
-
{TAILWIND_COLORS.map((color) => (
|
| 32 |
-
<div
|
| 33 |
-
key={color}
|
| 34 |
-
className={classNames(
|
| 35 |
-
"flex flex-col items-center justify-center p-3 size-16 min-w-16 gap-2 rounded-lg border border-neutral-800 bg-neutral-800/30 hover:brightness-120 cursor-pointer",
|
| 36 |
-
{
|
| 37 |
-
"!border-neutral-700 !bg-neutral-800/80 hover:!brightness-100":
|
| 38 |
-
value === color,
|
| 39 |
-
}
|
| 40 |
-
)}
|
| 41 |
-
data-color={color}
|
| 42 |
-
onClick={() => onChange(color)}
|
| 43 |
-
>
|
| 44 |
-
<div
|
| 45 |
-
className={`w-4 h-4 min-w-4 min-h-4 rounded-xl ${
|
| 46 |
-
["white", "black"].includes(color)
|
| 47 |
-
? `bg-${color}`
|
| 48 |
-
: `bg-${color}-500`
|
| 49 |
-
}`}
|
| 50 |
-
/>
|
| 51 |
-
<p className="text-xs capitalize text-neutral-200 truncate">
|
| 52 |
-
{color}
|
| 53 |
-
</p>
|
| 54 |
-
</div>
|
| 55 |
-
))}
|
| 56 |
-
</div>
|
| 57 |
-
);
|
| 58 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/prompt-builder/themes.tsx
DELETED
|
@@ -1,48 +0,0 @@
|
|
| 1 |
-
import { Theme } from "@/types";
|
| 2 |
-
import classNames from "classnames";
|
| 3 |
-
import { Moon, Sun } from "lucide-react";
|
| 4 |
-
import { useRef } from "react";
|
| 5 |
-
|
| 6 |
-
export const Themes = ({
|
| 7 |
-
value,
|
| 8 |
-
onChange,
|
| 9 |
-
}: {
|
| 10 |
-
value: Theme;
|
| 11 |
-
onChange: (value: Theme) => void;
|
| 12 |
-
}) => {
|
| 13 |
-
const ref = useRef<HTMLDivElement>(null);
|
| 14 |
-
|
| 15 |
-
return (
|
| 16 |
-
<div
|
| 17 |
-
ref={ref}
|
| 18 |
-
className="flex items-center justify-start gap-3 overflow-x-auto px-5 scrollbar-hide"
|
| 19 |
-
>
|
| 20 |
-
<div
|
| 21 |
-
className={classNames(
|
| 22 |
-
"flex flex-col items-center justify-center p-3 size-16 min-w-32 gap-2 rounded-lg border border-neutral-800 bg-neutral-800/30 hover:brightness-120 cursor-pointer",
|
| 23 |
-
{
|
| 24 |
-
"!border-neutral-700 !bg-neutral-800/80 hover:!brightness-100":
|
| 25 |
-
value === "light",
|
| 26 |
-
}
|
| 27 |
-
)}
|
| 28 |
-
onClick={() => onChange("light")}
|
| 29 |
-
>
|
| 30 |
-
<Sun className="size-4 text-amber-500" />
|
| 31 |
-
<p className="text-xs capitalize text-neutral-200 truncate">Light</p>
|
| 32 |
-
</div>
|
| 33 |
-
<div
|
| 34 |
-
className={classNames(
|
| 35 |
-
"flex flex-col items-center justify-center p-3 size-16 min-w-32 gap-2 rounded-lg border border-neutral-800 bg-neutral-800/30 hover:brightness-120 cursor-pointer",
|
| 36 |
-
{
|
| 37 |
-
"!border-neutral-700 !bg-neutral-800/80 hover:!brightness-100":
|
| 38 |
-
value === "dark",
|
| 39 |
-
}
|
| 40 |
-
)}
|
| 41 |
-
onClick={() => onChange("dark")}
|
| 42 |
-
>
|
| 43 |
-
<Moon className="size-4 text-indigo-500" />
|
| 44 |
-
<p className="text-xs capitalize text-neutral-200 truncate">Dark</p>
|
| 45 |
-
</div>
|
| 46 |
-
</div>
|
| 47 |
-
);
|
| 48 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/re-imagine.tsx
CHANGED
|
@@ -11,8 +11,6 @@ import {
|
|
| 11 |
import { Input } from "@/components/ui/input";
|
| 12 |
import Loading from "@/components/loading";
|
| 13 |
import { api } from "@/lib/api";
|
| 14 |
-
import { useAi } from "@/hooks/useAi";
|
| 15 |
-
import { useEditor } from "@/hooks/useEditor";
|
| 16 |
|
| 17 |
export function ReImagine({
|
| 18 |
onRedesign,
|
|
@@ -22,8 +20,6 @@ export function ReImagine({
|
|
| 22 |
const [url, setUrl] = useState<string>("");
|
| 23 |
const [open, setOpen] = useState(false);
|
| 24 |
const [isLoading, setIsLoading] = useState(false);
|
| 25 |
-
const { globalAiLoading } = useAi();
|
| 26 |
-
const { globalEditorLoading } = useEditor();
|
| 27 |
|
| 28 |
const checkIfUrlIsValid = (url: string) => {
|
| 29 |
const urlPattern = new RegExp(
|
|
@@ -63,13 +59,11 @@ export function ReImagine({
|
|
| 63 |
<form>
|
| 64 |
<PopoverTrigger asChild>
|
| 65 |
<Button
|
| 66 |
-
size="
|
| 67 |
-
variant=
|
| 68 |
-
className="!
|
| 69 |
-
disabled={globalAiLoading || globalEditorLoading}
|
| 70 |
>
|
| 71 |
-
<Paintbrush className="size-
|
| 72 |
-
Redesign
|
| 73 |
</Button>
|
| 74 |
</PopoverTrigger>
|
| 75 |
<PopoverContent
|
|
|
|
| 11 |
import { Input } from "@/components/ui/input";
|
| 12 |
import Loading from "@/components/loading";
|
| 13 |
import { api } from "@/lib/api";
|
|
|
|
|
|
|
| 14 |
|
| 15 |
export function ReImagine({
|
| 16 |
onRedesign,
|
|
|
|
| 20 |
const [url, setUrl] = useState<string>("");
|
| 21 |
const [open, setOpen] = useState(false);
|
| 22 |
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
|
|
|
| 23 |
|
| 24 |
const checkIfUrlIsValid = (url: string) => {
|
| 25 |
const urlPattern = new RegExp(
|
|
|
|
| 59 |
<form>
|
| 60 |
<PopoverTrigger asChild>
|
| 61 |
<Button
|
| 62 |
+
size="iconXs"
|
| 63 |
+
variant="outline"
|
| 64 |
+
className="!border-neutral-600 !text-neutral-400 !hover:!border-neutral-500 hover:!text-neutral-300"
|
|
|
|
| 65 |
>
|
| 66 |
+
<Paintbrush className="size-4" />
|
|
|
|
| 67 |
</Button>
|
| 68 |
</PopoverTrigger>
|
| 69 |
<PopoverContent
|