This view is limited to 50 files because it contains too many changes.  See the raw diff here.
Files changed (50) hide show
  1. MCP-SERVER.md +0 -428
  2. README.md +7 -13
  3. app/(public)/layout.tsx +1 -1
  4. app/(public)/page.tsx +43 -4
  5. app/(public)/projects/page.tsx +13 -0
  6. app/[namespace]/[repoId]/page.tsx +0 -28
  7. app/actions/auth.ts +1 -1
  8. app/actions/projects.ts +40 -24
  9. app/actions/rewrite-prompt.ts +35 -0
  10. app/api/{ask → ask-ai}/route.ts +272 -161
  11. app/api/auth/login-url/route.ts +0 -21
  12. app/api/auth/logout/route.ts +0 -25
  13. app/api/auth/route.ts +2 -2
  14. app/api/mcp/route.ts +0 -435
  15. app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts +0 -230
  16. app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/route.ts +0 -102
  17. app/api/me/projects/[namespace]/[repoId]/download/route.ts +0 -105
  18. app/api/me/projects/[namespace]/[repoId]/images/route.ts +27 -41
  19. app/api/me/projects/[namespace]/[repoId]/route.ts +181 -102
  20. app/api/me/projects/[namespace]/[repoId]/save/route.ts +0 -72
  21. app/api/me/projects/[namespace]/[repoId]/update/route.ts +0 -141
  22. app/api/me/projects/route.ts +95 -89
  23. app/api/me/route.ts +1 -22
  24. app/api/re-design/route.ts +16 -48
  25. app/auth/callback/page.tsx +42 -67
  26. app/layout.tsx +53 -86
  27. app/new/page.tsx +0 -14
  28. app/projects/[namespace]/[repoId]/page.tsx +42 -0
  29. app/projects/new/page.tsx +5 -0
  30. app/sitemap.ts +0 -28
  31. assets/deepseek.svg +0 -1
  32. assets/globals.css +1 -235
  33. assets/kimi.svg +0 -1
  34. assets/minimax.svg +0 -1
  35. assets/qwen.svg +0 -1
  36. assets/zai.svg +0 -13
  37. components.json +1 -1
  38. components/animated-blobs/index.tsx +0 -34
  39. components/animated-text/index.tsx +0 -123
  40. components/contexts/app-context.tsx +31 -26
  41. components/contexts/login-context.tsx +0 -62
  42. components/contexts/pro-context.tsx +0 -48
  43. components/discord-promo-modal/index.tsx +0 -225
  44. components/editor/ask-ai/context.tsx +0 -128
  45. components/editor/ask-ai/fake-ask.tsx +0 -97
  46. components/editor/ask-ai/follow-up-tooltip.tsx +36 -0
  47. components/editor/ask-ai/index.tsx +324 -177
  48. components/editor/ask-ai/loading.tsx +0 -68
  49. components/editor/ask-ai/prompt-builder/content-modal.tsx +0 -196
  50. components/editor/ask-ai/prompt-builder/index.tsx +0 -68
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 v3
3
  emoji: 🐳
4
  colorFrom: blue
5
  colorTo: blue
@@ -7,22 +7,16 @@ sdk: docker
7
  pinned: true
8
  app_port: 3000
9
  license: mit
10
- failure_strategy: rollback
11
- short_description: Generate any application by Vibe Coding
12
  models:
13
  - deepseek-ai/DeepSeek-V3-0324
14
  - deepseek-ai/DeepSeek-R1-0528
15
- - deepseek-ai/DeepSeek-V3.1
16
- - deepseek-ai/DeepSeek-V3.1-Terminus
17
- - deepseek-ai/DeepSeek-V3.2-Exp
18
- - Qwen/Qwen3-Coder-480B-A35B-Instruct
19
- - moonshotai/Kimi-K2-Instruct
20
- - moonshotai/Kimi-K2-Instruct-0905
21
- - zai-org/GLM-4.6
22
- - MiniMaxAI/MiniMax-M2
23
- - moonshotai/Kimi-K2-Thinking
24
  ---
25
 
26
  # DeepSite 🐳
27
 
28
- DeepSite is a Vibe Coding Platform 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.
 
 
 
 
 
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
  ---
15
 
16
  # DeepSite 🐳
17
 
