README.md CHANGED
@@ -7,7 +7,6 @@ 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
@@ -19,10 +18,12 @@ models:
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.
 
 
 
 
 
7
  pinned: true
8
  app_port: 3000
9
  license: mit
 
10
  short_description: Generate any application by Vibe Coding
11
  models:
12
  - deepseek-ai/DeepSeek-V3-0324
 
18
  - moonshotai/Kimi-K2-Instruct
19
  - moonshotai/Kimi-K2-Instruct-0905
20
  - zai-org/GLM-4.6
 
 
21
  ---
22
 
23
  # DeepSite 🐳
24
 
25
  DeepSite is a 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.
26
+
27
+ ## How to use it locally
28
+
29
+ Follow [this discussion](https://huggingface.co/spaces/enzostvs/deepsite/discussions/74)
app/api/ask/route.ts CHANGED
@@ -7,12 +7,11 @@ import { InferenceClient } from "@huggingface/inference";
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";
@@ -22,8 +21,7 @@ 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;
@@ -63,7 +61,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 +78,14 @@ export async function POST(request: NextRequest) {
80
  billTo = "huggingface";
81
  }
82
 
 
 
 
 
 
 
 
 
83
  try {
84
  const encoder = new TextEncoder();
85
  const stream = new TransformStream();
@@ -94,27 +100,26 @@ 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).
@@ -122,9 +127,7 @@ export async function POST(request: NextRequest) {
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
  );
@@ -243,7 +246,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,6 +263,8 @@ export async function PUT(request: NextRequest) {
260
  billTo = "huggingface";
261
  }
262
 
 
 
263
  try {
264
  const encoder = new TextEncoder();
265
  const stream = new TransformStream();
@@ -277,10 +282,7 @@ export async function PUT(request: NextRequest) {
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 || [];
@@ -293,9 +295,14 @@ export async function PUT(request: NextRequest) {
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",
@@ -314,9 +321,7 @@ export async function PUT(request: NextRequest) {
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
  );
 
7
  import { MODELS } from "@/lib/providers";
8
  import {
9
  FOLLOW_UP_SYSTEM_PROMPT,
 
10
  INITIAL_SYSTEM_PROMPT,
 
11
  MAX_REQUESTS_PER_IP,
12
  PROMPT_FOR_PROJECT_NAME,
13
  } from "@/lib/prompts";
14
+ import { calculateMaxTokens, estimateInputTokens, getProviderSpecificConfig } from "@/lib/max-tokens";
15
  import MY_TOKEN_KEY from "@/lib/get-cookie-name";
16
  import { Page } from "@/types";
17
  import { isAuthenticated } from "@/lib/auth";
 
21
 
22
  export async function POST(request: NextRequest) {
23
  const authHeaders = await headers();
24
+ const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
 
25
 
26
  const body = await request.json();
27
  const { prompt, provider, model, redesignMarkdown, enhancedSettings, pages } = body;
 
61
  ? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
62
  : authHeaders.get("x-forwarded-for");
63
 
64
+ if (!token) {
65
  ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
66
  if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
67
  return NextResponse.json(
 
78
  billTo = "huggingface";
79
  }
80
 
81
+ const selectedProvider = await getBestProvider(selectedModel.value, provider)
82
+
83
+ let rewrittenPrompt = redesignMarkdown ? `Here is my current design as a markdown:\n\n${redesignMarkdown}\n\nNow, please create a new design based on this markdown. Use the images in the markdown.` : prompt;
84
+
85
+ if (enhancedSettings.isActive) {
86
+ // rewrittenPrompt = await rewritePrompt(rewrittenPrompt, enhancedSettings, { token, billTo }, selectedModel.value, selectedProvider.provider);
87
+ }
88
+
89
  try {
90
  const encoder = new TextEncoder();
91
  const stream = new TransformStream();
 
100
  });
101
 
102
  (async () => {
103
+ // let completeResponse = "";
104
  try {
105
  const client = new InferenceClient(token);
106
 
107
+ const systemPrompt = INITIAL_SYSTEM_PROMPT;
108
+
109
+ const userPrompt = rewrittenPrompt;
110
+ const estimatedInputTokens = estimateInputTokens(systemPrompt, userPrompt);
111
+ const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, true);
112
+ const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens);
113
 
 
 
114
  const chatCompletion = client.chatCompletionStream(
115
  {
116
+ model: selectedModel.value,
117
+ provider: selectedProvider.provider,
118
  messages: [
119
  {
120
  role: "system",
121
  content: systemPrompt,
122
  },
 
 
 
 
123
  {
124
  role: "user",
125
  content: userPrompt + (enhancedSettings.isActive ? `1. I want to use the following primary color: ${enhancedSettings.primaryColor} (eg: bg-${enhancedSettings.primaryColor}-500).
 
127
  3. I want to use the following theme: ${enhancedSettings.theme} mode.` : "")
128
  },
129
  ],
130
+ ...providerConfig,
 
 
131
  },
132
  billTo ? { billTo } : {}
133
  );
 
246
  ? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
247
  : authHeaders.get("x-forwarded-for");
248
 
249
+ if (!token) {
250
  ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
251
  if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
252
  return NextResponse.json(
 
263
  billTo = "huggingface";
264
  }
265
 
266
+ const selectedProvider = await getBestProvider(selectedModel.value, provider);
267
+
268
  try {
269
  const encoder = new TextEncoder();
270
  const stream = new TransformStream();
 
282
  try {
283
  const client = new InferenceClient(token);
284
 
285
+ const systemPrompt = FOLLOW_UP_SYSTEM_PROMPT + (isNew ? PROMPT_FOR_PROJECT_NAME : "");
 
 
 
286
  const userContext = "You are modifying the HTML file based on the user's request.";
287
 
288
  const allPages = pages || [];
 
295
  : ""
296
  }. Current pages (${allPages.length} total): ${pagesContext}. ${files?.length > 0 ? `Available images: ${files?.map((f: string) => f).join(', ')}.` : ""}`;
297
 
298
+ const estimatedInputTokens = estimateInputTokens(systemPrompt, prompt, userContext + assistantContext);
299
+ const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, false);
300
+ const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens);
301
+
302
  const chatCompletion = client.chatCompletionStream(
303
  {
304
+ model: selectedModel.value,
305
+ provider: selectedProvider.provider,
306
  messages: [
307
  {
308
  role: "system",
 
321
  content: prompt,
322
  },
323
  ],
324
+ ...providerConfig,
 
 
325
  },
326
  billTo ? { billTo } : {}
327
  );
app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts CHANGED
@@ -1,5 +1,5 @@
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";
@@ -49,13 +49,13 @@ export async function POST(
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,
@@ -63,24 +63,26 @@ export async function POST(
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";
@@ -91,62 +93,18 @@ export async function POST(
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,
@@ -160,6 +118,7 @@ export async function POST(
160
  }
161
  }
162
 
 
163
  const filesToDelete: string[] = [];
164
  for (const mainFilePath of mainBranchFilePaths) {
165
  if (!commitFilePaths.has(mainFilePath)) {
@@ -174,6 +133,7 @@ export async function POST(
174
  );
175
  }
176
 
 
177
  if (filesToDelete.length > 0) {
178
  await deleteFiles({
179
  repo,
@@ -184,6 +144,7 @@ export async function POST(
184
  });
185
  }
186
 
 
187
  if (files.length > 0) {
188
  await uploadFiles({
189
  repo,
@@ -200,7 +161,6 @@ export async function POST(
200
  message: "Version promoted successfully",
201
  promotedCommit: commitId,
202
  pages: pages,
203
- files: mediaFiles,
204
  },
205
  { status: 200 }
206
  );
 
1
  import { NextRequest, NextResponse } from "next/server";
2
+ import { RepoDesignation, listFiles, spaceInfo, uploadFiles, deleteFiles } from "@huggingface/hub";
3
 
4
  import { isAuthenticated } from "@/lib/auth";
5
  import { Page } from "@/types";
 
49
  );
50
  }
51
 
52
+ // Fetch files from the specific commit
53
  const files: File[] = [];
54
  const pages: Page[] = [];
 
55
  const allowedExtensions = ["html", "md", "css", "js", "json", "txt"];
 
56
  const commitFilePaths: Set<string> = new Set();
57
 
58
+ // Get all files from the specific commit
59
  for await (const fileInfo of listFiles({
60
  repo,
61
  accessToken: user.token as string,
 
63
  })) {
64
  const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
65
 
66
+ if (allowedExtensions.includes(fileExtension || "")) {
67
  commitFilePaths.add(fileInfo.path);
68
 
69
+ // Fetch the file content from the specific commit
70
+ const response = await fetch(
71
+ `https://huggingface.co/spaces/${namespace}/${repoId}/raw/${commitId}/${fileInfo.path}`
72
+ );
 
 
 
 
73
 
74
+ if (response.ok) {
75
+ const content = await response.text();
76
  let mimeType = "text/plain";
77
 
78
  switch (fileExtension) {
79
  case "html":
80
  mimeType = "text/html";
81
+ // Add HTML files to pages array for client-side setPages
82
+ pages.push({
83
+ path: fileInfo.path,
84
+ html: content,
85
+ });
86
  break;
87
  case "css":
88
  mimeType = "text/css";
 
93
  case "json":
94
  mimeType = "application/json";
95
  break;
96
+ case "md":
97
+ mimeType = "text/markdown";
98
+ break;
 
 
 
 
 
 
 
 
 
99
  }
100
 
101
  const file = new File([content], fileInfo.path, { type: mimeType });
102
  files.push(file);
103
  }
104
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  }
106
 
107
+ // Get files currently in main branch to identify files to delete
108
  const mainBranchFilePaths: Set<string> = new Set();
109
  for await (const fileInfo of listFiles({
110
  repo,
 
118
  }
119
  }
120
 
121
+ // Identify files to delete (exist in main but not in commit)
122
  const filesToDelete: string[] = [];
123
  for (const mainFilePath of mainBranchFilePaths) {
124
  if (!commitFilePaths.has(mainFilePath)) {
 
133
  );
134
  }
135
 
136
+ // Delete files that exist in main but not in the commit being promoted
137
  if (filesToDelete.length > 0) {
138
  await deleteFiles({
139
  repo,
 
144
  });
145
  }
146
 
147
+ // Upload the files to the main branch with a promotion commit message
148
  if (files.length > 0) {
149
  await uploadFiles({
150
  repo,
 
161
  message: "Version promoted successfully",
162
  promotedCommit: commitId,
163
  pages: pages,
 
164
  },
165
  { status: 200 }
166
  );
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]/route.ts CHANGED
@@ -108,7 +108,7 @@ export async function GET(
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) {
 
108
  const allowedFilesExtensions = ["jpg", "jpeg", "png", "gif", "svg", "webp", "avif", "heic", "heif", "ico", "bmp", "tiff", "tif", "mp4", "webm", "ogg", "avi", "mov", "mp3", "wav", "ogg", "aac", "m4a"];
109
 
110
  for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
111
+ if (fileInfo.path.endsWith(".html") || fileInfo.path.endsWith(".css") || fileInfo.path.endsWith(".js")) {
112
  const blob = await downloadFile({ repo, accessToken: user.token as string, path: fileInfo.path, raw: true });
113
  const html = await blob?.text();
114
  if (!html) {
app/layout.tsx CHANGED
@@ -1,18 +1,20 @@
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",
@@ -76,24 +78,6 @@ export default async function RootLayout({
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
@@ -130,6 +114,7 @@ export default async function RootLayout({
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>
 
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
  import Script from "next/script";
 
 
6
 
7
  import "@/assets/globals.css";
8
  import { Toaster } from "@/components/ui/sonner";
9
+ import MY_TOKEN_KEY from "@/lib/get-cookie-name";
10
+ import { apiServer } from "@/lib/api";
11
  import IframeDetector from "@/components/iframe-detector";
12
  import AppContext from "@/components/contexts/app-context";
13
  import TanstackContext from "@/components/contexts/tanstack-query-context";
14
  import { LoginProvider } from "@/components/contexts/login-context";
15
  import { ProProvider } from "@/components/contexts/pro-context";
16
  import { generateSEO, generateStructuredData } from "@/lib/seo";
17
+ import DomainRedirect from "@/components/domain-redirect";
18
 
19
  const inter = Inter({
20
  variable: "--font-inter-sans",
 
78
  }: Readonly<{
79
  children: React.ReactNode;
80
  }>) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  // const data = await getMe();
82
 
83
  // Generate structured data
 
114
  data-domain="deepsite.hf.co"
115
  src="https://plausible.io/js/script.js"
116
  />
117
+ <DomainRedirect />
118
  <IframeDetector />
119
  <Toaster richColors position="bottom-center" />
120
  <TanstackContext>
assets/globals.css CHANGED
@@ -369,12 +369,3 @@ body {
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
- }
 
369
  z-index: 1;
370
  filter: blur(1px);
371
  }
 
 
 
 
 
 
 
 
 
assets/minimax.svg DELETED
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/domain-redirect/index.tsx ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+
5
+ export default function DomainRedirect() {
6
+ useEffect(() => {
7
+ if (typeof window === "undefined") return;
8
+
9
+ const host = window.location.host;
10
+
11
+ // Check if we're not on hf.co or huggingface.co
12
+ const isHfCo = host === "hf.co" || host.startsWith("hf.co:");
13
+ const isHuggingFaceCo =
14
+ host === "huggingface.co" || host.startsWith("huggingface.co:");
15
+
16
+ if (!isHfCo && !isHuggingFaceCo) {
17
+ // Redirect to the correct URL
18
+ window.location.replace("https://huggingface.co/deepsite");
19
+ }
20
+ }, []);
21
+
22
+ return null;
23
+ }
components/editor/ask-ai/context.tsx CHANGED
@@ -23,8 +23,6 @@ export const Context = () => {
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
  }
@@ -54,8 +52,6 @@ export const Context = () => {
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
 
23
  return <Braces className={size} />;
24
  } else if (filePath.endsWith(".js")) {
25
  return <FileCode className={size} />;
 
 
26
  } else {
27
  return <FileText className={size} />;
28
  }
 
52
  selectedFile && selectedFile.endsWith(".html"),
53
  "!bg-amber-500/10 !border-amber-500/30 !text-amber-400":
54
  selectedFile && selectedFile.endsWith(".js"),
 
 
55
  })}
56
  disabled={
57
  globalAiLoading || globalEditorLoading || pages.length === 0
components/editor/ask-ai/index.tsx CHANGED
@@ -22,7 +22,6 @@ 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,
@@ -39,7 +38,6 @@ export const AskAi = ({
39
  const {
40
  isAiWorking,
41
  isThinking,
42
- thinkingContent,
43
  selectedFiles,
44
  setSelectedFiles,
45
  selectedElement,
@@ -53,9 +51,6 @@ export const AskAi = ({
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] =
@@ -71,6 +66,7 @@ export const AskAi = ({
71
  const [prompt, setPrompt] = useState(
72
  promptStorage && promptStorage.trim() !== "" ? promptStorage : ""
73
  );
 
74
  const [openThink, setOpenThink] = useState(false);
75
  const [randomPromptLoading, setRandomPromptLoading] = useState(false);
76
 
@@ -80,7 +76,7 @@ export const AskAi = ({
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([]);
@@ -146,15 +142,7 @@ 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);
@@ -169,7 +157,7 @@ export const AskAi = ({
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 +189,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>
@@ -222,15 +210,6 @@ 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">
@@ -242,7 +221,7 @@ export const AskAi = ({
242
  ? "Uploading images..."
243
  : isAiWorking && !isSameHtml
244
  ? "DeepSite is working..."
245
- : "DeepSite is working..."
246
  }
247
  />
248
  {isAiWorking && (
@@ -272,8 +251,6 @@ export const AskAi = ({
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..."
@@ -318,13 +295,7 @@ export const AskAi = ({
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">
@@ -333,11 +304,9 @@ export const AskAi = ({
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>
 
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
 
26
  export const AskAi = ({
27
  project,
 
38
  const {
39
  isAiWorking,
40
  isThinking,
 
41
  selectedFiles,
42
  setSelectedFiles,
43
  selectedElement,
 
51
  const { openProModal } = useProModal();
52
  const [openProvider, setOpenProvider] = useState(false);
53
  const [providerError, setProviderError] = useState("");
 
 
 
54
  const refThink = useRef<HTMLDivElement>(null);
55
 
56
  const [enhancedSettings, setEnhancedSettings, removeEnhancedSettings] =
 
66
  const [prompt, setPrompt] = useState(
67
  promptStorage && promptStorage.trim() !== "" ? promptStorage : ""
68
  );
69
+ const [think, setThink] = useState("");
70
  const [openThink, setOpenThink] = useState(false);
71
  const [randomPromptLoading, setRandomPromptLoading] = useState(false);
72
 
 
76
  }
77
  });
78
 
79
+ const callAi = async (redesignMarkdown?: string) => {
80
  removePromptStorage();
81
  if (user && !user.isPro && projects.length >= MAX_FREE_PROJECTS)
82
  return openProModal([]);
 
142
  if (refThink.current) {
143
  refThink.current.scrollTop = refThink.current.scrollHeight;
144
  }
145
+ }, [think]);
 
 
 
 
 
 
 
 
146
 
147
  const randomPrompt = () => {
148
  setRandomPromptLoading(true);
 
157
  return (
158
  <div className="p-3 w-full">
159
  <div className="relative bg-neutral-800 border border-neutral-700 rounded-2xl ring-[4px] focus-within:ring-neutral-500/30 focus-within:border-neutral-600 ring-transparent z-20 w-full group">
160
+ {think && (
161
  <div className="w-full border-b border-neutral-700 relative overflow-hidden">
162
  <header
163
  className="flex items-center justify-between px-5 py-2.5 group hover:bg-neutral-600/20 transition-colors duration-200 cursor-pointer"
 
189
  )}
190
  >
191
  <p className="text-[13px] text-neutral-400 whitespace-pre-line px-5 pb-4 pt-3">
192
+ {think}
193
  </p>
194
  </main>
195
  </div>
 
210
  />
211
  </div>
212
  )}
 
 
 
 
 
 
 
 
 
213
  <div className="w-full relative flex items-center justify-between">
214
  {(isAiWorking || isUploading || isThinking || isLoadingProject) && (
215
  <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">
 
221
  ? "Uploading images..."
222
  : isAiWorking && !isSameHtml
223
  ? "DeepSite is working..."
224
+ : "DeepSite is thinking..."
225
  }
226
  />
227
  {isAiWorking && (
 
251
  placeholder={
252
  selectedElement
253
  ? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
 
 
254
  : isFollowUp && (!isSameHtml || pages?.length > 1)
255
  ? "Ask DeepSite for edits"
256
  : "Ask DeepSite anything..."
 
295
  onClose={setOpenProvider}
296
  />
297
  {!isNew && <Uploader project={project} />}
298
+ {isNew && <ReImagine onRedesign={(md) => callAi(md)} />}
 
 
 
 
 
 
299
  {!isNew && !isSameHtml && <Selector />}
300
  </div>
301
  <div className="flex items-center justify-end gap-2">
 
304
  variant="outline"
305
  className="!rounded-md"
306
  disabled={
307
+ isAiWorking || isUploading || isThinking || !prompt.trim()
 
 
308
  }
309
+ onClick={() => callAi()}
310
  >
311
  <ArrowUp className="size-4" />
312
  </Button>
components/editor/ask-ai/prompt-builder/content-modal.tsx CHANGED
@@ -16,7 +16,7 @@ export const ContentModal = ({
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">
 
16
  }) => {
17
  const [collapsed, setCollapsed] = useState(["colors", "theme"]);
18
  return (
19
+ <main className="overflow-x-hidden max-h-[50dvh] overflow-y-auto">
20
  <section className="w-full border-b border-neutral-800/80 px-6 py-3.5 sticky top-0 bg-neutral-900 z-10">
21
  <div className="flex items-center justify-between gap-3">
22
  <p className="text-base font-semibold text-neutral-200">
components/editor/ask-ai/prompt-builder/tailwind-colors.tsx CHANGED
@@ -26,7 +26,7 @@ export const TailwindColors = ({
26
  return (
27
  <div
28
  ref={ref}
29
- className="flex items-center justify-start gap-3 overflow-x-auto px-5 transparent-scroll"
30
  >
31
  {TAILWIND_COLORS.map((color) => (
32
  <div
 
26
  return (
27
  <div
28
  ref={ref}
29
+ className="flex items-center justify-start gap-3 overflow-x-auto px-5 scrollbar-hide"
30
  >
31
  {TAILWIND_COLORS.map((color) => (
32
  <div
components/editor/ask-ai/re-imagine.tsx CHANGED
@@ -13,12 +13,11 @@ import Loading from "@/components/loading";
13
  import { api } from "@/lib/api";
14
  import { useAi } from "@/hooks/useAi";
15
  import { useEditor } from "@/hooks/useEditor";
16
- import classNames from "classnames";
17
 
18
  export function ReImagine({
19
  onRedesign,
20
  }: {
21
- onRedesign: (md: string, url: string) => void;
22
  }) {
23
  const [url, setUrl] = useState<string>("");
24
  const [open, setOpen] = useState(false);
@@ -50,8 +49,8 @@ export function ReImagine({
50
  });
51
  if (response?.data?.ok) {
52
  setOpen(false);
53
- onRedesign(response.data.markdown, url.trim());
54
  setUrl("");
 
55
  toast.success("DeepSite is redesigning your site! Let him cook... 🔥");
56
  } else {
57
  toast.error(response?.data?.error || "Failed to redesign the site.");
@@ -118,11 +117,6 @@ export function ReImagine({
118
  }
119
  setUrl(inputUrl);
120
  }}
121
- onKeyDown={(e) => {
122
- if (e.key === "Enter") {
123
- handleClick();
124
- }
125
- }}
126
  className="!bg-white !border-neutral-300 !text-neutral-800 !placeholder:text-neutral-400 selection:!bg-blue-100"
127
  />
128
  </div>
 
13
  import { api } from "@/lib/api";
14
  import { useAi } from "@/hooks/useAi";
15
  import { useEditor } from "@/hooks/useEditor";
 
16
 
17
  export function ReImagine({
18
  onRedesign,
19
  }: {
20
+ onRedesign: (md: string) => void;
21
  }) {
22
  const [url, setUrl] = useState<string>("");
23
  const [open, setOpen] = useState(false);
 
49
  });
50
  if (response?.data?.ok) {
51
  setOpen(false);
 
52
  setUrl("");
53
+ onRedesign(response.data.markdown);
54
  toast.success("DeepSite is redesigning your site! Let him cook... 🔥");
55
  } else {
56
  toast.error(response?.data?.error || "Failed to redesign the site.");
 
117
  }
118
  setUrl(inputUrl);
119
  }}
 
 
 
 
 
120
  className="!bg-white !border-neutral-300 !text-neutral-800 !placeholder:text-neutral-400 selection:!bg-blue-100"
121
  />
122
  </div>
components/editor/ask-ai/selected-redesign-url.tsx DELETED
@@ -1,37 +0,0 @@
1
- import classNames from "classnames";
2
- import { Paintbrush, XCircle } from "lucide-react";
3
-
4
- export const SelectedRedesignUrl = ({
5
- url,
6
- isAiWorking = false,
7
- onDelete,
8
- }: {
9
- url: string;
10
- isAiWorking: boolean;
11
- onDelete?: () => void;
12
- }) => {
13
- return (
14
- <div
15
- className={classNames(
16
- "border border-emerald-500/50 bg-emerald-500/10 rounded-xl p-1.5 pr-3 max-w-max hover:brightness-110 transition-all duration-200 ease-in-out",
17
- {
18
- "cursor-pointer": !isAiWorking,
19
- "opacity-50 cursor-not-allowed": isAiWorking,
20
- }
21
- )}
22
- onClick={() => {
23
- if (!isAiWorking && onDelete) {
24
- onDelete();
25
- }
26
- }}
27
- >
28
- <div className="flex items-center justify-start gap-2">
29
- <div className="rounded-lg bg-emerald-500/20 size-6 flex items-center justify-center">
30
- <Paintbrush className="text-emerald-300 size-3.5" />
31
- </div>
32
- <p className="text-sm font-semibold text-emerald-200">{url}</p>
33
- <XCircle className="text-emerald-300 size-4 hover:text-emerald-200 transition-colors" />
34
- </div>
35
- </div>
36
- );
37
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/settings.tsx CHANGED
@@ -20,14 +20,7 @@ import {
20
  import { useMemo, useState, useEffect } from "react";
21
  import { useUpdateEffect } from "react-use";
22
  import Image from "next/image";
23
- import {
24
- BrainIcon,
25
- CheckCheck,
26
- ChevronDown,
27
- Sparkles,
28
- Zap,
29
- DollarSign,
30
- } from "lucide-react";
31
  import { useAi } from "@/hooks/useAi";
32
  import { getProviders } from "@/lib/get-providers";
33
  import Loading from "@/components/loading";
@@ -69,10 +62,7 @@ export function Settings({
69
  // }, [model]);
70
 
71
  useUpdateEffect(() => {
72
- if (
73
- !["auto", "fastest", "cheapest"].includes(provider as string) &&
74
- !providers.includes(provider as string)
75
- ) {
76
  setProvider("auto");
77
  }
78
  }, [model, provider]);
@@ -215,83 +205,47 @@ export function Settings({
215
  </div>
216
  )} */}
217
  <div className="flex flex-col gap-3">
218
- <div>
219
- <p className="text-neutral-300 text-sm mb-1">Provider Mode</p>
220
- <p className="text-neutral-400 text-xs mb-3 leading-relaxed">
221
- Choose how we select providers:{" "}
222
- <span className="px-1.5 py-0.5 rounded bg-pink-500/10 text-pink-500">
223
- Auto
224
- </span>{" "}
225
- (smart),{" "}
226
- <span className="px-1.5 py-0.5 rounded bg-yellow-500/10 text-yellow-500">
227
- Fastest
228
- </span>{" "}
229
- (speed), or{" "}
230
- <span className="px-1.5 py-0.5 rounded bg-green-500/10 text-green-500">
231
- Cheapest
232
- </span>{" "}
233
- (cost).
234
- </p>
235
- <div className="grid grid-cols-3 gap-1 bg-neutral-800 p-1 rounded-full">
236
- <button
237
- className={classNames(
238
- "flex flex-col items-center justify-center cursor-pointer py-1.5 rounded-full transition-all duration-200",
239
- {
240
- "bg-white text-neutral-800": provider === "auto",
241
- "text-neutral-400 hover:text-neutral-200":
242
- provider !== "auto",
243
- }
244
- )}
245
- onClick={() => setProvider("auto")}
246
- >
247
- <Sparkles
248
- className={classNames("size-3.5 mb-0.5", {
249
- "text-pink-400": provider !== "auto",
250
- })}
251
- />
252
- <span className="text-[10px] font-medium">Auto</span>
253
- </button>
254
- <button
255
- className={classNames(
256
- "flex flex-col items-center justify-center cursor-pointer py-1.5 rounded-full transition-all duration-200",
257
- {
258
- "bg-white text-neutral-800": provider === "fastest",
259
- "text-neutral-400 hover:text-neutral-200":
260
- provider !== "fastest",
261
- }
262
- )}
263
- onClick={() => setProvider("fastest")}
264
- >
265
- <Zap
266
- className={classNames("size-3.5 mb-0.5", {
267
- "text-yellow-400": provider !== "fastest",
268
- })}
269
- />
270
- <span className="text-[10px] font-medium">Fastest</span>
271
- </button>
272
- <button
273
  className={classNames(
274
- "flex flex-col items-center justify-center cursor-pointer py-1.5 rounded-full transition-all duration-200",
275
  {
276
- "bg-white text-neutral-800": provider === "cheapest",
277
- "text-neutral-400 hover:text-neutral-200":
278
- provider !== "cheapest",
279
  }
280
  )}
281
- onClick={() => setProvider("cheapest")}
282
- >
283
- <DollarSign
284
- className={classNames("size-3.5 mb-0.5", {
285
- "text-green-400": provider !== "cheapest",
286
- })}
287
- />
288
- <span className="text-[10px] font-medium">Cheapest</span>
289
- </button>
290
  </div>
291
  </div>
292
  <label className="block">
293
  <p className="text-neutral-300 text-sm mb-2">
294
- Or choose a specific provider
295
  </p>
296
  <div className="grid grid-cols-2 gap-1.5 relative">
297
  {loadingProviders ? (
 
20
  import { useMemo, useState, useEffect } from "react";
21
  import { useUpdateEffect } from "react-use";
22
  import Image from "next/image";
23
+ import { Brain, BrainIcon, CheckCheck, ChevronDown } from "lucide-react";
 
 
 
 
 
 
 
24
  import { useAi } from "@/hooks/useAi";
25
  import { getProviders } from "@/lib/get-providers";
26
  import Loading from "@/components/loading";
 
62
  // }, [model]);
63
 
64
  useUpdateEffect(() => {
65
+ if (provider !== "auto" && !providers.includes(provider as string)) {
 
 
 
66
  setProvider("auto");
67
  }
68
  }, [model, provider]);
 
205
  </div>
206
  )} */}
207
  <div className="flex flex-col gap-3">
208
+ <div className="flex items-center justify-between">
209
+ <div>
210
+ <p className="text-neutral-300 text-sm mb-1.5">
211
+ Use auto-provider
212
+ </p>
213
+ <p className="text-xs text-neutral-400/70">
214
+ We&apos;ll automatically select the best provider for you
215
+ based on your prompt.
216
+ </p>
217
+ </div>
218
+ <div
219
+ className={classNames(
220
+ "bg-neutral-700 rounded-full min-w-10 w-10 h-6 flex items-center justify-between p-1 cursor-pointer transition-all duration-200",
221
+ {
222
+ "!bg-sky-500": provider === "auto",
223
+ }
224
+ )}
225
+ onClick={() => {
226
+ const foundModel = MODELS.find(
227
+ (m: { value: string }) => m.value === model
228
+ );
229
+ if (provider === "auto" && foundModel?.autoProvider) {
230
+ setProvider(foundModel.autoProvider);
231
+ } else {
232
+ setProvider("auto");
233
+ }
234
+ }}
235
+ >
236
+ <div
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  className={classNames(
238
+ "w-4 h-4 rounded-full shadow-md transition-all duration-200 bg-neutral-200",
239
  {
240
+ "translate-x-4": provider === "auto",
 
 
241
  }
242
  )}
243
+ />
 
 
 
 
 
 
 
 
244
  </div>
245
  </div>
246
  <label className="block">
247
  <p className="text-neutral-300 text-sm mb-2">
248
+ Inference Provider
249
  </p>
250
  <div className="grid grid-cols-2 gap-1.5 relative">
251
  {loadingProviders ? (
components/editor/file-browser/index.tsx CHANGED
@@ -7,7 +7,6 @@ import {
7
  Folder,
8
  ChevronRight,
9
  ChevronDown,
10
- FileJson,
11
  } from "lucide-react";
12
  import classNames from "classnames";
13
 
@@ -198,7 +197,27 @@ export function FileBrowser() {
198
  </svg>
199
  );
200
  case "json":
201
- return <FileJson className="size-4 shrink-0 text-amber-400" />;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  default:
203
  return <FileCode2 className="size-4 shrink-0 text-neutral-400" />;
204
  }
@@ -230,7 +249,7 @@ export function FileBrowser() {
230
  case "json":
231
  return {
232
  name: "JSON",
233
- color: "bg-amber-500/20 border-amber-500/30 text-amber-400",
234
  };
235
  default:
236
  return {
@@ -439,13 +458,6 @@ export function FileBrowser() {
439
  JS: {pages.filter((p) => p.path.endsWith(".js")).length}
440
  </span>
441
  </div>
442
- <div className="flex items-center gap-2 text-neutral-500">
443
- <div className="size-2 rounded-full bg-yellow-600" />
444
- <span>
445
- JSON:{" "}
446
- {pages.filter((p) => p.path.endsWith(".json")).length}
447
- </span>
448
- </div>
449
  </div>
450
  </div>
451
  </SheetContent>
 
7
  Folder,
8
  ChevronRight,
9
  ChevronDown,
 
10
  } from "lucide-react";
11
  import classNames from "classnames";
12
 
 
197
  </svg>
198
  );
199
  case "json":
200
+ return (
201
+ <svg className="size-4 shrink-0" viewBox="0 0 32 32" fill="none">
202
+ <rect width="32" height="32" rx="2" fill="#F7DF1E" />
203
+ <path
204
+ d="M16 2L4 8v16l12 6 12-6V8L16 2zm8.8 20.4l-8.8 4.4-8.8-4.4V9.6l8.8-4.4 8.8 4.4v12.8z"
205
+ fill="#000"
206
+ opacity="0.15"
207
+ />
208
+ <text
209
+ x="50%"
210
+ y="50%"
211
+ dominantBaseline="middle"
212
+ textAnchor="middle"
213
+ fill="#000"
214
+ fontSize="14"
215
+ fontWeight="600"
216
+ >
217
+ {}
218
+ </text>
219
+ </svg>
220
+ );
221
  default:
222
  return <FileCode2 className="size-4 shrink-0 text-neutral-400" />;
223
  }
 
249
  case "json":
250
  return {
251
  name: "JSON",
252
+ color: "bg-yellow-500/20 border-yellow-500/30 text-yellow-400",
253
  };
254
  default:
255
  return {
 
458
  JS: {pages.filter((p) => p.path.endsWith(".js")).length}
459
  </span>
460
  </div>
 
 
 
 
 
 
 
461
  </div>
462
  </div>
463
  </SheetContent>
components/editor/header/index.tsx CHANGED
@@ -23,9 +23,8 @@ import {
23
  TooltipContent,
24
  TooltipTrigger,
25
  } from "@/components/ui/tooltip";
26
- import { DiscordIcon } from "@/components/icons/discord";
27
 
28
- export function Header({ isNew }: { isNew: boolean }) {
29
  const { project } = useEditor();
30
  const { user, openLoginWindow } = useUser();
31
  return (
@@ -60,26 +59,24 @@ export function Header({ isNew }: { isNew: boolean }) {
60
  <div className="lg:w-full px-2 lg:px-3 py-2 flex items-center justify-end lg:justify-between lg:border-l lg:border-neutral-800">
61
  <div className="font-mono text-muted-foreground flex items-center gap-2">
62
  <SwitchDevice />
63
- {isNew && (
64
- <Button
65
- size="xs"
66
- variant="bordered"
67
- className="max-lg:hidden"
68
- onClick={() => {
69
- const iframe = document.getElementById(
70
- "preview-iframe"
71
- ) as HTMLIFrameElement;
72
- if (iframe) {
73
- iframe.src = iframe.src;
74
- }
75
- }}
76
- >
77
- <RefreshCcw className="size-3 mr-0.5" />
78
- Refresh Preview
79
- </Button>
80
- )}
81
  <Link
82
- href="https://discord.gg/KpanwM3vXa"
83
  target="_blank"
84
  className="max-lg:hidden"
85
  >
@@ -88,20 +85,6 @@ export function Header({ isNew }: { isNew: boolean }) {
88
  Help
89
  </Button>
90
  </Link>
91
- <Link
92
- href="https://discord.gg/KpanwM3vXa"
93
- target="_blank"
94
- className="max-lg:hidden"
95
- >
96
- <Button
97
- size="xs"
98
- variant="bordered"
99
- className="!border-indigo-500 !text-white !bg-indigo-500"
100
- >
101
- <DiscordIcon className="size-3 mr-0.5" />
102
- Discord Community
103
- </Button>
104
- </Link>
105
  </div>
106
  <div className="flex items-center gap-2">
107
  {project?.space_id && (
@@ -127,7 +110,6 @@ export function Header({ isNew }: { isNew: boolean }) {
127
  </Button>
128
  </Link>
129
  )}
130
-
131
  {project?.private && (
132
  <Tooltip>
133
  <TooltipTrigger>
 
23
  TooltipContent,
24
  TooltipTrigger,
25
  } from "@/components/ui/tooltip";
 
26
 
27
+ export function Header() {
28
  const { project } = useEditor();
29
  const { user, openLoginWindow } = useUser();
30
  return (
 
59
  <div className="lg:w-full px-2 lg:px-3 py-2 flex items-center justify-end lg:justify-between lg:border-l lg:border-neutral-800">
60
  <div className="font-mono text-muted-foreground flex items-center gap-2">
61
  <SwitchDevice />
62
+ <Button
63
+ size="xs"
64
+ variant="bordered"
65
+ className="max-lg:hidden"
66
+ onClick={() => {
67
+ const iframe = document.getElementById(
68
+ "preview-iframe"
69
+ ) as HTMLIFrameElement;
70
+ if (iframe) {
71
+ iframe.src = iframe.src;
72
+ }
73
+ }}
74
+ >
75
+ <RefreshCcw className="size-3 mr-0.5" />
76
+ Refresh Preview
77
+ </Button>
 
 
78
  <Link
79
+ href="https://huggingface.co/spaces/enzostvs/deepsite/discussions/427"
80
  target="_blank"
81
  className="max-lg:hidden"
82
  >
 
85
  Help
86
  </Button>
87
  </Link>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  </div>
89
  <div className="flex items-center gap-2">
90
  {project?.space_id && (
 
110
  </Button>
111
  </Link>
112
  )}
 
113
  {project?.private && (
114
  <Tooltip>
115
  <TooltipTrigger>
components/editor/index.tsx CHANGED
@@ -15,7 +15,6 @@ import { FileBrowser } from "./file-browser";
15
  import { AskAi } from "./ask-ai";
16
  import { Preview } from "./preview";
17
  import { SaveChangesPopup } from "./save-changes-popup";
18
- import { DiscordPromoModal } from "@/components/discord-promo-modal";
19
  import Loading from "../loading";
20
  import { Page } from "@/types";
21
 
@@ -69,7 +68,6 @@ export const AppEditor = ({
69
  const path = currentPageData.path;
70
  if (path.endsWith(".css")) return "css";
71
  if (path.endsWith(".js")) return "javascript";
72
- if (path.endsWith(".json")) return "json";
73
  return "html";
74
  }, [currentPageData.path]);
75
 
@@ -78,13 +76,12 @@ export const AppEditor = ({
78
  if (editorLanguage === "css") return "CSS copied to clipboard!";
79
  if (editorLanguage === "javascript")
80
  return "JavaScript copied to clipboard!";
81
- if (editorLanguage === "json") return "JSON copied to clipboard!";
82
  return "HTML copied to clipboard!";
83
  }, [editorLanguage]);
84
 
85
  return (
86
  <section className="h-screen w-full bg-neutral-950 flex flex-col">
87
- <Header isNew={isNew} />
88
  <main className="bg-neutral-950 flex-1 max-lg:flex-col flex w-full relative">
89
  <div
90
  ref={editor}
@@ -156,9 +153,10 @@ export const AppEditor = ({
156
  }}
157
  />
158
  </div>
159
- <Preview isNew={isNew} namespace={namespace} repoId={repoId} />
160
  </main>
161
 
 
162
  <SaveChangesPopup
163
  isOpen={showSavePopup}
164
  onClose={() => setShowSavePopup(false)}
@@ -167,8 +165,6 @@ export const AppEditor = ({
167
  pages={pages}
168
  project={project}
169
  />
170
-
171
- <DiscordPromoModal />
172
  </section>
173
  );
174
  };
 
15
  import { AskAi } from "./ask-ai";
16
  import { Preview } from "./preview";
17
  import { SaveChangesPopup } from "./save-changes-popup";
 
18
  import Loading from "../loading";
19
  import { Page } from "@/types";
20
 
 
68
  const path = currentPageData.path;
69
  if (path.endsWith(".css")) return "css";
70
  if (path.endsWith(".js")) return "javascript";
 
71
  return "html";
72
  }, [currentPageData.path]);
73
 
 
76
  if (editorLanguage === "css") return "CSS copied to clipboard!";
77
  if (editorLanguage === "javascript")
78
  return "JavaScript copied to clipboard!";
 
79
  return "HTML copied to clipboard!";
80
  }, [editorLanguage]);
81
 
82
  return (
83
  <section className="h-screen w-full bg-neutral-950 flex flex-col">
84
+ <Header />
85
  <main className="bg-neutral-950 flex-1 max-lg:flex-col flex w-full relative">
86
  <div
87
  ref={editor}
 
153
  }}
154
  />
155
  </div>
156
+ <Preview isNew={isNew} />
157
  </main>
158
 
159
+ {/* Save Changes Popup */}
160
  <SaveChangesPopup
161
  isOpen={showSavePopup}
162
  onClose={() => setShowSavePopup(false)}
 
165
  pages={pages}
166
  project={project}
167
  />
 
 
168
  </section>
169
  );
170
  };
components/editor/preview/index.tsx CHANGED
@@ -15,18 +15,8 @@ import { defaultHTML } from "@/lib/consts";
15
  import { HistoryNotification } from "../history-notification";
16
  import { api } from "@/lib/api";
17
  import { toast } from "sonner";
18
- import { RefreshCcw, TriangleAlert } from "lucide-react";
19
- import { Page } from "@/types";
20
-
21
- export const Preview = ({
22
- isNew,
23
- namespace,
24
- repoId,
25
- }: {
26
- isNew: boolean;
27
- namespace?: string;
28
- repoId?: string;
29
- }) => {
30
  const {
31
  project,
32
  device,
@@ -40,8 +30,6 @@ export const Preview = ({
40
  setCurrentPage,
41
  previewPage,
42
  setPreviewPage,
43
- setLastSavedPages,
44
- hasUnsavedChanges,
45
  } = useEditor();
46
  const {
47
  isEditableModeEnabled,
@@ -60,10 +48,6 @@ export const Preview = ({
60
  const [stableHtml, setStableHtml] = useState<string>("");
61
  const [throttledHtml, setThrottledHtml] = useState<string>("");
62
  const lastUpdateTimeRef = useRef<number>(0);
63
- const [iframeKey, setIframeKey] = useState(0);
64
- const [commitPages, setCommitPages] = useState<Page[]>([]);
65
- const [isLoadingCommitPages, setIsLoadingCommitPages] = useState(false);
66
- const prevCommitRef = useRef<string | null>(null);
67
 
68
  useEffect(() => {
69
  if (!previewPage && pages.length > 0) {
@@ -75,189 +59,28 @@ export const Preview = ({
75
  }
76
  }, [pages, previewPage]);
77
 
78
- const pagesToUse = currentCommit ? commitPages : pages;
79
-
80
  const previewPageData = useMemo(() => {
81
- const found = pagesToUse.find((p) => {
82
  const normalizedPagePath = p.path.replace(/^\.?\//, "");
83
  const normalizedPreviewPage = previewPage.replace(/^\.?\//, "");
84
  return normalizedPagePath === normalizedPreviewPage;
85
  });
86
- return found || (pagesToUse.length > 0 ? pagesToUse[0] : currentPageData);
87
- }, [pagesToUse, previewPage, currentPageData]);
88
-
89
- // Fetch commit pages when currentCommit changes
90
- useEffect(() => {
91
- if (currentCommit && namespace && repoId) {
92
- setIsLoadingCommitPages(true);
93
- api
94
- .get(`/me/projects/${namespace}/${repoId}/commits/${currentCommit}`)
95
- .then((res) => {
96
- if (res.data.ok) {
97
- setCommitPages(res.data.pages);
98
- // Set preview page to index.html if available
99
- const indexPage = res.data.pages.find(
100
- (p: Page) =>
101
- p.path === "index.html" || p.path === "index" || p.path === "/"
102
- );
103
- if (indexPage) {
104
- setPreviewPage(indexPage.path);
105
- }
106
- // Refresh iframe to show commit version
107
- setIframeKey((prev) => prev + 1);
108
- }
109
- })
110
- .catch((err) => {
111
- toast.error(
112
- err.response?.data?.error || "Failed to fetch commit pages"
113
- );
114
- })
115
- .finally(() => {
116
- setIsLoadingCommitPages(false);
117
- });
118
- } else if (!currentCommit && prevCommitRef.current !== null) {
119
- // Only clear commitPages when transitioning from a commit to no commit
120
- setCommitPages([]);
121
- }
122
- prevCommitRef.current = currentCommit;
123
- }, [currentCommit, namespace, repoId]);
124
-
125
- // Create navigation interception script
126
- const createNavigationScript = useCallback((availablePages: Page[]) => {
127
- const pagePaths = availablePages.map((p) => p.path.replace(/^\.?\//, ""));
128
- return `
129
- (function() {
130
- const availablePages = ${JSON.stringify(pagePaths)};
131
-
132
- function normalizePath(path) {
133
- let normalized = path.replace(/^\.?\//, "");
134
- if (normalized === "" || normalized === "/") {
135
- normalized = "index.html";
136
- }
137
- const hashIndex = normalized.indexOf("#");
138
- if (hashIndex !== -1) {
139
- normalized = normalized.substring(0, hashIndex);
140
- }
141
- if (!normalized.includes(".")) {
142
- normalized = normalized + ".html";
143
- }
144
- return normalized;
145
- }
146
-
147
- function handleNavigation(url) {
148
- if (!url) return;
149
-
150
- // Handle hash-only navigation
151
- if (url.startsWith("#")) {
152
- const targetElement = document.querySelector(url);
153
- if (targetElement) {
154
- targetElement.scrollIntoView({ behavior: "smooth" });
155
- }
156
- // Search in shadow DOM
157
- const searchInShadows = (root) => {
158
- const elements = root.querySelectorAll("*");
159
- for (const el of elements) {
160
- if (el.shadowRoot) {
161
- const found = el.shadowRoot.querySelector(url);
162
- if (found) {
163
- found.scrollIntoView({ behavior: "smooth" });
164
- return;
165
- }
166
- searchInShadows(el.shadowRoot);
167
- }
168
- }
169
- };
170
- searchInShadows(document);
171
- return;
172
- }
173
-
174
- // Handle external URLs
175
- if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//")) {
176
- window.open(url, "_blank");
177
- return;
178
- }
179
-
180
- const normalizedPath = normalizePath(url);
181
- if (availablePages.includes(normalizedPath)) {
182
- // Dispatch custom event to notify parent
183
- window.parent.postMessage({ type: 'navigate', path: normalizedPath }, '*');
184
- } else {
185
- console.warn('Page not found:', normalizedPath);
186
- }
187
- }
188
-
189
- // Intercept window.location methods
190
- const originalAssign = window.location.assign;
191
- const originalReplace = window.location.replace;
192
-
193
- window.location.assign = function(url) {
194
- handleNavigation(url);
195
- };
196
-
197
- window.location.replace = function(url) {
198
- handleNavigation(url);
199
- };
200
-
201
- // Intercept window.location.href setter
202
- try {
203
- let currentHref = window.location.href;
204
- Object.defineProperty(window.location, 'href', {
205
- get: function() {
206
- return currentHref;
207
- },
208
- set: function(url) {
209
- handleNavigation(url);
210
- },
211
- configurable: true
212
- });
213
- } catch (e) {
214
- // Fallback: use proxy if defineProperty fails
215
- console.warn('Could not intercept location.href:', e);
216
- }
217
-
218
- // Intercept link clicks
219
- document.addEventListener('click', function(e) {
220
- const anchor = e.target.closest('a');
221
- if (anchor && anchor.href) {
222
- const href = anchor.getAttribute('href');
223
- if (href && !href.startsWith('http://') && !href.startsWith('https://') && !href.startsWith('//') && !href.startsWith('mailto:') && !href.startsWith('tel:')) {
224
- e.preventDefault();
225
- handleNavigation(href);
226
- }
227
- }
228
- }, true);
229
-
230
- // Intercept form submissions
231
- document.addEventListener('submit', function(e) {
232
- const form = e.target;
233
- if (form.action && !form.action.startsWith('http://') && !form.action.startsWith('https://') && !form.action.startsWith('//')) {
234
- e.preventDefault();
235
- handleNavigation(form.action);
236
- }
237
- }, true);
238
- })();
239
- `;
240
- }, []);
241
 
242
  const injectAssetsIntoHtml = useCallback(
243
- (html: string, pagesToUse: Page[] = pages): string => {
244
  if (!html) return html;
245
 
246
- const cssFiles = pagesToUse.filter(
247
  (p) => p.path.endsWith(".css") && p.path !== previewPageData?.path
248
  );
249
- const jsFiles = pagesToUse.filter(
250
  (p) => p.path.endsWith(".js") && p.path !== previewPageData?.path
251
  );
252
- const jsonFiles = pagesToUse.filter(
253
- (p) => p.path.endsWith(".json") && p.path !== previewPageData?.path
254
- );
255
 
256
  let modifiedHtml = html;
257
 
258
- // Inject navigation script for srcDoc
259
- const navigationScript = createNavigationScript(pagesToUse);
260
-
261
  // Inject all CSS files
262
  if (cssFiles.length > 0) {
263
  const allCssContent = cssFiles
@@ -324,69 +147,9 @@ export const Preview = ({
324
  });
325
  }
326
 
327
- // Inject all JSON files as script tags with type="application/json"
328
- if (jsonFiles.length > 0) {
329
- const allJsonContent = jsonFiles
330
- .map(
331
- (file) =>
332
- `<script type="application/json" data-injected-from="${
333
- file.path
334
- }" id="${file.path.replace(/[^a-zA-Z0-9]/g, "-")}">\n${
335
- file.html
336
- }\n</script>`
337
- )
338
- .join("\n");
339
-
340
- if (modifiedHtml.includes("</body>")) {
341
- modifiedHtml = modifiedHtml.replace(
342
- "</body>",
343
- `${allJsonContent}\n</body>`
344
- );
345
- } else if (modifiedHtml.includes("<body>")) {
346
- modifiedHtml = modifiedHtml + allJsonContent;
347
- } else {
348
- modifiedHtml = modifiedHtml + "\n" + allJsonContent;
349
- }
350
-
351
- jsonFiles.forEach((file) => {
352
- const escapedPath = file.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
353
- modifiedHtml = modifiedHtml.replace(
354
- new RegExp(
355
- `<script\\s+[^>]*src=["'][\\.\/]*${escapedPath}["'][^>]*><\\/script>`,
356
- "gi"
357
- ),
358
- ""
359
- );
360
- });
361
- }
362
-
363
- // Inject navigation script early in the document
364
- if (navigationScript) {
365
- // Try to inject right after <head> or <body> opening tag
366
- if (modifiedHtml.includes("<head>")) {
367
- modifiedHtml = modifiedHtml.replace(
368
- "<head>",
369
- `<head>\n<script>${navigationScript}</script>`
370
- );
371
- } else if (modifiedHtml.includes("<body>")) {
372
- modifiedHtml = modifiedHtml.replace(
373
- "<body>",
374
- `<body>\n<script>${navigationScript}</script>`
375
- );
376
- } else if (modifiedHtml.includes("</body>")) {
377
- modifiedHtml = modifiedHtml.replace(
378
- "</body>",
379
- `<script>${navigationScript}</script>\n</body>`
380
- );
381
- } else {
382
- modifiedHtml =
383
- `<script>${navigationScript}</script>\n` + modifiedHtml;
384
- }
385
- }
386
-
387
  return modifiedHtml;
388
  },
389
- [pages, previewPageData?.path, createNavigationScript]
390
  );
391
 
392
  useEffect(() => {
@@ -395,33 +158,24 @@ export const Preview = ({
395
  const timeSinceLastUpdate = now - lastUpdateTimeRef.current;
396
 
397
  if (lastUpdateTimeRef.current === 0 || timeSinceLastUpdate >= 3000) {
398
- const processedHtml = injectAssetsIntoHtml(
399
- previewPageData.html,
400
- pagesToUse
401
- );
402
  setThrottledHtml(processedHtml);
403
  lastUpdateTimeRef.current = now;
404
  } else {
405
  const timeUntilNextUpdate = 3000 - timeSinceLastUpdate;
406
  const timer = setTimeout(() => {
407
- const processedHtml = injectAssetsIntoHtml(
408
- previewPageData.html,
409
- pagesToUse
410
- );
411
  setThrottledHtml(processedHtml);
412
  lastUpdateTimeRef.current = Date.now();
413
  }, timeUntilNextUpdate);
414
  return () => clearTimeout(timer);
415
  }
416
  }
417
- }, [isNew, previewPageData?.html, injectAssetsIntoHtml, pagesToUse]);
418
 
419
  useEffect(() => {
420
  if (!isAiWorking && !globalAiLoading && previewPageData?.html) {
421
- const processedHtml = injectAssetsIntoHtml(
422
- previewPageData.html,
423
- pagesToUse
424
- );
425
  setStableHtml(processedHtml);
426
  }
427
  }, [
@@ -430,7 +184,6 @@ export const Preview = ({
430
  previewPageData?.html,
431
  injectAssetsIntoHtml,
432
  previewPage,
433
- pagesToUse,
434
  ]);
435
 
436
  useEffect(() => {
@@ -440,10 +193,7 @@ export const Preview = ({
440
  !isAiWorking &&
441
  !globalAiLoading
442
  ) {
443
- const processedHtml = injectAssetsIntoHtml(
444
- previewPageData.html,
445
- pagesToUse
446
- );
447
  setStableHtml(processedHtml);
448
  }
449
  }, [
@@ -452,7 +202,6 @@ export const Preview = ({
452
  isAiWorking,
453
  globalAiLoading,
454
  injectAssetsIntoHtml,
455
- pagesToUse,
456
  ]);
457
 
458
  const setupIframeListeners = () => {
@@ -473,17 +222,6 @@ export const Preview = ({
473
  }
474
  };
475
 
476
- // Listen for navigation messages from iframe
477
- useEffect(() => {
478
- const handleMessage = (event: MessageEvent) => {
479
- if (event.data?.type === "navigate" && event.data?.path) {
480
- setPreviewPage(event.data.path);
481
- }
482
- };
483
- window.addEventListener("message", handleMessage);
484
- return () => window.removeEventListener("message", handleMessage);
485
- }, [setPreviewPage]);
486
-
487
  useEffect(() => {
488
  const cleanupListeners = () => {
489
  if (iframeRef?.current?.contentDocument) {
@@ -512,10 +250,6 @@ export const Preview = ({
512
  };
513
  }, [isEditableModeEnabled, stableHtml, throttledHtml, previewPage]);
514
 
515
- const refreshIframe = () => {
516
- setIframeKey((prev) => prev + 1);
517
- };
518
-
519
  const promoteVersion = async () => {
520
  setIsPromotingVersion(true);
521
  await api
@@ -527,7 +261,6 @@ export const Preview = ({
527
  setCurrentCommit(null);
528
  setPages(res.data.pages);
529
  setCurrentPage(res.data.pages[0].path);
530
- setLastSavedPages(res.data.pages);
531
  setPreviewPage(res.data.pages[0].path);
532
  toast.success("Version promoted successfully");
533
  }
@@ -698,7 +431,7 @@ export const Preview = ({
698
  normalizedHref = normalizedHref + ".html";
699
  }
700
 
701
- const isPageExist = pagesToUse.some((page) => {
702
  const pagePath = page.path.replace(/^\.?\//, "");
703
  return pagePath === normalizedHref;
704
  });
@@ -756,36 +489,7 @@ export const Preview = ({
756
  </div>
757
  ) : (
758
  <>
759
- {isLoadingCommitPages && (
760
- <div className="top-0 left-0 right-0 z-20 bg-blue-500/90 backdrop-blur-sm border-b border-blue-600 px-4 py-2 flex items-center justify-center gap-3 text-sm w-full">
761
- <div className="flex items-center gap-2">
762
- <AiLoading
763
- text="Loading commit version..."
764
- className="flex-row"
765
- />
766
- </div>
767
- </div>
768
- )}
769
- {!isNew && !currentCommit && (
770
- <div className="top-0 left-0 right-0 z-20 bg-neutral-900/95 backdrop-blur-sm border-b border-neutral-800 px-4 py-2 max-h-[40px] flex items-center justify-between gap-3 text-xs w-full">
771
- <div className="flex items-center gap-2 flex-1">
772
- <TriangleAlert className="size-4 text-neutral-500 flex-shrink-0" />
773
- <span className="text-neutral-400 font-medium">
774
- Preview version of the project. Try refreshing the preview if
775
- you experience any issues.
776
- </span>
777
- </div>
778
- <button
779
- onClick={refreshIframe}
780
- className="cursor-pointer text-xs px-3 py-1 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 rounded-md font-medium transition-colors whitespace-nowrap flex items-center gap-1.5"
781
- >
782
- <RefreshCcw className="size-3 text-neutral-300 flex-shrink-0" />
783
- Refresh
784
- </button>
785
- </div>
786
- )}
787
  <iframe
788
- key={iframeKey}
789
  id="preview-iframe"
790
  ref={iframeRef}
791
  className={classNames(
@@ -796,45 +500,36 @@ export const Preview = ({
796
  }
797
  )}
798
  src={
799
- !currentCommit &&
800
- !isNew &&
801
- !hasUnsavedChanges &&
802
- project?.space_id &&
803
- !project?.private
804
- ? `https://${project.space_id.replaceAll(
805
  "/",
806
  "-"
807
- )}.static.hf.space`
808
  : undefined
809
  }
810
  srcDoc={
811
- currentCommit
812
- ? commitPages.length > 0 && previewPageData?.html
813
- ? injectAssetsIntoHtml(previewPageData.html, commitPages)
814
- : defaultHTML
815
- : isNew || hasUnsavedChanges || project?.private
816
  ? isNew
817
  ? throttledHtml || defaultHTML
818
  : stableHtml
819
  : undefined
820
  }
821
- onLoad={() => {
822
- if (
823
- currentCommit ||
824
- isNew ||
825
- hasUnsavedChanges ||
826
- project?.private
827
- ) {
828
- if (iframeRef?.current?.contentWindow?.document?.body) {
829
- iframeRef.current.contentWindow.document.body.scrollIntoView({
830
- block: isAiWorking ? "end" : "start",
831
- inline: "nearest",
832
- behavior: isAiWorking ? "instant" : "smooth",
833
- });
834
- }
835
- setupIframeListeners();
836
- }
837
- }}
838
  sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-modals allow-forms"
839
  allow="accelerometer; ambient-light-sensor; autoplay; battery; camera; clipboard-read; clipboard-write; display-capture; document-domain; encrypted-media; fullscreen; geolocation; gyroscope; layout-animations; legacy-image-formats; magnetometer; microphone; midi; oversized-images; payment; picture-in-picture; publickey-credentials-get; serial; sync-xhr; usb; vr ; wake-lock; xr-spatial-tracking"
840
  />
 
15
  import { HistoryNotification } from "../history-notification";
16
  import { api } from "@/lib/api";
17
  import { toast } from "sonner";
18
+
19
+ export const Preview = ({ isNew }: { isNew: boolean }) => {
 
 
 
 
 
 
 
 
 
 
20
  const {
21
  project,
22
  device,
 
30
  setCurrentPage,
31
  previewPage,
32
  setPreviewPage,
 
 
33
  } = useEditor();
34
  const {
35
  isEditableModeEnabled,
 
48
  const [stableHtml, setStableHtml] = useState<string>("");
49
  const [throttledHtml, setThrottledHtml] = useState<string>("");
50
  const lastUpdateTimeRef = useRef<number>(0);
 
 
 
 
51
 
52
  useEffect(() => {
53
  if (!previewPage && pages.length > 0) {
 
59
  }
60
  }, [pages, previewPage]);
61
 
 
 
62
  const previewPageData = useMemo(() => {
63
+ const found = pages.find((p) => {
64
  const normalizedPagePath = p.path.replace(/^\.?\//, "");
65
  const normalizedPreviewPage = previewPage.replace(/^\.?\//, "");
66
  return normalizedPagePath === normalizedPreviewPage;
67
  });
68
+ return found || currentPageData;
69
+ }, [pages, previewPage, currentPageData]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
  const injectAssetsIntoHtml = useCallback(
72
+ (html: string): string => {
73
  if (!html) return html;
74
 
75
+ const cssFiles = pages.filter(
76
  (p) => p.path.endsWith(".css") && p.path !== previewPageData?.path
77
  );
78
+ const jsFiles = pages.filter(
79
  (p) => p.path.endsWith(".js") && p.path !== previewPageData?.path
80
  );
 
 
 
81
 
82
  let modifiedHtml = html;
83
 
 
 
 
84
  // Inject all CSS files
85
  if (cssFiles.length > 0) {
86
  const allCssContent = cssFiles
 
147
  });
148
  }
149
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  return modifiedHtml;
151
  },
152
+ [pages, previewPageData?.path]
153
  );
154
 
155
  useEffect(() => {
 
158
  const timeSinceLastUpdate = now - lastUpdateTimeRef.current;
159
 
160
  if (lastUpdateTimeRef.current === 0 || timeSinceLastUpdate >= 3000) {
161
+ const processedHtml = injectAssetsIntoHtml(previewPageData.html);
 
 
 
162
  setThrottledHtml(processedHtml);
163
  lastUpdateTimeRef.current = now;
164
  } else {
165
  const timeUntilNextUpdate = 3000 - timeSinceLastUpdate;
166
  const timer = setTimeout(() => {
167
+ const processedHtml = injectAssetsIntoHtml(previewPageData.html);
 
 
 
168
  setThrottledHtml(processedHtml);
169
  lastUpdateTimeRef.current = Date.now();
170
  }, timeUntilNextUpdate);
171
  return () => clearTimeout(timer);
172
  }
173
  }
174
+ }, [isNew, previewPageData?.html, injectAssetsIntoHtml]);
175
 
176
  useEffect(() => {
177
  if (!isAiWorking && !globalAiLoading && previewPageData?.html) {
178
+ const processedHtml = injectAssetsIntoHtml(previewPageData.html);
 
 
 
179
  setStableHtml(processedHtml);
180
  }
181
  }, [
 
184
  previewPageData?.html,
185
  injectAssetsIntoHtml,
186
  previewPage,
 
187
  ]);
188
 
189
  useEffect(() => {
 
193
  !isAiWorking &&
194
  !globalAiLoading
195
  ) {
196
+ const processedHtml = injectAssetsIntoHtml(previewPageData.html);
 
 
 
197
  setStableHtml(processedHtml);
198
  }
199
  }, [
 
202
  isAiWorking,
203
  globalAiLoading,
204
  injectAssetsIntoHtml,
 
205
  ]);
206
 
207
  const setupIframeListeners = () => {
 
222
  }
223
  };
224
 
 
 
 
 
 
 
 
 
 
 
 
225
  useEffect(() => {
226
  const cleanupListeners = () => {
227
  if (iframeRef?.current?.contentDocument) {
 
250
  };
251
  }, [isEditableModeEnabled, stableHtml, throttledHtml, previewPage]);
252
 
 
 
 
 
253
  const promoteVersion = async () => {
254
  setIsPromotingVersion(true);
255
  await api
 
261
  setCurrentCommit(null);
262
  setPages(res.data.pages);
263
  setCurrentPage(res.data.pages[0].path);
 
264
  setPreviewPage(res.data.pages[0].path);
265
  toast.success("Version promoted successfully");
266
  }
 
431
  normalizedHref = normalizedHref + ".html";
432
  }
433
 
434
+ const isPageExist = pages.some((page) => {
435
  const pagePath = page.path.replace(/^\.?\//, "");
436
  return pagePath === normalizedHref;
437
  });
 
489
  </div>
490
  ) : (
491
  <>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
  <iframe
 
493
  id="preview-iframe"
494
  ref={iframeRef}
495
  className={classNames(
 
500
  }
501
  )}
502
  src={
503
+ currentCommit
504
+ ? `https://${project?.space_id?.replaceAll(
 
 
 
 
505
  "/",
506
  "-"
507
+ )}--rev-${currentCommit.slice(0, 7)}.static.hf.space`
508
  : undefined
509
  }
510
  srcDoc={
511
+ !currentCommit
 
 
 
 
512
  ? isNew
513
  ? throttledHtml || defaultHTML
514
  : stableHtml
515
  : undefined
516
  }
517
+ onLoad={
518
+ !currentCommit
519
+ ? () => {
520
+ if (iframeRef?.current?.contentWindow?.document?.body) {
521
+ iframeRef.current.contentWindow.document.body.scrollIntoView(
522
+ {
523
+ block: isAiWorking ? "end" : "start",
524
+ inline: "nearest",
525
+ behavior: isAiWorking ? "instant" : "smooth",
526
+ }
527
+ );
528
+ }
529
+ setupIframeListeners();
530
+ }
531
+ : undefined
532
+ }
 
533
  sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-modals allow-forms"
534
  allow="accelerometer; ambient-light-sensor; autoplay; battery; camera; clipboard-read; clipboard-write; display-capture; document-domain; encrypted-media; fullscreen; geolocation; gyroscope; layout-animations; legacy-image-formats; magnetometer; microphone; midi; oversized-images; payment; picture-in-picture; publickey-credentials-get; serial; sync-xhr; usb; vr ; wake-lock; xr-spatial-tracking"
535
  />
components/icons/discord.tsx DELETED
@@ -1,27 +0,0 @@
1
- export const DiscordIcon = ({ className }: { className?: string }) => {
2
- return (
3
- <svg
4
- width="1em"
5
- height="1em"
6
- viewBox="0 0 127 96"
7
- fill="none"
8
- aria-hidden="true"
9
- focusable="false"
10
- preserveAspectRatio="xMidYMid meet"
11
- xmlns="http://www.w3.org/2000/svg"
12
- className={className}
13
- >
14
- <g clipPath="url(#clip0_1084_3121)">
15
- <path
16
- d="M81.15 0C79.9124 2.1973 78.8011 4.4704 77.7909 6.794C68.1934 5.3544 58.4191 5.3544 48.7964 6.794C47.8114 4.4704 46.6748 2.1973 45.4373 0C36.4207 1.5407 27.6314 4.2431 19.2968 8.0568C2.77901 32.5304 -1.69139 56.3725 0.531208 79.8863C10.2044 87.0339 21.0395 92.4893 32.5817 95.9747C35.1831 92.4893 37.4815 88.7766 39.4515 84.9124C35.7135 83.5233 32.1018 81.7806 28.6417 79.7601C29.5509 79.1034 30.4349 78.4215 31.2936 77.7648C51.5746 87.3118 75.0632 87.3118 95.3694 77.7648C96.2281 78.472 97.1121 79.1539 98.0213 79.7601C94.5612 81.8058 90.9495 83.5233 87.1863 84.9377C89.1563 88.8019 91.4546 92.5146 94.0561 96C105.598 92.5146 116.433 87.0844 126.107 79.9369C128.733 52.6598 121.611 29.0197 107.29 8.0821C98.9811 4.2684 90.1918 1.5659 81.1752 0.0505L81.15 0ZM42.2802 65.4144C36.0419 65.4144 30.8643 59.7569 30.8643 52.7609C30.8643 45.7649 35.8398 40.0821 42.255 40.0821C48.6702 40.0821 53.7719 45.7901 53.6709 52.7609C53.5699 59.7317 48.6449 65.4144 42.2802 65.4144ZM84.3576 65.4144C78.0939 65.4144 72.9669 59.7569 72.9669 52.7609C72.9669 45.7649 77.9424 40.0821 84.3576 40.0821C90.7728 40.0821 95.8493 45.7901 95.7482 52.7609C95.6472 59.7317 90.7222 65.4144 84.3576 65.4144Z"
17
- fill="currentColor"
18
- />
19
- </g>
20
- <defs>
21
- <clipPath id="clip0_1084_3121">
22
- <rect width="126.644" height="96" fill="currentColor" />
23
- </clipPath>
24
- </defs>
25
- </svg>
26
- );
27
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/my-projects/project-card.tsx CHANGED
@@ -1,12 +1,6 @@
1
  import Link from "next/link";
2
  import { formatDistance } from "date-fns";
3
- import {
4
- Download,
5
- EllipsisVertical,
6
- Lock,
7
- Settings,
8
- Trash,
9
- } from "lucide-react";
10
 
11
  import { Button } from "@/components/ui/button";
12
  import {
@@ -17,8 +11,6 @@ import {
17
  DropdownMenuTrigger,
18
  } from "@/components/ui/dropdown-menu";
19
  import { ProjectType } from "@/types";
20
- import { toast } from "sonner";
21
- import { useUser } from "@/hooks/useUser";
22
 
23
  // from-red-500 to-red-500
24
  // from-yellow-500 to-yellow-500
@@ -36,7 +28,6 @@ export function ProjectCard({
36
  project: ProjectType;
37
  onDelete: () => void;
38
  }) {
39
- const { token } = useUser();
40
  const handleDelete = () => {
41
  if (
42
  confirm(
@@ -46,46 +37,6 @@ export function ProjectCard({
46
  onDelete();
47
  }
48
  };
49
-
50
- const handleDownload = async () => {
51
- try {
52
- toast.info("Preparing download...");
53
- const response = await fetch(
54
- `/deepsite/api/me/projects/${project.name}/download`,
55
- {
56
- credentials: "include",
57
- headers: {
58
- Accept: "application/zip",
59
- Authorization: `Bearer ${token}`,
60
- },
61
- }
62
- );
63
-
64
- if (!response.ok) {
65
- const error = await response
66
- .json()
67
- .catch(() => ({ error: "Download failed" }));
68
- toast.error(error.error || "Failed to download project");
69
- return;
70
- }
71
-
72
- const blob = await response.blob();
73
-
74
- const url = window.URL.createObjectURL(blob);
75
- const link = document.createElement("a");
76
- link.href = url;
77
- link.download = `${project.name.replace(/\//g, "-")}.zip`;
78
- document.body.appendChild(link);
79
- link.click();
80
- document.body.removeChild(link);
81
- window.URL.revokeObjectURL(url);
82
-
83
- toast.success("Download started!");
84
- } catch (error) {
85
- console.error("Download error:", error);
86
- toast.error("Failed to download project");
87
- }
88
- };
89
  // from-gray-600 to-gray-600
90
  // from-blue-600 to-blue-600
91
  // from-green-600 to-green-600
@@ -169,10 +120,6 @@ export function ProjectCard({
169
  Project Settings
170
  </DropdownMenuItem>
171
  </a>
172
- <DropdownMenuItem onClick={handleDownload}>
173
- <Download className="size-4 text-neutral-100" />
174
- Download as ZIP
175
- </DropdownMenuItem>
176
  <DropdownMenuItem variant="destructive" onClick={handleDelete}>
177
  <Trash className="size-4 text-red-500" />
178
  Delete Project
 
1
  import Link from "next/link";
2
  import { formatDistance } from "date-fns";
3
+ import { EllipsisVertical, Lock, Settings, Trash } from "lucide-react";
 
 
 
 
 
 
4
 
5
  import { Button } from "@/components/ui/button";
6
  import {
 
11
  DropdownMenuTrigger,
12
  } from "@/components/ui/dropdown-menu";
13
  import { ProjectType } from "@/types";
 
 
14
 
15
  // from-red-500 to-red-500
16
  // from-yellow-500 to-yellow-500
 
28
  project: ProjectType;
29
  onDelete: () => void;
30
  }) {
 
31
  const handleDelete = () => {
32
  if (
33
  confirm(
 
37
  onDelete();
38
  }
39
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  // from-gray-600 to-gray-600
41
  // from-blue-600 to-blue-600
42
  // from-green-600 to-green-600
 
120
  Project Settings
121
  </DropdownMenuItem>
122
  </a>
 
 
 
 
123
  <DropdownMenuItem variant="destructive" onClick={handleDelete}>
124
  <Trash className="size-4 text-red-500" />
125
  Delete Project
components/public/navigation/index.tsx CHANGED
@@ -12,7 +12,6 @@ import Logo from "@/assets/logo.svg";
12
  import { useUser } from "@/hooks/useUser";
13
  import { UserMenu } from "@/components/user-menu";
14
  import { ProTag } from "@/components/pro-modal";
15
- import { DiscordIcon } from "@/components/icons/discord";
16
 
17
  const navigationLinks = [
18
  {
@@ -97,9 +96,7 @@ export default function Navigation() {
97
  width={64}
98
  height={64}
99
  />
100
- <p className="font-sans text-white text-xl font-bold max-lg:hidden">
101
- DeepSite
102
- </p>
103
  {user?.isPro && <ProTag className="ml-1" />}
104
  </Link>
105
  <ul className="items-center justify-center gap-6 !hidden">
@@ -144,17 +141,7 @@ export default function Navigation() {
144
  <div className="size-1 bg-white rounded-full" />
145
  </div>
146
  </ul>
147
- <div className="flex items-center justify-end gap-3">
148
- <Link href="https://discord.gg/KpanwM3vXa" target="_blank">
149
- <Button
150
- variant="bordered"
151
- className="!border-indigo-500 !text-white !bg-indigo-500 transition-all duration-300"
152
- >
153
- <DiscordIcon className="size-4 mr-0.5" />
154
- <span className="max-lg:hidden">Discord Community</span>
155
- <span className="lg:hidden">Discord</span>
156
- </Button>
157
- </Link>
158
  {loading ? (
159
  <Button
160
  variant="ghostDarker"
 
12
  import { useUser } from "@/hooks/useUser";
13
  import { UserMenu } from "@/components/user-menu";
14
  import { ProTag } from "@/components/pro-modal";
 
15
 
16
  const navigationLinks = [
17
  {
 
96
  width={64}
97
  height={64}
98
  />
99
+ <p className="font-sans text-white text-xl font-bold">DeepSite</p>
 
 
100
  {user?.isPro && <ProTag className="ml-1" />}
101
  </Link>
102
  <ul className="items-center justify-center gap-6 !hidden">
 
141
  <div className="size-1 bg-white rounded-full" />
142
  </div>
143
  </ul>
144
+ <div className="flex items-center justify-end gap-2">
 
 
 
 
 
 
 
 
 
 
145
  {loading ? (
146
  <Button
147
  variant="ghostDarker"
hooks/useAi.ts CHANGED
@@ -19,7 +19,7 @@ export const useAi = (onScrollToBottom?: () => void) => {
19
  const [storageProvider, setStorageProvider] = useLocalStorage("provider", "auto");
20
  const [storageModel, setStorageModel] = useLocalStorage("model", MODELS[0].value);
21
  const router = useRouter();
22
- const { token } = useUser();
23
  const streamingPagesRef = useRef<Set<string>>(new Set());
24
 
25
  const { data: isAiWorking = false } = useQuery({
@@ -44,18 +44,6 @@ export const useAi = (onScrollToBottom?: () => void) => {
44
  client.setQueryData(["ai.isThinking"], newIsThinking);
45
  };
46
 
47
- const { data: thinkingContent } = useQuery<string>({
48
- queryKey: ["ai.thinkingContent"],
49
- queryFn: async () => "",
50
- refetchOnWindowFocus: false,
51
- refetchOnReconnect: false,
52
- refetchOnMount: false,
53
- initialData: ""
54
- });
55
- const setThinkingContent = (newThinkingContent: string) => {
56
- client.setQueryData(["ai.thinkingContent"], newThinkingContent);
57
- };
58
-
59
  const { data: selectedElement } = useQuery<HTMLElement | null>({
60
  queryKey: ["ai.selectedElement"],
61
  queryFn: async () => null,
@@ -179,7 +167,6 @@ export const useAi = (onScrollToBottom?: () => void) => {
179
  if (!redesignMarkdown && !prompt.trim()) return;
180
 
181
  setIsAiWorking(true);
182
- setThinkingContent(""); // Reset thinking content
183
  streamingPagesRef.current.clear(); // Reset tracking for new generation
184
 
185
  const abortController = new AbortController();
@@ -212,14 +199,6 @@ export const useAi = (onScrollToBottom?: () => void) => {
212
  const { done, value } = await reader.read();
213
 
214
  if (done) {
215
- // Final processing - extract and remove thinking content
216
- const thinkMatch = contentResponse.match(/<think>([\s\S]*?)<\/think>/);
217
- if (thinkMatch) {
218
- setThinkingContent(thinkMatch[1].trim());
219
- setIsThinking(false);
220
- contentResponse = contentResponse.replace(/<think>[\s\S]*?<\/think>/, '').trim();
221
- }
222
-
223
  const trimmedResponse = contentResponse.trim();
224
  if (trimmedResponse.startsWith("{") && trimmedResponse.endsWith("}")) {
225
  try {
@@ -244,7 +223,7 @@ export const useAi = (onScrollToBottom?: () => void) => {
244
  const newPages = formatPages(contentResponse, false);
245
  let projectName = contentResponse.match(/<<<<<<< PROJECT_NAME_START\s*([\s\S]*?)\s*>>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
246
  if (!projectName) {
247
- projectName = prompt.substring(0, 20).replace(/[^a-zA-Z0-9]/g, "-") + "-" + Math.random().toString(36).substring(2, 9);
248
  }
249
  setPages(newPages);
250
  setLastSavedPages([...newPages]);
@@ -259,26 +238,6 @@ export const useAi = (onScrollToBottom?: () => void) => {
259
  const chunk = decoder.decode(value, { stream: true });
260
  contentResponse += chunk;
261
 
262
- // Extract thinking content while streaming
263
- if (contentResponse.includes('</think>')) {
264
- // Thinking is complete, extract final content and stop thinking
265
- const thinkMatch = contentResponse.match(/<think>([\s\S]*?)<\/think>/);
266
- if (thinkMatch) {
267
- setThinkingContent(thinkMatch[1].trim());
268
- setIsThinking(false);
269
- }
270
- } else if (contentResponse.includes('<think>')) {
271
- // Still thinking, update content
272
- const thinkMatch = contentResponse.match(/<think>([\s\S]*)$/);
273
- if (thinkMatch) {
274
- const thinkingText = thinkMatch[1].trim();
275
- if (thinkingText) {
276
- setIsThinking(true);
277
- setThinkingContent(thinkingText);
278
- }
279
- }
280
- }
281
-
282
  const trimmedResponse = contentResponse.trim();
283
  if (trimmedResponse.startsWith("{") && trimmedResponse.endsWith("}")) {
284
  try {
@@ -311,7 +270,6 @@ export const useAi = (onScrollToBottom?: () => void) => {
311
  } catch (error: any) {
312
  setIsAiWorking(false);
313
  setIsThinking(false);
314
- setThinkingContent("");
315
  setController(null);
316
 
317
  if (!abortController.signal.aborted) {
@@ -331,7 +289,6 @@ export const useAi = (onScrollToBottom?: () => void) => {
331
 
332
 
333
  setIsAiWorking(true);
334
- setThinkingContent(""); // Reset thinking content
335
 
336
  const abortController = new AbortController();
337
  setController(abortController);
@@ -373,14 +330,6 @@ export const useAi = (onScrollToBottom?: () => void) => {
373
  const { done, value } = await reader.read();
374
 
375
  if (done) {
376
- // Extract and remove thinking content
377
- const thinkMatch = contentResponse.match(/<think>([\s\S]*?)<\/think>/);
378
- if (thinkMatch) {
379
- setThinkingContent(thinkMatch[1].trim());
380
- setIsThinking(false);
381
- contentResponse = contentResponse.replace(/<think>[\s\S]*?<\/think>/, '').trim();
382
- }
383
-
384
  const metadataMatch = contentResponse.match(/___METADATA_START___([\s\S]*?)___METADATA_END___/);
385
  if (metadataMatch) {
386
  try {
@@ -490,26 +439,6 @@ export const useAi = (onScrollToBottom?: () => void) => {
490
  const chunk = decoder.decode(value, { stream: true });
491
  contentResponse += chunk;
492
 
493
- // Extract thinking content while streaming
494
- if (contentResponse.includes('</think>')) {
495
- // Thinking is complete, extract final content and stop thinking
496
- const thinkMatch = contentResponse.match(/<think>([\s\S]*?)<\/think>/);
497
- if (thinkMatch) {
498
- setThinkingContent(thinkMatch[1].trim());
499
- setIsThinking(false);
500
- }
501
- } else if (contentResponse.includes('<think>')) {
502
- // Still thinking, update content
503
- const thinkMatch = contentResponse.match(/<think>([\s\S]*)$/);
504
- if (thinkMatch) {
505
- const thinkingText = thinkMatch[1].trim();
506
- if (thinkingText) {
507
- setIsThinking(true);
508
- setThinkingContent(thinkingText);
509
- }
510
- }
511
- }
512
-
513
  // Check for error responses during streaming
514
  const trimmedResponse = contentResponse.trim();
515
  if (trimmedResponse.startsWith("{") && trimmedResponse.endsWith("}")) {
@@ -542,7 +471,6 @@ export const useAi = (onScrollToBottom?: () => void) => {
542
  } catch (error: any) {
543
  setIsAiWorking(false);
544
  setIsThinking(false);
545
- setThinkingContent("");
546
  setController(null);
547
 
548
  if (!abortController.signal.aborted) {
@@ -690,8 +618,6 @@ export const useAi = (onScrollToBottom?: () => void) => {
690
  return {
691
  isThinking,
692
  setIsThinking,
693
- thinkingContent,
694
- setThinkingContent,
695
  callAiNewProject,
696
  callAiFollowUp,
697
  isAiWorking,
 
19
  const [storageProvider, setStorageProvider] = useLocalStorage("provider", "auto");
20
  const [storageModel, setStorageModel] = useLocalStorage("model", MODELS[0].value);
21
  const router = useRouter();
22
+ const { projects, setProjects, token } = useUser();
23
  const streamingPagesRef = useRef<Set<string>>(new Set());
24
 
25
  const { data: isAiWorking = false } = useQuery({
 
44
  client.setQueryData(["ai.isThinking"], newIsThinking);
45
  };
46
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  const { data: selectedElement } = useQuery<HTMLElement | null>({
48
  queryKey: ["ai.selectedElement"],
49
  queryFn: async () => null,
 
167
  if (!redesignMarkdown && !prompt.trim()) return;
168
 
169
  setIsAiWorking(true);
 
170
  streamingPagesRef.current.clear(); // Reset tracking for new generation
171
 
172
  const abortController = new AbortController();
 
199
  const { done, value } = await reader.read();
200
 
201
  if (done) {
 
 
 
 
 
 
 
 
202
  const trimmedResponse = contentResponse.trim();
203
  if (trimmedResponse.startsWith("{") && trimmedResponse.endsWith("}")) {
204
  try {
 
223
  const newPages = formatPages(contentResponse, false);
224
  let projectName = contentResponse.match(/<<<<<<< PROJECT_NAME_START\s*([\s\S]*?)\s*>>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
225
  if (!projectName) {
226
+ projectName = prompt.substring(0, 40).replace(/[^a-zA-Z0-9]/g, "-").slice(0, 40) + "-" + Math.random().toString(36).substring(2, 15);
227
  }
228
  setPages(newPages);
229
  setLastSavedPages([...newPages]);
 
238
  const chunk = decoder.decode(value, { stream: true });
239
  contentResponse += chunk;
240
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  const trimmedResponse = contentResponse.trim();
242
  if (trimmedResponse.startsWith("{") && trimmedResponse.endsWith("}")) {
243
  try {
 
270
  } catch (error: any) {
271
  setIsAiWorking(false);
272
  setIsThinking(false);
 
273
  setController(null);
274
 
275
  if (!abortController.signal.aborted) {
 
289
 
290
 
291
  setIsAiWorking(true);
 
292
 
293
  const abortController = new AbortController();
294
  setController(abortController);
 
330
  const { done, value } = await reader.read();
331
 
332
  if (done) {
 
 
 
 
 
 
 
 
333
  const metadataMatch = contentResponse.match(/___METADATA_START___([\s\S]*?)___METADATA_END___/);
334
  if (metadataMatch) {
335
  try {
 
439
  const chunk = decoder.decode(value, { stream: true });
440
  contentResponse += chunk;
441
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  // Check for error responses during streaming
443
  const trimmedResponse = contentResponse.trim();
444
  if (trimmedResponse.startsWith("{") && trimmedResponse.endsWith("}")) {
 
471
  } catch (error: any) {
472
  setIsAiWorking(false);
473
  setIsThinking(false);
 
474
  setController(null);
475
 
476
  if (!abortController.signal.aborted) {
 
618
  return {
619
  isThinking,
620
  setIsThinking,
 
 
621
  callAiNewProject,
622
  callAiFollowUp,
623
  isAiWorking,
lib/best-provider.ts CHANGED
@@ -3,13 +3,18 @@ export const getBestProvider = async (model: string, provider?: string) => {
3
  const { data } = await response.json()
4
  let bestProvider = null;
5
  if (provider === "auto") {
6
- return "auto";
 
 
 
 
 
7
  } else {
8
  const providerData = data.providers.find((p: any) => p.provider === provider)
9
  if (providerData?.status === "live") {
10
- bestProvider = providerData.provider;
11
  } else {
12
- bestProvider = "auto"
13
  }
14
  }
15
 
 
3
  const { data } = await response.json()
4
  let bestProvider = null;
5
  if (provider === "auto") {
6
+ const sortedProviders = data.providers.sort((a: any, b: any) => {
7
+ if (a.status === "live" && b.status !== "live") return -1
8
+ if (a.status !== "live" && b.status === "live") return 1
9
+ return a?.pricing?.output - b?.pricing?.output + a?.pricing?.input - b?.pricing?.input
10
+ })
11
+ bestProvider = sortedProviders[0]
12
  } else {
13
  const providerData = data.providers.find((p: any) => p.provider === provider)
14
  if (providerData?.status === "live") {
15
+ bestProvider = providerData
16
  } else {
17
+ bestProvider = data.providers?.find((p: any) => p.status === "live")
18
  }
19
  }
20
 
lib/prompts.ts CHANGED
@@ -15,168 +15,79 @@ export const PROMPT_FOR_IMAGE_GENERATION = `If you want to use image placeholder
15
  Examples: http://static.photos/red/320x240/133 (red-themed with seed 133), http://static.photos/640x360 (random category and image), http://static.photos/nature/1200x630/42 (nature-themed with seed 42).`
16
  export const PROMPT_FOR_PROJECT_NAME = `REQUIRED: Generate a name for the project, based on the user's request. Try to be creative and unique. Add a emoji at the end of the name. It should be short, like 6 words. Be fancy, creative and funny. DON'T FORGET IT, IT'S IMPORTANT!`
17
 
18
- export const INITIAL_SYSTEM_PROMPT_LIGHT = `You are an expert UI/UX and Front-End Developer.
19
- No need to explain what you did. Just return the expected result. Use always TailwindCSS, don't forget to import it.
20
- Return the results following this format:
21
- 1. Start with ${PROJECT_NAME_START}.
22
- 2. Add the name of the project, right after the start tag.
23
- 3. Close the start tag with the ${PROJECT_NAME_END}.
24
- 4. The name of the project should be short and concise.
25
- 5. Generate files in this ORDER: index.html FIRST, then style.css, then script.js, then web components if needed.
26
- 6. For each file, start with ${NEW_FILE_START}.
27
- 7. Add the file name right after the start tag.
28
- 8. Close the start tag with the ${NEW_FILE_END}.
29
- 9. Start the file content with the triple backticks and appropriate language marker
30
- 10. Insert the file content there.
31
- 11. Close with the triple backticks, like \`\`\`.
32
- 12. Repeat for each file.
33
- Example Code:
34
- ${PROJECT_NAME_START} Project Name ${PROJECT_NAME_END}
35
- ${NEW_FILE_START}index.html${NEW_FILE_END}
36
- \`\`\`html
37
- <!DOCTYPE html>
38
- <html lang="en">
39
- <head>
40
- <meta charset="UTF-8">
41
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
42
- <title>Index</title>
43
- <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
44
- <link rel="stylesheet" href="style.css">
45
- <script src="https://cdn.tailwindcss.com"></script>
46
- <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
47
- <script src="https://unpkg.com/feather-icons"></script>
48
- </head>
49
- <body>
50
- <h1>Hello World</h1>
51
- <custom-example></custom-example>
52
- <script src="components/example.js"></script>
53
- <script src="script.js"></script>
54
- <script>feather.replace();</script>
55
- </body>
56
- </html>
57
- \`\`\`
58
- CRITICAL: The first file MUST always be index.html.`
59
-
60
- export const FOLLOW_UP_SYSTEM_PROMPT_LIGHT = `You are an expert UI/UX and Front-End Developer modifying existing files (HTML, CSS, JavaScript).
61
- You MUST output ONLY the changes required using the following UPDATE_FILE_START and SEARCH/REPLACE format. Do NOT output the entire file.
62
- Do NOT explain the changes or what you did, just return the expected results.
63
- Update Format Rules:
64
- 1. Start with ${PROJECT_NAME_START}.
65
- 2. Add the name of the project, right after the start tag.
66
- 3. Close the start tag with the ${PROJECT_NAME_END}.
67
- 4. Start with ${UPDATE_FILE_START}
68
- 5. Provide the name of the file you are modifying (index.html, style.css, script.js, etc.).
69
- 6. Close the start tag with the ${UPDATE_FILE_END}.
70
- 7. Start with ${SEARCH_START}
71
- 8. Provide the exact lines from the current code that need to be replaced.
72
- 9. Use ${DIVIDER} to separate the search block from the replacement.
73
- 10. Provide the new lines that should replace the original lines.
74
- 11. End with ${REPLACE_END}
75
- 12. You can use multiple SEARCH/REPLACE blocks if changes are needed in different parts of the file.
76
- 13. To insert code, use an empty SEARCH block (only ${SEARCH_START} and ${DIVIDER} on their lines) if inserting at the very beginning, otherwise provide the line *before* the insertion point in the SEARCH block and include that line plus the new lines in the REPLACE block.
77
- 14. To delete code, provide the lines to delete in the SEARCH block and leave the REPLACE block empty (only ${DIVIDER} and ${REPLACE_END} on their lines).
78
- 15. IMPORTANT: The SEARCH block must *exactly* match the current code, including indentation and whitespace.
79
- Example Modifying Code:
80
- \`\`\`
81
- ${PROJECT_NAME_START} Project Name ${PROJECT_NAME_END}
82
- ${UPDATE_FILE_START}index.html${UPDATE_FILE_END}
83
- ${SEARCH_START}
84
- <h1>Old Title</h1>
85
- ${DIVIDER}
86
- <h1>New Title</h1>
87
- ${REPLACE_END}
88
- ${SEARCH_START}
89
- </body>
90
- ${DIVIDER}
91
- <script src="script.js"></script>
92
- </body>
93
- ${REPLACE_END}
94
- \`\`\`
95
- Example Updating CSS:
96
- \`\`\`
97
- ${UPDATE_FILE_START}style.css${UPDATE_FILE_END}
98
- ${SEARCH_START}
99
- body {
100
- background: white;
101
- }
102
- ${DIVIDER}
103
- body {
104
- background: linear-gradient(to right, #667eea, #764ba2);
105
- }
106
- ${REPLACE_END}
107
- \`\`\`
108
- Example Deleting Code:
109
- \`\`\`
110
- ${UPDATE_FILE_START}index.html${UPDATE_FILE_END}
111
- ${SEARCH_START}
112
- <p>This paragraph will be deleted.</p>
113
- ${DIVIDER}
114
- ${REPLACE_END}
115
- \`\`\`
116
- For creating new files, use the following format:
117
- 1. Start with ${NEW_FILE_START}.
118
- 2. Add the name of the file (e.g., about.html, style.css, script.js, components/navbar.js), right after the start tag.
119
- 3. Close the start tag with the ${NEW_FILE_END}.
120
- 4. Start the file content with the triple backticks and appropriate language marker (\`\`\`html, \`\`\`css, or \`\`\`javascript).
121
- 5. Insert the file content there.
122
- 6. Close with the triple backticks, like \`\`\`.
123
- 7. Repeat for additional files.
124
- Example Creating New HTML Page:
125
- ${NEW_FILE_START}about.html${NEW_FILE_END}
126
- \`\`\`html
127
- <!DOCTYPE html>
128
- <html lang="en">
129
- <head>
130
- <meta charset="UTF-8">
131
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
132
- <title>About</title>
133
- <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
134
- <link rel="stylesheet" href="style.css">
135
- <script src="https://cdn.tailwindcss.com"></script>
136
- </head>
137
- <body>
138
- <h1>About Page</h1>
139
- <script src="script.js"></script>
140
- </body>
141
- </html>
142
- \`\`\`
143
- No need to explain what you did. Just return the expected result.`
144
-
145
  export const INITIAL_SYSTEM_PROMPT = `You are an expert UI/UX and Front-End Developer.
146
  You create website in a way a designer would, using ONLY HTML, CSS and Javascript.
147
  Try to create the best UI possible. Important: Make the website responsive by using TailwindCSS. Use it as much as you can, if you can't use it, use custom css (make sure to import tailwind with <script src="https://cdn.tailwindcss.com"></script> in the head).
148
  Also try to elaborate as much as you can, to create something unique, with a great design.
149
  If you want to use ICONS import Feather Icons (Make sure to add <script src="https://unpkg.com/feather-icons"></script> and <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> in the head., and <script>feather.replace();</script> in the body. Ex : <i data-feather="user"></i>).
 
150
  Don't hesitate to use real public API for the datas, you can find good ones here https://github.com/public-apis/public-apis depending on what the user asks for.
151
  You can create multiple pages website at once (following the format rules below) or a Single Page Application. But make sure to create multiple pages if the user asks for different pages.
152
  IMPORTANT: To avoid duplicate code across pages, you MUST create separate style.css and script.js files for shared CSS and JavaScript code. Each HTML file should link to these files using <link rel="stylesheet" href="style.css"> and <script src="script.js"></script>.
153
  WEB COMPONENTS: For reusable UI elements like navbars, footers, sidebars, headers, etc., create Native Web Components as separate files in components/ folder:
154
- - Create each component as a separate .js file in components/ folder (e.g., components/example.js)
155
  - Each component file defines a class extending HTMLElement and registers it with customElements.define()
156
  - Use Shadow DOM for style encapsulation
157
  - Components render using template literals with inline styles
158
- - Include component files in HTML before using them: <script src="components/example.js"></script>
159
- - Use them in HTML pages with custom element tags (e.g., <custom-example></custom-example>)
160
  - If you want to use ICON you can use Feather Icons, as it's already included in the main pages.
161
  IMPORTANT: NEVER USE ONCLICK FUNCTION TO MAKE A REDIRECT TO NEW PAGE. MAKE SURE TO ALWAYS USE <a href=""/>, OTHERWISE IT WONT WORK WITH SHADOW ROOT AND WEB COMPONENTS.
162
- Example components/example.js:
163
- class CustomExample extends HTMLElement {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  connectedCallback() {
165
  this.attachShadow({ mode: 'open' });
166
  this.shadowRoot.innerHTML = \`
167
  <style>
168
- /* Add your styles here */
 
 
 
 
 
169
  </style>
170
- <div>
171
- <h1>Example Component</h1>
172
- </div>
173
  \`;
174
  }
175
  }
176
- customElements.define('custom-example', CustomExample);
 
177
  Then in HTML, include the component scripts and use the tags:
178
- <script src="components/example.js"></script>
179
- <custom-example></custom-example>
 
 
180
  ${PROMPT_FOR_IMAGE_GENERATION}
181
  ${PROMPT_FOR_PROJECT_NAME}
182
  No need to explain what you did. Just return the expected result. AVOID Chinese characters in the code if not asked by the user.
@@ -185,14 +96,15 @@ Return the results following this format:
185
  2. Add the name of the project, right after the start tag.
186
  3. Close the start tag with the ${PROJECT_NAME_END}.
187
  4. The name of the project should be short and concise.
188
- 5. Generate files in this ORDER: index.html FIRST, then style.css, then script.js, then web components if needed.
189
  6. For each file, start with ${NEW_FILE_START}.
190
- 7. Add the file name right after the start tag.
191
  8. Close the start tag with the ${NEW_FILE_END}.
192
- 9. Start the file content with the triple backticks and appropriate language marker
193
  10. Insert the file content there.
194
  11. Close with the triple backticks, like \`\`\`.
195
  12. Repeat for each file.
 
196
  Example Code:
197
  ${PROJECT_NAME_START} Project Name ${PROJECT_NAME_END}
198
  ${NEW_FILE_START}index.html${NEW_FILE_END}
@@ -210,15 +122,83 @@ ${NEW_FILE_START}index.html${NEW_FILE_END}
210
  <script src="https://unpkg.com/feather-icons"></script>
211
  </head>
212
  <body>
213
- <h1>Hello World</h1>
214
- <custom-example></custom-example>
215
- <script src="components/example.js"></script>
 
 
216
  <script src="script.js"></script>
217
  <script>feather.replace();</script>
218
  </body>
219
  </html>
220
  \`\`\`
221
- CRITICAL: The first file MUST always be index.html.`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
 
223
  export const FOLLOW_UP_SYSTEM_PROMPT = `You are an expert UI/UX and Front-End Developer modifying existing files (HTML, CSS, JavaScript).
224
  The user wants to apply changes and probably add new features/pages/styles/scripts to the website, based on their request.
 
15
  Examples: http://static.photos/red/320x240/133 (red-themed with seed 133), http://static.photos/640x360 (random category and image), http://static.photos/nature/1200x630/42 (nature-themed with seed 42).`
16
  export const PROMPT_FOR_PROJECT_NAME = `REQUIRED: Generate a name for the project, based on the user's request. Try to be creative and unique. Add a emoji at the end of the name. It should be short, like 6 words. Be fancy, creative and funny. DON'T FORGET IT, IT'S IMPORTANT!`
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  export const INITIAL_SYSTEM_PROMPT = `You are an expert UI/UX and Front-End Developer.
19
  You create website in a way a designer would, using ONLY HTML, CSS and Javascript.
20
  Try to create the best UI possible. Important: Make the website responsive by using TailwindCSS. Use it as much as you can, if you can't use it, use custom css (make sure to import tailwind with <script src="https://cdn.tailwindcss.com"></script> in the head).
21
  Also try to elaborate as much as you can, to create something unique, with a great design.
22
  If you want to use ICONS import Feather Icons (Make sure to add <script src="https://unpkg.com/feather-icons"></script> and <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> in the head., and <script>feather.replace();</script> in the body. Ex : <i data-feather="user"></i>).
23
+ For interactive animations you can use: Vanta.js (Make sure to add <script src="https://cdn.jsdelivr.net/npm/vanta@latest/dist/vanta.globe.min.js"></script> and <script>VANTA.GLOBE({...</script> in the body.).
24
  Don't hesitate to use real public API for the datas, you can find good ones here https://github.com/public-apis/public-apis depending on what the user asks for.
25
  You can create multiple pages website at once (following the format rules below) or a Single Page Application. But make sure to create multiple pages if the user asks for different pages.
26
  IMPORTANT: To avoid duplicate code across pages, you MUST create separate style.css and script.js files for shared CSS and JavaScript code. Each HTML file should link to these files using <link rel="stylesheet" href="style.css"> and <script src="script.js"></script>.
27
  WEB COMPONENTS: For reusable UI elements like navbars, footers, sidebars, headers, etc., create Native Web Components as separate files in components/ folder:
28
+ - Create each component as a separate .js file in components/ folder (e.g., components/navbar.js, components/footer.js)
29
  - Each component file defines a class extending HTMLElement and registers it with customElements.define()
30
  - Use Shadow DOM for style encapsulation
31
  - Components render using template literals with inline styles
32
+ - Include component files in HTML before using them: <script src="components/navbar.js"></script>
33
+ - Use them in HTML pages with custom element tags (e.g., <custom-navbar></custom-navbar>)
34
  - If you want to use ICON you can use Feather Icons, as it's already included in the main pages.
35
  IMPORTANT: NEVER USE ONCLICK FUNCTION TO MAKE A REDIRECT TO NEW PAGE. MAKE SURE TO ALWAYS USE <a href=""/>, OTHERWISE IT WONT WORK WITH SHADOW ROOT AND WEB COMPONENTS.
36
+ Example components/navbar.js:
37
+ class CustomNavbar extends HTMLElement {
38
+ connectedCallback() {
39
+ this.attachShadow({ mode: 'open' });
40
+ this.shadowRoot.innerHTML = \`
41
+ <style>
42
+ nav {
43
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
44
+ padding: 1rem;
45
+ display: flex;
46
+ justify-content: space-between;
47
+ align-items: center;
48
+ }
49
+ .logo { color: white; font-weight: bold; }
50
+ ul { display: flex; gap: 1rem; list-style: none; margin: 0; padding: 0; }
51
+ a { color: white; text-decoration: none; }
52
+ </style>
53
+ <nav>
54
+ <div class="logo">My Website</div>
55
+ <ul>
56
+ <li><a href="/">Home</a></li>
57
+ <li><a href="/about.html">About</a></li>
58
+ </ul>
59
+ </nav>
60
+ \`;
61
+ }
62
+ }
63
+ customElements.define('custom-navbar', CustomNavbar);
64
+
65
+ Example components/footer.js:
66
+ class CustomFooter extends HTMLElement {
67
  connectedCallback() {
68
  this.attachShadow({ mode: 'open' });
69
  this.shadowRoot.innerHTML = \`
70
  <style>
71
+ footer {
72
+ background: #1a202c;
73
+ color: white;
74
+ padding: 2rem;
75
+ text-align: center;
76
+ }
77
  </style>
78
+ <footer>
79
+ <p>&copy; 2024 My Website. All rights reserved.</p>
80
+ </footer>
81
  \`;
82
  }
83
  }
84
+ customElements.define('custom-footer', CustomFooter);
85
+
86
  Then in HTML, include the component scripts and use the tags:
87
+ <script src="components/navbar.js"></script>
88
+ <script src="components/footer.js"></script>
89
+ <custom-navbar></custom-navbar>
90
+ <custom-footer></custom-footer>
91
  ${PROMPT_FOR_IMAGE_GENERATION}
92
  ${PROMPT_FOR_PROJECT_NAME}
93
  No need to explain what you did. Just return the expected result. AVOID Chinese characters in the code if not asked by the user.
 
96
  2. Add the name of the project, right after the start tag.
97
  3. Close the start tag with the ${PROJECT_NAME_END}.
98
  4. The name of the project should be short and concise.
99
+ 5. Generate files in this ORDER: index.html FIRST, then style.css, then script.js, then web components (components/navbar.js, components/footer.js, etc.), then other HTML pages.
100
  6. For each file, start with ${NEW_FILE_START}.
101
+ 7. Add the file name (index.html, style.css, script.js, components/navbar.js, about.html, etc.) right after the start tag.
102
  8. Close the start tag with the ${NEW_FILE_END}.
103
+ 9. Start the file content with the triple backticks and appropriate language marker (\`\`\`html, \`\`\`css, or \`\`\`javascript).
104
  10. Insert the file content there.
105
  11. Close with the triple backticks, like \`\`\`.
106
  12. Repeat for each file.
107
+ 13. Web components should be in separate .js files in components/ folder and included via <script> tags before use.
108
  Example Code:
109
  ${PROJECT_NAME_START} Project Name ${PROJECT_NAME_END}
110
  ${NEW_FILE_START}index.html${NEW_FILE_END}
 
122
  <script src="https://unpkg.com/feather-icons"></script>
123
  </head>
124
  <body>
125
+ <custom-navbar></custom-navbar>
126
+ <h1>Hello World</h1>
127
+ <custom-footer></custom-footer>
128
+ <script src="components/navbar.js"></script>
129
+ <script src="components/footer.js"></script>
130
  <script src="script.js"></script>
131
  <script>feather.replace();</script>
132
  </body>
133
  </html>
134
  \`\`\`
135
+ ${NEW_FILE_START}style.css${NEW_FILE_END}
136
+ \`\`\`css
137
+ /* Shared styles across all pages */
138
+ body {
139
+ font-family: 'Inter', sans-serif;
140
+ }
141
+ \`\`\`
142
+ ${NEW_FILE_START}script.js${NEW_FILE_END}
143
+ \`\`\`javascript
144
+ // Shared JavaScript across all pages
145
+ console.log('App loaded');
146
+ \`\`\`
147
+ ${NEW_FILE_START}components/navbar.js${NEW_FILE_END}
148
+ \`\`\`javascript
149
+ class CustomNavbar extends HTMLElement {
150
+ connectedCallback() {
151
+ this.attachShadow({ mode: 'open' });
152
+ this.shadowRoot.innerHTML = \`
153
+ <style>
154
+ nav {
155
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
156
+ padding: 1rem;
157
+ display: flex;
158
+ justify-content: space-between;
159
+ align-items: center;
160
+ }
161
+ .logo { color: white; font-weight: bold; font-size: 1.25rem; }
162
+ ul { display: flex; gap: 1rem; list-style: none; margin: 0; padding: 0; }
163
+ a { color: white; text-decoration: none; transition: opacity 0.2s; }
164
+ a:hover { opacity: 0.8; }
165
+ </style>
166
+ <nav>
167
+ <div class="logo">My Website</div>
168
+ <ul>
169
+ <li><a href="/">Home</a></li>
170
+ <li><a href="/about.html">About</a></li>
171
+ </ul>
172
+ </nav>
173
+ \`;
174
+ }
175
+ }
176
+ customElements.define('custom-navbar', CustomNavbar);
177
+ \`\`\`
178
+ ${NEW_FILE_START}components/footer.js${NEW_FILE_END}
179
+ \`\`\`javascript
180
+ class CustomFooter extends HTMLElement {
181
+ connectedCallback() {
182
+ this.attachShadow({ mode: 'open' });
183
+ this.shadowRoot.innerHTML = \`
184
+ <style>
185
+ footer {
186
+ background: #1a202c;
187
+ color: white;
188
+ padding: 2rem;
189
+ text-align: center;
190
+ margin-top: auto;
191
+ }
192
+ </style>
193
+ <footer>
194
+ <p>&copy; 2024 My Website. All rights reserved.</p>
195
+ </footer>
196
+ \`;
197
+ }
198
+ }
199
+ customElements.define('custom-footer', CustomFooter);
200
+ \`\`\`
201
+ CRITICAL: The first file MUST always be index.html. Then generate style.css and script.js. If you create web components, place them in components/ folder as separate .js files. All HTML files MUST include <link rel="stylesheet" href="style.css"> and component scripts before using them (e.g., <script src="components/navbar.js"></script>), then <script src="script.js"></script>.`
202
 
203
  export const FOLLOW_UP_SYSTEM_PROMPT = `You are an expert UI/UX and Front-End Developer modifying existing files (HTML, CSS, JavaScript).
204
  The user wants to apply changes and probably add new features/pages/styles/scripts to the website, based on their request.
lib/providers.ts CHANGED
@@ -2,7 +2,6 @@ import DeepSeekLogo from "@/assets/deepseek.svg";
2
  import QwenLogo from "@/assets/qwen.svg";
3
  import KimiLogo from "@/assets/kimi.svg";
4
  import ZaiLogo from "@/assets/zai.svg";
5
- import MiniMaxLogo from "@/assets/minimax.svg";
6
 
7
  export const PROVIDERS = {
8
  "fireworks-ai": {
@@ -48,28 +47,29 @@ export const MODELS = [
48
  logo: DeepSeekLogo,
49
  companyName: "DeepSeek",
50
  },
51
- // {
52
- // value: "deepseek-ai/DeepSeek-V3.1",
53
- // label: "DeepSeek V3.1",
54
- // providers: ["fireworks-ai", "novita"],
55
- // autoProvider: "fireworks-ai",
56
- // logo: DeepSeekLogo,
57
- // companyName: "DeepSeek",
58
- // },
59
- // {
60
- // value: "deepseek-ai/DeepSeek-V3.1-Terminus",
61
- // label: "DeepSeek V3.1 Terminus",
62
- // providers: ["novita"],
63
- // autoProvider: "novita",
64
- // logo: DeepSeekLogo,
65
- // companyName: "DeepSeek",
66
- // },
67
  {
68
  value: "deepseek-ai/DeepSeek-V3.2-Exp",
69
  label: "DeepSeek V3.2 Exp",
70
  providers: ["novita"],
71
  autoProvider: "novita",
72
  logo: DeepSeekLogo,
 
73
  companyName: "DeepSeek",
74
  },
75
  {
@@ -88,35 +88,18 @@ export const MODELS = [
88
  logo: KimiLogo,
89
  companyName: "Kimi",
90
  },
91
- // {
92
- // value: "moonshotai/Kimi-K2-Instruct-0905",
93
- // label: "Kimi K2 Instruct 0905",
94
- // providers: ["together", "groq", "novita"],
95
- // autoProvider: "groq",
96
- // logo: KimiLogo,
97
- // companyName: "Kimi",
98
- // },
99
  {
100
- value: "moonshotai/Kimi-K2-Thinking",
101
- label: "Kimi K2 Thinking",
 
 
102
  logo: KimiLogo,
103
  companyName: "Kimi",
104
- isNew: true,
105
- temperature: 1.0,
106
  },
107
  {
108
  value: "zai-org/GLM-4.6",
109
  label: "GLM-4.6",
110
  logo: ZaiLogo,
111
  companyName: "Z.ai",
112
- },
113
- {
114
- value: "MiniMaxAI/MiniMax-M2",
115
- label: "MiniMax M2",
116
- logo: MiniMaxLogo,
117
- companyName: "MiniMax",
118
- top_k: 40,
119
- temperature: 1.0,
120
- top_p: 0.95,
121
- },
122
  ];
 
2
  import QwenLogo from "@/assets/qwen.svg";
3
  import KimiLogo from "@/assets/kimi.svg";
4
  import ZaiLogo from "@/assets/zai.svg";
 
5
 
6
  export const PROVIDERS = {
7
  "fireworks-ai": {
 
47
  logo: DeepSeekLogo,
48
  companyName: "DeepSeek",
49
  },
50
+ {
51
+ value: "deepseek-ai/DeepSeek-V3.1",
52
+ label: "DeepSeek V3.1",
53
+ providers: ["fireworks-ai", "novita"],
54
+ autoProvider: "fireworks-ai",
55
+ logo: DeepSeekLogo,
56
+ companyName: "DeepSeek",
57
+ },
58
+ {
59
+ value: "deepseek-ai/DeepSeek-V3.1-Terminus",
60
+ label: "DeepSeek V3.1 Terminus",
61
+ providers: ["novita"],
62
+ autoProvider: "novita",
63
+ logo: DeepSeekLogo,
64
+ companyName: "DeepSeek",
65
+ },
66
  {
67
  value: "deepseek-ai/DeepSeek-V3.2-Exp",
68
  label: "DeepSeek V3.2 Exp",
69
  providers: ["novita"],
70
  autoProvider: "novita",
71
  logo: DeepSeekLogo,
72
+ isNew: true,
73
  companyName: "DeepSeek",
74
  },
75
  {
 
88
  logo: KimiLogo,
89
  companyName: "Kimi",
90
  },
 
 
 
 
 
 
 
 
91
  {
92
+ value: "moonshotai/Kimi-K2-Instruct-0905",
93
+ label: "Kimi K2 Instruct 0905",
94
+ providers: ["together", "groq", "novita"],
95
+ autoProvider: "groq",
96
  logo: KimiLogo,
97
  companyName: "Kimi",
 
 
98
  },
99
  {
100
  value: "zai-org/GLM-4.6",
101
  label: "GLM-4.6",
102
  logo: ZaiLogo,
103
  companyName: "Z.ai",
104
+ }
 
 
 
 
 
 
 
 
 
105
  ];
middleware.ts CHANGED
@@ -1,10 +1,9 @@
1
  import { NextResponse } from "next/server";
2
  import type { NextRequest } from "next/server";
3
 
4
- export function middleware(request: NextRequest) {
5
  const headers = new Headers(request.headers);
6
  headers.set("x-current-host", request.nextUrl.host);
7
- headers.set("x-invoke-path", request.nextUrl.pathname + request.nextUrl.search);
8
 
9
  const response = NextResponse.next({ headers });
10
 
 
1
  import { NextResponse } from "next/server";
2
  import type { NextRequest } from "next/server";
3
 
4
+ export function middleware(request: NextRequest) {
5
  const headers = new Headers(request.headers);
6
  headers.set("x-current-host", request.nextUrl.host);
 
7
 
8
  const response = NextResponse.next({ headers });
9
 
package-lock.json CHANGED
@@ -33,7 +33,6 @@
33
  "clsx": "^2.1.1",
34
  "date-fns": "^4.1.0",
35
  "framer-motion": "^12.23.22",
36
- "jszip": "^3.10.1",
37
  "log4js": "^6.9.1",
38
  "log4js-json-layout": "^2.2.3",
39
  "lucide-react": "^0.542.0",
@@ -320,13 +319,13 @@
320
  }
321
  },
322
  "node_modules/@huggingface/inference": {
323
- "version": "4.13.1",
324
- "resolved": "https://registry.npmjs.org/@huggingface/inference/-/inference-4.13.1.tgz",
325
- "integrity": "sha512-tP63myCjsH+2CqkOaDklJ9MDpUzp6esGMS7RHFXpfv66DWub43kaGTDpI49arqLXM+yFH6FW868eIDgespw5Uw==",
326
  "license": "MIT",
327
  "dependencies": {
328
  "@huggingface/jinja": "^0.5.1",
329
- "@huggingface/tasks": "^0.19.62"
330
  },
331
  "engines": {
332
  "node": ">=18"
@@ -342,9 +341,9 @@
342
  }
343
  },
344
  "node_modules/@huggingface/tasks": {
345
- "version": "0.19.63",
346
- "resolved": "https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.19.63.tgz",
347
- "integrity": "sha512-hmd8e5fdjRiIJE7/EYWXS+Pm2SAu89xjZEgfZddN10ubWqlelXLyj2YgHZrVDEVkVA+5+ImMZUpQIez7b2//fw==",
348
  "license": "MIT"
349
  },
350
  "node_modules/@humanfs/core": {
@@ -3240,12 +3239,6 @@
3240
  "toggle-selection": "^1.0.6"
3241
  }
3242
  },
3243
- "node_modules/core-util-is": {
3244
- "version": "1.0.3",
3245
- "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
3246
- "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
3247
- "license": "MIT"
3248
- },
3249
  "node_modules/cross-spawn": {
3250
  "version": "7.0.6",
3251
  "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -4092,12 +4085,6 @@
4092
  "node": ">= 4"
4093
  }
4094
  },
4095
- "node_modules/immediate": {
4096
- "version": "3.0.6",
4097
- "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
4098
- "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
4099
- "license": "MIT"
4100
- },
4101
  "node_modules/import-fresh": {
4102
  "version": "3.3.1",
4103
  "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -4125,12 +4112,6 @@
4125
  "node": ">=0.8.19"
4126
  }
4127
  },
4128
- "node_modules/inherits": {
4129
- "version": "2.0.4",
4130
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
4131
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
4132
- "license": "ISC"
4133
- },
4134
  "node_modules/inline-style-prefixer": {
4135
  "version": "7.0.1",
4136
  "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz",
@@ -4177,12 +4158,6 @@
4177
  "node": ">=0.12.0"
4178
  }
4179
  },
4180
- "node_modules/isarray": {
4181
- "version": "1.0.0",
4182
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
4183
- "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
4184
- "license": "MIT"
4185
- },
4186
  "node_modules/isexe": {
4187
  "version": "2.0.0",
4188
  "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -4298,18 +4273,6 @@
4298
  "graceful-fs": "^4.1.6"
4299
  }
4300
  },
4301
- "node_modules/jszip": {
4302
- "version": "3.10.1",
4303
- "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
4304
- "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
4305
- "license": "(MIT OR GPL-3.0-or-later)",
4306
- "dependencies": {
4307
- "lie": "~3.3.0",
4308
- "pako": "~1.0.2",
4309
- "readable-stream": "~2.3.6",
4310
- "setimmediate": "^1.0.5"
4311
- }
4312
- },
4313
  "node_modules/kareem": {
4314
  "version": "2.6.3",
4315
  "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
@@ -4343,15 +4306,6 @@
4343
  "node": ">= 0.8.0"
4344
  }
4345
  },
4346
- "node_modules/lie": {
4347
- "version": "3.3.0",
4348
- "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
4349
- "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
4350
- "license": "MIT",
4351
- "dependencies": {
4352
- "immediate": "~3.0.5"
4353
- }
4354
- },
4355
  "node_modules/lightningcss": {
4356
  "version": "1.30.1",
4357
  "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
@@ -5132,12 +5086,6 @@
5132
  "url": "https://github.com/sponsors/sindresorhus"
5133
  }
5134
  },
5135
- "node_modules/pako": {
5136
- "version": "1.0.11",
5137
- "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
5138
- "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
5139
- "license": "(MIT AND Zlib)"
5140
- },
5141
  "node_modules/parent-module": {
5142
  "version": "1.0.1",
5143
  "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -5228,12 +5176,6 @@
5228
  "node": ">= 0.8.0"
5229
  }
5230
  },
5231
- "node_modules/process-nextick-args": {
5232
- "version": "2.0.1",
5233
- "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
5234
- "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
5235
- "license": "MIT"
5236
- },
5237
  "node_modules/proxy-from-env": {
5238
  "version": "1.1.0",
5239
  "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -5404,27 +5346,6 @@
5404
  "react-dom": "*"
5405
  }
5406
  },
5407
- "node_modules/readable-stream": {
5408
- "version": "2.3.8",
5409
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
5410
- "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
5411
- "license": "MIT",
5412
- "dependencies": {
5413
- "core-util-is": "~1.0.0",
5414
- "inherits": "~2.0.3",
5415
- "isarray": "~1.0.0",
5416
- "process-nextick-args": "~2.0.0",
5417
- "safe-buffer": "~5.1.1",
5418
- "string_decoder": "~1.1.1",
5419
- "util-deprecate": "~1.0.1"
5420
- }
5421
- },
5422
- "node_modules/readable-stream/node_modules/safe-buffer": {
5423
- "version": "5.1.2",
5424
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
5425
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
5426
- "license": "MIT"
5427
- },
5428
  "node_modules/require-from-string": {
5429
  "version": "2.0.2",
5430
  "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@@ -5587,12 +5508,6 @@
5587
  "node": ">=6.9"
5588
  }
5589
  },
5590
- "node_modules/setimmediate": {
5591
- "version": "1.0.5",
5592
- "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
5593
- "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
5594
- "license": "MIT"
5595
- },
5596
  "node_modules/sharp": {
5597
  "version": "0.34.3",
5598
  "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz",
@@ -5788,21 +5703,6 @@
5788
  "node": ">=8.0"
5789
  }
5790
  },
5791
- "node_modules/string_decoder": {
5792
- "version": "1.1.1",
5793
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
5794
- "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
5795
- "license": "MIT",
5796
- "dependencies": {
5797
- "safe-buffer": "~5.1.0"
5798
- }
5799
- },
5800
- "node_modules/string_decoder/node_modules/safe-buffer": {
5801
- "version": "5.1.2",
5802
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
5803
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
5804
- "license": "MIT"
5805
- },
5806
  "node_modules/strip-json-comments": {
5807
  "version": "3.1.1",
5808
  "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -6257,12 +6157,6 @@
6257
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
6258
  }
6259
  },
6260
- "node_modules/util-deprecate": {
6261
- "version": "1.0.2",
6262
- "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
6263
- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
6264
- "license": "MIT"
6265
- },
6266
  "node_modules/watchpack": {
6267
  "version": "2.4.4",
6268
  "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
 
33
  "clsx": "^2.1.1",
34
  "date-fns": "^4.1.0",
35
  "framer-motion": "^12.23.22",
 
36
  "log4js": "^6.9.1",
37
  "log4js-json-layout": "^2.2.3",
38
  "lucide-react": "^0.542.0",
 
319
  }
320
  },
321
  "node_modules/@huggingface/inference": {
322
+ "version": "4.7.1",
323
+ "resolved": "https://registry.npmjs.org/@huggingface/inference/-/inference-4.7.1.tgz",
324
+ "integrity": "sha512-gXrMocGDsE6kUZPEj82c3O+/OKnIfbHvg9rYjGA6svbWrYVmHCIAdCrrgCwNl2v5GELfPJrrfIv0bvzCTfa64A==",
325
  "license": "MIT",
326
  "dependencies": {
327
  "@huggingface/jinja": "^0.5.1",
328
+ "@huggingface/tasks": "^0.19.35"
329
  },
330
  "engines": {
331
  "node": ">=18"
 
341
  }
342
  },
343
  "node_modules/@huggingface/tasks": {
344
+ "version": "0.19.43",
345
+ "resolved": "https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.19.43.tgz",
346
+ "integrity": "sha512-ANO23K3ugclBl6VLwdt+7MxBkRkKEE17USUSqprHb29UB5ISigH+0AJcEuDA064uzn0hqYrG/nOcv1yARRt8bw==",
347
  "license": "MIT"
348
  },
349
  "node_modules/@humanfs/core": {
 
3239
  "toggle-selection": "^1.0.6"
3240
  }
3241
  },
 
 
 
 
 
 
3242
  "node_modules/cross-spawn": {
3243
  "version": "7.0.6",
3244
  "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
 
4085
  "node": ">= 4"
4086
  }
4087
  },
 
 
 
 
 
 
4088
  "node_modules/import-fresh": {
4089
  "version": "3.3.1",
4090
  "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
 
4112
  "node": ">=0.8.19"
4113
  }
4114
  },
 
 
 
 
 
 
4115
  "node_modules/inline-style-prefixer": {
4116
  "version": "7.0.1",
4117
  "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz",
 
4158
  "node": ">=0.12.0"
4159
  }
4160
  },
 
 
 
 
 
 
4161
  "node_modules/isexe": {
4162
  "version": "2.0.0",
4163
  "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
 
4273
  "graceful-fs": "^4.1.6"
4274
  }
4275
  },
 
 
 
 
 
 
 
 
 
 
 
 
4276
  "node_modules/kareem": {
4277
  "version": "2.6.3",
4278
  "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
 
4306
  "node": ">= 0.8.0"
4307
  }
4308
  },
 
 
 
 
 
 
 
 
 
4309
  "node_modules/lightningcss": {
4310
  "version": "1.30.1",
4311
  "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
 
5086
  "url": "https://github.com/sponsors/sindresorhus"
5087
  }
5088
  },
 
 
 
 
 
 
5089
  "node_modules/parent-module": {
5090
  "version": "1.0.1",
5091
  "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
 
5176
  "node": ">= 0.8.0"
5177
  }
5178
  },
 
 
 
 
 
 
5179
  "node_modules/proxy-from-env": {
5180
  "version": "1.1.0",
5181
  "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
 
5346
  "react-dom": "*"
5347
  }
5348
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5349
  "node_modules/require-from-string": {
5350
  "version": "2.0.2",
5351
  "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
 
5508
  "node": ">=6.9"
5509
  }
5510
  },
 
 
 
 
 
 
5511
  "node_modules/sharp": {
5512
  "version": "0.34.3",
5513
  "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz",
 
5703
  "node": ">=8.0"
5704
  }
5705
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5706
  "node_modules/strip-json-comments": {
5707
  "version": "3.1.1",
5708
  "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
 
6157
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
6158
  }
6159
  },
 
 
 
 
 
 
6160
  "node_modules/watchpack": {
6161
  "version": "2.4.4",
6162
  "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
package.json CHANGED
@@ -33,7 +33,6 @@
33
  "clsx": "^2.1.1",
34
  "date-fns": "^4.1.0",
35
  "framer-motion": "^12.23.22",
36
- "jszip": "^3.10.1",
37
  "log4js": "^6.9.1",
38
  "log4js-json-layout": "^2.2.3",
39
  "lucide-react": "^0.542.0",
 
33
  "clsx": "^2.1.1",
34
  "date-fns": "^4.1.0",
35
  "framer-motion": "^12.23.22",
 
36
  "log4js": "^6.9.1",
37
  "log4js-json-layout": "^2.2.3",
38
  "lucide-react": "^0.542.0",