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
@@ -6,24 +6,38 @@ import { InferenceClient } from "@huggingface/inference";
6
 
7
  import { MODELS } from "@/lib/providers";
8
  import {
 
9
  FOLLOW_UP_SYSTEM_PROMPT,
10
- FOLLOW_UP_SYSTEM_PROMPT_LIGHT,
11
  INITIAL_SYSTEM_PROMPT,
12
- INITIAL_SYSTEM_PROMPT_LIGHT,
13
  MAX_REQUESTS_PER_IP,
 
 
 
 
 
 
14
  PROMPT_FOR_PROJECT_NAME,
15
  } from "@/lib/prompts";
 
16
  import MY_TOKEN_KEY from "@/lib/get-cookie-name";
17
  import { Page } from "@/types";
 
18
  import { isAuthenticated } from "@/lib/auth";
19
  import { getBestProvider } from "@/lib/best-provider";
 
 
 
 
20
 
21
  const ipAddresses = new Map();
22
 
 
 
 
 
23
  export async function POST(request: NextRequest) {
24
  const authHeaders = await headers();
25
- const tokenInHeaders = authHeaders.get("Authorization");
26
- const userToken = tokenInHeaders ? tokenInHeaders.replace("Bearer ", "") : request.cookies.get(MY_TOKEN_KEY())?.value;
27
 
28
  const body = await request.json();
29
  const { prompt, provider, model, redesignMarkdown, enhancedSettings, pages } = body;
@@ -63,7 +77,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 +94,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 +116,29 @@ 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,28 +146,56 @@ 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
  );
131
 