18
+ 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.
19
+
20
+ ## How to use it locally
21
+
22
+ 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-neutral-950 z-1 relative overflow-auto scroll-smooth">
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 { MyProjects } from "@/components/my-projects";
2
-
3
- export default async function HomePage() {
4
- return <MyProjects />;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- "/deepsite/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;
 
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 { listSpaces } from "@huggingface/hub";
6
- import { ProjectType } from "@/types";
 
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
- const projects = [];
23
- for await (const space of listSpaces({
24
- accessToken: user.token as string,
25
- additionalFields: ["author", "cardData"],
26
- search: {
27
- owner: user.name,
28
- }
29
- })) {
30
- if (
31
- !space.private &&
32
- space.sdk === "static" &&
33
- Array.isArray((space.cardData as { tags?: string[] })?.tags) &&
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/actions/rewrite-prompt.ts ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { InferenceClient } from "@huggingface/inference";
2
+
3
+ const START_REWRITE_PROMPT = ">>>>>>> START PROMPT >>>>>>";
4
+ const END_REWRITE_PROMPT = ">>>>>>> END PROMPT >>>>>>";
5
+
6
+ export const callAiRewritePrompt = async (prompt: string, { token, billTo }: { token: string, billTo?: string | null }) => {
7
+ const client = new InferenceClient(token);
8
+ const response = await client.chatCompletion(
9
+ {
10
+ model: "deepseek-ai/DeepSeek-V3.1",
11
+ provider: "novita",
12
+ messages: [{
13
+ role: "system",
14
+ content: `You are a helpful assistant that rewrites prompts to make them better. All the prompts will be about creating a website or app.
15
+ Try to make the prompt more detailed and specific to create a good UI/UX Design and good code.
16
+ Format the result by following this format:
17
+ ${START_REWRITE_PROMPT}
18
+ new prompt here
19
+ ${END_REWRITE_PROMPT}
20
+ If you don't rewrite the prompt, return the original prompt.
21
+ Make sure to return the prompt in the same language as the prompt you are given. Also IMPORTANT: Make sure to keep the original intent of the prompt. Improve it it needed, but don't change the original intent.
22
+ `
23
+ },{ role: "user", content: prompt }],
24
+ },
25
+ billTo ? { billTo } : {}
26
+ );
27
+
28
+ const responseContent = response.choices[0]?.message?.content;
29
+ if (!responseContent) {
30
+ return prompt;
31
+ }
32
+ const startIndex = responseContent.indexOf(START_REWRITE_PROMPT);
33
+ const endIndex = responseContent.indexOf(END_REWRITE_PROMPT);
34
+ return responseContent.substring(startIndex + START_REWRITE_PROMPT.length, endIndex);
35
+ };
app/api/{ask → ask-ai}/route.ts RENAMED
@@ -4,29 +4,30 @@ 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
- FOLLOW_UP_SYSTEM_PROMPT_LIGHT,
11
  INITIAL_SYSTEM_PROMPT,
12
- INITIAL_SYSTEM_PROMPT_LIGHT,
13
  MAX_REQUESTS_PER_IP,
14
- PROMPT_FOR_PROJECT_NAME,
 
 
 
 
 
15
  } from "@/lib/prompts";
16
  import MY_TOKEN_KEY from "@/lib/get-cookie-name";
17
  import { Page } from "@/types";
18
- import { isAuthenticated } from "@/lib/auth";
19
- import { getBestProvider } from "@/lib/best-provider";
20
 
21
  const ipAddresses = new Map();
22
 
23
  export async function POST(request: NextRequest) {
24
  const authHeaders = await headers();
25
- const tokenInHeaders = authHeaders.get("Authorization");
26
- const userToken = tokenInHeaders ? tokenInHeaders.replace("Bearer ", "") : request.cookies.get(MY_TOKEN_KEY())?.value;
27
 
28
  const body = await request.json();
29
- const { prompt, provider, model, redesignMarkdown, enhancedSettings, pages } = body;
30
 
31
  if (!model || (!prompt && !redesignMarkdown)) {
32
  return NextResponse.json(
@@ -46,8 +47,18 @@ export async function POST(request: NextRequest) {
46
  );
47
  }
48
 
49
- let token: string | null = null;
50
- if (userToken) token = userToken;
 
 
 
 
 
 
 
 
 
 
51
  let billTo: string | null = null;
52
 
53
  /**
@@ -63,7 +74,7 @@ export async function POST(request: NextRequest) {
63
  ? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
64
  : authHeaders.get("x-forwarded-for");
65
 
66
- if (!token || token === "null" || token === "") {
67
  ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
68
  if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
69
  return NextResponse.json(
@@ -80,6 +91,19 @@ export async function POST(request: NextRequest) {
80
  billTo = "huggingface";
81
  }
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  try {
84
  const encoder = new TextEncoder();
85
  const stream = new TransformStream();
@@ -94,43 +118,36 @@ export async function POST(request: NextRequest) {
94
  });
95
 
96
  (async () => {
 
97
  try {
98
  const client = new InferenceClient(token);
99
-
100
- const systemPrompt = selectedModel.value.includes('MiniMax')
101
- ? INITIAL_SYSTEM_PROMPT_LIGHT
102
- : INITIAL_SYSTEM_PROMPT;
103
-
104
- const userPrompt = prompt;
105
-
106
  const chatCompletion = client.chatCompletionStream(
107
  {
108
- model: selectedModel.value + (provider !== "auto" ? `:${provider}` : ""),
 
109
  messages: [
110
  {
111
  role: "system",
112
- content: systemPrompt,
113
  },
114
- ...(redesignMarkdown ? [{
115
  role: "assistant",
116
- content: `User will ask you to redesign the site based on this markdown. Use the same images as the site, but you can improve the content and the design. Here is the markdown: ${redesignMarkdown}`
117
  }] : []),
118
  {
119
  role: "user",
120
- content: userPrompt + (enhancedSettings.isActive ? `1. I want to use the following primary color: ${enhancedSettings.primaryColor} (eg: bg-${enhancedSettings.primaryColor}-500).
121
- 2. I want to use the following secondary color: ${enhancedSettings.secondaryColor} (eg: bg-${enhancedSettings.secondaryColor}-500).
122
- 3. I want to use the following theme: ${enhancedSettings.theme} mode.` : "")
123
  },
124
  ],
125
- ...(selectedModel.top_k ? { top_k: selectedModel.top_k } : {}),
126
- ...(selectedModel.temperature ? { temperature: selectedModel.temperature } : {}),
127
- ...(selectedModel.top_p ? { top_p: selectedModel.top_p } : {}),
128
  },
129
  billTo ? { billTo } : {}
130
  );
131
 
132
  while (true) {
133
- const { done, value } = await chatCompletion.next()
134
  if (done) {
135
  break;
136
  }
@@ -140,8 +157,6 @@ export async function POST(request: NextRequest) {
140
  await writer.write(encoder.encode(chunk));
141
  }
142
  }
143
-
144
- await writer.close();
145
  } catch (error: any) {
146
  if (error.message?.includes("exceeded your monthly included credits")) {
147
  await writer.write(
@@ -153,18 +168,7 @@ export async function POST(request: NextRequest) {
153
  })
154
  )
155
  );
156
- } else if (error?.message?.includes("inference provider information")) {
157
- await writer.write(
158
- encoder.encode(
159
- JSON.stringify({
160
- ok: false,
161
- openSelectProvider: true,
162
- message: error.message,
163
- })
164
- )
165
- );
166
- }
167
- else {
168
  await writer.write(
169
  encoder.encode(
170
  JSON.stringify({
@@ -177,10 +181,7 @@ export async function POST(request: NextRequest) {
177
  );
178
  }
179
  } finally {
180
- try {
181
- await writer?.close();
182
- } catch {
183
- }
184
  }
185
  })();
186
 
@@ -199,15 +200,11 @@ export async function POST(request: NextRequest) {
199
  }
200
 
201
  export async function PUT(request: NextRequest) {
202
- const user = await isAuthenticated();
203
- if (user instanceof NextResponse || !user) {
204
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
205
- }
206
-
207
  const authHeaders = await headers();
 
208
 
209
  const body = await request.json();
210
- const { prompt, provider, selectedElementHtml, model, pages, files, repoId, isNew } =
211
  body;
212
 
213
  if (!prompt || pages.length === 0) {
@@ -227,7 +224,7 @@ export async function PUT(request: NextRequest) {
227
  );
228
  }
229
 
230
- let token = user.token as string;
231
  let billTo: string | null = null;
232
 
233
  /**
@@ -243,7 +240,7 @@ export async function PUT(request: NextRequest) {
243
  ? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
244
  : authHeaders.get("x-forwarded-for");
245
 
246
- if (!token || token === "null" || token === "") {
247
  ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
248
  if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
249
  return NextResponse.json(
@@ -260,131 +257,246 @@ export async function PUT(request: NextRequest) {
260
  billTo = "huggingface";
261
  }
262
 
 
 
 
 
 
 
 
 
263
  try {
264
- const encoder = new TextEncoder();
265
- const stream = new TransformStream();
266
- const writer = stream.writable.getWriter();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
- const response = new NextResponse(stream.readable, {
269
- headers: {
270
- "Content-Type": "text/plain; charset=utf-8",
271
- "Cache-Control": "no-cache",
272
- Connection: "keep-alive",
 
 
 
 
 
 
 
 
 
 
 
273
  },
274
- });
 
275
 
276
- (async () => {
277
- try {
278
- const client = new InferenceClient(token);
 
 
 
 
279
 
280
- const basePrompt = selectedModel.value.includes('MiniMax')
281
- ? FOLLOW_UP_SYSTEM_PROMPT_LIGHT
282
- : FOLLOW_UP_SYSTEM_PROMPT;
283
- const systemPrompt = basePrompt + (isNew ? PROMPT_FOR_PROJECT_NAME : "");
284
- const userContext = "You are modifying the HTML file based on the user's request.";
285
 
286
- const allPages = pages || [];
287
- const pagesContext = allPages
288
- .map((p: Page) => `- ${p.path}\n${p.html}`)
289
- .join("\n\n");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
 
291
- const assistantContext = `${selectedElementHtml
292
- ? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\` Could be in multiple pages, if so, update all the pages.`
293
- : ""
294
- }. Current pages (${allPages.length} total): ${pagesContext}. ${files?.length > 0 ? `Available images: ${files?.map((f: string) => f).join(', ')}.` : ""}`;
 
 
 
295
 
296
- const chatCompletion = client.chatCompletionStream(
297
- {
298
- model: selectedModel.value + (provider !== "auto" ? `:${provider}` : ""),
299
- messages: [
300
- {
301
- role: "system",
302
- content: systemPrompt,
303
- },
304
- {
305
- role: "user",
306
- content: userContext,
307
- },
308
- {
309
- role: "assistant",
310
- content: assistantContext,
311
- },
312
- {
313
- role: "user",
314
- content: prompt,
315
- },
316
- ],
317
- ...(selectedModel.top_k ? { top_k: selectedModel.top_k } : {}),
318
- ...(selectedModel.temperature ? { temperature: selectedModel.temperature } : {}),
319
- ...(selectedModel.top_p ? { top_p: selectedModel.top_p } : {}),
320
- },
321
- billTo ? { billTo } : {}
322
- );
323
 
324
- // Stream the response chunks to the client
325
- while (true) {
326
- const { done, value } = await chatCompletion.next();
327
- if (done) {
328
- break;
 
 
 
 
329
  }
330
 
331
- const chunk = value.choices[0]?.delta?.content;
332
- if (chunk) {
333
- await writer.write(encoder.encode(chunk));
 
334
  }
335
- }
336
 
337
- await writer.write(encoder.encode(`\n___METADATA_START___\n${JSON.stringify({
338
- repoId,
339
- isNew,
340
- userName: user.name,
341
- })}\n___METADATA_END___\n`));
342
 
343
- await writer.close();
344
- } catch (error: any) {
345
- if (error.message?.includes("exceeded your monthly included credits")) {
346
- await writer.write(
347
- encoder.encode(
348
- JSON.stringify({
349
- ok: false,
350
- openProModal: true,
351
- message: error.message,
352
- })
353
- )
354
  );
355
- } else if (error?.message?.includes("inference provider information")) {
356
- await writer.write(
357
- encoder.encode(
358
- JSON.stringify({
359
- ok: false,
360
- openSelectProvider: true,
361
- message: error.message,
362
- })
363
- )
364
- );
365
- } else {
366
- await writer.write(
367
- encoder.encode(
368
- JSON.stringify({
369
- ok: false,
370
- message:
371
- error.message ||
372
- "An error occurred while processing your request.",
373
- })
374
- )
375
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  }
377
- } finally {
378
- try {
379
- await writer?.close();
380
- } catch {
381
- // ignore
382
  }
383
  }
384
- })();
385
 
386
- return response;
 
 
 
 
 
 
 
 
 
 
387
  } catch (error: any) {
 
 
 
 
 
 
 
 
 
 
388
  return NextResponse.json(
389
  {
390
  ok: false,
@@ -396,4 +508,3 @@ export async function PUT(request: NextRequest) {
396
  );
397
  }
398
  }
399
-
 
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
+ NEW_PAGE_END,
14
+ NEW_PAGE_START,
15
+ REPLACE_END,
16
+ SEARCH_START,
17
+ UPDATE_PAGE_START,
18
+ UPDATE_PAGE_END,
19
  } from "@/lib/prompts";
20
  import MY_TOKEN_KEY from "@/lib/get-cookie-name";
21
  import { Page } from "@/types";
 
 
22
 
23
  const ipAddresses = new Map();
24
 
25
  export async function POST(request: NextRequest) {
26
  const authHeaders = await headers();
27
+ const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
 
28
 
29
  const body = await request.json();
30
+ const { prompt, provider, model, redesignMarkdown, previousPrompts, pages } = body;
31
 
32
  if (!model || (!prompt && !redesignMarkdown)) {
33
  return NextResponse.json(
 
47
  );
48
  }
49
 
50
+ if (!selectedModel.providers.includes(provider) && provider !== "auto") {
51
+ return NextResponse.json(
52
+ {
53
+ ok: false,
54
+ error: `The selected model does not support the ${provider} provider.`,
55
+ openSelectProvider: true,
56
+ },
57
+ { status: 400 }
58
+ );
59
+ }
60
+
61
+ let token = userToken;
62
  let billTo: string | null = null;
63
 
64
  /**
 
74
  ? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
75
  : authHeaders.get("x-forwarded-for");
76
 
77
+ if (!token) {
78
  ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
79
  if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
80
  return NextResponse.json(
 
91
  billTo = "huggingface";
92
  }
93
 
94
+ const DEFAULT_PROVIDER = PROVIDERS.novita;
95
+ const selectedProvider =
96
+ provider === "auto"
97
+ ? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS]
98
+ : PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
99
+
100
+ const rewrittenPrompt = prompt;
101
+
102
+ // if (prompt?.length < 240) {
103
+
104
+ //rewrittenPrompt = await callAiRewritePrompt(prompt, { token, billTo });
105
+ // }
106
+
107
  try {
108
  const encoder = new TextEncoder();
109
  const stream = new TransformStream();
 
118
  });
119
 
120
  (async () => {
121
+ // let completeResponse = "";
122
  try {
123
  const client = new InferenceClient(token);
 
 
 
 
 
 
 
124
  const chatCompletion = client.chatCompletionStream(
125
  {
126
+ model: selectedModel.value,
127
+ provider: selectedProvider.id as any,
128
  messages: [
129
  {
130
  role: "system",
131
+ content: INITIAL_SYSTEM_PROMPT,
132
  },
133
+ ...(pages?.length > 1 ? [{
134
  role: "assistant",
135
+ content: `Here are the current pages:\n\n${pages.map((p: Page) => `- ${p.path} \n${p.html}`).join("\n")}\n\nNow, please create a new page based on this code. Also here are the previous prompts:\n\n${previousPrompts.map((p: string) => `- ${p}`).join("\n")}`
136
  }] : []),
137
  {
138
  role: "user",
139
+ content: redesignMarkdown
140
+ ? `Here is my current design as a markdown:\n\n${redesignMarkdown}\n\nNow, please create a new design based on this markdown.`
141
+ : rewrittenPrompt,
142
  },
143
  ],
144
+ max_tokens: selectedProvider.max_tokens,
 
 
145
  },
146
  billTo ? { billTo } : {}
147
  );
148
 
149
  while (true) {
150
+ const { done, value } = await chatCompletion.next();
151
  if (done) {
152
  break;
153
  }
 
157
  await writer.write(encoder.encode(chunk));
158
  }
159
  }
 
 
160
  } catch (error: any) {
161
  if (error.message?.includes("exceeded your monthly included credits")) {
162
  await writer.write(
 
168
  })
169
  )
170
  );
171
+ } else {
 
 
 
 
 
 
 
 
 
 
 
172
  await writer.write(
173
  encoder.encode(
174
  JSON.stringify({
 
181
  );
182
  }
183
  } finally {
184
+ await writer?.close();
 
 
 
185
  }
186
  })();
187
 
 
200
  }
201
 
202
  export async function PUT(request: NextRequest) {
 
 
 
 
 
203
  const authHeaders = await headers();
204
+ const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
205
 
206
  const body = await request.json();
207
+ const { prompt, previousPrompts, provider, selectedElementHtml, model, pages, files, } =
208
  body;
209
 
210
  if (!prompt || pages.length === 0) {
 
224
  );
225
  }
226
 
227
+ let token = userToken;
228
  let billTo: string | null = null;
229
 
230
  /**
 
240
  ? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
241
  : authHeaders.get("x-forwarded-for");
242
 
243
+ if (!token) {
244
  ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
245
  if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
246
  return NextResponse.json(
 
257
  billTo = "huggingface";
258
  }
259
 
260
+ const client = new InferenceClient(token);
261
+
262
+ const DEFAULT_PROVIDER = PROVIDERS.novita;
263
+ const selectedProvider =
264
+ provider === "auto"
265
+ ? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS]
266
+ : PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
267
+
268
  try {
269
+ const response = await client.chatCompletion(
270
+ {
271
+ model: selectedModel.value,
272
+ provider: selectedProvider.id as any,
273
+ messages: [
274
+ {
275
+ role: "system",
276
+ content: FOLLOW_UP_SYSTEM_PROMPT,
277
+ },
278
+ {
279
+ role: "user",
280
+ content: previousPrompts
281
+ ? `Also here are the previous prompts:\n\n${previousPrompts.map((p: string) => `- ${p}`).join("\n")}`
282
+ : "You are modifying the HTML file based on the user's request.",
283
+ },
284
+ {
285
+ role: "assistant",
286
 
287
+ content: `${
288
+ selectedElementHtml
289
+ ? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\``
290
+ : ""
291
+ }. Current pages: ${pages?.map((p: Page) => `- ${p.path} \n${p.html}`).join("\n")}. ${files?.length > 0 ? `Current images: ${files?.map((f: string) => `- ${f}`).join("\n")}.` : ""}`,
292
+ },
293
+ {
294
+ role: "user",
295
+ content: prompt,
296
+ },
297
+ ],
298
+ ...(selectedProvider.id !== "sambanova"
299
+ ? {
300
+ max_tokens: selectedProvider.max_tokens,
301
+ }
302
+ : {}),
303
  },
304
+ billTo ? { billTo } : {}
305
+ );
306
 
307
+ const chunk = response.choices[0]?.message?.content;
308
+ if (!chunk) {
309
+ return NextResponse.json(
310
+ { ok: false, message: "No content returned from the model" },
311
+ { status: 400 }
312
+ );
313
+ }
314
 
315
+ if (chunk) {
316
+ const updatedLines: number[][] = [];
317
+ let newHtml = "";
318
+ const updatedPages = [...(pages || [])];
 
319
 
320
+ const updatePageRegex = new RegExp(`${UPDATE_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${UPDATE_PAGE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`, 'g');
321
+ let updatePageMatch;
322
+
323
+ while ((updatePageMatch = updatePageRegex.exec(chunk)) !== null) {
324
+ const [, pagePath, pageContent] = updatePageMatch;
325
+
326
+ const pageIndex = updatedPages.findIndex(p => p.path === pagePath);
327
+ if (pageIndex !== -1) {
328
+ let pageHtml = updatedPages[pageIndex].html;
329
+
330
+ let processedContent = pageContent;
331
+ const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
332
+ if (htmlMatch) {
333
+ processedContent = htmlMatch[1];
334
+ }
335
+ let position = 0;
336
+ let moreBlocks = true;
337
+
338
+ while (moreBlocks) {
339
+ const searchStartIndex = processedContent.indexOf(SEARCH_START, position);
340
+ if (searchStartIndex === -1) {
341
+ moreBlocks = false;
342
+ continue;
343
+ }
344
+
345
+ const dividerIndex = processedContent.indexOf(DIVIDER, searchStartIndex);
346
+ if (dividerIndex === -1) {
347
+ moreBlocks = false;
348
+ continue;
349
+ }
350
+
351
+ const replaceEndIndex = processedContent.indexOf(REPLACE_END, dividerIndex);
352
+ if (replaceEndIndex === -1) {
353
+ moreBlocks = false;
354
+ continue;
355
+ }
356
+
357
+ const searchBlock = processedContent.substring(
358
+ searchStartIndex + SEARCH_START.length,
359
+ dividerIndex
360
+ );
361
+ const replaceBlock = processedContent.substring(
362
+ dividerIndex + DIVIDER.length,
363
+ replaceEndIndex
364
+ );
365
+
366
+ if (searchBlock.trim() === "") {
367
+ pageHtml = `${replaceBlock}\n${pageHtml}`;
368
+ updatedLines.push([1, replaceBlock.split("\n").length]);
369
+ } else {
370
+ const blockPosition = pageHtml.indexOf(searchBlock);
371
+ if (blockPosition !== -1) {
372
+ const beforeText = pageHtml.substring(0, blockPosition);
373
+ const startLineNumber = beforeText.split("\n").length;
374
+ const replaceLines = replaceBlock.split("\n").length;
375
+ const endLineNumber = startLineNumber + replaceLines - 1;
376
+
377
+ updatedLines.push([startLineNumber, endLineNumber]);
378
+ pageHtml = pageHtml.replace(searchBlock, replaceBlock);
379
+ }
380
+ }
381
+
382
+ position = replaceEndIndex + REPLACE_END.length;
383
+ }
384
 
385
+ updatedPages[pageIndex].html = pageHtml;
386
+
387
+ if (pagePath === '/' || pagePath === '/index' || pagePath === 'index') {
388
+ newHtml = pageHtml;
389
+ }
390
+ }
391
+ }
392
 
393
+ const newPageRegex = new RegExp(`${NEW_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${NEW_PAGE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`, 'g');
394
+ let newPageMatch;
395
+
396
+ while ((newPageMatch = newPageRegex.exec(chunk)) !== null) {
397
+ const [, pagePath, pageContent] = newPageMatch;
398
+
399
+ let pageHtml = pageContent;
400
+ const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
401
+ if (htmlMatch) {
402
+ pageHtml = htmlMatch[1];
403
+ }
404
+
405
+ const existingPageIndex = updatedPages.findIndex(p => p.path === pagePath);
406
+
407
+ if (existingPageIndex !== -1) {
408
+ updatedPages[existingPageIndex] = {
409
+ path: pagePath,
410
+ html: pageHtml.trim()
411
+ };
412
+ } else {
413
+ updatedPages.push({
414
+ path: pagePath,
415
+ html: pageHtml.trim()
416
+ });
417
+ }
418
+ }
 
419
 
420
+ if (updatedPages.length === pages?.length && !chunk.includes(UPDATE_PAGE_START)) {
421
+ let position = 0;
422
+ let moreBlocks = true;
423
+
424
+ while (moreBlocks) {
425
+ const searchStartIndex = chunk.indexOf(SEARCH_START, position);
426
+ if (searchStartIndex === -1) {
427
+ moreBlocks = false;
428
+ continue;
429
  }
430
 
431
+ const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
432
+ if (dividerIndex === -1) {
433
+ moreBlocks = false;
434
+ continue;
435
  }
 
436
 
437
+ const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
438
+ if (replaceEndIndex === -1) {
439
+ moreBlocks = false;
440
+ continue;
441
+ }
442
 
443
+ const searchBlock = chunk.substring(
444
+ searchStartIndex + SEARCH_START.length,
445
+ dividerIndex
 
 
 
 
 
 
 
 
446
  );
447
+ const replaceBlock = chunk.substring(
448
+ dividerIndex + DIVIDER.length,
449
+ replaceEndIndex
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
  );
451
+
452
+ if (searchBlock.trim() === "") {
453
+ newHtml = `${replaceBlock}\n${newHtml}`;
454
+ updatedLines.push([1, replaceBlock.split("\n").length]);
455
+ } else {
456
+ const blockPosition = newHtml.indexOf(searchBlock);
457
+ if (blockPosition !== -1) {
458
+ const beforeText = newHtml.substring(0, blockPosition);
459
+ const startLineNumber = beforeText.split("\n").length;
460
+ const replaceLines = replaceBlock.split("\n").length;
461
+ const endLineNumber = startLineNumber + replaceLines - 1;
462
+
463
+ updatedLines.push([startLineNumber, endLineNumber]);
464
+ newHtml = newHtml.replace(searchBlock, replaceBlock);
465
+ }
466
+ }
467
+
468
+ position = replaceEndIndex + REPLACE_END.length;
469
  }
470
+
471
+ // Update the main HTML if it's the index page
472
+ const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
473
+ if (mainPageIndex !== -1) {
474
+ updatedPages[mainPageIndex].html = newHtml;
475
  }
476
  }
 
477
 
478
+ return NextResponse.json({
479
+ ok: true,
480
+ updatedLines,
481
+ pages: updatedPages,
482
+ });
483
+ } else {
484
+ return NextResponse.json(
485
+ { ok: false, message: "No content returned from the model" },
486
+ { status: 400 }
487
+ );
488
+ }
489
  } catch (error: any) {
490
+ if (error.message?.includes("exceeded your monthly included credits")) {
491
+ return NextResponse.json(
492
+ {
493
+ ok: false,
494
+ openProModal: true,
495
+ message: error.message,
496
+ },
497
+ { status: 402 }
498
+ );
499
+ }
500
  return NextResponse.json(
501
  {
502
  ok: false,
 
508
  );
509
  }
510
  }
 
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
- ? "huggingface.co/deepsite"
28
  : host;
29
  const redirect_uri =
30
  `${host.includes("localhost") ? "http://" : "https://"}` +
31
  url +
32
- "/deepsite/auth/callback";
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,230 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { RepoDesignation, listFiles, spaceInfo, uploadFiles, deleteFiles, downloadFile } 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
- const files: File[] = [];
53
- const pages: Page[] = [];
54
- const mediaFiles: string[] = [];
55
- const allowedExtensions = ["html", "md", "css", "js", "json", "txt"];
56
- 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"];
57
- const commitFilePaths: Set<string> = new Set();
58
-
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 (fileInfo.path.endsWith(".html") || fileInfo.path.endsWith(".css") || fileInfo.path.endsWith(".js") || fileInfo.path.endsWith(".json")) {
67
- commitFilePaths.add(fileInfo.path);
68
-
69
- const blob = await downloadFile({
70
- repo,
71
- accessToken: user.token as string,
72
- path: fileInfo.path,
73
- revision: commitId,
74
- raw: true
75
- });
76
- const content = await blob?.text();
77
-
78
- if (content) {
79
- let mimeType = "text/plain";
80
-
81
- switch (fileExtension) {
82
- case "html":
83
- mimeType = "text/html";
84
- break;
85
- case "css":
86
- mimeType = "text/css";
87
- break;
88
- case "js":
89
- mimeType = "application/javascript";
90
- break;
91
- case "json":
92
- mimeType = "application/json";
93
- break;
94
- }
95
-
96
- if (fileInfo.path === "index.html") {
97
- pages.unshift({
98
- path: fileInfo.path,
99
- html: content,
100
- });
101
- } else {
102
- pages.push({
103
- path: fileInfo.path,
104
- html: content,
105
- });
106
- }
107
-
108
- const file = new File([content], fileInfo.path, { type: mimeType });
109
- files.push(file);
110
- }
111
- }
112
- else if (fileInfo.type === "directory" && (["videos", "images", "audio"].includes(fileInfo.path) || fileInfo.path === "components")) {
113
- for await (const subFileInfo of listFiles({
114
- repo,
115
- accessToken: user.token as string,
116
- revision: commitId,
117
- path: fileInfo.path,
118
- })) {
119
- if (subFileInfo.path.includes("components")) {
120
- commitFilePaths.add(subFileInfo.path);
121
- const blob = await downloadFile({
122
- repo,
123
- accessToken: user.token as string,
124
- path: subFileInfo.path,
125
- revision: commitId,
126
- raw: true
127
- });
128
- const content = await blob?.text();
129
-
130
- if (content) {
131
- pages.push({
132
- path: subFileInfo.path,
133
- html: content,
134
- });
135
-
136
- const file = new File([content], subFileInfo.path, { type: "text/html" });
137
- files.push(file);
138
- }
139
- } else if (allowedFilesExtensions.includes(subFileInfo.path.split(".").pop() || "")) {
140
- commitFilePaths.add(subFileInfo.path);
141
- mediaFiles.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${subFileInfo.path}`);
142
- }
143
- }
144
- }
145
- else if (allowedExtensions.includes(fileExtension || "")) {
146
- commitFilePaths.add(fileInfo.path);
147
- }
148
- }
149
-
150
- const mainBranchFilePaths: Set<string> = new Set();
151
- for await (const fileInfo of listFiles({
152
- repo,
153
- accessToken: user.token as string,
154
- revision: "main",
155
- })) {
156
- const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
157
-
158
- if (allowedExtensions.includes(fileExtension || "")) {
159
- mainBranchFilePaths.add(fileInfo.path);
160
- }
161
- }
162
-
163
- const filesToDelete: string[] = [];
164
- for (const mainFilePath of mainBranchFilePaths) {
165
- if (!commitFilePaths.has(mainFilePath)) {
166
- filesToDelete.push(mainFilePath);
167
- }
168
- }
169
-
170
- if (files.length === 0 && filesToDelete.length === 0) {
171
- return NextResponse.json(
172
- { ok: false, error: "No files found in the specified commit and no files to delete" },
173
- { status: 404 }
174
- );
175
- }
176
-
177
- if (filesToDelete.length > 0) {
178
- await deleteFiles({
179
- repo,
180
- paths: filesToDelete,
181
- accessToken: user.token as string,
182
- commitTitle: `Removed files from promoting ${commitId.slice(0, 7)}`,
183
- commitDescription: `Removed files that don't exist in commit ${commitId}:\n${filesToDelete.map(path => `- ${path}`).join('\n')}`,
184
- });
185
- }
186
-
187
- if (files.length > 0) {
188
- await uploadFiles({
189
- repo,
190
- files,
191
- accessToken: user.token as string,
192
- commitTitle: `Promote version ${commitId.slice(0, 7)} to main`,
193
- commitDescription: `Promoted commit ${commitId} to main branch`,
194
- });
195
- }
196
-
197
- return NextResponse.json(
198
- {
199
- ok: true,
200
- message: "Version promoted successfully",
201
- promotedCommit: commitId,
202
- pages: pages,
203
- files: mediaFiles,
204
- },
205
- { status: 200 }
206
- );
207
-
208
- } catch (error: any) {
209
-
210
- // Handle specific HuggingFace API errors
211
- if (error.statusCode === 404) {
212
- return NextResponse.json(
213
- { ok: false, error: "Commit not found" },
214
- { status: 404 }
215
- );
216
- }
217
-
218
- if (error.statusCode === 403) {
219
- return NextResponse.json(
220
- { ok: false, error: "Access denied to repository" },
221
- { status: 403 }
222
- );
223
- }
224
-
225
- return NextResponse.json(
226
- { ok: false, error: error.message || "Failed to promote version" },
227
- { status: 500 }
228
- );
229
- }
230
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/route.ts DELETED
@@ -1,102 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { RepoDesignation, listFiles, spaceInfo, downloadFile } from "@huggingface/hub";
3
-
4
- import { isAuthenticated } from "@/lib/auth";
5
- import { Page } from "@/types";
6
-
7
- export async function GET(
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
- const pages: Page[] = [];
53
-
54
- for await (const fileInfo of listFiles({
55
- repo,
56
- accessToken: user.token as string,
57
- revision: commitId,
58
- })) {
59
- const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
60
-
61
- if (fileInfo.path.endsWith(".html") || fileInfo.path.endsWith(".css") || fileInfo.path.endsWith(".js") || fileInfo.path.endsWith(".json")) {
62
- const blob = await downloadFile({
63
- repo,
64
- accessToken: user.token as string,
65
- path: fileInfo.path,
66
- revision: commitId,
67
- raw: true
68
- });
69
- const content = await blob?.text();
70
-
71
- if (content) {
72
- if (fileInfo.path === "index.html") {
73
- pages.unshift({
74
- path: fileInfo.path,
75
- html: content,
76
- });
77
- } else {
78
- pages.push({
79
- path: fileInfo.path,
80
- html: content,
81
- });
82
- }
83
- }
84
- }
85
- }
86
-
87
- return NextResponse.json({
88
- ok: true,
89
- pages,
90
- });
91
- } catch (error: any) {
92
- console.error("Error fetching commit pages:", error);
93
- return NextResponse.json(
94
- {
95
- ok: false,
96
- error: error.message || "Failed to fetch commit pages",
97
- },
98
- { status: 500 }
99
- );
100
- }
101
- }
102
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/me/projects/[namespace]/[repoId]/download/route.ts DELETED
@@ -1,105 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { RepoDesignation, listFiles, spaceInfo, downloadFile } from "@huggingface/hub";
3
- import JSZip from "jszip";
4
-
5
- import { isAuthenticated } from "@/lib/auth";
6
-
7
- export async function GET(
8
- req: NextRequest,
9
- { params }: { params: Promise<{ namespace: string; repoId: string }> }
10
- ) {
11
- const user = await isAuthenticated();
12
-
13
- if (user instanceof NextResponse || !user) {
14
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
15
- }
16
-
17
- const param = await params;
18
- const { namespace, repoId } = param;
19
-
20
- try {
21
- const space = await spaceInfo({
22
- name: `${namespace}/${repoId}`,
23
- accessToken: user.token as string,
24
- additionalFields: ["author"],
25
- });
26
-
27
- if (!space || space.sdk !== "static") {
28
- return NextResponse.json(
29
- {
30
- ok: false,
31
- error: "Space is not a static space",
32
- },
33
- { status: 404 }
34
- );
35
- }
36
-
37
- if (space.author !== user.name) {
38
- return NextResponse.json(
39
- {
40
- ok: false,
41
- error: "Space does not belong to the authenticated user",
42
- },
43
- { status: 403 }
44
- );
45
- }
46
-
47
- const repo: RepoDesignation = {
48
- type: "space",
49
- name: `${namespace}/${repoId}`,
50
- };
51
-
52
- const zip = new JSZip();
53
-
54
- for await (const fileInfo of listFiles({
55
- repo,
56
- accessToken: user.token as string,
57
- recursive: true,
58
- })) {
59
- if (fileInfo.type === "directory" || fileInfo.path.startsWith(".")) {
60
- continue;
61
- }
62
-
63
- try {
64
- const blob = await downloadFile({
65
- repo,
66
- accessToken: user.token as string,
67
- path: fileInfo.path,
68
- raw: true
69
- });
70
-
71
- if (blob) {
72
- const arrayBuffer = await blob.arrayBuffer();
73
- zip.file(fileInfo.path, arrayBuffer);
74
- }
75
- } catch (error) {
76
- console.error(`Error downloading file ${fileInfo.path}:`, error);
77
- }
78
- }
79
-
80
- const zipBlob = await zip.generateAsync({
81
- type: "blob",
82
- compression: "DEFLATE",
83
- compressionOptions: {
84
- level: 6
85
- }
86
- });
87
-
88
- const projectName = `${namespace}-${repoId}`.replace(/[^a-zA-Z0-9-_]/g, '_');
89
- const filename = `${projectName}.zip`;
90
-
91
- return new NextResponse(zipBlob, {
92
- headers: {
93
- "Content-Type": "application/zip",
94
- "Content-Disposition": `attachment; filename="${filename}"`,
95
- "Content-Length": zipBlob.size.toString(),
96
- },
97
- });
98
- } catch (error: any) {
99
- return NextResponse.json(
100
- { ok: false, error: error.message || "Failed to create ZIP file" },
101
- { status: 500 }
102
- );
103
- }
104
- }
105
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/me/projects/[namespace]/[repoId]/images/route.ts CHANGED
@@ -1,10 +1,12 @@
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 }> }
@@ -16,45 +18,41 @@ export async function POST(
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
  {
@@ -65,30 +63,18 @@ export async function POST(
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
  }
@@ -103,21 +89,21 @@ export async function POST(
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
  );
 
1
  import { NextRequest, NextResponse } from "next/server";
2
+ import { 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
 
8
+ // No longer need the ImageUpload interface since we're handling FormData with File objects
9
+
10
  export async function POST(
11
  req: NextRequest,
12
  { params }: { params: Promise<{ namespace: string; repoId: string }> }
 
18
  return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
19
  }
20
 
21
+ await dbConnect();
22
  const param = await params;
23
  const { namespace, repoId } = param;
24
 
25
+ const project = await Project.findOne({
26
+ user_id: user.id,
27
+ space_id: `${namespace}/${repoId}`,
28
+ }).lean();
 
 
 
 
 
 
 
 
29
 
30
+ if (!project) {
31
  return NextResponse.json(
32
+ {
33
+ ok: false,
34
+ error: "Project not found",
35
+ },
36
+ { status: 404 }
37
  );
38
  }
39
 
40
+ // Parse the FormData to get the images
41
  const formData = await req.formData();
42
+ const imageFiles = formData.getAll("images") as File[];
43
 
44
+ if (!imageFiles || imageFiles.length === 0) {
45
  return NextResponse.json(
46
  {
47
  ok: false,
48
+ error: "At least one image file is required under the 'images' key",
49
  },
50
  { status: 400 }
51
  );
52
  }
53
 
54
  const files: File[] = [];
55
+ for (const file of imageFiles) {
56
  if (!(file instanceof File)) {
57
  return NextResponse.json(
58
  {
 
63
  );
64
  }
65
 
66
+ if (!file.type.startsWith('image/')) {
 
 
 
 
 
67
  return NextResponse.json(
68
  {
69
  ok: false,
70
+ error: `File ${file.name} is not an image`,
71
  },
72
  { status: 400 }
73
  );
74
  }
75
 
76
+ // Create File object with images/ folder prefix
77
+ const fileName = `images/${file.name}`;
 
 
 
 
 
 
 
78
  const processedFile = new File([file], fileName, { type: file.type });
79
  files.push(processedFile);
80
  }
 
89
  repo,
90
  files,
91
  accessToken: user.token as string,
92
+ commitTitle: `Upload ${files.length} image(s)`,
93
  });
94
 
95
  return NextResponse.json({
96
  ok: true,
97
+ message: `Successfully uploaded ${files.length} image(s) to ${namespace}/${repoId}/images/`,
98
  uploadedFiles: files.map((file) => `https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${file.name}`),
99
  }, { status: 200 });
100
 
101
  } catch (error) {
102
+ console.error('Error uploading images:', error);
103
  return NextResponse.json(
104
  {
105
  ok: false,
106
+ error: "Failed to upload images",
107
  },
108
  { status: 500 }
109
  );
app/api/me/projects/[namespace]/[repoId]/route.ts CHANGED
@@ -1,10 +1,12 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { RepoDesignation, spaceInfo, listFiles, deleteRepo, listCommits, downloadFile } from "@huggingface/hub";
3
 
4
  import { isAuthenticated } from "@/lib/auth";
5
- import { Commit, Page } from "@/types";
 
 
6
 
7
- export async function DELETE(
8
  req: NextRequest,
9
  { params }: { params: Promise<{ namespace: string; repoId: string }> }
10
  ) {
@@ -14,63 +16,23 @@ 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
- try {
21
- const space = await spaceInfo({
22
- name: `${namespace}/${repoId}`,
23
- accessToken: user.token as string,
24
- additionalFields: ["author"],
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
- { ok: false, error: error.message },
56
- { status: 500 }
 
 
 
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,
@@ -103,59 +65,37 @@ export async function GET(
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") || fileInfo.path.endsWith(".json")) {
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
  {
@@ -165,17 +105,14 @@ export async function GET(
165
  { status: 404 }
166
  );
167
  }
 
168
  return NextResponse.json(
169
  {
170
  project: {
171
- id: space.id,
172
- space_id: space.name,
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 +121,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 +136,141 @@ export async function GET(
195
  );
196
  }
197
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { NextRequest, NextResponse } from "next/server";
2
+ import { RepoDesignation, spaceInfo, uploadFiles, listFiles } from "@huggingface/hub";
3
 
4
  import { isAuthenticated } from "@/lib/auth";
5
+ import Project from "@/models/Project";
6
+ import dbConnect from "@/lib/mongodb";
7
+ import { Page } from "@/types";
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
  try {
37
  const space = await spaceInfo({
38
  name: namespace + "/" + repoId,
 
65
  };
66
 
67
  const htmlFiles: Page[] = [];
68
+ const images: string[] = [];
69
 
70
+ const allowedImagesExtensions = ["jpg", "jpeg", "png", "gif", "svg", "webp", "avif", "heic", "heif", "ico", "bmp", "tiff", "tif"];
71
 
72
  for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
73
+ if (fileInfo.path.endsWith(".html")) {
74
+ const res = await fetch(`https://huggingface.co/spaces/${namespace}/${repoId}/raw/main/${fileInfo.path}`);
75
+ if (res.ok) {
76
+ const html = await res.text();
77
+ if (fileInfo.path === "index.html") {
78
+ htmlFiles.unshift({
79
+ path: fileInfo.path,
80
+ html,
81
+ });
82
+ } else {
 
 
83
  htmlFiles.push({
84
  path: fileInfo.path,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  html,
86
  });
 
 
87
  }
88
  }
89
  }
90
+ if (fileInfo.type === "directory" && fileInfo.path === "images") {
91
+ for await (const imageInfo of listFiles({repo, accessToken: user.token as string, path: fileInfo.path})) {
92
+ if (allowedImagesExtensions.includes(imageInfo.path.split(".").pop() || "")) {
93
+ images.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${imageInfo.path}`);
94
+ }
95
+ }
96
  }
 
 
 
 
 
97
  }
98
+
99
  if (htmlFiles.length === 0) {
100
  return NextResponse.json(
101
  {
 
105
  { status: 404 }
106
  );
107
  }
108
+
109
  return NextResponse.json(
110
  {
111
  project: {
112
+ ...project,
113
+ pages: htmlFiles,
114
+ images,
 
115
  },
 
 
 
116
  ok: true,
117
  },
118
  { status: 200 }
 
121
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
122
  } catch (error: any) {
123
  if (error.statusCode === 404) {
124
+ await Project.deleteOne({
125
+ user_id: user.id,
126
+ space_id: `${namespace}/${repoId}`,
127
+ });
128
  return NextResponse.json(
129
  { error: "Space not found", ok: false },
130
  { status: 404 }
 
136
  );
137
  }
138
  }
139
+
140
+ export async function PUT(
141
+ req: NextRequest,
142
+ { params }: { params: Promise<{ namespace: string; repoId: string }> }
143
+ ) {
144
+ const user = await isAuthenticated();
145
+
146
+ if (user instanceof NextResponse || !user) {
147
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
148
+ }
149
+
150
+ await dbConnect();
151
+ const param = await params;
152
+ const { namespace, repoId } = param;
153
+ const { pages, prompts } = await req.json();
154
+
155
+ const project = await Project.findOne({
156
+ user_id: user.id,
157
+ space_id: `${namespace}/${repoId}`,
158
+ }).lean();
159
+ if (!project) {
160
+ return NextResponse.json(
161
+ {
162
+ ok: false,
163
+ error: "Project not found",
164
+ },
165
+ { status: 404 }
166
+ );
167
+ }
168
+
169
+ const repo: RepoDesignation = {
170
+ type: "space",
171
+ name: `${namespace}/${repoId}`,
172
+ };
173
+
174
+ const files: File[] = [];
175
+ const promptsFile = new File([prompts.join("\n")], "prompts.txt", {
176
+ type: "text/plain",
177
+ });
178
+ files.push(promptsFile);
179
+ pages.forEach((page: Page) => {
180
+ const file = new File([page.html], page.path, { type: "text/html" });
181
+ files.push(file);
182
+ });
183
+ await uploadFiles({
184
+ repo,
185
+ files,
186
+ accessToken: user.token as string,
187
+ commitTitle: `${prompts[prompts.length - 1]} - Follow Up Deployment`,
188
+ });
189
+
190
+ await Project.updateOne(
191
+ { user_id: user.id, space_id: `${namespace}/${repoId}` },
192
+ {
193
+ $set: {
194
+ prompts: [
195
+ ...prompts,
196
+ ],
197
+ },
198
+ }
199
+ );
200
+ return NextResponse.json({ ok: true }, { status: 200 });
201
+ }
202
+
203
+ export async function POST(
204
+ req: NextRequest,
205
+ { params }: { params: Promise<{ namespace: string; repoId: string }> }
206
+ ) {
207
+ const user = await isAuthenticated();
208
+
209
+ if (user instanceof NextResponse || !user) {
210
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
211
+ }
212
+
213
+ await dbConnect();
214
+ const param = await params;
215
+ const { namespace, repoId } = param;
216
+
217
+ const space = await spaceInfo({
218
+ name: namespace + "/" + repoId,
219
+ accessToken: user.token as string,
220
+ additionalFields: ["author"],
221
+ });
222
+
223
+ if (!space || space.sdk !== "static") {
224
+ return NextResponse.json(
225
+ {
226
+ ok: false,
227
+ error: "Space is not a static space",
228
+ },
229
+ { status: 404 }
230
+ );
231
+ }
232
+ if (space.author !== user.name) {
233
+ return NextResponse.json(
234
+ {
235
+ ok: false,
236
+ error: "Space does not belong to the authenticated user",
237
+ },
238
+ { status: 403 }
239
+ );
240
+ }
241
+
242
+ const project = await Project.findOne({
243
+ user_id: user.id,
244
+ space_id: `${namespace}/${repoId}`,
245
+ }).lean();
246
+ if (project) {
247
+ // redirect to the project page if it already exists
248
+ return NextResponse.json(
249
+ {
250
+ ok: false,
251
+ error: "Project already exists",
252
+ redirect: `/projects/${namespace}/${repoId}`,
253
+ },
254
+ { status: 400 }
255
+ );
256
+ }
257
+
258
+ const newProject = new Project({
259
+ user_id: user.id,
260
+ space_id: `${namespace}/${repoId}`,
261
+ prompts: [],
262
+ });
263
+
264
+ await newProject.save();
265
+ return NextResponse.json(
266
+ {
267
+ ok: true,
268
+ project: {
269
+ id: newProject._id,
270
+ space_id: newProject.space_id,
271
+ prompts: newProject.prompts,
272
+ },
273
+ },
274
+ { status: 201 }
275
+ );
276
+ }
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,127 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { RepoDesignation, createRepo, listCommits, spaceInfo, uploadFiles } from "@huggingface/hub";
3
 
4
  import { isAuthenticated } from "@/lib/auth";
5
- import { Commit, Page } from "@/types";
 
6
  import { COLORS } from "@/lib/utils";
7
- import { injectDeepSiteBadge, isIndexPage } from "@/lib/inject-badge";
8
 
9
- export async function POST(
10
- req: NextRequest,
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: titleFromRequest, pages, prompt } = await req.json();
18
-
19
- const title = titleFromRequest ?? "DeepSite Project";
20
-
21
- const formattedTitle = title
22
- .toLowerCase()
23
- .replace(/[^a-z0-9]+/g, "-")
24
- .split("-")
25
- .filter(Boolean)
26
- .join("-")
27
- .slice(0, 96);
28
-
29
- const repo: RepoDesignation = {
30
- type: "space",
31
- name: `${user.name}/${formattedTitle}`,
32
- };
33
- const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
34
- const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
35
- const README = `---
36
- title: ${title}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  colorFrom: ${colorFrom}
38
  colorTo: ${colorTo}
39
- emoji: 🐳
40
  sdk: static
41
  pinned: false
42
  tags:
43
- - deepsite-v3
44
  ---
45
 
46
- # Welcome to your new DeepSite project!
47
- This project was created with [DeepSite](https://huggingface.co/deepsite).
48
- `;
49
-
50
- const files: File[] = [];
51
- const readmeFile = new File([README], "README.md", { type: "text/markdown" });
52
- files.push(readmeFile);
53
- pages.forEach((page: Page) => {
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 commitTitle = !prompt || prompt.trim() === "" ? "Redesign my website" : prompt;
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
- const commits: Commit[] = [];
87
- for await (const commit of listCommits({ repo, accessToken: user.token as string })) {
88
- if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Promote version")) {
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
- let newProject = {
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 } from "@/lib/utils";
8
+ import { Page } from "@/types";
9
 
10
+ export async function GET() {
 
 
11
  const user = await isAuthenticated();
12
+
13
  if (user instanceof NextResponse || !user) {
14
  return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
15
  }
16
 
17
+ await dbConnect();
18
+
19
+ const projects = await Project.find({
20
+ user_id: user?.id,
21
+ })
22
+ .sort({ _createdAt: -1 })
23
+ .limit(100)
24
+ .lean();
25
+ if (!projects) {
26
+ return NextResponse.json(
27
+ {
28
+ ok: false,
29
+ projects: [],
30
+ },
31
+ { status: 404 }
32
+ );
33
+ }
34
+ return NextResponse.json(
35
+ {
36
+ ok: true,
37
+ projects,
38
+ },
39
+ { status: 200 }
40
+ );
41
+ }
42
+
43
+ export async function POST(request: NextRequest) {
44
+ const user = await isAuthenticated();
45
+
46
+ if (user instanceof NextResponse || !user) {
47
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
48
+ }
49
+
50
+ const { title, pages, prompts } = await request.json();
51
+
52
+ if (!title || !pages || pages.length === 0) {
53
+ return NextResponse.json(
54
+ { message: "Title and HTML content are required.", ok: false },
55
+ { status: 400 }
56
+ );
57
+ }
58
+
59
+ await dbConnect();
60
+
61
+ try {
62
+ let readme = "";
63
+
64
+ const newTitle = title
65
+ .toLowerCase()
66
+ .replace(/[^a-z0-9]+/g, "-")
67
+ .split("-")
68
+ .filter(Boolean)
69
+ .join("-")
70
+ .slice(0, 96);
71
+
72
+ const repo: RepoDesignation = {
73
+ type: "space",
74
+ name: `${user.name}/${newTitle}`,
75
+ };
76
+
77
+ const { repoUrl } = await createRepo({
78
+ repo,
79
+ accessToken: user.token as string,
80
+ });
81
+ const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
82
+ const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
83
+ readme = `---
84
+ title: ${newTitle}
85
+ emoji: 🐳
86
  colorFrom: ${colorFrom}
87
  colorTo: ${colorTo}
 
88
  sdk: static
89
  pinned: false
90
  tags:
91
+ - deepsite
92
  ---
93
 
94
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
+ const readmeFile = new File([readme], "README.md", {
97
+ type: "text/markdown",
98
+ });
99
+ const promptsFile = new File([prompts.join("\n")], "prompts.txt", {
100
+ type: "text/plain",
101
+ });
102
+ const files = [readmeFile, promptsFile];
103
+ pages.forEach((page: Page) => {
104
+ const file = new File([page.html], page.path, { type: "text/html" });
105
+ files.push(file);
106
  });
 
107
  await uploadFiles({
108
  repo,
109
  files,
110
  accessToken: user.token as string,
111
+ commitTitle: `${prompts[prompts.length - 1]} - Initial Deployment`,
112
  });
 
113
  const path = repoUrl.split("/").slice(-2).join("/");
114
+ const project = await Project.create({
115
+ user_id: user.id,
116
+ space_id: path,
117
+ prompts,
 
 
 
 
 
 
 
 
 
 
 
 
118
  });
119
+ return NextResponse.json({ project, path, ok: true }, { status: 201 });
120
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
 
 
 
 
 
 
 
 
 
 
 
121
  } catch (err: any) {
122
  return NextResponse.json(
123
  { error: err.message, ok: false },
124
  { status: 500 }
125
  );
126
  }
127
+ }
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
- const projects = [];
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
- // Create an AbortController for timeout
19
- const controller = new AbortController();
20
- const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
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
- const markdown = await response.text();
 
40
  return NextResponse.json(
41
- {
42
- ok: true,
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
- const isPopup = window.opener || window.parent !== window;
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(() => setShowButton(true), 7000);
 
 
 
44
 
45
  return (
46
- <div className="h-screen flex flex-col justify-center items-center bg-neutral-950 z-1 relative">
47
- <div className="background__noisy" />
48
- <div className="relative max-w-4xl py-10 flex items-center justify-center w-full">
49
- <div className="max-w-lg mx-auto !rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden ring-[8px] ring-white/20">
50
- <header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
51
- <div className="flex items-center justify-center -space-x-4 mb-3">
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
- <p className="text-xl font-semibold text-neutral-950">
63
- {isPopupAuth
64
- ? "Authentication Complete!"
65
- : "Login In Progress..."}
66
- </p>
67
- <p className="text-sm text-neutral-500 mt-1.5">
68
- {isPopupAuth
69
- ? "You can now close this tab and return to the previous page."
70
- : "Wait a moment while we log you in with your code."}
 
 
 
 
 
 
 
 
 
 
71
  </p>
72
- </header>
73
- <main className="space-y-4 p-6">
74
- <div>
75
- <p className="text-sm text-neutral-700 mb-4 max-w-xs">
76
- If you are not redirected automatically in the next 5 seconds,
77
- please click the button below
 
 
 
78
  </p>
79
- {showButton ? (
80
- <Link href="/">
81
- <Button variant="black" className="relative">
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
@@ -1,18 +1,16 @@
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
  import type { Metadata, Viewport } from "next";
3
  import { Inter, PT_Sans } from "next/font/google";
4
- import Script from "next/script";
5
- import { headers } from "next/headers";
6
- import { redirect } from "next/navigation";
7
 
 
8
  import "@/assets/globals.css";
9
  import { Toaster } from "@/components/ui/sonner";
10
- import IframeDetector from "@/components/iframe-detector";
 
11
  import AppContext from "@/components/contexts/app-context";
12
- import TanstackContext from "@/components/contexts/tanstack-query-context";
13
- import { LoginProvider } from "@/components/contexts/login-context";
14
- import { ProProvider } from "@/components/contexts/pro-context";
15
- import { generateSEO, generateStructuredData } from "@/lib/seo";
16
 
17
  const inter = Inter({
18
  variable: "--font-inter-sans",
@@ -26,12 +24,31 @@ const ptSans = PT_Sans({
26
  });
27
 
28
  export const metadata: Metadata = {
29
- ...generateSEO({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- path: "/",
34
- }),
35
  appleWebApp: {
36
  capable: true,
37
  title: "DeepSite",
@@ -42,9 +59,6 @@ export const metadata: Metadata = {
42
  shortcut: "/logo.svg",
43
  apple: "/logo.svg",
44
  },
45
- verification: {
46
- google: process.env.GOOGLE_SITE_VERIFICATION,
47
- },
48
  };
49
 
50
  export const viewport: Viewport = {
@@ -53,92 +67,45 @@ export const viewport: Viewport = {
53
  themeColor: "#000000",
54
  };
55
 
56
- // async function getMe() {
57
- // const cookieStore = await cookies();
58
- // const cookieName = MY_TOKEN_KEY();
59
- // const token = cookieStore.get(cookieName)?.value;
 
 
 
 
 
 
 
 
 
 
 
60
 
61
- // if (!token) return { user: null, projects: [], errCode: null };
62
- // try {
63
- // const res = await apiServer.get("/me", {
64
- // headers: {
65
- // Authorization: `Bearer ${token}`,
66
- // },
67
- // });
68
- // return { user: res.data.user, projects: res.data.projects, errCode: null };
69
- // } catch (err: any) {
70
- // return { user: null, projects: [], errCode: err.status };
71
- // }
72
- // }
73
 
74
  export default async function RootLayout({
75
  children,
76
  }: Readonly<{
77
  children: React.ReactNode;
78
  }>) {
79
- // Domain redirect check
80
- const headersList = await headers();
81
- const forwardedHost = headersList.get("x-forwarded-host");
82
- const host = headersList.get("host");
83
- const hostname = (forwardedHost || host || "").split(":")[0];
84
-
85
- const isLocalDev =
86
- hostname === "localhost" ||
87
- hostname === "127.0.0.1" ||
88
- hostname.startsWith("192.168.");
89
- const isHuggingFace =
90
- hostname === "huggingface.co" || hostname.endsWith(".huggingface.co");
91
-
92
- if (!isHuggingFace && !isLocalDev) {
93
- const pathname = headersList.get("x-invoke-path") || "/deepsite";
94
- redirect(`https://huggingface.co${pathname}`);
95
- }
96
-
97
- // const data = await getMe();
98
-
99
- // Generate structured data
100
- const structuredData = generateStructuredData("WebApplication", {
101
- name: "DeepSite",
102
- description: "Build websites with AI, no code required",
103
- url: "https://huggingface.co/deepsite",
104
- });
105
-
106
- const organizationData = generateStructuredData("Organization", {
107
- name: "DeepSite",
108
- url: "https://huggingface.co/deepsite",
109
- });
110
-
111
  return (
112
  <html lang="en">
 
 
 
 
 
113
  <body
114
  className={`${inter.variable} ${ptSans.variable} antialiased bg-black dark h-[100dvh] overflow-hidden`}
115
  >
116
- <script
117
- type="application/ld+json"
118
- dangerouslySetInnerHTML={{
119
- __html: JSON.stringify(structuredData),
120
- }}
121
- />
122
- <script
123
- type="application/ld+json"
124
- dangerouslySetInnerHTML={{
125
- __html: JSON.stringify(organizationData),
126
- }}
127
- />
128
- <Script
129
- defer
130
- data-domain="deepsite.hf.co"
131
- src="https://plausible.io/js/script.js"
132
- />
133
  <IframeDetector />
134
  <Toaster richColors position="bottom-center" />
135
- <TanstackContext>
136
- <AppContext>
137
- <LoginProvider>
138
- <ProProvider>{children}</ProProvider>
139
- </LoginProvider>
140
- </AppContext>
141
- </TanstackContext>
142
  </body>
143
  </html>
144
  );
 
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
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
+ import IframeDetector from "@/components/iframe-detector";
 
 
14
 
15
  const inter = Inter({
16
  variable: "--font-inter-sans",
 
24
  });
25
 
26
  export const metadata: Metadata = {
27
+ title: "DeepSite | Build with AI ✨",
28
+ description:
29
+ "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.",
30
+ openGraph: {
31
+ title: "DeepSite | Build with AI ✨",
32
+ description:
33
+ "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.",
34
+ url: "https://deepsite.hf.co",
35
+ siteName: "DeepSite",
36
+ images: [
37
+ {
38
+ url: "https://deepsite.hf.co/banner.png",
39
+ width: 1200,
40
+ height: 630,
41
+ alt: "DeepSite Open Graph Image",
42
+ },
43
+ ],
44
+ },
45
+ twitter: {
46
+ card: "summary_large_image",
47
  title: "DeepSite | Build with AI ✨",
48
  description:
49
  "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.",
50
+ images: ["https://deepsite.hf.co/banner.png"],
51
+ },
52
  appleWebApp: {
53
  capable: true,
54
  title: "DeepSite",
 
59
  shortcut: "/logo.svg",
60
  apple: "/logo.svg",
61
  },
 
 
 
62
  };
63
 
64
  export const viewport: Viewport = {
 
67
  themeColor: "#000000",
68
  };
69
 
70
+ async function getMe() {
71
+ const cookieStore = await cookies();
72
+ const token = cookieStore.get(MY_TOKEN_KEY())?.value;
73
+ if (!token) return { user: null, errCode: null };
74
+ try {
75
+ const res = await apiServer.get("/me", {
76
+ headers: {
77
+ Authorization: `Bearer ${token}`,
78
+ },
79
+ });
80
+ return { user: res.data.user, errCode: null };
81
+ } catch (err: any) {
82
+ return { user: null, errCode: err.status };
83
+ }
84
+ }
85
 
86
+ // if domain isn't deepsite.hf.co or enzostvs-deepsite.hf.space redirect to deepsite.hf.co
 
 
 
 
 
 
 
 
 
 
 
87
 
88
  export default async function RootLayout({
89
  children,
90
  }: Readonly<{
91
  children: React.ReactNode;
92
  }>) {
93
+ const data = await getMe();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  return (
95
  <html lang="en">
96
+ <Script
97
+ defer
98
+ data-domain="deepsite.hf.co"
99
+ src="https://plausible.io/js/script.js"
100
+ ></Script>
101
  <body
102
  className={`${inter.variable} ${ptSans.variable} antialiased bg-black dark h-[100dvh] overflow-hidden`}
103
  >
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  <IframeDetector />
105
  <Toaster richColors position="bottom-center" />
106
+ <TanstackProvider>
107
+ <AppContext me={data}>{children}</AppContext>
108
+ </TanstackProvider>
 
 
 
 
109
  </body>
110
  </html>
111
  );
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,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 data = await getProject(namespace, repoId);
36
+ if (!data?.pages) {
37
+ redirect("/projects");
38
+ }
39
+ return (
40
+ <AppEditor project={data} pages={data.pages} images={data.images ?? []} />
41
+ );
42
+ }
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 isNew />;
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("/deepsite/background_noisy.webp");
135
  @apply fixed w-screen h-screen -z-1 top-0 left-0;
136
  }
137
 
@@ -148,233 +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
- }
372
-
373
- .transparent-scroll {
374
- scrollbar-width: none; /* Firefox */
375
- -ms-overflow-style: none; /* IE and Edge */
376
- }
377
-
378
- .transparent-scroll::-webkit-scrollbar {
379
- display: none; /* Chrome, Safari, Opera */
380
- }
 
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/minimax.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": "assets/globals.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,&nbsp;
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 { ProjectType, User } from "@/types";
 
 
 
 
9
  import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
10
 
11
  export default function AppContext({
12
  children,
13
- }: // me: initialData,
14
- {
15
  children: React.ReactNode;
16
- // me?: {
17
- // user: User | null;
18
- // projects: ProjectType[];
19
- // errCode: number | null;
20
- // };
21
  }) {
22
- const { loginFromCode, user, logout, loading, errCode } = useUser();
 
23
  const pathname = usePathname();
24
  const router = useRouter();
25
 
26
- // useMount(() => {
27
- // if (!initialData?.user && !user) {
28
- // if ([401, 403].includes(errCode as number)) {
29
- // logout();
30
- // } else if (pathname.includes("/spaces")) {
31
- // if (errCode) {
32
- // toast.error("An error occured while trying to log in");
33
- // }
34
- // // If we did not manage to log in (probs because api is down), we simply redirect to the home page
35
- // router.push("/");
36
- // }
37
- // }
38
- // });
39
 
40
  const events: any = {};
41
 
@@ -48,5 +49,9 @@ export default function AppContext({
48
  }
49
  });
50
 
51
- return children;
 
 
 
 
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/discord-promo-modal/index.tsx DELETED
@@ -1,225 +0,0 @@
1
- "use client";
2
-
3
- import { useEffect, useState } from "react";
4
- import { useLocalStorage } from "react-use";
5
- import Image from "next/image";
6
- import { Button } from "@/components/ui/button";
7
- import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
8
- import { DiscordIcon } from "@/components/icons/discord";
9
- import Logo from "@/assets/logo.svg";
10
-
11
- const DISCORD_PROMO_KEY = "discord-promo-dismissed";
12
- const DISCORD_URL = "https://discord.gg/KpanwM3vXa";
13
-
14
- const Sparkle = ({
15
- size = "w-3 h-3",
16
- delay = "0s",
17
- top = "20%",
18
- left = "20%",
19
- }: {
20
- size?: string;
21
- delay?: string;
22
- top?: string;
23
- left?: string;
24
- }) => (
25
- <div
26
- className={`absolute ${size}`}
27
- style={{ top, left, animationDelay: delay }}
28
- >
29
- <svg
30
- viewBox="0 0 24 24"
31
- fill="none"
32
- xmlns="http://www.w3.org/2000/svg"
33
- className="w-full h-full animate-sparkle"
34
- >
35
- <path
36
- d="M12 0L13.5 8.5L22 10L13.5 11.5L12 20L10.5 11.5L2 10L10.5 8.5L12 0Z"
37
- fill="url(#sparkle-gradient)"
38
- />
39
- <defs>
40
- <linearGradient id="sparkle-gradient" x1="2" y1="10" x2="22" y2="10">
41
- <stop offset="0%" stopColor="#818cf8" />
42
- <stop offset="100%" stopColor="#a5b4fc" />
43
- </linearGradient>
44
- </defs>
45
- </svg>
46
- </div>
47
- );
48
-
49
- export const DiscordPromoModal = () => {
50
- const [open, setOpen] = useState(false);
51
- const [dismissed, setDismissed] = useLocalStorage<boolean>(
52
- DISCORD_PROMO_KEY,
53
- false
54
- );
55
-
56
- useEffect(() => {
57
- const cookieDismissed = document.cookie
58
- .split("; ")
59
- .find((row) => row.startsWith(`${DISCORD_PROMO_KEY}=`))
60
- ?.split("=")[1];
61
-
62
- if (dismissed || cookieDismissed === "true") {
63
- return;
64
- }
65
-
66
- const timer = setTimeout(() => {
67
- setOpen(true);
68
- }, 60000);
69
-
70
- return () => clearTimeout(timer);
71
- }, [dismissed]);
72
-
73
- const handleClose = () => {
74
- setOpen(false);
75
- setDismissed(true);
76
-
77
- const expiryDate = new Date();
78
- expiryDate.setDate(expiryDate.getDate() + 5);
79
- document.cookie = `${DISCORD_PROMO_KEY}=true; expires=${expiryDate.toUTCString()}; path=/; SameSite=Lax`;
80
- };
81
-
82
- const handleJoinDiscord = () => {
83
- window.open(DISCORD_URL, "_blank");
84
- handleClose();
85
- };
86
-
87
- return (
88
- <Dialog open={open} onOpenChange={handleClose}>
89
- <DialogContent
90
- className="sm:max-w-[480px] lg:!p-0 !rounded-3xl !bg-gradient-to-b !from-indigo-950/40 !via-neutral-900 !to-neutral-900 !border !border-neutral-800 overflow-hidden"
91
- showCloseButton={true}
92
- >
93
- <DialogTitle className="hidden" />
94
-
95
- <div className="relative">
96
- <div className="absolute inset-0 bg-gradient-to-br from-indigo-500/10 via-indigo-500/5 to-transparent pointer-events-none" />
97
-
98
- <div className="absolute inset-x-0 top-0 h-48 overflow-hidden pointer-events-none">
99
- <Sparkle size="w-2 h-2" delay="0s" top="15%" left="15%" />
100
- <Sparkle size="w-3 h-3" delay="0.5s" top="25%" left="75%" />
101
- <Sparkle size="w-2 h-2" delay="1s" top="35%" left="20%" />
102
- <Sparkle size="w-4 h-4" delay="1.5s" top="10%" left="80%" />
103
- <Sparkle size="w-2 h-2" delay="2s" top="30%" left="85%" />
104
- </div>
105
-
106
- <div className="relative pt-12 pb-8">
107
- <div className="relative z-10 flex justify-center">
108
- <div className="relative">
109
- <div className="absolute inset-0 bg-gradient-to-br from-indigo-400 via-indigo-500 to-indigo-600 rounded-full blur-md opacity-50" />
110
- <div className="relative w-32 h-32 rounded-full bg-gradient-to-br from-neutral-900 via-neutral-800 to-neutral-900 p-1 shadow-2xl">
111
- <div className="w-full h-full rounded-full bg-neutral-900 flex items-center justify-center overflow-hidden">
112
- <div className="relative w-20 h-20 bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-full flex items-center justify-center">
113
- <DiscordIcon className="w-12 h-12 text-white" />
114
- </div>
115
- </div>
116
- </div>
117
-
118
- <div className="absolute -bottom-2 -right-2 bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-full p-0.5 shadow-xl border-2 border-neutral-900">
119
- <div className="w-10 h-10 bg-neutral-900 rounded-full flex items-center justify-center">
120
- <Image
121
- src={Logo}
122
- alt="DeepSite"
123
- width={20}
124
- height={20}
125
- className="w-5 h-5"
126
- />
127
- </div>
128
- </div>
129
- </div>
130
- </div>
131
- </div>
132
-
133
- <main className="px-8 pb-8 pt-4">
134
- <div className="text-center mb-6">
135
- <h2 className="text-2xl font-bold text-white mb-2">
136
- Ready to level up your DeepSite experience?
137
- </h2>
138
- <p className="text-neutral-400 text-sm">
139
- Get help, share your projects and ask for suggestions!
140
- </p>
141
- </div>
142
-
143
- <div className="flex flex-col gap-3 mb-6">
144
- {[
145
- "Get exclusive preview to new features",
146
- "Share your projects and get feedback",
147
- "Priority support from the team",
148
- "Enjoy real-time updates",
149
- ].map((benefit, index) => (
150
- <div
151
- key={index}
152
- className="flex items-start gap-3 text-neutral-200"
153
- style={{
154
- animation: `fadeIn 0.4s ease-out ${index * 0.1}s both`,
155
- }}
156
- >
157
- <div className="flex-shrink-0 w-5 h-5 rounded-full bg-gradient-to-br from-indigo-500 to-indigo-600 flex items-center justify-center mt-0.5">
158
- <svg
159
- className="w-3 h-3 text-white"
160
- fill="none"
161
- viewBox="0 0 24 24"
162
- stroke="currentColor"
163
- >
164
- <path
165
- strokeLinecap="round"
166
- strokeLinejoin="round"
167
- strokeWidth={3}
168
- d="M5 13l4 4L19 7"
169
- />
170
- </svg>
171
- </div>
172
- <span className="text-sm leading-6">{benefit}</span>
173
- </div>
174
- ))}
175
- </div>
176
-
177
- {/* CTA Button */}
178
- <div className="flex flex-col gap-3 w-full">
179
- <Button
180
- onClick={handleJoinDiscord}
181
- className="w-full !h-12 !text-base font-semibold !bg-gradient-to-r !from-indigo-500 !to-indigo-600 hover:!from-indigo-600 hover:!to-indigo-700 !text-white !border-0 transform hover:scale-[1.02] transition-all duration-200 shadow-lg shadow-indigo-500/25"
182
- >
183
- <DiscordIcon className="w-5 h-5 mr-2" />
184
- Join Discord Community
185
- </Button>
186
-
187
- <p className="text-center text-xs text-neutral-500">
188
- Free to join. Connect instantly.
189
- </p>
190
- </div>
191
- </main>
192
- </div>
193
-
194
- <style jsx>{`
195
- @keyframes fadeIn {
196
- from {
197
- opacity: 0;
198
- transform: translateY(5px);
199
- }
200
- to {
201
- opacity: 1;
202
- transform: translateY(0);
203
- }
204
- }
205
-
206
- @keyframes sparkle {
207
- 0%,
208
- 100% {
209
- opacity: 0;
210
- transform: scale(0) rotate(0deg);
211
- }
212
- 50% {
213
- opacity: 1;
214
- transform: scale(1) rotate(180deg);
215
- }
216
- }
217
-
218
- :global(.animate-sparkle) {
219
- animation: sparkle 2s ease-in-out infinite;
220
- }
221
- `}</style>
222
- </DialogContent>
223
- </Dialog>
224
- );
225
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/context.tsx DELETED
@@ -1,128 +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 if (filePath.endsWith(".json")) {
27
- return <Braces className={size} />;
28
- } else {
29
- return <FileText className={size} />;
30
- }
31
- };
32
-
33
- const buttonContent = useMemo(() => {
34
- if (selectedFile) {
35
- return (
36
- <>
37
- <span className="truncate max-w-[120px]">{selectedFile}</span>
38
- </>
39
- );
40
- }
41
- return <>Add Context</>;
42
- }, [selectedFile]);
43
-
44
- return (
45
- <Popover open={open} onOpenChange={setOpen}>
46
- <PopoverTrigger asChild>
47
- <Button
48
- size="xs"
49
- variant={open ? "default" : "outline"}
50
- className={classNames("!rounded-md", {
51
- "!bg-blue-500/10 !border-blue-500/30 !text-blue-400":
52
- selectedFile && selectedFile.endsWith(".css"),
53
- "!bg-orange-500/10 !border-orange-500/30 !text-orange-400":
54
- selectedFile && selectedFile.endsWith(".html"),
55
- "!bg-amber-500/10 !border-amber-500/30 !text-amber-400":
56
- selectedFile && selectedFile.endsWith(".js"),
57
- "!bg-yellow-500/10 !border-yellow-500/30 !text-yellow-400":
58
- selectedFile && selectedFile.endsWith(".json"),
59
- })}
60
- disabled={
61
- globalAiLoading || globalEditorLoading || pages.length === 0
62
- }
63
- >
64
- <AtSign className="size-3.5" />
65
-
66
- {buttonContent}
67
- </Button>
68
- </PopoverTrigger>
69
- <PopoverContent
70
- align="start"
71
- className="w-64 !bg-neutral-900 !border-neutral-800 !p-0 !rounded-2xl overflow-hidden"
72
- >
73
- <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">
74
- Select a file to send as context
75
- </header>
76
- <main className="space-y-1 p-2">
77
- <div className="max-h-[200px] overflow-y-auto space-y-0.5">
78
- {pages.length === 0 ? (
79
- <div className="px-2 py-2 text-xs text-neutral-500">
80
- No files available
81
- </div>
82
- ) : (
83
- <>
84
- <button
85
- onClick={() => {
86
- setContextFile(null);
87
- setOpen(false);
88
- }}
89
- className={`cursor-pointer w-full px-2 py-1.5 text-xs text-left rounded-md hover:bg-neutral-800 transition-colors ${
90
- !selectedFile
91
- ? "bg-neutral-800 text-neutral-200 font-medium"
92
- : "text-neutral-400 hover:text-neutral-200"
93
- }`}
94
- >
95
- All files (default)
96
- </button>
97
- {pages.map((page) => (
98
- <button
99
- key={page.path}
100
- onClick={() => {
101
- setContextFile(page.path);
102
- setOpen(false);
103
- }}
104
- 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 ${
105
- selectedFile === page.path
106
- ? "bg-neutral-800 text-neutral-200 font-medium"
107
- : "text-neutral-400 hover:text-neutral-200"
108
- }`}
109
- >
110
- <span className="shrink-0">
111
- {getFileIcon(page.path, "size-3")}
112
- </span>
113
- <span className="truncate flex-1">{page.path}</span>
114
- {page.path === currentPage && (
115
- <span className="text-[10px] text-neutral-500 shrink-0">
116
- (current)
117
- </span>
118
- )}
119
- </button>
120
- ))}
121
- </>
122
- )}
123
- </div>
124
- </main>
125
- </PopoverContent>
126
- </Popover>
127
- );
128
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,111 +1,168 @@
1
- import { useRef, useState } from "react";
 
 
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 { useAi } from "@/hooks/useAi";
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 { Uploader } from "@/components/editor/ask-ai/uploader";
 
 
 
 
15
  import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
16
- import { Selector } from "@/components/editor/ask-ai/selector";
17
- import { PromptBuilder } from "@/components/editor/ask-ai/prompt-builder";
18
- import { Context } from "@/components/editor/ask-ai/context";
19
- import { useUser } from "@/hooks/useUser";
20
- import { useLoginModal } from "@/components/contexts/login-context";
21
- import { Settings } from "./settings";
22
- import { useProModal } from "@/components/contexts/pro-context";
23
- import { MAX_FREE_PROJECTS } from "@/lib/utils";
24
- import { PROMPTS_FOR_AI } from "@/lib/prompts";
25
- import { SelectedRedesignUrl } from "./selected-redesign-url";
26
 
27
- export const AskAi = ({
28
- project,
29
  isNew,
 
 
 
 
30
  onScrollToBottom,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  }: {
32
- project?: Project;
33
- files?: string[];
 
 
 
 
 
 
 
 
34
  isNew?: boolean;
35
- onScrollToBottom?: () => void;
36
- }) => {
37
- const { user, projects } = useUser();
38
- const { isSameHtml, isUploading, pages, isLoadingProject } = useEditor();
39
- const {
40
- isAiWorking,
41
- isThinking,
42
- thinkingContent,
43
- selectedFiles,
44
- setSelectedFiles,
45
- selectedElement,
46
- setSelectedElement,
47
- callAiNewProject,
48
- callAiFollowUp,
49
- audio: hookAudio,
50
- cancelRequest,
51
- } = useAi(onScrollToBottom);
52
- const { openLoginModal } = useLoginModal();
53
- const { openProModal } = useProModal();
54
  const [openProvider, setOpenProvider] = useState(false);
55
  const [providerError, setProviderError] = useState("");
56
- const [redesignData, setRedesignData] = useState<
57
- undefined | { markdown: string; url: string }
58
- >(undefined);
59
- const refThink = useRef<HTMLDivElement>(null);
60
-
61
- const [enhancedSettings, setEnhancedSettings, removeEnhancedSettings] =
62
- useLocalStorage<EnhancedSettings>("deepsite-enhancedSettings", {
63
- isActive: false,
64
- primaryColor: undefined,
65
- secondaryColor: undefined,
66
- theme: undefined,
67
- });
68
- const [promptStorage, , removePromptStorage] = useLocalStorage("prompt", "");
69
-
70
- const [isFollowUp, setIsFollowUp] = useState(true);
71
- const [prompt, setPrompt] = useState(
72
- promptStorage && promptStorage.trim() !== "" ? promptStorage : ""
73
- );
74
  const [openThink, setOpenThink] = useState(false);
75
- const [randomPromptLoading, setRandomPromptLoading] = useState(false);
 
 
 
 
76
 
77
- useMount(() => {
78
- if (promptStorage && promptStorage.trim() !== "") {
79
- callAi();
80
- }
 
 
 
 
 
 
 
 
 
 
 
 
81
  });
82
 
83
- const callAi = async (redesignMarkdown?: string | undefined) => {
84
- removePromptStorage();
85
- if (user && !user.isPro && projects.length >= MAX_FREE_PROJECTS)
86
- return openProModal([]);
 
87
  if (isAiWorking) return;
88
  if (!redesignMarkdown && !prompt.trim()) return;
89
 
90
  if (isFollowUp && !redesignMarkdown && !isSameHtml) {
91
- if (!user) return openLoginModal({ prompt });
92
- const result = await callAiFollowUp(prompt, enhancedSettings, isNew);
 
 
 
 
 
 
 
 
 
 
 
93
 
94
  if (result?.error) {
95
  handleError(result.error, result.message);
96
  return;
97
  }
98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  if (result?.success) {
100
  setPrompt("");
101
  }
102
  } else {
103
  const result = await callAiNewProject(
104
  prompt,
105
- enhancedSettings,
 
106
  redesignMarkdown,
107
- !!user,
108
- user?.name
 
 
109
  );
110
 
111
  if (result?.error) {
@@ -115,21 +172,30 @@ export const AskAi = ({
115
 
116
  if (result?.success) {
117
  setPrompt("");
 
 
 
118
  }
119
  }
120
  };
121
 
 
 
 
 
 
 
122
  const handleError = (error: string, message?: string) => {
123
  switch (error) {
124
  case "login_required":
125
- openLoginModal();
126
  break;
127
  case "provider_required":
128
  setOpenProvider(true);
129
  setProviderError(message || "");
130
  break;
131
  case "pro_required":
132
- openProModal([]);
133
  break;
134
  case "api_error":
135
  toast.error(message || "An error occurred");
@@ -146,30 +212,22 @@ export const AskAi = ({
146
  if (refThink.current) {
147
  refThink.current.scrollTop = refThink.current.scrollHeight;
148
  }
149
- // Auto-open dropdown when thinking content appears
150
- if (thinkingContent && isThinking && !openThink) {
151
- setOpenThink(true);
152
- }
153
- // Auto-collapse when thinking is complete
154
- if (thinkingContent && !isThinking && openThink) {
155
  setOpenThink(false);
156
  }
157
- }, [thinkingContent, isThinking]);
158
 
159
- const randomPrompt = () => {
160
- setRandomPromptLoading(true);
161
- setTimeout(() => {
162
- setPrompt(
163
- PROMPTS_FOR_AI[Math.floor(Math.random() * PROMPTS_FOR_AI.length)]
164
- );
165
- setRandomPromptLoading(false);
166
- }, 400);
167
- };
168
 
169
  return (
170
- <div className="p-3 w-full">
171
- <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">
172
- {thinkingContent && (
173
  <div className="w-full border-b border-neutral-700 relative overflow-hidden">
174
  <header
175
  className="flex items-center justify-between px-5 py-2.5 group hover:bg-neutral-600/20 transition-colors duration-200 cursor-pointer"
@@ -201,7 +259,7 @@ export const AskAi = ({
201
  )}
202
  >
203
  <p className="text-[13px] text-neutral-400 whitespace-pre-line px-5 pb-4 pt-3">
204
- {thinkingContent}
205
  </p>
206
  </main>
207
  </div>
@@ -210,7 +268,7 @@ export const AskAi = ({
210
  files={selectedFiles}
211
  isAiWorking={isAiWorking}
212
  onDelete={(file) =>
213
- setSelectedFiles(selectedFiles.filter((f) => f !== file))
214
  }
215
  />
216
  {selectedElement && (
@@ -222,58 +280,98 @@ export const AskAi = ({
222
  />
223
  </div>
224
  )}
225
- {redesignData && (
226
- <div className="px-4 pt-3">
227
- <SelectedRedesignUrl
228
- url={redesignData.url}
229
- isAiWorking={isAiWorking}
230
- onDelete={() => setRedesignData(undefined)}
231
- />
232
- </div>
233
- )}
234
  <div className="w-full relative flex items-center justify-between">
235
- {(isAiWorking || isUploading || isThinking || isLoadingProject) && (
236
  <div className="absolute bg-neutral-800 top-0 left-4 w-[calc(100%-30px)] h-full z-1 flex items-start pt-3.5 justify-between max-lg:text-sm">
237
- <AiLoading
238
- text={
239
- isLoadingProject
240
- ? "Fetching your project..."
241
- : isUploading
242
- ? "Uploading images..."
243
- : isAiWorking && !isSameHtml
244
- ? "DeepSite is working..."
245
- : "DeepSite is working..."
246
- }
247
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  {isAiWorking && (
249
- <Button
250
- size="iconXs"
251
- variant="outline"
252
- className="!rounded-md mr-0.5"
253
- onClick={cancelRequest}
254
  >
255
- <CircleStop className="size-4" />
256
- </Button>
 
257
  )}
258
  </div>
259
  )}
260
  <textarea
261
- disabled={
262
- isAiWorking || isUploading || isThinking || isLoadingProject
263
- }
264
  className={classNames(
265
  "w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4 resize-none",
266
  {
267
- "!pt-2.5":
268
- selectedElement &&
269
- !(isAiWorking || isUploading || isThinking),
270
  }
271
  )}
272
  placeholder={
273
  selectedElement
274
  ? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
275
- : redesignData
276
- ? "Ask DeepSite anything about the redesign of your site..."
277
  : isFollowUp && (!isSameHtml || pages?.length > 1)
278
  ? "Ask DeepSite for edits"
279
  : "Ask DeepSite anything..."
@@ -286,68 +384,117 @@ export const AskAi = ({
286
  }
287
  }}
288
  />
289
- {isNew && !isAiWorking && isSameHtml && (
290
- <Button
291
- size="iconXs"
292
- variant="outline"
293
- className="!rounded-md -translate-y-2 -translate-x-4"
294
- onClick={() => randomPrompt()}
295
- >
296
- <Dice6
297
- className={classNames("size-4", {
298
- "animate-spin animation-duration-500": randomPromptLoading,
299
- })}
300
- />
301
- </Button>
302
- )}
303
  </div>
304
  <div className="flex items-center justify-between gap-2 px-4 pb-3 mt-2">
305
- <div className="flex-1 flex items-center justify-start gap-1.5 flex-wrap">
306
- {isNew ? (
307
- <PromptBuilder
308
- enhancedSettings={enhancedSettings!}
309
- setEnhancedSettings={setEnhancedSettings}
310
- />
311
- ) : (
312
- <Context />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  )}
 
 
 
314
  <Settings
 
 
 
 
315
  open={openProvider}
316
  error={providerError}
317
  isFollowUp={!isSameHtml && isFollowUp}
318
  onClose={setOpenProvider}
319
  />
320
- {!isNew && <Uploader project={project} />}
321
- {isNew && (
322
- <ReImagine
323
- onRedesign={(md, url) =>
324
- setRedesignData({ markdown: md, url: url })
325
- }
326
- />
327
- )}
328
- {!isNew && !isSameHtml && <Selector />}
329
- </div>
330
- <div className="flex items-center justify-end gap-2">
331
  <Button
332
  size="iconXs"
333
- variant="outline"
334
- className="!rounded-md"
335
- disabled={
336
- !!redesignData?.url?.trim()
337
- ? false
338
- : isAiWorking || isUploading || isThinking || !prompt.trim()
339
- }
340
- onClick={() => callAi(redesignData?.markdown)}
341
  >
342
  <ArrowUp className="size-4" />
343
  </Button>
344
  </div>
345
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  </div>
347
  <audio ref={hookAudio} id="audio" className="hidden">
348
- <source src="/deepsite/success.mp3" type="audio/mpeg" />
349
  Your browser does not support the audio element.
350
  </audio>
351
  </div>
352
  );
353
- };
 
1
+ "use client";
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ import { useState, useMemo, useRef } 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, Page, Project } 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
+ import { useCallAi } from "@/hooks/useCallAi";
26
+ import { SelectedFiles } from "./selected-files";
27
+ import { Uploader } from "./uploader";
28
 
29
+ export function AskAI({
 
30
  isNew,
31
+ project,
32
+ images,
33
+ currentPage,
34
+ previousPrompts,
35
  onScrollToBottom,
36
+ isAiWorking,
37
+ setisAiWorking,
38
+ isEditableModeEnabled = false,
39
+ pages,
40
+ htmlHistory,
41
+ selectedElement,
42
+ setSelectedElement,
43
+ selectedFiles,
44
+ setSelectedFiles,
45
+ setIsEditableModeEnabled,
46
+ onNewPrompt,
47
+ onSuccess,
48
+ setPages,
49
+ setCurrentPage,
50
  }: {
51
+ project?: Project | null;
52
+ currentPage: Page;
53
+ images?: string[];
54
+ pages: Page[];
55
+ onScrollToBottom: () => void;
56
+ previousPrompts: string[];
57
+ isAiWorking: boolean;
58
+ onNewPrompt: (prompt: string) => void;
59
+ htmlHistory?: HtmlHistory[];
60
+ setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
61
  isNew?: boolean;
62
+ onSuccess: (page: Page[], p: string, n?: number[][]) => void;
63
+ isEditableModeEnabled: boolean;
64
+ setIsEditableModeEnabled: React.Dispatch<React.SetStateAction<boolean>>;
65
+ selectedElement?: HTMLElement | null;
66
+ setSelectedElement: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
67
+ selectedFiles: string[];
68
+ setSelectedFiles: React.Dispatch<React.SetStateAction<string[]>>;
69
+ setPages: React.Dispatch<React.SetStateAction<Page[]>>;
70
+ setCurrentPage: React.Dispatch<React.SetStateAction<string>>;
71
+ }) {
72
+ const refThink = useRef<HTMLDivElement | null>(null);
73
+
74
+ const [open, setOpen] = useState(false);
75
+ const [prompt, setPrompt] = useState("");
76
+ const [provider, setProvider] = useLocalStorage("provider", "auto");
77
+ const [model, setModel] = useLocalStorage("model", MODELS[0].value);
 
 
 
78
  const [openProvider, setOpenProvider] = useState(false);
79
  const [providerError, setProviderError] = useState("");
80
+ const [openProModal, setOpenProModal] = useState(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  const [openThink, setOpenThink] = useState(false);
82
+ const [isThinking, setIsThinking] = useState(true);
83
+ const [think, setThink] = useState("");
84
+ const [isFollowUp, setIsFollowUp] = useState(true);
85
+ const [isUploading, setIsUploading] = useState(false);
86
+ const [files, setFiles] = useState<string[]>(images ?? []);
87
 
88
+ const {
89
+ callAiNewProject,
90
+ callAiFollowUp,
91
+ callAiNewPage,
92
+ stopController,
93
+ audio: hookAudio,
94
+ } = useCallAi({
95
+ onNewPrompt,
96
+ onSuccess,
97
+ onScrollToBottom,
98
+ setPages,
99
+ setCurrentPage,
100
+ currentPage,
101
+ pages,
102
+ isAiWorking,
103
+ setisAiWorking,
104
  });
105
 
106
+ const selectedModel = useMemo(() => {
107
+ return MODELS.find((m: { value: string }) => m.value === model);
108
+ }, [model]);
109
+
110
+ const callAi = async (redesignMarkdown?: string) => {
111
  if (isAiWorking) return;
112
  if (!redesignMarkdown && !prompt.trim()) return;
113
 
114
  if (isFollowUp && !redesignMarkdown && !isSameHtml) {
115
+ // Use follow-up function for existing projects
116
+ const selectedElementHtml = selectedElement
117
+ ? selectedElement.outerHTML
118
+ : "";
119
+
120
+ const result = await callAiFollowUp(
121
+ prompt,
122
+ model,
123
+ provider,
124
+ previousPrompts,
125
+ selectedElementHtml,
126
+ selectedFiles
127
+ );
128
 
129
  if (result?.error) {
130
  handleError(result.error, result.message);
131
  return;
132
  }
133
 
134
+ if (result?.success) {
135
+ setPrompt("");
136
+ }
137
+ } else if (isFollowUp && pages.length > 1 && isSameHtml) {
138
+ const result = await callAiNewPage(
139
+ prompt,
140
+ model,
141
+ provider,
142
+ currentPage.path,
143
+ [
144
+ ...(previousPrompts ?? []),
145
+ ...(htmlHistory?.map((h) => h.prompt) ?? []),
146
+ ]
147
+ );
148
+ if (result?.error) {
149
+ handleError(result.error, result.message);
150
+ return;
151
+ }
152
+
153
  if (result?.success) {
154
  setPrompt("");
155
  }
156
  } else {
157
  const result = await callAiNewProject(
158
  prompt,
159
+ model,
160
+ provider,
161
  redesignMarkdown,
162
+ handleThink,
163
+ () => {
164
+ setIsThinking(false);
165
+ }
166
  );
167
 
168
  if (result?.error) {
 
172
 
173
  if (result?.success) {
174
  setPrompt("");
175
+ if (selectedModel?.isThinker) {
176
+ setModel(MODELS[0].value);
177
+ }
178
  }
179
  }
180
  };
181
 
182
+ const handleThink = (think: string) => {
183
+ setThink(think);
184
+ setIsThinking(true);
185
+ setOpenThink(true);
186
+ };
187
+
188
  const handleError = (error: string, message?: string) => {
189
  switch (error) {
190
  case "login_required":
191
+ setOpen(true);
192
  break;
193
  case "provider_required":
194
  setOpenProvider(true);
195
  setProviderError(message || "");
196
  break;
197
  case "pro_required":
198
+ setOpenProModal(true);
199
  break;
200
  case "api_error":
201
  toast.error(message || "An error occurred");
 
212
  if (refThink.current) {
213
  refThink.current.scrollTop = refThink.current.scrollHeight;
214
  }
215
+ }, [think]);
216
+
217
+ useUpdateEffect(() => {
218
+ if (!isThinking) {
 
 
219
  setOpenThink(false);
220
  }
221
+ }, [isThinking]);
222
 
223
+ const isSameHtml = useMemo(() => {
224
+ return isTheSameHtml(currentPage.html);
225
+ }, [currentPage.html]);
 
 
 
 
 
 
226
 
227
  return (
228
+ <div className="px-3">
229
+ <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">
230
+ {think && (
231
  <div className="w-full border-b border-neutral-700 relative overflow-hidden">
232
  <header
233
  className="flex items-center justify-between px-5 py-2.5 group hover:bg-neutral-600/20 transition-colors duration-200 cursor-pointer"
 
259
  )}
260
  >
261
  <p className="text-[13px] text-neutral-400 whitespace-pre-line px-5 pb-4 pt-3">
262
+ {think}
263
  </p>
264
  </main>
265
  </div>
 
268
  files={selectedFiles}
269
  isAiWorking={isAiWorking}
270
  onDelete={(file) =>
271
+ setSelectedFiles((prev) => prev.filter((f) => f !== file))
272
  }
273
  />
274
  {selectedElement && (
 
280
  />
281
  </div>
282
  )}
 
 
 
 
 
 
 
 
 
283
  <div className="w-full relative flex items-center justify-between">
284
+ {(isAiWorking || isUploading) && (
285
  <div className="absolute bg-neutral-800 top-0 left-4 w-[calc(100%-30px)] h-full z-1 flex items-start pt-3.5 justify-between max-lg:text-sm">
286
+ <div className="flex items-center justify-start gap-2">
287
+ <Loading overlay={false} className="!size-4 opacity-50" />
288
+ <p className="text-neutral-400 text-sm">
289
+ {isUploading ? (
290
+ "Uploading images..."
291
+ ) : isAiWorking && !isSameHtml ? (
292
+ "AI is working..."
293
+ ) : (
294
+ <span className="inline-flex">
295
+ {[
296
+ "D",
297
+ "e",
298
+ "e",
299
+ "p",
300
+ "S",
301
+ "i",
302
+ "t",
303
+ "e",
304
+ " ",
305
+ "i",
306
+ "s",
307
+ " ",
308
+ "T",
309
+ "h",
310
+ "i",
311
+ "n",
312
+ "k",
313
+ "i",
314
+ "n",
315
+ "g",
316
+ ".",
317
+ ".",
318
+ ".",
319
+ " ",
320
+ "W",
321
+ "a",
322
+ "i",
323
+ "t",
324
+ " ",
325
+ "a",
326
+ " ",
327
+ "m",
328
+ "o",
329
+ "m",
330
+ "e",
331
+ "n",
332
+ "t",
333
+ ".",
334
+ ".",
335
+ ".",
336
+ ].map((char, index) => (
337
+ <span
338
+ key={index}
339
+ className="bg-gradient-to-r from-neutral-100 to-neutral-300 bg-clip-text text-transparent animate-pulse"
340
+ style={{
341
+ animationDelay: `${index * 0.1}s`,
342
+ animationDuration: "1.3s",
343
+ animationIterationCount: "infinite",
344
+ }}
345
+ >
346
+ {char === " " ? "\u00A0" : char}
347
+ </span>
348
+ ))}
349
+ </span>
350
+ )}
351
+ </p>
352
+ </div>
353
  {isAiWorking && (
354
+ <div
355
+ 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"
356
+ onClick={stopController}
 
 
357
  >
358
+ <FaStopCircle />
359
+ Stop generation
360
+ </div>
361
  )}
362
  </div>
363
  )}
364
  <textarea
365
+ disabled={isAiWorking}
 
 
366
  className={classNames(
367
  "w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4 resize-none",
368
  {
369
+ "!pt-2.5": selectedElement && !isAiWorking,
 
 
370
  }
371
  )}
372
  placeholder={
373
  selectedElement
374
  ? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
 
 
375
  : isFollowUp && (!isSameHtml || pages?.length > 1)
376
  ? "Ask DeepSite for edits"
377
  : "Ask DeepSite anything..."
 
384
  }
385
  }}
386
  />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  </div>
388
  <div className="flex items-center justify-between gap-2 px-4 pb-3 mt-2">
389
+ <div className="flex-1 flex items-center justify-start gap-1.5">
390
+ <Uploader
391
+ pages={pages}
392
+ onLoading={setIsUploading}
393
+ isLoading={isUploading}
394
+ onFiles={setFiles}
395
+ onSelectFile={(file) => {
396
+ if (selectedFiles.includes(file)) {
397
+ setSelectedFiles((prev) => prev.filter((f) => f !== file));
398
+ } else {
399
+ setSelectedFiles((prev) => [...prev, file]);
400
+ }
401
+ }}
402
+ files={files}
403
+ selectedFiles={selectedFiles}
404
+ project={project}
405
+ />
406
+ {isNew && <ReImagine onRedesign={(md) => callAi(md)} />}
407
+ {!isSameHtml && (
408
+ <Tooltip>
409
+ <TooltipTrigger asChild>
410
+ <Button
411
+ size="xs"
412
+ variant={isEditableModeEnabled ? "default" : "outline"}
413
+ onClick={() => {
414
+ setIsEditableModeEnabled?.(!isEditableModeEnabled);
415
+ }}
416
+ className={classNames("h-[28px]", {
417
+ "!text-neutral-400 hover:!text-neutral-200 !border-neutral-600 !hover:!border-neutral-500":
418
+ !isEditableModeEnabled,
419
+ })}
420
+ >
421
+ <Crosshair className="size-4" />
422
+ Edit
423
+ </Button>
424
+ </TooltipTrigger>
425
+ <TooltipContent
426
+ align="start"
427
+ className="bg-neutral-950 text-xs text-neutral-200 py-1 px-2 rounded-md -translate-y-0.5"
428
+ >
429
+ Select an element on the page to ask DeepSite edit it
430
+ directly.
431
+ </TooltipContent>
432
+ </Tooltip>
433
  )}
434
+ {/* <InviteFriends /> */}
435
+ </div>
436
+ <div className="flex items-center justify-end gap-2">
437
  <Settings
438
+ provider={provider as string}
439
+ model={model as string}
440
+ onChange={setProvider}
441
+ onModelChange={setModel}
442
  open={openProvider}
443
  error={providerError}
444
  isFollowUp={!isSameHtml && isFollowUp}
445
  onClose={setOpenProvider}
446
  />
 
 
 
 
 
 
 
 
 
 
 
447
  <Button
448
  size="iconXs"
449
+ disabled={isAiWorking || !prompt.trim()}
450
+ onClick={() => callAi()}
 
 
 
 
 
 
451
  >
452
  <ArrowUp className="size-4" />
453
  </Button>
454
  </div>
455
  </div>
456
+ <LoginModal open={open} onClose={() => setOpen(false)} pages={pages} />
457
+ <ProModal
458
+ pages={pages}
459
+ open={openProModal}
460
+ onClose={() => setOpenProModal(false)}
461
+ />
462
+ {pages.length === 1 && (
463
+ <div className="border border-sky-500/20 bg-sky-500/40 hover:bg-sky-600 transition-all duration-200 text-sky-500 pl-2 pr-4 py-1.5 text-xs rounded-full absolute top-0 -translate-y-[calc(100%+8px)] left-0 max-w-max flex items-center justify-start gap-2">
464
+ <span className="rounded-full text-[10px] font-semibold bg-white text-neutral-900 px-1.5 py-0.5">
465
+ NEW
466
+ </span>
467
+ <p className="text-sm text-neutral-100">
468
+ DeepSite can now create multiple pages at once. Try it!
469
+ </p>
470
+ </div>
471
+ )}
472
+ {!isSameHtml && (
473
+ <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">
474
+ <label
475
+ htmlFor="diff-patch-checkbox"
476
+ className="flex items-center gap-1.5 cursor-pointer"
477
+ >
478
+ <Checkbox
479
+ id="diff-patch-checkbox"
480
+ checked={isFollowUp}
481
+ onCheckedChange={(e) => {
482
+ if (e === true && !isSameHtml && selectedModel?.isThinker) {
483
+ setModel(MODELS[0].value);
484
+ }
485
+ setIsFollowUp(e === true);
486
+ }}
487
+ />
488
+ Diff-Patch Update
489
+ </label>
490
+ <FollowUpTooltip />
491
+ </div>
492
+ )}
493
  </div>
494
  <audio ref={hookAudio} id="audio" className="hidden">
495
+ <source src="/success.mp3" type="audio/mpeg" />
496
  Your browser does not support the audio element.
497
  </audio>
498
  </div>
499
  );
500
+ }
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 transparent-scroll">
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
- };