enzostvs HF Staff commited on
Commit
35ef307
Β·
1 Parent(s): 15a77af

[BIG UPDATE πŸš€] New generation files

Browse files
app/api/ask/route.ts CHANGED
@@ -10,12 +10,12 @@ import {
10
  FOLLOW_UP_SYSTEM_PROMPT,
11
  INITIAL_SYSTEM_PROMPT,
12
  MAX_REQUESTS_PER_IP,
13
- NEW_PAGE_END,
14
- NEW_PAGE_START,
15
  REPLACE_END,
16
  SEARCH_START,
17
- UPDATE_PAGE_START,
18
- UPDATE_PAGE_END,
19
  PROMPT_FOR_PROJECT_NAME,
20
  } from "@/lib/prompts";
21
  import { calculateMaxTokens, estimateInputTokens, getProviderSpecificConfig } from "@/lib/max-tokens";
@@ -27,6 +27,7 @@ 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
 
31
  const ipAddresses = new Map();
32
 
@@ -192,11 +193,9 @@ export async function POST(request: NextRequest) {
192
  );
193
  }
194
  } finally {
195
- // Ensure the writer is always closed, even if already closed
196
  try {
197
  await writer?.close();
198
  } catch {
199
- // Ignore errors when closing the writer as it might already be closed
200
  }
201
  }
202
  })();
@@ -216,7 +215,6 @@ export async function POST(request: NextRequest) {
216
  }
217
 