132
- while (true) {
133
- const { done, value } = await chatCompletion.next()
134
- if (done) {
135
- break;
136
- }
137
-
138
- const chunk = value.choices[0]?.delta?.content;
139
- if (chunk) {
140
- await writer.write(encoder.encode(chunk));
141
- }
142
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
 
144
  await writer.close();
145
  } catch (error: any) {
146
- if (error.message?.includes("exceeded your monthly included credits")) {
 
 
 
 
 
 
 
 
 
 
 
 
147
  await writer.write(
148
  encoder.encode(
149
  JSON.stringify({
@@ -207,9 +259,11 @@ export async function PUT(request: NextRequest) {
207
  const authHeaders = await headers();
208
 
209
  const body = await request.json();
210
- const { prompt, provider, selectedElementHtml, model, pages, files, repoId, isNew } =
211
  body;
212
 
 
 
213
  if (!prompt || pages.length === 0) {
214
  return NextResponse.json(
215
  { ok: false, error: "Missing required fields" },
@@ -243,7 +297,7 @@ export async function PUT(request: NextRequest) {
243
  ? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
244
  : authHeaders.get("x-forwarded-for");
245
 
246
- if (!token || token === "null" || token === "") {
247
  ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
248
  if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
249
  return NextResponse.json(
@@ -260,131 +314,398 @@ export async function PUT(request: NextRequest) {
260
  billTo = "huggingface";
261
  }
262
 
263
- try {
264
- const encoder = new TextEncoder();
265
- const stream = new TransformStream();
266
- const writer = stream.writable.getWriter();
267
 
268
- const response = new NextResponse(stream.readable, {
269
- headers: {
270
- "Content-Type": "text/plain; charset=utf-8",
271
- "Cache-Control": "no-cache",
272
- Connection: "keep-alive",
273
- },
274
- });
275
 
276
- (async () => {
277
- try {
278
- const client = new InferenceClient(token);
 
 
 
 
 
279
 
280
- const basePrompt = selectedModel.value.includes('MiniMax')
281
- ? FOLLOW_UP_SYSTEM_PROMPT_LIGHT
282
- : FOLLOW_UP_SYSTEM_PROMPT;
283
- const systemPrompt = basePrompt + (isNew ? PROMPT_FOR_PROJECT_NAME : "");
284
- const userContext = "You are modifying the HTML file based on the user's request.";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
 
286
- const allPages = pages || [];
287
- const pagesContext = allPages
288
- .map((p: Page) => `- ${p.path}\n${p.html}`)
289
- .join("\n\n");
290
 
291
- const assistantContext = `${selectedElementHtml
292
- ? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\` Could be in multiple pages, if so, update all the pages.`
293
- : ""
294
- }. Current pages (${allPages.length} total): ${pagesContext}. ${files?.length > 0 ? `Available images: ${files?.map((f: string) => f).join(', ')}.` : ""}`;
 
 
295
 
296
- const chatCompletion = client.chatCompletionStream(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  {
298
- model: selectedModel.value + (provider !== "auto" ? `:${provider}` : ""),
299
- messages: [
300
- {
301
- role: "system",
302
- content: systemPrompt,
303
- },
304
- {
305
- role: "user",
306
- content: userContext,
307
- },
308
- {
309
- role: "assistant",
310
- content: assistantContext,
311
- },
312
- {
313
- role: "user",
314
- content: prompt,
315
- },
316
- ],
317
- ...(selectedModel.top_k ? { top_k: selectedModel.top_k } : {}),
318
- ...(selectedModel.temperature ? { temperature: selectedModel.temperature } : {}),
319
- ...(selectedModel.top_p ? { top_p: selectedModel.top_p } : {}),
320
  },
321
- billTo ? { billTo } : {}
322
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
 
324
- // Stream the response chunks to the client
325
- while (true) {
326
- const { done, value } = await chatCompletion.next();
327
- if (done) {
328
- break;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  }
330
 
331
- const chunk = value.choices[0]?.delta?.content;
332
- if (chunk) {
333
- await writer.write(encoder.encode(chunk));
 
334
  }
335
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
 
337
- await writer.write(encoder.encode(`\n___METADATA_START___\n${JSON.stringify({
338
- repoId,
339
- isNew,
340
- userName: user.name,
341
- })}\n___METADATA_END___\n`));
342
 
343
- await writer.close();
344
- } catch (error: any) {
345
- if (error.message?.includes("exceeded your monthly included credits")) {
346
- await writer.write(
347
- encoder.encode(
348
- JSON.stringify({
349
- ok: false,
350
- openProModal: true,
351
- message: error.message,
352
- })
353
- )
354
- );
355
- } else if (error?.message?.includes("inference provider information")) {
356
- await writer.write(
357
- encoder.encode(
358
- JSON.stringify({
359
- ok: false,
360
- openSelectProvider: true,
361
- message: error.message,
362
- })
363
- )
 
364
  );
365
- } else {
366
- await writer.write(
367
- encoder.encode(
368
- JSON.stringify({
369
- ok: false,
370
- message:
371
- error.message ||
372
- "An error occurred while processing your request.",
373
- })
374
- )
375
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  }
377
- } finally {
378
- try {
379
- await writer?.close();
380
- } catch {
381
- // ignore
382
  }
383
  }
384
- })();
385
 
386
- return response;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  } catch (error: any) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  return NextResponse.json(
389
  {
390
  ok: false,
 
6
 
7
  import { MODELS } from "@/lib/providers";
8
  import {
9
+ DIVIDER,
10
  FOLLOW_UP_SYSTEM_PROMPT,
 
11
  INITIAL_SYSTEM_PROMPT,
 
12
  MAX_REQUESTS_PER_IP,
13
+ NEW_FILE_END,
14
+ NEW_FILE_START,
15
+ REPLACE_END,
16
+ SEARCH_START,
17
+ UPDATE_FILE_START,
18
+ UPDATE_FILE_END,
19
  PROMPT_FOR_PROJECT_NAME,
20
  } from "@/lib/prompts";
21
+ import { calculateMaxTokens, estimateInputTokens, getProviderSpecificConfig } from "@/lib/max-tokens";
22
  import MY_TOKEN_KEY from "@/lib/get-cookie-name";
23
  import { Page } from "@/types";
24
+ import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
25
  import { isAuthenticated } from "@/lib/auth";
26
  import { getBestProvider } from "@/lib/best-provider";
27
+ // import { rewritePrompt } from "@/lib/rewrite-prompt";
28
+ import { COLORS } from "@/lib/utils";
29
+ import { templates } from "@/lib/templates";
30
+ import { injectDeepSiteBadge, isIndexPage } from "@/lib/inject-badge";
31
 
32
  const ipAddresses = new Map();
33
 
34
+ const STREAMING_TIMEOUT = 180000;
35
+ const REQUEST_TIMEOUT = 240000;
36
+ export const maxDuration = 300;
37
+
38
  export async function POST(request: NextRequest) {
39
  const authHeaders = await headers();
40
+ const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
 
41
 
42
  const body = await request.json();
43
  const { prompt, provider, model, redesignMarkdown, enhancedSettings, pages } = body;
 
77
  ? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
78
  : authHeaders.get("x-forwarded-for");
79
 
80
+ if (!token) {
81
  ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
82
  if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
83
  return NextResponse.json(
 
94
  billTo = "huggingface";
95
  }
96
 
97
+ const selectedProvider = await getBestProvider(selectedModel.value, provider)
98
+
99
+ 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;
100
+
101
+ if (enhancedSettings.isActive) {
102
+ // rewrittenPrompt = await rewritePrompt(rewrittenPrompt, enhancedSettings, { token, billTo }, selectedModel.value, selectedProvider.provider);
103
+ }
104
+
105
  try {
106
  const encoder = new TextEncoder();
107
  const stream = new TransformStream();
 
116
  });
117
 
118
  (async () => {
119
+ // let completeResponse = "";
120
+ let timeoutId: NodeJS.Timeout | null = null;
121
+ let isTimedOut = false;
122
+
123
  try {
124
  const client = new InferenceClient(token);
125
 
126
+ const systemPrompt = INITIAL_SYSTEM_PROMPT;
127
+
128
+ const userPrompt = rewrittenPrompt;
129
+ const estimatedInputTokens = estimateInputTokens(systemPrompt, userPrompt);
130
+ const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, true);
131
+ const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens);
132
 
 
 
133
  const chatCompletion = client.chatCompletionStream(
134
  {
135
+ model: selectedModel.value,
136
+ provider: selectedProvider.provider,
137
  messages: [
138
  {
139
  role: "system",
140
  content: systemPrompt,
141
  },
 
 
 
 
142
  {
143
  role: "user",
144
  content: userPrompt + (enhancedSettings.isActive ? `1. I want to use the following primary color: ${enhancedSettings.primaryColor} (eg: bg-${enhancedSettings.primaryColor}-500).
 
146
  3. I want to use the following theme: ${enhancedSettings.theme} mode.` : "")
147
  },
148
  ],
149
+ ...providerConfig,
 
 
150
  },
151
  billTo ? { billTo } : {}
152
  );
153
 
154
+ // Set up timeout
155
+ const timeoutPromise = new Promise((_, reject) => {
156
+ timeoutId = setTimeout(() => {
157
+ isTimedOut = true;
158
+ reject(new Error('Request timeout: The AI model took too long to respond. Please try again with a simpler prompt or try a different model.'));
159
+ }, STREAMING_TIMEOUT);
160
+ });
161
+
162
+ // Race between streaming and timeout
163
+ await Promise.race([
164
+ (async () => {
165
+ while (true) {
166
+ const { done, value } = await chatCompletion.next()
167
+ if (done) {
168
+ break;
169
+ }
170
+
171
+ const chunk = value.choices[0]?.delta?.content;
172
+ if (chunk) {
173
+ await writer.write(encoder.encode(chunk));
174
+ }
175
+ }
176
+ })(),
177
+ timeoutPromise
178
+ ]);
179
+
180
+ // Clear timeout if successful
181
+ if (timeoutId) clearTimeout(timeoutId);
182
 
183
+ // Explicitly close the writer after successful completion
184
  await writer.close();
185
  } catch (error: any) {
186
+ // Clear timeout on error
187
+ if (timeoutId) clearTimeout(timeoutId);
188
+
189
+ if (isTimedOut || error.message?.includes('timeout') || error.message?.includes('Request timeout')) {
190
+ await writer.write(
191
+ encoder.encode(
192
+ JSON.stringify({
193
+ ok: false,
194
+ message: "Request timeout: The AI model took too long to respond. Please try again with a simpler prompt or try a different model.",
195
+ })
196
+ )
197
+ );
198
+ } else if (error.message?.includes("exceeded your monthly included credits")) {
199
  await writer.write(
200
  encoder.encode(
201
  JSON.stringify({
 
259
  const authHeaders = await headers();
260
 
261
  const body = await request.json();
262
+ const { prompt, provider, selectedElementHtml, model, pages, files, repoId: repoIdFromBody, isNew } =
263
  body;
264
 
265
+ let repoId = repoIdFromBody;
266
+
267
  if (!prompt || pages.length === 0) {
268
  return NextResponse.json(
269
  { ok: false, error: "Missing required fields" },
 
297
  ? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
298
  : authHeaders.get("x-forwarded-for");
299
 
300
+ if (!token) {
301
  ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
302
  if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
303
  return NextResponse.json(
 
314
  billTo = "huggingface";
315
  }
316
 
317
+ const client = new InferenceClient(token);
 
 
 
318
 
319
+ const escapeRegExp = (string: string) => {
320
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
321
+ };
 
 
 
 
322
 
323
+ const createFlexibleHtmlRegex = (searchBlock: string) => {
324
+ let searchRegex = escapeRegExp(searchBlock)
325
+ .replace(/\s+/g, '\\s*')
326
+ .replace(/>\s*</g, '>\\s*<')
327
+ .replace(/\s*>/g, '\\s*>');
328
+
329
+ return new RegExp(searchRegex, 'g');
330
+ };
331
 
332
+ const selectedProvider = await getBestProvider(selectedModel.value, provider)
333
+
334
+ try {
335
+ const systemPrompt = FOLLOW_UP_SYSTEM_PROMPT + (isNew ? PROMPT_FOR_PROJECT_NAME : "");
336
+ const userContext = "You are modifying the HTML file based on the user's request.";
337
+
338
+ const allPages = pages || [];
339
+ const pagesContext = allPages
340
+ .map((p: Page) => `- ${p.path}\n${p.html}`)
341
+ .join("\n\n");
342
+
343
+ const assistantContext = `${
344
+ selectedElementHtml
345
+ ? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\` Could be in multiple pages, if so, update all the pages.`
346
+ : ""
347
+ }. Current pages (${allPages.length} total): ${pagesContext}. ${files?.length > 0 ? `Available images: ${files?.map((f: string) => f).join(', ')}.` : ""}`;
348
+
349
+ const estimatedInputTokens = estimateInputTokens(systemPrompt, prompt, userContext + assistantContext);
350
+ const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, false);
351
+ const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens);
352
+
353
+ const chatCompletion = client.chatCompletionStream(
354
+ {
355
+ model: selectedModel.value,
356
+ provider: selectedProvider.provider,
357
+ messages: [
358
+ {
359
+ role: "system",
360
+ content: systemPrompt,
361
+ },
362
+ {
363
+ role: "user",
364
+ content: userContext,
365
+ },
366
+ {
367
+ role: "assistant",
368
+ content: assistantContext,
369
+ },
370
+ {
371
+ role: "user",
372
+ content: prompt,
373
+ },
374
+ ],
375
+ ...providerConfig,
376
+ },
377
+ billTo ? { billTo } : {}
378
+ );
379
 
380
+ // Set up timeout for AI streaming
381
+ let chunk = "";
382
+ let timeoutId: NodeJS.Timeout | null = null;
383
+ let isTimedOut = false;
384
 
385
+ const timeoutPromise = new Promise<never>((_, reject) => {
386
+ timeoutId = setTimeout(() => {
387
+ isTimedOut = true;
388
+ reject(new Error('Request timeout: The AI model took too long to respond. Please try again with a simpler prompt or try a different model.'));
389
+ }, REQUEST_TIMEOUT);
390
+ });
391
 
392
+ try {
393
+ await Promise.race([
394
+ (async () => {
395
+ while (true) {
396
+ const { done, value } = await chatCompletion.next();
397
+ if (done) {
398
+ break;
399
+ }
400
+
401
+ const deltaContent = value.choices[0]?.delta?.content;
402
+ if (deltaContent) {
403
+ chunk += deltaContent;
404
+ }
405
+ }
406
+ })(),
407
+ timeoutPromise
408
+ ]);
409
+
410
+ // Clear timeout if successful
411
+ if (timeoutId) clearTimeout(timeoutId);
412
+ } catch (timeoutError: any) {
413
+ // Clear timeout on error
414
+ if (timeoutId) clearTimeout(timeoutId);
415
+
416
+ if (isTimedOut || timeoutError.message?.includes('timeout') || timeoutError.message?.includes('Request timeout')) {
417
+ return NextResponse.json(
418
  {
419
+ ok: false,
420
+ message: "Request timeout: The AI model took too long to respond. Please try again with a simpler prompt or try a different model.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  },
422
+ { status: 504 }
423
  );
424
+ }
425
+ throw timeoutError;
426
+ }
427
+ if (!chunk) {
428
+ return NextResponse.json(
429
+ { ok: false, message: "No content returned from the model" },
430
+ { status: 400 }
431
+ );
432
+ }
433
+
434
+ if (chunk) {
435
+ const updatedLines: number[][] = [];
436
+ let newHtml = "";
437
+ const updatedPages = [...(pages || [])];
438
 
439
+ const updateFileRegex = new RegExp(`${UPDATE_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${UPDATE_FILE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`, 'g');
440
+ let updateFileMatch;
441
+
442
+ while ((updateFileMatch = updateFileRegex.exec(chunk)) !== null) {
443
+ const [, filePath, fileContent] = updateFileMatch;
444
+
445
+ const pageIndex = updatedPages.findIndex(p => p.path === filePath);
446
+ if (pageIndex !== -1) {
447
+ let pageHtml = updatedPages[pageIndex].html;
448
+
449
+ let processedContent = fileContent;
450
+ const htmlMatch = fileContent.match(/```html\s*([\s\S]*?)\s*```/);
451
+ if (htmlMatch) {
452
+ processedContent = htmlMatch[1];
453
+ }
454
+ let position = 0;
455
+ let moreBlocks = true;
456
+
457
+ while (moreBlocks) {
458
+ const searchStartIndex = processedContent.indexOf(SEARCH_START, position);
459
+ if (searchStartIndex === -1) {
460
+ moreBlocks = false;
461
+ continue;
462
+ }
463
+
464
+ const dividerIndex = processedContent.indexOf(DIVIDER, searchStartIndex);
465
+ if (dividerIndex === -1) {
466
+ moreBlocks = false;
467
+ continue;
468
+ }
469
+
470
+ const replaceEndIndex = processedContent.indexOf(REPLACE_END, dividerIndex);
471
+ if (replaceEndIndex === -1) {
472
+ moreBlocks = false;
473
+ continue;
474
+ }
475
+
476
+ const searchBlock = processedContent.substring(
477
+ searchStartIndex + SEARCH_START.length,
478
+ dividerIndex
479
+ );
480
+ const replaceBlock = processedContent.substring(
481
+ dividerIndex + DIVIDER.length,
482
+ replaceEndIndex
483
+ );
484
+
485
+ if (searchBlock.trim() === "") {
486
+ pageHtml = `${replaceBlock}\n${pageHtml}`;
487
+ updatedLines.push([1, replaceBlock.split("\n").length]);
488
+ } else {
489
+ const regex = createFlexibleHtmlRegex(searchBlock);
490
+ const match = regex.exec(pageHtml);
491
+
492
+ if (match) {
493
+ const matchedText = match[0];
494
+ const beforeText = pageHtml.substring(0, match.index);
495
+ const startLineNumber = beforeText.split("\n").length;
496
+ const replaceLines = replaceBlock.split("\n").length;
497
+ const endLineNumber = startLineNumber + replaceLines - 1;
498
+
499
+ updatedLines.push([startLineNumber, endLineNumber]);
500
+ pageHtml = pageHtml.replace(matchedText, replaceBlock);
501
+ }
502
+ }
503
+
504
+ position = replaceEndIndex + REPLACE_END.length;
505
  }
506
 
507
+ updatedPages[pageIndex].html = pageHtml;
508
+
509
+ if (filePath === '/' || filePath === '/index' || filePath === 'index' || filePath === 'index.html') {
510
+ newHtml = pageHtml;
511
  }
512
  }
513
+ }
514
+
515
+ const newFileRegex = new RegExp(`${NEW_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${NEW_FILE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`, 'g');
516
+ let newFileMatch;
517
+
518
+ while ((newFileMatch = newFileRegex.exec(chunk)) !== null) {
519
+ const [, filePath, fileContent] = newFileMatch;
520
+
521
+ let fileData = fileContent;
522
+ // Try to extract content from code blocks
523
+ const htmlMatch = fileContent.match(/```html\s*([\s\S]*?)\s*```/);
524
+ const cssMatch = fileContent.match(/```css\s*([\s\S]*?)\s*```/);
525
+ const jsMatch = fileContent.match(/```javascript\s*([\s\S]*?)\s*```/);
526
+
527
+ if (htmlMatch) {
528
+ fileData = htmlMatch[1];
529
+ } else if (cssMatch) {
530
+ fileData = cssMatch[1];
531
+ } else if (jsMatch) {
532
+ fileData = jsMatch[1];
533
+ }
534
+
535
+ const existingFileIndex = updatedPages.findIndex(p => p.path === filePath);
536
+
537
+ if (existingFileIndex !== -1) {
538
+ updatedPages[existingFileIndex] = {
539
+ path: filePath,
540
+ html: fileData.trim()
541
+ };
542
+ } else {
543
+ updatedPages.push({
544
+ path: filePath,
545
+ html: fileData.trim()
546
+ });
547
+ }
548
+ }
549
 
550
+ if (updatedPages.length === pages?.length && !chunk.includes(UPDATE_FILE_START)) {
551
+ let position = 0;
552
+ let moreBlocks = true;
 
 
553
 
554
+ while (moreBlocks) {
555
+ const searchStartIndex = chunk.indexOf(SEARCH_START, position);
556
+ if (searchStartIndex === -1) {
557
+ moreBlocks = false;
558
+ continue;
559
+ }
560
+
561
+ const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
562
+ if (dividerIndex === -1) {
563
+ moreBlocks = false;
564
+ continue;
565
+ }
566
+
567
+ const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
568
+ if (replaceEndIndex === -1) {
569
+ moreBlocks = false;
570
+ continue;
571
+ }
572
+
573
+ const searchBlock = chunk.substring(
574
+ searchStartIndex + SEARCH_START.length,
575
+ dividerIndex
576
  );
577
+ const replaceBlock = chunk.substring(
578
+ dividerIndex + DIVIDER.length,
579
+ replaceEndIndex
 
 
 
 
 
 
 
580
  );
581
+
582
+ if (searchBlock.trim() === "") {
583
+ newHtml = `${replaceBlock}\n${newHtml}`;
584
+ updatedLines.push([1, replaceBlock.split("\n").length]);
585
+ } else {
586
+ const regex = createFlexibleHtmlRegex(searchBlock);
587
+ const match = regex.exec(newHtml);
588
+
589
+ if (match) {
590
+ const matchedText = match[0];
591
+ const beforeText = newHtml.substring(0, match.index);
592
+ const startLineNumber = beforeText.split("\n").length;
593
+ const replaceLines = replaceBlock.split("\n").length;
594
+ const endLineNumber = startLineNumber + replaceLines - 1;
595
+
596
+ updatedLines.push([startLineNumber, endLineNumber]);
597
+ newHtml = newHtml.replace(matchedText, replaceBlock);
598
+ }
599
+ }
600
+
601
+ position = replaceEndIndex + REPLACE_END.length;
602
  }
603
+
604
+ const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
605
+ if (mainPageIndex !== -1) {
606
+ updatedPages[mainPageIndex].html = newHtml;
 
607
  }
608
  }
 
609
 
610
+ const files: File[] = [];
611
+ updatedPages.forEach((page: Page) => {
612
+ let mimeType = "text/html";
613
+ if (page.path.endsWith(".css")) {
614
+ mimeType = "text/css";
615
+ } else if (page.path.endsWith(".js")) {
616
+ mimeType = "text/javascript";
617
+ } else if (page.path.endsWith(".json")) {
618
+ mimeType = "application/json";
619
+ }
620
+ const content = (mimeType === "text/html" && isIndexPage(page.path)) && isNew
621
+ ? injectDeepSiteBadge(page.html)
622
+ : page.html;
623
+ const file = new File([content], page.path, { type: mimeType });
624
+ files.push(file);
625
+ });
626
+
627
+ if (isNew) {
628
+ const projectName = chunk.match(/<<<<<<< PROJECT_NAME_START\s*([\s\S]*?)\s*>>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
629
+ const formattedTitle = projectName?.toLowerCase()
630
+ .replace(/[^a-z0-9]+/g, "-")
631
+ .split("-")
632
+ .filter(Boolean)
633
+ .join("-")
634
+ .slice(0, 96);
635
+ const repo: RepoDesignation = {
636
+ type: "space",
637
+ name: `${user.name}/${formattedTitle}`,
638
+ };
639
+ const { repoUrl} = await createRepo({
640
+ repo,
641
+ accessToken: user.token as string,
642
+ });
643
+ repoId = repoUrl.split("/").slice(-2).join("/");
644
+ const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
645
+ const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
646
+ const README = `---
647
+ title: ${projectName}
648
+ colorFrom: ${colorFrom}
649
+ colorTo: ${colorTo}
650
+ emoji: 🐳
651
+ sdk: static
652
+ pinned: false
653
+ tags:
654
+ - deepsite-v3
655
+ ---
656
+
657
+ # Welcome to your new DeepSite project!
658
+ This project was created with [DeepSite](https://huggingface.co/deepsite).
659
+ `;
660
+ files.push(new File([README], "README.md", { type: "text/markdown" }));
661
+ }
662
+
663
+ const response = await uploadFiles({
664
+ repo: {
665
+ type: "space",
666
+ name: repoId,
667
+ },
668
+ files,
669
+ commitTitle: prompt,
670
+ accessToken: user.token as string,
671
+ });
672
+
673
+ return NextResponse.json({
674
+ ok: true,
675
+ updatedLines,
676
+ pages: updatedPages,
677
+ repoId,
678
+ commit: {
679
+ ...response.commit,
680
+ title: prompt,
681
+ }
682
+ });
683
+ } else {
684
+ return NextResponse.json(
685
+ { ok: false, message: "No content returned from the model" },
686
+ { status: 400 }
687
+ );
688
+ }
689
  } catch (error: any) {
690
+ if (error.message?.includes('timeout') || error.message?.includes('Request timeout')) {
691
+ return NextResponse.json(
692
+ {
693
+ ok: false,
694
+ message: "Request timeout: The operation took too long to complete. Please try again with a simpler request or try a different model.",
695
+ },
696
+ { status: 504 }
697
+ );
698
+ }
699
+ if (error.message?.includes("exceeded your monthly included credits")) {
700
+ return NextResponse.json(
701
+ {
702
+ ok: false,
703
+ openProModal: true,
704
+ message: error.message,
705
+ },
706
+ { status: 402 }
707
+ );
708
+ }
709
  return NextResponse.json(
710
  {
711
  ok: false,
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/api/me/projects/[namespace]/[repoId]/update/route.ts DELETED
@@ -1,141 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
3
-
4
- import { isAuthenticated } from "@/lib/auth";
5
- import { Page } from "@/types";
6
- import { COLORS } from "@/lib/utils";
7
- import { injectDeepSiteBadge, isIndexPage } from "@/lib/inject-badge";
8
- import { pagesToFiles } from "@/lib/format-ai-response";
9
-
10
- /**
11
- * UPDATE route - for updating existing projects or creating new ones after AI streaming
12
- * This route handles the HuggingFace upload after client-side AI response processing
13
- */
14
- export async function PUT(
15
- req: NextRequest,
16
- { params }: { params: Promise<{ namespace: string; repoId: string }> }
17
- ) {
18
- const user = await isAuthenticated();
19
- if (user instanceof NextResponse || !user) {
20
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
21
- }
22
-
23
- const param = await params;
24
- let { namespace, repoId } = param;
25
- const { pages, commitTitle = "AI-generated changes", isNew, projectName } = await req.json();
26
-
27
- if (!pages || !Array.isArray(pages) || pages.length === 0) {
28
- return NextResponse.json(
29
- { ok: false, error: "Pages are required" },
30
- { status: 400 }
31
- );
32
- }
33
-
34
- try {
35
- let files: File[];
36
-
37
- if (isNew) {
38
- // Creating a new project
39
- const title = projectName || "DeepSite Project";
40
- const formattedTitle = title
41
- .toLowerCase()
42
- .replace(/[^a-z0-9]+/g, "-")
43
- .split("-")
44
- .filter(Boolean)
45
- .join("-")
46
- .slice(0, 96);
47
-
48
- const repo: RepoDesignation = {
49
- type: "space",
50
- name: `${user.name}/${formattedTitle}`,
51
- };
52
-
53
- try {
54
- const { repoUrl } = await createRepo({
55
- repo,
56
- accessToken: user.token as string,
57
- });
58
- namespace = user.name;
59
- repoId = repoUrl.split("/").slice(-2).join("/").split("/")[1];
60
- } catch (createRepoError: any) {
61
- return NextResponse.json(
62
- {
63
- ok: false,
64
- error: `Failed to create repository: ${createRepoError.message || 'Unknown error'}`,
65
- },
66
- { status: 500 }
67
- );
68
- }
69
-
70
- // Prepare files with badge injection for new projects
71
- files = [];
72
- pages.forEach((page: Page) => {
73
- let mimeType = "text/html";
74
- if (page.path.endsWith(".css")) {
75
- mimeType = "text/css";
76
- } else if (page.path.endsWith(".js")) {
77
- mimeType = "text/javascript";
78
- } else if (page.path.endsWith(".json")) {
79
- mimeType = "application/json";
80
- }
81
- const content = (mimeType === "text/html" && isIndexPage(page.path))
82
- ? injectDeepSiteBadge(page.html)
83
- : page.html;
84
- const file = new File([content], page.path, { type: mimeType });
85
- files.push(file);
86
- });
87
-
88
- // Add README.md for new projects
89
- const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
90
- const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
91
- const README = `---
92
- title: ${title}
93
- colorFrom: ${colorFrom}
94
- colorTo: ${colorTo}
95
- emoji: 🐳
96
- sdk: static
97
- pinned: false
98
- tags:
99
- - deepsite-v3
100
- ---
101
-
102
- # Welcome to your new DeepSite project!
103
- This project was created with [DeepSite](https://huggingface.co/deepsite).
104
- `;
105
- files.push(new File([README], "README.md", { type: "text/markdown" }));
106
- } else {
107
- // Updating existing project - no badge injection
108
- files = pagesToFiles(pages);
109
- }
110
-
111
- const response = await uploadFiles({
112
- repo: {
113
- type: "space",
114
- name: `${namespace}/${repoId}`,
115
- },
116
- files,
117
- commitTitle,
118
- accessToken: user.token as string,
119
- });
120
-
121
- return NextResponse.json({
122
- ok: true,
123
- pages,
124
- repoId: `${namespace}/${repoId}`,
125
- commit: {
126
- ...response.commit,
127
- title: commitTitle,
128
- }
129
- });
130
- } catch (error: any) {
131
- console.error("Error updating project:", error);
132
- return NextResponse.json(
133
- {
134
- ok: false,
135
- error: error.message || "Failed to update project",
136
- },
137
- { status: 500 }
138
- );
139
- }
140
- }
141
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/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([]);
@@ -104,8 +100,7 @@ export const AskAi = ({
104
  prompt,
105
  enhancedSettings,
106
  redesignMarkdown,
107
- !!user,
108
- user?.name
109
  );
110
 
111
  if (result?.error) {
@@ -146,15 +141,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 +156,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 +188,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 +209,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 +220,7 @@ export const AskAi = ({
242
  ? "Uploading images..."
243
  : isAiWorking && !isSameHtml
244
  ? "DeepSite is working..."
245
- : "DeepSite is working..."
246
  }
247
  />
248
  {isAiWorking && (
@@ -272,8 +250,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 +294,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 +303,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>
@@ -345,7 +313,7 @@ export const AskAi = ({
345
  </div>
346
  </div>
347
  <audio ref={hookAudio} id="audio" className="hidden">
348
- <source src="/deepsite/success.mp3" type="audio/mpeg" />
349
  Your browser does not support the audio element.
350
  </audio>
351
  </div>
 
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([]);
 
100
  prompt,
101
  enhancedSettings,
102
  redesignMarkdown,
103
+ !!user
 
104
  );
105
 
106
  if (result?.error) {
 
141
  if (refThink.current) {
142
  refThink.current.scrollTop = refThink.current.scrollHeight;
143
  }
144
+ }, [think]);
 
 
 
 
 
 
 
 
145
 
146
  const randomPrompt = () => {
147
  setRandomPromptLoading(true);
 
156
  return (
157
  <div className="p-3 w-full">
158
  <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">
159
+ {think && (
160
  <div className="w-full border-b border-neutral-700 relative overflow-hidden">
161
  <header
162
  className="flex items-center justify-between px-5 py-2.5 group hover:bg-neutral-600/20 transition-colors duration-200 cursor-pointer"
 
188
  )}
189
  >
190
  <p className="text-[13px] text-neutral-400 whitespace-pre-line px-5 pb-4 pt-3">
191
+ {think}
192
  </p>
193
  </main>
194
  </div>
 
209
  />
210
  </div>
211
  )}
 
 
 
 
 
 
 
 
 
212
  <div className="w-full relative flex items-center justify-between">
213
  {(isAiWorking || isUploading || isThinking || isLoadingProject) && (
214
  <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">
 
220
  ? "Uploading images..."
221
  : isAiWorking && !isSameHtml
222
  ? "DeepSite is working..."
223
+ : "DeepSite is thinking..."
224
  }
225
  />
226
  {isAiWorking && (
 
250
  placeholder={
251
  selectedElement
252
  ? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
 
 
253
  : isFollowUp && (!isSameHtml || pages?.length > 1)
254
  ? "Ask DeepSite for edits"
255
  : "Ask DeepSite anything..."
 
294
  onClose={setOpenProvider}
295
  />
296
  {!isNew && <Uploader project={project} />}
297
+ {isNew && <ReImagine onRedesign={(md) => callAi(md)} />}
 
 
 
 
 
 
298
  {!isNew && !isSameHtml && <Selector />}
299
  </div>
300
  <div className="flex items-center justify-end gap-2">
 
303
  variant="outline"
304
  className="!rounded-md"
305
  disabled={
306
+ isAiWorking || isUploading || isThinking || !prompt.trim()
 
 
307
  }
308
+ onClick={() => callAi()}
309
  >
310
  <ArrowUp className="size-4" />
311
  </Button>
 
313
  </div>
314
  </div>
315
  <audio ref={hookAudio} id="audio" className="hidden">
316
+ <source src="/success.mp3" type="audio/mpeg" />
317
  Your browser does not support the audio element.
318
  </audio>
319
  </div>
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,29 @@ 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
@@ -278,9 +102,11 @@ export const Preview = ({
278
  `<head>\n${allCssContent}`
279
  );
280
  } else {
 
281
  modifiedHtml = allCssContent + "\n" + modifiedHtml;
282
  }
283
 
 
284
  cssFiles.forEach((file) => {
285
  const escapedPath = file.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
286
  modifiedHtml = modifiedHtml.replace(
@@ -293,6 +119,7 @@ export const Preview = ({
293
  });
294
  }
295
 
 
296
  if (jsFiles.length > 0) {
297
  const allJsContent = jsFiles
298
  .map(
@@ -309,9 +136,11 @@ export const Preview = ({
309
  } else if (modifiedHtml.includes("<body>")) {
310
  modifiedHtml = modifiedHtml + allJsContent;
311
  } else {
 
312
  modifiedHtml = modifiedHtml + "\n" + allJsContent;
313
  }
314
 
 
315
  jsFiles.forEach((file) => {
316
  const escapedPath = file.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
317
  modifiedHtml = modifiedHtml.replace(
@@ -324,69 +153,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 +164,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 +190,6 @@ export const Preview = ({
430
  previewPageData?.html,
431
  injectAssetsIntoHtml,
432
  previewPage,
433
- pagesToUse,
434
  ]);
435
 
436
  useEffect(() => {
@@ -440,10 +199,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,13 +208,13 @@ export const Preview = ({
452
  isAiWorking,
453
  globalAiLoading,
454
  injectAssetsIntoHtml,
455
- pagesToUse,
456
  ]);
457
 
458
  const setupIframeListeners = () => {
459
  if (iframeRef?.current?.contentDocument) {
460
  const iframeDocument = iframeRef.current.contentDocument;
461
 
 
462
  iframeDocument.addEventListener(
463
  "click",
464
  handleCustomNavigation as any,
@@ -473,17 +229,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 +257,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 +268,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
  }
@@ -580,28 +320,17 @@ export const Preview = ({
580
  if (iframeRef?.current) {
581
  const iframeDocument = iframeRef.current.contentDocument;
582
  if (iframeDocument) {
583
- const path = event.composedPath();
584
- const targetElement = path[0] as HTMLElement;
585
 
586
  const findClosestAnchor = (
587
  element: HTMLElement
588
  ): HTMLAnchorElement | null => {
589
- let current: HTMLElement | null = element;
590
- while (current) {
591
- if (current.tagName?.toUpperCase() === "A") {
592
  return current as HTMLAnchorElement;
593
  }
594
- if (current === iframeDocument.body) {
595
- break;
596
- }
597
- const parent: Node | null = current.parentNode;
598
- if (parent && parent.nodeType === 11) {
599
- current = (parent as ShadowRoot).host as HTMLElement;
600
- } else if (parent && parent.nodeType === 1) {
601
- current = parent as HTMLElement;
602
- } else {
603
- break;
604
- }
605
  }
606
  return null;
607
  };
@@ -630,18 +359,15 @@ export const Preview = ({
630
  element: HTMLElement
631
  ): HTMLAnchorElement | null => {
632
  let current: HTMLElement | null = element;
633
- while (current) {
634
- if (current.tagName?.toUpperCase() === "A") {
635
  return current as HTMLAnchorElement;
636
  }
637
- if (current === iframeDocument.body) {
638
- break;
639
- }
640
  const parent: Node | null = current.parentNode;
641
- if (parent && parent.nodeType === 11) {
642
- current = (parent as ShadowRoot).host as HTMLElement;
643
- } else if (parent && parent.nodeType === 1) {
644
- current = parent as HTMLElement;
645
  } else {
646
  break;
647
  }
@@ -698,7 +424,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 +482,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 +493,37 @@ 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
+ // Find all CSS and JS files (including those in subdirectories)
76
+ const cssFiles = pages.filter(
77
  (p) => p.path.endsWith(".css") && p.path !== previewPageData?.path
78
  );
79
+ const jsFiles = pages.filter(
80
  (p) => p.path.endsWith(".js") && p.path !== previewPageData?.path
81
  );
 
 
 
82
 
83
  let modifiedHtml = html;
84
 
 
 
 
85
  // Inject all CSS files
86
  if (cssFiles.length > 0) {
87
  const allCssContent = cssFiles
 
102
  `<head>\n${allCssContent}`
103
  );
104
  } else {
105
+ // If no head tag, prepend to document
106
  modifiedHtml = allCssContent + "\n" + modifiedHtml;
107
  }
108
 
109
+ // Remove all link tags that reference CSS files we're injecting
110
  cssFiles.forEach((file) => {
111
  const escapedPath = file.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
112
  modifiedHtml = modifiedHtml.replace(
 
119
  });
120
  }
121
 
122
+ // Inject all JS files
123
  if (jsFiles.length > 0) {
124
  const allJsContent = jsFiles
125
  .map(
 
136
  } else if (modifiedHtml.includes("<body>")) {
137
  modifiedHtml = modifiedHtml + allJsContent;
138
  } else {
139
+ // If no body tag, append to document
140
  modifiedHtml = modifiedHtml + "\n" + allJsContent;
141
  }
142
 
143
+ // Remove all script tags that reference JS files we're injecting
144
  jsFiles.forEach((file) => {
145
  const escapedPath = file.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
146
  modifiedHtml = modifiedHtml.replace(
 
153
  });
154
  }
155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  return modifiedHtml;
157
  },
158
+ [pages, previewPageData?.path]
159
  );
160
 
161
  useEffect(() => {
 
164
  const timeSinceLastUpdate = now - lastUpdateTimeRef.current;
165
 
166
  if (lastUpdateTimeRef.current === 0 || timeSinceLastUpdate >= 3000) {
167
+ const processedHtml = injectAssetsIntoHtml(previewPageData.html);
 
 
 
168
  setThrottledHtml(processedHtml);
169
  lastUpdateTimeRef.current = now;
170
  } else {
171
  const timeUntilNextUpdate = 3000 - timeSinceLastUpdate;
172
  const timer = setTimeout(() => {
173
+ const processedHtml = injectAssetsIntoHtml(previewPageData.html);
 
 
 
174
  setThrottledHtml(processedHtml);
175
  lastUpdateTimeRef.current = Date.now();
176
  }, timeUntilNextUpdate);
177
  return () => clearTimeout(timer);
178
  }
179
  }
180
+ }, [isNew, previewPageData?.html, injectAssetsIntoHtml]);
181
 
182
  useEffect(() => {
183
  if (!isAiWorking && !globalAiLoading && previewPageData?.html) {
184
+ const processedHtml = injectAssetsIntoHtml(previewPageData.html);
 
 
 
185
  setStableHtml(processedHtml);
186
  }
187
  }, [
 
190
  previewPageData?.html,
191
  injectAssetsIntoHtml,
192
  previewPage,
 
193
  ]);
194
 
195
  useEffect(() => {
 
199
  !isAiWorking &&
200
  !globalAiLoading
201
  ) {
202
+ const processedHtml = injectAssetsIntoHtml(previewPageData.html);
 
 
 
203
  setStableHtml(processedHtml);
204
  }
205
  }, [
 
208
  isAiWorking,
209
  globalAiLoading,
210
  injectAssetsIntoHtml,
 
211
  ]);
212
 
213
  const setupIframeListeners = () => {
214
  if (iframeRef?.current?.contentDocument) {
215
  const iframeDocument = iframeRef.current.contentDocument;
216
 
217
+ // Use event delegation to catch clicks on anchors in both light and shadow DOM
218
  iframeDocument.addEventListener(
219
  "click",
220
  handleCustomNavigation as any,
 
229
  }
230
  };
231
 
 
 
 
 
 
 
 
 
 
 
 
232
  useEffect(() => {
233
  const cleanupListeners = () => {
234
  if (iframeRef?.current?.contentDocument) {
 
257
  };
258
  }, [isEditableModeEnabled, stableHtml, throttledHtml, previewPage]);
259
 
 
 
 
 
260
  const promoteVersion = async () => {
261
  setIsPromotingVersion(true);
262
  await api
 
268
  setCurrentCommit(null);
269
  setPages(res.data.pages);
270
  setCurrentPage(res.data.pages[0].path);
 
271
  setPreviewPage(res.data.pages[0].path);
272
  toast.success("Version promoted successfully");
273
  }
 
320
  if (iframeRef?.current) {
321
  const iframeDocument = iframeRef.current.contentDocument;
322
  if (iframeDocument) {
323
+ const targetElement = event.target as HTMLElement;
 
324
 
325
  const findClosestAnchor = (
326
  element: HTMLElement
327
  ): HTMLAnchorElement | null => {
328
+ let current = element;
329
+ while (current && current !== iframeDocument.body) {
330
+ if (current.tagName === "A") {
331
  return current as HTMLAnchorElement;
332
  }
333
+ current = current.parentElement as HTMLElement;
 
 
 
 
 
 
 
 
 
 
334
  }
335
  return null;
336
  };
 
359
  element: HTMLElement
360
  ): HTMLAnchorElement | null => {
361
  let current: HTMLElement | null = element;
362
+ while (current && current !== iframeDocument.body) {
363
+ if (current.tagName === "A") {
364
  return current as HTMLAnchorElement;
365
  }
 
 
 
366
  const parent: Node | null = current.parentNode;
367
+ if (parent instanceof ShadowRoot) {
368
+ current = parent.host as HTMLElement;
369
+ } else if (parent instanceof HTMLElement) {
370
+ current = parent;
371
  } else {
372
  break;
373
  }
 
424
  normalizedHref = normalizedHref + ".html";
425
  }
426
 
427
+ const isPageExist = pages.some((page) => {
428
  const pagePath = page.path.replace(/^\.?\//, "");
429
  return pagePath === normalizedHref;
430
  });
 
482
  </div>
483
  ) : (
484
  <>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
  <iframe
 
486
  id="preview-iframe"
487
  ref={iframeRef}
488
  className={classNames(
 
493
  }
494
  )}
495
  src={
496
+ currentCommit
497
+ ? `https://${project?.space_id?.replaceAll(
 
 
 
 
498
  "/",
499
  "-"
500
+ )}--rev-${currentCommit.slice(0, 7)}.static.hf.space`
501
  : undefined
502
  }
503
  srcDoc={
504
+ !currentCommit
 
 
 
 
505
  ? isNew
506
  ? throttledHtml || defaultHTML
507
  : stableHtml
508
  : undefined
509
  }
510
+ onLoad={
511
+ !currentCommit
512
+ ? () => {
513
+ if (iframeRef?.current?.contentWindow?.document?.body) {
514
+ iframeRef.current.contentWindow.document.body.scrollIntoView(
515
+ {
516
+ block: isAiWorking ? "end" : "start",
517
+ inline: "nearest",
518
+ behavior: isAiWorking ? "instant" : "smooth",
519
+ }
520
+ );
521
+ }
522
+ // Set up event listeners after iframe loads
523
+ setupIframeListeners();
524
+ }
525
+ : undefined
526
+ }
527
  sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-modals allow-forms"
528
  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"
529
  />
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,
@@ -136,37 +124,27 @@ export const useAi = (onScrollToBottom?: () => void) => {
136
  client.setQueryData(["ai.model"], newModel);
137
  };
138
 
139
- const createNewProject = async (prompt: string, htmlPages: Page[], projectName: string | undefined, isLoggedIn?: boolean, userName?: string) => {
140
- if (isLoggedIn && userName) {
141
- try {
142
- const uploadRequest = await fetch(`/deepsite/api/me/projects/${userName}/new/update`, {
143
- method: "PUT",
144
- body: JSON.stringify({
145
- pages: htmlPages,
146
- commitTitle: prompt,
147
- isNew: true,
148
- projectName,
149
- }),
150
- headers: {
151
- "Content-Type": "application/json",
152
- "Authorization": `Bearer ${token}`,
153
- },
154
- });
155
-
156
- const uploadRes = await uploadRequest.json();
157
-
158
- if (!uploadRequest.ok || !uploadRes.ok) {
159
- throw new Error(uploadRes.error || "Failed to create project");
160
  }
161
-
 
162
  setIsAiWorking(false);
163
- router.replace(`/${uploadRes.repoId}`);
164
- toast.success("AI responded successfully");
165
- if (audio.current) audio.current.play();
166
- } catch (error: any) {
167
- setIsAiWorking(false);
168
- toast.error(error?.message || "Failed to create project");
169
- }
170
  } else {
171
  setIsAiWorking(false);
172
  toast.success("AI responded successfully");
@@ -174,12 +152,11 @@ export const useAi = (onScrollToBottom?: () => void) => {
174
  }
175
  }
176
 
177
- const callAiNewProject = async (prompt: string, enhancedSettings?: EnhancedSettings, redesignMarkdown?: string, isLoggedIn?: boolean, userName?: string) => {
178
  if (isAiWorking) return;
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 +189,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,12 +213,12 @@ 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]);
251
  if (newPages.length > 0 && !isTheSameHtml(newPages[0].html)) {
252
- createNewProject(prompt, newPages, projectName, isLoggedIn, userName);
253
  }
254
  setPrompts([...prompts, prompt]);
255
 
@@ -259,26 +228,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 +260,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 +279,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);
@@ -364,191 +311,71 @@ export const useAi = (onScrollToBottom?: () => void) => {
364
  });
365
 
366
  if (request && request.body) {
367
- const reader = request.body.getReader();
368
- const decoder = new TextDecoder("utf-8");
369
- let contentResponse = "";
370
- let metadata: any = null;
371
-
372
- const read = async (): Promise<any> => {
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 {
387
- metadata = JSON.parse(metadataMatch[1]);
388
- contentResponse = contentResponse.replace(/___METADATA_START___[\s\S]*?___METADATA_END___/, '').trim();
389
- } catch (e) {
390
- console.error("Failed to parse metadata", e);
391
- }
392
- }
393
-
394
- const trimmedResponse = contentResponse.trim();
395
- if (trimmedResponse.startsWith("{") && trimmedResponse.endsWith("}")) {
396
- try {
397
- const jsonResponse = JSON.parse(trimmedResponse);
398
- if (jsonResponse && !jsonResponse.ok) {
399
- setIsAiWorking(false);
400
- if (jsonResponse.openLogin) {
401
- return { error: "login_required" };
402
- } else if (jsonResponse.openSelectProvider) {
403
- return { error: "provider_required", message: jsonResponse.message };
404
- } else if (jsonResponse.openProModal) {
405
- return { error: "pro_required" };
406
- } else {
407
- toast.error(jsonResponse.message);
408
- return { error: "api_error", message: jsonResponse.message };
409
- }
410
- }
411
- } catch (e) {
412
- // Not JSON, continue with normal processing
413
- }
414
- }
415
-
416
- const { processAiResponse, extractProjectName } = await import("@/lib/format-ai-response");
417
- const { updatedPages, updatedLines } = processAiResponse(contentResponse, pagesToSend);
418
-
419
- const updatedPagesMap = new Map(updatedPages.map((p: Page) => [p.path, p]));
420
- const mergedPages: Page[] = pages.map(page =>
421
- updatedPagesMap.has(page.path) ? updatedPagesMap.get(page.path)! : page
422
- );
423
- updatedPages.forEach((page: Page) => {
424
- if (!pages.find(p => p.path === page.path)) {
425
- mergedPages.push(page);
426
- }
427
- });
428
-
429
- let projectName = null;
430
- if (isNew) {
431
- projectName = extractProjectName(contentResponse);
432
- if (!projectName) {
433
- projectName = prompt.substring(0, 40).replace(/[^a-zA-Z0-9]/g, "-").slice(0, 40) + "-" + Math.random().toString(36).substring(2, 15);
434
- }
435
- }
436
-
437
- try {
438
- const uploadRequest = await fetch(`/deepsite/api/me/projects/${metadata?.userName || 'unknown'}/${isNew ? 'new' : (project?.space_id?.split('/')[1] || 'unknown')}/update`, {
439
- method: "PUT",
440
- body: JSON.stringify({
441
- pages: mergedPages,
442
- commitTitle: prompt,
443
- isNew,
444
- projectName,
445
- }),
446
- headers: {
447
- "Content-Type": "application/json",
448
- "Authorization": `Bearer ${token}`,
449
- },
450
- });
451
-
452
- const uploadRes = await uploadRequest.json();
453
-
454
- if (!uploadRequest.ok || !uploadRes.ok) {
455
- throw new Error(uploadRes.error || "Failed to upload to HuggingFace");
456
- }
457
-
458
- toast.success("AI responded successfully");
459
- const iframe = document.getElementById("preview-iframe") as HTMLIFrameElement;
460
-
461
- if (isNew && uploadRes.repoId) {
462
- router.push(`/${uploadRes.repoId}`);
463
- setIsAiWorking(false);
464
- } else {
465
- setPages(mergedPages);
466
- setLastSavedPages([...mergedPages]);
467
- setCommits([uploadRes.commit, ...commits]);
468
- setPrompts([...prompts, prompt]);
469
- setSelectedElement(null);
470
- setSelectedFiles([]);
471
- setIsEditableModeEnabled(false);
472
- setIsAiWorking(false);
473
- }
474
-
475
- if (audio.current) audio.current.play();
476
- if (iframe) {
477
- setTimeout(() => {
478
- iframe.src = iframe.src;
479
- }, 500);
480
- }
481
-
482
- return { success: true, updatedLines };
483
- } catch (uploadError: any) {
484
- setIsAiWorking(false);
485
- toast.error(uploadError.message || "Failed to save changes");
486
- return { error: "upload_error", message: uploadError.message };
487
- }
488
- }
489
-
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("}")) {
516
- try {
517
- const jsonResponse = JSON.parse(trimmedResponse);
518
- if (jsonResponse && !jsonResponse.ok) {
519
- setIsAiWorking(false);
520
- if (jsonResponse.openLogin) {
521
- return { error: "login_required" };
522
- } else if (jsonResponse.openSelectProvider) {
523
- return { error: "provider_required", message: jsonResponse.message };
524
- } else if (jsonResponse.openProModal) {
525
- return { error: "pro_required" };
526
- } else {
527
- toast.error(jsonResponse.message);
528
- return { error: "api_error", message: jsonResponse.message };
529
- }
530
- }
531
- } catch (e) {
532
- // Not complete JSON yet, continue
533
  }
534
- }
535
 
536
- return read();
537
- };
 
 
 
 
 
 
 
 
 
 
538
 
539
- return await read();
 
 
 
 
 
 
 
540
  }
541
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
542
  } catch (error: any) {
543
  setIsAiWorking(false);
544
- setIsThinking(false);
545
- setThinkingContent("");
546
- setController(null);
547
-
548
- if (!abortController.signal.aborted) {
549
- toast.error(error.message || "Network error occurred");
550
- }
551
-
552
  if (error.openLogin) {
553
  return { error: "login_required" };
554
  }
@@ -596,6 +423,7 @@ export const useAi = (onScrollToBottom?: () => void) => {
596
  if (pages.length > 0) {
597
  setPages(pages);
598
  if (isStreaming) {
 
599
  const newPages = pages.filter(p =>
600
  !streamingPagesRef.current.has(p.path)
601
  );
@@ -605,6 +433,7 @@ export const useAi = (onScrollToBottom?: () => void) => {
605
  setCurrentPage(newPage.path);
606
  streamingPagesRef.current.add(newPage.path);
607
 
 
608
  if (newPage.path.endsWith('.html') && !newPage.path.includes('/components/')) {
609
  setPreviewPage(newPage.path);
610
  }
@@ -624,30 +453,41 @@ export const useAi = (onScrollToBottom?: () => void) => {
624
  const extractFileContent = (chunk: string, filePath: string): string => {
625
  if (!chunk) return "";
626
 
 
627
  let content = chunk.trim();
628
 
 
629
  if (filePath.endsWith('.css')) {
 
630
  const cssMatch = content.match(/```css\s*([\s\S]*?)\s*```/);
631
  if (cssMatch) {
632
  content = cssMatch[1];
633
  } else {
 
634
  content = content.replace(/^```css\s*/i, "");
635
  }
 
636
  return content.replace(/```/g, "").trim();
637
  } else if (filePath.endsWith('.js')) {
 
638
  const jsMatch = content.match(/```(?:javascript|js)\s*([\s\S]*?)\s*```/);
639
  if (jsMatch) {
640
  content = jsMatch[1];
641
  } else {
 
642
  content = content.replace(/^```(?:javascript|js)\s*/i, "");
643
  }
 
644
  return content.replace(/```/g, "").trim();
645
  } else {
 
646
  const htmlMatch = content.match(/```html\s*([\s\S]*?)\s*```/);
647
  if (htmlMatch) {
648
  content = htmlMatch[1];
649
  } else {
 
650
  content = content.replace(/^```html\s*/i, "");
 
651
  const doctypeMatch = content.match(/<!DOCTYPE html>[\s\S]*/);
652
  if (doctypeMatch) {
653
  content = doctypeMatch[0];
@@ -690,8 +530,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,
 
124
  client.setQueryData(["ai.model"], newModel);
125
  };
126
 
127
+ const createNewProject = async (prompt: string, htmlPages: Page[], projectName: string | undefined, isLoggedIn?: boolean) => {
128
+ if (isLoggedIn) {
129
+ api.post("/me/projects", {
130
+ title: projectName,
131
+ pages: htmlPages,
132
+ prompt,
133
+ })
134
+ .then((response) => {
135
+ if (response.data.ok) {
136
+ setIsAiWorking(false);
137
+ router.replace(`/${response.data.space.project.space_id}`);
138
+ setProject(response.data.space);
139
+ setProjects([...projects, response.data.space]);
140
+ toast.success("AI responded successfully");
141
+ if (audio.current) audio.current.play();
 
 
 
 
 
 
142
  }
143
+ })
144
+ .catch((error) => {
145
  setIsAiWorking(false);
146
+ toast.error(error?.response?.data?.message || error?.message || "Failed to create project");
147
+ });
 
 
 
 
 
148
  } else {
149
  setIsAiWorking(false);
150
  toast.success("AI responded successfully");
 
152
  }
153
  }
154
 
155
+ const callAiNewProject = async (prompt: string, enhancedSettings?: EnhancedSettings, redesignMarkdown?: string, isLoggedIn?: boolean) => {
156
  if (isAiWorking) return;
157
  if (!redesignMarkdown && !prompt.trim()) return;
158
 
159
  setIsAiWorking(true);
 
160
  streamingPagesRef.current.clear(); // Reset tracking for new generation
161
 
162
  const abortController = new AbortController();
 
189
  const { done, value } = await reader.read();
190
 
191
  if (done) {
 
 
 
 
 
 
 
 
192
  const trimmedResponse = contentResponse.trim();
193
  if (trimmedResponse.startsWith("{") && trimmedResponse.endsWith("}")) {
194
  try {
 
213
  const newPages = formatPages(contentResponse, false);
214
  let projectName = contentResponse.match(/<<<<<<< PROJECT_NAME_START\s*([\s\S]*?)\s*>>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
215
  if (!projectName) {
216
+ projectName = prompt.substring(0, 40).replace(/[^a-zA-Z0-9]/g, "-").slice(0, 40);
217
  }
218
  setPages(newPages);
219
  setLastSavedPages([...newPages]);
220
  if (newPages.length > 0 && !isTheSameHtml(newPages[0].html)) {
221
+ createNewProject(prompt, newPages, projectName, isLoggedIn);
222
  }
223
  setPrompts([...prompts, prompt]);
224
 
 
228
  const chunk = decoder.decode(value, { stream: true });
229
  contentResponse += chunk;
230
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  const trimmedResponse = contentResponse.trim();
232
  if (trimmedResponse.startsWith("{") && trimmedResponse.endsWith("}")) {
233
  try {
 
260
  } catch (error: any) {
261
  setIsAiWorking(false);
262
  setIsThinking(false);
 
263
  setController(null);
264
 
265
  if (!abortController.signal.aborted) {
 
279
 
280
 
281
  setIsAiWorking(true);
 
282
 
283
  const abortController = new AbortController();
284
  setController(abortController);
 
311
  });
312
 
313
  if (request && request.body) {
314
+ const res = await request.json();
315
+
316
+ if (!request.ok) {
317
+ if (res.openLogin) {
318
+ setIsAiWorking(false);
319
+ return { error: "login_required" };
320
+ } else if (res.openSelectProvider) {
321
+ setIsAiWorking(false);
322
+ return { error: "provider_required", message: res.message };
323
+ } else if (res.openProModal) {
324
+ setIsAiWorking(false);
325
+ return { error: "pro_required" };
326
+ } else {
327
+ toast.error(res.message);
328
+ setIsAiWorking(false);
329
+ return { error: "api_error", message: res.message };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  }
331
+ }
332
 
333
+ toast.success("AI responded successfully");
334
+ const iframe = document.getElementById(
335
+ "preview-iframe"
336
+ ) as HTMLIFrameElement;
337
+
338
+ if (isNew && res.repoId) {
339
+ router.push(`/${res.repoId}`);
340
+ setIsAiWorking(false);
341
+ } else {
342
+ const returnedPages = res.pages as Page[];
343
+ const updatedPagesMap = new Map(returnedPages.map((p: Page) => [p.path, p]));
344
+ const mergedPages: Page[] = pages.map(page =>
345
+ updatedPagesMap.has(page.path) ? updatedPagesMap.get(page.path)! : page
346
+ );
347
+ returnedPages.forEach((page: Page) => {
348
+ if (!pages.find(p => p.path === page.path)) {
349
+ mergedPages.push(page);
 
 
 
350
  }
351
+ });
352
 
353
+ setPages(mergedPages);
354
+ setLastSavedPages([...mergedPages]);
355
+ setCommits([res.commit, ...commits]);
356
+ setPrompts(
357
+ [...prompts, prompt]
358
+ )
359
+ setSelectedElement(null);
360
+ setSelectedFiles([]);
361
+ // setContextFile(null); not needed yet, keep context for the next request.
362
+ setIsEditableModeEnabled(false);
363
+ setIsAiWorking(false);
364
+ }
365
 
366
+ if (audio.current) audio.current.play();
367
+ if (iframe) {
368
+ setTimeout(() => {
369
+ iframe.src = iframe.src;
370
+ }, 500);
371
+ }
372
+
373
+ return { success: true, html: res.html, updatedLines: res.updatedLines };
374
  }
375
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
376
  } catch (error: any) {
377
  setIsAiWorking(false);
378
+ toast.error(error.message);
 
 
 
 
 
 
 
379
  if (error.openLogin) {
380
  return { error: "login_required" };
381
  }
 
423
  if (pages.length > 0) {
424
  setPages(pages);
425
  if (isStreaming) {
426
+ // Find new pages that haven't been shown yet (HTML, CSS, JS, etc.)
427
  const newPages = pages.filter(p =>
428
  !streamingPagesRef.current.has(p.path)
429
  );
 
433
  setCurrentPage(newPage.path);
434
  streamingPagesRef.current.add(newPage.path);
435
 
436
+ // Update preview if it's an HTML file not in components folder
437
  if (newPage.path.endsWith('.html') && !newPage.path.includes('/components/')) {
438
  setPreviewPage(newPage.path);
439
  }
 
453
  const extractFileContent = (chunk: string, filePath: string): string => {
454
  if (!chunk) return "";
455
 
456
+ // Remove backticks first
457
  let content = chunk.trim();
458
 
459
+ // Handle different file types
460
  if (filePath.endsWith('.css')) {
461
+ // Try to extract CSS from complete code blocks first
462
  const cssMatch = content.match(/```css\s*([\s\S]*?)\s*```/);
463
  if (cssMatch) {
464
  content = cssMatch[1];
465
  } else {
466
+ // Handle incomplete code blocks during streaming (remove opening fence)
467
  content = content.replace(/^```css\s*/i, "");
468
  }
469
+ // Remove any remaining backticks
470
  return content.replace(/```/g, "").trim();
471
  } else if (filePath.endsWith('.js')) {
472
+ // Try to extract JavaScript from complete code blocks first
473
  const jsMatch = content.match(/```(?:javascript|js)\s*([\s\S]*?)\s*```/);
474
  if (jsMatch) {
475
  content = jsMatch[1];
476
  } else {
477
+ // Handle incomplete code blocks during streaming (remove opening fence)
478
  content = content.replace(/^```(?:javascript|js)\s*/i, "");
479
  }
480
+ // Remove any remaining backticks
481
  return content.replace(/```/g, "").trim();
482
  } else {
483
+ // Handle HTML files
484
  const htmlMatch = content.match(/```html\s*([\s\S]*?)\s*```/);
485
  if (htmlMatch) {
486
  content = htmlMatch[1];
487
  } else {
488
+ // Handle incomplete code blocks during streaming (remove opening fence)
489
  content = content.replace(/^```html\s*/i, "");
490
+ // Try to find HTML starting with DOCTYPE
491
  const doctypeMatch = content.match(/<!DOCTYPE html>[\s\S]*/);
492
  if (doctypeMatch) {
493
  content = doctypeMatch[0];
 
530
  return {
531
  isThinking,
532
  setIsThinking,
 
 
533
  callAiNewProject,
534
  callAiFollowUp,
535
  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/format-ai-response.ts DELETED
@@ -1,255 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { Page } from "@/types";
3
- import {
4
- DIVIDER,
5
- NEW_FILE_END,
6
- NEW_FILE_START,
7
- REPLACE_END,
8
- SEARCH_START,
9
- UPDATE_FILE_END,
10
- UPDATE_FILE_START,
11
- } from "./prompts";
12
-
13
- /**
14
- * Escape special regex characters in a string
15
- */
16
- export const escapeRegExp = (string: string): string => {
17
- return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
18
- };
19
-
20
- /**
21
- * Create a flexible HTML regex that accounts for whitespace variations
22
- */
23
- export const createFlexibleHtmlRegex = (searchBlock: string): RegExp => {
24
- let searchRegex = escapeRegExp(searchBlock)
25
- .replace(/\s+/g, '\\s*')
26
- .replace(/>\s*</g, '>\\s*<')
27
- .replace(/\s*>/g, '\\s*>');
28
-
29
- return new RegExp(searchRegex, 'g');
30
- };
31
-
32
- /**
33
- * Process AI response chunk and apply updates to pages
34
- * Returns updated pages and updated line numbers
35
- */
36
- export interface ProcessAiResponseResult {
37
- updatedPages: Page[];
38
- updatedLines: number[][];
39
- }
40
-
41
- export const processAiResponse = (
42
- chunk: string,
43
- pages: Page[]
44
- ): ProcessAiResponseResult => {
45
- const updatedLines: number[][] = [];
46
- const updatedPages = [...pages];
47
-
48
- // Process UPDATE_FILE blocks
49
- const updateFileRegex = new RegExp(
50
- `${UPDATE_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${UPDATE_FILE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`,
51
- 'g'
52
- );
53
- let updateFileMatch;
54
-
55
- while ((updateFileMatch = updateFileRegex.exec(chunk)) !== null) {
56
- const [, filePath, fileContent] = updateFileMatch;
57
-
58
- const pageIndex = updatedPages.findIndex(p => p.path === filePath);
59
- if (pageIndex !== -1) {
60
- let pageHtml = updatedPages[pageIndex].html;
61
-
62
- let processedContent = fileContent;
63
- const htmlMatch = fileContent.match(/```html\s*([\s\S]*?)\s*```/);
64
- if (htmlMatch) {
65
- processedContent = htmlMatch[1];
66
- }
67
- let position = 0;
68
- let moreBlocks = true;
69
-
70
- while (moreBlocks) {
71
- const searchStartIndex = processedContent.indexOf(SEARCH_START, position);
72
- if (searchStartIndex === -1) {
73
- moreBlocks = false;
74
- continue;
75
- }
76
-
77
- const dividerIndex = processedContent.indexOf(DIVIDER, searchStartIndex);
78
- if (dividerIndex === -1) {
79
- moreBlocks = false;
80
- continue;
81
- }
82
-
83
- const replaceEndIndex = processedContent.indexOf(REPLACE_END, dividerIndex);
84
- if (replaceEndIndex === -1) {
85
- moreBlocks = false;
86
- continue;
87
- }
88
-
89
- const searchBlock = processedContent.substring(
90
- searchStartIndex + SEARCH_START.length,
91
- dividerIndex
92
- );
93
- const replaceBlock = processedContent.substring(
94
- dividerIndex + DIVIDER.length,
95
- replaceEndIndex
96
- );
97
-
98
- if (searchBlock.trim() === "") {
99
- pageHtml = `${replaceBlock}\n${pageHtml}`;
100
- updatedLines.push([1, replaceBlock.split("\n").length]);
101
- } else {
102
- const regex = createFlexibleHtmlRegex(searchBlock);
103
- const match = regex.exec(pageHtml);
104
-
105
- if (match) {
106
- const matchedText = match[0];
107
- const beforeText = pageHtml.substring(0, match.index);
108
- const startLineNumber = beforeText.split("\n").length;
109
- const replaceLines = replaceBlock.split("\n").length;
110
- const endLineNumber = startLineNumber + replaceLines - 1;
111
-
112
- updatedLines.push([startLineNumber, endLineNumber]);
113
- pageHtml = pageHtml.replace(matchedText, replaceBlock);
114
- }
115
- }
116
-
117
- position = replaceEndIndex + REPLACE_END.length;
118
- }
119
-
120
- updatedPages[pageIndex].html = pageHtml;
121
- }
122
- }
123
-
124
- // Process NEW_FILE blocks
125
- const newFileRegex = new RegExp(
126
- `${NEW_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${NEW_FILE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`,
127
- 'g'
128
- );
129
- let newFileMatch;
130
-
131
- while ((newFileMatch = newFileRegex.exec(chunk)) !== null) {
132
- const [, filePath, fileContent] = newFileMatch;
133
-
134
- let fileData = fileContent;
135
- // Try to extract content from code blocks
136
- const htmlMatch = fileContent.match(/```html\s*([\s\S]*?)\s*```/);
137
- const cssMatch = fileContent.match(/```css\s*([\s\S]*?)\s*```/);
138
- const jsMatch = fileContent.match(/```javascript\s*([\s\S]*?)\s*```/);
139
-
140
- if (htmlMatch) {
141
- fileData = htmlMatch[1];
142
- } else if (cssMatch) {
143
- fileData = cssMatch[1];
144
- } else if (jsMatch) {
145
- fileData = jsMatch[1];
146
- }
147
-
148
- const existingFileIndex = updatedPages.findIndex(p => p.path === filePath);
149
-
150
- if (existingFileIndex !== -1) {
151
- updatedPages[existingFileIndex] = {
152
- path: filePath,
153
- html: fileData.trim()
154
- };
155
- } else {
156
- updatedPages.push({
157
- path: filePath,
158
- html: fileData.trim()
159
- });
160
- }
161
- }
162
-
163
- // Fallback: process SEARCH/REPLACE blocks without UPDATE_FILE wrapper (backward compatibility)
164
- if (updatedPages.length === pages.length && !chunk.includes(UPDATE_FILE_START)) {
165
- let position = 0;
166
- let moreBlocks = true;
167
- let newHtml = updatedPages[0]?.html || "";
168
-
169
- while (moreBlocks) {
170
- const searchStartIndex = chunk.indexOf(SEARCH_START, position);
171
- if (searchStartIndex === -1) {
172
- moreBlocks = false;
173
- continue;
174
- }
175
-
176
- const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
177
- if (dividerIndex === -1) {
178
- moreBlocks = false;
179
- continue;
180
- }
181
-
182
- const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
183
- if (replaceEndIndex === -1) {
184
- moreBlocks = false;
185
- continue;
186
- }
187
-
188
- const searchBlock = chunk.substring(
189
- searchStartIndex + SEARCH_START.length,
190
- dividerIndex
191
- );
192
- const replaceBlock = chunk.substring(
193
- dividerIndex + DIVIDER.length,
194
- replaceEndIndex
195
- );
196
-
197
- if (searchBlock.trim() === "") {
198
- newHtml = `${replaceBlock}\n${newHtml}`;
199
- updatedLines.push([1, replaceBlock.split("\n").length]);
200
- } else {
201
- const regex = createFlexibleHtmlRegex(searchBlock);
202
- const match = regex.exec(newHtml);
203
-
204
- if (match) {
205
- const matchedText = match[0];
206
- const beforeText = newHtml.substring(0, match.index);
207
- const startLineNumber = beforeText.split("\n").length;
208
- const replaceLines = replaceBlock.split("\n").length;
209
- const endLineNumber = startLineNumber + replaceLines - 1;
210
-
211
- updatedLines.push([startLineNumber, endLineNumber]);
212
- newHtml = newHtml.replace(matchedText, replaceBlock);
213
- }
214
- }
215
-
216
- position = replaceEndIndex + REPLACE_END.length;
217
- }
218
-
219
- const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
220
- if (mainPageIndex !== -1) {
221
- updatedPages[mainPageIndex].html = newHtml;
222
- }
223
- }
224
-
225
- return { updatedPages, updatedLines };
226
- };
227
-
228
- /**
229
- * Convert pages to File objects for upload to HuggingFace
230
- */
231
- export const pagesToFiles = (pages: Page[]): File[] => {
232
- const files: File[] = [];
233
- pages.forEach((page: Page) => {
234
- let mimeType = "text/html";
235
- if (page.path.endsWith(".css")) {
236
- mimeType = "text/css";
237
- } else if (page.path.endsWith(".js")) {
238
- mimeType = "text/javascript";
239
- } else if (page.path.endsWith(".json")) {
240
- mimeType = "application/json";
241
- }
242
- const file = new File([page.html], page.path, { type: mimeType });
243
- files.push(file);
244
- });
245
- return files;
246
- };
247
-
248
- /**
249
- * Extract project name from AI response
250
- */
251
- export const extractProjectName = (chunk: string): string | null => {
252
- const projectName = chunk.match(/<<<<<<< PROJECT_NAME_START\s*([\s\S]*?)\s*>>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
253
- return projectName || null;
254
- };
255
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
 
next.config.ts CHANGED
@@ -3,7 +3,6 @@ import type { NextConfig } from "next";
3
  const nextConfig: NextConfig = {
4
  /* config options here */
5
  basePath: '/deepsite',
6
- assetPrefix: '/deepsite',
7
  async redirects() {
8
  return [
9
  {
 
3
  const nextConfig: NextConfig = {
4
  /* config options here */
5
  basePath: '/deepsite',
 
6
  async redirects() {
7
  return [
8
  {
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",