218
  export async function PUT(request: NextRequest) {
219
- console.log("PUT request received");
220
  const user = await isAuthenticated();
221
  if (user instanceof NextResponse || !user) {
222
  return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
@@ -225,7 +223,7 @@ export async function PUT(request: NextRequest) {
225
  const authHeaders = await headers();
226
 
227
  const body = await request.json();
228
- const { prompt, previousPrompts, provider, selectedElementHtml, model, pages, files, repoId: repoIdFromBody, isNew, enhancedSettings } =
229
  body;
230
 
231
  let repoId = repoIdFromBody;
@@ -301,7 +299,6 @@ export async function PUT(request: NextRequest) {
301
  const systemPrompt = FOLLOW_UP_SYSTEM_PROMPT + (isNew ? PROMPT_FOR_PROJECT_NAME : "");
302
  const userContext = "You are modifying the HTML file based on the user's request.";
303
 
304
- // Send all pages without filtering
305
  const allPages = pages || [];
306
  const pagesContext = allPages
307
  .map((p: Page) => `- ${p.path}\n${p.html}`)
@@ -368,18 +365,18 @@ export async function PUT(request: NextRequest) {
368
  let newHtml = "";
369
  const updatedPages = [...(pages || [])];
370
 
371
- const updatePageRegex = new RegExp(`${UPDATE_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${UPDATE_PAGE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`, 'g');
372
- let updatePageMatch;
373
 
374
- while ((updatePageMatch = updatePageRegex.exec(chunk)) !== null) {
375
- const [, pagePath, pageContent] = updatePageMatch;
376
 
377
- const pageIndex = updatedPages.findIndex(p => p.path === pagePath);
378
  if (pageIndex !== -1) {
379
  let pageHtml = updatedPages[pageIndex].html;
380
 
381
- let processedContent = pageContent;
382
- const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
383
  if (htmlMatch) {
384
  processedContent = htmlMatch[1];
385
  }
@@ -438,40 +435,48 @@ export async function PUT(request: NextRequest) {
438
 
439
  updatedPages[pageIndex].html = pageHtml;
440
 
441
- if (pagePath === '/' || pagePath === '/index' || pagePath === 'index') {
442
  newHtml = pageHtml;
443
  }
444
  }
445
  }
446
 
447
- const newPageRegex = new RegExp(`${NEW_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${NEW_PAGE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`, 'g');
448
- let newPageMatch;
449
 
450
- while ((newPageMatch = newPageRegex.exec(chunk)) !== null) {
451
- const [, pagePath, pageContent] = newPageMatch;
 
 
 
 
 
 
452
 
453
- let pageHtml = pageContent;
454
- const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
455
  if (htmlMatch) {
456
- pageHtml = htmlMatch[1];
 
 
 
 
457
  }
458
 
459
- const existingPageIndex = updatedPages.findIndex(p => p.path === pagePath);
460
 
461
- if (existingPageIndex !== -1) {
462
- updatedPages[existingPageIndex] = {
463
- path: pagePath,
464
- html: pageHtml.trim()
465
  };
466
  } else {
467
  updatedPages.push({
468
- path: pagePath,
469
- html: pageHtml.trim()
470
  });
471
  }
472
  }
473
 
474
- if (updatedPages.length === pages?.length && !chunk.includes(UPDATE_PAGE_START)) {
475
  let position = 0;
476
  let moreBlocks = true;
477
 
@@ -525,7 +530,6 @@ export async function PUT(request: NextRequest) {
525
  position = replaceEndIndex + REPLACE_END.length;
526
  }
527
 
528
- // Update the main HTML if it's the index page
529
  const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
530
  if (mainPageIndex !== -1) {
531
  updatedPages[mainPageIndex].html = newHtml;
@@ -534,12 +538,23 @@ export async function PUT(request: NextRequest) {
534
 
535
  const files: File[] = [];
536
  updatedPages.forEach((page: Page) => {
537
- const file = new File([page.html], page.path, { type: "text/html" });
 
 
 
 
 
 
 
 
 
 
 
538
  files.push(file);
539
  });
540
 
541
  if (isNew) {
542
- const projectName = chunk.match(/<<<<<<< PROJECT_NAME_START ([\s\S]*?) >>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
543
  const formattedTitle = projectName?.toLowerCase()
544
  .replace(/[^a-z0-9]+/g, "-")
545
  .split("-")
 
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";
 
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
 
 
193
  );
194
  }
195
  } finally {
 
196
  try {
197
  await writer?.close();
198
  } catch {
 
199
  }
200
  }
201
  })();
 
215
  }
216
 
217
  export async function PUT(request: NextRequest) {
 
218
  const user = await isAuthenticated();
219
  if (user instanceof NextResponse || !user) {
220
  return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
 
223
  const authHeaders = await headers();
224
 
225
  const body = await request.json();
226
+ const { prompt, provider, selectedElementHtml, model, pages, files, repoId: repoIdFromBody, isNew } =
227
  body;
228
 
229
  let repoId = repoIdFromBody;
 
299
  const systemPrompt = FOLLOW_UP_SYSTEM_PROMPT + (isNew ? PROMPT_FOR_PROJECT_NAME : "");
300
  const userContext = "You are modifying the HTML file based on the user's request.";
301
 
 
302
  const allPages = pages || [];
303
  const pagesContext = allPages
304
  .map((p: Page) => `- ${p.path}\n${p.html}`)
 
365
  let newHtml = "";
366
  const updatedPages = [...(pages || [])];
367
 
368
+ 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');
369
+ let updateFileMatch;
370
 
371
+ while ((updateFileMatch = updateFileRegex.exec(chunk)) !== null) {
372
+ const [, filePath, fileContent] = updateFileMatch;
373
 
374
+ const pageIndex = updatedPages.findIndex(p => p.path === filePath);
375
  if (pageIndex !== -1) {
376
  let pageHtml = updatedPages[pageIndex].html;
377
 
378
+ let processedContent = fileContent;
379
+ const htmlMatch = fileContent.match(/```html\s*([\s\S]*?)\s*```/);
380
  if (htmlMatch) {
381
  processedContent = htmlMatch[1];
382
  }
 
435
 
436
  updatedPages[pageIndex].html = pageHtml;
437
 
438
+ if (filePath === '/' || filePath === '/index' || filePath === 'index' || filePath === 'index.html') {
439
  newHtml = pageHtml;
440
  }
441
  }
442
  }
443
 
444
+ 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');
445
+ let newFileMatch;
446
 
447
+ while ((newFileMatch = newFileRegex.exec(chunk)) !== null) {
448
+ const [, filePath, fileContent] = newFileMatch;
449
+
450
+ let fileData = fileContent;
451
+ // Try to extract content from code blocks
452
+ const htmlMatch = fileContent.match(/```html\s*([\s\S]*?)\s*```/);
453
+ const cssMatch = fileContent.match(/```css\s*([\s\S]*?)\s*```/);
454
+ const jsMatch = fileContent.match(/```javascript\s*([\s\S]*?)\s*```/);
455
 
 
 
456
  if (htmlMatch) {
457
+ fileData = htmlMatch[1];
458
+ } else if (cssMatch) {
459
+ fileData = cssMatch[1];
460
+ } else if (jsMatch) {
461
+ fileData = jsMatch[1];
462
  }
463
 
464
+ const existingFileIndex = updatedPages.findIndex(p => p.path === filePath);
465
 
466
+ if (existingFileIndex !== -1) {
467
+ updatedPages[existingFileIndex] = {
468
+ path: filePath,
469
+ html: fileData.trim()
470
  };
471
  } else {
472
  updatedPages.push({
473
+ path: filePath,
474
+ html: fileData.trim()
475
  });
476
  }
477
  }
478
 
479
+ if (updatedPages.length === pages?.length && !chunk.includes(UPDATE_FILE_START)) {
480
  let position = 0;
481
  let moreBlocks = true;
482
 
 
530
  position = replaceEndIndex + REPLACE_END.length;
531
  }
532
 
 
533
  const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
534
  if (mainPageIndex !== -1) {
535
  updatedPages[mainPageIndex].html = newHtml;
 
538
 
539
  const files: File[] = [];
540
  updatedPages.forEach((page: Page) => {
541
+ let mimeType = "text/html";
542
+ if (page.path.endsWith(".css")) {
543
+ mimeType = "text/css";
544
+ } else if (page.path.endsWith(".js")) {
545
+ mimeType = "text/javascript";
546
+ } else if (page.path.endsWith(".json")) {
547
+ mimeType = "application/json";
548
+ }
549
+ const content = (mimeType === "text/html" && isIndexPage(page.path))
550
+ ? injectDeepSiteBadge(page.html)
551
+ : page.html;
552
+ const file = new File([content], page.path, { type: mimeType });
553
  files.push(file);
554
  });
555
 
556
  if (isNew) {
557
+ const projectName = chunk.match(/<<<<<<< PROJECT_NAME_START\s*([\s\S]*?)\s*>>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
558
  const formattedTitle = projectName?.toLowerCase()
559
  .replace(/[^a-z0-9]+/g, "-")
560
  .split("-")
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")) {
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) {
@@ -126,10 +126,20 @@ export async function GET(
126
  });
127
  }
128
  }
129
- if (fileInfo.type === "directory" && ["videos", "images", "audio"].includes(fileInfo.path)) {
130
- for await (const imageInfo of listFiles({repo, accessToken: user.token as string, path: fileInfo.path})) {
131
- if (allowedFilesExtensions.includes(imageInfo.path.split(".").pop() || "")) {
132
- files.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${imageInfo.path}`);
 
 
 
 
 
 
 
 
 
 
133
  }
134
  }
135
  }
 
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) {
 
126
  });
127
  }
128
  }
129
+ if (fileInfo.type === "directory" && (["videos", "images", "audio"].includes(fileInfo.path) || fileInfo.path === "components")) {
130
+ for await (const subFileInfo of listFiles({repo, accessToken: user.token as string, path: fileInfo.path})) {
131
+ if (subFileInfo.path.includes("components")) {
132
+ const blob = await downloadFile({ repo, accessToken: user.token as string, path: subFileInfo.path, raw: true });
133
+ const html = await blob?.text();
134
+ if (!html) {
135
+ continue;
136
+ }
137
+ htmlFiles.push({
138
+ path: subFileInfo.path,
139
+ html,
140
+ });
141
+ } else if (allowedFilesExtensions.includes(subFileInfo.path.split(".").pop() || "")) {
142
+ files.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${subFileInfo.path}`);
143
  }
144
  }
145
  }
app/api/me/projects/[namespace]/[repoId]/save/route.ts CHANGED
@@ -3,6 +3,7 @@ import { uploadFiles } from "@huggingface/hub";
3
 
4
  import { isAuthenticated } from "@/lib/auth";
5
  import { Page } from "@/types";
 
6
 
7
  export async function PUT(
8
  req: NextRequest,
@@ -28,11 +29,23 @@ export async function PUT(
28
  // Prepare files for upload
29
  const files: File[] = [];
30
  pages.forEach((page: Page) => {
31
- const file = new File([page.html], page.path, { type: "text/html" });
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  files.push(file);
33
  });
34
 
35
- // Upload files to HuggingFace Hub
36
  const response = await uploadFiles({
37
  repo: {
38
  type: "space",
 
3
 
4
  import { isAuthenticated } from "@/lib/auth";
5
  import { Page } from "@/types";
6
+ import { injectDeepSiteBadge, isIndexPage } from "@/lib/inject-badge";
7
 
8
  export async function PUT(
9
  req: NextRequest,
 
29
  // Prepare files for upload
30
  const files: File[] = [];
31
  pages.forEach((page: Page) => {
32
+ // Determine MIME type based on file extension
33
+ let mimeType = "text/html";
34
+ if (page.path.endsWith(".css")) {
35
+ mimeType = "text/css";
36
+ } else if (page.path.endsWith(".js")) {
37
+ mimeType = "text/javascript";
38
+ } else if (page.path.endsWith(".json")) {
39
+ mimeType = "application/json";
40
+ }
41
+ // Inject the DeepSite badge script into index pages only (not components or other HTML files)
42
+ const content = (mimeType === "text/html" && isIndexPage(page.path))
43
+ ? injectDeepSiteBadge(page.html)
44
+ : page.html;
45
+ const file = new File([content], page.path, { type: mimeType });
46
  files.push(file);
47
  });
48
 
 
49
  const response = await uploadFiles({
50
  repo: {
51
  type: "space",
app/api/me/projects/route.ts CHANGED
@@ -4,6 +4,7 @@ import { RepoDesignation, createRepo, listCommits, spaceInfo, uploadFiles } from
4
  import { isAuthenticated } from "@/lib/auth";
5
  import { Commit, Page } from "@/types";
6
  import { COLORS } from "@/lib/utils";
 
7
 
8
  export async function POST(
9
  req: NextRequest,
@@ -50,7 +51,20 @@ This project was created with [DeepSite](https://deepsite.hf.co).
50
  const readmeFile = new File([README], "README.md", { type: "text/markdown" });
51
  files.push(readmeFile);
52
  pages.forEach((page: Page) => {
53
- const file = new File([page.html], page.path, { type: "text/html" });
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  files.push(file);
55
  });
56
 
 
4
  import { isAuthenticated } from "@/lib/auth";
5
  import { Commit, Page } from "@/types";
6
  import { COLORS } from "@/lib/utils";
7
+ import { injectDeepSiteBadge, isIndexPage } from "@/lib/inject-badge";
8
 
9
  export async function POST(
10
  req: NextRequest,
 
51
  const readmeFile = new File([README], "README.md", { type: "text/markdown" });
52
  files.push(readmeFile);
53
  pages.forEach((page: Page) => {
54
+ // Determine MIME type based on file extension
55
+ let mimeType = "text/html";
56
+ if (page.path.endsWith(".css")) {
57
+ mimeType = "text/css";
58
+ } else if (page.path.endsWith(".js")) {
59
+ mimeType = "text/javascript";
60
+ } else if (page.path.endsWith(".json")) {
61
+ mimeType = "application/json";
62
+ }
63
+ // Inject the DeepSite badge script into index pages only (not components or other HTML files)
64
+ const content = (mimeType === "text/html" && isIndexPage(page.path))
65
+ ? injectDeepSiteBadge(page.html)
66
+ : page.html;
67
+ const file = new File([content], page.path, { type: mimeType });
68
  files.push(file);
69
  });
70
 
components/editor/ask-ai/context.tsx ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useMemo } from "react";
2
+ import { FileCode, FileText, Braces, AtSign } from "lucide-react";
3
+
4
+ import { Button } from "@/components/ui/button";
5
+ import { useEditor } from "@/hooks/useEditor";
6
+ import { useAi } from "@/hooks/useAi";
7
+ import {
8
+ Popover,
9
+ PopoverContent,
10
+ PopoverTrigger,
11
+ } from "@/components/ui/popover";
12
+ import classNames from "classnames";
13
+
14
+ export const Context = () => {
15
+ const { pages, currentPage, globalEditorLoading } = useEditor();
16
+ const { contextFile, setContextFile, globalAiLoading } = useAi();
17
+ const [open, setOpen] = useState(false);
18
+
19
+ const selectedFile = contextFile || null;
20
+
21
+ const getFileIcon = (filePath: string, size = "size-3.5") => {
22
+ if (filePath.endsWith(".css")) {
23
+ return <Braces className={size} />;
24
+ } else if (filePath.endsWith(".js")) {
25
+ return <FileCode className={size} />;
26
+ } else {
27
+ return <FileText className={size} />;
28
+ }
29
+ };
30
+
31
+ const buttonContent = useMemo(() => {
32
+ if (selectedFile) {
33
+ return (
34
+ <>
35
+ <span className="truncate max-w-[120px]">{selectedFile}</span>
36
+ </>
37
+ );
38
+ }
39
+ return <>Add Context</>;
40
+ }, [selectedFile]);
41
+
42
+ return (
43
+ <Popover open={open} onOpenChange={setOpen}>
44
+ <PopoverTrigger asChild>
45
+ <Button
46
+ size="xs"
47
+ variant={open ? "default" : "outline"}
48
+ className={classNames("!rounded-md", {
49
+ "!bg-blue-500/10 !border-blue-500/30 !text-blue-400":
50
+ selectedFile && selectedFile.endsWith(".css"),
51
+ "!bg-orange-500/10 !border-orange-500/30 !text-orange-400":
52
+ selectedFile && selectedFile.endsWith(".html"),
53
+ "!bg-amber-500/10 !border-amber-500/30 !text-amber-400":
54
+ selectedFile && selectedFile.endsWith(".js"),
55
+ })}
56
+ disabled={
57
+ globalAiLoading || globalEditorLoading || pages.length === 0
58
+ }
59
+ >
60
+ <AtSign className="size-3.5" />
61
+
62
+ {buttonContent}
63
+ </Button>
64
+ </PopoverTrigger>
65
+ <PopoverContent
66
+ align="start"
67
+ className="w-64 !bg-neutral-900 !border-neutral-800 !p-0 !rounded-2xl overflow-hidden"
68
+ >
69
+ <header className="flex items-center justify-center text-xs px-2 py-2.5 border-b gap-2 bg-neutral-950 border-neutral-800 font-semibold text-neutral-200">
70
+ Select a file to send as context
71
+ </header>
72
+ <main className="space-y-1 p-2">
73
+ <div className="max-h-[200px] overflow-y-auto space-y-0.5">
74
+ {pages.length === 0 ? (
75
+ <div className="px-2 py-2 text-xs text-neutral-500">
76
+ No files available
77
+ </div>
78
+ ) : (
79
+ <>
80
+ <button
81
+ onClick={() => {
82
+ setContextFile(null);
83
+ setOpen(false);
84
+ }}
85
+ className={`cursor-pointer w-full px-2 py-1.5 text-xs text-left rounded-md hover:bg-neutral-800 transition-colors ${
86
+ !selectedFile
87
+ ? "bg-neutral-800 text-neutral-200 font-medium"
88
+ : "text-neutral-400 hover:text-neutral-200"
89
+ }`}
90
+ >
91
+ All files (default)
92
+ </button>
93
+ {pages.map((page) => (
94
+ <button
95
+ key={page.path}
96
+ onClick={() => {
97
+ setContextFile(page.path);
98
+ setOpen(false);
99
+ }}
100
+ className={`cursor-pointer w-full px-2 py-1.5 text-xs text-left rounded-md hover:bg-neutral-800 transition-colors flex items-center gap-1.5 ${
101
+ selectedFile === page.path
102
+ ? "bg-neutral-800 text-neutral-200 font-medium"
103
+ : "text-neutral-400 hover:text-neutral-200"
104
+ }`}
105
+ >
106
+ <span className="shrink-0">
107
+ {getFileIcon(page.path, "size-3")}
108
+ </span>
109
+ <span className="truncate flex-1">{page.path}</span>
110
+ {page.path === currentPage && (
111
+ <span className="text-[10px] text-neutral-500 shrink-0">
112
+ (current)
113
+ </span>
114
+ )}
115
+ </button>
116
+ ))}
117
+ </>
118
+ )}
119
+ </div>
120
+ </main>
121
+ </PopoverContent>
122
+ </Popover>
123
+ );
124
+ };
components/editor/ask-ai/index.tsx CHANGED
@@ -15,6 +15,7 @@ import { Uploader } from "@/components/editor/ask-ai/uploader";
15
  import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
16
  import { Selector } from "@/components/editor/ask-ai/selector";
17
  import { PromptBuilder } from "@/components/editor/ask-ai/prompt-builder";
 
18
  import { useUser } from "@/hooks/useUser";
19
  import { useLoginModal } from "@/components/contexts/login-context";
20
  import { Settings } from "./settings";
@@ -41,11 +42,8 @@ export const AskAi = ({
41
  setSelectedFiles,
42
  selectedElement,
43
  setSelectedElement,
44
- setIsThinking,
45
  callAiNewProject,
46
  callAiFollowUp,
47
- setModel,
48
- selectedModel,
49
  audio: hookAudio,
50
  cancelRequest,
51
  } = useAi(onScrollToBottom);
@@ -112,9 +110,6 @@ export const AskAi = ({
112
 
113
  if (result?.success) {
114
  setPrompt("");
115
- // if (selectedModel?.isThinker) {
116
- // setModel(MODELS[0].value);
117
- // }
118
  }
119
  }
120
  };
@@ -284,10 +279,14 @@ export const AskAi = ({
284
  </div>
285
  <div className="flex items-center justify-between gap-2 px-4 pb-3 mt-2">
286
  <div className="flex-1 flex items-center justify-start gap-1.5 flex-wrap">
287
- <PromptBuilder
288
- enhancedSettings={enhancedSettings!}
289
- setEnhancedSettings={setEnhancedSettings}
290
- />
 
 
 
 
291
  <Settings
292
  open={openProvider}
293
  error={providerError}
 
15
  import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
16
  import { Selector } from "@/components/editor/ask-ai/selector";
17
  import { PromptBuilder } from "@/components/editor/ask-ai/prompt-builder";
18
+ import { Context } from "@/components/editor/ask-ai/context";
19
  import { useUser } from "@/hooks/useUser";
20
  import { useLoginModal } from "@/components/contexts/login-context";
21
  import { Settings } from "./settings";
 
42
  setSelectedFiles,
43
  selectedElement,
44
  setSelectedElement,
 
45
  callAiNewProject,
46
  callAiFollowUp,
 
 
47
  audio: hookAudio,
48
  cancelRequest,
49
  } = useAi(onScrollToBottom);
 
110
 
111
  if (result?.success) {
112
  setPrompt("");
 
 
 
113
  }
114
  }
115
  };
 
279
  </div>
280
  <div className="flex items-center justify-between gap-2 px-4 pb-3 mt-2">
281
  <div className="flex-1 flex items-center justify-start gap-1.5 flex-wrap">
282
+ {isNew ? (
283
+ <PromptBuilder
284
+ enhancedSettings={enhancedSettings!}
285
+ setEnhancedSettings={setEnhancedSettings}
286
+ />
287
+ ) : (
288
+ <Context />
289
+ )}
290
  <Settings
291
  open={openProvider}
292
  error={providerError}
components/editor/ask-ai/uploader.tsx CHANGED
@@ -2,13 +2,12 @@ import { useRef, useState } from "react";
2
  import {
3
  CheckCircle,
4
  ImageIcon,
5
- Images,
6
- Link,
7
  Paperclip,
8
  Upload,
9
  Video,
10
  Music,
11
  FileVideo,
 
12
  } from "lucide-react";
13
  import Image from "next/image";
14
 
@@ -24,8 +23,12 @@ import { useUser } from "@/hooks/useUser";
24
  import { useEditor } from "@/hooks/useEditor";
25
  import { useAi } from "@/hooks/useAi";
26
  import { useLoginModal } from "@/components/contexts/login-context";
 
27
 
28
  export const getFileType = (url: string) => {
 
 
 
29
  const extension = url.split(".").pop()?.toLowerCase();
30
  if (["jpg", "jpeg", "png", "gif", "webp", "svg"].includes(extension || "")) {
31
  return "image";
@@ -40,7 +43,13 @@ export const getFileType = (url: string) => {
40
  export const Uploader = ({ project }: { project: Project | undefined }) => {
41
  const { user } = useUser();
42
  const { openLoginModal } = useLoginModal();
43
- const { uploadFiles, isUploading, files, globalEditorLoading } = useEditor();
 
 
 
 
 
 
44
  const { selectedFiles, setSelectedFiles, globalAiLoading } = useAi();
45
 
46
  const [open, setOpen] = useState(false);
@@ -112,6 +121,27 @@ export const Uploader = ({ project }: { project: Project | undefined }) => {
112
  </p>
113
  </header>
114
  <main className="space-y-4 p-5">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  <div>
116
  <p className="text-xs text-left text-neutral-700 mb-2">
117
  Uploaded Media Files
 
2
  import {
3
  CheckCircle,
4
  ImageIcon,
 
 
5
  Paperclip,
6
  Upload,
7
  Video,
8
  Music,
9
  FileVideo,
10
+ Lock,
11
  } from "lucide-react";
12
  import Image from "next/image";
13
 
 
23
  import { useEditor } from "@/hooks/useEditor";
24
  import { useAi } from "@/hooks/useAi";
25
  import { useLoginModal } from "@/components/contexts/login-context";
26
+ import Link from "next/link";
27
 
28
  export const getFileType = (url: string) => {
29
+ if (typeof url !== "string") {
30
+ return "unknown";
31
+ }
32
  const extension = url.split(".").pop()?.toLowerCase();
33
  if (["jpg", "jpeg", "png", "gif", "webp", "svg"].includes(extension || "")) {
34
  return "image";
 
43
  export const Uploader = ({ project }: { project: Project | undefined }) => {
44
  const { user } = useUser();
45
  const { openLoginModal } = useLoginModal();
46
+ const {
47
+ uploadFiles,
48
+ isUploading,
49
+ files,
50
+ globalEditorLoading,
51
+ project: editorProject,
52
+ } = useEditor();
53
  const { selectedFiles, setSelectedFiles, globalAiLoading } = useAi();
54
 
55
  const [open, setOpen] = useState(false);
 
121
  </p>
122
  </header>
123
  <main className="space-y-4 p-5">
124
+ {editorProject?.private && (
125
+ <div className="flex items-center justify-center flex-col gap-2 bg-amber-500/10 rounded-md p-3 border border-amber-500/10">
126
+ <Lock className="size-4 text-lg text-amber-700" />
127
+ <p className="text-xs text-amber-700">
128
+ You can upload media files to your private project, but
129
+ probably won't be able to see them in the preview.
130
+ </p>
131
+ <Link
132
+ href={`https://huggingface.co/spaces/${editorProject.space_id}/settings`}
133
+ target="_blank"
134
+ >
135
+ <Button
136
+ variant="black"
137
+ size="xs"
138
+ className="!bg-amber-600 !text-white"
139
+ >
140
+ Make it public
141
+ </Button>
142
+ </Link>
143
+ </div>
144
+ )}
145
  <div>
146
  <p className="text-xs text-left text-neutral-700 mb-2">
147
  Uploaded Media Files
components/editor/file-browser/index.tsx ADDED
@@ -0,0 +1,458 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useMemo } from "react";
4
+ import {
5
+ FolderOpen,
6
+ FileCode2,
7
+ Folder,
8
+ ChevronRight,
9
+ ChevronDown,
10
+ } from "lucide-react";
11
+ import classNames from "classnames";
12
+
13
+ import { Page } from "@/types";
14
+ import { useEditor } from "@/hooks/useEditor";
15
+ import { Button } from "@/components/ui/button";
16
+ import {
17
+ Sheet,
18
+ SheetContent,
19
+ SheetHeader,
20
+ SheetTitle,
21
+ SheetTrigger,
22
+ } from "@/components/ui/sheet";
23
+ import {
24
+ Tooltip,
25
+ TooltipContent,
26
+ TooltipProvider,
27
+ TooltipTrigger,
28
+ } from "@/components/ui/tooltip";
29
+
30
+ interface FileNode {
31
+ name: string;
32
+ path: string;
33
+ type: "file" | "folder";
34
+ children?: FileNode[];
35
+ page?: Page;
36
+ }
37
+
38
+ export function FileBrowser() {
39
+ const { pages, currentPage, setCurrentPage, globalEditorLoading, project } =
40
+ useEditor();
41
+ const [open, setOpen] = useState(false);
42
+ const [expandedFolders, setExpandedFolders] = useState<Set<string>>(
43
+ new Set(["/"])
44
+ );
45
+
46
+ const toggleFolder = (path: string) => {
47
+ setExpandedFolders((prev) => {
48
+ const next = new Set(prev);
49
+ if (next.has(path)) {
50
+ next.delete(path);
51
+ } else {
52
+ next.add(path);
53
+ }
54
+ return next;
55
+ });
56
+ };
57
+
58
+ const fileTree = useMemo(() => {
59
+ const root: FileNode = {
60
+ name: "root",
61
+ path: "/",
62
+ type: "folder",
63
+ children: [],
64
+ };
65
+
66
+ pages.forEach((page) => {
67
+ const parts = page.path.split("/").filter(Boolean);
68
+ let currentNode = root;
69
+
70
+ parts.forEach((part, index) => {
71
+ const isFile = index === parts.length - 1;
72
+ const currentPath = "/" + parts.slice(0, index + 1).join("/");
73
+
74
+ if (!currentNode.children) {
75
+ currentNode.children = [];
76
+ }
77
+
78
+ let existingNode = currentNode.children.find((n) => n.name === part);
79
+
80
+ if (!existingNode) {
81
+ existingNode = {
82
+ name: part,
83
+ path: currentPath,
84
+ type: isFile ? "file" : "folder",
85
+ children: isFile ? undefined : [],
86
+ page: isFile ? page : undefined,
87
+ };
88
+ currentNode.children.push(existingNode);
89
+ }
90
+
91
+ if (!isFile) {
92
+ currentNode = existingNode;
93
+ }
94
+ });
95
+ });
96
+
97
+ // Sort: folders first, then files, both alphabetically
98
+ const sortNodes = (nodes: FileNode[] = []): FileNode[] => {
99
+ return nodes
100
+ .sort((a, b) => {
101
+ if (a.type !== b.type) {
102
+ return a.type === "folder" ? -1 : 1;
103
+ }
104
+ return a.name.localeCompare(b.name);
105
+ })
106
+ .map((node) => ({
107
+ ...node,
108
+ children: node.children ? sortNodes(node.children) : undefined,
109
+ }));
110
+ };
111
+
112
+ root.children = sortNodes(root.children);
113
+ return root;
114
+ }, [pages]);
115
+
116
+ const getFileIcon = (path: string) => {
117
+ const extension = path.split(".").pop()?.toLowerCase();
118
+
119
+ switch (extension) {
120
+ case "html":
121
+ return (
122
+ <svg className="size-4 shrink-0" viewBox="0 0 32 32" fill="none">
123
+ <path
124
+ d="M5.902 27.201L3.656 2h24.688l-2.249 25.197L15.985 30 5.902 27.201z"
125
+ fill="#E44D26"
126
+ />
127
+ <path
128
+ d="M16 27.858l8.17-2.265 1.922-21.532H16v23.797z"
129
+ fill="#F16529"
130
+ />
131
+ <path
132
+ d="M16 13.407h4.09l.282-3.165H16V7.151h7.75l-.074.829-.759 8.518H16v-3.091z"
133
+ fill="#EBEBEB"
134
+ />
135
+ <path
136
+ d="M16 21.434l-.014.004-3.442-.929-.22-2.465H9.221l.433 4.852 6.332 1.758.014-.004v-3.216z"
137
+ fill="#EBEBEB"
138
+ />
139
+ <path
140
+ d="M19.90 16.18l-.372 4.148-3.543.956v3.216l6.336-1.755.047-.522.537-6.043H19.90z"
141
+ fill="#FFF"
142
+ />
143
+ <path
144
+ d="M16 7.151v3.091h-7.3l-.062-.695-.141-1.567-.074-.829H16zM16 13.407v3.091h-3.399l-.062-.695-.14-1.566-.074-.83H16z"
145
+ fill="#FFF"
146
+ />
147
+ </svg>
148
+ );
149
+ case "css":
150
+ return (
151
+ <svg className="size-4 shrink-0" viewBox="0 0 32 32" fill="none">
152
+ <path
153
+ d="M5.902 27.201L3.656 2h24.688l-2.249 25.197L15.985 30 5.902 27.201z"
154
+ fill="#1572B6"
155
+ />
156
+ <path
157
+ d="M16 27.858l8.17-2.265 1.922-21.532H16v23.797z"
158
+ fill="#33A9DC"
159
+ />
160
+ <path
161
+ d="M16 13.191h4.09l.282-3.165H16V6.935h7.75l-.074.829-.759 8.518H16v-3.091z"
162
+ fill="#FFF"
163
+ />
164
+ <path
165
+ d="M16.019 21.218l-.014.004-3.442-.929-.22-2.465H9.24l.433 4.852 6.331 1.758.015-.004v-3.216z"
166
+ fill="#EBEBEB"
167
+ />
168
+ <path
169
+ d="M19.827 16.151l-.372 4.148-3.436.929v3.216l6.336-1.755.047-.522.726-8.016h-7.636v3h4.335z"
170
+ fill="#FFF"
171
+ />
172
+ <path
173
+ d="M16.011 6.935v3.091h-7.3l-.062-.695-.141-1.567-.074-.829h7.577zM16 13.191v3.091h-3.399l-.062-.695-.14-1.566-.074-.83H16z"
174
+ fill="#EBEBEB"
175
+ />
176
+ </svg>
177
+ );
178
+ case "js":
179
+ case "jsx":
180
+ return (
181
+ <svg className="size-4 shrink-0" viewBox="0 0 32 32" fill="none">
182
+ <rect width="32" height="32" rx="2" fill="#F7DF1E" />
183
+ <path
184
+ d="M20.63 22.3c.54.88 1.24 1.53 2.48 1.53.98 0 1.6-.48 1.6-1.16 0-.8-.64-1.1-1.72-1.57l-.59-.25c-1.7-.72-2.83-1.63-2.83-3.55 0-1.77 1.35-3.12 3.46-3.12 1.5 0 2.58.52 3.36 1.9l-1.84 1.18c-.4-.72-.84-1-1.51-1-.69 0-1.12.43-1.12 1 0 .7.43 1 1.43 1.43l.59.25c2 .86 3.13 1.73 3.13 3.7 0 2.12-1.66 3.3-3.9 3.3-2.18 0-3.6-1.04-4.3-2.4l1.96-1.12z"
185
+ fill="#000"
186
+ />
187
+ <path
188
+ d="M11.14 22.56c.35.62.67 1.15 1.44 1.15.74 0 1.2-.29 1.2-1.42V14.7h2.4v7.63c0 2.34-1.37 3.4-3.37 3.4-1.8 0-2.85-.94-3.38-2.06l1.71-1.1z"
189
+ fill="#000"
190
+ />
191
+ </svg>
192
+ );
193
+ case "json":
194
+ return (
195
+ <svg className="size-4 shrink-0" viewBox="0 0 32 32" fill="none">
196
+ <rect width="32" height="32" rx="2" fill="#F7DF1E" />
197
+ <path
198
+ 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"
199
+ fill="#000"
200
+ opacity="0.15"
201
+ />
202
+ <text
203
+ x="50%"
204
+ y="50%"
205
+ dominantBaseline="middle"
206
+ textAnchor="middle"
207
+ fill="#000"
208
+ fontSize="14"
209
+ fontWeight="600"
210
+ >
211
+ {}
212
+ </text>
213
+ </svg>
214
+ );
215
+ default:
216
+ return <FileCode2 className="size-4 shrink-0 text-neutral-400" />;
217
+ }
218
+ };
219
+
220
+ const getFileExtension = (path: string) => {
221
+ return path.split(".").pop()?.toLowerCase() || "";
222
+ };
223
+
224
+ const getLanguageTag = (path: string) => {
225
+ const extension = path.split(".").pop()?.toLowerCase();
226
+
227
+ switch (extension) {
228
+ case "html":
229
+ return {
230
+ name: "HTML",
231
+ color: "bg-orange-500/20 border-orange-500/30 text-orange-400",
232
+ };
233
+ case "css":
234
+ return {
235
+ name: "CSS",
236
+ color: "bg-blue-500/20 border-blue-500/30 text-blue-400",
237
+ };
238
+ case "js":
239
+ return {
240
+ name: "JS",
241
+ color: "bg-yellow-500/20 border-yellow-500/30 text-yellow-400",
242
+ };
243
+ case "json":
244
+ return {
245
+ name: "JSON",
246
+ color: "bg-yellow-500/20 border-yellow-500/30 text-yellow-400",
247
+ };
248
+ default:
249
+ return {
250
+ name: extension?.toUpperCase() || "FILE",
251
+ color: "bg-neutral-500/20 border-neutral-500/30 text-neutral-400",
252
+ };
253
+ }
254
+ };
255
+
256
+ const currentPageData = pages.find((p) => p.path === currentPage);
257
+
258
+ const renderFileTree = (nodes: FileNode[], depth = 0) => {
259
+ return nodes.map((node) => {
260
+ if (node.type === "folder") {
261
+ const isExpanded = expandedFolders.has(node.path);
262
+ return (
263
+ <div key={node.path}>
264
+ <div
265
+ className="flex items-center gap-2 px-3 py-1 cursor-pointer text-[13px] group transition-colors hover:bg-neutral-800"
266
+ style={{ paddingLeft: `${depth * 12 + 12}px` }}
267
+ onClick={() => toggleFolder(node.path)}
268
+ >
269
+ {isExpanded ? (
270
+ <ChevronDown className="size-3.5 text-neutral-400 shrink-0" />
271
+ ) : (
272
+ <ChevronRight className="size-3.5 text-neutral-400 shrink-0" />
273
+ )}
274
+ <Folder className="size-4 text-blue-400 shrink-0" />
275
+ <span className="text-neutral-300 truncate flex-1 font-normal">
276
+ {node.name}
277
+ </span>
278
+ <span className="text-[10px] text-neutral-500">
279
+ {node.children?.length || 0}
280
+ </span>
281
+ </div>
282
+ {isExpanded &&
283
+ node.children &&
284
+ renderFileTree(node.children, depth + 1)}
285
+ </div>
286
+ );
287
+ } else {
288
+ const isActive = currentPage === node.page?.path;
289
+ return (
290
+ <div
291
+ key={node.path}
292
+ className={classNames(
293
+ "flex items-center gap-2.5 px-3 py-1 cursor-pointer text-[13px] group transition-colors relative",
294
+ {
295
+ "bg-neutral-700 text-white": isActive,
296
+ "text-neutral-300 hover:bg-neutral-800": !isActive,
297
+ }
298
+ )}
299
+ style={{ paddingLeft: `${depth * 12 + 12 + 16}px` }}
300
+ onClick={() => {
301
+ if (node.page) {
302
+ setCurrentPage(node.page.path);
303
+ setOpen(false);
304
+ }
305
+ }}
306
+ >
307
+ <div className="w-4 flex justify-center shrink-0">
308
+ {getFileIcon(node.name)}
309
+ </div>
310
+
311
+ <span className="truncate flex-1 font-normal">{node.name}</span>
312
+
313
+ <span
314
+ className={classNames(
315
+ "text-[10px] px-1.5 py-0.5 rounded uppercase font-semibold transition-opacity shrink-0",
316
+ isActive
317
+ ? `opacity-100 ${getLanguageTag(node.name).color}`
318
+ : "opacity-0 group-hover:opacity-100 bg-white/5 text-neutral-500"
319
+ )}
320
+ >
321
+ {getFileExtension(node.name)}
322
+ </span>
323
+
324
+ {isActive && (
325
+ <div className="absolute left-0 top-0 bottom-0 w-[2px] bg-blue-500" />
326
+ )}
327
+ </div>
328
+ );
329
+ }
330
+ });
331
+ };
332
+
333
+ return (
334
+ <div>
335
+ {/* VS Code-style Tab Bar */}
336
+ <div className="w-full flex items-center bg-neutral-900 min-h-[35px] border-b border-neutral-800">
337
+ <div className="flex items-stretch overflow-x-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-neutral-700">
338
+ {currentPageData && (
339
+ <div className="flex items-center gap-2 px-4 py-2.5 bg-neutral-900 border-r border-neutral-800 text-sm min-w-0 relative group">
340
+ <div className="flex items-center gap-2 flex-1 min-w-0">
341
+ {getFileIcon(currentPageData.path)}
342
+ <span className="text-neutral-300 truncate font-normal text-[13px]">
343
+ {currentPageData.path}
344
+ </span>
345
+ <span
346
+ className={classNames(
347
+ "text-[9px] px-1.5 py-0.5 rounded border backdrop-blur-sm font-semibold uppercase tracking-wide",
348
+ getLanguageTag(currentPageData.path).color
349
+ )}
350
+ >
351
+ {getLanguageTag(currentPageData.path).name}
352
+ </span>
353
+ </div>
354
+ </div>
355
+ )}
356
+ </div>
357
+
358
+ {/* Open Explorer Button */}
359
+ <TooltipProvider>
360
+ <Tooltip>
361
+ <Sheet open={open} onOpenChange={setOpen} modal={false}>
362
+ <TooltipTrigger asChild>
363
+ <SheetTrigger asChild>
364
+ <Button
365
+ size="sm"
366
+ variant="ghost"
367
+ disabled={pages.length === 0 || globalEditorLoading}
368
+ className="ml-auto mr-2 text-neutral-300 hover:text-white hover:bg-neutral-800 h-7 text-[13px] font-normal gap-1.5"
369
+ >
370
+ <FolderOpen className="size-3.5" />
371
+ <span className="hidden sm:inline">Files</span>
372
+ <span className="text-[11px] px-1.5 py-0.5 rounded bg-neutral-800 text-neutral-500 font-semibold">
373
+ {pages.length}
374
+ </span>
375
+ </Button>
376
+ </SheetTrigger>
377
+ </TooltipTrigger>
378
+ <TooltipContent
379
+ side="bottom"
380
+ className="bg-neutral-800 border-neutral-700 text-neutral-300 text-xs"
381
+ >
382
+ <p>
383
+ Open File Explorer ({pages.length}{" "}
384
+ {pages.length === 1 ? "file" : "files"})
385
+ </p>
386
+ </TooltipContent>
387
+
388
+ <SheetContent
389
+ side="left"
390
+ className="w-[320px] bg-neutral-900 border-neutral-800 p-0"
391
+ >
392
+ <SheetHeader className="px-5 py-2.5 border-b border-neutral-800 space-y-0">
393
+ <SheetTitle className="flex items-center gap-2">
394
+ <FolderOpen className="size-4 text-neutral-300" />
395
+ <span className="text-[11px] uppercase tracking-wider text-neutral-300 font-semibold">
396
+ Explorer
397
+ </span>
398
+ </SheetTitle>
399
+ </SheetHeader>
400
+
401
+ <div className="px-3 py-3 border-b border-neutral-800">
402
+ <div className="flex items-center gap-2 px-2 py-1">
403
+ <svg
404
+ className="size-4 text-neutral-300"
405
+ fill="currentColor"
406
+ viewBox="0 0 16 16"
407
+ >
408
+ <path d="M1.5 1h11l2 2v10l-2 2h-11l-2-2V3l2-2zm0 1l-1 1v10l1 1h11l1-1V3l-1-1h-11z" />
409
+ <path d="M7.5 4.5v3h-3v1h3v3h1v-3h3v-1h-3v-3h-1z" />
410
+ </svg>
411
+ <span className="text-[13px] text-neutral-300 font-normal">
412
+ {project?.space_id || "No space selected"}
413
+ </span>
414
+ <span className="ml-auto text-[11px] text-neutral-500">
415
+ {pages.length || 0}
416
+ </span>
417
+ </div>
418
+ </div>
419
+
420
+ <div
421
+ className="py-1 overflow-y-auto"
422
+ style={{ height: "calc(100vh - 160px)" }}
423
+ >
424
+ {fileTree.children && renderFileTree(fileTree.children)}
425
+ </div>
426
+
427
+ <div className="absolute bottom-0 left-0 right-0 px-5 py-3 border-t border-neutral-800 bg-neutral-900">
428
+ <div className="grid grid-cols-2 gap-2 text-[11px]">
429
+ <div className="flex items-center gap-2 text-neutral-500">
430
+ <div className="size-2 rounded-full bg-orange-600" />
431
+ <span>
432
+ HTML:{" "}
433
+ {pages.filter((p) => p.path.endsWith(".html")).length}
434
+ </span>
435
+ </div>
436
+ <div className="flex items-center gap-2 text-neutral-500">
437
+ <div className="size-2 rounded-full bg-blue-600" />
438
+ <span>
439
+ CSS:{" "}
440
+ {pages.filter((p) => p.path.endsWith(".css")).length}
441
+ </span>
442
+ </div>
443
+ <div className="flex items-center gap-2 text-neutral-500">
444
+ <div className="size-2 rounded-full bg-yellow-400" />
445
+ <span>
446
+ JS: {pages.filter((p) => p.path.endsWith(".js")).length}
447
+ </span>
448
+ </div>
449
+ </div>
450
+ </div>
451
+ </SheetContent>
452
+ </Sheet>
453
+ </Tooltip>
454
+ </TooltipProvider>
455
+ </div>
456
+ </div>
457
+ );
458
+ }
components/editor/header/index.tsx CHANGED
@@ -1,4 +1,11 @@
1
- import { ArrowRight, HelpCircle, RefreshCcw, Lock } from "lucide-react";
 
 
 
 
 
 
 
2
  import Image from "next/image";
3
  import Link from "next/link";
4
 
@@ -82,15 +89,24 @@ export function Header() {
82
  <div className="flex items-center gap-2">
83
  {project?.space_id && (
84
  <Link
85
- href={`https://huggingface.co/spaces/${project.space_id}`}
 
 
 
 
 
 
 
86
  target="_blank"
87
  >
88
  <Button
89
  size="xs"
90
  variant="bordered"
91
- className="flex items-center gap-1 justify-center border-gray-200/20 bg-gray-200/10 text-gray-200 max-lg:hidden"
92
  >
 
93
  See Live Preview
 
94
  </Button>
95
  </Link>
96
  )}
 
1
+ import {
2
+ ArrowRight,
3
+ HelpCircle,
4
+ RefreshCcw,
5
+ Lock,
6
+ Eye,
7
+ Sparkles,
8
+ } from "lucide-react";
9
  import Image from "next/image";
10
  import Link from "next/link";
11
 
 
89
  <div className="flex items-center gap-2">
90
  {project?.space_id && (
91
  <Link
92
+ href={
93
+ project?.private
94
+ ? `https://huggingface.co/spaces/${project.space_id}`
95
+ : `https://${project.space_id.replaceAll(
96
+ "/",
97
+ "-"
98
+ )}.static.hf.space`
99
+ }
100
  target="_blank"
101
  >
102
  <Button
103
  size="xs"
104
  variant="bordered"
105
+ className="flex items-center gap-1.5 justify-center bg-gradient-to-r from-emerald-500/20 to-cyan-500/20 hover:from-emerald-500/30 hover:to-cyan-500/30 border-emerald-500/30 text-emerald-400 hover:text-emerald-300 backdrop-blur-sm shadow-lg hover:shadow-emerald-500/20 transition-all duration-300 max-lg:hidden font-medium"
106
  >
107
+ <Eye className="size-3.5" />
108
  See Live Preview
109
+ <Sparkles className="size-3" />
110
  </Button>
111
  </Link>
112
  )}
components/editor/index.tsx CHANGED
@@ -10,9 +10,8 @@ import Editor from "@monaco-editor/react";
10
  import { useEditor } from "@/hooks/useEditor";
11
  import { Header } from "@/components/editor/header";
12
  import { useAi } from "@/hooks/useAi";
13
- import { defaultHTML } from "@/lib/consts";
14
 
15
- import { ListPages } from "./pages";
16
  import { AskAi } from "./ask-ai";
17
  import { Preview } from "./preview";
18
  import { SaveChangesPopup } from "./save-changes-popup";
@@ -37,6 +36,7 @@ export const AppEditor = ({
37
  currentCommit,
38
  hasUnsavedChanges,
39
  saveChanges,
 
40
  pages,
41
  } = useEditor(namespace, repoId);
42
  const { isAiWorking } = useAi();
@@ -63,6 +63,22 @@ export const AppEditor = ({
63
  }
64
  }, [hasUnsavedChanges, isAiWorking]);
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  return (
67
  <section className="h-screen w-full bg-neutral-950 flex flex-col">
68
  <Header />
@@ -77,16 +93,16 @@ export const AppEditor = ({
77
  }
78
  )}
79
  >
80
- <ListPages />
81
  <CopyIcon
82
  className="size-4 absolute top-14 right-5 text-neutral-500 hover:text-neutral-300 z-2 cursor-pointer"
83
  onClick={() => {
84
  copyToClipboard(currentPageData.html);
85
- toast.success("HTML copied to clipboard!");
86
  }}
87
  />
88
  <Editor
89
- defaultLanguage="html"
90
  theme="vs-dark"
91
  loading={<Loading overlay={false} />}
92
  className="h-full absolute left-0 top-0 lg:min-w-[600px]"
@@ -99,13 +115,16 @@ export const AppEditor = ({
99
  horizontal: "hidden",
100
  },
101
  wordWrap: "on",
102
- readOnly: !!isAiWorking || !!currentCommit,
103
  readOnlyMessage: {
104
- value: currentCommit
 
 
105
  ? "You can't edit the code, as this is an old version of the project."
106
  : "Wait for DeepSite to finish working...",
107
  isTrusted: true,
108
  },
 
109
  }}
110
  value={currentPageData.html}
111
  onChange={(value) => {
 
10
  import { useEditor } from "@/hooks/useEditor";
11
  import { Header } from "@/components/editor/header";
12
  import { useAi } from "@/hooks/useAi";
 
13
 
14
+ import { FileBrowser } from "./file-browser";
15
  import { AskAi } from "./ask-ai";
16
  import { Preview } from "./preview";
17
  import { SaveChangesPopup } from "./save-changes-popup";
 
36
  currentCommit,
37
  hasUnsavedChanges,
38
  saveChanges,
39
+ globalEditorLoading,
40
  pages,
41
  } = useEditor(namespace, repoId);
42
  const { isAiWorking } = useAi();
 
63
  }
64
  }, [hasUnsavedChanges, isAiWorking]);
65
 
66
+ // Determine the language based on file extension
67
+ const editorLanguage = useMemo(() => {
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
+
74
+ // Determine the copy message based on file type
75
+ const copyMessage = useMemo(() => {
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 />
 
93
  }
94
  )}
95
  >
96
+ <FileBrowser />
97
  <CopyIcon
98
  className="size-4 absolute top-14 right-5 text-neutral-500 hover:text-neutral-300 z-2 cursor-pointer"
99
  onClick={() => {
100
  copyToClipboard(currentPageData.html);
101
+ toast.success(copyMessage);
102
  }}
103
  />
104
  <Editor
105
+ language={editorLanguage}
106
  theme="vs-dark"
107
  loading={<Loading overlay={false} />}
108
  className="h-full absolute left-0 top-0 lg:min-w-[600px]"
 
115
  horizontal: "hidden",
116
  },
117
  wordWrap: "on",
118
+ readOnly: !!isAiWorking || !!currentCommit || globalEditorLoading,
119
  readOnlyMessage: {
120
+ value: globalEditorLoading
121
+ ? "Wait for DeepSite loading your project..."
122
+ : currentCommit
123
  ? "You can't edit the code, as this is an old version of the project."
124
  : "Wait for DeepSite to finish working...",
125
  isTrusted: true,
126
  },
127
+ cursorBlinking: "smooth",
128
  }}
129
  value={currentPageData.html}
130
  onChange={(value) => {
components/editor/preview/index.tsx CHANGED
@@ -1,6 +1,6 @@
1
  "use client";
2
 
3
- import { useRef, useState, useEffect } from "react";
4
  import { useUpdateEffect } from "react-use";
5
  import classNames from "classnames";
6
 
@@ -28,7 +28,8 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
28
  pages,
29
  setPages,
30
  setCurrentPage,
31
- isSameHtml,
 
32
  } = useEditor();
33
  const {
34
  isEditableModeEnabled,
@@ -48,70 +49,213 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
48
  const [throttledHtml, setThrottledHtml] = useState<string>("");
49
  const lastUpdateTimeRef = useRef<number>(0);
50
 
51
- // For new projects, throttle HTML updates to every 3 seconds
52
  useEffect(() => {
53
- if (isNew && currentPageData?.html) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  const now = Date.now();
55
  const timeSinceLastUpdate = now - lastUpdateTimeRef.current;
56
 
57
- // If this is the first update or 3 seconds have passed, update immediately
58
  if (lastUpdateTimeRef.current === 0 || timeSinceLastUpdate >= 3000) {
59
- setThrottledHtml(currentPageData.html);
 
60
  lastUpdateTimeRef.current = now;
61
  } else {
62
- // Otherwise, schedule an update for when 3 seconds will have passed
63
  const timeUntilNextUpdate = 3000 - timeSinceLastUpdate;
64
  const timer = setTimeout(() => {
65
- setThrottledHtml(currentPageData.html);
 
66
  lastUpdateTimeRef.current = Date.now();
67
  }, timeUntilNextUpdate);
68
  return () => clearTimeout(timer);
69
  }
70
  }
71
- }, [isNew, currentPageData?.html]);
72
 
73
  useEffect(() => {
74
- if (!isAiWorking && !globalAiLoading && currentPageData?.html) {
75
- setStableHtml(currentPageData.html);
 
76
  }
77
- }, [isAiWorking, globalAiLoading, currentPageData?.html]);
 
 
 
 
 
 
78
 
79
  useEffect(() => {
80
  if (
81
- currentPageData?.html &&
82
  !stableHtml &&
83
  !isAiWorking &&
84
  !globalAiLoading
85
  ) {
86
- setStableHtml(currentPageData.html);
 
87
  }
88
- }, [currentPageData?.html, stableHtml, isAiWorking, globalAiLoading]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
- useUpdateEffect(() => {
 
 
 
 
 
 
 
 
91
  const cleanupListeners = () => {
92
  if (iframeRef?.current?.contentDocument) {
93
  const iframeDocument = iframeRef.current.contentDocument;
 
 
 
 
 
94
  iframeDocument.removeEventListener("mouseover", handleMouseOver);
95
  iframeDocument.removeEventListener("mouseout", handleMouseOut);
96
  iframeDocument.removeEventListener("click", handleClick);
97
  }
98
  };
99
 
100
- if (iframeRef?.current) {
101
- const iframeDocument = iframeRef.current.contentDocument;
102
- if (iframeDocument) {
103
  cleanupListeners();
104
-
105
- if (isEditableModeEnabled) {
106
- iframeDocument.addEventListener("mouseover", handleMouseOver);
107
- iframeDocument.addEventListener("mouseout", handleMouseOut);
108
- iframeDocument.addEventListener("click", handleClick);
109
- }
110
  }
111
- }
112
 
113
- return cleanupListeners;
114
- }, [iframeRef, isEditableModeEnabled]);
 
 
 
115
 
116
  const promoteVersion = async () => {
117
  setIsPromotingVersion(true);
@@ -124,6 +268,7 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
124
  setCurrentCommit(null);
125
  setPages(res.data.pages);
126
  setCurrentPage(res.data.pages[0].path);
 
127
  toast.success("Version promoted successfully");
128
  }
129
  })
@@ -176,6 +321,26 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
176
  const iframeDocument = iframeRef.current.contentDocument;
177
  if (iframeDocument) {
178
  const targetElement = event.target as HTMLElement;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  if (targetElement !== iframeDocument.body) {
180
  setSelectedElement(targetElement);
181
  }
@@ -187,38 +352,85 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
187
  if (iframeRef?.current) {
188
  const iframeDocument = iframeRef.current.contentDocument;
189
  if (iframeDocument) {
 
 
 
190
  const findClosestAnchor = (
191
  element: HTMLElement
192
  ): HTMLAnchorElement | null => {
193
- let current = element;
194
  while (current && current !== iframeDocument.body) {
195
  if (current.tagName === "A") {
196
  return current as HTMLAnchorElement;
197
  }
198
- current = current.parentElement as HTMLElement;
 
 
 
 
 
 
 
199
  }
200
  return null;
201
  };
202
 
203
- const anchorElement = findClosestAnchor(event.target as HTMLElement);
204
  if (anchorElement) {
205
  let href = anchorElement.getAttribute("href");
206
  if (href) {
207
  event.stopPropagation();
208
  event.preventDefault();
209
 
210
- if (href.includes("#") && !href.includes(".html")) {
211
- const targetElement = iframeDocument.querySelector(href);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  if (targetElement) {
213
  targetElement.scrollIntoView({ behavior: "smooth" });
214
  }
215
  return;
216
  }
217
 
218
- href = href.split(".html")[0] + ".html";
219
- const isPageExist = pages.some((page) => page.path === href);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  if (isPageExist) {
221
- setCurrentPage(href);
222
  }
223
  }
224
  }
@@ -244,6 +456,7 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
244
  "[mask-image:radial-gradient(900px_circle_at_center,white,transparent)] opacity-40"
245
  )}
246
  />
 
247
  {!isAiWorking && hoveredElement && isEditableModeEnabled && (
248
  <div
249
  className="cursor-pointer absolute bg-sky-500/10 border-[2px] border-dashed border-sky-500 rounded-r-lg rounded-b-lg p-3 z-10 pointer-events-none"
@@ -306,20 +519,12 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
306
  }
307
  );
308
  }
309
- // add event listener to all links in the iframe to handle navigation
310
- if (iframeRef?.current?.contentWindow?.document) {
311
- const links =
312
- iframeRef.current.contentWindow.document.querySelectorAll(
313
- "a"
314
- );
315
- links.forEach((link) => {
316
- link.addEventListener("click", handleCustomNavigation);
317
- });
318
- }
319
  }
320
  : undefined
321
  }
322
- sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox"
323
  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"
324
  />
325
  {!isNew && (
 
1
  "use client";
2
 
3
+ import { useRef, useState, useEffect, useCallback, useMemo } from "react";
4
  import { useUpdateEffect } from "react-use";
5
  import classNames from "classnames";
6
 
 
28
  pages,
29
  setPages,
30
  setCurrentPage,
31
+ previewPage,
32
+ setPreviewPage,
33
  } = useEditor();
34
  const {
35
  isEditableModeEnabled,
 
49
  const [throttledHtml, setThrottledHtml] = useState<string>("");
50
  const lastUpdateTimeRef = useRef<number>(0);
51
 
 
52
  useEffect(() => {
53
+ if (!previewPage && pages.length > 0) {
54
+ const indexPage = pages.find(
55
+ (p) => p.path === "index.html" || p.path === "index" || p.path === "/"
56
+ );
57
+ const firstHtmlPage = pages.find((p) => p.path.endsWith(".html"));
58
+ setPreviewPage(indexPage?.path || firstHtmlPage?.path || "index.html");
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
88
+ .map(
89
+ (file) =>
90
+ `<style data-injected-from="${file.path}">\n${file.html}\n</style>`
91
+ )
92
+ .join("\n");
93
+
94
+ if (modifiedHtml.includes("</head>")) {
95
+ modifiedHtml = modifiedHtml.replace(
96
+ "</head>",
97
+ `${allCssContent}\n</head>`
98
+ );
99
+ } else if (modifiedHtml.includes("<head>")) {
100
+ modifiedHtml = modifiedHtml.replace(
101
+ "<head>",
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(
113
+ new RegExp(
114
+ `<link\\s+[^>]*href=["'][\\.\/]*${escapedPath}["'][^>]*>`,
115
+ "gi"
116
+ ),
117
+ ""
118
+ );
119
+ });
120
+ }
121
+
122
+ // Inject all JS files
123
+ if (jsFiles.length > 0) {
124
+ const allJsContent = jsFiles
125
+ .map(
126
+ (file) =>
127
+ `<script data-injected-from="${file.path}">\n${file.html}\n</script>`
128
+ )
129
+ .join("\n");
130
+
131
+ if (modifiedHtml.includes("</body>")) {
132
+ modifiedHtml = modifiedHtml.replace(
133
+ "</body>",
134
+ `${allJsContent}\n</body>`
135
+ );
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(
147
+ new RegExp(
148
+ `<script\\s+[^>]*src=["'][\\.\/]*${escapedPath}["'][^>]*><\\/script>`,
149
+ "gi"
150
+ ),
151
+ ""
152
+ );
153
+ });
154
+ }
155
+
156
+ return modifiedHtml;
157
+ },
158
+ [pages, previewPageData?.path]
159
+ );
160
+
161
+ useEffect(() => {
162
+ if (isNew && previewPageData?.html) {
163
  const now = Date.now();
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
+ }, [
188
+ isAiWorking,
189
+ globalAiLoading,
190
+ previewPageData?.html,
191
+ injectAssetsIntoHtml,
192
+ previewPage,
193
+ ]);
194
 
195
  useEffect(() => {
196
  if (
197
+ previewPageData?.html &&
198
  !stableHtml &&
199
  !isAiWorking &&
200
  !globalAiLoading
201
  ) {
202
+ const processedHtml = injectAssetsIntoHtml(previewPageData.html);
203
+ setStableHtml(processedHtml);
204
  }
205
+ }, [
206
+ previewPageData?.html,
207
+ stableHtml,
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,
221
+ true
222
+ );
223
 
224
+ if (isEditableModeEnabled) {
225
+ iframeDocument.addEventListener("mouseover", handleMouseOver);
226
+ iframeDocument.addEventListener("mouseout", handleMouseOut);
227
+ iframeDocument.addEventListener("click", handleClick);
228
+ }
229
+ }
230
+ };
231
+
232
+ useEffect(() => {
233
  const cleanupListeners = () => {
234
  if (iframeRef?.current?.contentDocument) {
235
  const iframeDocument = iframeRef.current.contentDocument;
236
+ iframeDocument.removeEventListener(
237
+ "click",
238
+ handleCustomNavigation as any,
239
+ true
240
+ );
241
  iframeDocument.removeEventListener("mouseover", handleMouseOver);
242
  iframeDocument.removeEventListener("mouseout", handleMouseOut);
243
  iframeDocument.removeEventListener("click", handleClick);
244
  }
245
  };
246
 
247
+ const timer = setTimeout(() => {
248
+ if (iframeRef?.current?.contentDocument) {
 
249
  cleanupListeners();
250
+ setupIframeListeners();
 
 
 
 
 
251
  }
252
+ }, 50);
253
 
254
+ return () => {
255
+ clearTimeout(timer);
256
+ cleanupListeners();
257
+ };
258
+ }, [isEditableModeEnabled, stableHtml, throttledHtml, previewPage]);
259
 
260
  const promoteVersion = async () => {
261
  setIsPromotingVersion(true);
 
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
  }
274
  })
 
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
+ };
337
+
338
+ const anchorElement = findClosestAnchor(targetElement);
339
+
340
+ if (anchorElement) {
341
+ return;
342
+ }
343
+
344
  if (targetElement !== iframeDocument.body) {
345
  setSelectedElement(targetElement);
346
  }
 
352
  if (iframeRef?.current) {
353
  const iframeDocument = iframeRef.current.contentDocument;
354
  if (iframeDocument) {
355
+ const path = event.composedPath();
356
+ const actualTarget = path[0] as HTMLElement;
357
+
358
  const findClosestAnchor = (
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
+ }
374
  }
375
  return null;
376
  };
377
 
378
+ const anchorElement = findClosestAnchor(actualTarget);
379
  if (anchorElement) {
380
  let href = anchorElement.getAttribute("href");
381
  if (href) {
382
  event.stopPropagation();
383
  event.preventDefault();
384
 
385
+ if (href.startsWith("#")) {
386
+ let targetElement = iframeDocument.querySelector(href);
387
+
388
+ if (!targetElement) {
389
+ const searchInShadows = (
390
+ root: Document | ShadowRoot
391
+ ): Element | null => {
392
+ const elements = root.querySelectorAll("*");
393
+ for (const el of elements) {
394
+ if (el.shadowRoot) {
395
+ const found = el.shadowRoot.querySelector(href);
396
+ if (found) return found;
397
+ const nested = searchInShadows(el.shadowRoot);
398
+ if (nested) return nested;
399
+ }
400
+ }
401
+ return null;
402
+ };
403
+ targetElement = searchInShadows(iframeDocument);
404
+ }
405
+
406
  if (targetElement) {
407
  targetElement.scrollIntoView({ behavior: "smooth" });
408
  }
409
  return;
410
  }
411
 
412
+ let normalizedHref = href.replace(/^\.?\//, "");
413
+
414
+ if (normalizedHref === "" || normalizedHref === "/") {
415
+ normalizedHref = "index.html";
416
+ }
417
+
418
+ const hashIndex = normalizedHref.indexOf("#");
419
+ if (hashIndex !== -1) {
420
+ normalizedHref = normalizedHref.substring(0, hashIndex);
421
+ }
422
+
423
+ if (!normalizedHref.includes(".")) {
424
+ normalizedHref = normalizedHref + ".html";
425
+ }
426
+
427
+ const isPageExist = pages.some((page) => {
428
+ const pagePath = page.path.replace(/^\.?\//, "");
429
+ return pagePath === normalizedHref;
430
+ });
431
+
432
  if (isPageExist) {
433
+ setPreviewPage(normalizedHref);
434
  }
435
  }
436
  }
 
456
  "[mask-image:radial-gradient(900px_circle_at_center,white,transparent)] opacity-40"
457
  )}
458
  />
459
+ {/* Preview page indicator */}
460
  {!isAiWorking && hoveredElement && isEditableModeEnabled && (
461
  <div
462
  className="cursor-pointer absolute bg-sky-500/10 border-[2px] border-dashed border-sky-500 rounded-r-lg rounded-b-lg p-3 z-10 pointer-events-none"
 
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
  />
530
  {!isNew && (
components/ui/sheet.tsx ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as SheetPrimitive from "@radix-ui/react-dialog";
5
+ import { cva, type VariantProps } from "class-variance-authority";
6
+ import { X } from "lucide-react";
7
+
8
+ import { cn } from "@/lib/utils";
9
+
10
+ const Sheet = SheetPrimitive.Root;
11
+
12
+ const SheetTrigger = SheetPrimitive.Trigger;
13
+
14
+ const SheetClose = SheetPrimitive.Close;
15
+
16
+ const SheetPortal = SheetPrimitive.Portal;
17
+
18
+ const SheetOverlay = React.forwardRef<
19
+ React.ElementRef<typeof SheetPrimitive.Overlay>,
20
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
21
+ >(({ className, ...props }, ref) => (
22
+ <SheetPrimitive.Overlay
23
+ className={cn(
24
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
25
+ className
26
+ )}
27
+ {...props}
28
+ ref={ref}
29
+ />
30
+ ));
31
+ SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
32
+
33
+ const sheetVariants = cva(
34
+ "fixed z-50 gap-4 bg-neutral-900 p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35
+ {
36
+ variants: {
37
+ side: {
38
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39
+ bottom:
40
+ "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42
+ right:
43
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44
+ },
45
+ },
46
+ defaultVariants: {
47
+ side: "right",
48
+ },
49
+ }
50
+ );
51
+
52
+ interface SheetContentProps
53
+ extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
54
+ VariantProps<typeof sheetVariants> {}
55
+
56
+ const SheetContent = React.forwardRef<
57
+ React.ElementRef<typeof SheetPrimitive.Content>,
58
+ SheetContentProps
59
+ >(({ side = "right", className, children, ...props }, ref) => (
60
+ <SheetPortal>
61
+ <SheetOverlay />
62
+ <SheetPrimitive.Content
63
+ ref={ref}
64
+ className={cn(sheetVariants({ side }), className)}
65
+ {...props}
66
+ >
67
+ {children}
68
+ <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
69
+ <X className="h-4 w-4" />
70
+ <span className="sr-only">Close</span>
71
+ </SheetPrimitive.Close>
72
+ </SheetPrimitive.Content>
73
+ </SheetPortal>
74
+ ));
75
+ SheetContent.displayName = SheetPrimitive.Content.displayName;
76
+
77
+ const SheetHeader = ({
78
+ className,
79
+ ...props
80
+ }: React.HTMLAttributes<HTMLDivElement>) => (
81
+ <div
82
+ className={cn(
83
+ "flex flex-col space-y-2 text-center sm:text-left",
84
+ className
85
+ )}
86
+ {...props}
87
+ />
88
+ );
89
+ SheetHeader.displayName = "SheetHeader";
90
+
91
+ const SheetFooter = ({
92
+ className,
93
+ ...props
94
+ }: React.HTMLAttributes<HTMLDivElement>) => (
95
+ <div
96
+ className={cn(
97
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
98
+ className
99
+ )}
100
+ {...props}
101
+ />
102
+ );
103
+ SheetFooter.displayName = "SheetFooter";
104
+
105
+ const SheetTitle = React.forwardRef<
106
+ React.ElementRef<typeof SheetPrimitive.Title>,
107
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
108
+ >(({ className, ...props }, ref) => (
109
+ <SheetPrimitive.Title
110
+ ref={ref}
111
+ className={cn("text-lg font-semibold text-foreground", className)}
112
+ {...props}
113
+ />
114
+ ));
115
+ SheetTitle.displayName = SheetPrimitive.Title.displayName;
116
+
117
+ const SheetDescription = React.forwardRef<
118
+ React.ElementRef<typeof SheetPrimitive.Description>,
119
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
120
+ >(({ className, ...props }, ref) => (
121
+ <SheetPrimitive.Description
122
+ ref={ref}
123
+ className={cn("text-sm text-muted-foreground", className)}
124
+ {...props}
125
+ />
126
+ ));
127
+ SheetDescription.displayName = SheetPrimitive.Description.displayName;
128
+
129
+ export {
130
+ Sheet,
131
+ SheetPortal,
132
+ SheetOverlay,
133
+ SheetTrigger,
134
+ SheetClose,
135
+ SheetContent,
136
+ SheetHeader,
137
+ SheetFooter,
138
+ SheetTitle,
139
+ SheetDescription,
140
+ };
hooks/useAi.ts CHANGED
@@ -14,12 +14,13 @@ import { isTheSameHtml } from "@/lib/compare-html-diff";
14
  export const useAi = (onScrollToBottom?: () => void) => {
15
  const client = useQueryClient();
16
  const audio = useRef<HTMLAudioElement | null>(null);
17
- const { setPages, setCurrentPage, setPrompts, prompts, pages, project, setProject, commits, setCommits, setLastSavedPages, isSameHtml } = useEditor();
18
  const [controller, setController] = useState<AbortController | null>(null);
19
  const [storageProvider, setStorageProvider] = useLocalStorage("provider", "auto");
20
  const [storageModel, setStorageModel] = useLocalStorage("model", MODELS[0].value);
21
  const router = useRouter();
22
  const { projects, setProjects } = useUser();
 
23
 
24
  const { data: isAiWorking = false } = useQuery({
25
  queryKey: ["ai.isAiWorking"],
@@ -78,6 +79,18 @@ export const useAi = (onScrollToBottom?: () => void) => {
78
  client.setQueryData(["ai.selectedFiles"], newFiles)
79
  };
80
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  const { data: provider } = useQuery({
82
  queryKey: ["ai.provider"],
83
  queryFn: async () => storageProvider ?? "auto",
@@ -113,19 +126,25 @@ export const useAi = (onScrollToBottom?: () => void) => {
113
 
114
  const createNewProject = async (prompt: string, htmlPages: Page[], projectName: string | undefined, isLoggedIn?: boolean) => {
115
  if (isLoggedIn) {
116
- const response = await api.post("/me/projects", {
117
  title: projectName,
118
  pages: htmlPages,
119
  prompt,
120
- });
121
- if (response.data.ok) {
 
 
 
 
 
 
 
 
 
 
122
  setIsAiWorking(false);
123
- router.replace(`/${response.data.space.project.space_id}`);
124
- setProject(response.data.space);
125
- setProjects([...projects, response.data.space]);
126
- toast.success("AI responded successfully");
127
- if (audio.current) audio.current.play();
128
- }
129
  } else {
130
  setIsAiWorking(false);
131
  toast.success("AI responded successfully");
@@ -138,6 +157,7 @@ export const useAi = (onScrollToBottom?: () => void) => {
138
  if (!redesignMarkdown && !prompt.trim()) return;
139
 
140
  setIsAiWorking(true);
 
141
 
142
  const abortController = new AbortController();
143
  setController(abortController);
@@ -186,12 +206,11 @@ export const useAi = (onScrollToBottom?: () => void) => {
186
  }
187
  }
188
  } catch (e) {
189
- // Not valid JSON, treat as normal content
190
  }
191
  }
192
 
193
- const newPages = formatPages(contentResponse);
194
- let projectName = contentResponse.match(/<<<<<<< PROJECT_NAME_START ([\s\S]*?) >>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
195
  if (!projectName) {
196
  projectName = prompt.substring(0, 40).replace(/[^a-zA-Z0-9]/g, "-").slice(0, 40);
197
  }
@@ -226,13 +245,11 @@ export const useAi = (onScrollToBottom?: () => void) => {
226
  }
227
  }
228
  } catch (e) {
229
- // Not a complete JSON yet, continue reading
230
  }
231
  }
232
 
233
- formatPages(contentResponse);
234
 
235
- // Continue reading
236
  return read();
237
  };
238
 
@@ -266,6 +283,10 @@ export const useAi = (onScrollToBottom?: () => void) => {
266
  setController(abortController);
267
 
268
  try {
 
 
 
 
269
  const request = await fetch("/api/ask", {
270
  method: "PUT",
271
  body: JSON.stringify({
@@ -273,7 +294,7 @@ export const useAi = (onScrollToBottom?: () => void) => {
273
  provider,
274
  previousPrompts: prompts,
275
  model,
276
- pages,
277
  selectedElementHtml: selectedElement?.outerHTML,
278
  files: selectedFiles,
279
  repoId: project?.space_id,
@@ -316,16 +337,28 @@ export const useAi = (onScrollToBottom?: () => void) => {
316
  router.push(`/${res.repoId}`);
317
  setIsAiWorking(false);
318
  } else {
319
- setPages(res.pages);
320
- setLastSavedPages([...res.pages]); // Mark AI changes as saved
 
 
 
 
 
 
 
 
 
 
 
321
  setCommits([res.commit, ...commits]);
322
  setPrompts(
323
  [...prompts, prompt]
324
  )
325
  setSelectedElement(null);
326
  setSelectedFiles([]);
 
327
  setIsEditableModeEnabled(false);
328
- setIsAiWorking(false); // This was missing!
329
  }
330
 
331
  if (audio.current) audio.current.play();
@@ -348,35 +381,36 @@ export const useAi = (onScrollToBottom?: () => void) => {
348
  }
349
  };
350
 
351
- const formatPages = (content: string) => {
352
  const pages: Page[] = [];
353
- if (!content.match(/<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/)) {
354
  return pages;
355
  }
356
 
357
  const cleanedContent = content.replace(
358
- /[\s\S]*?<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/,
359
- "<<<<<<< START_TITLE $1 >>>>>>> END_TITLE"
360
  );
361
- const htmlChunks = cleanedContent.split(
362
- /<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/
363
  );
364
  const processedChunks = new Set<number>();
365
 
366
- htmlChunks.forEach((chunk, index) => {
367
  if (processedChunks.has(index) || !chunk?.trim()) {
368
  return;
369
  }
370
- const htmlContent = extractHtmlContent(htmlChunks[index + 1]);
 
371
 
372
- if (htmlContent) {
373
  const page: Page = {
374
- path: chunk.trim(),
375
- html: htmlContent,
376
  };
377
  pages.push(page);
378
 
379
- if (htmlContent.length > 200) {
380
  onScrollToBottom?.();
381
  }
382
 
@@ -386,68 +420,82 @@ export const useAi = (onScrollToBottom?: () => void) => {
386
  });
387
  if (pages.length > 0) {
388
  setPages(pages);
389
- const lastPagePath = pages[pages.length - 1]?.path;
390
- setCurrentPage(lastPagePath || "index.html");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  }
392
 
393
  return pages;
394
  };
395
 
396
- const formatPage = (content: string, currentPagePath: string) => {
397
- if (!content.match(/<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/)) {
398
- return null;
399
- }
400
-
401
- const cleanedContent = content.replace(
402
- /[\s\S]*?<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/,
403
- "<<<<<<< START_TITLE $1 >>>>>>> END_TITLE"
404
- );
405
-
406
- const htmlChunks = cleanedContent.split(
407
- /<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/
408
- )?.filter(Boolean);
409
-
410
- const pagePath = htmlChunks[0]?.trim() || "";
411
- const htmlContent = extractHtmlContent(htmlChunks[1]);
412
-
413
- if (!pagePath || !htmlContent) {
414
- return null;
415
- }
416
-
417
- const page: Page = {
418
- path: pagePath,
419
- html: htmlContent,
420
- };
421
-
422
- setPages(prevPages => {
423
- const existingPageIndex = prevPages.findIndex(p => p.path === currentPagePath || p.path === pagePath);
424
-
425
- if (existingPageIndex !== -1) {
426
- const updatedPages = [...prevPages];
427
- updatedPages[existingPageIndex] = page;
428
- return updatedPages;
429
  } else {
430
- return [...prevPages, page];
 
431
  }
432
- });
433
-
434
- setCurrentPage(pagePath);
435
-
436
- if (htmlContent.length > 200) {
437
- onScrollToBottom?.();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
  }
439
-
440
- return page;
441
- };
442
-
443
- const extractHtmlContent = (chunk: string): string => {
444
- if (!chunk) return "";
445
- const htmlMatch = chunk.trim().match(/<!DOCTYPE html>[\s\S]*/);
446
- if (!htmlMatch) return "";
447
- let htmlContent = htmlMatch[0];
448
- htmlContent = ensureCompleteHtml(htmlContent);
449
- htmlContent = htmlContent.replace(/```/g, "");
450
- return htmlContent;
451
  };
452
 
453
  const ensureCompleteHtml = (html: string): string => {
@@ -488,6 +536,8 @@ export const useAi = (onScrollToBottom?: () => void) => {
488
  setSelectedElement,
489
  selectedFiles,
490
  setSelectedFiles,
 
 
491
  isEditableModeEnabled,
492
  setIsEditableModeEnabled,
493
  globalAiLoading: isThinking || isAiWorking,
 
14
  export const useAi = (onScrollToBottom?: () => void) => {
15
  const client = useQueryClient();
16
  const audio = useRef<HTMLAudioElement | null>(null);
17
+ const { setPages, setCurrentPage, setPreviewPage, setPrompts, prompts, pages, project, setProject, commits, setCommits, setLastSavedPages, isSameHtml } = useEditor();
18
  const [controller, setController] = useState<AbortController | null>(null);
19
  const [storageProvider, setStorageProvider] = useLocalStorage("provider", "auto");
20
  const [storageModel, setStorageModel] = useLocalStorage("model", MODELS[0].value);
21
  const router = useRouter();
22
  const { projects, setProjects } = useUser();
23
+ const streamingPagesRef = useRef<Set<string>>(new Set());
24
 
25
  const { data: isAiWorking = false } = useQuery({
26
  queryKey: ["ai.isAiWorking"],
 
79
  client.setQueryData(["ai.selectedFiles"], newFiles)
80
  };
81
 
82
+ const { data: contextFile } = useQuery<string | null>({
83
+ queryKey: ["ai.contextFile"],
84
+ queryFn: async () => null,
85
+ refetchOnWindowFocus: false,
86
+ refetchOnReconnect: false,
87
+ refetchOnMount: false,
88
+ initialData: null
89
+ });
90
+ const setContextFile = (newContextFile: string | null) => {
91
+ client.setQueryData(["ai.contextFile"], newContextFile)
92
+ };
93
+
94
  const { data: provider } = useQuery({
95
  queryKey: ["ai.provider"],
96
  queryFn: async () => storageProvider ?? "auto",
 
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");
 
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();
163
  setController(abortController);
 
206
  }
207
  }
208
  } catch (e) {
 
209
  }
210
  }
211
 
212
+ const newPages = formatPages(contentResponse, false);
213
+ let projectName = contentResponse.match(/<<<<<<< PROJECT_NAME_START\s*([\s\S]*?)\s*>>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
214
  if (!projectName) {
215
  projectName = prompt.substring(0, 40).replace(/[^a-zA-Z0-9]/g, "-").slice(0, 40);
216
  }
 
245
  }
246
  }
247
  } catch (e) {
 
248
  }
249
  }
250
 
251
+ formatPages(contentResponse, true);
252
 
 
253
  return read();
254
  };
255
 
 
283
  setController(abortController);
284
 
285
  try {
286
+ const pagesToSend = contextFile
287
+ ? pages.filter(page => page.path === contextFile)
288
+ : pages;
289
+
290
  const request = await fetch("/api/ask", {
291
  method: "PUT",
292
  body: JSON.stringify({
 
294
  provider,
295
  previousPrompts: prompts,
296
  model,
297
+ pages: pagesToSend,
298
  selectedElementHtml: selectedElement?.outerHTML,
299
  files: selectedFiles,
300
  repoId: project?.space_id,
 
337
  router.push(`/${res.repoId}`);
338
  setIsAiWorking(false);
339
  } else {
340
+ const returnedPages = res.pages as Page[];
341
+ const updatedPagesMap = new Map(returnedPages.map((p: Page) => [p.path, p]));
342
+ const mergedPages: Page[] = pages.map(page =>
343
+ updatedPagesMap.has(page.path) ? updatedPagesMap.get(page.path)! : page
344
+ );
345
+ returnedPages.forEach((page: Page) => {
346
+ if (!pages.find(p => p.path === page.path)) {
347
+ mergedPages.push(page);
348
+ }
349
+ });
350
+
351
+ setPages(mergedPages);
352
+ setLastSavedPages([...mergedPages]);
353
  setCommits([res.commit, ...commits]);
354
  setPrompts(
355
  [...prompts, prompt]
356
  )
357
  setSelectedElement(null);
358
  setSelectedFiles([]);
359
+ // setContextFile(null); not needed yet, keep context for the next request.
360
  setIsEditableModeEnabled(false);
361
+ setIsAiWorking(false);
362
  }
363
 
364
  if (audio.current) audio.current.play();
 
381
  }
382
  };
383
 
384
+ const formatPages = (content: string, isStreaming: boolean = true) => {
385
  const pages: Page[] = [];
386
+ if (!content.match(/<<<<<<< NEW_FILE_START (.*?) >>>>>>> NEW_FILE_END/)) {
387
  return pages;
388
  }
389
 
390
  const cleanedContent = content.replace(
391
+ /[\s\S]*?<<<<<<< NEW_FILE_START (.*?) >>>>>>> NEW_FILE_END/,
392
+ "<<<<<<< NEW_FILE_START $1 >>>>>>> NEW_FILE_END"
393
  );
394
+ const fileChunks = cleanedContent.split(
395
+ /<<<<<<< NEW_FILE_START (.*?) >>>>>>> NEW_FILE_END/
396
  );
397
  const processedChunks = new Set<number>();
398
 
399
+ fileChunks.forEach((chunk, index) => {
400
  if (processedChunks.has(index) || !chunk?.trim()) {
401
  return;
402
  }
403
+ const filePath = chunk.trim();
404
+ const fileContent = extractFileContent(fileChunks[index + 1], filePath);
405
 
406
+ if (fileContent) {
407
  const page: Page = {
408
+ path: filePath,
409
+ html: fileContent,
410
  };
411
  pages.push(page);
412
 
413
+ if (fileContent.length > 200) {
414
  onScrollToBottom?.();
415
  }
416
 
 
420
  });
421
  if (pages.length > 0) {
422
  setPages(pages);
423
+ if (isStreaming) {
424
+ // Find new pages that haven't been shown yet (HTML, CSS, JS, etc.)
425
+ const newPages = pages.filter(p =>
426
+ !streamingPagesRef.current.has(p.path)
427
+ );
428
+
429
+ if (newPages.length > 0) {
430
+ const newPage = newPages[0];
431
+ setCurrentPage(newPage.path);
432
+ streamingPagesRef.current.add(newPage.path);
433
+
434
+ // Update preview if it's an HTML file not in components folder
435
+ if (newPage.path.endsWith('.html') && !newPage.path.includes('/components/')) {
436
+ setPreviewPage(newPage.path);
437
+ }
438
+ }
439
+ } else {
440
+ streamingPagesRef.current.clear();
441
+ const indexPage = pages.find(p => p.path === 'index.html' || p.path === 'index' || p.path === '/');
442
+ if (indexPage) {
443
+ setCurrentPage(indexPage.path);
444
+ }
445
+ }
446
  }
447
 
448
  return pages;
449
  };
450
 
451
+ const extractFileContent = (chunk: string, filePath: string): string => {
452
+ if (!chunk) return "";
453
+
454
+ // Remove backticks first
455
+ let content = chunk.trim();
456
+
457
+ // Handle different file types
458
+ if (filePath.endsWith('.css')) {
459
+ // Try to extract CSS from complete code blocks first
460
+ const cssMatch = content.match(/```css\s*([\s\S]*?)\s*```/);
461
+ if (cssMatch) {
462
+ content = cssMatch[1];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
  } else {
464
+ // Handle incomplete code blocks during streaming (remove opening fence)
465
+ content = content.replace(/^```css\s*/i, "");
466
  }
467
+ // Remove any remaining backticks
468
+ return content.replace(/```/g, "").trim();
469
+ } else if (filePath.endsWith('.js')) {
470
+ // Try to extract JavaScript from complete code blocks first
471
+ const jsMatch = content.match(/```(?:javascript|js)\s*([\s\S]*?)\s*```/);
472
+ if (jsMatch) {
473
+ content = jsMatch[1];
474
+ } else {
475
+ // Handle incomplete code blocks during streaming (remove opening fence)
476
+ content = content.replace(/^```(?:javascript|js)\s*/i, "");
477
+ }
478
+ // Remove any remaining backticks
479
+ return content.replace(/```/g, "").trim();
480
+ } else {
481
+ // Handle HTML files
482
+ const htmlMatch = content.match(/```html\s*([\s\S]*?)\s*```/);
483
+ if (htmlMatch) {
484
+ content = htmlMatch[1];
485
+ } else {
486
+ // Handle incomplete code blocks during streaming (remove opening fence)
487
+ content = content.replace(/^```html\s*/i, "");
488
+ // Try to find HTML starting with DOCTYPE
489
+ const doctypeMatch = content.match(/<!DOCTYPE html>[\s\S]*/);
490
+ if (doctypeMatch) {
491
+ content = doctypeMatch[0];
492
+ }
493
+ }
494
+
495
+ let htmlContent = content.replace(/```/g, "");
496
+ htmlContent = ensureCompleteHtml(htmlContent);
497
+ return htmlContent;
498
  }
 
 
 
 
 
 
 
 
 
 
 
 
499
  };
500
 
501
  const ensureCompleteHtml = (html: string): string => {
 
536
  setSelectedElement,
537
  selectedFiles,
538
  setSelectedFiles,
539
+ contextFile,
540
+ setContextFile,
541
  isEditableModeEnabled,
542
  setIsEditableModeEnabled,
543
  globalAiLoading: isThinking || isAiWorking,
hooks/useEditor.ts CHANGED
@@ -98,6 +98,17 @@ export const useEditor = (namespace?: string, repoId?: string) => {
98
  client.setQueryData(["editor.currentPage"], newCurrentPage);
99
  };
100
 
 
 
 
 
 
 
 
 
 
 
 
101
  const { data: prompts = [] } = useQuery({
102
  queryKey: ["editor.prompts"],
103
  queryFn: async () => [],
@@ -338,6 +349,8 @@ export const useEditor = (namespace?: string, repoId?: string) => {
338
  setDevice,
339
  currentPage,
340
  setCurrentPage,
 
 
341
  currentPageData,
342
  currentTab,
343
  setCurrentTab,
 
98
  client.setQueryData(["editor.currentPage"], newCurrentPage);
99
  };
100
 
101
+ const { data: previewPage = "" } = useQuery({
102
+ queryKey: ["editor.previewPage"],
103
+ queryFn: async () => "",
104
+ refetchOnWindowFocus: false,
105
+ refetchOnReconnect: false,
106
+ refetchOnMount: false,
107
+ });
108
+ const setPreviewPage = (newPreviewPage: string) => {
109
+ client.setQueryData(["editor.previewPage"], newPreviewPage);
110
+ };
111
+
112
  const { data: prompts = [] } = useQuery({
113
  queryKey: ["editor.prompts"],
114
  queryFn: async () => [],
 
349
  setDevice,
350
  currentPage,
351
  setCurrentPage,
352
+ previewPage,
353
+ setPreviewPage,
354
  currentPageData,
355
  currentTab,
356
  setCurrentTab,
lib/inject-badge.ts ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Injects the DeepSite badge script into HTML content before the closing </body> tag.
3
+ * If the script already exists, it ensures only one instance is present.
4
+ *
5
+ * @param html - The HTML content to inject the script into
6
+ * @returns The HTML content with the badge script injected
7
+ */
8
+ export function injectDeepSiteBadge(html: string): string {
9
+ const badgeScript = '<script src="https://deepsite.hf.co/deepsite-badge.js"></script>';
10
+
11
+ // Remove any existing badge script to avoid duplicates
12
+ const cleanedHtml = html.replace(
13
+ /<script\s+src=["']https:\/\/deepsite\.hf\.co\/deepsite-badge\.js["']\s*><\/script>\s*/gi,
14
+ ''
15
+ );
16
+
17
+ // Check if there's a closing body tag
18
+ const bodyCloseIndex = cleanedHtml.lastIndexOf('</body>');
19
+
20
+ if (bodyCloseIndex !== -1) {
21
+ // Inject the script before the closing </body> tag
22
+ return (
23
+ cleanedHtml.slice(0, bodyCloseIndex) +
24
+ badgeScript +
25
+ '\n' +
26
+ cleanedHtml.slice(bodyCloseIndex)
27
+ );
28
+ }
29
+
30
+ // If no closing body tag, append the script at the end
31
+ return cleanedHtml + '\n' + badgeScript;
32
+ }
33
+
34
+ /**
35
+ * Checks if a page path represents the main index page
36
+ *
37
+ * @param path - The page path to check
38
+ * @returns True if the path represents the main index page
39
+ */
40
+ export function isIndexPage(path: string): boolean {
41
+ const normalizedPath = path.toLowerCase();
42
+ return (
43
+ normalizedPath === '/' ||
44
+ normalizedPath === 'index' ||
45
+ normalizedPath === '/index' ||
46
+ normalizedPath === 'index.html' ||
47
+ normalizedPath === '/index.html'
48
+ );
49
+ }
50
+
lib/prompts.ts CHANGED
@@ -2,20 +2,15 @@ export const SEARCH_START = "<<<<<<< SEARCH";
2
  export const DIVIDER = "=======";
3
  export const REPLACE_END = ">>>>>>> REPLACE";
4
  export const MAX_REQUESTS_PER_IP = 4;
5
- export const TITLE_PAGE_START = "<<<<<<< START_TITLE ";
6
- export const TITLE_PAGE_END = " >>>>>>> END_TITLE";
7
- export const NEW_PAGE_START = "<<<<<<< NEW_PAGE_START ";
8
- export const NEW_PAGE_END = " >>>>>>> NEW_PAGE_END";
9
- export const UPDATE_PAGE_START = "<<<<<<< UPDATE_PAGE_START ";
10
- export const UPDATE_PAGE_END = " >>>>>>> UPDATE_PAGE_END";
11
- export const PROJECT_NAME_START = "<<<<<<< PROJECT_NAME_START ";
12
- export const PROJECT_NAME_END = " >>>>>>> PROJECT_NAME_END";
13
  export const PROMPT_FOR_REWRITE_PROMPT = "<<<<<<< PROMPT_FOR_REWRITE_PROMPT ";
14
  export const PROMPT_FOR_REWRITE_PROMPT_END = " >>>>>>> PROMPT_FOR_REWRITE_PROMPT_END";
15
 
16
- // TODO REVIEW LINK. MAYBE GO BACK TO SANDPACK.
17
- // FIX PREVIEW LINK NOT WORKING ONCE THE SITE IS DEPLOYED.
18
-
19
  export const PROMPT_FOR_IMAGE_GENERATION = `If you want to use image placeholder, http://Static.photos Usage:Format: http://static.photos/[category]/[dimensions]/[seed] where dimensions must be one of: 200x200, 320x240, 640x360, 1024x576, or 1200x630; seed can be any number (1-999+) for consistent images or omit for random; categories include: nature, office, people, technology, minimal, abstract, aerial, blurred, bokeh, gradient, monochrome, vintage, white, black, blue, red, green, yellow, cityscape, workspace, food, travel, textures, industry, indoor, outdoor, studio, finance, medical, season, holiday, event, sport, science, legal, estate, restaurant, retail, wellness, agriculture, construction, craft, cosmetic, automotive, gaming, or education.
20
  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).`
21
  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!`
@@ -28,24 +23,91 @@ If you want to use ICONS import Feather Icons (Make sure to add <script src="htt
28
  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.).
29
  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.
30
  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.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  ${PROMPT_FOR_IMAGE_GENERATION}
32
  ${PROMPT_FOR_PROJECT_NAME}
33
  No need to explain what you did. Just return the expected result. AVOID Chinese characters in the code if not asked by the user.
34
- Return the results in a \`\`\`html\`\`\` markdown. Format the results like:
35
  1. Start with ${PROJECT_NAME_START}.
36
  2. Add the name of the project, right after the start tag.
37
  3. Close the start tag with the ${PROJECT_NAME_END}.
38
  4. The name of the project should be short and concise.
39
- 5. Start with ${TITLE_PAGE_START}.
40
- 6. Add the name of the page without special character, such as spaces or punctuation, using the .html format only, right after the start tag.
41
- 7. Close the start tag with the ${TITLE_PAGE_END}.
42
- 8. Start the HTML response with the triple backticks, like \`\`\`html.
43
- 9. Insert the following html there.
44
- 10. Close with the triple backticks, like \`\`\`.
45
- 11. Retry if another pages.
 
 
46
  Example Code:
47
- ${PROJECT_NAME_START}Project Name${PROJECT_NAME_END}
48
- ${TITLE_PAGE_START}index.html${TITLE_PAGE_END}
49
  \`\`\`html
50
  <!DOCTYPE html>
51
  <html lang="en">
@@ -54,34 +116,113 @@ ${TITLE_PAGE_START}index.html${TITLE_PAGE_END}
54
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
55
  <title>Index</title>
56
  <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
 
57
  <script src="https://cdn.tailwindcss.com"></script>
58
  <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
59
- <script src="https://cdn.jsdelivr.net/npm/animejs/lib/anime.iife.min.js"></script>
60
  <script src="https://unpkg.com/feather-icons"></script>
61
  </head>
62
  <body>
 
63
  <h1>Hello World</h1>
64
- <script>const { animate } = anime;</script>
 
 
 
65
  <script>feather.replace();</script>
66
  </body>
67
  </html>
68
  \`\`\`
69
- IMPORTANT: The first file should be always named index.html.`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
- export const FOLLOW_UP_SYSTEM_PROMPT = `You are an expert UI/UX and Front-End Developer modifying an existing HTML files.
72
- The user wants to apply changes and probably add new features/pages to the website, based on their request.
73
- You MUST output ONLY the changes required using the following UPDATE_PAGE_START and SEARCH/REPLACE format. Do NOT output the entire file.
74
  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.
75
- If it's a new page, you MUST applied the following NEW_PAGE_START and UPDATE_PAGE_END format.
 
 
 
 
 
 
 
 
 
76
  ${PROMPT_FOR_IMAGE_GENERATION}
77
  Do NOT explain the changes or what you did, just return the expected results.
78
  Update Format Rules:
79
  1. Start with ${PROJECT_NAME_START}.
80
  2. Add the name of the project, right after the start tag.
81
  3. Close the start tag with the ${PROJECT_NAME_END}.
82
- 4. Start with ${UPDATE_PAGE_START}
83
- 5. Provide the name of the page you are modifying.
84
- 6. Close the start tag with the ${UPDATE_PAGE_END}.
85
  7. Start with ${SEARCH_START}
86
  8. Provide the exact lines from the current code that need to be replaced.
87
  9. Use ${DIVIDER} to separate the search block from the replacement.
@@ -94,8 +235,8 @@ Update Format Rules:
94
  Example Modifying Code:
95
  \`\`\`
96
  Some explanation...
97
- ${PROJECT_NAME_START}Project Name${PROJECT_NAME_END}
98
- ${UPDATE_PAGE_START}index.html${UPDATE_PAGE_END}
99
  ${SEARCH_START}
100
  <h1>Old Title</h1>
101
  ${DIVIDER}
@@ -104,67 +245,184 @@ ${REPLACE_END}
104
  ${SEARCH_START}
105
  </body>
106
  ${DIVIDER}
107
- <script>console.log("Added script");</script>
108
  </body>
109
  ${REPLACE_END}
110
  \`\`\`
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  Example Deleting Code:
112
  \`\`\`
113
  Removing the paragraph...
114
- ${TITLE_PAGE_START}index.html${TITLE_PAGE_END}
115
  ${SEARCH_START}
116
  <p>This paragraph will be deleted.</p>
117
  ${DIVIDER}
118
  ${REPLACE_END}
119
  \`\`\`
120
- The user can also ask to add a new page, in this case you should return the new page in the following format:
121
- 1. Start with ${NEW_PAGE_START}.
122
- 2. Add the name of the page without special character, such as spaces or punctuation, using the .html format only, right after the start tag.
123
- 3. Close the start tag with the ${NEW_PAGE_END}.
124
- 4. Start the HTML response with the triple backticks, like \`\`\`html.
125
- 5. Insert the following html there.
126
  6. Close with the triple backticks, like \`\`\`.
127
- 7. Retry if another pages.
128
- Example Code:
129
- ${NEW_PAGE_START}index.html${NEW_PAGE_END}
130
  \`\`\`html
131
  <!DOCTYPE html>
132
  <html lang="en">
133
  <head>
134
  <meta charset="UTF-8">
135
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
136
- <title>Index</title>
137
  <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
 
138
  <script src="https://cdn.tailwindcss.com"></script>
139
- <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
140
- <script src="https://cdn.jsdelivr.net/npm/animejs/lib/anime.iife.min.js"></script>
141
- <script src="https://unpkg.com/feather-icons"></script>
142
  </head>
143
  <body>
144
- <h1>Hello World</h1>
145
- <script>const { animate } = anime;</script>
146
- <script>feather.replace();</script>
 
 
 
147
  </body>
148
  </html>
149
  \`\`\`
150
- IMPORTANT: While creating a new page, UPDATE ALL THE OTHERS (using the UPDATE_PAGE_START and SEARCH/REPLACE format) pages to add or replace the link to the new page, otherwise the user will not be able to navigate to the new page. (Dont use onclick to navigate, only href)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  No need to explain what you did. Just return the expected result.`
152
 
153
  export const PROMPTS_FOR_AI = [
154
- "Create a landing page for a SaaS product, with a hero section, a features section, a pricing section, and a call to action section.",
155
- "Create a portfolio website for a designer, with a hero section, a projects section, a about section, and a contact section.",
156
- "Create a blog website for a writer, with a hero section, a blog section, a about section, and a contact section.",
157
- "Create a Tic Tac Toe game, with a game board, a history section, and a score section.",
158
- "Create a Weather App, with a search bar, a weather section, and a forecast section.",
159
- "Create a Calculator, with a calculator section, and a history section.",
160
- "Create a Todo List, with a todo list section, and a history section.",
161
- "Create a Calendar, with a calendar section, and a history section.",
162
- "Create a Music Player, with a music player section, and a history section.",
163
- "Create a Quiz App, with a quiz section, and a history section.",
164
- "Create a Pomodoro Timer, with a timer section, and a history section.",
165
- "Create a Notes App, with a notes section, and a history section.",
166
- "Create a Task Manager, with a task list section, and a history section.",
167
- "Create a Password Generator, with a password generator section, and a history section.",
168
- "Create a Currency Converter, with a currency converter section, and a history section.",
169
- "Create a Dictionary, with a dictionary section, and a history section.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  ];
 
2
  export const DIVIDER = "=======";
3
  export const REPLACE_END = ">>>>>>> REPLACE";
4
  export const MAX_REQUESTS_PER_IP = 4;
5
+ export const NEW_FILE_START = "<<<<<<< NEW_FILE_START ";
6
+ export const NEW_FILE_END = " >>>>>>> NEW_FILE_END";
7
+ export const UPDATE_FILE_START = "<<<<<<< UPDATE_FILE_START ";
8
+ export const UPDATE_FILE_END = " >>>>>>> UPDATE_FILE_END";
9
+ export const PROJECT_NAME_START = "<<<<<<< PROJECT_NAME_START";
10
+ export const PROJECT_NAME_END = ">>>>>>> PROJECT_NAME_END";
 
 
11
  export const PROMPT_FOR_REWRITE_PROMPT = "<<<<<<< PROMPT_FOR_REWRITE_PROMPT ";
12
  export const PROMPT_FOR_REWRITE_PROMPT_END = " >>>>>>> PROMPT_FOR_REWRITE_PROMPT_END";
13
 
 
 
 
14
  export const PROMPT_FOR_IMAGE_GENERATION = `If you want to use image placeholder, http://Static.photos Usage:Format: http://static.photos/[category]/[dimensions]/[seed] where dimensions must be one of: 200x200, 320x240, 640x360, 1024x576, or 1200x630; seed can be any number (1-999+) for consistent images or omit for random; categories include: nature, office, people, technology, minimal, abstract, aerial, blurred, bokeh, gradient, monochrome, vintage, white, black, blue, red, green, yellow, cityscape, workspace, food, travel, textures, industry, indoor, outdoor, studio, finance, medical, season, holiday, event, sport, science, legal, estate, restaurant, retail, wellness, agriculture, construction, craft, cosmetic, automotive, gaming, or education.
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!`
 
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.
94
+ Return the results following this format:
95
  1. Start with ${PROJECT_NAME_START}.
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}
111
  \`\`\`html
112
  <!DOCTYPE html>
113
  <html lang="en">
 
116
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
117
  <title>Index</title>
118
  <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
119
+ <link rel="stylesheet" href="style.css">
120
  <script src="https://cdn.tailwindcss.com"></script>
121
  <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
 
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.
205
+ You MUST output ONLY the changes required using the following UPDATE_FILE_START and SEARCH/REPLACE format. Do NOT output the entire file.
206
  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.
207
+ If it's a new file (HTML page, CSS, JS, or Web Component), you MUST use the NEW_FILE_START and NEW_FILE_END format.
208
+ IMPORTANT: When adding shared CSS or JavaScript code, modify the style.css or script.js files. Make sure all HTML files include <link rel="stylesheet" href="style.css"> and <script src="script.js"></script> tags.
209
+ WEB COMPONENTS: For reusable UI elements like navbars, footers, sidebars, headers, etc., create or update Native Web Components as separate files in components/ folder:
210
+ - Create each component as a separate .js file in components/ folder (e.g., components/navbar.js, components/footer.js)
211
+ - Each component file defines a class extending HTMLElement and registers it with customElements.define()
212
+ - Use Shadow DOM (attachShadow) for style encapsulation
213
+ - Use template literals for HTML/CSS content
214
+ - Include component files in HTML pages where needed: <script src="components/navbar.js"></script>
215
+ - Use custom element tags in HTML (e.g., <custom-navbar></custom-navbar>, <custom-footer></custom-footer>)
216
+ 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.
217
  ${PROMPT_FOR_IMAGE_GENERATION}
218
  Do NOT explain the changes or what you did, just return the expected results.
219
  Update Format Rules:
220
  1. Start with ${PROJECT_NAME_START}.
221
  2. Add the name of the project, right after the start tag.
222
  3. Close the start tag with the ${PROJECT_NAME_END}.
223
+ 4. Start with ${UPDATE_FILE_START}
224
+ 5. Provide the name of the file you are modifying (index.html, style.css, script.js, etc.).
225
+ 6. Close the start tag with the ${UPDATE_FILE_END}.
226
  7. Start with ${SEARCH_START}
227
  8. Provide the exact lines from the current code that need to be replaced.
228
  9. Use ${DIVIDER} to separate the search block from the replacement.
 
235
  Example Modifying Code:
236
  \`\`\`
237
  Some explanation...
238
+ ${PROJECT_NAME_START} Project Name ${PROJECT_NAME_END}
239
+ ${UPDATE_FILE_START}index.html${UPDATE_FILE_END}
240
  ${SEARCH_START}
241
  <h1>Old Title</h1>
242
  ${DIVIDER}
 
245
  ${SEARCH_START}
246
  </body>
247
  ${DIVIDER}
248
+ <script src="script.js"></script>
249
  </body>
250
  ${REPLACE_END}
251
  \`\`\`
252
+ Example Updating CSS:
253
+ \`\`\`
254
+ ${UPDATE_FILE_START}style.css${UPDATE_FILE_END}
255
+ ${SEARCH_START}
256
+ body {
257
+ background: white;
258
+ }
259
+ ${DIVIDER}
260
+ body {
261
+ background: linear-gradient(to right, #667eea, #764ba2);
262
+ }
263
+ ${REPLACE_END}
264
+ \`\`\`
265
  Example Deleting Code:
266
  \`\`\`
267
  Removing the paragraph...
268
+ ${UPDATE_FILE_START}index.html${UPDATE_FILE_END}
269
  ${SEARCH_START}
270
  <p>This paragraph will be deleted.</p>
271
  ${DIVIDER}
272
  ${REPLACE_END}
273
  \`\`\`
274
+ The user can also ask to add a new file (HTML page, CSS, JS, or Web Component), in this case you should return the new file in the following format:
275
+ 1. Start with ${NEW_FILE_START}.
276
+ 2. Add the name of the file (e.g., about.html, style.css, script.js, components/navbar.html), right after the start tag.
277
+ 3. Close the start tag with the ${NEW_FILE_END}.
278
+ 4. Start the file content with the triple backticks and appropriate language marker (\`\`\`html, \`\`\`css, or \`\`\`javascript).
279
+ 5. Insert the file content there.
280
  6. Close with the triple backticks, like \`\`\`.
281
+ 7. Repeat for additional files.
282
+ Example Creating New HTML Page:
283
+ ${NEW_FILE_START}about.html${NEW_FILE_END}
284
  \`\`\`html
285
  <!DOCTYPE html>
286
  <html lang="en">
287
  <head>
288
  <meta charset="UTF-8">
289
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
290
+ <title>About</title>
291
  <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
292
+ <link rel="stylesheet" href="style.css">
293
  <script src="https://cdn.tailwindcss.com"></script>
 
 
 
294
  </head>
295
  <body>
296
+ <custom-navbar></custom-navbar>
297
+ <h1>About Page</h1>
298
+ <custom-footer></custom-footer>
299
+ <script src="components/navbar.js"></script>
300
+ <script src="components/footer.js"></script>
301
+ <script src="script.js"></script>
302
  </body>
303
  </html>
304
  \`\`\`
305
+ Example Creating New Web Component:
306
+ ${NEW_FILE_START}components/sidebar.js${NEW_FILE_END}
307
+ \`\`\`javascript
308
+ class CustomSidebar extends HTMLElement {
309
+ connectedCallback() {
310
+ this.attachShadow({ mode: 'open' });
311
+ this.shadowRoot.innerHTML = \`
312
+ <style>
313
+ aside {
314
+ width: 250px;
315
+ background: #f7fafc;
316
+ padding: 1rem;
317
+ height: 100vh;
318
+ position: fixed;
319
+ left: 0;
320
+ top: 0;
321
+ border-right: 1px solid #e5e7eb;
322
+ }
323
+ h3 { margin: 0 0 1rem 0; }
324
+ ul { list-style: none; padding: 0; margin: 0; }
325
+ li { margin: 0.5rem 0; }
326
+ a { color: #374151; text-decoration: none; }
327
+ a:hover { color: #667eea; }
328
+ </style>
329
+ <aside>
330
+ <h3>Sidebar</h3>
331
+ <ul>
332
+ <li><a href="/">Home</a></li>
333
+ <li><a href="/about.html">About</a></li>
334
+ </ul>
335
+ </aside>
336
+ \`;
337
+ }
338
+ }
339
+ customElements.define('custom-sidebar', CustomSidebar);
340
+ \`\`\`
341
+ Then UPDATE HTML files to include the component:
342
+ ${UPDATE_FILE_START}index.html${UPDATE_FILE_END}
343
+ ${SEARCH_START}
344
+ <script src="script.js"></script>
345
+ </body>
346
+ ${DIVIDER}
347
+ <script src="components/sidebar.js"></script>
348
+ <script src="script.js"></script>
349
+ </body>
350
+ ${REPLACE_END}
351
+ ${SEARCH_START}
352
+ <body>
353
+ <custom-navbar></custom-navbar>
354
+ ${DIVIDER}
355
+ <body>
356
+ <custom-sidebar></custom-sidebar>
357
+ <custom-navbar></custom-navbar>
358
+ ${REPLACE_END}
359
+ IMPORTANT: While creating a new HTML page, UPDATE ALL THE OTHER HTML files (using the UPDATE_FILE_START and SEARCH/REPLACE format) to add or replace the link to the new page, otherwise the user will not be able to navigate to the new page. (Don't use onclick to navigate, only href)
360
+ When creating new CSS/JS files, UPDATE ALL HTML files to include the appropriate <link> or <script> tags.
361
+ When creating new Web Components:
362
+ 1. Create a NEW FILE in components/ folder (e.g., components/sidebar.js) with the component definition
363
+ 2. UPDATE ALL HTML files that need the component to include <script src="components/componentname.js"></script> before the closing </body> tag
364
+ 3. Use the custom element tag (e.g., <custom-componentname></custom-componentname>) in HTML pages where needed
365
  No need to explain what you did. Just return the expected result.`
366
 
367
  export const PROMPTS_FOR_AI = [
368
+ // Business & SaaS
369
+ "Create a modern SaaS landing page with a hero section featuring a product demo, benefits section with icons, pricing plans comparison table, customer testimonials with photos, FAQ accordion, and a prominent call-to-action footer.",
370
+ "Create a professional startup landing page with animated hero section, problem-solution showcase, feature highlights with screenshots, team members grid, investor logos, press mentions, and email signup form.",
371
+ "Create a business consulting website with a hero banner, services we offer section with hover effects, case studies carousel, client testimonials, team profiles with LinkedIn links, blog preview, and contact form.",
372
+
373
+ // E-commerce & Retail
374
+ "Create an e-commerce product landing page with hero image carousel, product features grid, size/color selector, customer reviews with star ratings, related products section, add to cart button, and shipping information.",
375
+ "Create an online store homepage with navigation menu, banner slider, featured products grid with hover effects, category cards, special offers section, newsletter signup, and footer with social links.",
376
+ "Create a fashion brand website with a full-screen hero image, new arrivals section, shop by category grid, Instagram feed integration, brand story section, and styling lookbook gallery.",
377
+
378
+ // Food & Restaurant
379
+ "Create a restaurant website with a hero section showing signature dishes, menu with categories and prices, chef's special highlights, reservation form with date picker, location map, opening hours, and customer reviews.",
380
+ "Create a modern coffee shop website with a cozy hero image, menu board with drinks and pastries, about our story section, location finder, online ordering button, and Instagram gallery showing cafΓ© atmosphere.",
381
+ "Create a food delivery landing page with cuisine categories, featured restaurants carousel, how it works steps, delivery zones map, app download buttons, promotional offers banner, and customer testimonials.",
382
+
383
+ // Real Estate & Property
384
+ "Create a real estate agency website with property search filters (location, price, bedrooms), featured listings grid with images, virtual tour options, mortgage calculator, agent profiles, neighborhood guides, and contact form.",
385
+ "Create a luxury property showcase website with full-screen image slider, property details with floor plans, amenities icons, 360Β° virtual tour button, location highlights, similar properties section, and inquiry form.",
386
+
387
+ // Creative & Portfolio
388
+ "Create a professional portfolio website for a photographer with a masonry image gallery, project categories filter, full-screen lightbox viewer, about me section with photo, services offered, client logos, and contact form.",
389
+ "Create a creative agency portfolio with animated hero section, featured projects showcase with case studies, services we provide, team members grid, client testimonials slider, awards section, and get a quote form.",
390
+ "Create a UX/UI designer portfolio with hero section showcasing best work, projects grid with filter tags, detailed case studies with before/after, design process timeline, skills and tools, testimonials, and hire me button.",
391
+
392
+ // Personal & Blog
393
+ "Create a personal brand website with an engaging hero section, about me with professional photo, skills and expertise cards, featured blog posts, speaking engagements, social media links, and newsletter signup.",
394
+ "Create a modern blog website with featured post hero, article cards grid with thumbnails, categories sidebar, search functionality, author bio section, related posts, social sharing buttons, and comment section.",
395
+ "Create a travel blog with full-width destination photos, travel stories grid, interactive world map showing visited places, travel tips section, gear recommendations, and subscription form.",
396
+
397
+ // Health & Fitness
398
+ "Create a fitness gym website with motivational hero video, class schedule timetable, trainer profiles with specializations, membership pricing comparison, transformation gallery, facilities photos, and trial class signup form.",
399
+ "Create a yoga studio website with calming hero section, class types with descriptions, instructor bios with photos, weekly schedule calendar, pricing packages, meditation tips blog, studio location map, and booking form.",
400
+ "Create a health & wellness landing page with hero section, service offerings, nutritionist/trainer profiles, success stories before/after, health blog articles, free consultation booking, and testimonials slider.",
401
+
402
+ // Education & Learning
403
+ "Create an online course landing page with course overview, curriculum breakdown with expandable modules, instructor credentials, student testimonials with videos, pricing and enrollment options, FAQ section, and money-back guarantee badge.",
404
+ "Create a university/school website with hero carousel, academic programs grid, campus life photo gallery, upcoming events calendar, faculty directory, admissions process timeline, virtual campus tour, and application form.",
405
+ "Create a tutoring service website with subjects offered, tutor profiles with qualifications, pricing plans, scheduling calendar, student success stories, free trial lesson signup, learning resources, and parent testimonials.",
406
+
407
+ // Events & Entertainment
408
+ "Create an event conference website with hero countdown timer, speaker lineup with bios, schedule/agenda tabs, venue information with map, ticket tiers and pricing, sponsors logos grid, past event highlights, and registration form.",
409
+ "Create a music festival landing page with artist/band lineup, stage schedule, venue map, ticket options with early bird pricing, photo gallery from previous years, camping information, FAQ, and buy tickets button.",
410
+ "Create a wedding website with couple's story, event timeline, venue details with directions, RSVP form, photo gallery, gift registry links, accommodation suggestions, and message board for guests.",
411
+
412
+ // Professional Services
413
+ "Create a law firm website with practice areas grid, attorney profiles with expertise, case results/wins, legal resources blog, testimonials, office locations, consultation booking form, and trust badges.",
414
+ "Create a dental clinic website with services offered, meet the dentist section with credentials, patient testimonials, before/after smile gallery, insurance accepted, appointment booking system, emergency contact, and dental tips blog.",
415
+ "Create an architecture firm website with portfolio of completed projects with large images, services overview, design process timeline, team members, awards and recognition, sustainable design approach, and project inquiry form.",
416
+
417
+ // Technology & Apps
418
+ "Create an app landing page with hero section showing app screenshots, key features with icons, how it works steps, pricing plans, user testimonials, app store download buttons, video demo, and early access signup.",
419
+ "Create a software product page with hero demo video, features comparison table, integration logos, API documentation link, use cases with examples, security certifications, customer stories, and free trial signup.",
420
+
421
+ // Non-profit & Community
422
+ "Create a non-profit organization website with mission statement hero, our impact statistics, current campaigns, donation form with amounts, volunteer opportunities, success stories, upcoming events, and newsletter signup.",
423
+ "Create a community organization website with welcome hero, about our mission, programs and services offered, event calendar, member spotlights, resources library, donation/support options, and get involved form.",
424
+
425
+ // Misc & Utility (keeping a few interactive examples)
426
+ "Create an interactive weather dashboard with current conditions, 5-day forecast cards, hourly temperature graph, air quality index, UV index, sunrise/sunset times, and location search with autocomplete.",
427
+ "Create a modern calculator with basic operations, scientific mode toggle, calculation history log, memory functions, keyboard support, light/dark theme switch, and copy result button.",
428
  ];
public/deepsite-badge.js ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (function() {
2
+ 'use strict';
3
+
4
+ // Check if badge already exists
5
+ if (document.getElementById('deepsite-badge-wrapper')) {
6
+ return;
7
+ }
8
+
9
+ // Inject keyframes for gradient rotation
10
+ const style = document.createElement('style');
11
+ style.textContent = `
12
+ @keyframes deepsite-spin {
13
+ 0% {
14
+ transform: translate(-50%, -50%) rotate(0deg);
15
+ }
16
+ 100% {
17
+ transform: translate(-50%, -50%) rotate(360deg);
18
+ }
19
+ }
20
+
21
+ #deepsite-badge-wrapper i {
22
+ pointer-events: none;
23
+ position: absolute;
24
+ top: 0;
25
+ right: 0;
26
+ bottom: 0;
27
+ left: 0;
28
+ z-index: -1;
29
+ padding: 1.5px;
30
+ transition-property: all;
31
+ transition-timing-function: cubic-bezier(.4, 0, .2, 1);
32
+ transition-duration: .2s;
33
+ -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
34
+ mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
35
+ -webkit-mask-composite: xor;
36
+ mask-composite: exclude;
37
+ border-radius: inherit;
38
+ }
39
+
40
+ #deepsite-badge-wrapper i::before {
41
+ content: "";
42
+ position: absolute;
43
+ left: 50%;
44
+ top: 50%;
45
+ display: block;
46
+ border-radius: 9999px;
47
+ opacity: 0;
48
+ background: conic-gradient(from 0deg at 50% 50%, #ec4899, #fbbf24, #3b82f6, #ec4899);
49
+ width: calc(100% * 2);
50
+ padding-bottom: calc(100% * 2);
51
+ transform: translate(-50%, -50%);
52
+ z-index: -1;
53
+ will-change: transform;
54
+ }
55
+
56
+ #deepsite-badge-wrapper:hover i::before {
57
+ opacity: 1;
58
+ animation: deepsite-spin 3s linear infinite;
59
+ }
60
+ `;
61
+ document.head.appendChild(style);
62
+
63
+ // Create badge wrapper (like the button element)
64
+ const badgeWrapper = document.createElement('div');
65
+ badgeWrapper.id = 'deepsite-badge-wrapper';
66
+
67
+ // Create inner badge (like the span element)
68
+ const badgeInner = document.createElement('span');
69
+ badgeInner.id = 'deepsite-badge-inner';
70
+
71
+ // Create mask element (the i element)
72
+ const borderMask = document.createElement('i');
73
+
74
+ // Create link
75
+ const link = document.createElement('a');
76
+ link.href = 'https://deepsite.hf.co';
77
+ link.target = '_blank';
78
+ link.rel = 'noopener noreferrer';
79
+
80
+ // Create icon placeholder
81
+ const icon = document.createElement('img');
82
+ icon.src = 'https://deepsite.hf.co/logo.svg';
83
+ icon.alt = 'DeepSite';
84
+ icon.style.marginRight = '8px';
85
+ icon.style.width = '20px';
86
+ icon.style.height = '20px';
87
+ icon.style.filter = 'brightness(0) invert(1)';
88
+
89
+ // Create text
90
+ const text = document.createTextNode('Made with DeepSite');
91
+
92
+ // Apply styles to wrapper (like button element)
93
+ Object.assign(badgeWrapper.style, {
94
+ position: 'fixed',
95
+ bottom: '20px',
96
+ left: '20px',
97
+ zIndex: '999999',
98
+ color: '#ffffff',
99
+ borderRadius: '9999px',
100
+ background: 'rgba(0, 0, 0, 0.4)',
101
+ fontSize: '14px',
102
+ fontWeight: '500',
103
+ display: 'inline-block',
104
+ cursor: 'pointer',
105
+ padding: '1.5px',
106
+ overflow: 'hidden',
107
+ backdropFilter: 'blur(16px) saturate(180%)',
108
+ WebkitBackdropFilter: 'blur(16px) saturate(180%)',
109
+ });
110
+
111
+ // Apply styles to inner badge (like span element)
112
+ Object.assign(badgeInner.style, {
113
+ background: 'rgba(0, 0, 0, 0.6)',
114
+ padding: '10px 20px',
115
+ display: 'flex',
116
+ alignItems: 'center',
117
+ borderRadius: '9999px',
118
+ boxShadow: '0 8px 32px 0 rgba(0, 0, 0, 0.5)',
119
+ transition: 'all 0.3s ease',
120
+ border: '1px solid rgba(255, 255, 255, 0.1)'
121
+ });
122
+
123
+ // Apply styles to link
124
+ Object.assign(link.style, {
125
+ color: '#ffffff',
126
+ textDecoration: 'none',
127
+ fontWeight: '500',
128
+ display: 'flex',
129
+ alignItems: 'center',
130
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
131
+ textShadow: '0 2px 4px rgba(0, 0, 0, 0.3)'
132
+ });
133
+
134
+ // Add hover effect
135
+ badgeWrapper.addEventListener('mouseenter', function() {
136
+ badgeInner.style.background = 'rgba(0, 0, 0, 0.75)';
137
+ badgeInner.style.boxShadow = '0 8px 32px 0 rgba(0, 0, 0, 0.7)';
138
+ });
139
+
140
+ badgeWrapper.addEventListener('mouseleave', function() {
141
+ badgeInner.style.background = 'rgba(0, 0, 0, 0.6)';
142
+ badgeInner.style.boxShadow = '0 8px 32px 0 rgba(0, 0, 0, 0.5)';
143
+ });
144
+
145
+ // Append elements
146
+ link.appendChild(icon);
147
+ link.appendChild(text);
148
+ badgeInner.appendChild(link);
149
+ badgeWrapper.appendChild(badgeInner);
150
+ badgeWrapper.appendChild(borderMask);
151
+
152
+ // Wait for DOM to be ready
153
+ function init() {
154
+ if (document.body) {
155
+ document.body.appendChild(badgeWrapper);
156
+ } else {
157
+ document.addEventListener('DOMContentLoaded', function() {
158
+ document.body.appendChild(badgeWrapper);
159
+ });
160
+ }
161
+ }
162
+
163
+ // Initialize
164
+ if (document.readyState === 'loading') {
165
+ document.addEventListener('DOMContentLoaded', init);
166
+ } else {
167
+ init();
168
+ }
169
+ })();
170
+